From 3fe0fc80ff4eb9abf8a3c3141f549e0f6421e62e Mon Sep 17 00:00:00 2001 From: Brian Date: Wed, 1 Apr 2026 02:41:19 -0400 Subject: [PATCH] feat: add kortex-cli extension to embed CLI for workspace management 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 #1127 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Brian --- extensions/kortex-cli/icon.png | Bin 0 -> 23160 bytes extensions/kortex-cli/package.json | 33 +++ extensions/kortex-cli/scripts/build.js | 46 ++++ extensions/kortex-cli/src/extension.ts | 43 ++++ .../kortex-cli/src/kortex-cli-downloader.ts | 235 ++++++++++++++++++ extensions/kortex-cli/src/kortex-cli.ts | 128 ++++++++++ extensions/kortex-cli/src/utils/system.ts | 43 ++++ extensions/kortex-cli/tsconfig.json | 16 ++ extensions/kortex-cli/vite.config.js | 67 +++++ package.json | 3 +- pnpm-lock.yaml | 73 +++--- 11 files changed, 642 insertions(+), 45 deletions(-) create mode 100644 extensions/kortex-cli/icon.png create mode 100644 extensions/kortex-cli/package.json create mode 100644 extensions/kortex-cli/scripts/build.js create mode 100644 extensions/kortex-cli/src/extension.ts create mode 100644 extensions/kortex-cli/src/kortex-cli-downloader.ts create mode 100644 extensions/kortex-cli/src/kortex-cli.ts create mode 100644 extensions/kortex-cli/src/utils/system.ts create mode 100644 extensions/kortex-cli/tsconfig.json create mode 100644 extensions/kortex-cli/vite.config.js diff --git a/extensions/kortex-cli/icon.png b/extensions/kortex-cli/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0cad7b063348519f1df550eb4a886761a2fd0c18 GIT binary patch literal 23160 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

