From 1901818f54296d3f2e4ce76271878e2942d1a9e6 Mon Sep 17 00:00:00 2001 From: junjuny0227 Date: Fri, 6 Nov 2026 11:37:17 +0900 Subject: [PATCH 1/4] feat(cli): add an install script for Linux and macOS --- .github/workflows/ci.yml | 35 ++++++++ AGENTS.md | 1 + apps/site/public/install.sh | 135 ++++++++++++++++++++++++++++ scripts/install-sh/install.test.mjs | 132 +++++++++++++++++++++++++++ 4 files changed, 303 insertions(+) create mode 100644 apps/site/public/install.sh create mode 100644 scripts/install-sh/install.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0b6185..b27d28f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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' diff --git a/AGENTS.md b/AGENTS.md index 011bf6f..98ea6aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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를 따른다. diff --git a/apps/site/public/install.sh b/apps/site/public/install.sh new file mode 100644 index 0000000..7234f8e --- /dev/null +++ b/apps/site/public/install.sh @@ -0,0 +1,135 @@ +#!/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") + [ -n "$expected" ] && [ "$expected" = "$actual" ] || + fail "checksum mismatch for $name.tar.gz (expected $expected, got $actual); nothing was installed" + + 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 "$@" diff --git a/scripts/install-sh/install.test.mjs b/scripts/install-sh/install.test.mjs new file mode 100644 index 0000000..1ecef81 --- /dev/null +++ b/scripts/install-sh/install.test.mjs @@ -0,0 +1,132 @@ +// Runs apps/site/public/install.sh against a local stand-in for GitHub Releases, so no +// network is used. CI also runs the script against the real latest release (ci.yml, +// `install-script`). Needs a POSIX shell, so it lives outside scripts/*.test.mjs, which the +// Windows package-map job runs. +import assert from 'node:assert/strict'; +import { execFileSync, spawn } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { + chmodSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + writeFileSync, +} from 'node:fs'; +import { createServer } from 'node:http'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { after, before, test } from 'node:test'; + +const SCRIPT = new URL('../../apps/site/public/install.sh', import.meta.url).pathname; +const os = { Linux: 'unknown-linux-gnu', Darwin: 'apple-darwin' }[ + execFileSync('uname', ['-s'], { encoding: 'utf8' }).trim() +]; +const arch = { x86_64: 'x86_64', arm64: 'aarch64', aarch64: 'aarch64' }[ + execFileSync('uname', ['-m'], { encoding: 'utf8' }).trim() +]; +const TARGET = `${arch}-${os}`; + +const work = mkdtempSync(join(tmpdir(), 'repot-install-')); +const files = new Map(); // URL path -> contents +let server; +let base; + +// A release with a stand-in `repot` that prints its version, like the real one. +function release(version, { badChecksum = false } = {}) { + const name = `repot-cli-${version}-${TARGET}`; + const dir = join(work, 'build', version); + mkdirSync(join(dir, name), { recursive: true }); + writeFileSync(join(dir, name, 'repot'), `#!/bin/sh\necho "repot ${version}"\n`); + chmodSync(join(dir, name, 'repot'), 0o755); + writeFileSync(join(dir, name, 'LICENSE'), 'license\n'); + execFileSync('tar', ['-czf', `${name}.tar.gz`, name], { cwd: dir }); + const archive = readFileSync(join(dir, `${name}.tar.gz`)); + const sum = badChecksum ? '0'.repeat(64) : createHash('sha256').update(archive).digest('hex'); + files.set(`/download/v${version}/${name}.tar.gz`, archive); + files.set(`/download/v${version}/${name}.tar.gz.sha256`, `${sum} ${name}.tar.gz\n`); +} + +before(async () => { + release('9.9.9'); + release('1.0.0'); + release('6.6.6', { badChecksum: true }); + server = createServer((req, res) => { + if (req.url === '/latest') { + res.writeHead(302, { location: `${base}/tag/v9.9.9` }).end(); + } else if (req.url === '/tag/v9.9.9') { + res.writeHead(200).end('release page'); + } else if (files.has(req.url)) { + res.writeHead(200).end(files.get(req.url)); + } else { + res.writeHead(404).end(); + } + }); + await new Promise((r) => server.listen(0, '127.0.0.1', r)); + base = `http://127.0.0.1:${server.address().port}`; +}); +after(() => server.closeAllConnections() || server.close()); + +// Async on purpose: spawnSync would block the event loop that serves the downloads. +function install(env = {}) { + const home = mkdtempSync(join(work, 'home-')); + const child = spawn('sh', [SCRIPT], { + env: { + PATH: process.env.PATH, + HOME: home, + SHELL: '/bin/zsh', + REPOT_RELEASES_URL: base, + ...env, + }, + }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (d) => (stdout += d)); + child.stderr.on('data', (d) => (stderr += d)); + return new Promise((resolve) => + child.on('close', (status) => + resolve({ status, stdout, stderr, home, bin: join(home, '.local', 'bin', 'repot') }), + ), + ); +} + +test('installs the latest release into ~/.local/bin and says how to add it to PATH', async () => { + const r = await install(); + assert.equal(r.status, 0, r.stderr); + assert.match(r.stdout, new RegExp(`Downloading repot 9\\.9\\.9 for ${TARGET}`)); + assert.equal(execFileSync(r.bin, { encoding: 'utf8' }), 'repot 9.9.9\n'); + assert.match(r.stdout, /Installed repot 9\.9\.9 to .*\/\.local\/bin\/repot/); + assert.match(r.stdout, /is not on your PATH[\s\S]*>> ~\/\.zshrc/); + assert.ok(!existsSync(join(r.home, '.local', 'bin', 'LICENSE')), 'only repot is copied'); +}); + +test('REPOT_VERSION and REPOT_INSTALL_DIR choose the release and the folder', async () => { + const dir = join(mkdtempSync(join(work, 'dir-')), 'bin'); + const r = await install({ + REPOT_VERSION: 'v1.0.0', + REPOT_INSTALL_DIR: dir, + PATH: `${dir}:${process.env.PATH}`, + }); + assert.equal(r.status, 0, r.stderr); + assert.equal(execFileSync(join(dir, 'repot'), { encoding: 'utf8' }), 'repot 1.0.0\n'); + assert.doesNotMatch(r.stdout, /not on your PATH/); +}); + +test('a checksum mismatch installs nothing', async () => { + const r = await install({ REPOT_VERSION: '6.6.6' }); + assert.equal(r.status, 1); + assert.match(r.stderr, /checksum mismatch .*nothing was installed/); + assert.ok(!existsSync(r.bin)); +}); + +test('a missing release fails with the URL it tried', async () => { + const r = await install({ REPOT_VERSION: '0.0.1' }); + assert.equal(r.status, 1); + assert.match( + r.stderr, + new RegExp( + `could not download .*/download/v0\\.0\\.1/repot-cli-0\\.0\\.1-${TARGET}\\.tar\\.gz`, + ), + ); + assert.ok(!existsSync(r.bin)); +}); From 54125b11b2c3528fa1320ad9558aace9cc95a524 Mon Sep 17 00:00:00 2001 From: junjuny0227 Date: Fri, 6 Nov 2026 11:37:17 +0900 Subject: [PATCH 2/4] feat(site): install the CLI on Linux with the install script --- apps/site/src/__tests__/app.test.tsx | 13 ++++++++----- apps/site/src/content.ts | 13 +++++++------ apps/site/src/locales/en.json | 3 ++- apps/site/src/locales/ja.json | 3 ++- apps/site/src/locales/ko.json | 3 ++- apps/site/src/locales/zh-Hans.json | 3 ++- apps/site/src/sections/Install.tsx | 5 +++++ 7 files changed, 28 insertions(+), 15 deletions(-) diff --git a/apps/site/src/__tests__/app.test.tsx b/apps/site/src/__tests__/app.test.tsx index 276075d..8d147c7 100644 --- a/apps/site/src/__tests__/app.test.tsx +++ b/apps/site/src/__tests__/app.test.tsx @@ -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 = 'curl -fsSL https://junjuny0227.github.io/Repot/install.sh | sh'; 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('brew install junjuny0227/tap/repot'); + expect(install).toContain( + 'href="https://github.com/junjuny0227/Repot/blob/main/apps/site/public/install.sh"', + ); }); test('every command on the page is copyable', () => { diff --git a/apps/site/src/content.ts b/apps/site/src/content.ts index 358abd2..42a4e0e 100644 --- a/apps/site/src/content.ts +++ b/apps/site/src/content.ts @@ -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 = { 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', }; @@ -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: { @@ -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: { diff --git a/apps/site/src/locales/en.json b/apps/site/src/locales/en.json index 58e68e3..86e6c7c 100644 --- a/apps/site/src/locales/en.json +++ b/apps/site/src/locales/en.json @@ -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.", diff --git a/apps/site/src/locales/ja.json b/apps/site/src/locales/ja.json index 7d236bf..24ff79d 100644 --- a/apps/site/src/locales/ja.json +++ b/apps/site/src/locales/ja.json @@ -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 ライセンスで公開しています。", diff --git a/apps/site/src/locales/ko.json b/apps/site/src/locales/ko.json index 22573e0..1e04978 100644 --- a/apps/site/src/locales/ko.json +++ b/apps/site/src/locales/ko.json @@ -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 라이선스로 배포됩니다.", diff --git a/apps/site/src/locales/zh-Hans.json b/apps/site/src/locales/zh-Hans.json index 361be47..9f07e94 100644 --- a/apps/site/src/locales/zh-Hans.json +++ b/apps/site/src/locales/zh-Hans.json @@ -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 许可证发布。", diff --git a/apps/site/src/sections/Install.tsx b/apps/site/src/sections/Install.tsx index 9237e8f..e2ede74 100644 --- a/apps/site/src/sections/Install.tsx +++ b/apps/site/src/sections/Install.tsx @@ -65,6 +65,11 @@ export function Install({ platform }: { platform: Platform }) {

