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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable user-visible changes are recorded here. Versions follow [Semantic Ve

## Unreleased

## [0.12.1] — 2026-09-12

### Added

- Vault management on the Account page: what the vault is, the session
Expand All @@ -15,12 +17,20 @@ All notable user-visible changes are recorded here. Versions follow [Semantic Ve

### Changed

- Give the accounts app a clear primary position in the landing-page header
and hero now that browser-started sessions are available.
- Searching and exporting the audit log run in the browser, on the decrypted
entries.
- The service refuses unencrypted input entries for the audit log.

### Fixed

- Accept `--auto-close today`, which the CLI reference documents but which
never worked: it meant midnight, so it had already passed whenever the
command ran. It now means the end of today.
- Say that an unquoted `--auto-close` date has passed, instead of calling it
invalid. `--auto-close=2020-01-01` already said so; `--auto-close 2020-01-01`
reported a grammar error for a value it had understood perfectly well.
- The sign-up page said typed input was not recorded. It is recorded, now
end-to-end encrypted for your team.

Expand Down
Binary file modified app/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 1 addition & 15 deletions app/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/public/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 1 addition & 7 deletions app/public/icons/claude-code.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/public/icons/hermes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/public/icons/openclaw.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified app/public/icons/terminal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 23 additions & 1 deletion cmd/shell/autoclose.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,18 @@ func normalizeAutoCloseArguments(arguments []string, now time.Time) ([]string, e
}

lastValid := -1
// Why the first token's error is kept: when no prefix parses, the
// value the user actually wrote is the single token, and its own
// error says what is wrong with it. Reporting "invalid" for all of
// them turns "that time has passed" into a grammar complaint.
var firstError error
for end := index + 1; end < len(arguments); end++ {
candidate := strings.Join(arguments[index+1:end+1], " ")
if _, err := parseCloseDeadline(candidate, now); err == nil {
lastValid = end
continue
} else if end == index+1 {
firstError = err
}
if lastValid >= 0 {
break
Expand All @@ -83,6 +90,9 @@ func normalizeAutoCloseArguments(arguments []string, now time.Time) ([]string, e
break
}
if lastValid < 0 {
if firstError != nil {
return nil, firstError
}
return nil, fmt.Errorf("invalid auto-close value %q", arguments[index+1])
}
normalized = append(normalized, "--auto-close="+strings.Join(arguments[index+1:lastValid+1], " "))
Expand Down Expand Up @@ -197,15 +207,27 @@ func parseAbsoluteDeadline(value string, now time.Time) (time.Time, bool) {
if prefix == "tomorrow" {
day = day.AddDate(0, 0, 1)
}
// A bare day keyword carries no time of day, so it has to stand
// for one. "tomorrow" takes the start of that day, which is still
// ahead. "today" cannot: midnight has already passed whenever the
// command runs, so the only reading of "close this today" that is
// ever a deadline is the end of it. The last second rather than
// the last minute, so that the form still works during 23:59.
clock := "00:00"
second := 0
if prefix == "today" {
clock = "23:59"
second = 59
}
if len(value) > len(prefix) {
clock = strings.TrimSpace(value[len(prefix):])
second = 0
}
parsedClock, err := time.ParseInLocation("15:04", clock, now.Location())
if err != nil {
return time.Time{}, false
}
return time.Date(day.Year(), day.Month(), day.Day(), parsedClock.Hour(), parsedClock.Minute(), 0, 0, now.Location()), true
return time.Date(day.Year(), day.Month(), day.Day(), parsedClock.Hour(), parsedClock.Minute(), second, 0, now.Location()), true
}
}

Expand Down
39 changes: 39 additions & 0 deletions cmd/shell/autoclose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"reflect"
"strings"
"testing"
"time"
)
Expand All @@ -24,6 +25,9 @@ func TestParseCloseDeadline(t *testing.T) {
{name: "years", value: "1y", want: now.AddDate(1, 0, 0)},
{name: "in prefix", value: "in 15m", want: now.Add(15 * time.Minute)},
{name: "tomorrow", value: "tomorrow 09:15", want: time.Date(2026, time.August, 20, 9, 15, 0, 0, location)},
{name: "bare tomorrow", value: "tomorrow", want: time.Date(2026, time.August, 20, 0, 0, 0, 0, location)},
{name: "bare today", value: "today", want: time.Date(2026, time.August, 19, 23, 59, 59, 0, location)},
{name: "today with clock", value: "today 23:50", want: time.Date(2026, time.August, 19, 23, 50, 0, 0, location)},
{name: "clock rolls forward", value: "22:00", want: time.Date(2026, time.August, 20, 22, 0, 0, 0, location)},
{name: "local date", value: "2026-08-21 12:30", want: time.Date(2026, time.August, 21, 12, 30, 0, 0, location)},
}
Expand Down Expand Up @@ -86,6 +90,7 @@ func TestNormalizeAutoCloseArguments(t *testing.T) {
}{
{input: []string{"--auto-close", "5m", "sleep", "30"}, want: []string{"--auto-close=5m", "sleep", "30"}},
{input: []string{"--auto-close", "tomorrow", "09:00", "sleep", "30"}, want: []string{"--auto-close=tomorrow 09:00", "sleep", "30"}},
{input: []string{"--auto-close", "today", "sleep", "30"}, want: []string{"--auto-close=today", "sleep", "30"}},
{input: []string{"--auto-close", "in", "15m", "sleep", "30"}, want: []string{"--auto-close=in 15m", "sleep", "30"}},
{input: []string{"--auto-close", "2d", "3h", "sleep", "30"}, want: []string{"--auto-close=2d 3h", "sleep", "30"}},
{input: []string{"--auto-close=2h", "echo"}, want: []string{"--auto-close=2h", "echo"}},
Expand Down Expand Up @@ -116,3 +121,37 @@ func TestNormalizeAutoCloseArgumentsRejectsMissingAndInvalidValues(t *testing.T)
}
}
}

