Huakun Shen 402208e95a
Feature: license check (#67)
* feat: add required license field to manifest

* chore: add license MIT to all templates

* feat: add license check to jsr and npm validation

* fix: supabase export

split every item into its own subexport for better debugability

* fix: supabase imports

* feat: hide app when escape is pressed

* fix: update package version in api test from 0.0.6 to 0.0.20

* fix: update test for kunkun extension with new version and commit details

* fix: update kunkun extension test to use version 0.0.4

* fix: update kunkun extension test to reflect new version 0.0.20 and updated commit details

* feat: display downloads in extension details

* feat: add downloads display to store
2025-01-18 22:55:43 -05:00

196 lines
6.1 KiB
TypeScript

/**
* This file contains helper functions for operations related to installing extensions
* including install, uninstall, upgrade, check app-extension compatibility, etc.
*/
import { isCompatible } from "@kksh/api"
import { db, decompressTarball } from "@kksh/api/commands"
import type { ExtPackageJsonExtra } from "@kksh/api/models"
import { SBExt } from "@kksh/supabase/models"
import { greaterThan, parse as parseSemver } from "@std/semver"
import * as path from "@tauri-apps/api/path"
import * as dialog from "@tauri-apps/plugin-dialog"
import * as fs from "@tauri-apps/plugin-fs"
import { download } from "@tauri-apps/plugin-upload"
import { v4 as uuidv4 } from "uuid"
import { z, ZodError } from "zod"
import { loadExtensionManifestFromDisk } from "./load"
import { isExtPathInDev } from "./utils"
/**
*
* @param tarballPath path to .tar.gz file
*/
export async function installTarball(
tarballPath: string,
extsDir: string,
extras?: {
overwritePackageJson?: string
}
): Promise<string> {
const tempDirPath = await path.tempDir()
if (!extsDir) {
return Promise.reject("Extension Folder Not Set")
}
// decompress tarball to tempDir
const decompressDest = await decompressTarball(
tarballPath,
await path.join(tempDirPath, uuidv4()),
{
overwrite: true
}
)
const pkgJsonPath = await path.join(decompressDest, "package.json")
if (extras?.overwritePackageJson) {
await fs.writeTextFile(pkgJsonPath, extras.overwritePackageJson)
}
return loadExtensionManifestFromDisk(pkgJsonPath)
.then(async (manifest) => {
// The extension folder name will be the identifier
const extInstallPath = await path.join(extsDir, manifest.kunkun.identifier)
if (await fs.exists(extInstallPath)) {
const overwrite = await dialog.ask(
`Extension ${manifest.kunkun.identifier} already exists, do you want to overwrite it?`
)
if (!overwrite) {
return Promise.reject("Extension Already Exists")
}
await fs.remove(extInstallPath, { recursive: true })
}
// find extension in db, if exists, ask user if they want to overwrite it
const exts = await db.getAllExtensionsByIdentifier(manifest.kunkun.identifier)
const storeExts = exts.filter((ext) => ext.path && !isExtPathInDev(extsDir, ext.path))
if (storeExts.length > 0) {
const overwrite = await dialog.ask(
`Extension ${manifest.kunkun.identifier} already exists in database (but not on disk), do you want to overwrite it?`
)
if (!overwrite) {
return Promise.reject("Extension Already Exists")
}
if (exts[0].path) {
console.log(
"delete extension in db (overwrite in order to install a new version)",
exts[0].path
)
await db.deleteExtensionByPath(exts[0].path)
}
}
await fs.rename(decompressDest, extInstallPath)
await db.createExtension({
identifier: manifest.kunkun.identifier,
version: manifest.version,
enabled: true,
path: extInstallPath,
data: undefined
})
console.log("installTarball in DB success", manifest)
return extInstallPath
})
.catch((err) => {
if (err instanceof ZodError) {
console.error(err)
throw new Error("Invalid Manifest or Extension")
}
console.error("installTarball error", err)
throw new Error(err)
})
}
/**
* Install extension tarball from a URL
* @param tarballUrl URL to the tarball
* @param extsDir Target directory to install the tarball
* @returns
*/
export async function installTarballUrl(
tarballUrl: string,
extsDir: string,
extras?: { overwritePackageJson?: string }
): Promise<string> {
const filename = await path.basename(tarballUrl)
if (filename) {
const tempDirPath = await path.tempDir()
let tarballPath = await path.join(tempDirPath, filename)
console.log("tarballPath", tarballPath)
await download(tarballUrl, tarballPath)
const extInstallPath = await installTarball(tarballPath, extsDir, extras)
await fs.remove(tarballPath)
return extInstallPath
} else {
return Promise.reject("Invalid Tarball URL. Cannot parse filename")
}
}
/**
* Install dev extension from a local directory
* @param extPath Path to the extension directory
* @returns
*/
export async function installDevExtensionDir(extPath: string): Promise<ExtPackageJsonExtra> {
const manifestPath = await path.join(extPath, "package.json")
if (!(await fs.exists(manifestPath))) {
return Promise.reject(
`Invalid Extension Folder. Manifest package.json doesn't exist at ${manifestPath}`
)
}
return loadExtensionManifestFromDisk(manifestPath)
.then(async (manifest) => {
const exts = await db.getAllExtensionsByIdentifier(manifest.kunkun.identifier)
const extExists = exts.find((ext) => ext.path === extPath)
if (extExists) {
return Promise.reject(`Extension Already Exists at ${extExists.path}. It will be skipped.`)
}
// manifest.extPath
return db
.createExtension({
identifier: manifest.kunkun.identifier,
version: manifest.version,
enabled: true,
path: extPath,
data: undefined
})
.then(() => {
return manifest
})
.catch((err) => {
return Promise.reject(err)
})
})
.catch((err) => {
return Promise.reject(err)
})
}
export async function installThroughNpmAPI(url: string, extsDir: string) {
return fetch(url)
.then((res) => res.json())
.then((json) => {
const tarball = z.string().safeParse(json?.dist?.tarball)
if (tarball.success) {
return installTarballUrl(tarball.data, extsDir)
} else {
return Promise.reject("Tarball Not Found")
}
})
}
export async function installFromNpmPackageName(name: string, extsDir: string) {
return installThroughNpmAPI(`https://registry.npmjs.org/${name}/latest`, extsDir)
}
export async function uninstallExtensionByPath(extPath: string) {
if (!(await fs.exists(extPath))) throw new Error(`Extension ${extPath} not found`)
return fs.remove(extPath, { recursive: true }).then(() => db.deleteExtensionByPath(extPath))
}
export function isUpgradable(dbExt: SBExt, installedExtVersion: string) {
const upgradable =
greaterThan(parseSemver(dbExt.version), parseSemver(installedExtVersion)) && dbExt.api_version
? isCompatible(dbExt.api_version)
: false
return upgradable
}