Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The release and Nix flake support `x86_64-linux` only. With
[Nix](https://nixos.org/download) and flakes enabled:

```sh
nix run github:nerima-lisp/nshell/v0.6.3
nix run github:nerima-lisp/nshell/v0.6.4
```

At the prompt, enter `string upper hello`. Commands and paths colorize live, and a
Expand Down Expand Up @@ -62,16 +62,16 @@ and session settings.
## Install

```sh
nix profile install github:nerima-lisp/nshell/v0.6.3
nix profile install github:nerima-lisp/nshell/v0.6.4
```

Pin a release tag rather than following the default branch.

To install without Nix, use the `x86_64-linux` release bundle. It removes Nix store references, carries its
ELF runtime library closure, and is checked for required files and dependency
metadata. CI checks an extracted archive and exercises its executable with a
real PTY, including editing, signals, job control, history restart, and terminal
restoration. See [Getting
real PTY, including multiline editing and paste, completion with spaces,
signals, job control, resize, history restart, and terminal restoration. See [Getting
started](https://nerima-lisp.github.io/nshell/getting-started/) for the bundle
verification and installation procedure.

Expand Down
32 changes: 16 additions & 16 deletions docs/src/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ Install [Nix](https://nixos.org/download) and enable its `nix-command` and
`flakes` experimental features before running:

```sh
nix run github:nerima-lisp/nshell/v0.6.3
nix run github:nerima-lisp/nshell/v0.6.4
```

## Install

```sh
nix profile install github:nerima-lisp/nshell/v0.6.3
nix profile install github:nerima-lisp/nshell/v0.6.4
man nshell # the manual page is installed alongside the binary
nshell
```
Expand All @@ -26,34 +26,34 @@ leave the interactive shell.
### Prebuilt Linux bundle

Without Nix, download both assets from the
[v0.6.3 release](https://github.com/nerima-lisp/nshell/releases/tag/v0.6.3),
[v0.6.4 release](https://github.com/nerima-lisp/nshell/releases/tag/v0.6.4),
verify the checksum, and extract the bundle. On `x86_64-linux`, with `curl`,
`sha256sum`, and `tar` available, run these commands in an empty directory:

```sh
curl --fail --location --remote-name https://github.com/nerima-lisp/nshell/releases/download/v0.6.3/nshell-v0.6.3-x86_64-linux.tar.gz
curl --fail --location --remote-name https://github.com/nerima-lisp/nshell/releases/download/v0.6.3/nshell-v0.6.3-x86_64-linux.tar.gz.sha256
sha256sum --check nshell-v0.6.3-x86_64-linux.tar.gz.sha256
curl --fail --location --remote-name https://github.com/nerima-lisp/nshell/releases/download/v0.6.4/nshell-v0.6.4-x86_64-linux.tar.gz
curl --fail --location --remote-name https://github.com/nerima-lisp/nshell/releases/download/v0.6.4/nshell-v0.6.4-x86_64-linux.tar.gz.sha256
sha256sum --check nshell-v0.6.4-x86_64-linux.tar.gz.sha256
```

Proceed only if the checksum command exits successfully and prints
`nshell-v0.6.3-x86_64-linux.tar.gz: OK`:
`nshell-v0.6.4-x86_64-linux.tar.gz: OK`:

```sh
tar -xzf nshell-v0.6.3-x86_64-linux.tar.gz
./nshell-v0.6.3-x86_64-linux/bin/nshell --version
man ./nshell-v0.6.3-x86_64-linux/share/man/man1/nshell.1
./nshell-v0.6.3-x86_64-linux/bin/nshell
tar -xzf nshell-v0.6.4-x86_64-linux.tar.gz
./nshell-v0.6.4-x86_64-linux/bin/nshell --version
man ./nshell-v0.6.4-x86_64-linux/share/man/man1/nshell.1
./nshell-v0.6.4-x86_64-linux/bin/nshell
```

Keep the extracted directory intact: the launcher resolves libraries and
helpers relative to itself. The version output must identify `v0.6.3`.
helpers relative to itself. The version output must identify `v0.6.4`.
Type `exit` to return to your existing shell. To use `nshell` by name in
the examples below, add the extracted `bin` directory to that shell's
`PATH`. For Bash or Zsh, from the download directory:

```sh
export PATH="$PWD/nshell-v0.6.3-x86_64-linux/bin:$PATH"
export PATH="$PWD/nshell-v0.6.4-x86_64-linux/bin:$PATH"
```

This changes only the current terminal session. Alternatively, replace
Expand Down Expand Up @@ -95,14 +95,14 @@ nshell -c 'string upper hello'
### Run a script

```sh
curl --fail --location --output greet.nsh https://raw.githubusercontent.com/nerima-lisp/nshell/v0.6.3/examples/greet.nsh
curl --fail --location --output greet.nsh https://raw.githubusercontent.com/nerima-lisp/nshell/v0.6.4/examples/greet.nsh
nshell greet.nsh World
```

Script files support multiline blocks (functions, `if`/`for`/`while`/`switch`),
comments, and a `#!` shebang; arguments after the script name are available as
`$argv`. See
[`examples/`](https://github.com/nerima-lisp/nshell/tree/v0.6.3/examples) for a
[`examples/`](https://github.com/nerima-lisp/nshell/tree/v0.6.4/examples) for a
runnable sample.

### Command line
Expand Down Expand Up @@ -131,7 +131,7 @@ The Nix build supplies [SBCL](http://www.sbcl.org/), ASDF, and the dependencies.
On `x86_64-linux`, with Git and flake-enabled Nix installed:

```sh
git clone --branch v0.6.3 https://github.com/nerima-lisp/nshell
git clone --branch v0.6.4 https://github.com/nerima-lisp/nshell
cd nshell
nix build
./result/bin/nshell
Expand Down
55 changes: 40 additions & 15 deletions docs/src/guide/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@

## Draw a pipeline without running it

`pipeline-graph` renders a typed pipeline as a validated Graphviz DOT graph — or
a Mermaid flowchart with `--mermaid` — without executing it. Quote the pipeline
`pipeline-graph` renders a pipeline as a Graphviz DOT graph, or a Mermaid
flowchart with `--mermaid`, without executing it. Quote the pipeline
so the shell passes it as a single argument rather than running it:

```sh
pipeline-graph 'cat access.log | grep 404 | wc -l'
pipeline-graph --mermaid 'cat access.log | grep 404 | wc -l'
```

It is a diagnostic built on the
[cl-dataflow-kit](https://github.com/nerima-lisp/cl-dataflow-kit) computation-graph
toolkit, and it validates the pipeline as it builds the graph, so a malformed
redirect shows up as an error rather than a picture.
Malformed redirects are reported as errors instead of rendered as graphs.

## Write a script

Expand All @@ -27,13 +24,17 @@ end
greet $argv[1]
```

Run it with `nshell greet.nsh World`. Multiline blocks (`function`, `if`, `for`,
Save this as `greet.nsh` and run `nshell greet.nsh World`. Multiline blocks (`function`, `if`, `for`,
`while`, `switch`, `begin`/`end`), comments, and a `#!` shebang all work;
arguments after the script name arrive as `$argv`.

## Use the assistant

Start an interactive assistant task from a terminal:
The assistant requires an authenticated `claude` CLI on PATH. Set
`NSHELL_AI_COMMAND` to use another executable implementing the same streaming
protocol. `NSHELL_AI_DISABLE` disables the assistant.

Start an interactive assistant task from an nshell terminal:

```sh
agent "inspect the current job state"
Expand All @@ -45,22 +46,46 @@ commands are not executed. Approval applies to one proposal, not the rest of
the task. Review each command, especially in a directory containing sensitive
data.

The assistant refuses to start if its sidecar reports tools or MCP servers
outside the permitted initialization contract, even if a later initialization
event reports an acceptable configuration.
The safety label comes from static command analysis, not a security sandbox.
For example, recursive deletion of the filesystem root is blocked. A safe label
does not replace your review of the command, its arguments, and its working
directory.

Before command approval, the assistant receives your task and context: the last
command, its exit status and duration, the working directory, Git branch and
dirty state, a bounded tail of command output, and environment-variable names.
Known sensitive paths, commands, and environment values are redacted, but this
does not guarantee that task text or command output contains no secrets. Do not
submit confidential information unless the configured provider is permitted to
receive it.

At the review panel, press `e` to edit, `s` to skip the proposal, or `q` to cancel
the task. While editing, `q` and `s` insert text; Ctrl-C cancels the task. Enter
reclassifies the edited command and approves it for execution if it is not
blocked.

The sidecar may report only the `StructuredOutput` tool (or no tools) and no MCP
servers. Both fields must be present in its initialization event. Other
configurations are rejected even if a later event reports an acceptable one.

Inspect assistant state and configure the session with `ai`:

```sh
ai status
ai set effort medium
ai set model 'model-name'
ai set max-steps 20
ai set budget 1.50
ai log 10
ai reset
```

`ai log 10` shows the last ten audit entries. `ai reset` resets the conversation
and usage counters. `ai set model 'model-name'` selects a model identifier
supported by your CLI; without a setting, the CLI chooses its default model.
The default maximum is ten proposals per task. Skipped and blocked proposals
also count toward `max-steps`; editing a proposal does not add a step.

`ai set budget 1.50` sets a US-dollar reference amount for the usage display.
It does **not** enforce a spending limit or stop provider requests.

`ai set` changes the current session. The corresponding environment variables
are `NSHELL_AI_EFFORT`, `NSHELL_AI_MODEL`, `NSHELL_AI_MAX_STEPS`, and
`NSHELL_AI_BUDGET_USD`.
Expand All @@ -85,7 +110,7 @@ perl -e '$SIG{ALRM}=sub { exit 124 }; alarm 300; exec @ARGV' nix run .#test
perl -e '$SIG{ALRM}=sub { exit 124 }; alarm 300; exec @ARGV' sbcl --script run-tests.lisp
```

Just the focused completion suite (`nshell/weave`):
Just the focused completion suite (`nshell/weave`), inside `nix develop`:

```sh
perl -e '$SIG{ALRM}=sub { exit 124 }; alarm 300; exec @ARGV' sbcl --script scripts/weave.lisp
Expand Down
7 changes: 4 additions & 3 deletions docs/src/project/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ tools on the Linux verification host.
manifest, store-reference hygiene, platform library closure, and smoke
startup. Do not replace this with a check of the unbundled default package.
- `./result/bin/nshell --version` reports the intended version.
- `./result/bin/nshell --help` and `man ./man/nshell.1` match the documentation
- `./result/bin/nshell --help` and `man ./result/share/man/man1/nshell.1` match the documentation
and shipped behaviour.
- Package the built bundle using GNU tar and gzip, as the release workflow
does. Replace `vX.Y.Z` with the intended tag:
Expand Down Expand Up @@ -80,8 +80,9 @@ tools on the Linux verification host.
perl scripts/test-release-pty.pl "$extracted/$name"
```

The PTY check exercises editing, Ctrl-C, Ctrl-Z/`bg`/`fg`, history across
restart, and terminal restoration. A source-loaded suite is not a substitute.
The PTY check exercises multiline editing and paste, completion with spaces,
Ctrl-C, Ctrl-Z/`bg`/`fg`, resize during editing, history across restart, and
terminal restoration. A source-loaded suite is not a substitute.
- Release notes are drafted. Review changes since the previous tag and describe
user-visible features, fixes, and any required migration. After the
workflow goes green, paste them in and publish the draft:
Expand Down
2 changes: 1 addition & 1 deletion man/nshell.1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.TH NSHELL 1 "2026-09-30" "nshell 0.6.3" "User Commands"
.TH NSHELL 1 "2026-09-30" "nshell 0.6.4" "User Commands"
.SH NAME
nshell \- a fish-inspired interactive shell written in Common Lisp
.SH SYNOPSIS
Expand Down
8 changes: 4 additions & 4 deletions nshell.asd
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
:license "MIT"
;; Single source of truth for the version. flake.nix reads this form
;; line-by-line, and release.yml refuses a tag that disagrees with it.
:version "0.6.3"
:version "0.6.4"
:homepage "https://github.com/nerima-lisp/nshell"
:bug-tracker "https://github.com/nerima-lisp/nshell/issues"
:source-control (:git "https://github.com/nerima-lisp/nshell.git")
Expand Down Expand Up @@ -403,7 +403,7 @@
:in-order-to ((test-op (test-op "nshell/test"))))

(asdf:defsystem "nshell/test"
:version "0.6.3"
:version "0.6.4"
:author "takeokunn <bararararatty@gmail.com>"
:maintainer "takeokunn <bararararatty@gmail.com>"
:license "MIT"
Expand Down Expand Up @@ -590,7 +590,7 @@
(error "cl-weave tests failed"))))

(asdf:defsystem "nshell/weave"
:version "0.6.3"
:version "0.6.4"
:author "takeokunn <bararararatty@gmail.com>"
:maintainer "takeokunn <bararararatty@gmail.com>"
:license "MIT"
Expand Down Expand Up @@ -620,7 +620,7 @@ primary suite in nshell/test."
:reporter :spec)
(error "cl-weave suite failed"))))
(asdf:defsystem "nshell/benchmark"
:version "0.6.3"
:version "0.6.4"
:author "takeokunn <bararararatty@gmail.com>"
:maintainer "takeokunn <bararararatty@gmail.com>"
:license "MIT"
Expand Down
78 changes: 73 additions & 5 deletions scripts/test-release-pty.pl
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@
expected = ('rel-%s:<%s>' % (label, value)).encode()
start = len(self.output)
self.send(command)
match, end = self.expect(re.escape(expected), start, command)
match, end = self.expect(re.escape(expected).replace(re.escape(b'\n'), rb'\r?\n'), start, command)
self.expect(re.escape(prompt), end)
return command

Expand Down Expand Up @@ -335,6 +335,73 @@
session.wait(lambda: session.foreground() == pid)
interrupt_job(pid)
check(stage)
stage = 'multiline cross-line editing'
label = 'multiline-' + nonce
line = ("printf 'rel-%%s:<%%s>\\n' %s 'Lx\nR'" % label).encode()
command = b'\x1b[200~' + line + b'\x1b[201~' + b'\x1b[D' * 3 + b'\x08' + b'\x1b[C' * 3 + b'\n'
start = len(session.output)
session.send(command)
_, end = session.expect(re.escape(('rel-%s:<L' % label).encode()) + rb'\r?\nR>', start, command)
session.expect(re.escape(prompt), end)
check(stage)

stage = 'quoted multiline paste'
quoted = 'a "b" \\ c\nd'
label = 'paste-' + nonce
line = ("printf 'rel-%%s:<%%s>\\n' %s '%s'" % (label, quoted)).encode()
command = b'\x1b[200~' + line + b'\x1b[201~\n'
start = len(session.output)
session.send(command)
expected = re.escape(('rel-%s:<%s>' % (label, quoted)).encode()).replace(re.escape(b'\n'), rb'\r?\n')
_, end = session.expect(expected, start, command)
session.expect(re.escape(prompt), end)
check(stage)

stage = 'Tab completion spaced filename'
filename = 'complete-' + nonce + ' space.txt'
expected = ('rel-complete-%s:<file-content>' % nonce).encode()
with open(os.path.join(isolation, filename), 'xb') as completed_file:
completed_file.write(expected + b'\n')
command = ('cat ./complete-' + nonce).encode() + b'\t\n'
start = len(session.output)
session.send(command)
_, end = session.expect(re.escape(expected), start, command)
session.expect(re.escape(prompt), end)
check(stage)

stage = 'Ctrl-C multiline pending input'
label = 'cancel-' + nonce
pending = ("printf 'rel-%%s:<%%s>\\n' %s 'cancel-first\ncancel-second" % label).encode()
start = len(session.output)
session.send(pending)
session.expect(ansi.join(re.escape(bytes([byte])) for byte in b'cancel-second'), start)
start = len(session.output)
session.send(b'\x03')
session.expect(re.escape(prompt), start)
session.marker('multiline-recovered', 'yes')
if ('rel-%s:<' % label).encode() in session.output[start:]:
raise RuntimeError('cancelled multiline input executed')
if session.foreground() != session.pid:
raise RuntimeError('shell did not retain foreground after multiline interrupt')
check(stage)

stage = 'resize pending wrapped editing'
label = 'resize-' + nonce
pending = ("printf 'rel-%%s:<%%s>\\n' %s okx" % label).encode()
start = len(session.output)
session.send(pending)
session.expect(ansi.join(re.escape(bytes([byte])) for byte in b'okx'), start)
size = fcntl.ioctl(session.master, termios.TIOCGWINSZ, b'\0' * 8)
try:
fcntl.ioctl(session.master, termios.TIOCSWINSZ, struct.pack('HHHH', 24, 20, 0, 0))
command = b'\x08\n'
session.send(command)
_, end = session.expect(re.escape(('rel-%s:<ok>' % label).encode()), start, pending + command)
session.expect(re.escape(prompt), end)
finally:
fcntl.ioctl(session.master, termios.TIOCSWINSZ, size)
check(stage)

stage = 'interactive termios restoration'
session.finish()
check(stage)
Expand All @@ -343,15 +410,16 @@
writer = Session(history=True)
writer.expect(re.escape(prompt), 0)
writer.marker('history-older', 'older')
writer.marker('history-newest', 'newest')
writer.marker('history-newest', quoted)
writer.finish()
reader = Session(history=True)
reader.expect(re.escape(prompt), 0)
if writer.pid == reader.pid:
raise RuntimeError('history restart did not start a different process')
start = len(reader.output)
reader.send(b'\x1b[A\n')
_, end = reader.expect(re.escape(('rel-history-newest-%s:<newest>' % nonce).encode()), start, b'\x1b[A\n')
expected = re.escape(('rel-history-newest-%s:<%s>' % (nonce, quoted)).encode()).replace(re.escape(b'\n'), rb'\r?\n')
_, end = reader.expect(expected, start, b'\x1b[A\n')
reader.expect(re.escape(prompt), end)
reader.finish()
check(stage)
Expand Down Expand Up @@ -479,10 +547,10 @@
for error in cleanup_errors:
print('cleanup: ' + error, file=sys.stderr)
sys.exit(1)
if len(passed) != 6 or len(sessions) != 3 or any(s.status != 0 for s in sessions):
if len(passed) != 11 or len(sessions) != 3 or any(s.status != 0 for s in sessions):
print('FAIL: incomplete PTY execution', file=sys.stderr)
sys.exit(1)
print('PASS: 6 PTY checks; 3 executable sessions exited zero; owned processes cleaned', flush=True)
print('PASS: 11 PTY checks; 3 executable sessions exited zero; owned processes cleaned', flush=True)
PYTHON

my $pid = fork // die "cannot start PTY backend: $!\n";
Expand Down
Loading
Loading