X@^*IiT$Yu9$B1jjI_PDm5Ug)PP z@798vvuDn1QBpdL20kv@GUdMoF#Cd^n1dB7_m0X1lbal`!P^wN)B>@%mGW0G!6tPg7SIK5cE65lXD2v33T!Lj8c6Y(^L|2UTy^%}qWgbgYi8 zF5(hYrjTY#h1NC0D#L z)7vF4AwU|C1!2<$oY{Z@&4J-(LbQV-2JdcSlA}?HgQbJTj8=}TxF>AZp`1d_ z%1t-;zJs|L)fEhkBmmALv)T3dubms+Kg(!1-Fg88o^H#SF`Bok++=D+IqN~mWswVG z-Vh8|gTLdtGa$K(D_zh&!-caguAE$oK*;W>((A?4xGmJOjh5r6@VoqLW4MwdzoJdC z(IUP=-9o6@PPLsBh^9)T8kRlXq|s;qe;|d)Q!16fASt#R4PMSie|q2J@iV7=zD?D= zo`=~wwfaRWilo2>Rkf$4GN{#nNB!smUm-j+lnJB(Dx6OsOHP&8KlNkcWjQ(Pz^=u6 zSyolW0%<}uuzYmAgKqVn3VSyV0GNX+0I=&K3zXYSbwsCDiC9#%q>6&;vB?c*ARW&V&DStrLiDI4`CVbc1aex?~oV}Jz<_cOPw&C&d+5M*$3mSJk zYY#8t$H)>$ literal 0 HcmV?d00001 diff --git a/extensions/kortex-cli/package.json b/extensions/kortex-cli/package.json new file mode 100644 index 0000000000..1b17c38208 --- /dev/null +++ b/extensions/kortex-cli/package.json @@ -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": { + "@octokit/rest": "^21.1.0", + "unzipper": "^0.11.6" + }, + "devDependencies": { + "@kortex-app/api": "workspace:*", + "@types/unzipper": "^0.10.11", + "mkdirp": "^3.0.1", + "vite": "^7.0.6", + "vitest": "^4.0.10" + } +} diff --git a/extensions/kortex-cli/scripts/build.js b/extensions/kortex-cli/scripts/build.js new file mode 100644 index 0000000000..f49867d055 --- /dev/null +++ b/extensions/kortex-cli/scripts/build.js @@ -0,0 +1,46 @@ +#!/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 +mkdirp(unzippedDirectory).then(() => { + const unzip = new AdmZip(destFile); + unzip.extractAllTo(unzippedDirectory); +}); diff --git a/extensions/kortex-cli/src/extension.ts b/extensions/kortex-cli/src/extension.ts new file mode 100644 index 0000000000..f08ac0ae95 --- /dev/null +++ b/extensions/kortex-cli/src/extension.ts @@ -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 { + 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 {} diff --git a/extensions/kortex-cli/src/kortex-cli-downloader.ts b/extensions/kortex-cli/src/kortex-cli-downloader.ts new file mode 100644 index 0000000000..b12cfd5070 --- /dev/null +++ b/extensions/kortex-cli/src/kortex-cli-downloader.ts @@ -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 { + if (!existsSync(this.#installDirectory)) { + await mkdir(this.#installDirectory, { recursive: true }); + } + } + + dispose(): void {} + + extractTarGz(filePath: string, outDir: string): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line sonarjs/os-command + exec(`tar -xzf "${filePath}" -C "${outDir}"`, err => { + if (err) reject(err); + else resolve(); + }); + }); + } + + async install(release: ReleaseArtifactMetadata): Promise { + 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 { + if (existsSync(this.#installDirectory)) { + await rm(this.#installDirectory, { recursive: true }); + } + } + + getKortexExecutablePath(): string { + const executable = this.envAPI.isWindows ? 'kortex-cli.exe' : 'kortex-cli'; + return join(this.#installDirectory, executable); + } + + async selectVersion(cliInfo?: CliTool): Promise { + 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'); + } + return selectedRelease; + } + + async grabLatestReleasesMetadata(): Promise { + 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 { + const latestReleases = await this.grabLatestReleasesMetadata(); + return latestReleases[0]; + } + + async getReleaseAssetId(releaseId: number): Promise { + 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 { + 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 { + 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)); + } +} diff --git a/extensions/kortex-cli/src/kortex-cli.ts b/extensions/kortex-cli/src/kortex-cli.ts new file mode 100644 index 0000000000..99b971633f --- /dev/null +++ b/extensions/kortex-cli/src/kortex-cli.ts @@ -0,0 +1,128 @@ +/********************************************************************** + * 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 { existsSync } from 'node:fs'; + +import type { + cli as CliAPI, + CliTool, + CliToolInstallationSource, + Disposable, + env as EnvAPI, + process as ProcessAPI, +} from '@kortex-app/api'; + +import type { KortexCliDownloader, ReleaseArtifactMetadata } from './kortex-cli-downloader'; +import { whereBinary } from './utils/system'; + +export const KORTEX_CLI_NAME = 'kortex-cli'; + +export class KortexCLI implements Disposable { + private cli: CliTool | undefined = undefined; + + constructor( + private readonly cliAPI: typeof CliAPI, + private readonly processAPI: typeof ProcessAPI, + private readonly downloader: KortexCliDownloader, + private readonly envAPI: typeof EnvAPI, + ) {} + + protected async findKortexVersion(): Promise< + { path: string; version: string; installationSource: CliToolInstallationSource } | undefined + > { + // Check extension storage first + try { + const path = this.downloader.getKortexExecutablePath(); + if (existsSync(path)) { + const { stdout } = await this.processAPI.exec(path, ['version']); + return { + path, + version: this.parseVersion(stdout), + installationSource: 'extension', + }; + } + } catch (err: unknown) { + console.warn(err); + } + + // Check system PATH + const executable = this.envAPI.isWindows ? 'kortex-cli.exe' : 'kortex-cli'; + try { + const { stdout } = await this.processAPI.exec(executable, ['version']); + const location = await whereBinary(this.envAPI, this.processAPI, executable); + return { + path: location, + version: this.parseVersion(stdout), + installationSource: 'external', + }; + } catch (err: unknown) { + return undefined; + } + } + + // Parse version from "kortex-cli version X.Y.Z" output + protected parseVersion(stdout: string): string { + const parts = stdout.trim().split(' '); + return parts[parts.length - 1]; + } + + async init(): Promise { + const info = await this.findKortexVersion(); + + this.cli = this.cliAPI.createCliTool({ + name: KORTEX_CLI_NAME, + displayName: 'Kortex CLI', + markdownDescription: 'CLI for managing Kortex workspaces', + images: {}, + version: info?.version, + path: info?.path, + installationSource: info?.installationSource, + }); + + if (!this.cli.version) { + let artifact: ReleaseArtifactMetadata | undefined; + + this.cli.registerInstaller({ + selectVersion: async () => { + const release = await this.downloader.selectVersion(this.cli); + artifact = release; + return release.tag.replace('v', '').trim(); + }, + doInstall: async () => { + if (!artifact) throw new Error('No version selected'); + const installPath = await this.downloader.install(artifact); + this.cli?.updateVersion({ + version: artifact.tag.replace('v', '').trim(), + path: installPath, + }); + }, + doUninstall: async () => { + await this.downloader.uninstall(); + this.cli?.updateVersion({ + version: undefined, + path: undefined, + }); + }, + }); + } + } + + dispose(): void { + this.cli?.dispose(); + } +} diff --git a/extensions/kortex-cli/src/utils/system.ts b/extensions/kortex-cli/src/utils/system.ts new file mode 100644 index 0000000000..b54994e649 --- /dev/null +++ b/extensions/kortex-cli/src/utils/system.ts @@ -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 { env as EnvAPI, process as ProcessAPI } from '@kortex-app/api'; + +export async function whereBinary( + envAPI: typeof EnvAPI, + processAPI: typeof ProcessAPI, + executable: string, +): Promise { + 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); + } + } else if (envAPI.isWindows) { + 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); + } + } + + throw new Error(`binary ${executable} not found.`); +} diff --git a/extensions/kortex-cli/tsconfig.json b/extensions/kortex-cli/tsconfig.json new file mode 100644 index 0000000000..32944f54e6 --- /dev/null +++ b/extensions/kortex-cli/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "strict": true, + "module": "esnext", + "lib": ["ES2024"], + "sourceMap": true, + "rootDir": "src", + "outDir": "dist", + "target": "esnext", + "moduleResolution": "node", + "skipLibCheck": true, + "types": ["node"], + "esModuleInterop": true + }, + "include": ["src"] +} diff --git a/extensions/kortex-cli/vite.config.js b/extensions/kortex-cli/vite.config.js new file mode 100644 index 0000000000..55f50ac573 --- /dev/null +++ b/extensions/kortex-cli/vite.config.js @@ -0,0 +1,67 @@ +/********************************************************************** + * 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 { join } from 'path'; +import { builtinModules } from 'module'; + +const PACKAGE_ROOT = __dirname; + +/** + * @type {import('vite').UserConfig} + * @see https://vitejs.dev/config/ + */ +const config = { + mode: process.env.MODE, + root: PACKAGE_ROOT, + envDir: process.cwd(), + resolve: { + alias: { + '/@/': join(PACKAGE_ROOT, 'src') + '/', + }, + }, + build: { + sourcemap: 'inline', + target: 'esnext', + outDir: 'dist', + assetsDir: '.', + minify: process.env.MODE === 'production' ? 'esbuild' : false, + lib: { + entry: 'src/extension.ts', + formats: ['cjs'], + }, + rollupOptions: { + external: ['@kortex-app/api', ...builtinModules.flatMap(p => [p, `node:${p}`])], + output: { + entryFileNames: '[name].js', + }, + }, + emptyOutDir: true, + reportCompressedSize: false, + }, + test: { + globals: true, + environment: 'node', + include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'], + globalSetup: [join(PACKAGE_ROOT, '..', '..', '__mocks__', 'vitest-generate-api-global-setup.ts')], + alias: { + '@kortex-app/api': join(PACKAGE_ROOT, '..', '..', '__mocks__/@kortex-app/api.js'), + }, + }, +}; + +export default config; diff --git a/package.json b/package.json index 1d5350646f..b0632e54a4 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,12 @@ "scripts": { "build": "pnpm run build:main && pnpm run build:preload && pnpm run build:preload-webview && npm run build:preload:types && pnpm run build:renderer && pnpm run build:extensions", "build:main": "cd ./packages/main && vite build", - "build:extensions": "pnpm run build:extensions:container && pnpm run build:extensions:docling && pnpm run build:extensions:gemini && pnpm run build:extensions:goose && pnpm run build:extensions:mcp-registries && pnpm run build:extensions:milvus && pnpm run build:extensions:ollama && pnpm run build:extensions:openai-compatible && pnpm run build:extensions:openshift-ai && pnpm run build:extensions:ramalama", + "build:extensions": "pnpm run build:extensions:container && pnpm run build:extensions:docling && pnpm run build:extensions:gemini && pnpm run build:extensions:goose && pnpm run build:extensions:kortex-cli && pnpm run build:extensions:mcp-registries && pnpm run build:extensions:milvus && pnpm run build:extensions:ollama && pnpm run build:extensions:openai-compatible && pnpm run build:extensions:openshift-ai && pnpm run build:extensions:ramalama", "build:extensions:container": "cd ./extensions/container/packages/extension && pnpm run build", "build:extensions:docling": "cd ./extensions/docling && pnpm run build", "build:extensions:gemini": "cd ./extensions/gemini && pnpm run build", "build:extensions:goose": "cd ./extensions/goose && pnpm run build", + "build:extensions:kortex-cli": "cd ./extensions/kortex-cli && pnpm run build", "build:extensions:mcp-registries": "cd ./extensions/mcp-registries && pnpm run build", "build:extensions:milvus": "cd ./extensions/milvus && pnpm run build", "build:extensions:ollama": "cd ./extensions/ollama && pnpm run build", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9e2d38a8e..9241f19081 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -510,6 +510,31 @@ importers: specifier: ^4.0.10 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@24.12.2)(typescript@5.9.3))(terser@5.36.0)(tsx@4.20.3)(yaml@2.8.2) + extensions/kortex-cli: + dependencies: + '@octokit/rest': + specifier: ^21.1.0 + version: 21.1.1 + unzipper: + specifier: ^0.11.6 + version: 0.11.6 + devDependencies: + '@kortex-app/api': + specifier: workspace:* + version: link:../../packages/extension-api + '@types/unzipper': + specifier: ^0.10.11 + version: 0.10.11 + mkdirp: + specifier: ^3.0.1 + version: 3.0.1 + vite: + specifier: ^7.0.6 + version: 7.3.1(@types/node@24.12.2)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.36.0)(tsx@4.20.3)(yaml@2.8.2) + vitest: + specifier: ^4.0.10 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.31.1)(msw@2.12.10(@types/node@24.12.2)(typescript@5.9.3))(terser@5.36.0)(tsx@4.20.3)(yaml@2.8.2) + extensions/mcp-registries: devDependencies: '@kortex-app/api': @@ -911,12 +936,6 @@ packages: '@adobe/css-tools@4.4.3': resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} - '@ai-sdk/gateway@3.0.85': - resolution: {integrity: sha512-oPvs3bYnxndBY/O0gFSFuc5aA/QKCJbk/CaJaRnKgA/ZPH17jeVvEtiUBE6/N8hWhK7XgX53NFI7F3CGmDfm1g==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.87': resolution: {integrity: sha512-knLx/VY0u5KAZGgrTorWCTbEnwK3oCCdm8yjxVQm3s14erqVo60SP08dsFWm+xNULPTusftQGVD/l0/hx5QOHg==} engines: {node: '>=18'} @@ -953,12 +972,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@4.0.21': - resolution: {integrity: sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/provider-utils@4.0.22': resolution: {integrity: sha512-B2OTFcRw/Pdka9ZTjpXv6T6qZ6RruRuLokyb8HwW+aoW9ndJ3YasA3/mVswyJw7VMBF8ofXgqvcrCt9KYvFifg==} engines: {node: '>=18'} @@ -2856,12 +2869,6 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ai@6.0.143: - resolution: {integrity: sha512-wVDb7StQ1EPQ9GDAOmi1AsuAXQRSii+zZT2sFK+MCisH4vV7XNEAdzXL+sKsUAFhhq+EtVFWWlB4mCk4hcoIMw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - ai@6.0.145: resolution: {integrity: sha512-RbMiFsPZxE4uf5Hhs8rscp5bIwvjQOrqS5dQGWNVRHGM947QZgkKX7Ih5hto8MK/7xkbtneoOZruZ8oSLO828A==} engines: {node: '>=18'} @@ -7818,13 +7825,6 @@ snapshots: '@adobe/css-tools@4.4.3': {} - '@ai-sdk/gateway@3.0.85(zod@4.3.6)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.21(zod@4.3.6) - '@vercel/oidc': 3.1.0 - zod: 4.3.6 - '@ai-sdk/gateway@3.0.87(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -7865,13 +7865,6 @@ snapshots: eventsource-parser: 3.0.6 zod: 4.3.6 - '@ai-sdk/provider-utils@4.0.21(zod@4.3.6)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.6 - zod: 4.3.6 - '@ai-sdk/provider-utils@4.0.22(zod@4.3.6)': dependencies: '@ai-sdk/provider': 3.0.8 @@ -8175,8 +8168,8 @@ snapshots: make-fetch-happen: 10.2.1 nopt: 6.0.0 proc-log: 2.0.1 - semver: 7.7.3 - tar: 7.5.10 + semver: 7.7.4 + tar: 7.5.13 which: 2.0.2 transitivePeerDependencies: - bluebird @@ -8806,7 +8799,7 @@ snapshots: '@npmcli/fs@2.1.2': dependencies: '@gar/promisify': 1.1.3 - semver: 7.7.3 + semver: 7.7.4 '@npmcli/fs@4.0.0': dependencies: @@ -9980,14 +9973,6 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ai@6.0.143(zod@4.3.6): - dependencies: - '@ai-sdk/gateway': 3.0.85(zod@4.3.6) - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.21(zod@4.3.6) - '@opentelemetry/api': 1.9.0 - zod: 4.3.6 - ai@6.0.145(zod@4.3.6): dependencies: '@ai-sdk/gateway': 3.0.87(zod@4.3.6) @@ -10597,7 +10582,7 @@ snapshots: promise-inflight: 1.0.1 rimraf: 3.0.2 ssri: 9.0.1 - tar: 7.5.10 + tar: 7.5.13 unique-filename: 2.0.1 transitivePeerDependencies: - bluebird