Skip to content

Install GitHub plugins when Git is unavailable - #127

Open
balki97 wants to merge 1 commit into
got-feedBack:mainfrom
balki97:fix/plugin-install-without-git
Open

Install GitHub plugins when Git is unavailable#127
balki97 wants to merge 1 commit into
got-feedBack:mainfrom
balki97:fix/plugin-install-without-git

Conversation

@balki97

@balki97 balki97 commented Jul 30, 2026

Copy link
Copy Markdown

Summary

  • fall back to GitHub's source archive when the Plugin Manager cannot spawn Git
  • retain the repository URL so archive-installed plugins remain updateable
  • stage archive updates before replacing the installed copy

Verification

  • npm run typecheck
  • node --test tests/plugin-source.test.js
  • downloaded and extracted balki97/feedforge-connect with the same GitHub codeload/tar path on Windows

Full npm test reached 100 passing tests; two unrelated environment checks fail because this shallow inspection clone has no installed Electron binary or JUCE submodule.

Summary by CodeRabbit

  • New Features

    • Plugins can now be installed and updated from public GitHub repositories even when Git is unavailable.
    • The plugin manager recognizes archive-based installations and displays the Update option when supported.
    • GitHub repository URLs are validated before archive installation or updates.
  • Bug Fixes

    • Improved handling of invalid plugin locations and Git execution failures.
    • Archive-based updates safely replace existing plugin installations.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Plugin management now supports GitHub archive installation and updates when Git is unavailable. Stored archive metadata determines update eligibility, and the renderer uses the new canUpdate flag.

Changes

GitHub archive plugin flow

Layer / File(s) Summary
Plugin source contract
src/main/plugin-source.ts, tests/plugin-source.test.js
Adds the archive metadata filename and GitHub repository URL parser, with tests for valid and invalid URLs.
Archive installation and update lifecycle
src/main/plugin-manager.ts
Preserves git error codes, installs GitHub archives after git is unavailable, records archive metadata, and updates non-git plugins through staged directory replacement.
Update control wiring
src/renderer/plugin-manager/screen.js
Shows the Update action according to plugin.canUpdate.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PluginManager
  participant GitHub
  participant PluginDirectory
  PluginManager->>GitHub: Download repository archive
  GitHub-->>PluginManager: Return tarball
  PluginManager->>PluginDirectory: Extract plugin and write source metadata
  PluginManager->>PluginDirectory: Stage archive and swap plugin directories
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: GitHub plugin installation now falls back to archives when git is unavailable.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests

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.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/plugin-manager.ts (1)

255-262: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not report a successful swap as a failed update.

If deletion of backupDir fails after stagedDir was promoted, the outer catch returns failure even though the live plugin was already replaced. Handle backup cleanup separately (log and retry/defer it) so the operation reports its actual outcome.

Proposed fix
-                fs.rmSync(backupDir, { recursive: true, force: true });
+                try {
+                    fs.rmSync(backupDir, { recursive: true, force: true });
+                } catch (cleanupError) {
+                    console.warn(`[plugins] Could not remove backup for "${name}"`, cleanupError);
+                }
🤖 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 `@src/main/plugin-manager.ts` around lines 255 - 262, In the plugin update flow
surrounding the stagedDir promotion and backupDir cleanup, prevent errors
deleting backupDir from reaching the outer catch after the live plugin has
already been replaced. Handle backup cleanup independently by logging the
failure and retrying or deferring removal, while preserving failure reporting
for errors that occur before or during the swap so the returned success status
reflects the actual update outcome.
🤖 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 `@src/main/plugin-manager.ts`:
- Around line 85-87: Update installGitHubArchive() to stream the GitHub response
directly into a temporary file instead of buffering response.arrayBuffer() in
memory. Enforce a configurable maximum byte limit while streaming, and use an
AbortSignal with a timeout to terminate stalled downloads; preserve the existing
HTTP status validation and archive installation flow.

In `@src/main/plugin-source.ts`:
- Around line 3-10: The parseGitHubRepository function must reject GitHub URLs
containing credentials before accepting the repository path. Validate that
url.username and url.password are both empty, return null otherwise, and add a
regression test covering a credential-bearing URL to ensure it is not persisted
or accepted.