// A bare "today" is only useful if it is a deadline at every hour it can be
// typed, including during the last minute of the day.
func TestBareTodayIsADeadlineAllDay(t *testing.T) {
location := time.FixedZone("test", 2*60*60)
for _, clock := range []struct{ hour, minute, second int }{
{0, 0, 0}, {9, 15, 0}, {23, 58, 0}, {23, 59, 30},
} {
now := time.Date(2026, time.August, 19, clock.hour, clock.minute, clock.second, 0, location)
deadline, err := parseCloseDeadline("today", now)
if err != nil {
t.Fatalf("parseCloseDeadline(\"today\") at %v: %v", now, err)
}
if !deadline.After(now) {
t.Fatalf("parseCloseDeadline(\"today\") at %v = %v, want a future deadline", now, deadline)
}
if deadline.Day() != now.Day() {
t.Fatalf("parseCloseDeadline(\"today\") at %v = %v, want the same day", now, deadline)
}
}
}

// A date that has passed is a different mistake from one that cannot be read,
// and the unquoted form used to report both as "invalid".
func TestNormalizeAutoCloseArgumentsReportsPastDeadlines(t *testing.T) {
now := time.Date(2026, time.August, 19, 23, 40, 0, 0, time.UTC)
_, err := normalizeAutoCloseArguments([]string{"--auto-close", "2020-01-01", "sleep", "30"}, now)
if err == nil {
t.Fatal("normalizeAutoCloseArguments unexpectedly accepted a past date")
}
if !strings.Contains(err.Error(), "must be in the future") {
t.Fatalf("normalizeAutoCloseArguments error = %q, want it to say the deadline has passed", err)
}
}
3 changes: 2 additions & 1 deletion cmd/shell/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,8 @@ START OPTIONS
--auto-close <duration-or-date>
Always close when the task exits; optionally add an earlier deadline.
Units: ms, s, m, h, d, w, mo, y. Units may be combined, such as 1h30m.
Dates: RFC3339, YYYY-MM-DD[ HH:MM[:SS]], HH:MM, today, or tomorrow HH:MM.
Dates: RFC3339, YYYY-MM-DD[ HH:MM[:SS]], HH:MM, or today/tomorrow [HH:MM].
Bare today means the end of today; bare tomorrow means 00:00 tomorrow.
Multi-token dates may be written directly, for example: --auto-close tomorrow 09:00.
A missing or invalid value returns status 2 and never becomes the command.
--json
Expand Down
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
services:
shell-online:
image: ghcr.io/teoslayer/shell.online:0.12.0
image: ghcr.io/teoslayer/shell.online:0.12.1
build:
context: .
args:
VERSION: "0.12.0"
VERSION: "0.12.1"
restart: unless-stopped
environment:
SHELL_ONLINE_E2EE_PASSWORD: "${SHELL_ONLINE_E2EE_PASSWORD:-}"
Expand Down
4 changes: 2 additions & 2 deletions docs/content.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.12.0",
"version": "0.12.1",
"pages": {
"docs": {
"eyebrow": "Documentation",
Expand Down Expand Up @@ -553,7 +553,7 @@
"cards": [
[
"Start once",
"Pull ghcr.io/teoslayer/shell.online:0.12.0 or run docker compose up --build -d, then docker compose logs shell-online. First launch prints the stable URL and a generated eight-character password. The tagged amd64/arm64 image includes an SBOM and build provenance."
"Pull ghcr.io/teoslayer/shell.online:0.12.1 or run docker compose up --build -d, then docker compose logs shell-online. First launch prints the stable URL and a generated eight-character password. The tagged amd64/arm64 image includes an SBOM and build provenance."
],
[
"Two durable volumes",
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
},
"applicationCategory": "DeveloperApplication",
"operatingSystem": "macOS, Windows, Linux, FreeBSD, OpenBSD, NetBSD, DragonFly BSD, Solaris",
"softwareVersion": "0.12.0",
"softwareVersion": "0.12.1",
"softwareRequirements": "A supported architecture, outbound HTTPS and WebSockets, and a PTY or Windows ConPTY",
"isAccessibleForFree": true,
"downloadUrl": "https://shell.online/install",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "shell-online",
"version": "0.12.0",
"version": "0.12.1",
"description": "Turn any terminal process into a collaborative browser link.",
"private": true,
"license": "MIT",
Expand Down
Loading
Loading