Skip to content

feat: add kortex-cli extension to embed CLI for workspace management#1213

Open
bmahabirbu wants to merge 1 commit intokortex-hub:mainfrom
bmahabirbu:embed-kortex-cli-extension
Open

feat: add kortex-cli extension to embed CLI for workspace management#1213
bmahabirbu wants to merge 1 commit intokortex-hub:mainfrom
bmahabirbu:embed-kortex-cli-extension

Conversation

@bmahabirbu
Copy link
Copy Markdown
Contributor

Summary

  • Adds a new kortex-cli built-in extension that downloads and registers the kortex CLI binary from kortex-hub/kortex-cli GitHub releases
  • Supports all platforms (macOS, Linux, Windows) and architectures (x64, arm64)
  • Follows the same extension pattern as the Goose CLI integration (downloader + CLI tool registration)

Closes #1127

Test plan

  • pnpm install resolves dependencies for the new extension
  • pnpm run build:extensions:kortex-cli builds successfully
  • pnpm run test:extensions passes
  • Start the app in dev mode and verify "Kortex CLI" appears in CLI Tools with install option
  • Install the CLI through the UI and verify it downloads the correct platform binary

🤖 Generated with Claude Code

@bmahabirbu bmahabirbu requested a review from a team as a code owner April 1, 2026 06:42
@bmahabirbu bmahabirbu requested review from benoitf and fbricon and removed request for a team April 1, 2026 06:42
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

Adds a new kortex-cli extension: package manifest and build tooling, Vite/TypeScript config, an extension entrypoint, a GitHub-release-based downloader, CLI discovery/installer wiring, and a platform helper to resolve binaries.

Changes

Cohort / File(s) Summary
Extension package & build config
extensions/kortex-cli/package.json, extensions/kortex-cli/tsconfig.json, extensions/kortex-cli/vite.config.js
New extension manifest, npm scripts, dependencies/devDependencies; TypeScript config; Vite build and Vitest test setup with externals and test aliases.
Packaging script & root build integration
extensions/kortex-cli/scripts/build.js, package.json
New build script that zips extension into a .cdix and extracts it into builtin/; root package.json updated to run build:extensions:kortex-cli.
Extension entry & wiring
extensions/kortex-cli/src/extension.ts
Adds exported activate/deactivate, creates Octokit client, initializes and registers KortexCliDownloader and KortexCLI.
Downloader implementation
extensions/kortex-cli/src/kortex-cli-downloader.ts
New KortexCliDownloader handling GitHub release listing, asset selection, download, extraction (.zip via unzipper, .tar.gz via tar), install/uninstall, storage path management, and permission handling.
CLI lifecycle manager
extensions/kortex-cli/src/kortex-cli.ts
New KortexCLI (exporting KORTEX_CLI_NAME) that discovers extension-managed or system CLI, creates CliTool, and wires installer (select/install/uninstall) via the downloader.
Platform helper
extensions/kortex-cli/src/utils/system.ts
Adds whereBinary(envAPI, processAPI, executable) to resolve full binary path using which (Unix) or where.exe (Windows) with error handling.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Host as Extension Host
    participant KCLI as KortexCLI
    participant Downloader as KortexCliDownloader
    participant GitHub as GitHub API
    participant FS as File System
    participant Sys as System Shell

    User->>Host: Activate extension
    Host->>KCLI: init()
    KCLI->>KCLI: findKortexVersion()
    alt extension-managed executable exists
        KCLI->>FS: existsSync(installDir)
        FS-->>KCLI: exists
        KCLI->>Sys: exec("--version") on installed binary
        Sys-->>KCLI: version
    else not found / user chooses install
        KCLI->>Downloader: selectVersion()
        Downloader->>GitHub: listReleases()
        GitHub-->>Downloader: releases
        Downloader-->>User: show QuickPick
        User->>Downloader: select release
        Downloader->>GitHub: getReleaseAsset(assetId)
        GitHub-->>Downloader: asset stream
        Downloader->>FS: write asset file
        Downloader->>FS: extract (.zip/.tar.gz)
        FS-->>Downloader: extraction complete
        Downloader->>FS: chmod +x on Unix
        Downloader-->>KCLI: return installed path
        KCLI->>KCLI: update tool metadata (path, version)
    end
    KCLI-->>Host: CLI initialized
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title clearly summarizes the main change: adding a new kortex-cli extension for embedding the CLI in the workspace.
Description check ✅ Passed The description explains the key changes: adding the kortex-cli extension with platform/architecture support, following the Goose CLI pattern, and links to issue #1127.
Linked Issues check ✅ Passed The PR fulfills issue #1127 requirements: embeds Kortex CLI functionality through the new extension, enabling users to work with workspaces immediately.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the kortex-cli extension and integrating it into the build system, with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
extensions/kortex-cli/src/extension.ts (1)

38-40: Inconsistent ordering: subscriptions.push before vs after init().

The downloader is registered in subscriptions after init() completes (lines 35-36), but kortexCLI is registered before init() is called (lines 39-40). This inconsistency could cause issues if kortexCLI.init() throws—the object would remain in subscriptions in a partially initialized state.

Consider making the ordering consistent:

♻️ Suggested fix
   const kortexCLI = new KortexCLI(cli, processAPI, downloader, env);
-  extensionContext.subscriptions.push(kortexCLI);
   await kortexCLI.init();
