# Development Notes

Technical details for `pi-parse-commands`.

## Architecture

The extension wraps the built-in `bash` tool:

- `createBashToolDefinition(process.cwd())` produces the original definition.
- `pi.registerTool` re-registers `bash` with the same name, delegating
  `execute` and `renderResult` to the original.
- A custom `renderCall` prints the normal `$ <command>` line and then, when the
  command contains more than one segment, adds a nested box with one line per
  command.

Re-registering a built-in tool by name replaces it; only rendering changes.
Execution, truncation, timing, and expansion are the original implementation.

## Parsing

`parseShellCommands` is a small single-pass scanner. It tracks single quotes,
double quotes, and backslash escapes, and only recognizes a separator when the
scanner is outside both quote states.

Segment separators:

- default: `&&`, `||`, `;`
- optional, enabled through the `separators` config: `|&`, `|`, `&`
- newlines (recorded without an operator label)

An explicit empty `separators` list is honored, leaving only newline
splitting. Unknown entries are dropped and duplicates are collapsed.

The scanner deliberately keeps these out of the split:

- separators inside quotes or escaped with a backslash;
- `#` comments, but only when the `#` starts a word;
- `&` that belongs to a redirection (`2>&1`, `>&2`, `&>file`).

Command substitution, here-documents, and `case` statements are not parsed as
nested constructs. Separators inside them become command boundaries. This is a
known limitation kept intentionally for a quick visual overview.

The result is a `ShellCommandSegment[]` where each segment carries the command
text and the operator that terminates it (the last segment has no operator).

## Rendering

`formatCommandBreakdown` numbers the segments, colors the number with `muted`,
appends the trailing operator with `dim`, and highlights configured executable
names. At most `MAX_BREAKDOWN_COMMANDS` (25) lines are drawn; the remainder is
summarized as `... and N more`.

`config.ts` loads the first `config.jsonc`, `config.json`, `config.yaml`, or
`config.yml` found in `~/.pi/agent/extensions/pi-parse-commands-config/` (or the
configured directory). JSONC comments/trailing commas and YAML are supported.
Levels 0 through 3 map to the theme's green, yellow, orange, and red colors.
The optional `separators` array selects which list operators split the
breakdown; it defaults to `["&&", "||", ";"]`.

`migrateCommandConfig` keeps an existing config current as new top-level options
are introduced. It feature-detects missing keys against `CONFIG_FEATURES` (no
schema-version field), inserts them before the root close brace for JSON/JSONC or
appends a block for YAML, and writes a `.bak` copy first. Editing the text in
place preserves user comments and formatting; re-serializing would discard both.
The migration is idempotent, never creates a config file, and skips malformed
files. It runs once when the extension loads.

`renderCall` reuses the container from `context.lastComponent` and clears it on
each pass so re-renders do not duplicate the command. It also mirrors the
built-in timing state (`startedAt` / `endedAt`) so the original `renderResult`
can still display the elapsed time.

The breakdown uses a padded `Text` component with
`theme.bg("customMessageBg", ...)`. Since nested background helpers reset ANSI
background state, each line restores the parent tool background with
`theme.getBgAnsi(...)`; otherwise the built-in tool renderer's right padding
would appear as a black strip.

## Testing

`test/parse-commands.test.ts` covers two layers:

1. The pure parser with a matrix of quoting, escaping, comment, redirection,
   and mixed-separator cases, including the dense real-world command.
2. The tool override: registration, preservation of execution/schema metadata,
   the breakdown box, single-command suppression, the display cap, re-render
   de-duplication, and timing state.

The coding-agent package is mocked in the tests because the extension only
needs a bash definition to wrap; its execution path is not exercised. Config
tests cover JSONC/YAML parsing, scanner edge cases (block comments, comment
markers inside strings, trailing commas), level/separator normalization, file
precedence (`jsonc` > `json` > `yaml` > `yml`), malformed-file fallback, and
default discovery through `PI_CODING_AGENT_DIR`/`HOME`; the tool rendering
tests cover configured highlighting. Migration tests cover comment preservation,
the `.bak` backup, idempotency, YAML and empty-object insertion, malformed-file
fallback, and the no-config no-op; an extension-level test verifies migration is
triggered when the extension loads against a stale config directory.

`test/package.test.ts` covers the publishable artifact: package metadata, the
`npm pack` file list, and an end-to-end install that runs the real `pi` CLI
against the extracted tarball inside a throwaway `PI_CODING_AGENT_DIR`.
`test/release-version.test.ts` covers the release workflow's version resolver
in `scripts/release-version.ts`.

```bash
npm install
npm test
npm run typecheck
npm run lint
```

### Local pi layout

For a local checkout, link the whole extension directory rather than only its
entry point:

```bash
ln -s /path/to/pi-parse-commands ~/.pi/agent/extensions/pi-parse-commands
```

Keep user configuration in the sibling directory
`~/.pi/agent/extensions/pi-parse-commands-config/`. If pi does not discover
nested extension files automatically, add `+extensions/pi-parse-commands/index.ts`
to the `extensions` list in `~/.pi/agent/settings.json`.

## Release

Releases run automatically through `.github/workflows/release.yml` on every
push to `main`. `scripts/release-version.ts` resolves the version:

- If `v<current-version>` is not tagged yet, it releases the version already in
  `package.json` (this is what makes the first release `1.0.0`).
- If that tag exists, it bumps the patch version, commits, and tags.

The workflow opens a GitHub release with generated notes; it does not publish
to npm. The bot commit carries `[skip ci]` so it does not trigger itself.

### Manual publish

Publishing to npm is manual:

```bash
npm login          # once per machine
npm test && npm run typecheck && npm run lint
npm version patch  # or minor / major
npm pack
```

Smoke-test the tarball before publishing. `test/package.test.ts` automates this,
but you can do it by hand with a throwaway config dir:

```bash
tar xzf lglen-pi-parse-commands-*.tgz
PI_CODING_AGENT_DIR=/tmp/pi-smoke pi install ./package --no-approve
```

Then publish and push the tag:

```bash
npm publish --access public
git push --follow-tags
```

The published tarball contains the extension sources (`index.ts`, `config.ts`,
`highlight.ts`), README/changelog/license, `docs/**/*.md`, and `package.json`.
Tests, workflows, scripts, and build config are excluded through the `files`
field.

## Package identity

The package is extracted from the in-tree example at
`packages/coding-agent/examples/extensions/bash-command-breakdown/` in the pi
repository and published to npm as `@lglen/pi-parse-commands` (repository
`LaishGlenberg/pi-parse-commands`). Keep the package name, README installation
commands, and changelog links aligned if either is renamed.
