Repository navigation
Releases: roc-lang/basic-cli
Release list
0.26.0
basic-cli 0.26.0
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.26.0/GddgvsGjiNXrHFW6au5i5dUDbTs8FinQdrgE2Gqmw53t.tar.zst" }Requires the Roc new-compiler nightly nightly-2026-10-12-2664a27 or later.
One way to do each thing
Until now each basic-cli module had grown its own habits. A timeout was a bare U64 of milliseconds in one place and a tagged value in another, and zero meant "fail at once" for TCP but "no deadline" for a command. "No more input" was Err(EndOfFile) from stdin, an empty list from a file and an empty string from a socket. Errors arrived in a different shape from every module, and some could not be compared with == at all.
0.26.0 settles these questions once and applies the answers everywhere:
- Basic-cli timeout options use
Duration, andDuration.zeroalways expires immediately. The shared HTTP package keeps its existing request-timeout representation, described below. - The end of input is
Err(EndOfInput), on stdin, files and sockets alike. An emptyOkis real data. - Anything you can read, you can loop over:
lines()andchunks()return aStreamon stdin, file readers, TCP connections and HTTP responses, androwsdoes the same for SQLite. - A module's failures arrive under one tag (
TcpErr(IOErr),CmdErr(Cmd.Err),SqliteErr(Sqlite.Err)), while conditions such asEndOfInputandInterruptedremain bare tags. Every error can be compared with==.
The result is that code written against one module reads like code written against another:
conn = Tcp.connect!("example.org", 7, { connect_timeout: Duration.seconds(1), io_timeout: Duration.seconds(5) })?
for! item in conn.lines() {
match item {
Ok(line) => Stdout.line!(line)?
Err(BadUtf8(_)) => {}
Err(ReadLimitExceeded(_)) => {}
Err(TcpErr(err)) => { return Err(TcpErr(err)) }
Err(Interrupted) => { return Err(Interrupted) }
}
}This is a large release with many breaking changes. The upgrade guide below lists every one with its replacement. The rules themselves are written down in CONTRIBUTING.md, so future modules follow them too.
Steady allocation performance on Linux
The statically linked Linux hosts now use mimalloc for Rust and Roc general
allocations. Repeated small allocations reuse freed pages, avoiding musl
mallocng's heap-layout-dependent mmap/munmap churn (#530).
The Roc allocation ABI and resource-handle lifetime behavior are unchanged.
Time has types
start = Monotonic.now!()
build!()?
Stdout.line!("Completed in ${start.elapsed!().to_str()}")? # "Completed in 1.5s"
stale = Utc.now!().since(path.time_modified!()?) > Duration.minutes(5)Durationis a non-negative length of time with nanosecond resolution:Duration.millis(250),Duration.seconds(5),Duration.minutes(2). It supports+,-,==, the ordering operators andList.sort, and prints as"250ms"or"1.5s". Nothing overflows or goes negative: results are clamped, sobudget - start.elapsed!()is simply zero once the budget is spent.Utcis now a real type holding a signed instant.Utc.now!()no longer returns 1970 when the clock cannot be read, and times before 1970 work, including into_iso_8601.Monotonicis an opaque clock reading.start.elapsed!()andlater.since(earlier)return aDuration, so a reading can no longer be mistaken for a length of time.- File times (
time_accessed!,time_modified!,time_created!) returnUtcand succeed for files older than 1970. - Sleep is now fallible: use
Sleep.millis!(250)?orduration.sleep!()?. A caught stop request returnsErr(Interrupted).
Utc supplies clock readings and signed epoch conversion. Timestamp parsing, calendar calculations and richer formatting belong in packages such as roc-time, rather than the native host. See its supported profiles: roc-time 0.3 uses microseconds, while Utc stores nanoseconds. Convert units explicitly between them. Utc.to_iso_8601 remains a whole-second display; to_nanos_since_epoch and from_nanos_since_epoch preserve the full stored precision.
Loop over input
Reading lines used to mean hand-written recursion. Now it is a loop, and the same loop works on every source:
var $count = 0
for! item in reader.lines() {
match item {
Ok(_) => { $count = $count + 1 }
Err(BadUtf8(_)) => {}
Err(FileErr(err)) => { return Err(FileErr(err)) }
}
}Stdin.lines(),Stdin.chunks(),reader.lines()andreader.chunks()are new. Lines never include the\n, and one preceding\ris removed. A final line without a terminator is still a line.File.Reader.read_until!(byte)returns bytes through and including a delimiter.- A line that is not valid UTF-8 is one
Err(BadUtf8(_))item and the loop carries on. A failure of the source itself is the last item. - Streams buffer nothing in Roc. You can
breakout ofStdin.lines()after a header and hand the rest toStdin.read_to_end!()without losing a byte, and mixreader.lines()withseek!andread_exactly!.
Write files piece by piece
Path.write_utf8! replaces a file with one payload, so output had to be built in memory first. File.Writer opens a file once and writes to it as you go:
File.with_writer!("report.csv", Truncate, {}, |writer| {
writer.write_utf8!("name,total\n")?
for row in rows {
writer.write_utf8!("${row.name},${row.total.to_str()}\n")?
}
Ok({})
})?- The mode is
Truncate,AppendorRequireNew(fails withAlreadyExistsif anything is at the path). writer.write_stream!(stream)writes any chunk stream as it arrives, so a file can be copied or transformed in bounded memory:writer.write_stream!(reader.chunks()).File.with_writer!flushes when your callback returns and reports a failed flush. WithFile.open_writer!, callflush!yourself after the last write.Path.append_bytes!andPath.append_utf8!are also new, for adding to a file in one call (#516).
Create private files and symbolic links
The writer options choose permissions for a new regular file, with the same access policy on Linux, macOS and Windows:
File.with_writer!("credentials.json", RequireNew, { create_permissions: OwnerOnly }, |writer| {
writer.write_utf8!(credentials_json)
})?OwnerOnlygrants the file's owner read/write access and grants others no access.PublicReadadditionally grants everyone read access. Both replace inherited discretionary access grants and remove execute permission;{}keeps the operating system's default creation policy.- Permissions are enforced before a writer is returned or any content is accepted. If the filesystem cannot enforce the requested policy, opening fails. A failed creation can leave an empty file. Existing handles and privileged operating system access remain outside these policies.
- Existing files keep their permissions, including with
AppendorTruncate. UseRequireNewwithOwnerOnlyfor secrets: an existing file or symbolic link fails withAlreadyExists, before its contents can change. path.set_permissions!(OwnerOnly)andpath.set_permissions!(PublicRead)replace an existing regular file's access grants while preserving its owner and contents. They follow symbolic links and reject directories and special files. On macOS the target currently must be readable so the platform can open a handle before replacing its ACL; a mode-000 file returnsPermissionDenied. Linux can restore access to a mode-000 file through a held metadata handle and requires/proc/self/fdfor that repair.
Create a link with Path.symlink!(target, link, File) or Path.symlink!(target, link, Directory), and read its stored target with link.read_link!(). A relative target resolves from the link's parent directory and is returned unchanged. Dangling targets are allowed; the declared kind must match the target when it exists. Creating a link never replaces an existing path. Windows requires Developer Mode or permission to create symbolic links.
Download without holding the body in memory
Http.send_streaming! returns as soon as the status and headers arrive, and hands you the body as a stream:
request = Request.from_method(GET).with_uri(url.to_str())
response = Http.send_streaming!(request, {})?
if response.status() != 200 {
return Err(UnexpectedStatus(response.status()))
}
File.with_writer!(destination, Truncate, {}, |writer| writer.write_stream!(response.chunks()))?- You see the status before any of the body is read, so you can decline a 404's body.
idle_timeout(30 seconds unless you set it) bounds each single wait, not the whole download. A slow but healthy download keeps going; a stalled one stops. After an idle timeout the response is still usable.- The shared HTTP
Requestkeeps its existing timeout representation:TimeoutMilliseconds(n)sets the overall deadline, whileNoTimeout(the default) andTimeoutMilliseconds(0)mean no deadline. Streamingidle_timeoutusesAfter(Duration)orNoTimeout;After(Duration.zero)expires immediately. response.read_to_end!(MaxBytes(1_048_576))reads the rest with a bound, andresponse.content_length()isKnown(n)orUnknown.Http.send!,send_json!,get!andget_utf8!now take trailing options: pass{}to return the first response, as before.- Requests send
User-Agent: roc-basic-cli/0.26by default, so APIs that require a user agent can be called directly. An explicitUser-Agentov...
0.26.0-rc3
basic-cli 0.26.0-rc3
Release candidate for 0.26.0. The APIs are still being tested before the final release.
This follows 0.26.0-rc2
and adds the release feedback from PR #529:
- Catch Ctrl-C and Unix SIGTERM with
Signal.catch!/poll!, and handle cooperativeInterruptedresults so applications can clean up before exiting. - Use compact
Cmd.call!, a reusableCmd.Commandbuilder, and UnixCmd.exec!for process replacement. - Add OS witnesses, enforced file permissions, symlinks, Unix stream sockets, and opt-in HTTP redirects.
- Fix Linux musl allocation syscall churn (#530) and Windows console cancellation.
The draft 0.26.0 notes and migration guide follow. Please report behavior that differs from these notes.
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.26.0-rc3/72LPrJNPhao1zzEbawpgzKnkwAPRKy6Xm8jM1rPchQ7o.tar.zst" }Requires the Roc new-compiler nightly nightly-2026-10-12-2664a27 or later.
One way to do each thing
Until now each basic-cli module had grown its own habits. A timeout was a bare U64 of milliseconds in one place and a tagged value in another, and zero meant "fail at once" for TCP but "no deadline" for a command. "No more input" was Err(EndOfFile) from stdin, an empty list from a file and an empty string from a socket. Errors arrived in a different shape from every module, and some could not be compared with == at all.
0.26.0 settles these questions once and applies the answers everywhere:
- A timeout is a
Duration, andDuration.zeroalways expires immediately. - The end of input is
Err(EndOfInput), on stdin, files and sockets alike. An emptyOkis real data. - Anything you can read, you can loop over:
lines()andchunks()return aStreamon stdin, file readers, TCP connections and HTTP responses, androwsdoes the same for SQLite. - A module's failures arrive under one tag (
TcpErr(IOErr),CmdErr(Cmd.Err),SqliteErr(Sqlite.Err)), and every error can be compared with==.
The result is that code written against one module reads like code written against another:
conn = Tcp.connect!("example.org", 7, { connect_timeout: Duration.seconds(1), io_timeout: Duration.seconds(5) })?
for! item in conn.lines() {
match item {
Ok(line) => Stdout.line!(line)?
Err(BadUtf8(_)) => {}
Err(ReadLimitExceeded(_)) => {}
Err(TcpErr(err)) => { return Err(TcpErr(err)) }
Err(Interrupted) => { return Err(Interrupted) }
}
}This is a large release with many breaking changes. The upgrade guide below lists every one with its replacement. The rules themselves are written down in CONTRIBUTING.md, so future modules follow them too.
Steady allocation performance on Linux
The statically linked Linux hosts now use mimalloc for Rust and Roc general
allocations. Repeated small allocations reuse freed pages, avoiding musl
mallocng's heap-layout-dependent mmap/munmap churn (#530).
The Roc allocation ABI and resource-handle lifetime behavior are unchanged.
Time has types
start = Monotonic.now!()
build!()?
Stdout.line!("Completed in ${start.elapsed!().to_str()}")? # "Completed in 1.5s"
stale = Utc.now!().since(path.time_modified!()?) > Duration.minutes(5)Durationis a non-negative length of time with nanosecond resolution:Duration.millis(250),Duration.seconds(5),Duration.minutes(2). It supports+,-,==, the ordering operators andList.sort, and prints as"250ms"or"1.5s". Nothing overflows or goes negative: results are clamped, sobudget - start.elapsed!()is simply zero once the budget is spent.Utcis now a real type holding a signed instant.Utc.now!()no longer returns 1970 when the clock cannot be read, and times before 1970 work, including into_iso_8601.Monotonicis an opaque clock reading.start.elapsed!()andlater.since(earlier)return aDuration, so a reading can no longer be mistaken for a length of time.- File times (
time_accessed!,time_modified!,time_created!) returnUtcand succeed for files older than 1970. - Sleep is now fallible: use
Sleep.millis!(250)?orduration.sleep!()?. A caught stop request returnsErr(Interrupted).
Loop over input
Reading lines used to mean hand-written recursion. Now it is a loop, and the same loop works on every source:
var $count = 0
for! item in reader.lines() {
match item {
Ok(_) => { $count = $count + 1 }
Err(BadUtf8(_)) => {}
Err(FileErr(err)) => { return Err(FileErr(err)) }
}
}Stdin.lines(),Stdin.chunks(),reader.lines()andreader.chunks()are new. Lines never include the\n, and one preceding\ris removed. A final line without a terminator is still a line.File.Reader.read_until!(byte)returns bytes through and including a delimiter.- A line that is not valid UTF-8 is one
Err(BadUtf8(_))item and the loop carries on. A failure of the source itself is the last item. - Streams buffer nothing in Roc. You can
breakout ofStdin.lines()after a header and hand the rest toStdin.read_to_end!()without losing a byte, and mixreader.lines()withseek!andread_exactly!.
Write files piece by piece
Path.write_utf8! replaces a file with one payload, so output had to be built in memory first. File.Writer opens a file once and writes to it as you go:
File.with_writer!("report.csv", Truncate, {}, |writer| {
writer.write_utf8!("name,total\n")?
for row in rows {
writer.write_utf8!("${row.name},${row.total.to_str()}\n")?
}
Ok({})
})?- The mode is
Truncate,AppendorRequireNew(fails withAlreadyExistsif anything is at the path). writer.write_stream!(stream)writes any chunk stream as it arrives, so a file can be copied or transformed in bounded memory:writer.write_stream!(reader.chunks()).File.with_writer!flushes when your callback returns and reports a failed flush. WithFile.open_writer!, callflush!yourself after the last write.Path.append_bytes!andPath.append_utf8!are also new, for adding to a file in one call (#516).
Create private files and symbolic links
The writer options choose permissions for a new regular file, with the same access policy on Linux, macOS and Windows:
File.with_writer!("credentials.json", RequireNew, { create_permissions: OwnerOnly }, |writer| {
writer.write_utf8!(credentials_json)
})?OwnerOnlygrants the file's owner read/write access and grants others no access.PublicReadadditionally grants everyone read access. Both replace inherited discretionary access grants and remove execute permission;{}keeps the operating system's default creation policy.- Permissions are enforced before a writer is returned or any content is accepted. If the filesystem cannot enforce the requested policy, opening fails. A failed creation can leave an empty file. Existing handles and privileged operating system access remain outside these policies.
- Existing files keep their permissions, including with
AppendorTruncate. UseRequireNewwithOwnerOnlyfor secrets: an existing file or symbolic link fails withAlreadyExists, before its contents can change. path.set_permissions!(OwnerOnly)andpath.set_permissions!(PublicRead)replace an existing regular file's access grants while preserving its owner and contents. They follow symbolic links and reject directories and special files. On macOS the target currently must be readable so the platform can open a handle before replacing its ACL; a mode-000 file returnsPermissionDenied. Linux can restore access to a mode-000 file through a held metadata handle and requires/proc/self/fdfor that repair.
Create a link with Path.symlink!(target, link, File) or Path.symlink!(target, link, Directory), and read its stored target with link.read_link!(). A relative target resolves from the link's parent directory and is returned unchanged. Dangling targets are allowed; the declared kind must match the target when it exists. Creating a link never replaces an existing path. Windows requires Developer Mode or permission to create symbolic links.
Download without holding the body in memory
Http.send_streaming! returns as soon as the status and headers arrive, and hands you the body as a stream:
request = Request.from_method(GET).with_uri(url.to_str())
response = Http.send_streaming!(request, {})?
if response.status() != 200 {
return Err(UnexpectedStatus(response.status()))
}
File.with_writer!(destination, Truncate, {}, |writer| writer.write_stream!(response.chunks()))?- You see the status before any of the body is read, so you can decline a 404's body.
idle_timeout(30 seconds unless you set it) bounds each single wait, not the whole download. A slow but healthy download keeps going; a stalled one stops. After an idle timeout the response is still usable.response.read_to_end!(MaxBytes(1_048_576))reads the rest with a bound, andresponse.content_length()isKnown(n)orUnknown.Http.send!,send_json!,get!andget_utf8!now take trailing options: pass{}to return the first response, as before.- Redirects are opt-in:
Http.send!(request, { max_redirects: 5 })follows 301, 302, 303, 307 and 308. Streaming accepts the samemax_redirectsoption. Intermediate bodies are closed without collecting them; the final status, including a 404, is still a response. - Redirects use one overall request deadline, including ...
0.26.0-rc2
basic-cli 0.26.0-rc2
Release candidate for 0.26.0, for trying the new APIs before they are final.
It follows 0.26.0-rc1,
which tested the Windows MSVC host, and adds the API conventions work from
PR #528. The notes below are
the draft notes for 0.26.0; please report anything that does not behave as
they say.
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.26.0-rc2/Gj3zVDr3qrQa6aTh7Z1zgUfe9NeBE6R76T5ho3xrc8af.tar.zst" }Requires the Roc new-compiler nightly nightly-2026-10-12-2664a27 or later.
One way to do each thing
Until now each basic-cli module had grown its own habits. A timeout was a bare U64 of milliseconds in one place and a tagged value in another, and zero meant "fail at once" for TCP but "no deadline" for a command. "No more input" was Err(EndOfFile) from stdin, an empty list from a file and an empty string from a socket. Errors arrived in a different shape from every module, and some could not be compared with == at all.
0.26.0 settles these questions once and applies the answers everywhere:
- A timeout is a
Duration, andDuration.zeroalways expires immediately. - The end of input is
Err(EndOfInput), on stdin, files and sockets alike. An emptyOkis real data. - Anything you can read, you can loop over:
lines()andchunks()return aStreamon stdin, file readers, TCP connections and HTTP responses, androwsdoes the same for SQLite. - A module's failures arrive under one tag (
TcpErr(IOErr),CmdErr(Cmd.Err),SqliteErr(Sqlite.Err)), and every error can be compared with==.
The result is that code written against one module reads like code written against another:
conn = Tcp.connect!("example.org", 7, { connect_timeout: Duration.seconds(1), io_timeout: Duration.seconds(5) })?
for! item in conn.lines() {
match item {
Ok(line) => Stdout.line!(line)?
Err(BadUtf8(_)) => {}
Err(ReadLimitExceeded(_)) => {}
Err(TcpErr(err)) => { return Err(TcpErr(err)) }
}
}This is a large release with many breaking changes. The upgrade guide below lists every one with its replacement. The rules themselves are written down in CONTRIBUTING.md, so future modules follow them too.
Time has types
start = Monotonic.now!()
build!()?
Stdout.line!("Completed in ${start.elapsed!().to_str()}")? # "Completed in 1.5s"
stale = Utc.now!().since(path.time_modified!()?) > Duration.minutes(5)Durationis a non-negative length of time with nanosecond resolution:Duration.millis(250),Duration.seconds(5),Duration.minutes(2). It supports+,-,==, the ordering operators andList.sort, and prints as"250ms"or"1.5s". Nothing overflows or goes negative: results are clamped, sobudget - start.elapsed!()is simply zero once the budget is spent.Utcis now a real type holding a signed instant.Utc.now!()no longer returns 1970 when the clock cannot be read, and times before 1970 work, including into_iso_8601.Monotonicis an opaque clock reading.start.elapsed!()andlater.since(earlier)return aDuration, so a reading can no longer be mistaken for a length of time.- File times (
time_accessed!,time_modified!,time_created!) returnUtcand succeed for files older than 1970. Sleep.millis!(250)is unchanged. For a wait you computed, callduration.sleep!().
Loop over input
Reading lines used to mean hand-written recursion. Now it is a loop, and the same loop works on every source:
var $count = 0
for! item in reader.lines() {
match item {
Ok(_) => { $count = $count + 1 }
Err(BadUtf8(_)) => {}
Err(FileErr(err)) => { return Err(FileErr(err)) }
}
}Stdin.lines(),Stdin.chunks(),reader.lines()andreader.chunks()are new. Lines never include the\n, and one preceding\ris removed. A final line without a terminator is still a line.File.Reader.read_until!(byte)returns bytes through and including a delimiter.- A line that is not valid UTF-8 is one
Err(BadUtf8(_))item and the loop carries on. A failure of the source itself is the last item. - Streams buffer nothing in Roc. You can
breakout ofStdin.lines()after a header and hand the rest toStdin.read_to_end!()without losing a byte, and mixreader.lines()withseek!andread_exactly!.
Write files piece by piece
Path.write_utf8! replaces a file with one payload, so output had to be built in memory first. File.Writer opens a file once and writes to it as you go:
File.with_writer!("report.csv", Truncate, |writer| {
writer.write_utf8!("name,total\n")?
for row in rows {
writer.write_utf8!("${row.name},${row.total.to_str()}\n")?
}
Ok({})
})?- The mode is
Truncate,AppendorRequireNew(fails withAlreadyExistsif anything is at the path). writer.write_stream!(stream)writes any chunk stream as it arrives, so a file can be copied or transformed in bounded memory:writer.write_stream!(reader.chunks()).File.with_writer!flushes when your callback returns and reports a failed flush. WithFile.open_writer!, callflush!yourself after the last write.Path.append_bytes!andPath.append_utf8!are also new, for adding to a file in one call (#516).
Download without holding the body in memory
Http.send_streaming! returns as soon as the status and headers arrive, and hands you the body as a stream:
request = Request.from_method(GET).with_uri(url.to_str())
response = Http.send_streaming!(request, {})?
if response.status() != 200 {
return Err(UnexpectedStatus(response.status()))
}
File.with_writer!(destination, Truncate, |writer| writer.write_stream!(response.chunks()))?- You see the status before any of the body is read, so you can decline a 404's body.
idle_timeout(30 seconds unless you set it) bounds each single wait, not the whole download. A slow but healthy download keeps going; a stalled one stops. After an idle timeout the response is still usable.response.read_to_end!(MaxBytes(1_048_576))reads the rest with a bound, andresponse.content_length()isKnown(n)orUnknown.Http.send!and the functions built on it are unchanged.
TCP: typed errors and a timeout the connection remembers
# 0.25
stream = Tcp.connect!("127.0.0.1", 8085, 1_000) ? |err| ConnectFailed(err)
stream.write_utf8!("${out_msg}\n", 5_000) ? |err| TcpWriteFailed(err)
in_msg = stream.read_line!(1_048_576, 5_000) ? |err| TcpReadFailed(err)
# 0.26
conn = Tcp.connect!("127.0.0.1", 8085, { connect_timeout: Duration.seconds(1), io_timeout: Duration.seconds(5) })?
conn.write_utf8!("${out_msg}\n")?
in_msg = conn.read_line!()?Tcp.Streamis nowTcp.Connection. It holds its I/O timeout and line limit;conn.with_timeout(d)andconn.with_line_limit(n)return the same connection with another setting.- Every failure is
TcpErr(IOErr), soresult == Err(TcpErr(TimedOut))andErr(TcpErr(ConnectionRefused))can be matched directly. Windows takes about two seconds to report a refused connection, so a shorterconnect_timeoutgivesTimedOutthere.IOErrgainsTimedOut,ConnectionRefused,ConnectionReset,AddrInUseandAddrNotAvailable. - A read that times out consumes nothing. The bytes that had arrived are returned by the next read, so a client that tolerates idle periods just calls
conn.lines()again. - A line longer than the limit is
Err(ReadLimitExceeded(n)); the rest of it is discarded and the next read starts at the next line. listener.accept!({ accept_timeout: NoTimeout, io_timeout: Duration.seconds(30) })separates how long to wait for a client from the timeout that client's connection gets.
Commands: four ways to run, one result, one error
Cmd had seven ways to run a command, which disagreed on whether a non-zero exit was an error. There are now four:
Cmd.new("cargo").args(["build", "--release"]).exec!()? # inherit the terminal; must succeed
sha = Cmd.new("git").args(["rev-parse", "HEAD"]).capture!()?.trim() # stdout as text; must succeed
output = Cmd.new("cat").arg("notes.txt").run!()? # status and output as data
child = Cmd.new("server").stdout(Pipe).spawn!()? # run in the backgroundrun!returns aCmd.Outputwithstatus,stdout_bytesandstderr_bytes, plusrequire_success(),stdout_utf8(),stdout_utf8_lossy()andstderr_utf8_lossy(). It prints its output as text, not as a list of byte numbers.- Every failure is
CmdErr(Cmd.Err):StartFailed(IOErr, program),TimedOut(partial_output),OutputLimitExceeded(partial_output)orIOErr(IOErr). A missing program isErr(CmdErr(StartFailed(NotFound, _))). .timeout(Duration.minutes(5))bounds the whole run, including a spawned child, which is killed at the deadline.- Errors and
to_inspectno longer contain environment values, so a secret passed with.envcannot leak into a log. .stderr(MergeWithStdout)and.output_limit(NoLimit)say directly what used to be encoded in aBoolor a zero.
SQLite: a connection you hold, rows you can stream
decode_todo : Decoder({ id : I64, task : Str }, Sqlite.DecodeErr)
decode_todo = { id: Decoder.i64("id"), task: Decoder.str("task") }.Decoder
db = Sqlite.open!("app.db", {})?
todo = db.query_one!("SELECT id, task FROM todos WHERE id = ${Integer(id)}", decode_todo)?Sqlite.open!returns aDbthat you pass around and canclose!. Open read-only with{ mode: ReadOnly }. A locked database is now waited on (five seconds unless you set `busy_timeo...
0.26.0-rc1
basic-cli 0.26.0-rc1
Release candidate from PR #512,
for testing the MSVC host with Roc nightly-2026-10-11-7d256b9.
The Windows x64 host keeps the MSVC ABI and supplies the startup/runtime inputs
required by this nightly. Native C dependencies use Zig, and generated runtime
and Windows import archives are combined into windows_support.lib. Original
licenses, notices and source/build provenance are included in the bundle.
The producer validates every Windows binary input and uses compiler identities
to invalidate cached native builds when Zig or its adapter changes.
Use this platform URL in your application's header:
https://github.com/roc-lang/basic-cli/releases/download/0.26.0-rc1/GMezHWvmUP42Leqg8YkABvEk27cMUSVM6p7oz3XPj11N.tar.zstWindows execution is tested on Server 2025 CI. Testing on clean Windows 10/11
installations without Visual C++ redistributables is still needed.
0.25.0
What's Changed
- fix example by @Anton-4 in #406
- use shorter ? syntax by @Anton-4 in #408
- add random seed generation by @jfmontanaro in #410
- use setup-roc by @Anton-4 in #411
- better command aliases by @Anton-4 in #412
- Add Tty and Locale modules by @codegod100 in #418
- update bytes for dependabot moderate alert by @Anton-4 in #422
- Bump rustls-webpki from 0.103.7 to 0.103.10 by @dependabot[bot] in #424
- Bump rustls-webpki from 0.103.10 to 0.103.12 by @dependabot[bot] in #425
- Bump rustls-webpki from 0.103.12 to 0.103.13 by @dependabot[bot] in #426
- WIP edits to migrate-zig-compiler branch by @Anton-4 in #423
- Migrate to new Zig compiler by @lukewilliamboswell in #413
- Attach docs before publishing immutable releases by @lukewilliamboswell in #428
- Strip Linux hosts from release bundles by @lukewilliamboswell in #429
- Update examples and docs deployment for 0.21.0-rc4 by @github-actions[bot] in #430
- misc improvements by @Anton-4 in #431
- misc improvements examples by @Anton-4 in #433
Json.ParseErr->InvalidJson(Str)by @niclas-ahden in #434- Test all examples with Valgrind in CI by @lukewilliamboswell in #453
- Document supported targets and host runtime behavior by @lukewilliamboswell in #451
- Represent special filesystem path types by @lukewilliamboswell in #457
- Synchronize Cargo version during releases by @lukewilliamboswell in #450
- Close unused SQLite connections by @lukewilliamboswell in #452
- Rename integer shift built-ins by @niclas-ahden in #458
- Add TCP timeouts and bounded delimiter reads by @lukewilliamboswell in #456
- Update Rust to 1.97.1 by @lukewilliamboswell in #459
- Add Sleep.seconds! by @dmisiuk in #460
- Bump rustls-webpki from 0.103.7 to 0.103.13 by @dependabot[bot] in #463
- Add Path.replace_utf8! by @dmisiuk in #461
- Update sources for 0.21.0 by @github-actions[bot] in #465
- update bytes for dependebot security alert by @Anton-4 in #467
- Re-pin release-package actions to the follow-up PR lookup fix by @Anton-4 in #469
- Include the failing path in Path errors by @dmisiuk in #468
- correct expected output by @Anton-4 in #470
- Add Cmd.check_available! by @dmisiuk in #466
- Fix SQLite binding double free by @lukewilliamboswell in #472
- Normalize and validate host locales by @lukewilliamboswell in #473
- Update sources for 0.22.0 by @github-actions[bot] in #474
- Add Nix Flake devshell + direnv by @thebrandonlucas in #432
- Fix sqlite decoder inference by @Karakatiza666 in #475
- Support ROC override and latest nightly by @lukewilliamboswell in #477
- Update sources for 0.22.1 by @github-actions[bot] in #478
- Return an open error union from Path.to_str by @rtfeldman in #479
- Update sources for 0.22.2 by @github-actions[bot] in #480
- Add OsStr.to_bytes by @Anton-4 in #490
- Add native scripting APIs and managed resource cleanup by @lukewilliamboswell in #491
- Update sources for 0.23.0-rc1 by @github-actions[bot] in #492
- Move program name from
main!args toEnv.program_name!by @ageron in #494 - Prevent hanging on Windows when exiting within 10s of a HTTP request by @niclas-ahden in #493
- update implicit open unions by @Anton-4 in #495
- Prepare release notes for 0.23.0 by @lukewilliamboswell in #496
- Avoid recursive SQLite row decoder check hang by @lukewilliamboswell in #498
- Upgrade Roc nightly to September 23 for 0.23.0 by @lukewilliamboswell in #499
- Add binary file streams, Nix release packages, and bounded release checks by @lukewilliamboswell in #501
- Update sources for 0.23.0 by @github-actions[bot] in #502
- Upgrade to 2026-10-04-nightly by @ageron in #504
- Update sources for 0.24.0 by @github-actions[bot] in #505
- Upgrade to 2026-10-09 nightly by @Anton-4 in #508
- Bump rustls from 0.23.32 to 0.23.45 by @dependabot[bot] in #509
New Contributors
- @jfmontanaro made their first contribution in #410
- @codegod100 made their first contribution in #418
- @github-actions[bot] made their first contribution in #430
- @dmisiuk made their first contribution in #460
- @thebrandonlucas made their first contribution in #432
- @Karakatiza666 made their first contribution in #475
- @ageron made their first contribution in #494
Full Changelog: 0.20.0...0.25.0
Roc API Changes
── ✗ type mismatch ─ ../../../.cache/roc/packages/AEjfyaMFFbh8FJrkkHJy68riVNPr3Qp6c6PawWQjBwMH/Locale.roc:318:13
This expression is used in an unexpected way.
decoded == Err(Json.invalid_json)
^^^^^^^^^^^^^^^^^^^^^^
It has the type:
[Err({} -> [InvalidJson(Str)]), Ok(Locale)]
But you are trying to use it as:
Try(Locale, [InvalidJson(Str)])
── ✗ type mismatch ─ ../../../.cache/roc/packages/AEjfyaMFFbh8FJrkkHJy68riVNPr3Qp6c6PawWQjBwMH/OsStr.roc:257:10
The from_interpolation method on OsStr has an incompatible type.
value = "${name}.toml"
^^^^^^^^^^^^^^
The method from_interpolation has the type:
Str, Iter((Str, Str)) -> OsStr
But I need it to have the type:
List(Str) -> Try(List(b) -> OsStr, [InvalidInterpolation(Str)])
where [b.from_quote : Str -> Try(b, [BadQuotedBytes(Str)])]
Hint: This function expects 1 argument but got 2.
── ✗ type mismatch ─ ../../../.cache/roc/packages/AEjfyaMFFbh8FJrkkHJy68riVNPr3Qp6c6PawWQjBwMH/Path.roc:644:9
The from_interpolation method on Path has an incompatible type.
path = "${directory}/app.toml"
^^^^^^^^^^^^^^^^^^^^^^^
The method from_interpolation has the type:
Str, Iter((Str, Str)) -> Path
But I need it to have the type:
List(Str) -> Try(List(c) -> Path, [InvalidInterpolation(Str)])
where [c.from_quote : Str -> Try(c, [BadQuotedBytes(Str)])]
Hint: This function expects 1 argument but got 2.
── ✗ type mismatch ─ ../../../.cache/roc/packages/AEjfyaMFFbh8FJrkkHJy68riVNPr3Qp6c6PawWQjBwMH/Url.roc:1390:13
This expression is used in an unexpected way.
decoded == Err(Json.invalid_json)
^^^^^^^^^^^^^^^^^^^^^^
It has the type:
[Err({} -> [InvalidJson(Str)]), Ok(Url)]
But you are trying to use it as:
Try(Url, [InvalidJson(Str)])
── ✗ package does not compile ──────────────────────────────────────────────────
roc bump failed.
The old package (/Users/runner/.cache/roc/packages/AEjfyaMFFbh8FJrkkHJy68riVNPr3Qp6c6PawWQjBwMH/main.roc) does not compile with this compiler. roc bump needs both packages to compile so their public APIs can be compared.
roc bump exited with status 1.
Docs
0.24.0
What's Changed
- fix example by @Anton-4 in #406
- use shorter ? syntax by @Anton-4 in #408
- add random seed generation by @jfmontanaro in #410
- use setup-roc by @Anton-4 in #411
- better command aliases by @Anton-4 in #412
- Add Tty and Locale modules by @codegod100 in #418
- update bytes for dependabot moderate alert by @Anton-4 in #422
- Bump rustls-webpki from 0.103.7 to 0.103.10 by @dependabot[bot] in #424
- Bump rustls-webpki from 0.103.10 to 0.103.12 by @dependabot[bot] in #425
- Bump rustls-webpki from 0.103.12 to 0.103.13 by @dependabot[bot] in #426
- WIP edits to migrate-zig-compiler branch by @Anton-4 in #423
- Migrate to new Zig compiler by @lukewilliamboswell in #413
- Attach docs before publishing immutable releases by @lukewilliamboswell in #428
- Strip Linux hosts from release bundles by @lukewilliamboswell in #429
- Update examples and docs deployment for 0.21.0-rc4 by @github-actions[bot] in #430
- misc improvements by @Anton-4 in #431
- misc improvements examples by @Anton-4 in #433
Json.ParseErr->InvalidJson(Str)by @niclas-ahden in #434- Test all examples with Valgrind in CI by @lukewilliamboswell in #453
- Document supported targets and host runtime behavior by @lukewilliamboswell in #451
- Represent special filesystem path types by @lukewilliamboswell in #457
- Synchronize Cargo version during releases by @lukewilliamboswell in #450
- Close unused SQLite connections by @lukewilliamboswell in #452
- Rename integer shift built-ins by @niclas-ahden in #458
- Add TCP timeouts and bounded delimiter reads by @lukewilliamboswell in #456
- Update Rust to 1.97.1 by @lukewilliamboswell in #459
- Add Sleep.seconds! by @dmisiuk in #460
- Bump rustls-webpki from 0.103.7 to 0.103.13 by @dependabot[bot] in #463
- Add Path.replace_utf8! by @dmisiuk in #461
- Update sources for 0.21.0 by @github-actions[bot] in #465
- update bytes for dependebot security alert by @Anton-4 in #467
- Re-pin release-package actions to the follow-up PR lookup fix by @Anton-4 in #469
- Include the failing path in Path errors by @dmisiuk in #468
- correct expected output by @Anton-4 in #470
- Add Cmd.check_available! by @dmisiuk in #466
- Fix SQLite binding double free by @lukewilliamboswell in #472
- Normalize and validate host locales by @lukewilliamboswell in #473
- Update sources for 0.22.0 by @github-actions[bot] in #474
- Add Nix Flake devshell + direnv by @thebrandonlucas in #432
- Fix sqlite decoder inference by @Karakatiza666 in #475
- Support ROC override and latest nightly by @lukewilliamboswell in #477
- Update sources for 0.22.1 by @github-actions[bot] in #478
- Return an open error union from Path.to_str by @rtfeldman in #479
- Update sources for 0.22.2 by @github-actions[bot] in #480
- Add OsStr.to_bytes by @Anton-4 in #490
- Add native scripting APIs and managed resource cleanup by @lukewilliamboswell in #491
- Update sources for 0.23.0-rc1 by @github-actions[bot] in #492
- Move program name from
main!args toEnv.program_name!by @ageron in #494 - Prevent hanging on Windows when exiting within 10s of a HTTP request by @niclas-ahden in #493
- update implicit open unions by @Anton-4 in #495
- Prepare release notes for 0.23.0 by @lukewilliamboswell in #496
- Avoid recursive SQLite row decoder check hang by @lukewilliamboswell in #498
- Upgrade Roc nightly to September 23 for 0.23.0 by @lukewilliamboswell in #499
- Add binary file streams, Nix release packages, and bounded release checks by @lukewilliamboswell in #501
- Update sources for 0.23.0 by @github-actions[bot] in #502
- Upgrade to 2026-10-04-nightly by @ageron in #504
New Contributors
- @jfmontanaro made their first contribution in #410
- @codegod100 made their first contribution in #418
- @github-actions[bot] made their first contribution in #430
- @dmisiuk made their first contribution in #460
- @thebrandonlucas made their first contribution in #432
- @Karakatiza666 made their first contribution in #475
- @ageron made their first contribution in #494
Full Changelog: 0.20.0...0.24.0
Docs
0.23.0
basic-cli 0.23.0
app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.23.0/GNN5tt2gKdX4dhawg4915C4YB193woHFdcCkz31fhGxv.tar.zst" }Requires the Roc new-compiler nightly nightly-2026-09-23-c7852fd or later.
Write the script in Roc, not bash
Most command-line programs eventually need a bash script alongside them. They call mktemp -d to get a scratch directory, cp -r to stage files, timeout 300 to stop a stuck build, and a trap to clean up afterwards. Those scripts are hard to make portable, the errors come back as exit codes and text, and a child process can easily be left running.
0.23.0 gives basic-cli native APIs for this work, so the whole script can be a Roc program. Arguments are passed straight through without shell quoting. Exit codes, timeouts and output are returned as data you can match on. Processes, listeners and temporary directories are cleaned up when your program finishes with them.
Here is a release-staging script that would usually need mktemp, cp -r and a trap:
Env.with_temp_dir!(|workspace| {
staged = Path.join(workspace, "release")
Path.copy_dir!(source, staged)?
Path.write_utf8!(Path.join(staged, "RELEASE.txt"), "Prepared with basic-cli\n")?
Path.copy_dir!(staged, destination)? # fails rather than overwrite an existing release
Ok({})
})?The temporary workspace is deleted whether the callback succeeds or fails. If both the callback and the cleanup fail, you get both errors, not just one. See the full filesystem-tools.roc and process-control.roc examples.
Run and supervise processes
Cmd now covers what bash's timeout, tee and & are usually used for:
output = Cmd.new("cargo")
.args(["build", "--release"])
.stdout(Tee) # stream to the terminal *and* capture
.stderr(Tee)
.timeout_ms(300_000) # hard deadline, including draining output
.manage_tree(Bool.True) # on timeout, kill grandchildren too
.run!()?
match output.status {
Exited(0) => Stdout.line!("Build succeeded")
Exited(code) => Err(BuildFailed(code))
Signaled(signal) => Err(BuildKilled(signal))
}Cmd.run!runs a command to completion. A nonzero exit is returned as a status for you to handle, not as an error. Errors are reserved for failures of the command itself:IO,Timeout(partial_output)andOutputLimit(partial_output). Both limit errors include the output collected before the command was stopped.Cmd.spawn!starts a process in the background and returns aChildhandle. Usepid!,wait!,try_wait!,kill!andclose!to manage it. To interact with it, set.stdin(Pipe)and.stdout(Pipe), then usewrite!,read!andclose_stdin!.- New builder options:
cwd,stdin,stdoutandstderr(Inherit,Null,Capture,Pipe,Tee, andBytesfor stdin),timeout_ms,output_limit,pending_limit,manage_treeandmerge_stderr. - Cleanup is automatic. When your program drops its last reference to a
Child, the process is terminated and reaped. Callclose!if you need this to happen at a specific point.
The existing exec!, exec_cmd!, exec_output! and exec_output_bytes! helpers now run on the same process engine.
Files, paths and temporary workspaces
Path.copy!copies a file, including its permissions.Path.copy_dir!copies a directory tree to a destination that must not exist yet.Path.copy_dir_with!lets you choose whether symlinks are preserved or followed, and whether an existing destination is merged into. Copies that would loop, or where the source and destination overlap, are rejected.Path.absolute!makes a path absolute without touching the filesystem.Path.canonicalize!resolves an existing path, including symlinks.Env.create_temp_dir!,Env.create_temp_dir_with_prefix!andEnv.create_temp_dir_in!create private temporary directories.Env.with_temp_dir!also deletes the directory when your callback finishes.OsStr.to_bytesreturns the raw native bytes of an OS string.
Servers and timing
- Accept TCP connections.
Tcp.listen!(host, port, timeout_ms)returns aTcp.Listenerwithaccept!,local_port!andclose!methods. Listen on port0and readlocal_port!to have the OS pick a free port. This is useful for tests and local helper servers. - Measure elapsed time.
Monotonic.now!returns nanoseconds (U64) from a clock that system clock changes don't affect. Subtract two readings to get a duration. The values are only meaningful within a single run of your program, so don't store them.
Upgrading from 0.22
main! no longer receives the program name. args now contains only the arguments the user passed, which matches Roc's default platform (#494). Code that skipped the first element will now silently drop a real argument, so check every place that handles args:
# 0.22
main! = |args| {
user_args = args.drop_first(1)
...
}
# 0.23
main! = |args| {
# args are already the user's arguments.
# Program name, if needed:
name = Env.program_name!()? # Err(ProgramNameUnavailable) if the launcher didn't supply one
...
}Remove .. from error types in return position. Roc now makes tag unions in return position open automatically, so basic-cli's signatures are now written without .. (for example, Path.read_bytes! : Path => Try(List(U8), [PathErr(IOErr, Path)])), and yours can be too (#495). This change is why 0.23.0 needs the nightly listed at the top.
Captured output is capped at 16 MiB by default. When stdout and stderr together exceed the limit, the command is cancelled. run! returns OutputLimit(partial_output), and the exec_output* helpers return FailedToGetExitCode with the message "Process output limit exceeded". If a command legitimately produces more output, raise the limit with .output_limit(bytes), or use .stdout(Inherit) to send the output straight to the terminal.
Update the platform URL. The native host interface changed to support managed resources, so programs built against 0.22 need to be rebuilt against 0.23.0. Mixing hosts and platform sources from different versions is not supported.
Fixes
- Windows: programs no longer hang on exit if they made an HTTP request in the previous ten seconds (#493).
- File readers, TCP streams and SQLite statements now release their OS resources as soon as the last reference to them is dropped, instead of at program exit.
- SQLite: fixed a type-inference problem in the row decoders that could cause errors in application code using them. Columns are now looked up by name (#475, 0.22.1).
Path.to_strerrors now combine with other errors in the same function when you use?(#479, 0.22.2). With the implicit-open change above, every basic-cli function now behaves this way.
Documentation and tooling
- The API docs have been revised, with runnable examples for the new process, filesystem and TCP APIs.
- Contributors can use the new Nix flake devshell with direnv (#432), and point the test runner at a specific compiler with
ROC=...(#477).
Thanks
Thanks to everyone who contributed to 0.22.1 through 0.23.0: @ageron, @Anton-4, @Karakatiza666, @lukewilliamboswell, @niclas-ahden, @rtfeldman and @thebrandonlucas.
0.23.0-rc1
What's Changed
- Update examples and docs deployment for 0.21.0-rc4 by @github-actions[bot] in #430
- misc improvements by @Anton-4 in #431
- misc improvements examples by @Anton-4 in #433
Json.ParseErr->InvalidJson(Str)by @niclas-ahden in #434- Test all examples with Valgrind in CI by @lukewilliamboswell in #453
- Document supported targets and host runtime behavior by @lukewilliamboswell in #451
- Represent special filesystem path types by @lukewilliamboswell in #457
- Synchronize Cargo version during releases by @lukewilliamboswell in #450
- Close unused SQLite connections by @lukewilliamboswell in #452
- Rename integer shift built-ins by @niclas-ahden in #458
- Add TCP timeouts and bounded delimiter reads by @lukewilliamboswell in #456
- Update Rust to 1.97.1 by @lukewilliamboswell in #459
- Add Sleep.seconds! by @dmisiuk in #460
- Bump rustls-webpki from 0.103.7 to 0.103.13 by @dependabot[bot] in #463
- Add Path.replace_utf8! by @dmisiuk in #461
- Update sources for 0.21.0 by @github-actions[bot] in #465
- update bytes for dependebot security alert by @Anton-4 in #467
- Re-pin release-package actions to the follow-up PR lookup fix by @Anton-4 in #469
- Include the failing path in Path errors by @dmisiuk in #468
- correct expected output by @Anton-4 in #470
- Add Cmd.check_available! by @dmisiuk in #466
- Fix SQLite binding double free by @lukewilliamboswell in #472
- Normalize and validate host locales by @lukewilliamboswell in #473
- Update sources for 0.22.0 by @github-actions[bot] in #474
- Add Nix Flake devshell + direnv by @thebrandonlucas in #432
- Fix sqlite decoder inference by @Karakatiza666 in #475
- Support ROC override and latest nightly by @lukewilliamboswell in #477
- Update sources for 0.22.1 by @github-actions[bot] in #478
- Return an open error union from Path.to_str by @rtfeldman in #479
- Update sources for 0.22.2 by @github-actions[bot] in #480
- Add OsStr.to_bytes by @Anton-4 in #490
- Add native scripting APIs and managed resource cleanup by @lukewilliamboswell in #491
New Contributors
- @github-actions[bot] made their first contribution in #430
- @dmisiuk made their first contribution in #460
- @thebrandonlucas made their first contribution in #432
- @Karakatiza666 made their first contribution in #475
Full Changelog: 0.21.0-rc4...0.23.0-rc1
Roc API Changes
── ✗ cannot extract public api ─────────────────────────────────────────────────
roc bump failed.
The old package's public API references the type `InternalHttp.TransportErr`, which is not itself part of the exposed API.
Docs
0.22.2
What's Changed
- fix example by @Anton-4 in #406
- use shorter ? syntax by @Anton-4 in #408
- add random seed generation by @jfmontanaro in #410
- use setup-roc by @Anton-4 in #411
- better command aliases by @Anton-4 in #412
- Add Tty and Locale modules by @codegod100 in #418
- update bytes for dependabot moderate alert by @Anton-4 in #422
- Bump rustls-webpki from 0.103.7 to 0.103.10 by @dependabot[bot] in #424
- Bump rustls-webpki from 0.103.10 to 0.103.12 by @dependabot[bot] in #425
- Bump rustls-webpki from 0.103.12 to 0.103.13 by @dependabot[bot] in #426
- WIP edits to migrate-zig-compiler branch by @Anton-4 in #423
- Migrate to new Zig compiler by @lukewilliamboswell in #413
- Attach docs before publishing immutable releases by @lukewilliamboswell in #428
- Strip Linux hosts from release bundles by @lukewilliamboswell in #429
- Update examples and docs deployment for 0.21.0-rc4 by @github-actions[bot] in #430
- misc improvements by @Anton-4 in #431
- misc improvements examples by @Anton-4 in #433
Json.ParseErr->InvalidJson(Str)by @niclas-ahden in #434- Test all examples with Valgrind in CI by @lukewilliamboswell in #453
- Document supported targets and host runtime behavior by @lukewilliamboswell in #451
- Represent special filesystem path types by @lukewilliamboswell in #457
- Synchronize Cargo version during releases by @lukewilliamboswell in #450
- Close unused SQLite connections by @lukewilliamboswell in #452
- Rename integer shift built-ins by @niclas-ahden in #458
- Add TCP timeouts and bounded delimiter reads by @lukewilliamboswell in #456
- Update Rust to 1.97.1 by @lukewilliamboswell in #459
- Add Sleep.seconds! by @dmisiuk in #460
- Bump rustls-webpki from 0.103.7 to 0.103.13 by @dependabot[bot] in #463
- Add Path.replace_utf8! by @dmisiuk in #461
- Update sources for 0.21.0 by @github-actions[bot] in #465
- update bytes for dependebot security alert by @Anton-4 in #467
- Re-pin release-package actions to the follow-up PR lookup fix by @Anton-4 in #469
- Include the failing path in Path errors by @dmisiuk in #468
- correct expected output by @Anton-4 in #470
- Add Cmd.check_available! by @dmisiuk in #466
- Fix SQLite binding double free by @lukewilliamboswell in #472
- Normalize and validate host locales by @lukewilliamboswell in #473
- Update sources for 0.22.0 by @github-actions[bot] in #474
- Add Nix Flake devshell + direnv by @thebrandonlucas in #432
- Fix sqlite decoder inference by @Karakatiza666 in #475
- Support ROC override and latest nightly by @lukewilliamboswell in #477
- Update sources for 0.22.1 by @github-actions[bot] in #478
- Return an open error union from Path.to_str by @rtfeldman in #479
New Contributors
- @jfmontanaro made their first contribution in #410
- @codegod100 made their first contribution in #418
- @github-actions[bot] made their first contribution in #430
- @dmisiuk made their first contribution in #460
- @thebrandonlucas made their first contribution in #432
- @Karakatiza666 made their first contribution in #475
Full Changelog: 0.20.0...0.22.2
Roc API Changes
── ✗ cannot extract public api ─────────────────────────────────────────────────
roc bump failed.
The old package's public API references the type `InternalHttp.TransportErr`, which is not itself part of the exposed API.
Docs
0.22.1
What's Changed
- fix example by @Anton-4 in #406
- use shorter ? syntax by @Anton-4 in #408
- add random seed generation by @jfmontanaro in #410
- use setup-roc by @Anton-4 in #411
- better command aliases by @Anton-4 in #412
- Add Tty and Locale modules by @codegod100 in #418
- update bytes for dependabot moderate alert by @Anton-4 in #422
- Bump rustls-webpki from 0.103.7 to 0.103.10 by @dependabot[bot] in #424
- Bump rustls-webpki from 0.103.10 to 0.103.12 by @dependabot[bot] in #425
- Bump rustls-webpki from 0.103.12 to 0.103.13 by @dependabot[bot] in #426
- WIP edits to migrate-zig-compiler branch by @Anton-4 in #423
- Migrate to new Zig compiler by @lukewilliamboswell in #413
- Attach docs before publishing immutable releases by @lukewilliamboswell in #428
- Strip Linux hosts from release bundles by @lukewilliamboswell in #429
- Update examples and docs deployment for 0.21.0-rc4 by @github-actions[bot] in #430
- misc improvements by @Anton-4 in #431
- misc improvements examples by @Anton-4 in #433
Json.ParseErr->InvalidJson(Str)by @niclas-ahden in #434- Test all examples with Valgrind in CI by @lukewilliamboswell in #453
- Document supported targets and host runtime behavior by @lukewilliamboswell in #451
- Represent special filesystem path types by @lukewilliamboswell in #457
- Synchronize Cargo version during releases by @lukewilliamboswell in #450
- Close unused SQLite connections by @lukewilliamboswell in #452
- Rename integer shift built-ins by @niclas-ahden in #458
- Add TCP timeouts and bounded delimiter reads by @lukewilliamboswell in #456
- Update Rust to 1.97.1 by @lukewilliamboswell in #459
- Add Sleep.seconds! by @dmisiuk in #460
- Bump rustls-webpki from 0.103.7 to 0.103.13 by @dependabot[bot] in #463
- Add Path.replace_utf8! by @dmisiuk in #461
- Update sources for 0.21.0 by @github-actions[bot] in #465
- update bytes for dependebot security alert by @Anton-4 in #467
- Re-pin release-package actions to the follow-up PR lookup fix by @Anton-4 in #469
- Include the failing path in Path errors by @dmisiuk in #468
- correct expected output by @Anton-4 in #470
- Add Cmd.check_available! by @dmisiuk in #466
- Fix SQLite binding double free by @lukewilliamboswell in #472
- Normalize and validate host locales by @lukewilliamboswell in #473
- Update sources for 0.22.0 by @github-actions[bot] in #474
- Add Nix Flake devshell + direnv by @thebrandonlucas in #432
- Fix sqlite decoder inference by @Karakatiza666 in #475
- Support ROC override and latest nightly by @lukewilliamboswell in #477
New Contributors
- @jfmontanaro made their first contribution in #410
- @codegod100 made their first contribution in #418
- @github-actions[bot] made their first contribution in #430
- @dmisiuk made their first contribution in #460
- @thebrandonlucas made their first contribution in #432
- @Karakatiza666 made their first contribution in #475
Full Changelog: 0.20.0...0.22.1
Roc API Changes
── ✗ cannot extract public api ─────────────────────────────────────────────────
roc bump failed.
The old package's public API references the type `InternalHttp.TransportErr`, which is not itself part of the exposed API.