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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,41 @@ jobs:
- run: pnpm --filter @repot/site test
- run: pnpm --filter @repot/site build

# The CLI install script (apps/site/public/install.sh, served by the site). It runs on
# every change: the script lives under apps/site/, which the site-only shortcut skips.
install-script:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
- name: Lint the install script
if: runner.os == 'Linux'
run: shellcheck -s sh apps/site/public/install.sh
# Offline: a local stand-in for GitHub Releases (latest redirect, checksum mismatch, 404).
- run: node --test scripts/install-sh/*.test.mjs
# Online: the real latest release, piped to sh as the site tells users to.
- name: Install the latest release
shell: bash
run: |
sh < apps/site/public/install.sh
~/.local/bin/repot --version
- name: Install on other Linux distributions
if: runner.os == 'Linux'
shell: bash
run: |
for image in debian:stable fedora:latest archlinux:latest; do
echo "== $image"
docker run --rm -v "$PWD/apps/site/public/install.sh:/install.sh:ro" "$image" sh -c '
command -v curl > /dev/null || { apt-get update -qq && apt-get install -yqq curl ca-certificates > /dev/null; }
sh < /install.sh && ~/.local/bin/repot --version'
done

scripts:
needs: changes
if: needs.changes.outputs.site_only != 'true'
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
| `pnpm package-map:gen candidates` / `run` | verified (2026-11-03, 보고할 문제가 있으면 exit 1) | 계획 10 (로컬 전용, 네트워크와 `JEB_AI_HUB_KEY` 필요, CI에서 돌리지 않는다) |
| `pnpm package-map:gen apply-review` (에이전트 결정은 `--reviewer agent`, provenance `agent-review`) | verified (2026-11-03, `--reviewer agent`로 실제 반영) | 계획 10 |
| `cargo run -p repot-cli -- scan` / `export --dry-run` | verified (exit 0, 2026-11-04) | 계획 15 Task 3 (실제 기기를 읽기만 함) |
| `node --test scripts/install-sh/*.test.mjs` / `shellcheck -s sh apps/site/public/install.sh` | verified (exit 0, 2026-11-06) | CLI 설치 스크립트 (로컬 가짜 릴리스만 쓴다, Windows에서는 돌리지 않는다) |
| `pnpm check` (검사만, CI는 `pnpm exec biome ci .`) / `pnpm fix` (포맷·린트 자동 수정) | verified (exit 0, 2026-11-04) | Biome 도입 |

- 툴체인은 `rust-toolchain.toml`(stable, clippy, rustfmt)과 pnpm workspace를 따른다.
Expand Down
7 changes: 7 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ scoop bucket add repot https://github.com/junjuny0227/scoop-bucket
scoop install repot/repot
```

Linux や macOS で Homebrew を使わない場合は、インストールスクリプトを使います。この機器に合うリリースの
アーカイブをダウンロードし、SHA-256 で確認して `repot` を `~/.local/bin` に置きます([先に読む](https://github.com/junjuny0227/Repot/blob/main/apps/site/public/install.sh))。

```sh
curl -fsSL https://junjuny0227.github.io/Repot/install.sh | sh
```

どちらもリリースの公開時に更新されるので、`brew upgrade` / `scoop update repot` で最新に保てます。
パッケージマネージャーを使わない場合は [GitHub Releases](https://github.com/junjuny0227/Repot/releases/latest) から
`repot-cli-<バージョン>-<target>.tar.gz`(macOS、Linux)または `.zip`(Windows)を
Expand Down
7 changes: 7 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ scoop bucket add repot https://github.com/junjuny0227/scoop-bucket
scoop install repot/repot
```

Linux나 macOS에서 Homebrew 없이 설치하려면 설치 스크립트를 씁니다. 이 기기에 맞는 릴리스 압축 파일을
받아 SHA-256으로 확인하고 `repot`을 `~/.local/bin`에 넣습니다([먼저 읽어 보기](https://github.com/junjuny0227/Repot/blob/main/apps/site/public/install.sh)).

```sh
curl -fsSL https://junjuny0227.github.io/Repot/install.sh | sh
```

릴리스가 공개되면 둘 다 갱신되므로 `brew upgrade` / `scoop update repot`으로 최신을 유지합니다.
패키지 매니저 없이 쓰려면 [GitHub Releases](https://github.com/junjuny0227/Repot/releases/latest)에서
`repot-cli-<버전>-<target>.tar.gz`(macOS, Linux)나 `.zip`(Windows)을 받아 `.sha256`
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ scoop bucket add repot https://github.com/junjuny0227/scoop-bucket
scoop install repot/repot
```

Without Homebrew on Linux or macOS, the install script downloads the release archive for
your machine, checks its SHA-256 and puts `repot` in `~/.local/bin`
([read it first](https://github.com/junjuny0227/Repot/blob/main/apps/site/public/install.sh)):

```sh
curl -fsSL https://junjuny0227.github.io/Repot/install.sh | sh
```

Both are updated when a release is published, so `brew upgrade` / `scoop update repot`
keep it current. Without a package manager, download
`repot-cli-<version>-<target>.tar.gz` (macOS, Linux) or `.zip` (Windows) from
Expand Down
7 changes: 7 additions & 0 deletions README.zh-Hans.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ scoop bucket add repot https://github.com/junjuny0227/scoop-bucket
scoop install repot/repot
```

在 Linux 或 macOS 上不使用 Homebrew 时,可以使用安装脚本。它会下载适合本机的发布压缩包,
用 SHA-256 校验后把 `repot` 放到 `~/.local/bin`([先阅读脚本](https://github.com/junjuny0227/Repot/blob/main/apps/site/public/install.sh)):

```sh
curl -fsSL https://junjuny0227.github.io/Repot/install.sh | sh
```

发布新版本时两者都会更新,因此可用 `brew upgrade` / `scoop update repot` 保持最新。
不使用包管理器时,从 [GitHub Releases](https://github.com/junjuny0227/Repot/releases/latest) 下载
`repot-cli-<版本>-<target>.tar.gz`(macOS、Linux)或 `.zip`(Windows),用 `.sha256`
Expand Down
136 changes: 136 additions & 0 deletions apps/site/public/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/bin/sh
# Installs the repot command-line tool from a GitHub release:
#
# curl -fsSL https://junjuny0227.github.io/Repot/install.sh | sh
#
# It downloads the archive for this OS and CPU, checks it against the release's .sha256 file
# and copies `repot` into ~/.local/bin. Nothing else on the machine is changed: no sudo, no
# shell profile edits. Read it first: https://github.com/junjuny0227/Repot/blob/main/apps/site/public/install.sh
#
# Settings (environment variables):
# REPOT_VERSION version to install, e.g. 0.9.0 (default: the latest release)
# REPOT_INSTALL_DIR where to put repot (default: ~/.local/bin)
#
# The source of this file is apps/site/public/install.sh; the site publishes it as is.

set -eu

# Everything runs from main(), called on the last line, so a download cut short by the
# network cannot run half a script.
main() {
releases="${REPOT_RELEASES_URL:-https://github.com/junjuny0227/Repot/releases}"
install_dir="${REPOT_INSTALL_DIR:-$HOME/.local/bin}"

need curl
need tar
need uname
need mktemp

target=$(detect_target)
version=$(resolve_version "$releases")
name="repot-cli-$version-$target"
url="$releases/download/v$version/$name.tar.gz"

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

say "Downloading repot $version for $target"
curl -fsSL -o "$tmp/$name.tar.gz" "$url" || fail "could not download $url"
curl -fsSL -o "$tmp/$name.tar.gz.sha256" "$url.sha256" || fail "could not download $url.sha256"

expected=$(cut -d ' ' -f 1 <"$tmp/$name.tar.gz.sha256")
actual=$(sha256 "$tmp/$name.tar.gz")
if [ -z "$expected" ] || [ "$expected" != "$actual" ]; then
fail "checksum mismatch for $name.tar.gz (expected $expected, got $actual); nothing was installed"
fi

tar -xzf "$tmp/$name.tar.gz" -C "$tmp"
[ -f "$tmp/$name/repot" ] || fail "$name.tar.gz has no repot in it; nothing was installed"

mkdir -p "$install_dir"
# Copy next to the target, then rename: a running repot is never left half-written.
cp "$tmp/$name/repot" "$install_dir/.repot.new"
chmod 755 "$install_dir/.repot.new"
mv -f "$install_dir/.repot.new" "$install_dir/repot"

say "Installed $("$install_dir/repot" --version) to $install_dir/repot"
path_hint "$install_dir"
}

say() { printf '%s\n' "$1"; }

fail() {
printf 'repot install: %s\n' "$1" >&2
exit 1
}

need() {
command -v "$1" >/dev/null 2>&1 || fail "needs $1, which was not found"
}

# Release archives exist for these targets only (see release.yml).
detect_target() {
case "$(uname -s)" in
Linux) os=unknown-linux-gnu ;;
Darwin) os=apple-darwin ;;
*) fail "no prebuilt repot for $(uname -s); see https://github.com/junjuny0227/Repot/blob/main/crates/cli/README.md" ;;
esac
case "$(uname -m)" in
x86_64 | amd64) arch=x86_64 ;;
arm64 | aarch64) arch=aarch64 ;;
*) arch=$(uname -m) ;;
esac
case "$arch-$os" in
x86_64-unknown-linux-gnu | x86_64-apple-darwin | aarch64-apple-darwin) printf '%s\n' "$arch-$os" ;;
*) fail "no prebuilt repot for $arch-$os yet; with Rust, run: cargo install --git https://github.com/junjuny0227/Repot repot-cli" ;;
esac
}

# REPOT_VERSION, or the tag that /releases/latest redirects to (no API call, no token).
resolve_version() {
if [ -n "${REPOT_VERSION:-}" ]; then
printf '%s\n' "${REPOT_VERSION#v}"
return
fi
latest=$(curl -fsSLI -o /dev/null -w '%{url_effective}' "$1/latest") ||
fail "could not reach $1/latest"
case "$latest" in
*/tag/v*) printf '%s\n' "${latest##*/tag/v}" ;;
*) fail "could not find the latest release at $1/latest" ;;
esac
}

