UniPty v0.2.0: safer command input
UniPty v0.2.0 adds two optional official parser packages. @unipty/shell-parser classifies Bash command text into structured argv or an explicit shell script request; @unipty/powershell-parser answers only to the official Parser.ParseInput. Nothing is executed at any point. One release now carries 8 public packages, up from 6.
Эта запись написана на английский. Читать на китайский →
$ npm view @unipty/shell-parser version
0.2.0
UniPty v0.2.0 is out (the GitHub Release was cut on 2026-08-20). This release lands the parser ecosystem the v1 architecture reserved: two optional official packages take command text and classify it into structured argv you can launch directly, or a script request that carries shell semantics and needs your explicit acceptance. Nothing is executed at any point along the way. One release now carries 8 public packages, up from 6.
Background: one contract, three runtimes
Node, Bun, and Deno each expose an incompatible PTY substrate: installation models, I/O representations, lifecycle semantics, and native deployment constraints all differ (the opening motivation of the v1 architecture proposal). UniPty absorbs those differences behind one public contract: Core owns every observable behavior, developers pick the Backend explicitly, and support claims are evidence-gated. The three official routes are Node over third-party node-pty, Bun over runtime-native Bun.Terminal, and Deno over self-contained @sigma/pty-ffi.
Core accepts only structured argv; there is no string-command overload. Projects migrating from string-command tooling had to hand-write the text-to-argv translation themselves, and one mistake there is one implicit-shell accident.
Mechanism: how support claims are evidence-gated
A runtime/platform pair is marked verified only when the public conformance suite passes in full against the installed package artifact (packed and installed into an isolated consumer, driven only through public exports). Everything else is declared-unverified or not-targeted. Failures stay CI diagnostics and never harden into permanent unsupported claims (see the conformance docs). The parser packages stay out of this matrix; their verification is the per-package fixture corpora.
Bash command text no longer needs a hand-written translation
@unipty/shell-parser reads a piece of command text and returns one of five classes, telling you whether it can be launched directly:
import { parse } from "@unipty/shell-parser";
parse("git status --force");
// → { kind: "argv", argv: ["git", "status", "--force"] }
parse("ls *.txt | wc -l");
// → { kind: "script", language: "bash", source: "ls *.txt | wc -l" }
parse("echo 'unterminated");
// → { kind: "incomplete", diagnostics: [...] }
The five-way classification argv | script | incomplete | unsupported | invalid is the only stable boundary this package promises. argv means the text is lexically exactly one simple command (quoting and empty arguments preserved) and can go straight to unipty.spawn(argv). script means the text carries shell semantics (pipelines, redirection, expansions, globs), and launching becomes possible only after you explicitly accept the named shell policy. incomplete means the text is not finished yet, invalid means a syntax error, and unsupported means a word construct the walker cannot judge and refuses to guess.
How executables resolve (PATH, builtins, functions, aliases) stays your decision; the parser never claims process-launch equivalence for a command name.
The classification policy is deliberately conservative: escaped metacharacters (echo \*) and unquoted bracket characters ([ -f x ]) classify as script too, and anything ambiguous stays on the shell-request side.
The package is a synchronous thin wrapper over unbash (pinned exactly at 4.0.10). Its only public surface is the classification result, and the unbash AST is never exposed. It is pure JavaScript with zero platform APIs and has no runtime relationship with unipty. See the package docs.
Mechanism: how the walker proves literalness
argv comes from the walker proving each word of the raw text: every word must be literal after static quote processing, and any construct that needs shell evaluation (expansions, substitutions, globs, redirects, and the rest) pushes the text toward script. Comment-only or shebang-carrying text classifies as script as well; diagnostics carry UTF-16 source ranges and split incomplete from invalid by end-of-input-shaped messages.
PowerShell text is judged by the official parser
PowerShell syntax is not something JavaScript should guess. The only semantic authority of @unipty/powershell-parser is PowerShell's own Parser.ParseInput, run by a host you name, pwsh by default:
import { parsePowershell } from "@unipty/powershell-parser";
await parsePowershell('dotnet build -c "My Config"');
// → { kind: "argv", argv: ["dotnet", "build", "-c", "My Config"] }
Your text travels as base64-encoded UTF-8 over stdin (the static adapter script travels as -EncodedCommand), never interpolated into a command line, and immune to Windows console code-page ambiguity.
Failures are typed in three parts, and none of them is silent: a missing host gives capability-unavailable and never falls back to Bash-style parsing; a host that starts but exits abnormally or returns an unknown result gives host-failure and never downgrades to script; an exceeded budget (15 seconds by default, configurable through timeoutMs) gives host-timeout.
Host selection is explicit too: isPowershellHostAvailable() probes first, and options.host can name another executable, for example powershell for Windows PowerShell 5.1, which also provides Parser.ParseInput. See the package docs.
Details: diagnostics serialization and host probing
Official ParseError records serialize as { message, errorId, incomplete, range } with UTF-16 offsets; errors flagged IncompleteInput map to incomplete, everything else to invalid.
import { isPowershellHostAvailable, PowershellParseError } from "@unipty/powershell-parser";
await isPowershellHostAvailable(); // → false when no pwsh exists
await parsePowershell("x", { host: "pwsh-preview" }); // explicit host choice
Other changes
The release order publishes the two parser packages before the core and backends: the newest package names are the most likely to lack a configured Trusted Publisher, so an authentication failure has to happen before any already-published package creates an irreversible partial release (release workflow). The npm timestamps confirm it:
@unipty/shell-parser@0.2.0landed at 11:00:13,@unipty/powershell-parser@0.2.0at 11:00:20,unipty@0.2.0followed at 11:00:26, and GitHub Release v0.2.0 was cut at 11:01.shell-parser keeps locale strings (
$"...") and NUL-bearing ANSI-C words out of argv (5bcd7d2)powershell-parser moved text transport to stdin and enforced strict host failures (7f121ff)
the docs site consumes the real release-catalog schema and requires the aggregator (aggregation-check) suite's passing stamp (586e24c, e489776)
The full list is in compare v0.1.1...v0.2.0. This release has no breaking changes. Since 0.1.1 there are 12 commits: 2 feat / 4 fix / 3 docs / 2 chore / 1 ci (counted by conventional-commit prefix).
Upgrade
No breaking changes. The parser ecosystem is purely additive, with zero changes to Core / Backend / acquisition / helper / conformance code (the change proposal's Impact section, verbatim). Upgrading is:
npm i unipty@0.2.0
# optional: add the parsers as needed
npm i @unipty/shell-parser@0.2.0 @unipty/powershell-parser@0.2.0
Acknowledgements
@unipty/shell-parser is built on unbash; the three Backend routes respectively thank node-pty (via the @lydell/node-pty prebuilt distribution), Bun.Terminal, and @sigma/pty-ffi.
Links
Changelog: GitHub Release v0.2.0 · compare v0.1.1...v0.2.0
Docs: unipty.jixoai.com · architecture · capability specs
Upgrade: see the Upgrade section above (no breakage, one
npm i)Discussion: GitHub Issues
npm: unipty · @unipty/shell-parser · @unipty/powershell-parser
Chinese version: /zh/blog/2026-09-06-unipty-v0-2-0/