+  extensionContext.subscriptions.push(kortexCLI);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/extension.ts` around lines 38 - 40, The
subscription registration ordering is inconsistent and can leave a
partially-initialized KortexCLI in subscriptions if init() throws; change the
code so KortexCLI is initialized before being registered: call await
kortexCLI.init() first, then call
extensionContext.subscriptions.push(kortexCLI). Ensure this mirrors the
downloader registration pattern (register downloader in
extensionContext.subscriptions only after its initialization completes) and
update any related cleanup logic that expects the object to be present only
after successful init.
extensions/kortex-cli/scripts/build.js (1)

42-46: Extraction runs asynchronously after script appears to complete.

The mkdirp().then() pattern means the Node process may exit before extraction finishes, potentially leaving an incomplete builtin/ directory. While this matches the pattern in sibling extensions (e.g., extensions/ollama/scripts/build.js), using await would be more robust.

♻️ Proposed fix using async/await
-// create unzipped built-in
-mkdirp(unzippedDirectory).then(() => {
-  const unzip = new AdmZip(destFile);
-  unzip.extractAllTo(unzippedDirectory);
-});
+// create unzipped built-in
+await mkdirp(unzippedDirectory);
+const unzip = new AdmZip(destFile);
+unzip.extractAllTo(unzippedDirectory);

This would require wrapping the script body in an async IIFE or making it a module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/scripts/build.js` around lines 42 - 46, The extraction
currently runs asynchronously because mkdirp(...).then(...) may let the Node
process exit before AdmZip(destFile).extractAllTo finishes; change this to use
async/await so the process waits: wrap the script body in an async IIFE (or make
the file an ES module) and await mkdirp(unzippedDirectory) before creating new
AdmZip(destFile) and await the extraction step (ensure extractAllTo is awaited
or use a synchronous alternative) so that unzippedDirectory is fully written
before the script exits; reference mkdirp, AdmZip, destFile, unzippedDirectory,
and extractAllTo when locating the code to change.
extensions/kortex-cli/src/kortex-cli-downloader.ts (2)

89-94: Fail fast if extraction didn’t produce the CLI binary.

install() currently returns the expected path even when that file was not extracted. Throwing here keeps the failure in the install flow instead of surfacing later when the tool is launched.

Suggested change
    const executablePath = this.getKortexExecutablePath();