sha256() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | cut -d ' ' -f 1
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | cut -d ' ' -f 1
else
fail "needs sha256sum or shasum to check the download"
fi
}

# Says how to put the folder on PATH; changing shell profiles is left to the user.
path_hint() {
case ":$PATH:" in
*":$1:"*) return ;;
esac
# The ~ is printed for the user to type, not expanded here.
# shellcheck disable=SC2088
case "$(basename "${SHELL:-sh}")" in
zsh) profile='~/.zshrc' ;;
bash) profile='~/.bashrc' ;;
fish)
say ""
say "$1 is not on your PATH. To add it, run: fish_add_path $1"
return
;;
*) profile='~/.profile' ;;
esac
say ""
say "$1 is not on your PATH. To add it, run:"
say " echo 'export PATH=\"$1:\$PATH\"' >> $profile"
say "then open a new terminal."
}

main "$@"
13 changes: 8 additions & 5 deletions apps/site/src/__tests__/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,22 @@ test('the hero shows the CLI install command for the detected platform, with a c
}
});

test('Linux gets the release archive, not Homebrew, with Homebrew as the alternative', () => {
test('Linux gets the install script, not Homebrew, with Homebrew as the alternative', () => {
const linux = {
userAgent: 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0',
};
const html = render(linux);
const archive = `curl -fsSL https://github.com/junjuny0227/Repot/releases/download/v${VERSION}/repot-cli-${VERSION}-x86_64-unknown-linux-gnu.tar.gz | tar -xz -C ~/.local/bin --strip-components=1 repot-cli-${VERSION}-x86_64-unknown-linux-gnu/repot`;
const script = '<code>curl -fsSL https://junjuny0227.github.io/Repot/install.sh | sh</code>';
const hero = html.slice(0, html.indexOf('id="how"'));
expect(hero).toContain(archive);
expect(hero).toContain(script);
expect(hero).not.toContain('brew install');
const install = html.slice(html.indexOf('id="install"'));
expect(install).toContain(archive);
expect(install).toContain('~/.local/bin. If your shell does not find it');
expect(install).toContain(script);
expect(install).toContain('checks the download against its SHA-256');
expect(install).toContain('<code>brew install junjuny0227/tap/repot</code>');
expect(install).toContain(
'href="https://github.com/junjuny0227/Repot/blob/main/apps/site/public/install.sh"',
);
});

test('every command on the page is copyable', () => {
Expand Down
13 changes: 7 additions & 6 deletions apps/site/src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@
// links, product and tool names, and terminal commands stay here because they are not translated.
// Facts come from README.md and the MVP spec only.
import { createContext, useContext } from 'react';
import { type AssetId, type DesktopOs, VERSION } from './downloads';
import { format, formatList, type Locale, messagesFor } from './i18n';
import type { AssetId, DesktopOs } from './downloads';
import { BASE, format, formatList, type Locale, messagesFor, SITE_ORIGIN } from './i18n';
import type { PrincipleKey, ToolKey } from './icons';

const REPO = 'https://github.com/junjuny0227/Repot';

const BREW = 'brew install junjuny0227/tap/repot';
const LINUX_TARGET = `repot-cli-${VERSION}-x86_64-unknown-linux-gnu`;
// apps/site/public/install.sh, published with the site.
const INSTALL_SCRIPT = `${SITE_ORIGIN}${BASE}install.sh`;

// How to install the repot CLI on each OS; the hero and the install section show the same one.
// Few Linux users have Homebrew, so Linux unpacks the release archive into ~/.local/bin instead.
const CLI_COMMAND: Record<DesktopOs, string> = {
mac: BREW,
linux: `mkdir -p ~/.local/bin && curl -fsSL ${REPO}/releases/download/v${VERSION}/${LINUX_TARGET}.tar.gz | tar -xz -C ~/.local/bin --strip-components=1 ${LINUX_TARGET}/repot`,
linux: `curl -fsSL ${INSTALL_SCRIPT} | sh`,
windows:
'scoop bucket add repot https://github.com/junjuny0227/scoop-bucket\nscoop install repot/repot',
};
Expand Down Expand Up @@ -75,6 +74,7 @@ export function content(locale: Locale) {
version: (version: string) => format(m.hero.version, { version }),
cliCommand: CLI_COMMAND,
brewCommand: BREW,
installScriptSource: `${REPO}/blob/main/apps/site/public/install.sh`,
},

how: {
Expand Down Expand Up @@ -106,6 +106,7 @@ export function content(locale: Locale) {
cliGuideUrl: `${REPO}/blob/main/crates/cli/README${locale === 'en' ? '' : `.${locale}`}.md`,
cliCommand: CLI_COMMAND,
brewCommand: BREW,
installScriptSource: `${REPO}/blob/main/apps/site/public/install.sh`,
},

footer: {
Expand Down
3 changes: 2 additions & 1 deletion apps/site/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@
"cliTitle": "Command-line tool.",
"cliText": "The repot command does the same scan, comparison and export in a terminal. To install it:",
"cliGuide": "How to use the command-line tool",
"cliLinuxNote": "This puts repot in ~/.local/bin. If your shell does not find it, add that folder to PATH. With Homebrew, run this instead:"
"cliLinuxNote": "The script checks the download against its SHA-256, puts repot in ~/.local/bin and tells you if that folder is not on PATH. It changes nothing else. With Homebrew, run this instead:",
"installScript": "Read the install script"
},
"footer": {
"summary": "Repot is free and open source. Apache-2.0 license.",
Expand Down
3 changes: 2 additions & 1 deletion apps/site/src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@
"cliTitle": "コマンドラインツール。",
"cliText": "repot コマンドで同じスキャン、比較、書き出しをターミナルで行えます。インストールするには:",
"cliGuide": "コマンドラインツールの使い方",
"cliLinuxNote": "repot を ~/.local/bin に置きます。シェルで見つからない場合は、このフォルダを PATH に追加してください。Homebrew を使う場合は、代わりにこのコマンドを実行します。"
"cliLinuxNote": "スクリプトはダウンロードを SHA-256 で確認し、repot を ~/.local/bin に置きます。このフォルダが PATH にない場合は知らせます。ほかには何も変更しません。Homebrew を使う場合は、代わりにこのコマンドを実行します。",
"installScript": "インストールスクリプトを読む"
},
"footer": {
"summary": "Repot は無料のオープンソースソフトウェアで、Apache-2.0 ライセンスで公開しています。",
Expand Down
3 changes: 2 additions & 1 deletion apps/site/src/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@
"cliTitle": "명령줄 도구.",
"cliText": "repot 명령으로 같은 스캔·비교·내보내기를 터미널에서 할 수 있습니다. 설치 명령:",
"cliGuide": "명령줄 도구 사용법",
"cliLinuxNote": "repot을 ~/.local/bin에 넣습니다. 셸에서 찾지 못하면 이 폴더를 PATH에 추가하세요. Homebrew를 쓴다면 대신 이 명령을 실행하세요."
"cliLinuxNote": "스크립트는 내려받은 파일을 SHA-256으로 확인하고 repot을 ~/.local/bin에 넣습니다. 이 폴더가 PATH에 없으면 알려 주며, 그 밖에는 아무것도 바꾸지 않습니다. Homebrew를 쓴다면 대신 이 명령을 실행하세요.",
"installScript": "설치 스크립트 보기"
},
"footer": {
"summary": "Repot은 무료 오픈 소스이며 Apache-2.0 라이선스로 배포됩니다.",
Expand Down
3 changes: 2 additions & 1 deletion apps/site/src/locales/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@
"cliTitle": "命令行工具。",
"cliText": "repot 命令可在终端中完成同样的扫描、比较和导出。安装命令:",
"cliGuide": "命令行工具使用指南",
"cliLinuxNote": "这会把 repot 放到 ~/.local/bin。如果 shell 找不到它,请把该文件夹加入 PATH。使用 Homebrew 的话,可以改为运行:"
"cliLinuxNote": "脚本会用 SHA-256 校验下载的文件,把 repot 放到 ~/.local/bin,并在该文件夹不在 PATH 中时提示你。除此之外不做任何更改。使用 Homebrew 的话,可以改为运行:",
"installScript": "查看安装脚本"
},
"footer": {
"summary": "Repot 是免费的开源软件,基于 Apache-2.0 许可证发布。",
Expand Down
5 changes: 5 additions & 0 deletions apps/site/src/sections/Install.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export function Install({ platform }: { platform: Platform }) {
<p>
<a href={C.install.cliGuideUrl}>{C.install.cliGuide}</a>
</p>
{os === 'linux' && (
<p>
<a href={C.install.installScriptSource}>{C.install.installScript}</a>
</p>
)}
</div>
</div>
<div>
Expand Down
1 change: 1 addition & 0 deletions crates/cli/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ SSH 接続、自分のスクリプトから行えます。
| | |
| --- | --- |
| macOS、Linux(Homebrew) | `brew install junjuny0227/tap/repot` |
| Linux、macOS(インストールスクリプト) | `curl -fsSL https://junjuny0227.github.io/Repot/install.sh \| sh` ([スクリプト](https://github.com/junjuny0227/Repot/blob/main/apps/site/public/install.sh)) |
| Windows(Scoop) | `scoop bucket add repot https://github.com/junjuny0227/scoop-bucket` のあと `scoop install repot/repot` |
| すべての OS(手動) | [Releases](https://github.com/junjuny0227/Repot/releases/latest) から `repot-cli-<バージョン>-<target>.tar.gz` / `.zip` をダウンロードし、`.sha256` ファイルで確認してから `repot` を `PATH` に置きます |
| Rust がある場合 | `cargo install --git https://github.com/junjuny0227/Repot repot-cli` |
Expand Down
1 change: 1 addition & 0 deletions crates/cli/README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
| | |
| --- | --- |
| macOS, Linux (Homebrew) | `brew install junjuny0227/tap/repot` |
| Linux, macOS(설치 스크립트) | `curl -fsSL https://junjuny0227.github.io/Repot/install.sh \| sh` ([스크립트](https://github.com/junjuny0227/Repot/blob/main/apps/site/public/install.sh)) |
| Windows (Scoop) | `scoop bucket add repot https://github.com/junjuny0227/scoop-bucket` 다음에 `scoop install repot/repot` |
| 모든 OS, 직접 설치 | [Releases](https://github.com/junjuny0227/Repot/releases/latest)에서 `repot-cli-<버전>-<target>.tar.gz` / `.zip`을 받아 `.sha256` 파일로 확인한 뒤 `repot`을 `PATH`에 둡니다 |
| Rust가 있으면 | `cargo install --git https://github.com/junjuny0227/Repot repot-cli` |
Expand Down
1 change: 1 addition & 0 deletions crates/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Command output is in English.
| | |
| --- | --- |
| macOS, Linux (Homebrew) | `brew install junjuny0227/tap/repot` |
| Linux, macOS (install script) | `curl -fsSL https://junjuny0227.github.io/Repot/install.sh \| sh` ([source](https://github.com/junjuny0227/Repot/blob/main/apps/site/public/install.sh)) |
| Windows (Scoop) | `scoop bucket add repot https://github.com/junjuny0227/scoop-bucket` then `scoop install repot/repot` |
| Any OS, by hand | Download `repot-cli-<version>-<target>.tar.gz` / `.zip` from [Releases](https://github.com/junjuny0227/Repot/releases/latest), check it against its `.sha256` file and put `repot` on your `PATH` |
| With Rust | `cargo install --git https://github.com/junjuny0227/Repot repot-cli` |
Expand Down
Loading
Loading