decopin-cli

↑ ↓ to navigate · Enter to open · Esc to close

Conventionsstdin.tsx

stdin.tsx

A command without stdin.tsx never touches stdin. The one that does says what the input is.

A command without stdin.tsx never touches stdin. The most common CLI accident, running a command in a terminal and having it hang on input you did not know it wanted, cannot happen by construction.

// app/count/stdin.tsx
import { Stdin } from 'decopin-cli';

export default function DefineStdin() {
  return <Stdin mode="lines" required />;
}
// app/count/cmd.tsx
import { Line, type CmdProps } from 'decopin-cli';

export default function Command({ stdin }: CmdProps<'count'>) {
  // stdin is string[], because mode="lines" and required
  return <Line>{stdin.length}</Line>;
}
$ printf 'a\nb\n\nc\n' | ./dist/index.js count
4

Modes

modeWhat the command receives
textstring (the whole input). trim drops the trailing newline
linesstring[] (split on newlines)
jsonthe type you declared in children, otherwise unknown

Optional and required

Without required, a command run in a terminal receives undefined, and the type says | undefined, so code that forgets to handle it does not compile.

// app/upper/cmd.tsx
import { Line, type CmdProps } from 'decopin-cli';

export default function Command({ stdin }: CmdProps<'upper'>) {
  // stdin is string | undefined
  return <Line>{(stdin ?? 'nothing piped').toUpperCase()}</Line>;
}

With required, a run in a terminal without a pipe is a usage error (exit 2) instead of a hang. An empty pipe is still input: count </dev/null prints 0.

Structured input

mode="json" takes the same Type.* children as argv.tsx, so structured input gets the same treatment as arguments: validated before the command runs, and typed from the declaration.

// app/user/import/stdin.tsx
import { Stdin, Type } from 'decopin-cli';

export default function DefineStdin() {
  return (
    <Stdin mode="json" required>
      <Type.Array minItems={1}>
        <Type.Object>
          <Type.Field name="name" required>
            <Type.String minLength={1} />
          </Type.Field>
          <Type.Field name="admin" defaultValue={false}>
            <Type.Boolean />
          </Type.Field>
        </Type.Object>
      </Type.Array>
    </Stdin>
  );
}
// app/user/import/cmd.tsx
import { Line, Text, type CmdProps } from 'decopin-cli';

export default function Command({ stdin }: CmdProps<'user/import'>) {
  // stdin is { name: string; admin: boolean }[]
  return (
    <>
      {stdin.map((user) => (
        <Line key={user.name}>
          {user.name}
          {user.admin ? <Text color="yellow"> (admin)</Text> : null}
        </Line>
      ))}
      <Line>
        <Text dim>imported {stdin.length}</Text>
      </Line>
    </>
  );
}

Input that does not match fails before the command runs, through the same error.tsx chain as everything else, with error.kind === 'stdin'.

Visible in --help

$ ./dist/index.js count --help
Usage: decopin-cli count [options]

Count lines coming from stdin.

Stdin:
  lines            required (pipe something in)
...

An MCP tool built from a command with stdin.tsx gains a stdin argument, so an agent can pass the input the pipe would have carried.