-   if (!this.envAPI.isWindows && existsSync(executablePath)) {
-     await chmod(executablePath, 0o755);
+   if (!existsSync(executablePath)) {
+     throw new Error('Kortex CLI executable was not found after extraction');
+   }
+   if (!this.envAPI.isWindows) {
+     await chmod(executablePath, 0o755);
    }
 
    return executablePath;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts` around lines 89 - 94, The
install() flow currently returns getKortexExecutablePath() even if extraction
failed; update the end of install() to verify the binary exists and is
executable: after calling getKortexExecutablePath() and performing the chmod for
non-Windows (using envAPI.isWindows and existsSync), if the file does not exist
(existsSync returns false) throw a descriptive Error (e.g. "Kortex CLI binary
not found after extraction: <path>") so the install step fails fast rather than
returning a non-existent path.

19-19: Prefer execFile() over shell interpolation for tar.

filePath is derived from release metadata, so passing it through exec("tar ...") makes extraction sensitive to shell metacharacters and quoting edge cases. execFile() keeps the arguments opaque.

Suggested change
-import { exec } from 'node:child_process';
+import { execFile } from 'node:child_process';
@@
  extractTarGz(filePath: string, outDir: string): Promise<void> {
    return new Promise((resolve, reject) => {
-     // eslint-disable-next-line sonarjs/os-command
-     exec(`tar -xzf "${filePath}" -C "${outDir}"`, err => {
+     execFile('tar', ['-xzf', filePath, '-C', outDir], err => {
        if (err) reject(err);
        else resolve();
      });
    });
  }

Also applies to: 65-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts` at line 19, The code uses
exec(...) to run tar with a shell-interpolated filePath (see the import exec
from 'node:child_process' and the locations where filePath is passed into a tar
command around lines 19 and 65-69), which is unsafe; replace those exec calls
with execFile (or spawn) so you pass tar and its arguments as an array (e.g.,
program 'tar' and args like '-xzf', filePath, '-C', dest) to keep filePath
opaque to the shell, and update the callback/error handling to match execFile's
signature; ensure you remove any shell interpolation and still handle errors and
stdout/stderr the same way.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@extensions/kortex-cli/src/kortex-cli-downloader.ts`:
- Around line 57-60: The install path fails if `#installDirectory` was removed by
uninstall() because init() is only called once; update the install() method in
the KortexCliDownloader (and similarly ensure any other extraction callers) to
recreate the target directory before running tar -C by calling
mkdir(this.#installDirectory, { recursive: true }) or equivalent; ensure
install() checks/creates this.#installDirectory immediately before extraction so
repeated uninstall/install in the same session succeeds.
- Around line 108-123: selectVersion may filter out all releases
(releasesMetadata) and still call showQuickPick, and getLatestVersionAsset can
return undefined despite its non-optional signature; after you filter
releasesMetadata (in selectVersion) check for releasesMetadata.length === 0 and
throw a clear Error like "no other Kortex CLI versions available" before calling
windowAPI.showQuickPick, and similarly update getLatestVersionAsset to validate
its candidate list (or the asset result) and throw a descriptive Error instead
of returning undefined so both methods always either return the expected
non-optional type or raise a clear exception (refer to selectVersion,
releasesMetadata, windowAPI.showQuickPick and getLatestVersionAsset to locate
the code to change).

In `@extensions/kortex-cli/src/utils/system.ts`:
- Around line 26-32: The code in the envAPI.isLinux/isMac branch returns the raw
stdout from processAPI.exec('which', [executable]) (captured as fullPath) which
includes a trailing newline; fix by trimming whitespace (e.g., call .trim() on
the stdout before returning) so callers get a clean path string from
processAPI.exec / fullPath; ensure you still handle the catch path unchanged.
- Around line 34-39: The code currently strips all newlines from the output of
processAPI.exec('where.exe', [executable]) resulting in concatenated paths;
change the post-processing of the returned fullPath to split on newline (e.g.,
/\r?\n/) and return only the first non-empty trimmed line (use the result of
processAPI.exec and the fullPath variable), leaving the existing catch behavior
intact. This ensures only the first path from where.exe is returned.

---

Nitpick comments:
In `@extensions/kortex-cli/scripts/build.js`:
- Around line 42-46: The extraction currently runs asynchronously because
mkdirp(...).then(...) may let the Node process exit before
AdmZip(destFile).extractAllTo finishes; change this to use async/await so the
process waits: wrap the script body in an async IIFE (or make the file an ES
module) and await mkdirp(unzippedDirectory) before creating new AdmZip(destFile)
and await the extraction step (ensure extractAllTo is awaited or use a
synchronous alternative) so that unzippedDirectory is fully written before the
script exits; reference mkdirp, AdmZip, destFile, unzippedDirectory, and
extractAllTo when locating the code to change.

In `@extensions/kortex-cli/src/extension.ts`:
- Around line 38-40: The subscription registration ordering is inconsistent and
can leave a partially-initialized KortexCLI in subscriptions if init() throws;
change the code so KortexCLI is initialized before being registered: call await
kortexCLI.init() first, then call
extensionContext.subscriptions.push(kortexCLI). Ensure this mirrors the
downloader registration pattern (register downloader in
extensionContext.subscriptions only after its initialization completes) and
update any related cleanup logic that expects the object to be present only
after successful init.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts`:
- Around line 89-94: The install() flow currently returns
getKortexExecutablePath() even if extraction failed; update the end of install()
to verify the binary exists and is executable: after calling
getKortexExecutablePath() and performing the chmod for non-Windows (using
envAPI.isWindows and existsSync), if the file does not exist (existsSync returns
false) throw a descriptive Error (e.g. "Kortex CLI binary not found after
extraction: <path>") so the install step fails fast rather than returning a
non-existent path.
- Line 19: The code uses exec(...) to run tar with a shell-interpolated filePath
(see the import exec from 'node:child_process' and the locations where filePath
is passed into a tar command around lines 19 and 65-69), which is unsafe;
replace those exec calls with execFile (or spawn) so you pass tar and its
arguments as an array (e.g., program 'tar' and args like '-xzf', filePath, '-C',
dest) to keep filePath opaque to the shell, and update the callback/error
handling to match execFile's signature; ensure you remove any shell
interpolation and still handle errors and stdout/stderr the same way.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9679b3cb-01aa-47fb-8495-f987ee8f822f

📥 Commits

Reviewing files that changed from the base of the PR and between 409886b and 7391cfc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • extensions/kortex-cli/package.json
  • extensions/kortex-cli/scripts/build.js
  • extensions/kortex-cli/src/extension.ts
  • extensions/kortex-cli/src/kortex-cli-downloader.ts
  • extensions/kortex-cli/src/kortex-cli.ts
  • extensions/kortex-cli/src/utils/system.ts
  • extensions/kortex-cli/tsconfig.json
  • extensions/kortex-cli/vite.config.js
  • package.json

Comment on lines +57 to +60
async init(): Promise<void> {
if (!existsSync(this.#installDirectory)) {
await mkdir(this.#installDirectory, { recursive: 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.

⚠️ Potential issue | 🔴 Critical

Recreate #installDirectory inside install().

init() is the only place that creates this directory, but uninstall() removes it. A reinstall in the same session will fail on macOS/Linux because tar -C expects the extraction target to already exist.

Proposed fix
  async install(release: ReleaseArtifactMetadata): Promise<string> {
    const destFile = await this.download(release);
+   await mkdir(this.#installDirectory, { recursive: true });
 
    if (destFile.endsWith('.zip')) {
      const directory = await Open.file(destFile);
      await directory.extract({ path: this.#installDirectory });

Also applies to: 75-82, 97-100

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts` around lines 57 - 60, The
install path fails if `#installDirectory` was removed by uninstall() because
init() is only called once; update the install() method in the
KortexCliDownloader (and similarly ensure any other extraction callers) to
recreate the target directory before running tar -C by calling
mkdir(this.#installDirectory, { recursive: true }) or equivalent; ensure
install() checks/creates this.#installDirectory immediately before extraction so
repeated uninstall/install in the same session succeeds.

Comment on lines +108 to +123
async selectVersion(cliInfo?: CliTool): Promise<ReleaseArtifactMetadata> {
let releasesMetadata = await this.grabLatestReleasesMetadata();

if (releasesMetadata.length === 0) throw new Error('cannot grab kortex-cli releases');

if (cliInfo) {
releasesMetadata = releasesMetadata.filter(release => release.tag.slice(1) !== cliInfo.version);
}

const selectedRelease = await this.windowAPI.showQuickPick(releasesMetadata, {
placeHolder: 'Select Kortex CLI version to download',
});

if (!selectedRelease) {
throw new Error('No version selected');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle the empty release list after post-processing.

After filtering out the currently installed version, selectVersion() can end up with no candidates and still fall through to No version selected. getLatestVersionAsset() has the same gap and currently returns undefined despite its non-optional return type.

Proposed fix
  async selectVersion(cliInfo?: CliTool): Promise<ReleaseArtifactMetadata> {
    let releasesMetadata = await this.grabLatestReleasesMetadata();
 
    if (releasesMetadata.length === 0) throw new Error('cannot grab kortex-cli releases');
 
    if (cliInfo) {
      releasesMetadata = releasesMetadata.filter(release => release.tag.slice(1) !== cliInfo.version);
    }
+   if (releasesMetadata.length === 0) {
+     throw new Error('No other Kortex CLI versions are available');
+   }
 
    const selectedRelease = await this.windowAPI.showQuickPick(releasesMetadata, {
      placeHolder: 'Select Kortex CLI version to download',
    });
@@
  async getLatestVersionAsset(): Promise<ReleaseArtifactMetadata> {
    const latestReleases = await this.grabLatestReleasesMetadata();
+   if (latestReleases.length === 0) {
+     throw new Error('cannot grab kortex-cli releases');
+   }
    return latestReleases[0];
  }

Also applies to: 144-146

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts` around lines 108 - 123,
selectVersion may filter out all releases (releasesMetadata) and still call
showQuickPick, and getLatestVersionAsset can return undefined despite its
non-optional signature; after you filter releasesMetadata (in selectVersion)
check for releasesMetadata.length === 0 and throw a clear Error like "no other
Kortex CLI versions available" before calling windowAPI.showQuickPick, and
similarly update getLatestVersionAsset to validate its candidate list (or the
asset result) and throw a descriptive Error instead of returning undefined so
both methods always either return the expected non-optional type or raise a
clear exception (refer to selectVersion, releasesMetadata,
windowAPI.showQuickPick and getLatestVersionAsset to locate the code to change).

Comment on lines +26 to +32
if (envAPI.isLinux || envAPI.isMac) {
try {
const { stdout: fullPath } = await processAPI.exec('which', [executable]);
return fullPath;
} catch (err) {
console.warn('Error getting full path', err);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Linux/macOS: trim trailing newline from which output.

The which command output includes a trailing newline. Returning it untrimmed could cause path comparison or file access issues.

🐛 Proposed fix
   if (envAPI.isLinux || envAPI.isMac) {
     try {
       const { stdout: fullPath } = await processAPI.exec('which', [executable]);
-      return fullPath;
+      return fullPath.trim();
     } catch (err) {
       console.warn('Error getting full path', 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
if (envAPI.isLinux || envAPI.isMac) {
try {
const { stdout: fullPath } = await processAPI.exec('which', [executable]);
return fullPath;
} catch (err) {
console.warn('Error getting full path', err);
}
if (envAPI.isLinux || envAPI.isMac) {
try {
const { stdout: fullPath } = await processAPI.exec('which', [executable]);
return fullPath.trim();
} catch (err) {
console.warn('Error getting full path', err);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/utils/system.ts` around lines 26 - 32, The code in
the envAPI.isLinux/isMac branch returns the raw stdout from
processAPI.exec('which', [executable]) (captured as fullPath) which includes a
trailing newline; fix by trimming whitespace (e.g., call .trim() on the stdout
before returning) so callers get a clean path string from processAPI.exec /
fullPath; ensure you still handle the catch path unchanged.

Comment on lines +34 to +39
try {
const { stdout: fullPath } = await processAPI.exec('where.exe', [executable]);
return fullPath.replace(/(\r\n|\n|\r)/gm, '');
} catch (err) {
console.warn('Error getting full path', err);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Windows where.exe may return multiple paths; only the first should be used.

where.exe outputs one path per line if the executable exists in multiple PATH locations. The current regex removes all line breaks, concatenating the paths into an invalid string (e.g., C:\path1\kortex.exeC:\path2\kortex.exe).

🐛 Proposed fix: return only the first line
     try {
       const { stdout: fullPath } = await processAPI.exec('where.exe', [executable]);
-      return fullPath.replace(/(\r\n|\n|\r)/gm, '');
+      return fullPath.split(/\r?\n/)[0].trim();
     } catch (err) {
       console.warn('Error getting full path', 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 {
const { stdout: fullPath } = await processAPI.exec('where.exe', [executable]);
return fullPath.replace(/(\r\n|\n|\r)/gm, '');
} catch (err) {
console.warn('Error getting full path', err);
}
try {
const { stdout: fullPath } = await processAPI.exec('where.exe', [executable]);
return fullPath.split(/\r?\n/)[0].trim();
} catch (err) {
console.warn('Error getting full path', err);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/utils/system.ts` around lines 34 - 39, The code
currently strips all newlines from the output of processAPI.exec('where.exe',
[executable]) resulting in concatenated paths; change the post-processing of the
returned fullPath to split on newline (e.g., /\r?\n/) and return only the first
non-empty trimmed line (use the result of processAPI.exec and the fullPath
variable), leaving the existing catch behavior intact. This ensures only the
first path from where.exe is returned.

@bmahabirbu bmahabirbu force-pushed the embed-kortex-cli-extension branch from 7391cfc to 868314f Compare April 2, 2026 01:24
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
extensions/kortex-cli/src/kortex-cli-downloader.ts (2)

75-83: ⚠️ Potential issue | 🔴 Critical

Recreate install directory inside install flow before extraction.

After uninstall() removes the directory, a same-session reinstall can fail because extraction targets a missing path.

Suggested fix
   async install(release: ReleaseArtifactMetadata): Promise<string> {
     const destFile = await this.download(release);
+    await mkdir(this.#installDirectory, { recursive: true });
 
     if (destFile.endsWith('.zip')) {
       const directory = await Open.file(destFile);
       await directory.extract({ path: this.#installDirectory });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts` around lines 75 - 83, The
install flow can fail if uninstall() removed the install directory—ensure the
directory exists before extracting: in install(release: ReleaseArtifactMetadata)
after const destFile = await this.download(release) and before any extraction
(both the '.zip' branch that calls Open.file(...).extract(...) and the '.tar.gz'
branch that calls extractTarGz), recreate the install directory
(this.#installDirectory) atomically (mkdir -p semantics) with appropriate
permissions so extraction has a valid target; update extractTarGz and zip
extraction call sites to assume the directory is present.

113-117: ⚠️ Potential issue | 🟡 Minor

Guard empty release candidates after filtering and before returning latest.

Both flows can end up with no candidates; one shows a misleading selection error and the other can return undefined from a non-optional method.

Suggested fix
   async selectVersion(cliInfo?: CliTool): Promise<ReleaseArtifactMetadata> {
     let releasesMetadata = await this.grabLatestReleasesMetadata();
@@
     if (cliInfo) {
       releasesMetadata = releasesMetadata.filter(release => release.tag.slice(1) !== cliInfo.version);
     }
+    if (releasesMetadata.length === 0) {
+      throw new Error('No other Kortex CLI versions are available');
+    }
@@
   async getLatestVersionAsset(): Promise<ReleaseArtifactMetadata> {
     const latestReleases = await this.grabLatestReleasesMetadata();
+    if (latestReleases.length === 0) {
+      throw new Error('cannot grab kortex-cli releases');
+    }
     return latestReleases[0];
   }

Also applies to: 144-146

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts` around lines 113 - 117,
After filtering out the current CLI version (in the block using cliInfo and
releasesMetadata) add a guard that checks if releasesMetadata is empty before
calling this.windowAPI.showQuickPick and before the code path that returns the
latest release; if empty, surface a clear error/prompt to the user (or
throw/return a defined value) instead of proceeding and producing a misleading
selection error or returning undefined from the non-optional method. Apply the
same empty-check guard to the other flow referenced around the code that returns
the latest release (the alternate branch at lines ~144-146) so both
selectedRelease and the “latest” return path never operate on an empty
releasesMetadata array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@extensions/kortex-cli/src/kortex-cli-downloader.ts`:
- Around line 65-69: The extractTarGz function currently uses exec with an
interpolated shell command causing command-injection risk; change it to call
execFile (imported from child_process) or the promisified variant and pass "tar"
as the executable with an argv array like ["-xzf", filePath, "-C", outDir] so
paths are passed as arguments (no shell parsing), and update the Promise
callback to resolve/reject based on execFile's error/result signature in
extractTarGz.

---

Duplicate comments:
In `@extensions/kortex-cli/src/kortex-cli-downloader.ts`:
- Around line 75-83: The install flow can fail if uninstall() removed the
install directory—ensure the directory exists before extracting: in
install(release: ReleaseArtifactMetadata) after const destFile = await
this.download(release) and before any extraction (both the '.zip' branch that
calls Open.file(...).extract(...) and the '.tar.gz' branch that calls
extractTarGz), recreate the install directory (this.#installDirectory)
atomically (mkdir -p semantics) with appropriate permissions so extraction has a
valid target; update extractTarGz and zip extraction call sites to assume the
directory is present.
- Around line 113-117: After filtering out the current CLI version (in the block
using cliInfo and releasesMetadata) add a guard that checks if releasesMetadata
is empty before calling this.windowAPI.showQuickPick and before the code path
that returns the latest release; if empty, surface a clear error/prompt to the
user (or throw/return a defined value) instead of proceeding and producing a
misleading selection error or returning undefined from the non-optional method.
Apply the same empty-check guard to the other flow referenced around the code
that returns the latest release (the alternate branch at lines ~144-146) so both
selectedRelease and the “latest” return path never operate on an empty
releasesMetadata array.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 651667c0-47e4-47ae-8843-e020d4aa3292

📥 Commits

Reviewing files that changed from the base of the PR and between 7391cfc and 868314f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • extensions/kortex-cli/package.json
  • extensions/kortex-cli/scripts/build.js
  • extensions/kortex-cli/src/extension.ts
  • extensions/kortex-cli/src/kortex-cli-downloader.ts
  • extensions/kortex-cli/src/kortex-cli.ts
  • extensions/kortex-cli/src/utils/system.ts
  • extensions/kortex-cli/tsconfig.json
  • extensions/kortex-cli/vite.config.js
  • package.json
✅ Files skipped from review due to trivial changes (4)
  • extensions/kortex-cli/tsconfig.json
  • extensions/kortex-cli/package.json
  • extensions/kortex-cli/scripts/build.js
  • extensions/kortex-cli/src/utils/system.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • package.json
  • extensions/kortex-cli/vite.config.js
  • extensions/kortex-cli/src/extension.ts
  • extensions/kortex-cli/src/kortex-cli.ts

Comment on lines +65 to +69
extractTarGz(filePath: string, outDir: string): Promise<void> {
return new Promise((resolve, reject) => {
// eslint-disable-next-line sonarjs/os-command
exec(`tar -xzf "${filePath}" -C "${outDir}"`, err => {
if (err) reject(err);
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Apr 2, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify shell-based command execution and data flow to command arguments.
rg -n -C2 'exec\(|execFile\(' extensions/kortex-cli/src/kortex-cli-downloader.ts
rg -n -C2 'asset\.name|destination|extractTarGz\(' extensions/kortex-cli/src/kortex-cli-downloader.ts

Repository: kortex-hub/kortex

Length of output: 1308


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check imports at top of file
head -50 extensions/kortex-cli/src/kortex-cli-downloader.ts | grep -n "import.*child_process"

# Get full extractTarGz method implementation
sed -n '65,75p' extensions/kortex-cli/src/kortex-cli-downloader.ts

# Check how filePath/outDir are used - verify no escaping
rg -n -B5 -A10 'extractTarGz\(destFile' extensions/kortex-cli/src/kortex-cli-downloader.ts

# Check asset.name and storageData sources
rg -n -B3 'const destination = join' extensions/kortex-cli/src/kortex-cli-downloader.ts

Repository: kortex-hub/kortex

Length of output: 1255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify GitHub release asset names can contain special characters
# Check GitHub API documentation constraints on asset.name

# Also verify if there's any sanitization of asset.name before it's used
rg -n 'asset\.name' extensions/kortex-cli/src/kortex-cli-downloader.ts

# Check if there's any string escaping utility applied
rg -n 'escape|quote|shell' extensions/kortex-cli/src/kortex-cli-downloader.ts

Repository: kortex-hub/kortex

Length of output: 119


Use execFile() with argv array instead of exec() to prevent command injection via interpolated paths.

The exec() call on line 68 runs the command through a shell; filePath and outDir are interpolated as template literals. Since filePath derives from asset.name (which can contain arbitrary characters from GitHub releases), this allows shell metacharacters (backticks, $(...), semicolons, etc.) to be executed as commands rather than passed as literal arguments. Use execFile() with an arguments array to pass data directly to tar without shell parsing.

Suggested fix
-import { exec } from 'node:child_process';
+import { execFile } from 'node:child_process';
@@
   extractTarGz(filePath: string, outDir: string): Promise<void> {
     return new Promise((resolve, reject) => {
-      // eslint-disable-next-line sonarjs/os-command
-      exec(`tar -xzf "${filePath}" -C "${outDir}"`, err => {
+      execFile('tar', ['-xzf', filePath, '-C', outDir], err => {
         if (err) reject(err);
         else resolve();
       });
     });
   }
📝 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
extractTarGz(filePath: string, outDir: string): Promise<void> {
return new Promise((resolve, reject) => {
// eslint-disable-next-line sonarjs/os-command
exec(`tar -xzf "${filePath}" -C "${outDir}"`, err => {
if (err) reject(err);
extractTarGz(filePath: string, outDir: string): Promise<void> {
return new Promise((resolve, reject) => {
execFile('tar', ['-xzf', filePath, '-C', outDir], err => {
if (err) reject(err);
else resolve();
});
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts` around lines 65 - 69, The
extractTarGz function currently uses exec with an interpolated shell command
causing command-injection risk; change it to call execFile (imported from
child_process) or the promisified variant and pass "tar" as the executable with
an argv array like ["-xzf", filePath, "-C", outDir] so paths are passed as
arguments (no shell parsing), and update the Promise callback to resolve/reject
based on execFile's error/result signature in extractTarGz.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The exec → execFile suggestion was not applied because the goose extension uses the same
exec + // eslint-disable-next-line sonarjs/os-command pattern, and switching would require a
different lint suppression that doesn't match the codebase conventions

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the goose extension for the exec + sonarjs/os-command pattern
rg -n -C3 'sonarjs/os-command\|exec\b' extensions/goose/ 2>/dev/null || \
rg -n -C3 'os-command' extensions/goose/ 2>/dev/null || \
rg -rn -C3 'sonarjs/os-command' extensions/ 2>/dev/null

Length of output: 589


@bmahabirbu, confirmed — extensions/goose/src/goose-downloader.ts lines 68–69 use the exact same exec + // eslint-disable-next-line sonarjs/os-command pattern. Keeping consistency with the established convention is reasonable.

That said, the underlying shell-injection risk remains for both extensions if filePath or outDir ever contain shell metacharacters. It may be worth tracking this as a follow-up to harden both the goose and kortex-cli extractors together at some point. Would you like me to open a GitHub issue to track that?


✏️ Learnings added
Learnt from: bmahabirbu
Repo: kortex-hub/kortex PR: 1213
File: extensions/kortex-cli/src/kortex-cli-downloader.ts:65-69
Timestamp: 2026-04-02T03:06:18.286Z
Learning: In the kortex-hub/kortex repository, the codebase convention for suppressing the `sonarjs/os-command` lint rule when using `exec()` for shell commands (e.g., tar extraction) is `// eslint-disable-next-line sonarjs/os-command` inline before the `exec()` call. This pattern is established in `extensions/goose/src/goose-downloader.ts` (line 68) and mirrored in `extensions/kortex-cli/src/kortex-cli-downloader.ts`. Switching to `execFile()` is intentionally avoided to maintain consistency with this pattern, even though `execFile()` would eliminate the shell-injection risk.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Adds a new built-in extension that downloads and registers the kortex CLI
binary from kortex-hub/kortex-cli GitHub releases, enabling users to work
with workspaces out of the box.

Closes kortex-hub#1127

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Brian <bmahabir@bu.edu>
@bmahabirbu bmahabirbu force-pushed the embed-kortex-cli-extension branch from 868314f to 835b0e5 Compare April 2, 2026 03:04
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
extensions/kortex-cli/src/kortex-cli-downloader.ts (4)

75-82: ⚠️ Potential issue | 🔴 Critical

Recreate the extraction directory inside install().

If uninstall() (Line 100-103) ran earlier, install() can fail because extraction assumes this.#installDirectory already exists. Create it right before extraction.

Proposed fix
   async install(release: ReleaseArtifactMetadata): Promise<string> {
     const destFile = await this.download(release);
+    await mkdir(this.#installDirectory, { recursive: true });
 
     if (destFile.endsWith('.zip')) {
       const directory = await Open.file(destFile);
       await directory.extract({ path: this.#installDirectory });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts` around lines 75 - 82, The
install() method assumes this.#installDirectory exists before extracting; ensure
the directory is recreated just before extraction to avoid failures after
uninstall() removed it. Modify install() so after downloading (destFile) and
before calling Open.file(...).extract(...) or extractTarGz(destFile,...), create
or ensure existence of this.#installDirectory (e.g., mkdir -p behavior) so both
.zip extraction via Open.file(...) and .tar.gz extraction in extractTarGz(...)
have a valid target directory.

147-149: ⚠️ Potential issue | 🟠 Major

Do not return undefined from a non-optional Promise<ReleaseArtifactMetadata>.

Line 149 can return undefined when no releases are available, violating the declared return contract and causing downstream runtime failures.

#!/bin/bash
set -euo pipefail

# Verify missing empty-list guard in getLatestVersionAsset()
rg -n -C3 'async getLatestVersionAsset|return latestReleases\[0\]' extensions/kortex-cli/src/kortex-cli-downloader.ts
Proposed fix
   async getLatestVersionAsset(): Promise<ReleaseArtifactMetadata> {
     const latestReleases = await this.grabLatestReleasesMetadata();
+    if (latestReleases.length === 0) {
+      throw new Error('cannot grab kortex-cli releases');
+    }
     return latestReleases[0];
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts` around lines 147 - 149,
The getLatestVersionAsset function currently returns latestReleases[0] which can
be undefined when grabLatestReleasesMetadata() returns an empty array; update
getLatestVersionAsset to guard against an empty latestReleases (call to
grabLatestReleasesMetadata) and if no releases exist, throw a clear Error (or
return a rejected Promise) describing "no releases found" so the function never
resolves to undefined; reference the getLatestVersionAsset and
grabLatestReleasesMetadata symbols and ensure the thrown error type matches the
surrounding error-handling conventions.

65-69: ⚠️ Potential issue | 🟠 Major

Avoid shell interpolation in archive extraction command.

Line 68 interpolates filePath/outDir into a shell command. Because filePath is derived from release asset names, shell metacharacters can be interpreted.

#!/bin/bash
set -euo pipefail

# Verify interpolation path from asset.name into exec() command
rg -n -C3 'exec\(`tar -xzf' extensions/kortex-cli/src/kortex-cli-downloader.ts
rg -n -C3 'const destination = join\(storageData, asset\.name\)' extensions/kortex-cli/src/kortex-cli-downloader.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts` around lines 65 - 69, The
extractTarGz function currently passes filePath and outDir into a shell string
via exec, which risks shell injection from asset-derived names; change it to
invoke the tar binary without a shell or use a safe library: replace the
exec(`tar -xzf "${filePath}" -C "${outDir}"`, ...) call in extractTarGz with a
shellless invocation (e.g. child_process.spawn or execFile with args ['-xzf',
filePath, '-C', outDir']) or use a Node tar library (e.g. tar.x) to extract into
outDir, ensuring filePath and outDir are provided as arguments rather than
interpolated into a shell string.

116-126: ⚠️ Potential issue | 🟡 Minor

Handle the “no versions left” case after filtering current version.

After Line 117 filtering, releasesMetadata can be empty and showQuickPick falls through to a misleading “No version selected” error.

Proposed fix
   if (cliInfo) {
     releasesMetadata = releasesMetadata.filter(release => release.tag.slice(1) !== cliInfo.version);
   }
+  if (releasesMetadata.length === 0) {
+    throw new Error('No other Kortex CLI versions are available');
+  }
 
   const selectedRelease = await this.windowAPI.showQuickPick(releasesMetadata, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli-downloader.ts` around lines 116 - 126,
After filtering out the current version from releasesMetadata (using cliInfo),
handle the case where releasesMetadata becomes empty before calling
windowAPI.showQuickPick: check if releasesMetadata.length === 0 and either throw
a clear error (e.g. 'No other Kortex CLI versions available') or call
windowAPI.showInformationMessage to inform the user, then return/exit; otherwise
proceed to call windowAPI.showQuickPick and keep the existing selectedRelease
null check. Reference symbols: releasesMetadata, cliInfo,
windowAPI.showQuickPick, selectedRelease.
🧹 Nitpick comments (1)
extensions/kortex-cli/src/kortex-cli.ts (1)

31-31: Use the /@/ alias for non-sibling import paths.

./utils/system is not a same-directory sibling import; please switch this to the repository alias style.

As per coding guidelines: “Use /@/ path aliases for imports outside the current directory's module group … Use relative imports only for sibling modules within the same directory.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli.ts` at line 31, Replace the non-sibling
relative import of whereBinary in kortex-cli.ts with the repository alias form;
specifically change the import that currently references './utils/system' to use
the /@/ path alias pointing to the utils/system module so the symbol whereBinary
is imported via the alias rather than a relative path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@extensions/kortex-cli/src/kortex-cli.ts`:
- Around line 67-70: The value returned from whereBinary (assigned to the local
variable location) may contain trailing whitespace/newline; trim that value
before persisting it as the CLI path. Update the return in the function that
calls whereBinary (the block assigning const location = await
whereBinary(this.envAPI, this.processAPI, executable)) to use location.trim()
(or otherwise sanitize) so the returned object’s path property is stored without
trailing newlines or spaces.

---

Duplicate comments:
In `@extensions/kortex-cli/src/kortex-cli-downloader.ts`:
- Around line 75-82: The install() method assumes this.#installDirectory exists
before extracting; ensure the directory is recreated just before extraction to
avoid failures after uninstall() removed it. Modify install() so after
downloading (destFile) and before calling Open.file(...).extract(...) or
extractTarGz(destFile,...), create or ensure existence of this.#installDirectory
(e.g., mkdir -p behavior) so both .zip extraction via Open.file(...) and .tar.gz
extraction in extractTarGz(...) have a valid target directory.
- Around line 147-149: The getLatestVersionAsset function currently returns
latestReleases[0] which can be undefined when grabLatestReleasesMetadata()
returns an empty array; update getLatestVersionAsset to guard against an empty
latestReleases (call to grabLatestReleasesMetadata) and if no releases exist,
throw a clear Error (or return a rejected Promise) describing "no releases
found" so the function never resolves to undefined; reference the
getLatestVersionAsset and grabLatestReleasesMetadata symbols and ensure the
thrown error type matches the surrounding error-handling conventions.
- Around line 65-69: The extractTarGz function currently passes filePath and
outDir into a shell string via exec, which risks shell injection from
asset-derived names; change it to invoke the tar binary without a shell or use a
safe library: replace the exec(`tar -xzf "${filePath}" -C "${outDir}"`, ...)
call in extractTarGz with a shellless invocation (e.g. child_process.spawn or
execFile with args ['-xzf', filePath, '-C', outDir']) or use a Node tar library
(e.g. tar.x) to extract into outDir, ensuring filePath and outDir are provided
as arguments rather than interpolated into a shell string.
- Around line 116-126: After filtering out the current version from
releasesMetadata (using cliInfo), handle the case where releasesMetadata becomes
empty before calling windowAPI.showQuickPick: check if releasesMetadata.length
=== 0 and either throw a clear error (e.g. 'No other Kortex CLI versions
available') or call windowAPI.showInformationMessage to inform the user, then
return/exit; otherwise proceed to call windowAPI.showQuickPick and keep the
existing selectedRelease null check. Reference symbols: releasesMetadata,
cliInfo, windowAPI.showQuickPick, selectedRelease.

---

Nitpick comments:
In `@extensions/kortex-cli/src/kortex-cli.ts`:
- Line 31: Replace the non-sibling relative import of whereBinary in
kortex-cli.ts with the repository alias form; specifically change the import
that currently references './utils/system' to use the /@/ path alias pointing to
the utils/system module so the symbol whereBinary is imported via the alias
rather than a relative path.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49d5b3d8-65fc-4baf-90db-5305ba219852

📥 Commits

Reviewing files that changed from the base of the PR and between 868314f and 835b0e5.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • extensions/kortex-cli/package.json
  • extensions/kortex-cli/scripts/build.js
  • extensions/kortex-cli/src/extension.ts
  • extensions/kortex-cli/src/kortex-cli-downloader.ts
  • extensions/kortex-cli/src/kortex-cli.ts
  • extensions/kortex-cli/src/utils/system.ts
  • extensions/kortex-cli/tsconfig.json
  • extensions/kortex-cli/vite.config.js
  • package.json
✅ Files skipped from review due to trivial changes (4)
  • extensions/kortex-cli/tsconfig.json
  • extensions/kortex-cli/src/utils/system.ts
  • extensions/kortex-cli/scripts/build.js
  • extensions/kortex-cli/vite.config.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • package.json
  • extensions/kortex-cli/src/extension.ts
  • extensions/kortex-cli/package.json

Comment on lines +67 to +70
const location = await whereBinary(this.envAPI, this.processAPI, executable);
return {
path: location,
version: stdout.trim(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Trim whereBinary() output before persisting it as the CLI path.

On Linux/macOS, which output commonly includes a trailing newline; storing it untrimmed can produce an invalid/ugly path.

Proposed fix
-      const location = await whereBinary(this.envAPI, this.processAPI, executable);
+      const location = (await whereBinary(this.envAPI, this.processAPI, executable)).trim();
       return {
         path: location,
         version: stdout.trim(),
         installationSource: 'external',
📝 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 location = await whereBinary(this.envAPI, this.processAPI, executable);
return {
path: location,
version: stdout.trim(),
const location = (await whereBinary(this.envAPI, this.processAPI, executable)).trim();
return {
path: location,
version: stdout.trim(),
installationSource: 'external',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extensions/kortex-cli/src/kortex-cli.ts` around lines 67 - 70, The value
returned from whereBinary (assigned to the local variable location) may contain
trailing whitespace/newline; trim that value before persisting it as the CLI
path. Update the return in the function that calls whereBinary (the block
assigning const location = await whereBinary(this.envAPI, this.processAPI,
executable)) to use location.trim() (or otherwise sanitize) so the returned
object’s path property is stored without trailing newlines or spaces.

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.

Update installer to embed latest version of kortex cli

1 participant