---

Outside diff comments:
In `@src/main/plugin-manager.ts`:
- Around line 255-262: In the plugin update flow surrounding the stagedDir
promotion and backupDir cleanup, prevent errors deleting backupDir from reaching
the outer catch after the live plugin has already been replaced. Handle backup
cleanup independently by logging the failure and retrying or deferring removal,
while preserving failure reporting for errors that occur before or during the
swap so the returned success status reflects the actual update outcome.
🪄 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 Plus

Run ID: 1c0a5540-07dd-4fd3-9842-1f80db008b6f

📥 Commits

Reviewing files that changed from the base of the PR and between 00653cf and 38f2a64.

📒 Files selected for processing (4)
  • src/main/plugin-manager.ts
  • src/main/plugin-source.ts
  • src/renderer/plugin-manager/screen.js
  • tests/plugin-source.test.js

Comment on lines +85 to +87
const response = await fetch(`https://codeload.github.com/${repository.owner}/${repository.repo}/tar.gz/HEAD`);
if (!response.ok) throw new Error(`GitHub download returned HTTP ${response.status}`);
fs.writeFileSync(archivePath, Buffer.from(await response.arrayBuffer()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant plugin-manager implementation around the reported lines,
# plus nearby timeout/limit behavior if present.
if [ -f src/main/plugin-manager.ts ]; then
  printf '--- plugin-manager.ts outline ---\n'
  ast-grep outline src/main/plugin-manager.ts || true
  printf '\n--- lines 60-110 ---\n'
  sed -n '60,110p' src/main/plugin-manager.ts | nl -ba -v60
  printf '\n--- all fetch/timeout/limit references ---\n'
  rg -n "fetch\\(|arrayBuffer|timeout|AbortController|writeFile|Response|Readable|createWriteStream|max|size|limit" src/main/plugin-manager.ts || true
else
  printf 'src/main/plugin-manager.ts not found\n'
  git ls-files | rg 'plugin-manager\.ts$' || true
fi

Repository: got-feedBack/feedBack-desktop

Length of output: 258


🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [ -f src/main/plugin-manager.ts ]; then
  echo '--- plugin-manager.ts outline ---'
  ast-grep outline src/main/plugin-manager.ts || true
  echo
  echo '--- lines 60-110 ---'
  sed -n '60,110p' src/main/plugin-manager.ts | nl -ba -v60
  echo
  echo '--- fetch/timeout/limit references ---'
  rg -n "fetch\\(|arrayBuffer|timeout|AbortController|writeFile|Response|Readable|createWriteStream|max|size|limit" src/main/plugin-manager.ts || true
else
  echo 'src/main/plugin-manager.ts not found'
  git ls-files | rg 'plugin-manager\.ts$' || true
fi

Repository: got-feedBack/feedBack-desktop

Length of output: 1262


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- lines 78-95 ---'
sed -n '78,95p' src/main/plugin-manager.ts | awk '{print NR+77": "$0}'

echo
echo '--- fetch/timeout/limit references ---'
rg -n "fetch\\(|arrayBuffer|timeout|AbortController|writeFile|Response|Readable|createWriteStream|max|size|limit|archivePath" src/main/plugin-manager.ts || true

echo
echo '--- package runtime hints ---'
grep -n '"type":' package.json 2>/dev/null || true
sed -n '1,120p' src/main/plugin-manager.ts | awk '{print NR": "$0}'

Repository: got-feedBack/feedBack-desktop

Length of output: 7702


🌐 Web query:

Node.js global fetch default timeout no abort controller arrayBuffer buffers entire response

💡 Result:

In Node.js, the global fetch API—powered by undici—has a default timeout of 300 seconds [1][2][3]. This applies to both headers and the body, and is not a per-request configurable timeout in the traditional sense, but rather a default configuration within the undici dispatcher [1][2]. There is no global setting to change this default; to enforce a shorter timeout, the standard and recommended approach is to use AbortSignal.timeout passed within the fetch options [4][5][3]. Regarding arrayBuffer, calling response.arrayBuffer does indeed buffer the entire response body into memory [6]. The method is designed to read the response stream to completion and resolve with an ArrayBuffer containing the full data [6]. Because this process consumes the entire body into memory, it is not suitable for large payloads where memory efficiency is a concern; for large data, using the response body as a stream is the recommended approach [7][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- updatePlugin installGitHubArchive call sites ---'
sed -n '220,265p' src/main/plugin-manager.ts | awk '{print NR+220": "$0}'

echo
echo '--- installPlugin call site ---'
sed -n '152,200p' src/main/plugin-manager.ts | awk '{print NR+152": "$0}'

Repository: got-feedBack/feedBack-desktop

Length of output: 4792


Bound archive downloads.

installGitHubArchive() uses response.arrayBuffer() without any size limit and does not pass an AbortSignal, so stalled GitHub downloads can hang and large archives can block memory before being written to disk. Stream the archive to a temp file with a configurable byte limit and an abort timeout.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 86-86: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(archivePath, Buffer.from(await response.arrayBuffer()))
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

🤖 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 `@src/main/plugin-manager.ts` around lines 85 - 87, Update
installGitHubArchive() to stream the GitHub response directly into a temporary
file instead of buffering response.arrayBuffer() in memory. Enforce a
configurable maximum byte limit while streaming, and use an AbortSignal with a
timeout to terminate stalled downloads; preserve the existing HTTP status
validation and archive installation flow.

Comment thread src/main/plugin-source.ts
Comment on lines +3 to +10
export function parseGitHubRepository(value: string): { owner: string; repo: string } | null {
try {
const url = new URL(value);
const parts = url.pathname.replace(/^\/+|\/+$/g, '').split('/');
if (url.protocol !== 'https:' || url.hostname.toLowerCase() !== 'github.com' || parts.length !== 2) return null;
const owner = parts[0];
const repo = parts[1].replace(/\.git$/, '');
if (!/^[A-Za-z0-9_.-]+$/.test(owner) || !/^[A-Za-z0-9_.-]+$/.test(repo)) return null;

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

Reject URLs containing credentials.

URL accepts https://token@github.com/owner/repo; the archive flow then writes that raw URL to .feedback-plugin-source.json, persisting a token/password in plaintext. Reject non-empty url.username or url.password, and add a regression test.

Proposed fix
-        if (url.protocol !== 'https:' || url.hostname.toLowerCase() !== 'github.com' || parts.length !== 2) return null;
+        if (url.protocol !== 'https:' || url.hostname.toLowerCase() !== 'github.com' ||
+            url.username || url.password || parts.length !== 2) return null;
📝 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
export function parseGitHubRepository(value: string): { owner: string; repo: string } | null {
try {
const url = new URL(value);
const parts = url.pathname.replace(/^\/+|\/+$/g, '').split('/');
if (url.protocol !== 'https:' || url.hostname.toLowerCase() !== 'github.com' || parts.length !== 2) return null;
const owner = parts[0];
const repo = parts[1].replace(/\.git$/, '');
if (!/^[A-Za-z0-9_.-]+$/.test(owner) || !/^[A-Za-z0-9_.-]+$/.test(repo)) return null;
export function parseGitHubRepository(value: string): { owner: string; repo: string } | null {
try {
const url = new URL(value);
const parts = url.pathname.replace(/^\/+|\/+$/g, '').split('/');
if (url.protocol !== 'https:' || url.hostname.toLowerCase() !== 'github.com' ||
url.username || url.password || parts.length !== 2) return null;
const owner = parts[0];
const repo = parts[1].replace(/\.git$/, '');
if (!/^[A-Za-z0-9_.-]+$/.test(owner) || !/^[A-Za-z0-9_.-]+$/.test(repo)) return null;
🤖 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 `@src/main/plugin-source.ts` around lines 3 - 10, The parseGitHubRepository
function must reject GitHub URLs containing credentials before accepting the
repository path. Validate that url.username and url.password are both empty,
return null otherwise, and add a regression test covering a credential-bearing
URL to ensure it is not persisted or accepted.

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