{C.install.cliGuide}

+ {os === 'linux' && ( +

+ {C.install.installScript} +

+ )}
From 8dc2ec61c476469b14523c594be30c5f3025ddbe Mon Sep 17 00:00:00 2001 From: junjuny0227 Date: Fri, 6 Nov 2026 11:37:17 +0900 Subject: [PATCH 3/4] docs: describe the CLI install script --- README.ja.md | 7 +++++++ README.ko.md | 7 +++++++ README.md | 8 ++++++++ README.zh-Hans.md | 7 +++++++ crates/cli/README.ja.md | 1 + crates/cli/README.ko.md | 1 + crates/cli/README.md | 1 + crates/cli/README.zh-Hans.md | 1 + docs/plans/2026-11-03-06-site.md | 2 +- docs/specs/2026-11-02-repot-mvp-design.md | 1 + 10 files changed, 35 insertions(+), 1 deletion(-) diff --git a/README.ja.md b/README.ja.md index 5785178..df2378e 100644 --- a/README.ja.md +++ b/README.ja.md @@ -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-<バージョン>-.tar.gz`(macOS、Linux)または `.zip`(Windows)を diff --git a/README.ko.md b/README.ko.md index d22f8ea..ad3b49e 100644 --- a/README.ko.md +++ b/README.ko.md @@ -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-<버전>-.tar.gz`(macOS, Linux)나 `.zip`(Windows)을 받아 `.sha256` diff --git a/README.md b/README.md index 29cf5bc..7ef9943 100644 --- a/README.md +++ b/README.md @@ -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--.tar.gz` (macOS, Linux) or `.zip` (Windows) from diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 4cf708d..201ba0b 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -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-<版本>-.tar.gz`(macOS、Linux)或 `.zip`(Windows),用 `.sha256` diff --git a/crates/cli/README.ja.md b/crates/cli/README.ja.md index aa404eb..67d559d 100644 --- a/crates/cli/README.ja.md +++ b/crates/cli/README.ja.md @@ -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-<バージョン>-.tar.gz` / `.zip` をダウンロードし、`.sha256` ファイルで確認してから `repot` を `PATH` に置きます | | Rust がある場合 | `cargo install --git https://github.com/junjuny0227/Repot repot-cli` | diff --git a/crates/cli/README.ko.md b/crates/cli/README.ko.md index e43f141..5f0cafc 100644 --- a/crates/cli/README.ko.md +++ b/crates/cli/README.ko.md @@ -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-<버전>-.tar.gz` / `.zip`을 받아 `.sha256` 파일로 확인한 뒤 `repot`을 `PATH`에 둡니다 | | Rust가 있으면 | `cargo install --git https://github.com/junjuny0227/Repot repot-cli` | diff --git a/crates/cli/README.md b/crates/cli/README.md index 8a1517a..65e8552 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -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--.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` | diff --git a/crates/cli/README.zh-Hans.md b/crates/cli/README.zh-Hans.md index 6ef0899..21c6873 100644 --- a/crates/cli/README.zh-Hans.md +++ b/crates/cli/README.zh-Hans.md @@ -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` | | 任意系统(手动) | 从 [Releases](https://github.com/junjuny0227/Repot/releases/latest) 下载 `repot-cli-<版本>-.tar.gz` / `.zip`,用 `.sha256` 文件校验后把 `repot` 放到 `PATH` 中 | | 已安装 Rust | `cargo install --git https://github.com/junjuny0227/Repot repot-cli` | diff --git a/docs/plans/2026-11-03-06-site.md b/docs/plans/2026-11-03-06-site.md index d4818cd..df2a4fe 100644 --- a/docs/plans/2026-11-03-06-site.md +++ b/docs/plans/2026-11-03-06-site.md @@ -169,7 +169,7 @@ git commit -m "feat(site): build download links from the app version" 순서와 내용(문구는 README 기준): 1. **헤더**: 앱 아이콘 + "Repot", 섹션 링크(How it works, Principles, Tools, Install), GitHub. 반투명 툴바(`backdrop-filter`), `prefers-reduced-transparency`면 불투명. 모바일은 섹션 링크를 숨기고 GitHub만. -2. **히어로**: 아이콘, `h1` "Repot", 한 줄 소개("Move your dev environment to a new machine — explained, selective, and safe."), 판별된 OS의 기본 다운로드 버튼(mac은 Apple silicon·Intel 두 버튼), 그 아래 판별된 OS의 CLI 설치 명령(macOS는 Homebrew, Windows는 Scoop, Linux는 Homebrew를 쓰는 사람이 적어 릴리스 압축 파일을 `~/.local/bin`에 푸는 `curl … | tar` 명령. 설치 섹션의 Linux 탭에는 Homebrew 명령도 함께 둔다. OS를 모르면 생략), "Other downloads" 링크(#install), "v0.1.0 · Unsigned beta" 표시와 첫 실행 안내 링크, 검토 화면 스크린샷(``로 라이트·다크, `srcset` 1100w·2200w, `width`/`height` 지정). +2. **히어로**: 아이콘, `h1` "Repot", 한 줄 소개("Move your dev environment to a new machine — explained, selective, and safe."), 판별된 OS의 기본 다운로드 버튼(mac은 Apple silicon·Intel 두 버튼), 그 아래 판별된 OS의 CLI 설치 명령(macOS는 Homebrew, Windows는 Scoop, Linux는 Homebrew를 쓰는 사람이 적어 설치 스크립트 `curl -fsSL https://junjuny0227.github.io/Repot/install.sh | sh`(스펙 참조). 설치 섹션의 Linux 탭에는 Homebrew 명령도 함께 둔다. OS를 모르면 생략), "Other downloads" 링크(#install), "v0.1.0 · Unsigned beta" 표시와 첫 실행 안내 링크, 검토 화면 스크린샷(``로 라이트·다크, `srcset` 1100w·2200w, `width`/`height` 지정). 3. **How it works**: Scan → Select → Export 세 단계(번호 목록 `ol`), 옆에 내보내기 화면 스크린샷. 4. **Principles**: Never changes your machine, Secrets masked, No network, Review before you run. "No network"에는 복원 스크립트가 각 도구로 패키지를 내려받을 수 있다는 README 문장을 함께 쓴다. 비밀 감지가 패턴 기반이라는 한계도 쓴다. 5. **Supported tools**: README의 여섯 카테고리를 System Settings식 그룹 목록으로(색 사각형 glyph + 카테고리 + 도구 이름 텍스트). 서드파티 로고 없음. 아래에 "Package-manager entries can only move to the same OS…" 주석. diff --git a/docs/specs/2026-11-02-repot-mvp-design.md b/docs/specs/2026-11-02-repot-mvp-design.md index 7ca39bc..37873c0 100644 --- a/docs/specs/2026-11-02-repot-mvp-design.md +++ b/docs/specs/2026-11-02-repot-mvp-design.md @@ -505,6 +505,7 @@ repot-export-2026-11-02/ - `tauri-action`으로 macOS(`.dmg`, Apple silicon·Intel), Windows(`.msi`/설치 파일), Linux(`.AppImage`, `.deb`)를 빌드해 GitHub Releases에 올린다. - CLI는 같은 릴리스에 `repot-cli-<버전>-.tar.gz`(macOS arm64·x64, Linux x64)와 `.zip`(Windows x64)으로 올린다. 자체 Homebrew tap(`brew install junjuny0227/tap/repot`)과 Scoop bucket(`junjuny0227/scoop-bucket`)은 릴리스를 공개하면 워크플로가 갱신한다. `cargo install --git`도 안내한다. crates.io 게시는 하지 않는다. (계획 15) +- Homebrew가 없는 Linux·macOS 사용자를 위해 설치 스크립트(`apps/site/public/install.sh`, 사이트가 `https://junjuny0227.github.io/Repot/install.sh`로 그대로 배포)를 둔다. `curl -fsSL …/install.sh | sh`는 이 기기의 OS·CPU에 맞는 릴리스 압축 파일을 받아 `.sha256`으로 확인하고 `repot`만 `~/.local/bin`(`REPOT_INSTALL_DIR`)에 복사한다. 버전은 `/releases/latest` 리디렉션으로 정하고(`REPOT_VERSION`으로 고정 가능), sudo·셸 설정 수정은 하지 않으며 폴더가 PATH에 없으면 추가 방법만 알려 준다. 사이트는 Linux에 이 명령을, macOS에 Homebrew를 보여 준다. CI `install-script` 작업이 로컬 가짜 릴리스(오프라인)와 실제 최신 릴리스(Ubuntu·macOS·Debian·Fedora·Arch)로 확인한다. - winget(`microsoft/winget-pkgs`)에는 앱(`junjuny0227.Repot`, `.msi`)과 CLI(`junjuny0227.RepotCLI`, `.zip` 안의 `repot.exe`, 명령 `repot`)를 따로 올린다. 첫 버전(0.9.0)은 손으로 제출했고, 다음 버전부터는 릴리스를 공개하면 `winget.yml`이 사용자의 `winget-pkgs` 포크에서 PR을 연다(`WINGET_TOKEN`, classic PAT `public_repo`·`workflow`). Microsoft의 자동 검사와 검토를 거친 뒤에야 `winget install`에 나타나므로, README·사이트 안내는 첫 버전이 받아들여진 뒤에 더한다. MSI 제작자 이름은 `bundle.publisher`(`junjuny0227`)로 정한다. - 코드 서명이 없으므로 README에 첫 실행 방법(macOS "확인 없이 열기", Windows "추가 정보 → 실행")을 안내한다. - 업데이트 자산은 별도의 updater 키로 서명한다. 공개 키만 앱에 포함하고 비밀 키/암호는 GitHub secrets로 전달한다. `bundle.createUpdaterArtifacts`와 tauri-action `uploadUpdaterJson`을 켠다. Windows 설치 모드는 passive다. From ae150fa63128e727545c955185605299b6043d77 Mon Sep 17 00:00:00 2001 From: junjuny0227 Date: Fri, 6 Nov 2026 11:52:46 +0900 Subject: [PATCH 4/4] fix(cli): spell out the checksum check for shellcheck --- apps/site/public/install.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/site/public/install.sh b/apps/site/public/install.sh index 7234f8e..4e35d6b 100644 --- a/apps/site/public/install.sh +++ b/apps/site/public/install.sh @@ -40,8 +40,9 @@ main() { expected=$(cut -d ' ' -f 1 <"$tmp/$name.tar.gz.sha256") actual=$(sha256 "$tmp/$name.tar.gz") - [ -n "$expected" ] && [ "$expected" = "$actual" ] || + 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"