Skip to content

CLI/플러그인 릴리스 감지 및 자동 업데이트 추가 - #4

Merged
4mGLn merged 2 commits into
mainfrom
feature/self-update
Sep 2, 2026
Merged

4mGLn merged 2 commits into
mainfrom
feature/self-update

Conversation

@4mGLn

@4mGLn 4mGLn commented Sep 2, 2026

Copy link
Copy Markdown
Owner

목표

/goal implement new release detection and auto update (additionally manual update with cli also) cli+plugin both

변경

  • CLI: sessionforge check-update / sessionforge update 추가. 감지는 자동(24시간 캐시, 실패해도
    절대 안 던짐, 다른 명령 끝에 한 줄 알림), 실제 교체는 항상 명시적 update 명령으로만 — 몰래 바이너리를
    바꾸지 않음. Windows는 실행 중인 exe를 바로 덮어쓸 수 없어 .old.exe로 rename 후 교체하는 표준 패턴 사용.
  • 플러그인: scripts/package-plugin.mjs가 패키징된 번들에 .sessionforge-version 마커를 기록,
    paseo-status가 설치된 플러그인 버전과 실행 중인 CLI 버전의 드리프트를 보여줌.

검증

  • 별도 사본에서 v0.1.0 → 실제 GitHub Release v0.2.0으로 진짜 다운로드+자체 교체 성공, 교체 후 정상 동작 확인
  • 실제 바이너리로 플러그인 버전 드리프트 감지 및 "unknown"(구버전 설치) 케이스 모두 확인
  • 테스트 31개 추가, 전체 121개 통과

- packages/cli/src/core/update.server.ts 신규: GitHub 최신 릴리스 조회,
  버전 비교(단순 문자열 비교가 아닌 숫자 필드별 비교), 24시간 캐시된 백그라운드
  체크(3초 타임아웃, 실패해도 절대 던지지 않음), 플랫폼별 바이너리 자체 교체
  로직(Windows는 실행 중인 exe를 덮어쓰거나 지울 수 없어 .old.exe로 먼저
  rename한 뒤 새 바이너리를 원래 이름으로 rename하는 방식 사용)
- sessionforge check-update / sessionforge update 명령 추가 — 감지는
  자동(다른 모든 명령 끝에 한 줄 알림)이지만 실제 교체는 항상 명시적 명령
  으로만 — 사용자 동의 없이 실행 중인 바이너리를 몰래 바꾸지 않음
- 실제로 검증함: 별도로 복사한 사본에서 v0.1.0 → 진짜 v0.2.0 GitHub Release로
  실제 다운로드 및 자체 교체 성공, 교체된 바이너리 정상 동작 확인
- scripts/package-plugin.mjs가 패키징된 플러그인 번들에 .sessionforge-version
  마커 파일을 기록하도록 함 (release.yml이 SESSIONFORGE_VERSION 전달)
- paseo-wire.server.ts에 getInstalledPluginVersion 추가, paseo-status가
  설치된 플러그인 버전과 실행 중인 CLI 버전의 드리프트를 보여주도록 확장
  (dev-main 빌드에서는 비교 자체가 의미 없으므로 드리프트 안내를 생략함) —
  실제 바이너리로 드리프트 감지와 "unknown"(마커 없는 구버전 설치) 케이스
  모두 실제로 확인함
- 테스트 31개 추가(update.server 19개, paseo-wire 2개), 전체 121개 통과
- README.md, README.ko.md, docs/MANUAL.md(새 "Staying up to date" 섹션),
  docs/REFERENCE.md, packages/cli/README.md 문서 갱신
- downloadCliBinary 테스트: process.platform을 "linux"로 흉내내도 chmodSync의
  실행 비트는 실제 호스트가 진짜 Linux/macOS일 때만 의미가 있음 — Windows
  NTFS에는 그런 개념이 없어 실행 비트 검증만 실제 POSIX일 때로 분리
  (activity.server.test.ts의 실제 /proc 테스트와 동일한 원칙)
- checkForUpdateCached 테스트: process.env.HOME만 오버라이드하고
  USERPROFILE은 빼먹어서, 실제 윈도우에서는 os.homedir()이 진짜 사용자
  홈 디렉터리를 가리켜 캐시 파일이 샌드박스 밖으로 새어나가 테스트 간
  오염이 발생했음 — 이미 이번 세션에서 trash.server.test.ts에 적용했던
  것과 완전히 같은 종류의 버그를 새 테스트 파일에 또 반복한 것이라
  동일하게 HOME과 USERPROFILE을 함께 오버라이드하도록 수정
- 로컬에서 typecheck 통과, 테스트 122개 전부 통과 확인

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6cf1961618

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

const isWindows = process.platform === "win32";
const newBinaryPath = join(tmpdir(), `sessionforge-update-${latest}${isWindows ? ".exe" : ""}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Create update files without following temp symlinks

On a shared temporary directory, another local user can pre-create the predictable sessionforge-update-<latest> pathname as a symlink to a file they control before the victim runs update. downloadCliBinary follows that symlink when writing, and selfReplaceBinary then renames the symlink into the victim's executable path; the attacker can subsequently replace its target so the victim executes attacker-controlled code. Use a private, uniquely created staging file (or an exclusive file in the executable's directory) rather than this predictable tmpdir() name.

Useful? React with 👍 / 👎.

}

const isWindows = process.platform === "win32";
const newBinaryPath = join(tmpdir(), `sessionforge-update-${latest}${isWindows ? ".exe" : ""}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stage the replacement on the binary's filesystem

The updater downloads into tmpdir(), but selfReplaceBinary uses rename to install it. When /tmp is a separate filesystem from the installed binary (for example, the documented ~/.local/bin/sessionforge on Linux systems with a tmpfs /tmp), that rename fails with EXDEV, so every update leaves the old binary installed. Stage the download in the executable's directory, or copy it there before the final same-filesystem rename.

Useful? React with 👍 / 👎.

if (!SKIP_UPDATE_NOTICE.has(command)) {
const check = await checkForUpdateCached(getVersion());
if (check?.updateAvailable) {
console.log(`\n(sessionforge v${check.latestVersion} is available — run \`sessionforge update\` to install it.)`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep update notices out of JSON stdout

When a released CLI is behind and a user invokes any JSON-producing command such as list --json, show --json, search --json, cleanup --json, or audit --json, this appends a human-readable update notice to stdout after the JSON document. That makes the documented structured output unparsable for scripts precisely when an update is available; suppress the notice for --json or send it to stderr.

Useful? React with 👍 / 👎.

@4mGLn
4mGLn merged commit ecc30ff into main Sep 2, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant