feat: update changelog (#131)
closes #130 - CHANGELOG.md file for people who can view changelog from github --- `iris changelog` command: `iris changelog [version]` (e.g. iris changelog v0.5.1) `iris changelog -n 3` (see changelog of 3 latest releaseas) => `iris changelog` looks just like CHANGELOG.md, grouped headers and all. It only stops looking that way if we personally overwrite that release's description with something else => With 1-hour local cache (invalidated on updater-channel change), `--refresh` flag to bypass, graceful fallback to stale cache on GitHub rate-limiting --- - New auto update config ```toml # 0 = off (default, notify only), 1 = auto-install, 2 = always confirm first auto-update = 1 ``` - New just debug-changelog and just debug-autoupdate recipes (fully offline, mocked HTTP servers, no real network/config touched)
This commit is contained in:
@@ -33,8 +33,10 @@ jobs:
|
|||||||
IFS='.' read -r major minor patch <<< "$VERSION_NUM"
|
IFS='.' read -r major minor patch <<< "$VERSION_NUM"
|
||||||
patch=$((patch + 1))
|
patch=$((patch + 1))
|
||||||
NEXT_BASE="v${major}.${minor}.${patch}"
|
NEXT_BASE="v${major}.${minor}.${patch}"
|
||||||
SHORT=$(git rev-parse --short HEAD)
|
SHORT=$(git rev-parse --short=6 HEAD)
|
||||||
echo "version=${NEXT_BASE}-nightly.${SHORT}" >> $GITHUB_OUTPUT
|
echo "version=${NEXT_BASE}-nightly.${SHORT}" >> $GITHUB_OUTPUT
|
||||||
|
echo "short_sha=${SHORT}" >> $GITHUB_OUTPUT
|
||||||
|
echo "subject=$(git log -1 --format=%s)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build Binaries
|
- name: Build Binaries
|
||||||
run: |
|
run: |
|
||||||
@@ -71,12 +73,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.version.outputs.version }}
|
tag_name: ${{ steps.version.outputs.version }}
|
||||||
name: ${{ steps.version.outputs.version }}
|
name: ${{ steps.version.outputs.version }}
|
||||||
|
# shaped like GoReleaser's changelog groups so `iris changelog` renders it
|
||||||
body: |
|
body: |
|
||||||
# 🌙 **Nightly build** - `${{ steps.version.outputs.version }}`
|
# 🌙 **Nightly build** - `${{ steps.version.outputs.version }}`
|
||||||
Nightly release from commit: ${{ github.sha }}
|
|
||||||
|
|
||||||
# Update:
|
### Nightly
|
||||||
`iris update`
|
|
||||||
|
* ${{ steps.version.outputs.short_sha }} ${{ steps.version.outputs.subject }}
|
||||||
|
|
||||||
|
## Update
|
||||||
|
```bash
|
||||||
|
iris update
|
||||||
|
```
|
||||||
|
|
||||||
> install manually, this build will **not** trigger an update notification
|
> install manually, this build will **not** trigger an update notification
|
||||||
files: dist/*.tar.gz
|
files: dist/*.tar.gz
|
||||||
|
|||||||
@@ -62,3 +62,30 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# nightly tags also match refs/tags/v*, so they're excluded below
|
||||||
|
- name: Generate CHANGELOG.md
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-nightly.')
|
||||||
|
uses: orhun/git-cliff-action@v4
|
||||||
|
with:
|
||||||
|
config: cliff.toml
|
||||||
|
args: --tag ${{ github.ref_name }} --output /tmp/CHANGELOG.md
|
||||||
|
|
||||||
|
- name: Commit CHANGELOG.md
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-nightly.')
|
||||||
|
run: |
|
||||||
|
# avoids a fast-forward race against the tag's detached HEAD
|
||||||
|
git fetch origin main
|
||||||
|
git checkout -B main origin/main
|
||||||
|
cp /tmp/CHANGELOG.md CHANGELOG.md
|
||||||
|
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add CHANGELOG.md
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "CHANGELOG.md unchanged, nothing to commit"
|
||||||
|
else
|
||||||
|
# [skip ci] avoids retriggering nightly.yml for a docs-only push
|
||||||
|
git commit -m "docs(changelog): update for ${GITHUB_REF_NAME} [skip ci]"
|
||||||
|
git push origin main
|
||||||
|
fi
|
||||||
|
|||||||
@@ -14,6 +14,35 @@ git:
|
|||||||
# already has them and fails with 422 already_exists
|
# already has them and fails with 422 already_exists
|
||||||
prerelease_suffix: "-nightly"
|
prerelease_suffix: "-nightly"
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
use: git
|
||||||
|
sort: asc
|
||||||
|
abbrev: 6
|
||||||
|
format: "{{ .SHA }} {{ .Message }}"
|
||||||
|
filters:
|
||||||
|
# these would otherwise fall under the "Bug fixes" group below since
|
||||||
|
# they start with `fix`, but they're infra-only and not user-facing
|
||||||
|
exclude:
|
||||||
|
- "(?i)^fix\\(ci\\)"
|
||||||
|
- "(?i)^fix\\(release\\)"
|
||||||
|
groups:
|
||||||
|
- title: "Features"
|
||||||
|
regexp: '(?i)^.*?feat(\(.+\))?!?:.+$'
|
||||||
|
order: 0
|
||||||
|
- title: "Bug fixes"
|
||||||
|
regexp: '(?i)^.*?fix(\(.+\))?!?:.+$'
|
||||||
|
order: 1
|
||||||
|
- title: "Performance"
|
||||||
|
regexp: '(?i)^.*?perf(\(.+\))?!?:.+$'
|
||||||
|
order: 2
|
||||||
|
- title: "Refactors"
|
||||||
|
regexp: '(?i)^.*?refactor(\(.+\))?!?:.+$'
|
||||||
|
order: 3
|
||||||
|
- title: "Documentation"
|
||||||
|
regexp: '(?i)^.*?docs?(\(.+\))?!?:.+$'
|
||||||
|
order: 4
|
||||||
|
# unmatched types (chore, build, ci, test, ...) are silently dropped
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- env:
|
- env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
|
|||||||
+428
@@ -0,0 +1,428 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project are documented in this file.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Follow the shell working directory in the wrapper process ([#129](https://github.com/versenilvis/iris/issues/129)) ([4a4ab3](https://github.com/versenilvis/iris/commit/4a4ab3c102067f7a24f737987e80fbb7cedbe86c))
|
||||||
|
- Fall back to raw body for nightly releases ([005ed5](https://github.com/versenilvis/iris/commit/005ed5ae469953eec4d20d91fe647cb42151b98b))
|
||||||
|
- Invalidate cache when the updater channel changes ([5845fc](https://github.com/versenilvis/iris/commit/5845fcb0511b3a6b6bfae2cadba3735e244e3331))
|
||||||
|
- Only strip GoReleaser's exact heading, not real content ([a8779d](https://github.com/versenilvis/iris/commit/a8779d56e67beb859caaee3af7371bbdb829ef77))
|
||||||
|
- Use full commit SHAs in CHANGELOG.md for GitHub autolinking ([4d5e86](https://github.com/versenilvis/iris/commit/4d5e86ac9d142c38be7c9231f6576cc73371a73d))
|
||||||
|
- Link commit SHAs and PR numbers in CHANGELOG.md ([4416d1](https://github.com/versenilvis/iris/commit/4416d16fe6c216950d25856d212a9c8d3af8d0bf))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Document changelog and auto-update ([80ee17](https://github.com/versenilvis/iris/commit/80ee178932a7d2b1b75dd516e9b95aee035c05b5))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Generate CHANGELOG.md with git-cliff ([6f9a2b](https://github.com/versenilvis/iris/commit/6f9a2b9500535a70f08b2bd47949d9e560122013))
|
||||||
|
- Group release notes by commit type ([0421ed](https://github.com/versenilvis/iris/commit/0421ed853b79759ba6e215a95288c9f5a72ef9c3))
|
||||||
|
- Add changelog command ([5745dc](https://github.com/versenilvis/iris/commit/5745dc7ca46872076d3f9369d85fb3743db856e5))
|
||||||
|
- Show changelog in update notification ([27e745](https://github.com/versenilvis/iris/commit/27e7453ab27998c0ffd643cb8821c0a56c998989))
|
||||||
|
- Add auto-update option ([db2055](https://github.com/versenilvis/iris/commit/db2055a4c3e50a11099a5a32c881c5241014bfc9))
|
||||||
|
- Auto install updates in background ([359480](https://github.com/versenilvis/iris/commit/359480d77c065f783761ee86e988eaee58c1ec31))
|
||||||
|
- Shape nightly changelog like stable release groups ([ae39b4](https://github.com/versenilvis/iris/commit/ae39b4731f381d2e5f192b805e49a57f463f38e7))
|
||||||
|
- Render release notes with glamour ([8b2dba](https://github.com/versenilvis/iris/commit/8b2dba99fa0022016d7d731ef617fb2aaa1868f8))
|
||||||
|
- Compact rendering and per-version lookup ([445553](https://github.com/versenilvis/iris/commit/445553b15180dff0af5113e8a89be55442f54d64))
|
||||||
|
|
||||||
|
## [v0.5.2](https://github.com/versenilvis/iris/releases/tag/v0.5.2) - 2026-08-06
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Start iris in multiplexer panes instead of inheriting a dead one ([#120](https://github.com/versenilvis/iris/issues/120)) ([fc1634](https://github.com/versenilvis/iris/commit/fc1634b00799b2c88ef8f02710da1e5e39787e7d))
|
||||||
|
|
||||||
|
## [v0.5.1](https://github.com/versenilvis/iris/releases/tag/v0.5.1) - 2026-08-06
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Mode doesn't work correctly ([#128](https://github.com/versenilvis/iris/issues/128)) ([7a97ea](https://github.com/versenilvis/iris/commit/7a97ea9016a40693f89115be089b4b8e6ad58c2d))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Remove ⌘ ([ce4043](https://github.com/versenilvis/iris/commit/ce404335ed8114c64a2d7068dcd1ea2437a751d2))
|
||||||
|
|
||||||
|
## [v0.5.0](https://github.com/versenilvis/iris/releases/tag/v0.5.0) - 2026-08-06
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Update stale vendorHash after lipgloss v2 upgrade ([#99](https://github.com/versenilvis/iris/issues/99)) ([476ca3](https://github.com/versenilvis/iris/commit/476ca3946b5e129471663a9269d1481b22fd4963))
|
||||||
|
- Support kitty keyboard protocol for ctrl keybindings ([#101](https://github.com/versenilvis/iris/issues/101)) ([d669e9](https://github.com/versenilvis/iris/commit/d669e97423a7ca9326d17b1289d06ba90942bd77))
|
||||||
|
- Isolate __complete probe from the controlling terminal ([#111](https://github.com/versenilvis/iris/issues/111)) ([0b77c9](https://github.com/versenilvis/iris/commit/0b77c9334b218eb1d7249983ceae424904ebe071))
|
||||||
|
- Make Right Arrow configurable and correct overlay positioning ([#110](https://github.com/versenilvis/iris/issues/110)) ([dfaa05](https://github.com/versenilvis/iris/commit/dfaa051f31671eaf45b0bd0bc5a2694df00e3db0))
|
||||||
|
- Handle non-ascii characters command ([#118](https://github.com/versenilvis/iris/issues/118)) ([75aa1f](https://github.com/versenilvis/iris/commit/75aa1fbe383c05e33ca10c13798225af2c23cdf3))
|
||||||
|
- Update nightly releases ([#125](https://github.com/versenilvis/iris/issues/125)) ([0595dd](https://github.com/versenilvis/iris/commit/0595ddd4b903639464b6248be60de3c76ed77738))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Add navigate-right config ([b79d25](https://github.com/versenilvis/iris/commit/b79d25a0827805a5eaed12ca56b022e5e9e97969))
|
||||||
|
- Theme config and list of theme templates ([#116](https://github.com/versenilvis/iris/issues/116)) ([b5734b](https://github.com/versenilvis/iris/commit/b5734b4e85dfb3cf6654300b42663adbb6ace231))
|
||||||
|
- Add themes directory reference ([3a97ef](https://github.com/versenilvis/iris/commit/3a97efad183a59d46dbd3f7b7aa2f64a4e2eb118))
|
||||||
|
- AI guide ([#117](https://github.com/versenilvis/iris/issues/117)) ([30619a](https://github.com/versenilvis/iris/commit/30619a862182371d3654989cc4d273eba381f0f5))
|
||||||
|
- Add auto-start setup instructions ([53f104](https://github.com/versenilvis/iris/commit/53f104577f5b7f5a0c398b061b03e77f74c0f71a))
|
||||||
|
- Small note in shell config and fix line space ([994ff8](https://github.com/versenilvis/iris/commit/994ff836752bf4c5246a3aec236dd597421dccf0))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Adopt x/ansi instead of hand rolling escape sequences ([#96](https://github.com/versenilvis/iris/issues/96)) ([df42c9](https://github.com/versenilvis/iris/commit/df42c95fa935d647fe27fad4414630e606951187))
|
||||||
|
- Support tool aliases ([#107](https://github.com/versenilvis/iris/issues/107)) ([6faa2e](https://github.com/versenilvis/iris/commit/6faa2e3274b71c9056f3b8d8445f2d3fc7e609ff))
|
||||||
|
- Add theme customization ([#81](https://github.com/versenilvis/iris/issues/81)) ([b14b26](https://github.com/versenilvis/iris/commit/b14b26e663fe0c5b6144d46370c2ea6bb2d956e4))
|
||||||
|
|
||||||
|
### Refactors
|
||||||
|
|
||||||
|
- Use XDG config for all platforms ([#119](https://github.com/versenilvis/iris/issues/119)) ([d299d1](https://github.com/versenilvis/iris/commit/d299d1c7c7c66394c6a260451410f87b6559ac9c))
|
||||||
|
|
||||||
|
## [v0.4.21](https://github.com/versenilvis/iris/releases/tag/v0.4.21) - 2026-07-31
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Reorder badge and description ([23bd71](https://github.com/versenilvis/iris/commit/23bd7188990253500bfd142bc8213c886d7be78a))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Support for XDG base directories ([#95](https://github.com/versenilvis/iris/issues/95)) ([bf75dc](https://github.com/versenilvis/iris/commit/bf75dc239bd2432c10c3e36bc313b5407c23807f))
|
||||||
|
- Upgrade to lipgloss v2 ([#94](https://github.com/versenilvis/iris/issues/94)) ([efc49b](https://github.com/versenilvis/iris/commit/efc49bacfe7249880e3d2d2410abc43df51f5c18))
|
||||||
|
|
||||||
|
## [v0.4.20](https://github.com/versenilvis/iris/releases/tag/v0.4.20) - 2026-07-31
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Dynamic select key ui component ([#90](https://github.com/versenilvis/iris/issues/90)) ([8a1cec](https://github.com/versenilvis/iris/commit/8a1cec331aff82cb7c9463be77240c1775eeb8df))
|
||||||
|
|
||||||
|
## [v0.4.19](https://github.com/versenilvis/iris/releases/tag/v0.4.19) - 2026-07-31
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Resolve enter key submission and keybinding issues ([#89](https://github.com/versenilvis/iris/issues/89)) ([235a7f](https://github.com/versenilvis/iris/commit/235a7f98ea2cac3f9c546955dc0b70097c9e442f))
|
||||||
|
|
||||||
|
## [v0.4.15](https://github.com/versenilvis/iris/releases/tag/v0.4.15) - 2026-07-31
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Respect zdotdir config ([#83](https://github.com/versenilvis/iris/issues/83)) ([e9e50c](https://github.com/versenilvis/iris/commit/e9e50c7dea0186efe45e86a194fa8327e9c8d441))
|
||||||
|
|
||||||
|
## [v0.4.14](https://github.com/versenilvis/iris/releases/tag/v0.4.14) - 2026-07-31
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Sync config_cmd.go template with init.go template ([966378](https://github.com/versenilvis/iris/commit/9663788de65b06767ab21a9f5ac1c41386364c55))
|
||||||
|
|
||||||
|
## [v0.4.13](https://github.com/versenilvis/iris/releases/tag/v0.4.13) - 2026-07-31
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Warning about potential conflicts ([4421d6](https://github.com/versenilvis/iris/commit/4421d6e6a53cc3ed43a2688abac16f9d46535772))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Ui width, auto exec, selection and navigation configs ([#80](https://github.com/versenilvis/iris/issues/80)) ([a885c7](https://github.com/versenilvis/iris/commit/a885c7462abaacb44607df11bbf6fa3b37c4ccd3))
|
||||||
|
|
||||||
|
## [v0.4.12](https://github.com/versenilvis/iris/releases/tag/v0.4.12) - 2026-07-31
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Arrow up history entries ([#77](https://github.com/versenilvis/iris/issues/77)) ([cee756](https://github.com/versenilvis/iris/commit/cee756e2c110f98681f3e9f4283e44bf1963999b))
|
||||||
|
- Sync shell working directory for suggestions ([#78](https://github.com/versenilvis/iris/issues/78)) ([9f43fc](https://github.com/versenilvis/iris/commit/9f43fcb3527d5a72c108d53ccc381cb5fd7943f9))
|
||||||
|
|
||||||
|
## [v0.4.11](https://github.com/versenilvis/iris/releases/tag/v0.4.11) - 2026-07-30
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add shell login option ([#75](https://github.com/versenilvis/iris/issues/75)) ([2573ad](https://github.com/versenilvis/iris/commit/2573ad73b4dd55932248f54558ff0e31d6fb349e))
|
||||||
|
|
||||||
|
## [v0.4.10](https://github.com/versenilvis/iris/releases/tag/v0.4.10) - 2026-07-30
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Substring fuzzy search ([#74](https://github.com/versenilvis/iris/issues/74)) ([37182f](https://github.com/versenilvis/iris/commit/37182f72127b6ebf134edaae22c7d87b0a491b4c))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Move user guide to top level readme ([#70](https://github.com/versenilvis/iris/issues/70)) ([004925](https://github.com/versenilvis/iris/commit/004925684443b7f250a8174a0286a99b638590b7))
|
||||||
|
|
||||||
|
## [v0.4.9](https://github.com/versenilvis/iris/releases/tag/v0.4.9) - 2026-07-30
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Show symbolic links in suggestion ([#69](https://github.com/versenilvis/iris/issues/69)) ([979bd6](https://github.com/versenilvis/iris/commit/979bd6f5d608b9b5f34efc682a940190fd9a99ce))
|
||||||
|
|
||||||
|
## [v0.4.8](https://github.com/versenilvis/iris/releases/tag/v0.4.8) - 2026-07-29
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Iris stops working after exec a command in bash ([#66](https://github.com/versenilvis/iris/issues/66)) ([812d73](https://github.com/versenilvis/iris/commit/812d73b42a27f920ed08a1bbc1c2fd83c13c3a74))
|
||||||
|
|
||||||
|
## [v0.4.7](https://github.com/versenilvis/iris/releases/tag/v0.4.7) - 2026-07-29
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Stdin handling and exit command ([#58](https://github.com/versenilvis/iris/issues/58)) ([d73921](https://github.com/versenilvis/iris/commit/d73921e4199330e8996056f639d68fd9c3c62781))
|
||||||
|
|
||||||
|
## [v0.4.6](https://github.com/versenilvis/iris/releases/tag/v0.4.6) - 2026-07-29
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add some more config options ([#64](https://github.com/versenilvis/iris/issues/64)) ([d6cfb9](https://github.com/versenilvis/iris/commit/d6cfb9c90981db34a188744d95f4352261f33332))
|
||||||
|
|
||||||
|
## [v0.4.5](https://github.com/versenilvis/iris/releases/tag/v0.4.5) - 2026-07-28
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Add Nix flake for NixOS support ([#52](https://github.com/versenilvis/iris/issues/52)) ([69fb77](https://github.com/versenilvis/iris/commit/69fb77910deab604ae16d1f4752ccb0972c70a8f))
|
||||||
|
|
||||||
|
## [v0.4.4](https://github.com/versenilvis/iris/releases/tag/v0.4.4) - 2026-07-28
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- ZDOTDIR and XDG_CONFIG_HOME config detecting ([#51](https://github.com/versenilvis/iris/issues/51)) ([ebb0be](https://github.com/versenilvis/iris/commit/ebb0bead7dce702c3e111ed7c1ed4e5f86ee6eb9))
|
||||||
|
|
||||||
|
## [v0.4.3](https://github.com/versenilvis/iris/releases/tag/v0.4.3) - 2026-07-28
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Set hex color for ghost text instead of hardcoded ANSI bright-black ([#49](https://github.com/versenilvis/iris/issues/49)) ([6f488c](https://github.com/versenilvis/iris/commit/6f488c02cd79219eb33c16c72fa8f90a0dd2315e))
|
||||||
|
|
||||||
|
## [v0.4.2](https://github.com/versenilvis/iris/releases/tag/v0.4.2) - 2026-07-28
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Ranking ([#46](https://github.com/versenilvis/iris/issues/46)) ([93061c](https://github.com/versenilvis/iris/commit/93061c89cab1c0191d429af9f822c72afadef017))
|
||||||
|
|
||||||
|
## [v0.4.0](https://github.com/versenilvis/iris/releases/tag/v0.4.0) - 2026-07-27
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Fuzzy search not working ([#44](https://github.com/versenilvis/iris/issues/44)) ([84f2c8](https://github.com/versenilvis/iris/commit/84f2c8f49a82a197a36eae3fdaa3ee1af4ec05c5))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Update docs ([#43](https://github.com/versenilvis/iris/issues/43)) ([078e5e](https://github.com/versenilvis/iris/commit/078e5e817c276b65fafda158007210b6d77b0ab2))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Transition workflow learning and fix history UX ([#39](https://github.com/versenilvis/iris/issues/39)) ([6ffb9f](https://github.com/versenilvis/iris/commit/6ffb9f83f21ee71d40ad77c9feb1cd494584e316))
|
||||||
|
- Better suggestion with cobra's cli __complete subcmd ([#40](https://github.com/versenilvis/iris/issues/40)) ([c4e9bc](https://github.com/versenilvis/iris/commit/c4e9bcdd88cdb3db818549e2974e58a46ab543f2))
|
||||||
|
- Better file path suggestions ([#41](https://github.com/versenilvis/iris/issues/41)) ([27e079](https://github.com/versenilvis/iris/commit/27e0792560cb2217f6c88485c78fb5c54399bbcb))
|
||||||
|
|
||||||
|
### Refactors
|
||||||
|
|
||||||
|
- Split justfile into small files ([#42](https://github.com/versenilvis/iris/issues/42)) ([1540b9](https://github.com/versenilvis/iris/commit/1540b9655af472345a7c8ef2e5b109c23758fe37))
|
||||||
|
|
||||||
|
## [v0.3.3](https://github.com/versenilvis/iris/releases/tag/v0.3.3) - 2026-07-14
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Skip alias expansion on paste ([#38](https://github.com/versenilvis/iris/issues/38)) ([85ac76](https://github.com/versenilvis/iris/commit/85ac76e85a276890b8924ab73d733d22431aab04))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Scoring and frecency ([#37](https://github.com/versenilvis/iris/issues/37)) ([860b47](https://github.com/versenilvis/iris/commit/860b475b53e285cb2f80b16a4bb27b4cb4cfcc92))
|
||||||
|
|
||||||
|
## [v0.3.1](https://github.com/versenilvis/iris/releases/tag/v0.3.1) - 2026-07-13
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Enhance prompt ([#36](https://github.com/versenilvis/iris/issues/36)) ([f54e97](https://github.com/versenilvis/iris/commit/f54e97a6c0f3e8003f00f43ae3b07d647f90a942))
|
||||||
|
|
||||||
|
### Refactors
|
||||||
|
|
||||||
|
- Better project structure ([#35](https://github.com/versenilvis/iris/issues/35)) ([1e0494](https://github.com/versenilvis/iris/commit/1e049492421aa2b10cfcc584ac585d800d1883eb))
|
||||||
|
|
||||||
|
## [v0.3.0](https://github.com/versenilvis/iris/releases/tag/v0.3.0) - 2026-07-11
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Add showcase for kitty terminal ([#33](https://github.com/versenilvis/iris/issues/33)) ([4728ca](https://github.com/versenilvis/iris/commit/4728ca0c7f8202094e5956655550e64355063b03))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- AI suggestion ([#34](https://github.com/versenilvis/iris/issues/34)) ([94c8af](https://github.com/versenilvis/iris/commit/94c8af7b1d4408caddf1c6ed323defdc1867d037))
|
||||||
|
|
||||||
|
### Refactors
|
||||||
|
|
||||||
|
- Separate commands and its core into two folders ([#32](https://github.com/versenilvis/iris/issues/32)) ([ebde9b](https://github.com/versenilvis/iris/commit/ebde9ba68f72194ba0358aef2bca3ce8690617d0))
|
||||||
|
|
||||||
|
## [v0.2.8](https://github.com/versenilvis/iris/releases/tag/v0.2.8) - 2026-07-04
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Cannot update in dev mode ([#31](https://github.com/versenilvis/iris/issues/31)) ([6b9845](https://github.com/versenilvis/iris/commit/6b98455957303045071073738ac7a000feefa97c))
|
||||||
|
|
||||||
|
## [v0.2.7](https://github.com/versenilvis/iris/releases/tag/v0.2.7) - 2026-07-04
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Update to latest config and commands ([#28](https://github.com/versenilvis/iris/issues/28)) ([34f49e](https://github.com/versenilvis/iris/commit/34f49ed690aae3e232ac95f5a5c8f5dbf8614fce))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- More commands ([#30](https://github.com/versenilvis/iris/issues/30)) ([2ef3c9](https://github.com/versenilvis/iris/commit/2ef3c9081904885111d1880fb41df50c6d784fd7))
|
||||||
|
|
||||||
|
## [v0.2.6](https://github.com/versenilvis/iris/releases/tag/v0.2.6) - 2026-07-03
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Remove old binary before cp to avoid Text file busy error ([#27](https://github.com/versenilvis/iris/issues/27)) ([ffb2b9](https://github.com/versenilvis/iris/commit/ffb2b991a08d1a9a39b522d3db6f07577ea7a183))
|
||||||
|
|
||||||
|
## [v0.2.5](https://github.com/versenilvis/iris/releases/tag/v0.2.5) - 2026-07-03
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Actually execute install script when updating ([#26](https://github.com/versenilvis/iris/issues/26)) ([37b065](https://github.com/versenilvis/iris/commit/37b0651651459c62d0677fbb22026e446084d2d2))
|
||||||
|
|
||||||
|
## [v0.2.4](https://github.com/versenilvis/iris/releases/tag/v0.2.4) - 2026-07-03
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Allow passing specific shell to setup command ([#25](https://github.com/versenilvis/iris/issues/25)) ([ee1ef2](https://github.com/versenilvis/iris/commit/ee1ef2dd733607f74125c28e8385e562f90ca38c))
|
||||||
|
|
||||||
|
## [v0.2.3](https://github.com/versenilvis/iris/releases/tag/v0.2.3) - 2026-07-03
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Allow left/right movement after up arrow history recall ([#24](https://github.com/versenilvis/iris/issues/24)) ([4ca744](https://github.com/versenilvis/iris/commit/4ca744983c3c1e2234debf68add1fa755ec543e4))
|
||||||
|
|
||||||
|
## [v0.2.2](https://github.com/versenilvis/iris/releases/tag/v0.2.2) - 2026-07-03
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Installer ([#22](https://github.com/versenilvis/iris/issues/22)) ([df968e](https://github.com/versenilvis/iris/commit/df968e8d7f1f93cf9091425096df443b4eb1fcd8))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Iris uninstall command ([#21](https://github.com/versenilvis/iris/issues/21)) ([2223f1](https://github.com/versenilvis/iris/commit/2223f1736be05c16df31ae15ce7805261b6bec49))
|
||||||
|
|
||||||
|
## [v0.2.0](https://github.com/versenilvis/iris/releases/tag/v0.2.0) - 2026-07-03
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Better suggestion and overlay movement on key presses ([#12](https://github.com/versenilvis/iris/issues/12)) ([aca3d1](https://github.com/versenilvis/iris/commit/aca3d1670cc7792765d3175c51da57d9b3634281))
|
||||||
|
- Menu deboucing ([#14](https://github.com/versenilvis/iris/issues/14)) ([c7960a](https://github.com/versenilvis/iris/commit/c7960aac46f6b7b4b985f4c01cf3282dcb37fb4c))
|
||||||
|
- Data race and overlay bugs ([#20](https://github.com/versenilvis/iris/issues/20)) ([ca9d30](https://github.com/versenilvis/iris/commit/ca9d30af9c0f9361cb5515d46afa36fdd12140d4))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Update guidelines for new user and developer ([#16](https://github.com/versenilvis/iris/issues/16)) ([b9cec2](https://github.com/versenilvis/iris/commit/b9cec2b3ac0e2baa62add488948b987138bc0763))
|
||||||
|
- Introducing to iris ([#11](https://github.com/versenilvis/iris/issues/11)) ([24fdda](https://github.com/versenilvis/iris/commit/24fddaccfef8302a7850434f3ddebe5a0d2d8015))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Better debugger ([#13](https://github.com/versenilvis/iris/issues/13)) ([f37f06](https://github.com/versenilvis/iris/commit/f37f0681ce2e22054fc636f37acc79d8347c6a1f))
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Prevent memory leak, data race and speed up ComputeCursorCol processing ([#15](https://github.com/versenilvis/iris/issues/15)) ([8f89c1](https://github.com/versenilvis/iris/commit/8f89c15e725e8487ded62912ef264ca7a220a76c))
|
||||||
|
|
||||||
|
## [v0.1.0](https://github.com/versenilvis/iris/releases/tag/v0.1.0) - 2026-05-30
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Hide history when delete whole line ([afa3c2](https://github.com/versenilvis/iris/commit/afa3c28a8862f72cb489fe4781a0b75a8164a3d7))
|
||||||
|
- Continue to suggest command after tab ([61f05a](https://github.com/versenilvis/iris/commit/61f05a4c82b1c3a40af0d1c870a7ed13998dd98a))
|
||||||
|
- Handle quote when tokenize ([e465d9](https://github.com/versenilvis/iris/commit/e465d900a81ad6c847cfc4b9e408f13c2117ad56))
|
||||||
|
- Handle value attached flags ([110903](https://github.com/versenilvis/iris/commit/1109034c8729234424d47bd94e7cc01dcec15ce3))
|
||||||
|
- Add token injection to resolve shell aliases ([503d38](https://github.com/versenilvis/iris/commit/503d38e5c497ad8e15a06560c072d371555a849c))
|
||||||
|
- Remove sync.Once so we can always up-to-date with shell config ([8b25bd](https://github.com/versenilvis/iris/commit/8b25bdac0fc321f4b4d9bb53c4f36fdc9ab2bb80))
|
||||||
|
- Prioritize aliases ([2b760f](https://github.com/versenilvis/iris/commit/2b760fb82ae6c0e64a0c79963c30499a538ba295))
|
||||||
|
- Iris now reloads via signal handoff ([bed9c9](https://github.com/versenilvis/iris/commit/bed9c98747f5ed1395c7b255faf8cebfa6575c67))
|
||||||
|
- Return last shell insteadd of default shell on reload ([ccbf59](https://github.com/versenilvis/iris/commit/ccbf59201aa3953aa5c771ee43918a46bbe10328))
|
||||||
|
- Print reloading ([8dff68](https://github.com/versenilvis/iris/commit/8dff68601e5e2fa630d407074ef8c092547b0c15))
|
||||||
|
- Eliminate child shell leakage and terminal corruption on reload ([11225d](https://github.com/versenilvis/iris/commit/11225df6c839b5c287bf31dd5f7cc38085ea2251))
|
||||||
|
- Ctrl R open reverse search instead of switching to history ([f30656](https://github.com/versenilvis/iris/commit/f306566602d2eae39428864b8e8b14934d32e708))
|
||||||
|
- Iris keep suggesting when press tab continually ([fc21a6](https://github.com/versenilvis/iris/commit/fc21a6b0cc4ea13887a7809b36b72a190e715481))
|
||||||
|
- Up/down arrow hide menu ([c79867](https://github.com/versenilvis/iris/commit/c798673069e1b37f8ffdb12d0fe9742c57bd2285))
|
||||||
|
- Extra left border ([38118a](https://github.com/versenilvis/iris/commit/38118a9a4789329be922ac57d457e2c0b12a1b94))
|
||||||
|
- Should show in priority ([803244](https://github.com/versenilvis/iris/commit/8032448cb6f30fd6d4a412a308abbbc08e32a1b4))
|
||||||
|
- Prioritize exact matching then order ([a5c27b](https://github.com/versenilvis/iris/commit/a5c27bdb942e3c3dd2fc2e934c20e379c5ee155e))
|
||||||
|
- Now with same fuzzy tier, which command has higher score will be in higher order ([b045aa](https://github.com/versenilvis/iris/commit/b045aaf6af71ed9cc7332849f8147972762bd6b7))
|
||||||
|
- Key reading loop conflict ([27c7b9](https://github.com/versenilvis/iris/commit/27c7b9732658745e7afdc201d930f5799e7f641a))
|
||||||
|
- Wrong suggestion on chmod command ([d2909d](https://github.com/versenilvis/iris/commit/d2909d93aaeee6667d2d0c042126b05fd7eb7dee))
|
||||||
|
- Text file busy bug on setup ([44ff34](https://github.com/versenilvis/iris/commit/44ff34f3789d0c4f5f61671365a701960872d0c8))
|
||||||
|
- Cd and zoxide only shows dir ([9720b5](https://github.com/versenilvis/iris/commit/9720b5f4bcf2dd888973b91c3984187c5015ec20))
|
||||||
|
- Support folder contains space ([3c5a05](https://github.com/versenilvis/iris/commit/3c5a0580d1e6709dbc054fd4373a2f7a654e7ea0))
|
||||||
|
- Fix all problems related to test ([e587f0](https://github.com/versenilvis/iris/commit/e587f027982701d390fc1bc75702c93e0d66ee7c))
|
||||||
|
- All issues by golangci-lint ([c76541](https://github.com/versenilvis/iris/commit/c76541258acc79abce4a3685590e3e35dbdb0ed7))
|
||||||
|
- Fuzzy search shows commands that no one need ([88d6ee](https://github.com/versenilvis/iris/commit/88d6ee4e57d324076ae6b41aacc49241ad980ca5))
|
||||||
|
- Typo ([07b7d1](https://github.com/versenilvis/iris/commit/07b7d19347bfe3fe7188e331bfc6b10af937ad58))
|
||||||
|
- Err check rules and fix config ([56e04b](https://github.com/versenilvis/iris/commit/56e04be4098b22662a7ad96b3a10cbe4cf8752e0))
|
||||||
|
- Add user email and name to support ci/cd ([ae067f](https://github.com/versenilvis/iris/commit/ae067fa2004790f677f8350b744b3ff34f365ff1))
|
||||||
|
- Git doesnt show branch suggesstion properly ([0ec946](https://github.com/versenilvis/iris/commit/0ec946f408f4416aca1c520f8c326326a565a10d))
|
||||||
|
- Git ([9b206f](https://github.com/versenilvis/iris/commit/9b206fe701ab024a14903c9ff1315bb055991676))
|
||||||
|
- Issue #4 and #5 ([#6](https://github.com/versenilvis/iris/issues/6)) ([d06526](https://github.com/versenilvis/iris/commit/d06526cf21d59336d2d2f7fee5d492b1ead2f73c))
|
||||||
|
- Correct nightly tag, fix tmux startup, full-word suggestions only, and keep current dir on reload ([#8](https://github.com/versenilvis/iris/issues/8)) ([8556fa](https://github.com/versenilvis/iris/commit/8556fa626aecd555e1a87f0bd39621fe517cebd5))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- LICENSE ([2eac0c](https://github.com/versenilvis/iris/commit/2eac0cd38e21cde76cd45a5d9d450805e759e140))
|
||||||
|
- Add hot reload and shell aliases ([97d009](https://github.com/versenilvis/iris/commit/97d009f7ef6c0b9e020f69be155440fa7fa0a5ef))
|
||||||
|
- Updater ([72dd69](https://github.com/versenilvis/iris/commit/72dd6940e422932caa4977f61778f38917511ed6))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Entry point ([c384c5](https://github.com/versenilvis/iris/commit/c384c57cb9ee749cf594505c5ec7c499cc062b47))
|
||||||
|
- Build and run ([f69e39](https://github.com/versenilvis/iris/commit/f69e390e3b55c87dea82a716045081a4e62faf63))
|
||||||
|
- Ipc server ([55bd6e](https://github.com/versenilvis/iris/commit/55bd6e1b481df0a4d773b0165543e54020f72a96))
|
||||||
|
- Threat-safe terminal writer ([551a09](https://github.com/versenilvis/iris/commit/551a096d2d0b23ee3e019695778702b3d8ee04d8))
|
||||||
|
- Iris command ([1fb279](https://github.com/versenilvis/iris/commit/1fb279e588f790e3402a8b8734eda72d6dfa71f2))
|
||||||
|
- History search ([515d6f](https://github.com/versenilvis/iris/commit/515d6f2a0a772708227936490aa77878fcb2f601))
|
||||||
|
- Drop down menu tui ([25ff2b](https://github.com/versenilvis/iris/commit/25ff2bcb2c4cf60da3a884cc48c30ed2c82ffd0f))
|
||||||
|
- Spec ([d38469](https://github.com/versenilvis/iris/commit/d384694ab341fcc384fd323bcf89ee5788088627))
|
||||||
|
- Some common commands ([aab3c6](https://github.com/versenilvis/iris/commit/aab3c6be2ed85e897149bf1a273761f5e4248b5d))
|
||||||
|
- Add partial ([e559c9](https://github.com/versenilvis/iris/commit/e559c960d9b5589b2609045f23eac7be4cc4c531))
|
||||||
|
- File generator for file suggestion ([76458c](https://github.com/versenilvis/iris/commit/76458ce52ef27f090d204d1885a274fc7119cc50))
|
||||||
|
- Refactor and add more commands ([f4816d](https://github.com/versenilvis/iris/commit/f4816d2b5f0fc371a413f61ebb13f321e610e8d7))
|
||||||
|
- Tracking CWD and simply return file desc ([7b37b8](https://github.com/versenilvis/iris/commit/7b37b8ae2823a4bad1660cba8486fce7a70e9534))
|
||||||
|
- Change priority ([b01844](https://github.com/versenilvis/iris/commit/b01844639bfc02dc7a727df3aae93a08648a2b6b))
|
||||||
|
- Optimize build command ([4cc785](https://github.com/versenilvis/iris/commit/4cc7853256480dbe1a2c0cbe1c9d2abf2b1acbb5))
|
||||||
|
- Dynamic git command completion ([f64c80](https://github.com/versenilvis/iris/commit/f64c8022b264e78ac3248e52b8b3d6386868c100))
|
||||||
|
- System PATH scannee ([525eac](https://github.com/versenilvis/iris/commit/525eac1bfa6e1c20246c6cc6d200be70e75790f1))
|
||||||
|
- Support aliases ([1f483c](https://github.com/versenilvis/iris/commit/1f483c5ab291bea8cdc35eec2a13ec9da21b6096))
|
||||||
|
- Scan shell aliases like .zshrc, .bashrc, ... ([4c70c2](https://github.com/versenilvis/iris/commit/4c70c242990acc4d1311712dfd7d189994a1b8ab))
|
||||||
|
- Rebuild and update pkg ([02fdd9](https://github.com/versenilvis/iris/commit/02fdd99e400b09b8c39dd1a86f7b2140cf056a48))
|
||||||
|
- In-place reloading on current TTY ([78e191](https://github.com/versenilvis/iris/commit/78e19199b0478efa1de09d549ef044c8f5d0b491))
|
||||||
|
- Shift tab to toggle menu ([7a9c28](https://github.com/versenilvis/iris/commit/7a9c28e6f68f3914d69649de6fd87e519ec5fa67))
|
||||||
|
- Stop showing suggestion when delete entire line ([887b5a](https://github.com/versenilvis/iris/commit/887b5af9580b29fa10bdf86fb478e9622d0aec79))
|
||||||
|
- Now pressing space show alias as full command ([7c11a3](https://github.com/versenilvis/iris/commit/7c11a3a620e2e6b0fd704b2a094399e4f98a0fe2))
|
||||||
|
- Continue last mode ([c7106f](https://github.com/versenilvis/iris/commit/c7106f9fc2a849f8262d36bcc73e92dd3791868a))
|
||||||
|
- Add bash and fish ([00826a](https://github.com/versenilvis/iris/commit/00826a5c075020a000948546cb9800489e7b182e))
|
||||||
|
- Ghost text ([edab80](https://github.com/versenilvis/iris/commit/edab80202690c088faaa7af0ba9e535a73b66260))
|
||||||
|
- Disable ghost text when using arrow keys ([ff8874](https://github.com/versenilvis/iris/commit/ff8874fefcbdcc5ec036e7e5381efbe8bff96753))
|
||||||
|
- Run with terminal ([5c8ac9](https://github.com/versenilvis/iris/commit/5c8ac9e4a65434f11004d430fcc694001dbb7d8c))
|
||||||
|
- Add more options and limit file extension ([667cf9](https://github.com/versenilvis/iris/commit/667cf9feae0b22690f5c911b5d36bd6f6f8076f6))
|
||||||
|
- Scan 1 level deeper in folder ([66b2b2](https://github.com/versenilvis/iris/commit/66b2b22d94a4f60618bbd31b905bf1242a900341))
|
||||||
|
- : install script ([ef2660](https://github.com/versenilvis/iris/commit/ef26602ded90ba3fdd64f6f74addcd9dc7773814))
|
||||||
|
- Setup ([b6e509](https://github.com/versenilvis/iris/commit/b6e5096092a8de4ec92ec35070999cf4e3998432))
|
||||||
|
- Detect another command is running to stop iris ([cea2a7](https://github.com/versenilvis/iris/commit/cea2a7bee72ec9dc5c689eb76d1dbdf89a59dca7))
|
||||||
|
- Show preview command when using arrow keys ([23e5b1](https://github.com/versenilvis/iris/commit/23e5b13f6d11b436dcac52126f023b4362eb1f64))
|
||||||
|
- Up/down arrow key now can choose last commands ([3c8eca](https://github.com/versenilvis/iris/commit/3c8eca3a62315b9594fde752125dfc88a1a5f504))
|
||||||
|
- Zoxide ([5d8187](https://github.com/versenilvis/iris/commit/5d81871aef68ad6b1e88b76832066579bb094e8f))
|
||||||
|
- Copy to local bin ([be3c08](https://github.com/versenilvis/iris/commit/be3c08171bdbf7b312551efa5413a9c43bc33c69))
|
||||||
|
- All tests ([17f33f](https://github.com/versenilvis/iris/commit/17f33ff480ff8f443107d682322d72f4b51cceb0))
|
||||||
|
- More commands, tests, add Registry conflict between tests ([d74c95](https://github.com/versenilvis/iris/commit/d74c959be4ebc8ac535686ab18eadb3f0d648af5))
|
||||||
|
- Version ([e6f4bd](https://github.com/versenilvis/iris/commit/e6f4bdd36d6024b2286985def87df30888244222))
|
||||||
|
- Updater ([1e923a](https://github.com/versenilvis/iris/commit/1e923a13d9816ce0414dbbd05d0fc219e40027e2))
|
||||||
|
- Update test ([f9eb19](https://github.com/versenilvis/iris/commit/f9eb1900464cb0190a0120acacde2846b1726e4a))
|
||||||
|
- Uninstaller ([eaa734](https://github.com/versenilvis/iris/commit/eaa7344a24a0a2b5feaad1daba379a3611152557))
|
||||||
|
- Updates and versioning ([#1](https://github.com/versenilvis/iris/issues/1)) ([9fd54c](https://github.com/versenilvis/iris/commit/9fd54c8c3dfa6e87750b1365615ccabef3c53a44))
|
||||||
|
- Issue template and release ci/cd ([b5c0e5](https://github.com/versenilvis/iris/commit/b5c0e5ec3d99ec4d754c6722cbe15d49f7a11fed))
|
||||||
|
- Issue template and ci/cd ([#2](https://github.com/versenilvis/iris/issues/2)) ([8f4d8b](https://github.com/versenilvis/iris/commit/8f4d8b7db7c32ac919f3f1121af251f3bbcff887))
|
||||||
|
- Nightly built ([9c1c77](https://github.com/versenilvis/iris/commit/9c1c7749b08983db182cee0f0d4ffaf485a6ed6d))
|
||||||
|
- Crash log ([#7](https://github.com/versenilvis/iris/issues/7)) ([bf67e5](https://github.com/versenilvis/iris/commit/bf67e50ea612b1a1e146e4dfc67de843877374e2))
|
||||||
|
- Toml config support ([#9](https://github.com/versenilvis/iris/issues/9)) ([3d76a3](https://github.com/versenilvis/iris/commit/3d76a3ee8816ac8fa2c5026551ff93d77bbd4762))
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- Using string.Builder instead of concatenation ([81b80d](https://github.com/versenilvis/iris/commit/81b80d079ce20956ee99e0343fc90c0d4d8b9295))
|
||||||
|
- Finding the first space and using a case-insensitive comparison ([caf4aa](https://github.com/versenilvis/iris/commit/caf4aa344e3f9ae1c0691cde3c91e89fa23d4629))
|
||||||
|
|
||||||
|
### Refactors
|
||||||
|
|
||||||
|
- Improve reading aliases in shell config ([71a75c](https://github.com/versenilvis/iris/commit/71a75ced2857f9ded98185ca2f110555f3c2146b))
|
||||||
|
- Shell adaptor ([736723](https://github.com/versenilvis/iris/commit/736723fbe8a6d520ec70ecd939e3a91bf013f624))
|
||||||
|
- Seperate into small files ([db8b6e](https://github.com/versenilvis/iris/commit/db8b6ec91bf741519ce3a056ab2d93d8dafcd1d6))
|
||||||
|
- Seperate into small files ([3adc04](https://github.com/versenilvis/iris/commit/3adc045c09575996ce966e8b83b4932a761bd64e))
|
||||||
|
|
||||||
|
|
||||||
@@ -229,6 +229,21 @@ cd iris
|
|||||||
just reload
|
just reload
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
iris update
|
||||||
|
```
|
||||||
|
|
||||||
|
Checks for and installs the latest release. To see what's changed first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
iris changelog # latest release
|
||||||
|
iris changelog -n 3 # last 3 releases
|
||||||
|
```
|
||||||
|
|
||||||
|
IRIS can also check for and install updates on its own - see `updater.auto-update` in the [configuration guide](#configuration-guide) below.
|
||||||
|
|
||||||
## Uninstall
|
## Uninstall
|
||||||
|
|
||||||
To completely uninstall IRIS, remove all configurations, and clean up your shell integration files, simply run:
|
To completely uninstall IRIS, remove all configurations, and clean up your shell integration files, simply run:
|
||||||
@@ -310,6 +325,7 @@ deduplicate-branches = true
|
|||||||
check-on-startup = true
|
check-on-startup = true
|
||||||
channel = "stable" # "stable" or "nightly"
|
channel = "stable" # "stable" or "nightly"
|
||||||
check-interval = "24h"
|
check-interval = "24h"
|
||||||
|
auto-update = 0 # 0 = off, 1 = auto-install, 2 = always confirm first
|
||||||
|
|
||||||
[ai]
|
[ai]
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|||||||
+1
-1
@@ -11,4 +11,4 @@ uncomplete = "uncomplete"
|
|||||||
edn = "edn"
|
edn = "edn"
|
||||||
|
|
||||||
[files]
|
[files]
|
||||||
extend-exclude = ["internal/scoring/scorer_test.go"]
|
extend-exclude = ["internal/scoring/scorer_test.go", "CHANGELOG.md"]
|
||||||
|
|||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
[changelog]
|
||||||
|
header = "# Changelog\n\nAll notable changes to this project are documented in this file.\n\n"
|
||||||
|
body = """
|
||||||
|
{% if version %}\
|
||||||
|
## [{{ version }}](https://github.com/versenilvis/iris/releases/tag/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
{% else %}\
|
||||||
|
## [Unreleased]
|
||||||
|
{% endif %}\
|
||||||
|
{{ "\n" }}\
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}\
|
||||||
|
### {{ group | upper_first }}
|
||||||
|
{{ "\n" }}\
|
||||||
|
{% for commit in commits %}\
|
||||||
|
- {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=6, end="") }}](https://github.com/versenilvis/iris/commit/{{ commit.id }}))
|
||||||
|
{% endfor %}\
|
||||||
|
{{ "\n" }}\
|
||||||
|
{% endfor %}\
|
||||||
|
"""
|
||||||
|
trim = true
|
||||||
|
footer = ""
|
||||||
|
|
||||||
|
[git]
|
||||||
|
conventional_commits = true
|
||||||
|
filter_unconventional = true
|
||||||
|
split_commits = false
|
||||||
|
commit_preprocessors = [
|
||||||
|
{ pattern = '\(#([0-9]+)\)', replace = "([#${1}](https://github.com/versenilvis/iris/issues/${1}))" },
|
||||||
|
]
|
||||||
|
# stable tags only - `ignore_tags` was tried first but git-cliff merges an
|
||||||
|
# ignored tag's commits into the wrong release when nightly and stable tags share a commit range
|
||||||
|
tag_pattern = "^v[0-9]+\\.[0-9]+\\.[0-9]+$"
|
||||||
|
sort_commits = "oldest"
|
||||||
|
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "(?i)^feat", group = "Features" },
|
||||||
|
{ message = "(?i)^fix\\(ci\\)", skip = true },
|
||||||
|
{ message = "(?i)^fix\\(release\\)", skip = true },
|
||||||
|
{ message = "(?i)^fix", group = "Bug fixes" },
|
||||||
|
{ message = "(?i)^perf", group = "Performance" },
|
||||||
|
{ message = "(?i)^refactor", group = "Refactors" },
|
||||||
|
{ message = "(?i)^docs?", group = "Documentation" },
|
||||||
|
{ message = "(?i)^chore", skip = true },
|
||||||
|
{ message = "(?i)^build", skip = true },
|
||||||
|
{ message = "(?i)^ci", skip = true },
|
||||||
|
{ message = "(?i)^test", skip = true },
|
||||||
|
{ message = "(?i)^style", skip = true },
|
||||||
|
# catch-all: unlisted types (e.g. historical "pkg:") drop instead of leaking a stray group
|
||||||
|
{ message = ".*", skip = true },
|
||||||
|
]
|
||||||
@@ -21,7 +21,7 @@ We use `just` as our primary command runner. Recipes are organized into modular
|
|||||||
- `justfiles/test.just` (`[test]` group): `test`, `lint`, `analyze` (alias `ana`)
|
- `justfiles/test.just` (`[test]` group): `test`, `lint`, `analyze` (alias `ana`)
|
||||||
- `justfiles/gen.just` (`[gen]` group): `gen-docs`
|
- `justfiles/gen.just` (`[gen]` group): `gen-docs`
|
||||||
- `justfiles/pkg.just` (`[pkg]` group): `pkg`
|
- `justfiles/pkg.just` (`[pkg]` group): `pkg`
|
||||||
- `justfiles/debug.just` (`[debug]` group): `debug`, `debug-update`, `debug-notify`, `debug-install`
|
- `justfiles/debug.just` (`[debug]` group): `debug`, `debug-update`, `debug-changelog`, `debug-notify`, `debug-install`, `debug-autoupdate`
|
||||||
|
|
||||||
Run `just -l` to quickly view all available recipes organized by group.
|
Run `just -l` to quickly view all available recipes organized by group.
|
||||||
|
|
||||||
|
|||||||
+70
-18
@@ -2,24 +2,39 @@
|
|||||||
|
|
||||||
Iris has a built-in update notification system designed to be non-intrusive and zero-latency
|
Iris has a built-in update notification system designed to be non-intrusive and zero-latency
|
||||||
|
|
||||||
The update system is designed to keep Iris up to date while staying out of the way. It performs an asynchronous network check when the shell starts and notifies you exactly once per version after you run a command. This prevents redundant notifications and ensures that you only see an update message when a new release is actually available. The system also includes dedicated debugging tools to verify the notification logic without requiring real releases
|
The update system is designed to keep Iris up to date while staying out of the way. It performs an asynchronous network check when the shell starts and notifies you exactly once per version after you run a command. This prevents redundant notifications and ensures that you only see an update message when a new release is actually available. Optionally, it can also install updates for you in the background. The system also includes dedicated debugging tools to verify the notification and auto-update logic without requiring real releases
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. **Background check**: every time you open a new terminal, Iris launches a background goroutine to check for updates
|
1. **Background check**: every time you open a new terminal, Iris launches a background goroutine to check for updates
|
||||||
- checks the network at most once every 6 hours to avoid GitHub API rate limiting
|
- checks the network at most once every `check-interval` (24h by default) to avoid GitHub API rate limiting
|
||||||
- has a 5 second timeout so it never hangs on a slow or missing network connection
|
- has a 5 second timeout so it never hangs on a slow or missing network connection
|
||||||
- if there is no network, it fails silently with zero impact on startup
|
- if there is no network, it fails silently with zero impact on startup
|
||||||
|
|
||||||
2. **State persistence**: state is stored in `~/.iris/update_state.json` with two fields:
|
2. **State persistence**: state is stored in `$XDG_DATA_HOME/iris/state.toml` (or `~/.local/share/iris/state.toml`), including:
|
||||||
- `last_check` - unix timestamp of the last network check
|
- `updater.last-check-time` / `updater.seen-version` - when the last check ran, and the latest version already notified about
|
||||||
- `seen_version` - the latest version the user was already notified about
|
- `updater.auto-update-target` / `updater.auto-update-attempt` - the auto-update escalation ladder's state (see [Auto-update](#auto-update) below)
|
||||||
|
- `updater.declined-version` - a version the user explicitly declined at an auto-update confirm prompt, never re-prompted
|
||||||
|
|
||||||
3. **Smart notification**:
|
3. **Smart notification**:
|
||||||
- the notice only appears after you run your first command (triggered by the `IRIS_CMD_STOP` IPC signal from the shell hook)
|
- the notice only appears after you run your first command (triggered by the `IRIS_CMD_STOP` IPC signal from the shell hook)
|
||||||
- appears only once per session, never again even if you keep the terminal open
|
- appears only once per session, never again even if you keep the terminal open
|
||||||
- if you have already been notified about a specific version, it will not show again until a newer GitHub release tag is detected
|
- if you have already been notified about a specific version, it will not show again until a newer GitHub release tag is detected
|
||||||
- when `iris update` is run successfully, the `seen_version` flag is cleared
|
- when `iris update` is run successfully, the `seen-version` flag is cleared
|
||||||
|
- includes up to two changelog bullets pulled from the release's grouped notes (see [Implementation details](#implementation-details) for where those come from)
|
||||||
|
|
||||||
|
## Auto-update
|
||||||
|
|
||||||
|
`updater.auto-update` (see [Configuration](#configuration)) controls whether the background check can install updates itself, not just notify:
|
||||||
|
|
||||||
|
- **`0` (default)**: notify only, exactly the behavior above.
|
||||||
|
- **`1`**: auto-install, with a per-version escalation ladder so a flaky network or a failing installer can't turn into a silent retry loop:
|
||||||
|
1. first detection of a newer version: installs it in the background, no prompt. Output is captured (not streamed) since the wrapper's terminal is in raw mode - a clean one-line notice is printed once the install finishes, if you're still in the session.
|
||||||
|
2. if that version is still current on the *next* check (the install didn't stick), it stops auto-installing and shows a confirm prompt instead: `[y/N]`, pressed like any other single keystroke.
|
||||||
|
3. if it's still not current after that, Iris gives up on that version - prints one notice pointing at `iris update`, and won't try again until a *newer* version is released.
|
||||||
|
- **`2`**: always confirm first, every time - never installs silently, never gives up, just keeps asking until you say yes or the version changes.
|
||||||
|
|
||||||
|
A confirm prompt declined with `n`/Esc/Ctrl+C records that version so it's never re-prompted (until a newer one comes along).
|
||||||
|
|
||||||
## Build-time versioning
|
## Build-time versioning
|
||||||
|
|
||||||
@@ -31,14 +46,27 @@ go build -ldflags="-X github.com/versenilvis/iris/root.Version=v1.2.0" -o iris m
|
|||||||
|
|
||||||
If not provided, the version defaults to dev. The dev version will never trigger an update notification
|
If not provided, the version defaults to dev. The dev version will never trigger an update notification
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The `[updater]` section of `~/.config/iris/config.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[updater]
|
||||||
|
check-on-startup = true
|
||||||
|
channel = "stable" # "stable" or "nightly"
|
||||||
|
check-interval = "24h"
|
||||||
|
auto-update = 0 # 0 = off, 1 = auto-install, 2 = always confirm first
|
||||||
|
```
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- `iris version` - print the current version of the running binary
|
- `iris version` - print the current version of the running binary
|
||||||
- `iris update` - manually check for and apply the latest release
|
- `iris update` - manually check for and apply the latest release
|
||||||
|
- `iris changelog` - show grouped release notes fetched from GitHub (`-n <count>`, `--refresh`)
|
||||||
|
|
||||||
## Debugging and testing
|
## Debugging and testing
|
||||||
|
|
||||||
There are two separate things to test: the update command itself, and the in-session notification banner
|
Four separate things to test: the update command, the in-session notification banner, the changelog command, and the background auto-updater
|
||||||
|
|
||||||
### Test 1: update command
|
### Test 1: update command
|
||||||
|
|
||||||
@@ -71,27 +99,51 @@ just debug-notify v1.99.0
|
|||||||
Inside the new session, run any command. Expected output after the command
|
Inside the new session, run any command. Expected output after the command
|
||||||
```
|
```
|
||||||
[IRIS] new version v0.0.1 -> v1.99.0 available, run iris update to upgrade
|
[IRIS] new version v0.0.1 -> v1.99.0 available, run iris update to upgrade
|
||||||
|
- <first changelog bullet>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3: changelog command
|
||||||
|
|
||||||
|
Tests fetching, grouping, the response cache, and rate-limit fallback. No full Iris session needed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just build-release v0.0.1
|
||||||
|
just debug-changelog v1.99.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 4: background auto-updater
|
||||||
|
|
||||||
|
Tests the escalation ladder (silent install, then escalating to a confirm-worthy state, then giving up rather than looping) fully offline, by inspecting `state.toml` after each simulated check cycle. The confirm-prompt UI itself is interactive and isn't exercised here - set `auto-update` in a real config and watch a live session with shell hooks active to see it rendered
|
||||||
|
|
||||||
|
```bash
|
||||||
|
just debug-autoupdate
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
| Variable | Purpose |
|
| Variable | Purpose |
|
||||||
| -------------------------- | ----------------------------------------------------------------------------------- |
|
| --------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||||
| `IRIS_UPDATE_URL` | override the GitHub API endpoint with a custom URL (used by `debug-update`) |
|
| `IRIS_UPDATE_URL` | override the release-check endpoint with a custom URL (used by `debug-update`, `debug-autoupdate`) |
|
||||||
| `IRIS_MOCK_LATEST_VERSION` | skip network entirely, resolve to this version immediately (used by `debug-notify`) |
|
| `IRIS_MOCK_LATEST_VERSION` | skip network entirely, resolve to this version immediately (used by `debug-notify`) |
|
||||||
|
| `IRIS_CHANGELOG_URL` | override the changelog-fetch endpoint with a custom URL (used by `debug-changelog`) |
|
||||||
|
| `IRIS_INSTALL_URL` | override the install-script fetch URL (used by `debug-autoupdate`) |
|
||||||
|
|
||||||
### State reset
|
### State reset
|
||||||
|
|
||||||
To force a fresh network check on next launch, delete the state file:
|
To force a fresh check on next launch, delete the state file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rm ~/.iris/update_state.json
|
rm ~/.local/share/iris/state.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Implementation details
|
## Implementation details
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
| ----------------- | ---------------------------------------------------------------------------------------- |
|
| ------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||||
| `root/version.go` | holds the `Version` constant, defaults to `dev` |
|
| `root/version.go` | holds the `Version` variable, defaults to `dev` |
|
||||||
| `root/update.go` | all update logic: state persistence, network fetch, version compare, commands |
|
| `root/update.go` | version fetch/compare, state persistence, the `iris update`/`iris version` commands |
|
||||||
| `root/wrapper.go` | wires the background check into the IPC loop, prints the notice on first `IRIS_CMD_STOP` |
|
| `root/changelog_cmd.go` | the `iris changelog` command: fetch, cache, grouping-aware rendering |
|
||||||
|
| `root/autoupdate.go` | `performUpdate`, the auto-update escalation ladder, and the confirm-prompt keystroke handler |
|
||||||
|
| `root/wrapper.go` | wires the background check into the IPC loop, prints notices on `IRIS_CMD_STOP` |
|
||||||
|
| `.goreleaser.yaml` | `changelog:` block groups commits by type for the GitHub release body |
|
||||||
|
| `cliff.toml` | generates the committed `CHANGELOG.md` from the same commit history on each stable release |
|
||||||
|
|||||||
@@ -5,37 +5,53 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
charm.land/lipgloss/v2 v2.0.5
|
charm.land/lipgloss/v2 v2.0.5
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
|
github.com/charmbracelet/glamour v1.0.0
|
||||||
github.com/charmbracelet/x/ansi v0.11.7
|
github.com/charmbracelet/x/ansi v0.11.7
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/versenilvis/fuzzy v0.1.0-rc1.2
|
github.com/versenilvis/fuzzy v0.1.0-rc1.2
|
||||||
go.uber.org/goleak v1.3.0
|
go.uber.org/goleak v1.3.0
|
||||||
golang.org/x/sys v0.46.0
|
golang.org/x/sys v0.46.0
|
||||||
golang.org/x/term v0.30.0
|
golang.org/x/term v0.36.0
|
||||||
modernc.org/sqlite v1.53.0
|
modernc.org/sqlite v1.53.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
|
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
github.com/yuin/goldmark v1.7.13 // indirect
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||||
|
golang.org/x/net v0.38.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
|
golang.org/x/text v0.30.0 // indirect
|
||||||
modernc.org/libc v1.73.4 // indirect
|
modernc.org/libc v1.73.4 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|||||||
@@ -2,12 +2,34 @@ charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY=
|
|||||||
charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc=
|
charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc=
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||||
|
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
|
||||||
|
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
|
||||||
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
|
github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08=
|
||||||
|
github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI=
|
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI=
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI=
|
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
||||||
|
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
@@ -23,30 +45,45 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
|||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||||
|
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
@@ -61,6 +98,10 @@ github.com/versenilvis/fuzzy v0.1.0-rc1.2 h1:wX7v7Oxt+3O9VOiBpmMwpWZCkCdeYeicjUX
|
|||||||
github.com/versenilvis/fuzzy v0.1.0-rc1.2/go.mod h1:TCgCwxBa4px8NhCXH8SPePCyJsf/qPsl1i+G/HDdjyg=
|
github.com/versenilvis/fuzzy v0.1.0-rc1.2/go.mod h1:TCgCwxBa4px8NhCXH8SPePCyJsf/qPsl1i+G/HDdjyg=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||||
|
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||||
|
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
@@ -68,13 +109,17 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM
|
|||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||||
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ type UpdaterConfig struct {
|
|||||||
CheckOnStartup bool `toml:"check-on-startup"`
|
CheckOnStartup bool `toml:"check-on-startup"`
|
||||||
Channel string `toml:"channel"`
|
Channel string `toml:"channel"`
|
||||||
CheckInterval Duration `toml:"check-interval"`
|
CheckInterval Duration `toml:"check-interval"`
|
||||||
|
// AutoUpdate: 0 = off (default), 1 = auto-install, 2 = always confirm first
|
||||||
|
AutoUpdate int `toml:"auto-update"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeybindingsConfig struct {
|
type KeybindingsConfig struct {
|
||||||
@@ -281,6 +283,10 @@ func validate(cfg *Config) error {
|
|||||||
return fmt.Errorf("updater.channel: invalid value %q (want: stable|nightly)", cfg.Updater.Channel)
|
return fmt.Errorf("updater.channel: invalid value %q (want: stable|nightly)", cfg.Updater.Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.Updater.AutoUpdate < 0 || cfg.Updater.AutoUpdate > 2 {
|
||||||
|
return fmt.Errorf("updater.auto-update: invalid value %d (want: 0=off, 1=auto, 2=confirm)", cfg.Updater.AutoUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.UI.MaxSuggestions < 1 || cfg.UI.MaxSuggestions > 500 {
|
if cfg.UI.MaxSuggestions < 1 || cfg.UI.MaxSuggestions > 500 {
|
||||||
return fmt.Errorf("ui.max-suggestions: must be between 1 and 500")
|
return fmt.Errorf("ui.max-suggestions: must be between 1 and 500")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,24 @@ model = "qwen-2.5-coder-32b"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateAutoUpdateRange(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
|
||||||
|
for _, valid := range []int{0, 1, 2} {
|
||||||
|
cfg.Updater.AutoUpdate = valid
|
||||||
|
if err := validate(cfg); err != nil {
|
||||||
|
t.Errorf("expected auto-update=%d to be valid, got error: %v", valid, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, invalid := range []int{-1, 3} {
|
||||||
|
cfg.Updater.AutoUpdate = invalid
|
||||||
|
if err := validate(cfg); err == nil {
|
||||||
|
t.Errorf("expected auto-update=%d to be rejected", invalid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadSave(t *testing.T) {
|
func TestLoadSave(t *testing.T) {
|
||||||
tmpDir, err := os.MkdirTemp("", "iris-config-test")
|
tmpDir, err := os.MkdirTemp("", "iris-config-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ func DefaultConfig() *Config {
|
|||||||
CheckOnStartup: true,
|
CheckOnStartup: true,
|
||||||
Channel: "stable",
|
Channel: "stable",
|
||||||
CheckInterval: Duration(24 * time.Hour),
|
CheckInterval: Duration(24 * time.Hour),
|
||||||
|
AutoUpdate: 0,
|
||||||
},
|
},
|
||||||
AI: AIConfig{
|
AI: AIConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
@@ -59,8 +60,11 @@ func DefaultState() *State {
|
|||||||
return &State{
|
return &State{
|
||||||
LastMode: "spec",
|
LastMode: "spec",
|
||||||
Updater: UpdaterState{
|
Updater: UpdaterState{
|
||||||
LastCheckTime: time.Time{},
|
LastCheckTime: time.Time{},
|
||||||
SeenVersion: "",
|
SeenVersion: "",
|
||||||
|
AutoUpdateTarget: "",
|
||||||
|
AutoUpdateAttempt: 0,
|
||||||
|
DeclinedVersion: "",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import (
|
|||||||
type UpdaterState struct {
|
type UpdaterState struct {
|
||||||
LastCheckTime time.Time `toml:"last-check-time"`
|
LastCheckTime time.Time `toml:"last-check-time"`
|
||||||
SeenVersion string `toml:"seen-version"`
|
SeenVersion string `toml:"seen-version"`
|
||||||
|
// auto-update escalation ladder state, see decideAutoUpdateAction
|
||||||
|
AutoUpdateTarget string `toml:"auto-update-target"`
|
||||||
|
AutoUpdateAttempt int `toml:"auto-update-attempt"`
|
||||||
|
DeclinedVersion string `toml:"declined-version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
|
|||||||
@@ -18,12 +18,228 @@ debug-update version="v1.99.0":
|
|||||||
kill $SERVER_PID 2>/dev/null
|
kill $SERVER_PID 2>/dev/null
|
||||||
rm -rf "$tmp"
|
rm -rf "$tmp"
|
||||||
|
|
||||||
|
# test the changelog command (fetch, grouping, cache, rate limiting)
|
||||||
|
# usage: just debug-changelog v1.99.0
|
||||||
|
[group('debug')]
|
||||||
|
debug-changelog version="v1.99.0":
|
||||||
|
#!/bin/sh
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
body='## Changelog\n### Bug fixes\n* abc1234 fix something\n\n## Update\n```bash\niris update\n```\n'
|
||||||
|
printf '[{"tag_name":"%s","published_at":"2026-01-01T00:00:00Z","body":"%s","prerelease":false},{"tag_name":"v0.0.1","published_at":"2025-12-01T00:00:00Z","body":"## Changelog\\n### Documentation\\n* def5678 older release\\n","prerelease":false}]' "{{version}}" "$body" > "$tmp/response.json"
|
||||||
|
python3 -m http.server 19997 --directory "$tmp" 2>/dev/null &
|
||||||
|
SERVER_PID=$!
|
||||||
|
sleep 0.3
|
||||||
|
|
||||||
|
# isolate the changelog cache so this doesn't read or write the real
|
||||||
|
# ~/.cache/iris/changelog-cache.json
|
||||||
|
export XDG_CACHE_HOME="$tmp/cache"
|
||||||
|
|
||||||
|
echo "--- testing iris changelog (latest only) ---"
|
||||||
|
IRIS_CHANGELOG_URL="http://localhost:19997/response.json" ./iris changelog
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- testing iris changelog -n 2 ---"
|
||||||
|
IRIS_CHANGELOG_URL="http://localhost:19997/response.json" ./iris changelog -n 2
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- testing cache hit (server killed, should still work) ---"
|
||||||
|
kill $SERVER_PID 2>/dev/null
|
||||||
|
IRIS_CHANGELOG_URL="http://localhost:19997/response.json" ./iris changelog
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- testing --refresh with no server (should error, not hang) ---"
|
||||||
|
IRIS_CHANGELOG_URL="http://localhost:19997/response.json" ./iris changelog --refresh
|
||||||
|
|
||||||
|
rm -rf "$tmp"
|
||||||
|
|
||||||
# test the in-session update notification banner (requires iris.zsh hook to be active)
|
# test the in-session update notification banner (requires iris.zsh hook to be active)
|
||||||
# usage: just debug-notify v1.99.0
|
# usage: just debug-notify v1.99.0
|
||||||
[group('debug')]
|
[group('debug')]
|
||||||
debug-notify version="v1.99.0":
|
debug-notify version="v1.99.0":
|
||||||
IRIS_PID="" IRIS_MOCK_LATEST_VERSION="{{version}}" ./iris
|
IRIS_PID="" IRIS_MOCK_LATEST_VERSION="{{version}}" ./iris
|
||||||
|
|
||||||
|
# test the background auto-updater's escalation ladder (silent install,
|
||||||
|
# then repeated failures escalating to confirm, then giving up). fully
|
||||||
|
# offline - mocks the release API, install script, and release asset.
|
||||||
|
# usage: just debug-autoupdate
|
||||||
|
[group('debug')]
|
||||||
|
debug-autoupdate:
|
||||||
|
#!/bin/sh
|
||||||
|
PORT=19996
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
trap "kill \$SERVER_PID 2>/dev/null; pkill -f \"$TMP/iris\" 2>/dev/null; rm -rf $TMP" EXIT
|
||||||
|
|
||||||
|
ok() { PASS=$((PASS+1)); printf " \033[32m✓\033[0m %s\n" "$1"; }
|
||||||
|
fail() { FAIL=$((FAIL+1)); printf " \033[31m✗\033[0m %s\n" "$1"; }
|
||||||
|
|
||||||
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
MACH=$(uname -m)
|
||||||
|
case "$MACH" in
|
||||||
|
x86_64|amd64) ARCH="amd64" ;;
|
||||||
|
aarch64|arm64) ARCH="arm64" ;;
|
||||||
|
*) echo "unsupported arch: $MACH"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
ARCHIVE_NAME="iris_${OS}_${ARCH}.tar.gz"
|
||||||
|
|
||||||
|
echo "building old binary (v0.0.1)..."
|
||||||
|
go build -ldflags="-X github.com/versenilvis/iris/root.Version=v0.0.1" -o "$TMP/iris" ./cmd/iris
|
||||||
|
|
||||||
|
echo "building new binary (v1.99.0) + archive..."
|
||||||
|
mkdir -p "$TMP/new"
|
||||||
|
go build -ldflags="-X github.com/versenilvis/iris/root.Version=v1.99.0" -o "$TMP/new/iris" ./cmd/iris
|
||||||
|
tar -czf "$TMP/$ARCHIVE_NAME" -C "$TMP/new" iris
|
||||||
|
|
||||||
|
cp scripts/install.sh "$TMP/install.sh"
|
||||||
|
printf '#!/bin/sh\nexit 1\n' > "$TMP/install-fail.sh"
|
||||||
|
|
||||||
|
# --- mock server: release JSON, install script, and the tarball ---
|
||||||
|
python3 -c 'import sys, textwrap; exec(textwrap.dedent(r"""
|
||||||
|
import os, sys
|
||||||
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||||
|
|
||||||
|
port = int(sys.argv[1])
|
||||||
|
tmp_dir = sys.argv[2]
|
||||||
|
archive_name = sys.argv[3]
|
||||||
|
|
||||||
|
class mock_handler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == "/update-response.json":
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
body = "## Changelog\\n### Bug fixes\\n* abc1234 a test fix\\n"
|
||||||
|
resp = "{\"tag_name\":\"v1.99.0\",\"published_at\":\"2026-01-01T00:00:00Z\",\"body\":\"" + body + "\",\"prerelease\":false}"
|
||||||
|
self.wfile.write(resp.encode("utf-8"))
|
||||||
|
elif self.path in ("/install.sh", "/install-fail.sh"):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/plain")
|
||||||
|
self.end_headers()
|
||||||
|
with open(os.path.join(tmp_dir, self.path.lstrip("/")), "rb") as f:
|
||||||
|
self.wfile.write(f.read())
|
||||||
|
elif "/repos/versenilvis/iris/releases/latest" in self.path:
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/json")
|
||||||
|
self.end_headers()
|
||||||
|
download_url = "http://localhost:" + str(port) + "/" + archive_name
|
||||||
|
self.wfile.write(("{\"tag_name\":\"v1.99.0\",\"assets\":[{\"browser_download_url\":\"" + download_url + "\"}]}").encode("utf-8"))
|
||||||
|
elif self.path == "/" + archive_name:
|
||||||
|
filepath = os.path.join(tmp_dir, archive_name)
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/octet-stream")
|
||||||
|
self.end_headers()
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
self.wfile.write(f.read())
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def log_message(self, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
HTTPServer(("", port), mock_handler).serve_forever()
|
||||||
|
"""))' "$PORT" "$TMP" "$ARCHIVE_NAME" 2>/dev/null &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
i=0
|
||||||
|
until curl -sf "http://localhost:${PORT}/update-response.json" >/dev/null 2>&1; do
|
||||||
|
i=$((i+1))
|
||||||
|
[ $i -ge 30 ] && echo "mock server failed to start" && exit 1
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
mkdir -p "$TMP/cfg/iris"
|
||||||
|
cat > "$TMP/cfg/iris/config.toml" << 'CFGEOF'
|
||||||
|
[updater]
|
||||||
|
check-on-startup = true
|
||||||
|
channel = "stable"
|
||||||
|
check-interval = "0s"
|
||||||
|
auto-update = 1
|
||||||
|
CFGEOF
|
||||||
|
|
||||||
|
# fresh process = fresh check cycle, since check-interval is 0s
|
||||||
|
run_check() {
|
||||||
|
HOME="$TMP/home" \
|
||||||
|
XDG_CONFIG_HOME="$TMP/cfg" \
|
||||||
|
XDG_DATA_HOME="$TMP/data" \
|
||||||
|
BIN_DIR="$TMP/installed" \
|
||||||
|
IRIS_UPDATE_URL="http://localhost:${PORT}/update-response.json" \
|
||||||
|
IRIS_INSTALL_URL="$1" \
|
||||||
|
"$TMP/iris" >"$TMP/out.log" 2>&1 &
|
||||||
|
RUN_PID=$!
|
||||||
|
sleep 1.5
|
||||||
|
kill $RUN_PID 2>/dev/null
|
||||||
|
wait $RUN_PID 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
state_field() {
|
||||||
|
# tolerate BurntSushi/toml's indentation of nested-table fields
|
||||||
|
grep -E "^[[:space:]]*$1" "$TMP/data/iris/state.toml" 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- running test cases ---"
|
||||||
|
|
||||||
|
# --- test 1: attempt 1 installs silently and succeeds ---
|
||||||
|
printf "test 1: silent auto-install (attempt 1)... "
|
||||||
|
run_check "http://localhost:${PORT}/install.sh"
|
||||||
|
if [ -x "$TMP/installed/iris" ] && "$TMP/installed/iris" version 2>&1 | grep -q "v1.99.0"; then
|
||||||
|
ok "binary auto-installed to v1.99.0"
|
||||||
|
else
|
||||||
|
fail "binary was not installed; see $TMP/out.log"
|
||||||
|
fi
|
||||||
|
if state_field "auto-update-attempt" | grep -q " 0$"; then
|
||||||
|
ok "attempt counter cleared after success"
|
||||||
|
else
|
||||||
|
fail "expected attempt counter 0 after success, got: $(state_field auto-update-attempt)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- tests 2-5: forced failures escalate, then give up instead of looping ---
|
||||||
|
rm -rf "$TMP/data" "$TMP/installed"
|
||||||
|
|
||||||
|
printf "test 2: attempt 1 (forced failure)... "
|
||||||
|
run_check "http://localhost:${PORT}/install-fail.sh"
|
||||||
|
if state_field "auto-update-attempt" | grep -q " 1$"; then
|
||||||
|
ok "attempt counter is 1"
|
||||||
|
else
|
||||||
|
fail "expected attempt counter 1, got: $(state_field auto-update-attempt)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "test 3: attempt 2 (forced failure again, escalates)... "
|
||||||
|
run_check "http://localhost:${PORT}/install-fail.sh"
|
||||||
|
if state_field "auto-update-attempt" | grep -q " 2$"; then
|
||||||
|
ok "attempt counter is 2 (would show a confirm prompt)"
|
||||||
|
else
|
||||||
|
fail "expected attempt counter 2, got: $(state_field auto-update-attempt)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "test 4: attempt 3 (gives up)... "
|
||||||
|
run_check "http://localhost:${PORT}/install-fail.sh"
|
||||||
|
if state_field "auto-update-attempt" | grep -q " 3$"; then
|
||||||
|
ok "attempt counter is 3 (gave up)"
|
||||||
|
else
|
||||||
|
fail "expected attempt counter 3, got: $(state_field auto-update-attempt)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "test 5: attempt 4+ (stays given up, no infinite retry)... "
|
||||||
|
run_check "http://localhost:${PORT}/install-fail.sh"
|
||||||
|
if state_field "auto-update-attempt" | grep -q " 3$"; then
|
||||||
|
ok "attempt counter stayed at 3"
|
||||||
|
else
|
||||||
|
fail "expected attempt counter to stay at 3, got: $(state_field auto-update-attempt)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
printf "results: \033[32m%d passed\033[0m, \033[31m%d failed\033[0m\n" "$PASS" "$FAIL"
|
||||||
|
echo ""
|
||||||
|
echo "note: the confirm-prompt UI (auto-update=2, or auto-update=1's 2nd"
|
||||||
|
echo "attempt) is interactive and needs a real terminal with iris shell"
|
||||||
|
echo "hooks active to see rendered - set auto-update in your real config"
|
||||||
|
echo "and watch a live session for that part."
|
||||||
|
|
||||||
|
[ $FAIL -eq 0 ] || exit 1
|
||||||
|
|
||||||
# test the install script locally
|
# test the install script locally
|
||||||
# usage: just debug-install v1.0.0
|
# usage: just debug-install v1.0.0
|
||||||
[group('debug')]
|
[group('debug')]
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package root
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/versenilvis/iris/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const installScriptURL = "https://raw.githubusercontent.com/versenilvis/iris/main/scripts/install.sh"
|
||||||
|
|
||||||
|
func resolveInstallScriptURL() string {
|
||||||
|
if url := os.Getenv("IRIS_INSTALL_URL"); url != "" {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
return installScriptURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-interactive output is captured, not streamed - the wrapper's terminal
|
||||||
|
// is in raw mode, so streaming installer output would corrupt the display
|
||||||
|
func performUpdate(latest string, interactive bool) (output string, err error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
if !interactive {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, 5*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdRun := exec.CommandContext(ctx, "sh", "-c", "curl -sSL "+resolveInstallScriptURL()+" | sh")
|
||||||
|
if config.Get().Updater.Channel == "nightly" {
|
||||||
|
cmdRun.Env = append(os.Environ(), "IRIS_RELEASE_TAG="+latest)
|
||||||
|
}
|
||||||
|
|
||||||
|
if interactive {
|
||||||
|
cmdRun.Stdout = os.Stdout
|
||||||
|
cmdRun.Stderr = os.Stderr
|
||||||
|
cmdRun.Stdin = os.Stdin
|
||||||
|
return "", cmdRun.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
out, runErr := cmdRun.CombinedOutput()
|
||||||
|
return string(out), runErr
|
||||||
|
}
|
||||||
|
|
||||||
|
type autoUpdateAction int
|
||||||
|
|
||||||
|
const (
|
||||||
|
autoUpdateNotifyOnly autoUpdateAction = iota
|
||||||
|
autoUpdateInstallSilently
|
||||||
|
autoUpdateConfirm
|
||||||
|
autoUpdateGiveUp
|
||||||
|
)
|
||||||
|
|
||||||
|
// mode 1 escalates per target version: attempt 1 installs silently, attempt
|
||||||
|
// 2 asks first, attempt 3+ gives up. mode 2 always asks, never gives up
|
||||||
|
func decideAutoUpdateAction(mode int, latest string, st config.UpdaterState) autoUpdateAction {
|
||||||
|
if mode == 0 {
|
||||||
|
return autoUpdateNotifyOnly
|
||||||
|
}
|
||||||
|
if latest == st.DeclinedVersion {
|
||||||
|
return autoUpdateNotifyOnly
|
||||||
|
}
|
||||||
|
if mode == 2 {
|
||||||
|
return autoUpdateConfirm
|
||||||
|
}
|
||||||
|
|
||||||
|
attempt := st.AutoUpdateAttempt
|
||||||
|
if st.AutoUpdateTarget != latest {
|
||||||
|
attempt = 0
|
||||||
|
}
|
||||||
|
switch attempt {
|
||||||
|
case 0:
|
||||||
|
return autoUpdateInstallSilently
|
||||||
|
case 1:
|
||||||
|
return autoUpdateConfirm
|
||||||
|
default:
|
||||||
|
return autoUpdateGiveUp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
pendingAutoUpdateConfirm atomic.Bool
|
||||||
|
pendingAutoUpdateVersion atomic.Value
|
||||||
|
)
|
||||||
|
|
||||||
|
func armAutoUpdateConfirm(version string) {
|
||||||
|
pendingAutoUpdateVersion.Store(version)
|
||||||
|
pendingAutoUpdateConfirm.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reports whether the byte was consumed - callers must not fall through
|
||||||
|
// to normal key handling when true
|
||||||
|
func handleAutoUpdateConfirmKey(b byte) bool {
|
||||||
|
if !pendingAutoUpdateConfirm.Load() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch b {
|
||||||
|
case 'y', 'Y':
|
||||||
|
pendingAutoUpdateConfirm.Store(false)
|
||||||
|
version, _ := pendingAutoUpdateVersion.Load().(string)
|
||||||
|
go runConfirmedAutoUpdate(version)
|
||||||
|
case 'n', 'N', 0x1b, 0x03: // n, N, Esc, Ctrl+C
|
||||||
|
pendingAutoUpdateConfirm.Store(false)
|
||||||
|
version, _ := pendingAutoUpdateVersion.Load().(string)
|
||||||
|
declineAutoUpdate(version)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfirmedAutoUpdate(version string) {
|
||||||
|
if version == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeStdout([]byte("\r\033[K\033[36m[IRIS] updating...\033[0m\n"))
|
||||||
|
|
||||||
|
if _, err := performUpdate(version, false); err != nil {
|
||||||
|
writeStdout(fmt.Appendf(nil, "\033[31m[IRIS] update failed: %v\033[0m\n", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := config.LoadState()
|
||||||
|
state.Updater.AutoUpdateTarget = ""
|
||||||
|
state.Updater.AutoUpdateAttempt = 0
|
||||||
|
state.Updater.SeenVersion = ""
|
||||||
|
_ = config.SaveState(state)
|
||||||
|
|
||||||
|
writeStdout(fmt.Appendf(nil,
|
||||||
|
"\033[32m[IRIS] updated to %s, restart your terminal to use it\033[0m\n", version,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func declineAutoUpdate(version string) {
|
||||||
|
if version == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state := config.LoadState()
|
||||||
|
state.Updater.DeclinedVersion = version
|
||||||
|
state.Updater.AutoUpdateTarget = ""
|
||||||
|
state.Updater.AutoUpdateAttempt = 0
|
||||||
|
_ = config.SaveState(state)
|
||||||
|
|
||||||
|
writeStdout([]byte("\r\033[K\033[33m[IRIS] update declined, run `iris update` any time to install manually\033[0m\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printAutoUpdateInstalledNotice(latest string) {
|
||||||
|
writeStdout(fmt.Appendf(nil,
|
||||||
|
"\r\033[K\033[32m[IRIS] auto-updated %s → %s\033[0m\n\033[32m[IRIS] restart your terminal to use the new version\033[0m\n",
|
||||||
|
Version, latest,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printAutoUpdateConfirmPrompt(latest, notes string) {
|
||||||
|
var b strings.Builder
|
||||||
|
fmt.Fprintf(&b, "\r\033[K\033[33m[IRIS] new version %s → %s available\033[0m\n", Version, latest)
|
||||||
|
for _, line := range changelogSummaryLines(notes, 2) {
|
||||||
|
fmt.Fprintf(&b, "\033[33m - %s\033[0m\n", line)
|
||||||
|
}
|
||||||
|
fmt.Fprint(&b, "\033[33minstall now? [y/N] \033[0m")
|
||||||
|
writeStdout([]byte(b.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printAutoUpdateGiveUpNotice(latest string) {
|
||||||
|
writeStdout(fmt.Appendf(nil,
|
||||||
|
"\r\033[K\033[33m[IRIS] could not auto-update to %s after repeated attempts, run \033[1miris update\033[0m\033[33m to install manually\033[0m\n",
|
||||||
|
latest,
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package root
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/versenilvis/iris/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// captureStdout redirects os.Stdout for the duration of fn and returns
|
||||||
|
// everything written to it. writeStdout writes to os.Stdout directly, so
|
||||||
|
// this is the only way to observe it without a real terminal.
|
||||||
|
func captureStdout(t *testing.T, fn func()) string {
|
||||||
|
t.Helper()
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("os.Pipe: %v", err)
|
||||||
|
}
|
||||||
|
original := os.Stdout
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
fn()
|
||||||
|
|
||||||
|
_ = w.Close()
|
||||||
|
os.Stdout = original
|
||||||
|
out, _ := io.ReadAll(r)
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecideAutoUpdateActionOff(t *testing.T) {
|
||||||
|
st := config.UpdaterState{AutoUpdateTarget: "v1.1.0", AutoUpdateAttempt: 5}
|
||||||
|
if got := decideAutoUpdateAction(0, "v1.1.0", st); got != autoUpdateNotifyOnly {
|
||||||
|
t.Errorf("mode 0: expected notifyOnly, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecideAutoUpdateActionDeclinedVersionNeverRePrompted(t *testing.T) {
|
||||||
|
st := config.UpdaterState{DeclinedVersion: "v1.1.0"}
|
||||||
|
if got := decideAutoUpdateAction(1, "v1.1.0", st); got != autoUpdateNotifyOnly {
|
||||||
|
t.Errorf("mode 1, declined: expected notifyOnly, got %v", got)
|
||||||
|
}
|
||||||
|
if got := decideAutoUpdateAction(2, "v1.1.0", st); got != autoUpdateNotifyOnly {
|
||||||
|
t.Errorf("mode 2, declined: expected notifyOnly, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecideAutoUpdateActionModeTwoAlwaysConfirms(t *testing.T) {
|
||||||
|
cases := []config.UpdaterState{
|
||||||
|
{},
|
||||||
|
{AutoUpdateTarget: "v1.1.0", AutoUpdateAttempt: 0},
|
||||||
|
{AutoUpdateTarget: "v1.1.0", AutoUpdateAttempt: 1},
|
||||||
|
{AutoUpdateTarget: "v1.1.0", AutoUpdateAttempt: 3},
|
||||||
|
}
|
||||||
|
for _, st := range cases {
|
||||||
|
if got := decideAutoUpdateAction(2, "v1.1.0", st); got != autoUpdateConfirm {
|
||||||
|
t.Errorf("mode 2, state %+v: expected confirm, got %v", st, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecideAutoUpdateActionModeOneEscalationLadder(t *testing.T) {
|
||||||
|
// a different or absent target: this is a fresh version, try #1
|
||||||
|
if got := decideAutoUpdateAction(1, "v1.1.0", config.UpdaterState{}); got != autoUpdateInstallSilently {
|
||||||
|
t.Errorf("fresh target: expected installSilently, got %v", got)
|
||||||
|
}
|
||||||
|
if got := decideAutoUpdateAction(1, "v1.1.0", config.UpdaterState{AutoUpdateTarget: "v1.0.5", AutoUpdateAttempt: 2}); got != autoUpdateInstallSilently {
|
||||||
|
t.Errorf("different target: expected installSilently (attempt resets), got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// same target, one attempt already made: this is try #2
|
||||||
|
st := config.UpdaterState{AutoUpdateTarget: "v1.1.0", AutoUpdateAttempt: 1}
|
||||||
|
if got := decideAutoUpdateAction(1, "v1.1.0", st); got != autoUpdateConfirm {
|
||||||
|
t.Errorf("attempt 1 stored: expected confirm, got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// same target, two attempts already made: this is try #3, give up
|
||||||
|
st = config.UpdaterState{AutoUpdateTarget: "v1.1.0", AutoUpdateAttempt: 2}
|
||||||
|
if got := decideAutoUpdateAction(1, "v1.1.0", st); got != autoUpdateGiveUp {
|
||||||
|
t.Errorf("attempt 2 stored: expected giveUp, got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// already given up: stays given up, doesn't loop back to installing
|
||||||
|
st = config.UpdaterState{AutoUpdateTarget: "v1.1.0", AutoUpdateAttempt: 3}
|
||||||
|
if got := decideAutoUpdateAction(1, "v1.1.0", st); got != autoUpdateGiveUp {
|
||||||
|
t.Errorf("attempt 3 stored: expected giveUp (stays given up), got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecideAutoUpdateActionNewerVersionResetsLadder(t *testing.T) {
|
||||||
|
// gave up on v1.1.0, but v1.2.0 is a different target entirely - fresh start
|
||||||
|
st := config.UpdaterState{AutoUpdateTarget: "v1.1.0", AutoUpdateAttempt: 3}
|
||||||
|
if got := decideAutoUpdateAction(1, "v1.2.0", st); got != autoUpdateInstallSilently {
|
||||||
|
t.Errorf("newer target after giving up on an older one: expected installSilently, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintAutoUpdateInstalledNotice(t *testing.T) {
|
||||||
|
originalVersion := Version
|
||||||
|
Version = "v1.0.0"
|
||||||
|
t.Cleanup(func() { Version = originalVersion })
|
||||||
|
|
||||||
|
out := captureStdout(t, func() { printAutoUpdateInstalledNotice("v1.1.0") })
|
||||||
|
|
||||||
|
if !strings.Contains(out, "v1.0.0") || !strings.Contains(out, "v1.1.0") {
|
||||||
|
t.Errorf("expected the version transition, got %q", out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "restart your terminal") {
|
||||||
|
t.Errorf("expected a restart notice, got %q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintAutoUpdateConfirmPrompt(t *testing.T) {
|
||||||
|
originalVersion := Version
|
||||||
|
Version = "v1.0.0"
|
||||||
|
t.Cleanup(func() { Version = originalVersion })
|
||||||
|
|
||||||
|
out := captureStdout(t, func() {
|
||||||
|
printAutoUpdateConfirmPrompt("v1.1.0", "## Changelog\n### Bug fixes\n* abc1234 fix something\n")
|
||||||
|
})
|
||||||
|
|
||||||
|
if !strings.Contains(out, "v1.1.0") {
|
||||||
|
t.Errorf("expected the target version, got %q", out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "fix something") {
|
||||||
|
t.Errorf("expected the changelog summary, got %q", out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "install now?") {
|
||||||
|
t.Errorf("expected the confirm prompt text, got %q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintAutoUpdateGiveUpNotice(t *testing.T) {
|
||||||
|
out := captureStdout(t, func() { printAutoUpdateGiveUpNotice("v1.1.0") })
|
||||||
|
|
||||||
|
if !strings.Contains(out, "v1.1.0") {
|
||||||
|
t.Errorf("expected the target version, got %q", out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "iris update") {
|
||||||
|
t.Errorf("expected a pointer to manual iris update, got %q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeclineAutoUpdateRecordsState(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
t.Setenv("HOME", tmpDir)
|
||||||
|
t.Setenv("XDG_DATA_HOME", tmpDir+"/.local/share")
|
||||||
|
|
||||||
|
state := config.LoadState()
|
||||||
|
state.Updater.AutoUpdateTarget = "v1.1.0"
|
||||||
|
state.Updater.AutoUpdateAttempt = 2
|
||||||
|
if err := config.SaveState(state); err != nil {
|
||||||
|
t.Fatalf("SaveState: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
captureStdout(t, func() { declineAutoUpdate("v1.1.0") })
|
||||||
|
|
||||||
|
got := config.LoadState()
|
||||||
|
if got.Updater.DeclinedVersion != "v1.1.0" {
|
||||||
|
t.Errorf("expected DeclinedVersion v1.1.0, got %q", got.Updater.DeclinedVersion)
|
||||||
|
}
|
||||||
|
if got.Updater.AutoUpdateTarget != "" || got.Updater.AutoUpdateAttempt != 0 {
|
||||||
|
t.Errorf("expected the escalation ladder to be reset, got target=%q attempt=%d",
|
||||||
|
got.Updater.AutoUpdateTarget, got.Updater.AutoUpdateAttempt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleAutoUpdateConfirmKeyNotArmedIsNoop(t *testing.T) {
|
||||||
|
pendingAutoUpdateConfirm.Store(false)
|
||||||
|
|
||||||
|
if handleAutoUpdateConfirmKey('y') {
|
||||||
|
t.Error("expected handleAutoUpdateConfirmKey to report unhandled when nothing is pending")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleAutoUpdateConfirmKeyDeclineFlow(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
t.Setenv("HOME", tmpDir)
|
||||||
|
t.Setenv("XDG_DATA_HOME", tmpDir+"/.local/share")
|
||||||
|
t.Cleanup(func() { pendingAutoUpdateConfirm.Store(false) })
|
||||||
|
|
||||||
|
armAutoUpdateConfirm("v1.1.0")
|
||||||
|
|
||||||
|
captureStdout(t, func() {
|
||||||
|
if !handleAutoUpdateConfirmKey('n') {
|
||||||
|
t.Error("expected 'n' to be reported as handled while a confirm is pending")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if pendingAutoUpdateConfirm.Load() {
|
||||||
|
t.Error("expected the pending confirm flag to clear after declining")
|
||||||
|
}
|
||||||
|
if got := config.LoadState().Updater.DeclinedVersion; got != "v1.1.0" {
|
||||||
|
t.Errorf("expected the decline to be recorded, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleAutoUpdateConfirmKeySwallowsUnrecognizedBytes(t *testing.T) {
|
||||||
|
t.Cleanup(func() { pendingAutoUpdateConfirm.Store(false) })
|
||||||
|
armAutoUpdateConfirm("v1.1.0")
|
||||||
|
|
||||||
|
if !handleAutoUpdateConfirmKey('x') {
|
||||||
|
t.Error("expected an unrecognized byte to still be reported as consumed while pending")
|
||||||
|
}
|
||||||
|
if !pendingAutoUpdateConfirm.Load() {
|
||||||
|
t.Error("expected the prompt to stay armed until y/n/Esc/Ctrl+C")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
package root
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/glamour"
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/versenilvis/iris/internal/config"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Release struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
PublishedAt time.Time `json:"published_at"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
Prerelease bool `json:"prerelease"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type changelogCache struct {
|
||||||
|
FetchedAt time.Time `json:"fetched_at"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Releases []Release `json:"releases"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const changelogCacheTTL = time.Hour
|
||||||
|
|
||||||
|
func changelogCachePath() (string, error) {
|
||||||
|
dir, err := config.CachePath()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(dir, "changelog-cache.json"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadChangelogCache() (*changelogCache, error) {
|
||||||
|
path, err := changelogCachePath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var cache changelogCache
|
||||||
|
if err := json.Unmarshal(data, &cache); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cache, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveChangelogCache(releases []Release) {
|
||||||
|
path, err := changelogCachePath()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(changelogCache{FetchedAt: time.Now(), Channel: config.Get().Updater.Channel, Releases: releases})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = os.MkdirAll(filepath.Dir(path), 0o755)
|
||||||
|
_ = os.WriteFile(path, data, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchReleases(limit int) ([]Release, error) {
|
||||||
|
ctx, cancel := newGitHubRequestContext()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
endpoint := os.Getenv("IRIS_CHANGELOG_URL")
|
||||||
|
if endpoint == "" {
|
||||||
|
endpoint = "https://api.github.com/repos/versenilvis/iris/releases?per_page=100"
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := fetchGitHubBody(ctx, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var releases []Release
|
||||||
|
if err := json.Unmarshal(body, &releases); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Get().Updater.Channel != "nightly" {
|
||||||
|
filtered := releases[:0]
|
||||||
|
for _, r := range releases {
|
||||||
|
if !r.Prerelease {
|
||||||
|
filtered = append(filtered, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
releases = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
return truncateReleases(releases, limit), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateReleases(releases []Release, limit int) []Release {
|
||||||
|
if limit > 0 && len(releases) > limit {
|
||||||
|
return releases[:limit]
|
||||||
|
}
|
||||||
|
return releases
|
||||||
|
}
|
||||||
|
|
||||||
|
// falls back to a stale cache rather than failing outright when rate limited
|
||||||
|
func FetchReleasesCached(limit int, refresh bool) (releases []Release, rateLimited bool, err error) {
|
||||||
|
channel := config.Get().Updater.Channel
|
||||||
|
|
||||||
|
if !refresh {
|
||||||
|
if cache, cacheErr := loadChangelogCache(); cacheErr == nil && cache.Channel == channel && time.Since(cache.FetchedAt) < changelogCacheTTL {
|
||||||
|
return truncateReleases(cache.Releases, limit), false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fresh, fetchErr := FetchReleases(100)
|
||||||
|
if fetchErr != nil {
|
||||||
|
if errors.Is(fetchErr, ErrRateLimited) {
|
||||||
|
if cache, cacheErr := loadChangelogCache(); cacheErr == nil && cache.Channel == channel && len(cache.Releases) > 0 {
|
||||||
|
return truncateReleases(cache.Releases, limit), true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false, fetchErr
|
||||||
|
}
|
||||||
|
|
||||||
|
saveChangelogCache(fresh)
|
||||||
|
return truncateReleases(fresh, limit), false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseURL(tag string) string {
|
||||||
|
return "https://github.com/versenilvis/iris/releases/tag/" + tag
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
changelogRenderer *glamour.TermRenderer
|
||||||
|
changelogRendererOnce sync.Once
|
||||||
|
errChangelogRenderer error
|
||||||
|
)
|
||||||
|
|
||||||
|
func changelogRenderWidth() int {
|
||||||
|
if w, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && w > 0 && w < 120 {
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
return 120
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderChangelogMarkdown(body string) (string, error) {
|
||||||
|
changelogRendererOnce.Do(func() {
|
||||||
|
changelogRenderer, errChangelogRenderer = glamour.NewTermRenderer(
|
||||||
|
glamour.WithStandardStyle("dark"),
|
||||||
|
glamour.WithWordWrap(changelogRenderWidth()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if errChangelogRenderer != nil {
|
||||||
|
return "", errChangelogRenderer
|
||||||
|
}
|
||||||
|
return changelogRenderer.Render(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripRedundantHeading(body string) string {
|
||||||
|
lines := strings.Split(body, "\n")
|
||||||
|
filtered := lines[:0]
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.TrimSpace(line) == "## Changelog" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, line)
|
||||||
|
}
|
||||||
|
return strings.Join(filtered, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// judged post-strip: glamour's margin padding has color codes but no glyphs
|
||||||
|
func isBlankLine(line string) bool {
|
||||||
|
return strings.TrimSpace(ansi.Strip(line)) == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func squeezeBlankLines(s string) string {
|
||||||
|
lines := strings.Split(s, "\n")
|
||||||
|
out := make([]string, 0, len(lines))
|
||||||
|
prevBlank := false
|
||||||
|
for _, line := range lines {
|
||||||
|
blank := isBlankLine(line)
|
||||||
|
if blank && prevBlank {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, line)
|
||||||
|
prevBlank = blank
|
||||||
|
}
|
||||||
|
return strings.Join(out, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimBlankLines(s string) string {
|
||||||
|
lines := strings.Split(s, "\n")
|
||||||
|
start := 0
|
||||||
|
for start < len(lines) && isBlankLine(lines[start]) {
|
||||||
|
start++
|
||||||
|
}
|
||||||
|
end := len(lines)
|
||||||
|
for end > start && isBlankLine(lines[end-1]) {
|
||||||
|
end--
|
||||||
|
}
|
||||||
|
return strings.Join(lines[start:end], "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// the CTA is rendered separately by printRelease based on IsNewer, so the
|
||||||
|
// GoReleaser footer is cut before handing the body to glamour
|
||||||
|
func printChangelogBody(out io.Writer, body string) {
|
||||||
|
if idx := strings.Index(body, "## Update"); idx != -1 {
|
||||||
|
body = body[:idx]
|
||||||
|
}
|
||||||
|
body = strings.TrimSpace(stripRedundantHeading(body))
|
||||||
|
if body == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered, err := renderChangelogMarkdown(body)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(out, body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintln(out, trimBlankLines(squeezeBlankLines(rendered)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRelease(out io.Writer, release Release, showUpdateCTA bool) {
|
||||||
|
label := release.TagName
|
||||||
|
if strings.TrimPrefix(release.TagName, "v") == strings.TrimPrefix(Version, "v") {
|
||||||
|
label += " (current)"
|
||||||
|
}
|
||||||
|
|
||||||
|
date := release.PublishedAt.Format("2006-01-02")
|
||||||
|
fmt.Fprintf(out, "\033[1;36mIRIS %s - %s\033[0m\n", label, date)
|
||||||
|
fmt.Fprintf(out, "\033[2m%s\033[0m\n\n", releaseURL(release.TagName))
|
||||||
|
|
||||||
|
printChangelogBody(out, release.Body)
|
||||||
|
|
||||||
|
if showUpdateCTA && IsNewer(Version, release.TagName) {
|
||||||
|
fmt.Fprintln(out)
|
||||||
|
fmt.Fprintln(out, "run `iris update` to install")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
changelogCount int
|
||||||
|
changelogRefresh bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var ChangelogCmd = &cobra.Command{
|
||||||
|
Use: "changelog [version]",
|
||||||
|
Short: "show what changed in recent iris releases",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
limit := changelogCount
|
||||||
|
if len(args) == 1 {
|
||||||
|
limit = 0 // searching a specific version, not just the most recent N
|
||||||
|
}
|
||||||
|
|
||||||
|
releases, rateLimited, err := FetchReleasesCached(limit, changelogRefresh)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrRateLimited) {
|
||||||
|
fmt.Fprintln(cmd.ErrOrStderr(), "\033[31m[IRIS] rate limited by GitHub API, try again later\033[0m")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(cmd.ErrOrStderr(), "\033[31m[IRIS] could not fetch changelog: %v\033[0m\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(releases) == 0 {
|
||||||
|
cmd.Println("no releases found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := cmd.OutOrStdout()
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
target := strings.TrimPrefix(args[0], "v")
|
||||||
|
for _, release := range releases {
|
||||||
|
if strings.TrimPrefix(release.TagName, "v") == target {
|
||||||
|
printRelease(out, release, true)
|
||||||
|
if rateLimited {
|
||||||
|
fmt.Fprintln(out)
|
||||||
|
fmt.Fprintln(out, "\033[33m[IRIS] rate limited by GitHub API, showing cached data\033[0m")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(cmd.ErrOrStderr(), "\033[31m[IRIS] release %s not found\033[0m\n", args[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, release := range releases {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Fprintln(out)
|
||||||
|
}
|
||||||
|
printRelease(out, release, i == 0)
|
||||||
|
}
|
||||||
|
if rateLimited {
|
||||||
|
fmt.Fprintln(out)
|
||||||
|
fmt.Fprintln(out, "\033[33m[IRIS] rate limited by GitHub API, showing cached data\033[0m")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ChangelogCmd.Flags().IntVarP(&changelogCount, "count", "n", 1, "number of releases to show")
|
||||||
|
ChangelogCmd.Flags().BoolVar(&changelogRefresh, "refresh", false, "bypass the cache and fetch fresh data")
|
||||||
|
rootCmd.AddCommand(ChangelogCmd)
|
||||||
|
}
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
package root
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
"github.com/versenilvis/iris/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFetchReleasesFiltersPrereleasesOnStableChannel(t *testing.T) {
|
||||||
|
originalConfig := config.Get()
|
||||||
|
t.Cleanup(func() { config.Init(originalConfig) })
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Updater.Channel = "stable"
|
||||||
|
config.Init(cfg)
|
||||||
|
|
||||||
|
releases := []Release{
|
||||||
|
{TagName: "v0.6.0-nightly.abc", Prerelease: true, Body: "nightly"},
|
||||||
|
{TagName: "v0.5.2", Prerelease: false, Body: "stable"},
|
||||||
|
{TagName: "v0.5.1", Prerelease: false, Body: "stable"},
|
||||||
|
}
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(releases)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
t.Setenv("IRIS_CHANGELOG_URL", srv.URL)
|
||||||
|
|
||||||
|
got, err := FetchReleases(0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchReleases: %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("expected 2 stable releases, got %d: %+v", len(got), got)
|
||||||
|
}
|
||||||
|
for _, r := range got {
|
||||||
|
if r.Prerelease {
|
||||||
|
t.Errorf("prerelease %q leaked into stable channel results", r.TagName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchReleasesLimitsCount(t *testing.T) {
|
||||||
|
originalConfig := config.Get()
|
||||||
|
t.Cleanup(func() { config.Init(originalConfig) })
|
||||||
|
config.Init(config.DefaultConfig())
|
||||||
|
|
||||||
|
releases := []Release{
|
||||||
|
{TagName: "v0.5.2"}, {TagName: "v0.5.1"}, {TagName: "v0.5.0"},
|
||||||
|
}
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(releases)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
t.Setenv("IRIS_CHANGELOG_URL", srv.URL)
|
||||||
|
|
||||||
|
got, err := FetchReleases(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchReleases: %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("expected limit of 2, got %d", len(got))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedChangelogCache(t *testing.T, fetchedAt time.Time, releases []Release) {
|
||||||
|
t.Helper()
|
||||||
|
path, err := changelogCachePath()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("changelogCachePath: %v", err)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(changelogCache{FetchedAt: fetchedAt, Channel: config.Get().Updater.Channel, Releases: releases})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal cache: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatalf("mkdir cache dir: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||||
|
t.Fatalf("write cache: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchReleasesCachedServesWithinTTL(t *testing.T) {
|
||||||
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||||
|
seedChangelogCache(t, time.Now(), []Release{{TagName: "v1.0.0", Body: "cached"}})
|
||||||
|
|
||||||
|
// point at an address nothing listens on: a network call here is a test failure
|
||||||
|
t.Setenv("IRIS_CHANGELOG_URL", "http://127.0.0.1:1/unreachable")
|
||||||
|
|
||||||
|
got, rateLimited, err := FetchReleasesCached(1, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchReleasesCached: %v", err)
|
||||||
|
}
|
||||||
|
if rateLimited {
|
||||||
|
t.Error("expected rateLimited=false for a fresh cache hit")
|
||||||
|
}
|
||||||
|
if len(got) != 1 || got[0].TagName != "v1.0.0" {
|
||||||
|
t.Fatalf("expected cached release, got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchReleasesCachedChannelChangeBypassesStaleCache(t *testing.T) {
|
||||||
|
originalConfig := config.Get()
|
||||||
|
t.Cleanup(func() { config.Init(originalConfig) })
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Updater.Channel = "nightly"
|
||||||
|
config.Init(cfg)
|
||||||
|
|
||||||
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||||
|
seedChangelogCache(t, time.Now(), []Release{{TagName: "v1.0.0-nightly.abc", Body: "nightly cache", Prerelease: true}})
|
||||||
|
|
||||||
|
cfg.Updater.Channel = "stable"
|
||||||
|
config.Init(cfg)
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode([]Release{{TagName: "v1.0.0", Body: "stable release"}})
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
t.Setenv("IRIS_CHANGELOG_URL", srv.URL)
|
||||||
|
|
||||||
|
got, _, err := FetchReleasesCached(1, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchReleasesCached: %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != 1 || got[0].TagName != "v1.0.0" {
|
||||||
|
t.Fatalf("expected the channel switch to bypass the nightly-channel cache and fetch fresh, got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchReleasesCachedRefreshBypassesTTL(t *testing.T) {
|
||||||
|
originalConfig := config.Get()
|
||||||
|
t.Cleanup(func() { config.Init(originalConfig) })
|
||||||
|
config.Init(config.DefaultConfig())
|
||||||
|
|
||||||
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||||
|
seedChangelogCache(t, time.Now(), []Release{{TagName: "v1.0.0", Body: "stale-but-fresh"}})
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode([]Release{{TagName: "v2.0.0", Body: "fresh"}})
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
t.Setenv("IRIS_CHANGELOG_URL", srv.URL)
|
||||||
|
|
||||||
|
got, _, err := FetchReleasesCached(1, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchReleasesCached: %v", err)
|
||||||
|
}
|
||||||
|
if len(got) != 1 || got[0].TagName != "v2.0.0" {
|
||||||
|
t.Fatalf("expected --refresh to bypass the cache, got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchReleasesCachedRateLimitFallsBackToStaleCache(t *testing.T) {
|
||||||
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||||
|
seedChangelogCache(t, time.Now().Add(-2*changelogCacheTTL), []Release{{TagName: "v1.0.0", Body: "stale"}})
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
t.Setenv("IRIS_CHANGELOG_URL", srv.URL)
|
||||||
|
|
||||||
|
got, rateLimited, err := FetchReleasesCached(1, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FetchReleasesCached: %v", err)
|
||||||
|
}
|
||||||
|
if !rateLimited {
|
||||||
|
t.Error("expected rateLimited=true when the API 403s and a stale cache exists")
|
||||||
|
}
|
||||||
|
if len(got) != 1 || got[0].TagName != "v1.0.0" {
|
||||||
|
t.Fatalf("expected stale cached release, got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchReleasesCachedRateLimitNoCacheReturnsError(t *testing.T) {
|
||||||
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
t.Setenv("IRIS_CHANGELOG_URL", srv.URL)
|
||||||
|
|
||||||
|
_, _, err := FetchReleasesCached(1, false)
|
||||||
|
if !errors.Is(err, ErrRateLimited) {
|
||||||
|
t.Fatalf("expected ErrRateLimited, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintReleaseSuppressesUpdateCTAWhenCurrent(t *testing.T) {
|
||||||
|
originalVersion := Version
|
||||||
|
Version = "v1.0.0"
|
||||||
|
t.Cleanup(func() { Version = originalVersion })
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
printRelease(&buf, Release{TagName: "v1.0.0", Body: "## Changelog\n### Bug fixes\n* abc1234 fix something\n"}, true)
|
||||||
|
|
||||||
|
if strings.Contains(buf.String(), "run `iris update`") {
|
||||||
|
t.Error("expected no update CTA when already on the latest version")
|
||||||
|
}
|
||||||
|
if !strings.Contains(buf.String(), "(current)") {
|
||||||
|
t.Error("expected the version label to be marked (current)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintReleaseShowsUpdateCTAWhenNewer(t *testing.T) {
|
||||||
|
originalVersion := Version
|
||||||
|
Version = "v0.9.0"
|
||||||
|
t.Cleanup(func() { Version = originalVersion })
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
printRelease(&buf, Release{TagName: "v1.0.0", Body: "## Changelog\n### Bug fixes\n* abc1234 fix something\n"}, true)
|
||||||
|
|
||||||
|
if !strings.Contains(buf.String(), "run `iris update`") {
|
||||||
|
t.Error("expected an update CTA when a newer release is shown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintChangelogBodyStopsAtUpdateFooter(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
body := "## Changelog\n### Bug fixes\n* abc1234 fix something\n## Update\n```bash\niris update\n```\n"
|
||||||
|
printChangelogBody(&buf, body)
|
||||||
|
plain := ansi.Strip(buf.String())
|
||||||
|
|
||||||
|
if strings.Contains(plain, "Update") {
|
||||||
|
t.Error("expected the GoReleaser footer to be excluded from rendered body")
|
||||||
|
}
|
||||||
|
if !strings.Contains(plain, "fix something") {
|
||||||
|
t.Errorf("expected the changelog entry to be rendered, got %q", plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintChangelogBodyRendersArbitraryMarkdown(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
body := "# 🌙 **Nightly build** - `v0.6.0-nightly.abc123`\nNightly release from commit: abc123\n"
|
||||||
|
printChangelogBody(&buf, body)
|
||||||
|
plain := ansi.Strip(buf.String())
|
||||||
|
|
||||||
|
if !strings.Contains(plain, "Nightly build") {
|
||||||
|
t.Errorf("expected the body rendered via glamour, got %q", plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintChangelogBodyEmptyBodyPrintsNothing(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
printChangelogBody(&buf, "")
|
||||||
|
|
||||||
|
if buf.Len() != 0 {
|
||||||
|
t.Errorf("expected no output for an empty body, got %q", buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintChangelogBodyStripsRedundantHeadingAndBlankRuns(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
body := "## Changelog\n### Bug fixes\n* abc1234 fix something\n"
|
||||||
|
printChangelogBody(&buf, body)
|
||||||
|
plain := ansi.Strip(buf.String())
|
||||||
|
|
||||||
|
if strings.Contains(plain, "Changelog") {
|
||||||
|
t.Errorf("expected the redundant ## Changelog heading to be stripped, got %q", plain)
|
||||||
|
}
|
||||||
|
if strings.Contains(buf.String(), "\n\n\n") {
|
||||||
|
t.Errorf("expected consecutive blank lines to be squeezed, got %q", buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStripRedundantHeadingRemovesExactGoReleaserLabelOnly(t *testing.T) {
|
||||||
|
got := stripRedundantHeading("## Changelog\n### Bug fixes\n* item\n")
|
||||||
|
want := "### Bug fixes\n* item\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStripRedundantHeadingPreservesHandWrittenHeadings(t *testing.T) {
|
||||||
|
body := "## Theme is here\n<img src=\"...\" />\n"
|
||||||
|
got := stripRedundantHeading(body)
|
||||||
|
if got != body {
|
||||||
|
t.Errorf("expected a hand-written ## heading to be preserved, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintChangelogBodyPreservesHandWrittenAnnouncement(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
body := "## Theme is here\r\n<img width=\"1818\" src=\"...\" />\r\n\r\n## Update\r\n```bash\r\niris update\r\n```\r\n"
|
||||||
|
printChangelogBody(&buf, body)
|
||||||
|
plain := ansi.Strip(buf.String())
|
||||||
|
|
||||||
|
if !strings.Contains(plain, "Theme is here") {
|
||||||
|
t.Errorf("expected the hand-written release heading to be rendered, got %q", plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSqueezeBlankLinesCollapsesRuns(t *testing.T) {
|
||||||
|
got := squeezeBlankLines("a\n\n\n\nb\n")
|
||||||
|
want := "a\n\nb\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChangelogCmdShowsSpecificVersion(t *testing.T) {
|
||||||
|
originalConfig := config.Get()
|
||||||
|
t.Cleanup(func() { config.Init(originalConfig) })
|
||||||
|
config.Init(config.DefaultConfig())
|
||||||
|
|
||||||
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||||
|
|
||||||
|
releases := []Release{
|
||||||
|
{TagName: "v0.5.2", Body: "## Changelog\n### Bug fixes\n* xyz9999 unrelated fix\n"},
|
||||||
|
{TagName: "v0.5.1", Body: "## Changelog\n### Bug fixes\n* abc1234 fix mode\n"},
|
||||||
|
}
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(releases)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
t.Setenv("IRIS_CHANGELOG_URL", srv.URL)
|
||||||
|
|
||||||
|
var out, errOut bytes.Buffer
|
||||||
|
ChangelogCmd.SetOut(&out)
|
||||||
|
ChangelogCmd.SetErr(&errOut)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ChangelogCmd.SetOut(nil)
|
||||||
|
ChangelogCmd.SetErr(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
ChangelogCmd.Run(ChangelogCmd, []string{"v0.5.1"})
|
||||||
|
got := ansi.Strip(out.String())
|
||||||
|
|
||||||
|
if !strings.Contains(got, "v0.5.1") {
|
||||||
|
t.Fatalf("expected the requested version in output, got %q", got)
|
||||||
|
}
|
||||||
|
if strings.Contains(got, "v0.5.2") || strings.Contains(got, "unrelated fix") {
|
||||||
|
t.Errorf("expected only the requested version, got %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "fix mode") {
|
||||||
|
t.Errorf("expected the matched release's body, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChangelogCmdVersionNotFound(t *testing.T) {
|
||||||
|
originalConfig := config.Get()
|
||||||
|
t.Cleanup(func() { config.Init(originalConfig) })
|
||||||
|
config.Init(config.DefaultConfig())
|
||||||
|
|
||||||
|
t.Setenv("XDG_CACHE_HOME", t.TempDir())
|
||||||
|
|
||||||
|
releases := []Release{{TagName: "v0.5.2", Body: "latest"}}
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(releases)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
t.Setenv("IRIS_CHANGELOG_URL", srv.URL)
|
||||||
|
|
||||||
|
var out, errOut bytes.Buffer
|
||||||
|
ChangelogCmd.SetOut(&out)
|
||||||
|
ChangelogCmd.SetErr(&errOut)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ChangelogCmd.SetOut(nil)
|
||||||
|
ChangelogCmd.SetErr(nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
ChangelogCmd.Run(ChangelogCmd, []string{"v9.9.9"})
|
||||||
|
|
||||||
|
if !strings.Contains(errOut.String(), "not found") {
|
||||||
|
t.Errorf("expected a not-found error, got stdout=%q stderr=%q", out.String(), errOut.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,6 +98,9 @@ channel = "stable"
|
|||||||
# interval between update checks, e.g. "24h", "6h", "30m"
|
# interval between update checks, e.g. "24h", "6h", "30m"
|
||||||
check-interval = "24h"
|
check-interval = "24h"
|
||||||
|
|
||||||
|
# 0 = off (default, notify only), 1 = auto-install, 2 = always confirm first
|
||||||
|
auto-update = 0
|
||||||
|
|
||||||
[keybindings]
|
[keybindings]
|
||||||
toggle-mode = "ctrl+r"
|
toggle-mode = "ctrl+r"
|
||||||
toggle-menu = "shift+tab"
|
toggle-menu = "shift+tab"
|
||||||
|
|||||||
@@ -282,6 +282,9 @@ channel = "stable"
|
|||||||
# interval between update checks, e.g. "24h", "6h", "30m"
|
# interval between update checks, e.g. "24h", "6h", "30m"
|
||||||
check-interval = "24h"
|
check-interval = "24h"
|
||||||
|
|
||||||
|
# 0 = off (default, notify only), 1 = auto-install, 2 = always confirm first
|
||||||
|
auto-update = 0
|
||||||
|
|
||||||
[keybindings]
|
[keybindings]
|
||||||
toggle-mode = "ctrl+r"
|
toggle-mode = "ctrl+r"
|
||||||
toggle-menu = "shift+tab"
|
toggle-menu = "shift+tab"
|
||||||
|
|||||||
+151
-60
@@ -3,11 +3,11 @@ package root
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -16,18 +16,59 @@ import (
|
|||||||
"github.com/versenilvis/iris/internal/config"
|
"github.com/versenilvis/iris/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type updateResultKind int
|
||||||
|
|
||||||
|
const (
|
||||||
|
updateResultNotify updateResultKind = iota
|
||||||
|
updateResultAutoInstalled
|
||||||
|
updateResultConfirm
|
||||||
|
updateResultGiveUp
|
||||||
|
)
|
||||||
|
|
||||||
// updateResult is passed from the async checker to the main loop
|
// updateResult is passed from the async checker to the main loop
|
||||||
type updateResult struct {
|
type updateResult struct {
|
||||||
|
kind updateResultKind
|
||||||
latestVersion string
|
latestVersion string
|
||||||
|
notes string
|
||||||
hasUpdate bool
|
hasUpdate bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// pendingUpdate is set by the background goroutine and consumed once after the first IRIS_CMD_STOP
|
// pendingUpdate is set by the background goroutine and consumed once after the first IRIS_CMD_STOP
|
||||||
var pendingUpdate chan updateResult
|
var pendingUpdate chan updateResult
|
||||||
|
|
||||||
// FetchLatestVersion hits the GitHub Releases API and returns the latest tag name
|
// ErrRateLimited is returned when the GitHub API responds 403/429.
|
||||||
func FetchLatestVersion() (string, error) {
|
var ErrRateLimited = errors.New("rate limited by GitHub API")
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
|
func newGitHubRequestContext() (context.Context, context.CancelFunc) {
|
||||||
|
return context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchGitHubBody(ctx context.Context, endpoint string) ([]byte, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Accept", "application/vnd.github+json")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusTooManyRequests {
|
||||||
|
return nil, ErrRateLimited
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchLatestRelease hits the GitHub Releases API and returns the latest release
|
||||||
|
func FetchLatestRelease() (Release, error) {
|
||||||
|
ctx, cancel := newGitHubRequestContext()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
endpoint := os.Getenv("IRIS_UPDATE_URL")
|
endpoint := os.Getenv("IRIS_UPDATE_URL")
|
||||||
@@ -39,50 +80,39 @@ func FetchLatestVersion() (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
body, err := fetchGitHubBody(ctx, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return Release{}, err
|
||||||
}
|
|
||||||
req.Header.Set("Accept", "application/vnd.github+json")
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Get().Updater.Channel == "nightly" && os.Getenv("IRIS_UPDATE_URL") == "" {
|
if config.Get().Updater.Channel == "nightly" && os.Getenv("IRIS_UPDATE_URL") == "" {
|
||||||
var releases []struct {
|
var releases []Release
|
||||||
TagName string `json:"tag_name"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(body, &releases); err != nil {
|
if err := json.Unmarshal(body, &releases); err != nil {
|
||||||
return "", err
|
return Release{}, err
|
||||||
}
|
}
|
||||||
if len(releases) == 0 {
|
if len(releases) == 0 {
|
||||||
return "", fmt.Errorf("no releases found")
|
return Release{}, fmt.Errorf("no releases found")
|
||||||
}
|
}
|
||||||
return releases[0].TagName, nil
|
return releases[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var result struct {
|
var release Release
|
||||||
TagName string `json:"tag_name"`
|
if err := json.Unmarshal(body, &release); err != nil {
|
||||||
|
return Release{}, err
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &result); err != nil {
|
if release.TagName == "" {
|
||||||
|
return Release{}, fmt.Errorf("no tag_name in response")
|
||||||
|
}
|
||||||
|
return release, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchLatestVersion hits the GitHub Releases API and returns the latest tag name
|
||||||
|
func FetchLatestVersion() (string, error) {
|
||||||
|
release, err := FetchLatestRelease()
|
||||||
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if result.TagName == "" {
|
return release.TagName, nil
|
||||||
return "", fmt.Errorf("no tag_name in response")
|
|
||||||
}
|
|
||||||
return result.TagName, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsNewer returns true if latest is a newer semantic version than current.
|
// IsNewer returns true if latest is a newer semantic version than current.
|
||||||
@@ -175,40 +205,111 @@ func startBackgroundUpdateCheck() chan updateResult {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
latest, err := FetchLatestVersion()
|
release, err := FetchLatestRelease()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// no network or API error: silently do nothing
|
// no network or API error: silently do nothing
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
latest := release.TagName
|
||||||
|
|
||||||
// update the last check time regardless of result
|
// update the last check time regardless of result
|
||||||
state.Updater.LastCheckTime = time.Now()
|
state.Updater.LastCheckTime = time.Now()
|
||||||
|
|
||||||
if IsNewer(Version, latest) {
|
if IsNewer(Version, latest) {
|
||||||
// only notify if user hasn't already seen this specific version notification
|
mode := config.Get().Updater.AutoUpdate
|
||||||
if state.Updater.SeenVersion != latest {
|
switch decideAutoUpdateAction(mode, latest, state.Updater) {
|
||||||
ch <- updateResult{latestVersion: latest, hasUpdate: true}
|
case autoUpdateNotifyOnly:
|
||||||
|
// only notify if user hasn't already seen this specific version notification
|
||||||
|
if state.Updater.SeenVersion != latest {
|
||||||
|
ch <- updateResult{kind: updateResultNotify, latestVersion: latest, notes: release.Body, hasUpdate: true}
|
||||||
|
}
|
||||||
|
// save the latest as seen_version so future sessions don't re-notify
|
||||||
|
// unless a NEWER version comes out (different tag)
|
||||||
|
state.Updater.SeenVersion = latest
|
||||||
|
_ = config.SaveState(state)
|
||||||
|
return
|
||||||
|
|
||||||
|
case autoUpdateInstallSilently:
|
||||||
|
state.Updater.AutoUpdateTarget = latest
|
||||||
|
state.Updater.AutoUpdateAttempt = 1
|
||||||
|
// write before installing so a crash still counts as an attempt
|
||||||
|
_ = config.SaveState(state)
|
||||||
|
if _, installErr := performUpdate(latest, false); installErr == nil {
|
||||||
|
state.Updater.AutoUpdateTarget = ""
|
||||||
|
state.Updater.AutoUpdateAttempt = 0
|
||||||
|
state.Updater.SeenVersion = ""
|
||||||
|
_ = config.SaveState(state)
|
||||||
|
ch <- updateResult{kind: updateResultAutoInstalled, latestVersion: latest, notes: release.Body, hasUpdate: true}
|
||||||
|
}
|
||||||
|
// on failure: state already recorded attempt 1 for this
|
||||||
|
// target, so the next check escalates to a confirm prompt
|
||||||
|
return
|
||||||
|
|
||||||
|
case autoUpdateConfirm:
|
||||||
|
nextAttempt := 1
|
||||||
|
if state.Updater.AutoUpdateTarget == latest {
|
||||||
|
nextAttempt = state.Updater.AutoUpdateAttempt + 1
|
||||||
|
}
|
||||||
|
state.Updater.AutoUpdateTarget = latest
|
||||||
|
state.Updater.AutoUpdateAttempt = nextAttempt
|
||||||
|
_ = config.SaveState(state)
|
||||||
|
ch <- updateResult{kind: updateResultConfirm, latestVersion: latest, notes: release.Body, hasUpdate: true}
|
||||||
|
return
|
||||||
|
|
||||||
|
case autoUpdateGiveUp:
|
||||||
|
// only announce the exact transition into giving up, not
|
||||||
|
// every cycle after
|
||||||
|
announce := state.Updater.AutoUpdateAttempt == 2
|
||||||
|
state.Updater.AutoUpdateAttempt = 3
|
||||||
|
_ = config.SaveState(state)
|
||||||
|
if announce {
|
||||||
|
ch <- updateResult{kind: updateResultGiveUp, latestVersion: latest, hasUpdate: true}
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// save the latest as seen_version so future sessions don't re-notify
|
|
||||||
// unless a NEWER version comes out (different tag)
|
|
||||||
state.Updater.SeenVersion = latest
|
|
||||||
} else {
|
|
||||||
// up to date: clear the seen_version flag so the next update triggers a fresh notification
|
|
||||||
state.Updater.SeenVersion = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// up to date, or nothing left to do: clear update-related state so
|
||||||
|
// the next detected version starts a fresh notify/attempt cycle
|
||||||
|
state.Updater.SeenVersion = ""
|
||||||
|
state.Updater.AutoUpdateTarget = ""
|
||||||
|
state.Updater.AutoUpdateAttempt = 0
|
||||||
_ = config.SaveState(state)
|
_ = config.SaveState(state)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
// printUpdateNotice writes the one-time update message to stdout
|
func changelogSummaryLines(body string, max int) []string {
|
||||||
func printUpdateNotice(latest string) {
|
var lines []string
|
||||||
fmt.Printf(
|
for line := range strings.SplitSeq(body, "\n") {
|
||||||
|
line = strings.TrimRight(line, "\r")
|
||||||
|
if !strings.HasPrefix(line, "* ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entry := strings.TrimPrefix(line, "* ")
|
||||||
|
_, msg, ok := strings.Cut(entry, " ")
|
||||||
|
if !ok {
|
||||||
|
msg = entry
|
||||||
|
}
|
||||||
|
lines = append(lines, msg)
|
||||||
|
if len(lines) >= max {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func printUpdateNotice(latest, notes string) {
|
||||||
|
var b strings.Builder
|
||||||
|
fmt.Fprintf(&b,
|
||||||
"\r\033[K\033[33m[IRIS] new version %s → %s available, run \033[1miris update\033[0m\033[33m to upgrade\033[0m\n",
|
"\r\033[K\033[33m[IRIS] new version %s → %s available, run \033[1miris update\033[0m\033[33m to upgrade\033[0m\n",
|
||||||
Version, latest,
|
Version, latest,
|
||||||
)
|
)
|
||||||
|
for _, line := range changelogSummaryLines(notes, 2) {
|
||||||
|
fmt.Fprintf(&b, "\033[33m - %s\033[0m\n", line)
|
||||||
|
}
|
||||||
|
writeStdout([]byte(b.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -247,23 +348,13 @@ var updateCmd = &cobra.Command{
|
|||||||
|
|
||||||
fmt.Printf("\033[36m[IRIS] updating %s → %s\033[0m\n", Version, latest)
|
fmt.Printf("\033[36m[IRIS] updating %s → %s\033[0m\n", Version, latest)
|
||||||
|
|
||||||
// download and replace the binary using the install script
|
|
||||||
installScript := "https://raw.githubusercontent.com/versenilvis/iris/main/scripts/install.sh"
|
|
||||||
command := "curl -sSL " + installScript + " | sh"
|
|
||||||
runningPrefix := ""
|
runningPrefix := ""
|
||||||
if config.Get().Updater.Channel == "nightly" {
|
if config.Get().Updater.Channel == "nightly" {
|
||||||
runningPrefix = fmt.Sprintf("IRIS_RELEASE_TAG=%s ", latest)
|
runningPrefix = fmt.Sprintf("IRIS_RELEASE_TAG=%s ", latest)
|
||||||
}
|
}
|
||||||
fmt.Printf("running: %s%s\n\n", runningPrefix, command)
|
fmt.Printf("running: %scurl -sSL %s | sh\n\n", runningPrefix, resolveInstallScriptURL())
|
||||||
|
|
||||||
cmdRun := exec.Command("sh", "-c", command)
|
if _, err := performUpdate(latest, true); err != nil {
|
||||||
if config.Get().Updater.Channel == "nightly" {
|
|
||||||
cmdRun.Env = append(os.Environ(), "IRIS_RELEASE_TAG="+latest)
|
|
||||||
}
|
|
||||||
cmdRun.Stdout = os.Stdout
|
|
||||||
cmdRun.Stderr = os.Stderr
|
|
||||||
cmdRun.Stdin = os.Stdin
|
|
||||||
if err := cmdRun.Run(); err != nil {
|
|
||||||
fmt.Printf("\n\033[31m[IRIS] update failed: %v\033[0m\n", err)
|
fmt.Printf("\n\033[31m[IRIS] update failed: %v\033[0m\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package root
|
package root
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -76,3 +78,78 @@ func TestUpdateState(t *testing.T) {
|
|||||||
t.Errorf("Expected LastCheck %v, got %v", state.Updater.LastCheckTime, loaded.Updater.LastCheckTime)
|
t.Errorf("Expected LastCheck %v, got %v", state.Updater.LastCheckTime, loaded.Updater.LastCheckTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChangelogSummaryLines(t *testing.T) {
|
||||||
|
body := "## Changelog\n### Bug fixes\n* abc1234 fix something\n* def5678 fix another thing\n* ghi9012 a third fix\n## Update\n```bash\niris update\n```\n"
|
||||||
|
|
||||||
|
got := changelogSummaryLines(body, 2)
|
||||||
|
want := []string{"fix something", "fix another thing"}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("expected %d lines, got %d: %v", len(want), len(got), got)
|
||||||
|
}
|
||||||
|
for i, line := range want {
|
||||||
|
if got[i] != line {
|
||||||
|
t.Errorf("line %d: expected %q, got %q", i, line, got[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChangelogSummaryLinesEmptyBody(t *testing.T) {
|
||||||
|
if got := changelogSummaryLines("", 2); len(got) != 0 {
|
||||||
|
t.Errorf("expected no lines for an empty body, got %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintUpdateNoticeIncludesChangelogSummary(t *testing.T) {
|
||||||
|
originalVersion := Version
|
||||||
|
Version = "v1.0.0"
|
||||||
|
t.Cleanup(func() { Version = originalVersion })
|
||||||
|
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("os.Pipe: %v", err)
|
||||||
|
}
|
||||||
|
originalStdout := os.Stdout
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
printUpdateNotice("v1.1.0", "## Changelog\n### Bug fixes\n* abc1234 fix something\n* def5678 fix another thing\n* ghi9012 a third fix\n")
|
||||||
|
|
||||||
|
_ = w.Close()
|
||||||
|
os.Stdout = originalStdout
|
||||||
|
out, _ := io.ReadAll(r)
|
||||||
|
got := string(out)
|
||||||
|
|
||||||
|
if !strings.Contains(got, "v1.0.0") || !strings.Contains(got, "v1.1.0") {
|
||||||
|
t.Errorf("expected the version transition in the notice, got %q", got)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got, "fix something") || !strings.Contains(got, "fix another thing") {
|
||||||
|
t.Errorf("expected the first two changelog entries in the notice, got %q", got)
|
||||||
|
}
|
||||||
|
if strings.Contains(got, "a third fix") {
|
||||||
|
t.Errorf("expected the summary to cap at 2 entries, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintUpdateNoticeWithoutNotes(t *testing.T) {
|
||||||
|
originalVersion := Version
|
||||||
|
Version = "v1.0.0"
|
||||||
|
t.Cleanup(func() { Version = originalVersion })
|
||||||
|
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("os.Pipe: %v", err)
|
||||||
|
}
|
||||||
|
originalStdout := os.Stdout
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
printUpdateNotice("v1.1.0", "")
|
||||||
|
|
||||||
|
_ = w.Close()
|
||||||
|
os.Stdout = originalStdout
|
||||||
|
out, _ := io.ReadAll(r)
|
||||||
|
got := string(out)
|
||||||
|
|
||||||
|
if !strings.Contains(got, "v1.1.0") {
|
||||||
|
t.Errorf("expected the notice to still show without changelog notes, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+17
-1
@@ -555,7 +555,17 @@ func runWrapper() {
|
|||||||
select {
|
select {
|
||||||
case result, ok := <-pendingUpdate:
|
case result, ok := <-pendingUpdate:
|
||||||
if ok && result.hasUpdate {
|
if ok && result.hasUpdate {
|
||||||
printUpdateNotice(result.latestVersion)
|
switch result.kind {
|
||||||
|
case updateResultAutoInstalled:
|
||||||
|
printAutoUpdateInstalledNotice(result.latestVersion)
|
||||||
|
case updateResultConfirm:
|
||||||
|
printAutoUpdateConfirmPrompt(result.latestVersion, result.notes)
|
||||||
|
armAutoUpdateConfirm(result.latestVersion)
|
||||||
|
case updateResultGiveUp:
|
||||||
|
printAutoUpdateGiveUpNotice(result.latestVersion)
|
||||||
|
default:
|
||||||
|
printUpdateNotice(result.latestVersion, result.notes)
|
||||||
|
}
|
||||||
updatePrinted = true
|
updatePrinted = true
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -778,6 +788,12 @@ func runWrapper() {
|
|||||||
b := inputSlice[i]
|
b := inputSlice[i]
|
||||||
intercepted = false
|
intercepted = false
|
||||||
|
|
||||||
|
// while an auto-update confirm prompt is pending, every
|
||||||
|
// byte goes to it instead of normal key handling
|
||||||
|
if handleAutoUpdateConfirmKey(b) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.ToggleMenu); matched {
|
if matched, consumed := config.MatchKey(inputSlice[i:], config.Get().Keybindings.ToggleMenu); matched {
|
||||||
intercepted = true
|
intercepted = true
|
||||||
suggestionsEnabled = !suggestionsEnabled
|
suggestionsEnabled = !suggestionsEnabled
|
||||||
|
|||||||
Reference in New Issue
Block a user