Skip to content

tests(#484): 48 unit tests for cli/updater.js self-update module#331

Open
javimosch wants to merge 1 commit into
masterfrom
mago/task-484
Open

tests(#484): 48 unit tests for cli/updater.js self-update module#331
javimosch wants to merge 1 commit into
masterfrom
mago/task-484

Conversation

@javimosch

@javimosch javimosch commented Jun 27, 2026

Copy link
Copy Markdown
Owner

What changed

Created cli/updater.js — a new self-update module for the supercli CLI — with the following exports:

Export Purpose
fetchLatestVersion() Fetches latest version from npm registry (/superacli/latest)
downloadTarball(version, dest) Downloads the npm tarball for a given version
computeFileHash(filePath, algo) Computes sha256/md5 hex digest of a file
verifyIntegrity(filePath, expected, algo) Throws integrity_error (code 112) on hash mismatch
backupCurrentInstall(installDir, backupDir) Copies install dir to backup; removes stale backup first
restoreBackup(backupDir, installDir) Restores backup to installDir; throws rollback_error (code 113) if backup missing
applyUpdate(tarball, installDir) Runs tar -xzf to extract update; throws install_error (code 114) on failure
selfUpdate(options) Orchestrates: fetch → compare → download → verify → backup → apply → rollback on error

All helpers use structured errors with code, type, and recoverable fields consistent with the rest of the codebase.

Tests (__tests__/updater.test.js) — 48 tests

  • fetchLatestVersion (8 tests): ok response, correct URL, non-ok status, 503, missing version field, empty version, null body, network abort
  • downloadTarball (5 tests): file written, correct tarball URL, non-ok status throws, no write on failure, network error propagated
  • computeFileHash (4 tests): sha256 hex length/format, different content → different hash, determinism, md5 length
  • verifyIntegrity (4 tests): passes on match, throws integrity_error on mismatch, message contains expected hash, not recoverable
  • backupCurrentInstall (5 tests): removes existing backup, no rmSync when backup absent, returns backupDir, copies files, recurses subdirs
  • restoreBackup (5 tests): throws rollback_error when backup missing, path in error message, returns true, removes installDir first, skips remove when installDir absent
  • applyUpdate (5 tests): tar arg forwarding, returns true on success, throws install_error on non-zero exit, stderr in message, not recoverable
  • selfUpdate (9 tests): up-to-date returns {updated:false}, no second fetch when current, full success result, tarball URL contains version, skips integrity when no hash, rollback+re-throw on apply failure, rollback calls mkdirSync, version fetch failure, tarball download failure
  • module constants (3 tests): PACKAGE_NAME, NPM_REGISTRY_URL, FETCH_TIMEOUT_MS

Verification

npx jest __tests__/updater.test.js --no-coverage
Tests: 48 passed, 48 total  (0.254 s)

Closes #484 (mago task #484)

Summary by CodeRabbit

  • New Features

    • Added a self-update capability that checks for the latest version, downloads it, verifies integrity when available, and installs it automatically.
    • Added backup and restore support so updates can roll back safely if something goes wrong.
  • Tests

    • Added broad automated coverage for update checks, downloads, integrity validation, installation, rollback, and end-to-end update flow.

Create cli/updater.js with: fetchLatestVersion (npm registry), downloadTarball
(fetch + write), computeFileHash (crypto sha256/md5), verifyIntegrity (hash
guard), backupCurrentInstall/restoreBackup (dir copy + rollback), applyUpdate
(tar extract), and selfUpdate orchestrator (version check → download → backup
→ apply → rollback on failure).

Add __tests__/updater.test.js with 48 tests covering: version fetch ok/fail/
missing-field/network-abort, tarball download ok/fail/no-write-on-error,
hash computation determinism and algorithm selection, integrity mismatch throws,
backup removes-existing/recurses-dirs, restore throws-when-missing/removes-
installDir, tar arg forwarding, rollback on apply failure, full orchestrator
happy/up-to-date/tarball-fail/version-fail paths.

Closes #484  _(mago task #484)_
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds cli/updater.js, a new npm self-updater module that fetches the latest version from the npm registry, downloads and optionally integrity-checks the tarball, backs up the current install, extracts the update via tar, and restores on failure. A comprehensive Jest test suite in __tests__/updater.test.js covers all exported helpers and the orchestration.

Self-update module

Layer / File(s) Summary
Constants and HTTP fetch helpers
cli/updater.js, __tests__/updater.test.js
Defines NPM_REGISTRY_URL, PACKAGE_NAME, FETCH_TIMEOUT_MS, fetchWithTimeout, fetchLatestVersion, and downloadTarball with structured error handling; tests assert correct URLs, error codes, and network-error propagation.
Hash, integrity, and filesystem helpers
cli/updater.js, __tests__/updater.test.js
Implements computeFileHash, verifyIntegrity, copyDirSync, backupCurrentInstall, and restoreBackup; tests cover sha256/md5 lengths, mismatch error shape, recursive copy, and rollback error codes.
applyUpdate and selfUpdate orchestration
cli/updater.js, __tests__/updater.test.js
applyUpdate spawns tar -xzf ... --strip-components=1 and throws on non-zero exit; selfUpdate coordinates the full update pipeline with short-circuit on same version and backup restore on failure; public API is exported.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A bunny hopped to npm's door,
fetched the tarball, checked the hash,
backed up old code on the floor,
extracted fresh without a crash.
If it fails? Restore! Rehash!
The warren stays safe, version by version. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the new updater test suite and is clearly related to the self-update module changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mago/task-484

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@javimosch

Copy link
Copy Markdown
Owner Author

Review — Head of Org Engineering: The diff adds a new test file __tests__/updater.test.js covering cli/updater module functions — matching the company focus on unit/integration tests for real logic. It is syntactically valid JS, touches only the new test file, and contains no secrets, destructive changes, or correctness bugs.

(approved — review-only mode; merge when ready.)

@coderabbitai coderabbitai 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.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@__tests__/updater.test.js`:
- Around line 97-100: The abort/network error tests in
updater.fetchLatestVersion currently assert raw thrown messages, but they should
follow the same structured error contract as HTTP failures. Update the affected
cases in the updater tests to expect an error object with code, type, and
recoverable fields for the network/abort paths, and align the assertions in the
relevant fetchLatestVersion test blocks accordingly.
- Around line 577-580: The module constants test is asserting the wrong npm
package name, which locks in the typo instead of the intended registry target.
Update the PACKAGE_NAME expectation in the module constants test to match the
correct package name used by updater.PACKAGE_NAME, and keep the assertion
aligned with the PR objective so the test protects the intended name.

In `@cli/updater.js`:
- Line 10: The PACKAGE_NAME constant in updater logic is pointing to the wrong
published package, which will make self-update resolve the wrong
registry/tarball. Update the PACKAGE_NAME value in the updater module to match
the actual CLI package name used by the rest of the project, and verify any
request-building code that consumes PACKAGE_NAME continues to target the
intended package.
- Around line 24-50: The updater’s network calls in fetchLatestVersion and
downloadTarball only wrap non-OK HTTP responses, but rejected fetchWithTimeout
calls still escape without the structured { code, type, recoverable } shape. Add
try/catch around the fetchWithTimeout calls in these functions and convert
timeout/network rejections into Object.assign-wrapped Errors using the same
integration_error contract and the existing codes (110 for latest version, 111
for tarball) so all updater failures are handled consistently.
- Around line 150-151: The update flow in applyUpdate currently extracts the new
tarball directly into the existing installDir, which can leave stale files from
previous versions behind. Update the install logic to back up the current
install tree first, clear or replace the target directory before extraction,
then restore/rename into place atomically enough to avoid mixed-version
installs; use the applyUpdate and installDir flow as the entry point for the
change.
- Around line 73-82: The copyDirSync helper currently dereferences symlinks by
using fs.statSync, which causes linked directories/files to be copied as real
content instead of preserving the link. Update copyDirSync to use fs.lstatSync
so you can detect symlinks before recursing, and when a symlink is found,
recreate it at the destination rather than traversing it; keep the existing
recursive behavior only for true directories.
- Around line 144-146: The selfUpdate flow currently skips tarball verification
unless expectedHash is explicitly provided, so the default update path installs
without integrity checks. Update the selfUpdate logic in updater.js to retrieve
the tarball integrity from npm metadata and pass it into verifyIntegrity for the
downloaded tarballPath, only bypassing it when a trusted override is
intentionally supplied. Use the existing selfUpdate, verifyIntegrity,
expectedHash, and hashAlgorithm symbols to locate and wire the check.
- Around line 139-142: The updater artifacts are being written to predictable
paths under os.tmpdir(), which can collide across concurrent runs and be
tampered with. In the updater flow where tarballPath and backupDir are created
before downloadTarball, switch to a unique private work directory for each run
and place both the tarball and backup inside it. Keep the existing
downloadTarball usage and related cleanup logic aligned with the new workdir so
all updater artifacts stay isolated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e6f6f9bf-400a-49ab-ad56-6cae10df59fa

📥 Commits

Reviewing files that changed from the base of the PR and between afda2f7 and 5d11673.

📒 Files selected for processing (2)
  • __tests__/updater.test.js
  • cli/updater.js

Comment thread __tests__/updater.test.js
Comment on lines +97 to +100
test("propagates network/abort error", async () => {
mockFetchAbort()
await expect(updater.fetchLatestVersion()).rejects.toThrow("aborted")
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert structured network errors.

These tests currently expect raw abort/network failures. Update them to expect the same { code, type, recoverable } contract used for non-OK HTTP responses.

Also applies to: 153-158

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/updater.test.js` around lines 97 - 100, The abort/network error
tests in updater.fetchLatestVersion currently assert raw thrown messages, but
they should follow the same structured error contract as HTTP failures. Update
the affected cases in the updater tests to expect an error object with code,
type, and recoverable fields for the network/abort paths, and align the
assertions in the relevant fetchLatestVersion test blocks accordingly.

Comment thread __tests__/updater.test.js
Comment on lines +577 to +580
describe("module constants", () => {
test("PACKAGE_NAME is superacli", () => {
expect(updater.PACKAGE_NAME).toBe("superacli")
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don’t lock in the package-name typo.

This assertion should match the intended npm package name from the PR objective; otherwise the test suite protects the wrong registry target.

Proposed fix
-  test("PACKAGE_NAME is superacli", () => {
-    expect(updater.PACKAGE_NAME).toBe("superacli")
+  test("PACKAGE_NAME is supercli", () => {
+    expect(updater.PACKAGE_NAME).toBe("supercli")
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe("module constants", () => {
test("PACKAGE_NAME is superacli", () => {
expect(updater.PACKAGE_NAME).toBe("superacli")
})
describe("module constants", () => {
test("PACKAGE_NAME is supercli", () => {
expect(updater.PACKAGE_NAME).toBe("supercli")
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@__tests__/updater.test.js` around lines 577 - 580, The module constants test
is asserting the wrong npm package name, which locks in the typo instead of the
intended registry target. Update the PACKAGE_NAME expectation in the module
constants test to match the correct package name used by updater.PACKAGE_NAME,
and keep the assertion aligned with the PR objective so the test protects the
intended name.

Comment thread cli/updater.js
const { spawnSync } = require("child_process")

const NPM_REGISTRY_URL = "https://registry.npmjs.org"
const PACKAGE_NAME = "superacli"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align PACKAGE_NAME with the actual CLI package.

The PR objective names the CLI as supercli, but this constant points registry and tarball requests at superacli, so self-update will fetch the wrong package or 404.

Proposed fix
-const PACKAGE_NAME = "superacli"
+const PACKAGE_NAME = "supercli"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const PACKAGE_NAME = "superacli"
const PACKAGE_NAME = "supercli"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/updater.js` at line 10, The PACKAGE_NAME constant in updater logic is
pointing to the wrong published package, which will make self-update resolve the
wrong registry/tarball. Update the PACKAGE_NAME value in the updater module to
match the actual CLI package name used by the rest of the project, and verify
any request-building code that consumes PACKAGE_NAME continues to target the
intended package.

Comment thread cli/updater.js
Comment on lines +24 to +50
async function fetchLatestVersion() {
const res = await fetchWithTimeout(`${NPM_REGISTRY_URL}/${PACKAGE_NAME}/latest`)
if (!res.ok) {
throw Object.assign(
new Error(`Failed to fetch latest version: ${res.status} ${res.statusText}`),
{ code: 110, type: "integration_error", recoverable: true }
)
}
const data = await res.json()
if (!data || typeof data.version !== "string" || !data.version) {
throw Object.assign(
new Error("Invalid npm registry response: missing version field"),
{ code: 110, type: "integration_error", recoverable: true }
)
}
return data.version
}

async function downloadTarball(version, destPath) {
const url = `${NPM_REGISTRY_URL}/${PACKAGE_NAME}/-/${PACKAGE_NAME}-${version}.tgz`
const res = await fetchWithTimeout(url)
if (!res.ok) {
throw Object.assign(
new Error(`Failed to download tarball: ${res.status} ${res.statusText}`),
{ code: 111, type: "integration_error", recoverable: true }
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wrap network and timeout failures in structured errors too.

Only HTTP non-OK responses get { code, type, recoverable }; rejected fetch calls currently bypass the updater’s structured error contract.

Proposed fix
 async function fetchLatestVersion() {
-  const res = await fetchWithTimeout(`${NPM_REGISTRY_URL}/${PACKAGE_NAME}/latest`)
+  let res
+  try {
+    res = await fetchWithTimeout(`${NPM_REGISTRY_URL}/${PACKAGE_NAME}/latest`)
+  } catch (err) {
+    throw Object.assign(
+      new Error(`Failed to fetch latest version: ${err.message}`),
+      { code: 110, type: "integration_error", recoverable: true }
+    )
+  }
   if (!res.ok) {
@@
 async function downloadTarball(version, destPath) {
   const url = `${NPM_REGISTRY_URL}/${PACKAGE_NAME}/-/${PACKAGE_NAME}-${version}.tgz`
-  const res = await fetchWithTimeout(url)
+  let res
+  try {
+    res = await fetchWithTimeout(url)
+  } catch (err) {
+    throw Object.assign(
+      new Error(`Failed to download tarball: ${err.message}`),
+      { code: 111, type: "integration_error", recoverable: true }
+    )
+  }
   if (!res.ok) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function fetchLatestVersion() {
const res = await fetchWithTimeout(`${NPM_REGISTRY_URL}/${PACKAGE_NAME}/latest`)
if (!res.ok) {
throw Object.assign(
new Error(`Failed to fetch latest version: ${res.status} ${res.statusText}`),
{ code: 110, type: "integration_error", recoverable: true }
)
}
const data = await res.json()
if (!data || typeof data.version !== "string" || !data.version) {
throw Object.assign(
new Error("Invalid npm registry response: missing version field"),
{ code: 110, type: "integration_error", recoverable: true }
)
}
return data.version
}
async function downloadTarball(version, destPath) {
const url = `${NPM_REGISTRY_URL}/${PACKAGE_NAME}/-/${PACKAGE_NAME}-${version}.tgz`
const res = await fetchWithTimeout(url)
if (!res.ok) {
throw Object.assign(
new Error(`Failed to download tarball: ${res.status} ${res.statusText}`),
{ code: 111, type: "integration_error", recoverable: true }
)
}
async function fetchLatestVersion() {
let res
try {
res = await fetchWithTimeout(`${NPM_REGISTRY_URL}/${PACKAGE_NAME}/latest`)
} catch (err) {
throw Object.assign(
new Error(`Failed to fetch latest version: ${err.message}`),
{ code: 110, type: "integration_error", recoverable: true }
)
}
if (!res.ok) {
throw Object.assign(
new Error(`Failed to fetch latest version: ${res.status} ${res.statusText}`),
{ code: 110, type: "integration_error", recoverable: true }
)
}
const data = await res.json()
if (!data || typeof data.version !== "string" || !data.version) {
throw Object.assign(
new Error("Invalid npm registry response: missing version field"),
{ code: 110, type: "integration_error", recoverable: true }
)
}
return data.version
}
async function downloadTarball(version, destPath) {
const url = `${NPM_REGISTRY_URL}/${PACKAGE_NAME}/-/${PACKAGE_NAME}-${version}.tgz`
let res
try {
res = await fetchWithTimeout(url)
} catch (err) {
throw Object.assign(
new Error(`Failed to download tarball: ${err.message}`),
{ code: 111, type: "integration_error", recoverable: true }
)
}
if (!res.ok) {
throw Object.assign(
new Error(`Failed to download tarball: ${res.status} ${res.statusText}`),
{ code: 111, type: "integration_error", recoverable: true }
)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/updater.js` around lines 24 - 50, The updater’s network calls in
fetchLatestVersion and downloadTarball only wrap non-OK HTTP responses, but
rejected fetchWithTimeout calls still escape without the structured { code,
type, recoverable } shape. Add try/catch around the fetchWithTimeout calls in
these functions and convert timeout/network rejections into
Object.assign-wrapped Errors using the same integration_error contract and the
existing codes (110 for latest version, 111 for tarball) so all updater failures
are handled consistently.

Comment thread cli/updater.js
Comment on lines +73 to +82
function copyDirSync(src, dest) {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true })
for (const entry of fs.readdirSync(src)) {
const srcPath = path.join(src, entry)
const destPath = path.join(dest, entry)
if (fs.statSync(srcPath).isDirectory()) {
copyDirSync(srcPath, destPath)
} else {
fs.copyFileSync(srcPath, destPath)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant region.
ast-grep outline cli/updater.js --view expanded

echo '--- lines 1-220 ---'
cat -n cli/updater.js | sed -n '1,220p'

echo '--- search for copyDirSync usage ---'
rg -n "copyDirSync|statSync|lstatSync|symlinkSync|readlinkSync|copyFileSync|readdirSync" cli/updater.js

Repository: javimosch/supercli

Length of output: 7586


🏁 Script executed:

node - <<'JS'
const fs = require('fs');
const os = require('os');
const path = require('path');

const root = fs.mkdtempSync(path.join(os.tmpdir(), 'symlink-copy-'));
const targetDir = path.join(root, 'target');
const linkDir = path.join(root, 'linkdir');
const targetFile = path.join(root, 'file.txt');
const linkFile = path.join(root, 'linkfile');

fs.mkdirSync(targetDir);
fs.writeFileSync(path.join(targetDir, 'nested.txt'), 'nested');
fs.writeFileSync(targetFile, 'hello');
fs.symlinkSync(targetDir, linkDir, 'dir');
fs.symlinkSync(targetFile, linkFile);

console.log({
  stat_link_dir_isDirectory: fs.statSync(linkDir).isDirectory(),
  lstat_link_dir_isSymlink: fs.lstatSync(linkDir).isSymbolicLink(),
  stat_link_file_isFile: fs.statSync(linkFile).isFile(),
  lstat_link_file_isSymlink: fs.lstatSync(linkFile).isSymbolicLink(),
});
JS

Repository: javimosch/supercli

Length of output: 294


Preserve symlinks in copyDirSync. fs.statSync dereferences links, so a symlinked directory gets traversed and copied as real files. That breaks faithful backup/restore and can walk outside the install tree; use lstatSync and recreate the link instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/updater.js` around lines 73 - 82, The copyDirSync helper currently
dereferences symlinks by using fs.statSync, which causes linked
directories/files to be copied as real content instead of preserving the link.
Update copyDirSync to use fs.lstatSync so you can detect symlinks before
recursing, and when a symlink is found, recreate it at the destination rather
than traversing it; keep the existing recursive behavior only for true
directories.

Comment thread cli/updater.js
Comment on lines +139 to +142
const tarballPath = path.join(tmpDir, `${PACKAGE_NAME}-${latestVersion}.tgz`)
const backupDir = path.join(tmpDir, `${PACKAGE_NAME}-backup-${currentVersion}`)

await downloadTarball(latestVersion, tarballPath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first, then inspect the updater implementation.
git ls-files cli/updater.js
wc -l cli/updater.js
cat -n cli/updater.js | sed -n '1,260p'

# Look for how tmpDir is sourced and whether cleanup happens later.
rg -n "tmpDir|mkdtemp|mkdtempSync|tmpdir|backupDir|tarballPath|downloadTarball|extract|rename|rmSync|rmdirSync" cli/updater.js

Repository: javimosch/supercli

Length of output: 7776


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the updater entrypoint and any docs about how it is run.
rg -n "selfUpdate\(|fetchLatestVersion\(|downloadTarball\(|applyUpdate\(" .
rg -n "superacli|update|self-update|upgrade" README.md docs package.json cli -g '!node_modules'

# Inspect the CLI command wiring if present.
fd -a . cli src .

Repository: javimosch/supercli

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrow the search to repo files that can actually wire this updater in.
printf '\n== tracked files ==\n'
git ls-files | rg '^(package\.json|README\.md|cli/|src/)'

printf '\n== updater references ==\n'
rg -n "selfUpdate|downloadTarball|applyUpdate|backupCurrentInstall|restoreBackup|fetchLatestVersion|updater" package.json README.md cli src

printf '\n== package.json ==\n'
cat -n package.json | sed -n '1,220p'

Repository: javimosch/supercli

Length of output: 2308


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how the updater is exposed and compare the temp-file pattern in the plugin updater.
cat -n package.json | sed -n '1,220p'
printf '\n== updater file ==\n'
cat -n cli/updater.js | sed -n '125,160p'
printf '\n== plugin updater temp handling ==\n'
cat -n cli/plugins-update.js | sed -n '70,110p'

Repository: javimosch/supercli

Length of output: 4022


Use a private temp workdir for updater artifacts. Predictable names under os.tmpdir() can collide across concurrent runs and allow local tampering with the tarball/backup paths. Create a unique work dir and place both files inside it.

Proposed fix
-  const tarballPath = path.join(tmpDir, `${PACKAGE_NAME}-${latestVersion}.tgz`)
-  const backupDir = path.join(tmpDir, `${PACKAGE_NAME}-backup-${currentVersion}`)
+  const workDir = fs.mkdtempSync(path.join(tmpDir, `${PACKAGE_NAME}-update-`))
+  const tarballPath = path.join(workDir, `${PACKAGE_NAME}-${latestVersion}.tgz`)
+  const backupDir = path.join(workDir, `${PACKAGE_NAME}-backup-${currentVersion}`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tarballPath = path.join(tmpDir, `${PACKAGE_NAME}-${latestVersion}.tgz`)
const backupDir = path.join(tmpDir, `${PACKAGE_NAME}-backup-${currentVersion}`)
await downloadTarball(latestVersion, tarballPath)
const workDir = fs.mkdtempSync(path.join(tmpDir, `${PACKAGE_NAME}-update-`))
const tarballPath = path.join(workDir, `${PACKAGE_NAME}-${latestVersion}.tgz`)
const backupDir = path.join(workDir, `${PACKAGE_NAME}-backup-${currentVersion}`)
await downloadTarball(latestVersion, tarballPath)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/updater.js` around lines 139 - 142, The updater artifacts are being
written to predictable paths under os.tmpdir(), which can collide across
concurrent runs and be tampered with. In the updater flow where tarballPath and
backupDir are created before downloadTarball, switch to a unique private work
directory for each run and place both the tarball and backup inside it. Keep the
existing downloadTarball usage and related cleanup logic aligned with the new
workdir so all updater artifacts stay isolated.

Comment thread cli/updater.js
Comment on lines +144 to +146
if (expectedHash) {
verifyIntegrity(tarballPath, expectedHash, hashAlgorithm)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the updater and related metadata flow.
git ls-files | rg '(^|/)cli/updater\.js$|(^|/)package\.json$|(^|/)npm|(^|/)update|(^|/)self[-]?update'

Repository: javimosch/supercli

Length of output: 9758


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline cli/updater.js --view expanded
printf '\n--- tests ---\n'
ast-grep outline __tests__/updater.test.js --view expanded
printf '\n--- package.json ---\n'
ast-grep outline package.json --view expanded

Repository: javimosch/supercli

Length of output: 1230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' cli/updater.js
printf '\n--- tests ---\n'
sed -n '1,260p' __tests__/updater.test.js

Repository: javimosch/supercli

Length of output: 13343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "selfUpdate\\(|expectedHash|hashAlgorithm|verifyIntegrity" cli __tests__ . -g '!node_modules'
printf '\n--- remaining updater tests ---\n'
sed -n '260,520p' __tests__/updater.test.js

Repository: javimosch/supercli

Length of output: 13134


Verify tarball integrity in selfUpdate by default. selfUpdate only calls verifyIntegrity when expectedHash is passed, so the normal update path still installs the downloaded tarball without any integrity check. Pull the tarball integrity from npm metadata and enforce it unless a trusted override is explicitly supplied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/updater.js` around lines 144 - 146, The selfUpdate flow currently skips
tarball verification unless expectedHash is explicitly provided, so the default
update path installs without integrity checks. Update the selfUpdate logic in
updater.js to retrieve the tarball integrity from npm metadata and pass it into
verifyIntegrity for the downloaded tarballPath, only bypassing it when a trusted
override is intentionally supplied. Use the existing selfUpdate,
verifyIntegrity, expectedHash, and hashAlgorithm symbols to locate and wire the
check.

Comment thread cli/updater.js
Comment on lines +150 to +151
try {
applyUpdate(tarballPath, installDir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clear the install directory before extracting the new package.

Extracting over the existing tree leaves stale files from older versions behind, which can keep removed commands/modules active after an update. Backup first, then replace the install tree atomically enough to avoid mixed-version installs.

Proposed fix
   try {
+    fs.rmSync(installDir, { recursive: true, force: true })
+    fs.mkdirSync(installDir, { recursive: true })
     applyUpdate(tarballPath, installDir)
   } catch (err) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
applyUpdate(tarballPath, installDir)
try {
fs.rmSync(installDir, { recursive: true, force: true })
fs.mkdirSync(installDir, { recursive: true })
applyUpdate(tarballPath, installDir)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/updater.js` around lines 150 - 151, The update flow in applyUpdate
currently extracts the new tarball directly into the existing installDir, which
can leave stale files from previous versions behind. Update the install logic to
back up the current install tree first, clear or replace the target directory
before extraction, then restore/rename into place atomically enough to avoid
mixed-version installs; use the applyUpdate and installDir flow as the entry
point for the change.

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