-
Notifications
You must be signed in to change notification settings - Fork 19
feat: add kortex-cli extension to embed CLI for workspace management #1213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| { | ||
| "name": "kortex-cli", | ||
| "displayName": "Kortex CLI", | ||
| "description": "Kortex CLI integration", | ||
| "version": "0.0.1-next", | ||
| "icon": "icon.png", | ||
| "publisher": "kortex", | ||
| "license": "Apache-2.0", | ||
| "engines": { | ||
| "kortex": "^0.0.1" | ||
| }, | ||
| "main": "./dist/extension.js", | ||
| "contributes": { | ||
| "configuration": {} | ||
| }, | ||
| "scripts": { | ||
| "build": "vite build && node scripts/build.js", | ||
| "test": "vitest run --coverage --passWithNoTests", | ||
| "test:watch": "vitest watch --coverage --passWithNoTests", | ||
| "watch": "vite build --watch" | ||
| }, | ||
| "dependencies": { | ||
| "@kortex-app/api": "workspace:*", | ||
| "@octokit/rest": "^21.1.0", | ||
| "unzipper": "^0.11.6" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/unzipper": "^0.10.11", | ||
| "mkdirp": "^3.0.1", | ||
| "vite": "^7.0.6", | ||
| "vitest": "^4.0.10" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| #!/usr/bin/env node | ||
| /********************************************************************** | ||
| * Copyright (C) 2026 Red Hat, Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| * | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| ***********************************************************************/ | ||
|
|
||
| const AdmZip = require('adm-zip'); | ||
| const path = require('path'); | ||
| const packageJson = require('../package.json'); | ||
| const fs = require('fs'); | ||
| const { mkdirp } = require('mkdirp'); | ||
|
|
||
| const destFile = path.resolve(__dirname, `../${packageJson.name}.cdix`); | ||
| const builtinDirectory = path.resolve(__dirname, '../builtin'); | ||
| const unzippedDirectory = path.resolve(builtinDirectory, `${packageJson.name}.cdix`); | ||
| // remove the .cdix file before zipping | ||
| if (fs.existsSync(destFile)) { | ||
| fs.rmSync(destFile); | ||
| } | ||
| // remove the builtin folder before zipping | ||
| if (fs.existsSync(builtinDirectory)) { | ||
| fs.rmSync(builtinDirectory, { recursive: true, force: true }); | ||
| } | ||
|
|
||
| const zip = new AdmZip(); | ||
| zip.addLocalFolder(path.resolve(__dirname, '../')); | ||
| zip.writeZip(destFile); | ||
|
|
||
| // create unzipped built-in | ||
| (async () => { | ||
| await mkdirp(unzippedDirectory); | ||
| const unzip = new AdmZip(destFile); | ||
| unzip.extractAllTo(unzippedDirectory); | ||
| })(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /********************************************************************** | ||
| * Copyright (C) 2026 Red Hat, Inc. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| * | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| ***********************************************************************/ | ||
|
|
||
| import type { ExtensionContext } from '@kortex-app/api'; | ||
| import { cli, env, process as processAPI, window } from '@kortex-app/api'; | ||
| import type { OctokitOptions } from '@octokit/core'; | ||
| import { Octokit } from '@octokit/rest'; | ||
|
|
||
| import { KortexCLI } from './kortex-cli'; | ||
| import { KortexCliDownloader } from './kortex-cli-downloader'; | ||
|
|
||
| export async function activate(extensionContext: ExtensionContext): Promise<void> { | ||
| const octokitOptions: OctokitOptions = {}; | ||
| if (process.env.GITHUB_TOKEN) { | ||
| octokitOptions.auth = process.env.GITHUB_TOKEN; | ||
| } | ||
| const octokit = new Octokit(octokitOptions); | ||
|
|
||
| const downloader = new KortexCliDownloader(extensionContext, octokit, env, window); | ||
| await downloader.init(); | ||
| extensionContext.subscriptions.push(downloader); | ||
|
|
||
| const kortexCLI = new KortexCLI(cli, processAPI, downloader, env); | ||
| await kortexCLI.init(); | ||
| extensionContext.subscriptions.push(kortexCLI); | ||
| } | ||
|
|
||
| export function deactivate(): void {} |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,235 @@ | ||||||||||||||||||||||||||||
| /********************************************************************** | ||||||||||||||||||||||||||||
| * Copyright (C) 2026 Red Hat, Inc. | ||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||||||||||||||||||||||||
| * you may not use this file except in compliance with the License. | ||||||||||||||||||||||||||||
| * You may obtain a copy of the License at | ||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||
| * Unless required by applicable law or agreed to in writing, software | ||||||||||||||||||||||||||||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||||||||||||||||||||||||||||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||||||||||||||||||||||||
| * See the License for the specific language governing permissions and | ||||||||||||||||||||||||||||
| * limitations under the License. | ||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||
| * SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||||||||
| ***********************************************************************/ | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import { exec } from 'node:child_process'; | ||||||||||||||||||||||||||||
| import { existsSync } from 'node:fs'; | ||||||||||||||||||||||||||||
| import { chmod, mkdir, rm, unlink, writeFile } from 'node:fs/promises'; | ||||||||||||||||||||||||||||
| import { arch } from 'node:os'; | ||||||||||||||||||||||||||||
| import { join } from 'node:path'; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| import type { | ||||||||||||||||||||||||||||
| CliTool, | ||||||||||||||||||||||||||||
| Disposable, | ||||||||||||||||||||||||||||
| env as EnvAPI, | ||||||||||||||||||||||||||||
| ExtensionContext, | ||||||||||||||||||||||||||||
| QuickPickItem, | ||||||||||||||||||||||||||||
| window as WindowAPI, | ||||||||||||||||||||||||||||
| } from '@kortex-app/api'; | ||||||||||||||||||||||||||||
| import type { components as OctokitComponents } from '@octokit/openapi-types'; | ||||||||||||||||||||||||||||
| import type { Octokit } from '@octokit/rest'; | ||||||||||||||||||||||||||||
| import { Open } from 'unzipper'; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const GITHUB_ORG = 'kortex-hub'; | ||||||||||||||||||||||||||||
| const GITHUB_REPO = 'kortex-cli'; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export interface ReleaseArtifactMetadata extends QuickPickItem { | ||||||||||||||||||||||||||||
| tag: string; | ||||||||||||||||||||||||||||
| id: number; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export class KortexCliDownloader implements Disposable { | ||||||||||||||||||||||||||||
| #installDirectory: string; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| constructor( | ||||||||||||||||||||||||||||
| private readonly extensionContext: ExtensionContext, | ||||||||||||||||||||||||||||
| private readonly octokit: Octokit, | ||||||||||||||||||||||||||||
| private readonly envAPI: typeof EnvAPI, | ||||||||||||||||||||||||||||
| private readonly windowAPI: typeof WindowAPI, | ||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||
| this.#installDirectory = join(this.extensionContext.storagePath, 'kortex-cli-package'); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async init(): Promise<void> { | ||||||||||||||||||||||||||||
| if (!existsSync(this.#installDirectory)) { | ||||||||||||||||||||||||||||
| await mkdir(this.#installDirectory, { recursive: true }); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| dispose(): void {} | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.tsRepository: 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.tsRepository: 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.tsRepository: kortex-hub/kortex Length of output: 119 Use The 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
Suggested change
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Length of output: 589
That said, the underlying shell-injection risk remains for both extensions if ✏️ Learnings added
|
||||||||||||||||||||||||||||
| else resolve(); | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async install(release: ReleaseArtifactMetadata): Promise<string> { | ||||||||||||||||||||||||||||
| const destFile = await this.download(release); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (destFile.endsWith('.zip')) { | ||||||||||||||||||||||||||||
| const directory = await Open.file(destFile); | ||||||||||||||||||||||||||||
| await directory.extract({ path: this.#installDirectory }); | ||||||||||||||||||||||||||||
| } else if (destFile.endsWith('.tar.gz') && (this.envAPI.isMac || this.envAPI.isLinux)) { | ||||||||||||||||||||||||||||
| await this.extractTarGz(destFile, this.#installDirectory); | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| throw new Error(`Unsupported archive format: ${destFile}`); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| await unlink(destFile); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const executablePath = this.getKortexExecutablePath(); | ||||||||||||||||||||||||||||
| if (!existsSync(executablePath)) { | ||||||||||||||||||||||||||||
| throw new Error(`Kortex CLI executable was not found after extraction: ${executablePath}`); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| if (!this.envAPI.isWindows) { | ||||||||||||||||||||||||||||
| await chmod(executablePath, 0o755); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return executablePath; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async uninstall(): Promise<void> { | ||||||||||||||||||||||||||||
| if (existsSync(this.#installDirectory)) { | ||||||||||||||||||||||||||||
| await rm(this.#installDirectory, { recursive: true }); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| getKortexExecutablePath(): string { | ||||||||||||||||||||||||||||
| const executable = this.envAPI.isWindows ? 'kortex.exe' : 'kortex'; | ||||||||||||||||||||||||||||
| return join(this.#installDirectory, executable); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| 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'); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+111
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle the empty release list after post-processing. After filtering out the currently installed version, 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 |
||||||||||||||||||||||||||||
| return selectedRelease; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async grabLatestReleasesMetadata(): Promise<ReleaseArtifactMetadata[]> { | ||||||||||||||||||||||||||||
| const lastReleases = await this.octokit.repos.listReleases({ | ||||||||||||||||||||||||||||
| owner: GITHUB_ORG, | ||||||||||||||||||||||||||||
| repo: GITHUB_REPO, | ||||||||||||||||||||||||||||
| per_page: 10, | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return lastReleases.data | ||||||||||||||||||||||||||||
| .filter(release => !release.prerelease) | ||||||||||||||||||||||||||||
| .map(release => ({ | ||||||||||||||||||||||||||||
| label: release.name ?? release.tag_name, | ||||||||||||||||||||||||||||
| tag: release.tag_name, | ||||||||||||||||||||||||||||
| id: release.id, | ||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||
| .slice(0, 5); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async getLatestVersionAsset(): Promise<ReleaseArtifactMetadata> { | ||||||||||||||||||||||||||||
| const latestReleases = await this.grabLatestReleasesMetadata(); | ||||||||||||||||||||||||||||
| return latestReleases[0]; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async getReleaseAssetId(releaseId: number): Promise<OctokitComponents['schemas']['release-asset']> { | ||||||||||||||||||||||||||||
| const architecture = arch(); | ||||||||||||||||||||||||||||
| let assetName: string; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (this.envAPI.isWindows) { | ||||||||||||||||||||||||||||
| switch (architecture) { | ||||||||||||||||||||||||||||
| case 'x64': | ||||||||||||||||||||||||||||
| assetName = 'windows_amd64.zip'; | ||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||
| case 'arm64': | ||||||||||||||||||||||||||||
| assetName = 'windows_arm64.zip'; | ||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||
| throw new Error(`Unsupported architecture for Windows: ${architecture}`); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } else if (this.envAPI.isMac) { | ||||||||||||||||||||||||||||
| switch (architecture) { | ||||||||||||||||||||||||||||
| case 'arm64': | ||||||||||||||||||||||||||||
| assetName = 'darwin_arm64.tar.gz'; | ||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||
| case 'x64': | ||||||||||||||||||||||||||||
| assetName = 'darwin_amd64.tar.gz'; | ||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||
| throw new Error(`Unsupported architecture for macOS: ${architecture}`); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } else if (this.envAPI.isLinux) { | ||||||||||||||||||||||||||||
| switch (architecture) { | ||||||||||||||||||||||||||||
| case 'arm64': | ||||||||||||||||||||||||||||
| assetName = 'linux_arm64.tar.gz'; | ||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||
| case 'x64': | ||||||||||||||||||||||||||||
| assetName = 'linux_amd64.tar.gz'; | ||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||
| throw new Error(`Unsupported architecture for Linux: ${architecture}`); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||
| throw new Error('Unsupported platform'); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const listOfAssets = await this.octokit.repos.listReleaseAssets({ | ||||||||||||||||||||||||||||
| owner: GITHUB_ORG, | ||||||||||||||||||||||||||||
| repo: GITHUB_REPO, | ||||||||||||||||||||||||||||
| release_id: releaseId, | ||||||||||||||||||||||||||||
| per_page: 60, | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const asset = listOfAssets.data.find(a => a.name.endsWith(assetName)); | ||||||||||||||||||||||||||||
| if (!asset) { | ||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||
| `No asset found for ${architecture} on ${this.envAPI.isWindows ? 'Windows' : this.envAPI.isMac ? 'macOS' : 'Linux'}`, | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return asset; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| async download(release: ReleaseArtifactMetadata): Promise<string> { | ||||||||||||||||||||||||||||
| const asset = await this.getReleaseAssetId(release.id); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const storageData = this.extensionContext.storagePath; | ||||||||||||||||||||||||||||
| if (!existsSync(storageData)) { | ||||||||||||||||||||||||||||
| await mkdir(storageData, { recursive: true }); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| const destination = join(storageData, asset.name); | ||||||||||||||||||||||||||||
| await this.downloadReleaseAsset(asset.id, destination); | ||||||||||||||||||||||||||||
| return destination; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| protected async downloadReleaseAsset(assetId: number, destination: string): Promise<void> { | ||||||||||||||||||||||||||||
| const asset = await this.octokit.repos.getReleaseAsset({ | ||||||||||||||||||||||||||||
| owner: GITHUB_ORG, | ||||||||||||||||||||||||||||
| repo: GITHUB_REPO, | ||||||||||||||||||||||||||||
| asset_id: assetId, | ||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||
| accept: 'application/octet-stream', | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| await writeFile(destination, Buffer.from(asset.data as unknown as ArrayBuffer)); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recreate
#installDirectoryinsideinstall().init()is the only place that creates this directory, butuninstall()removes it. A reinstall in the same session will fail on macOS/Linux becausetar -Cexpects 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