Feature: install extension from JSR (#53)

* chore: upgrade many dependencies

* fix: @kksh/svelte, migrate 5

* refactor: move dialog plugin code out of ui package

* chore: disable check-types in api build.ts

Causing build error in kunkun-services

* feat: add jsr package

* feat: implement jsr package with API and parsers for jsr

* feat: modify API, add function to extract linked github repo from html

* perf: improve jsr package API with @hk/jsr-client

* feat: add jsr package version table for publishing extension

* fix: dependency and type incompatibility in ui package

* feat: add validateJsrPackageAsKunkunExtension function in jsr package

* feat: improve jsr table

* feat: add a ElementAlert component

* feat: update ElementAlert UI

* chore: update deno.lock

* chore: enable submodule support in jsr-publish workflow

* chore: bump version to 0.0.48 in jsr.json

* feat: regenerate supabase types, add author_id

* Move @kksh/jsr package to @kksh/api

* update deno.lock

* chore: change @tauri-plugin/plugin-upload version from git url to version

* feat: add rounded corner for ElementAlert

* chore: update deno.lock

* chore: bump version to 0.0.51 in jsr.json and update import paths for ExtPackageJson

* feat: add publishExtJSR API to SupabaseAPI

* refactor: replace "@hk/jsr-client" from jsr with @huakunshen/jsr-client from npm

* chore: update  deno.lock

* feat: update validateJsrPackageAsKunkunExtension return type

* refactor: improve error message

* feat: add metadata to ext_publish, update database.types.ts

* feat: add models module for Supabase with ExtPublishMetadata and source type enumeration

* feat: support installing JSR package as extension

Since JSR overwrites package.json with its own code to be compatible with npm, causing manifest parsing to be impossible. I add metadata field to ext_publish.
When extension comes from jsr, kunkun app will fetch the original package.json from jsr and overwrite the one modified by jsr.

* fix: add missing dep @tauri-apps/plugin-upload to @kksh/extension

* chore: update version to 0.0.52 in version.ts
This commit is contained in:
Huakun Shen 2025-01-10 08:23:18 -05:00 committed by GitHub
parent e096e10bc0
commit e21bef154e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 16803 additions and 2599 deletions

View File

@ -12,6 +12,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
submodules: "true"
- uses: denoland/setup-deno@v2 - uses: denoland/setup-deno@v2
with: with:
deno-version: v2.x deno-version: v2.x

View File

@ -51,7 +51,7 @@
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bits-ui": "1.0.0-next.72", "bits-ui": "1.0.0-next.72",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-svelte": "^0.468.0", "lucide-svelte": "^0.469.0",
"svelte-radix": "^2.0.1", "svelte-radix": "^2.0.1",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwind-variants": "^0.3.0", "tailwind-variants": "^0.3.0",

View File

@ -12,7 +12,11 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
getExtensionsFromStore: () => ExtPackageJsonExtra[] getExtensionsFromStore: () => ExtPackageJsonExtra[]
installTarball: (tarballPath: string, extsDir: string) => Promise<ExtPackageJsonExtra> installTarball: (tarballPath: string, extsDir: string) => Promise<ExtPackageJsonExtra>
installDevExtensionDir: (dirPath: string) => Promise<ExtPackageJsonExtra> installDevExtensionDir: (dirPath: string) => Promise<ExtPackageJsonExtra>
installFromTarballUrl: (tarballUrl: string, installDir: string) => Promise<ExtPackageJsonExtra> installFromTarballUrl: (
tarballUrl: string,
installDir: string,
extras?: { overwritePackageJson?: string }
) => Promise<ExtPackageJsonExtra>
installFromNpmPackageName: (name: string, installDir: string) => Promise<ExtPackageJsonExtra> installFromNpmPackageName: (name: string, installDir: string) => Promise<ExtPackageJsonExtra>
findStoreExtensionByIdentifier: (identifier: string) => ExtPackageJsonExtra | undefined findStoreExtensionByIdentifier: (identifier: string) => ExtPackageJsonExtra | undefined
registerNewExtensionByPath: (extPath: string) => Promise<ExtPackageJsonExtra> registerNewExtensionByPath: (extPath: string) => Promise<ExtPackageJsonExtra>
@ -86,8 +90,12 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
}) })
} }
async function installFromTarballUrl(tarballUrl: string, extsDir: string) { async function installFromTarballUrl(
return extAPI.installTarballUrl(tarballUrl, extsDir).then((extInstallPath) => { tarballUrl: string,
extsDir: string,
extras?: { overwritePackageJson?: string }
) {
return extAPI.installTarballUrl(tarballUrl, extsDir, extras).then((extInstallPath) => {
return registerNewExtensionByPath(extInstallPath) return registerNewExtensionByPath(extInstallPath)
}) })
} }

View File

@ -3,6 +3,8 @@
import { extensions, installedStoreExts } from "@/stores/extensions.js" import { extensions, installedStoreExts } from "@/stores/extensions.js"
import { supabaseAPI } from "@/supabase" import { supabaseAPI } from "@/supabase"
import { goBack } from "@/utils/route.js" import { goBack } from "@/utils/route.js"
import type { Tables } from "@kksh/api/supabase/types"
import { ExtPublishMetadata } from "@kksh/supabase/models"
import { Button } from "@kksh/svelte5" import { Button } from "@kksh/svelte5"
import { cn } from "@kksh/svelte5/utils" import { cn } from "@kksh/svelte5/utils"
import { Constants } from "@kksh/ui" import { Constants } from "@kksh/ui"
@ -13,10 +15,11 @@
import { ArrowLeftIcon } from "lucide-svelte" import { ArrowLeftIcon } from "lucide-svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import { toast } from "svelte-sonner" import { toast } from "svelte-sonner"
import { get, derived as storeDerived } from "svelte/store" import { derived as storeDerived } from "svelte/store"
import { getInstallExtras } from "./helper.js"
const { data } = $props() const { data } = $props()
const ext = $derived(data.ext) const ext: Tables<"ext_publish"> & { metadata: ExtPublishMetadata } = $derived(data.ext)
const manifest = $derived(data.manifest) const manifest = $derived(data.manifest)
const installedExt = storeDerived(installedStoreExts, ($e) => { const installedExt = storeDerived(installedStoreExts, ($e) => {
return $e.find((e) => e.kunkun.identifier === ext.identifier) return $e.find((e) => e.kunkun.identifier === ext.identifier)
@ -69,24 +72,27 @@
async function onInstallSelected() { async function onInstallSelected() {
loading.install = true loading.install = true
const tarballUrl = supabaseAPI.translateExtensionFilePathToUrl(ext.tarball_path) const tarballUrl = ext.tarball_path.startsWith("http")
? ext.tarball_path
: supabaseAPI.translateExtensionFilePathToUrl(ext.tarball_path)
const installExtras = await getInstallExtras(ext)
const installDir = await getExtensionsFolder() const installDir = await getExtensionsFolder()
return extensions return extensions
.installFromTarballUrl(tarballUrl, installDir) .installFromTarballUrl(tarballUrl, installDir, installExtras)
.then(() => toast.success(`Plugin ${ext.name} Installed`)) .then(() => toast.success(`Plugin ${ext.name} Installed`))
.then(async (loadedExt) => .then((loadedExt) => {
supabaseAPI.incrementDownloads({ supabaseAPI.incrementDownloads({
identifier: ext.identifier, identifier: ext.identifier,
version: ext.version version: ext.version
}) })
) showBtn.install = false
showBtn.uninstall = true
})
.catch((err) => { .catch((err) => {
toast.error("Fail to install tarball", { description: err }) toast.error("Fail to install tarball", { description: err })
}) })
.finally(() => { .finally(() => {
loading.install = false loading.install = false
showBtn.install = false
showBtn.uninstall = true
}) })
} }
@ -146,7 +152,7 @@
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
class={cn("fixed left-3 top-3", Constants.CLASSNAMES.BACK_BUTTON)} class={cn("fixed left-3 top-3 z-50", Constants.CLASSNAMES.BACK_BUTTON)}
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON} data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
onclick={() => goto("/app/extension/store")} onclick={() => goto("/app/extension/store")}
> >

View File

@ -2,30 +2,29 @@ import { extensions } from "@/stores"
import { supabaseAPI } from "@/supabase" import { supabaseAPI } from "@/supabase"
import { KunkunExtManifest, type ExtPackageJsonExtra } from "@kksh/api/models" import { KunkunExtManifest, type ExtPackageJsonExtra } from "@kksh/api/models"
import type { Tables } from "@kksh/api/supabase/types" import type { Tables } from "@kksh/api/supabase/types"
import { ExtPublishMetadata } from "@kksh/supabase/models"
import { error } from "@sveltejs/kit" import { error } from "@sveltejs/kit"
import { toast } from "svelte-sonner" import { toast } from "svelte-sonner"
import { get } from "svelte/store"
import * as v from "valibot" import * as v from "valibot"
import type { PageLoad } from "./$types" import type { PageLoad } from "./$types"
export const load: PageLoad = async ({ export const load: PageLoad = async ({
params params
}): Promise<{ }): Promise<{
ext: Tables<"ext_publish"> ext: Tables<"ext_publish"> & { metadata: ExtPublishMetadata }
manifest: KunkunExtManifest manifest: KunkunExtManifest
params: { params: {
identifier: string identifier: string
} }
}> => { }> => {
console.log("store[identifier] params", params)
const { error: dbError, data: ext } = await supabaseAPI.getLatestExtPublish(params.identifier) const { error: dbError, data: ext } = await supabaseAPI.getLatestExtPublish(params.identifier)
const metadataParse = v.safeParse(ExtPublishMetadata, ext?.metadata ?? {})
if (dbError) { if (dbError) {
return error(400, { return error(400, {
message: dbError.message message: dbError.message
}) })
} }
const metadata = metadataParse.success ? metadataParse.output : {}
const parseManifest = v.safeParse(KunkunExtManifest, ext.manifest) const parseManifest = v.safeParse(KunkunExtManifest, ext.manifest)
if (!parseManifest.success) { if (!parseManifest.success) {
const errMsg = "Invalid extension manifest, you may need to upgrade your app." const errMsg = "Invalid extension manifest, you may need to upgrade your app."
@ -34,7 +33,7 @@ export const load: PageLoad = async ({
} }
return { return {
ext, ext: { ...ext, metadata },
params, params,
manifest: parseManifest.output manifest: parseManifest.output
} }

View File

@ -0,0 +1,22 @@
import type { Tables } from "@kksh/api/supabase/types"
import type { ExtPublishMetadata } from "@kksh/supabase/models"
export async function getInstallExtras(
ext: Tables<"ext_publish"> & { metadata: ExtPublishMetadata }
): Promise<{ overwritePackageJson?: string }> {
const extras: { overwritePackageJson?: string } = {}
if (ext.metadata.sourceType) {
if (ext.metadata.sourceType === "jsr") {
if (ext.metadata.source) {
try {
const res = await fetch(`${ext.metadata.source}/package.json`)
const pkgJsonContent = await res.text()
extras.overwritePackageJson = pkgJsonContent
} catch (error) {
console.error("Fail to fetch jsr package.json", error)
}
}
}
}
return extras
}

14191
deno.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,23 +13,23 @@
"devDependencies": { "devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.0", "@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@kksh/api": "workspace:*", "@kksh/api": "workspace:*",
"@kksh/svelte5": "0.1.10", "@kksh/svelte5": "0.1.11",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2", "prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.9", "prettier-plugin-tailwindcss": "^0.6.9",
"svelte": "^5.16.2", "svelte": "^5.16.6",
"svelte-check": "^4.1.1", "svelte-check": "^4.1.1",
"turbo": "^2.3.3", "turbo": "^2.3.3",
"typescript": "5.7.2" "typescript": "5.7.2"
}, },
"packageManager": "pnpm@9.15.2", "packageManager": "pnpm@9.15.3",
"engines": { "engines": {
"node": ">=22" "node": ">=22"
}, },
"dependencies": { "dependencies": {
"@changesets/cli": "^2.27.11", "@changesets/cli": "^2.27.11",
"@iconify/svelte": "^4.2.0", "@iconify/svelte": "^4.2.0",
"@supabase/supabase-js": "^2.47.10", "@supabase/supabase-js": "^2.47.11",
"@tauri-apps/api": "^2.2.0", "@tauri-apps/api": "^2.2.0",
"@tauri-apps/cli": "^2.2.2", "@tauri-apps/cli": "^2.2.2",
"@tauri-apps/plugin-deep-link": "^2.2.0", "@tauri-apps/plugin-deep-link": "^2.2.0",
@ -43,14 +43,13 @@
"@tauri-apps/plugin-process": "2.2.0", "@tauri-apps/plugin-process": "2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0", "@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-store": "^2.2.0", "@tauri-apps/plugin-store": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.3.0", "@tauri-apps/plugin-updater": "^2.3.1",
"@tauri-apps/plugin-upload": "https://gitpkg.vercel.app/HuakunShen/tauri-plugins-workspace/plugins/upload?69b198b0ccba269fe7622a95ec6a33ae392bff03",
"supabase": "^2.2.1", "supabase": "^2.2.1",
"tauri-plugin-network-api": "workspace:*", "tauri-plugin-network-api": "workspace:*",
"tauri-plugin-keyring-api": "workspace:*", "tauri-plugin-keyring-api": "workspace:*",
"tauri-plugin-shellx-api": "^2.0.14", "tauri-plugin-shellx-api": "^2.0.14",
"tauri-plugin-system-info-api": "workspace:*", "tauri-plugin-system-info-api": "workspace:*",
"valibot": "^1.0.0-beta.10", "valibot": "^1.0.0-beta.11",
"zod": "^3.24.1" "zod": "^3.24.1"
}, },
"workspaces": [ "workspaces": [

View File

@ -18,4 +18,3 @@ if (!schemaFile.exists()) {
} }
await $`bun patch-version.ts` await $`bun patch-version.ts`
await $`bun run check-types`

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://jsr.io/schema/config-file.v1.json", "$schema": "https://jsr.io/schema/config-file.v1.json",
"name": "@kunkun/api", "name": "@kunkun/api",
"version": "0.0.47", "version": "0.0.52",
"license": "MIT", "license": "MIT",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
@ -15,7 +15,8 @@
"./supabase": "./src/supabase/index.ts", "./supabase": "./src/supabase/index.ts",
"./supabase/types": "./src/supabase/database.types.ts", "./supabase/types": "./src/supabase/database.types.ts",
"./dev": "./src/dev/index.ts", "./dev": "./src/dev/index.ts",
"./events": "./src/events.ts" "./events": "./src/events.ts",
"./extensions/jsr": "./src/extensions/jsr/index.ts"
}, },
"imports": {} "imports": {}
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@kksh/api", "name": "@kksh/api",
"version": "0.0.48", "version": "0.0.52",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
@ -16,7 +16,8 @@
"./events": "./src/events.ts", "./events": "./src/events.ts",
"./supabase": "./src/supabase/index.ts", "./supabase": "./src/supabase/index.ts",
"./supabase/types": "./src/supabase/database.types.ts", "./supabase/types": "./src/supabase/database.types.ts",
"./package.json": "./package.json" "./package.json": "./package.json",
"./extensions/jsr": "./src/extensions/jsr/index.ts"
}, },
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
@ -41,6 +42,7 @@
"typescript": "^5.0.0" "typescript": "^5.0.0"
}, },
"dependencies": { "dependencies": {
"@huakunshen/jsr-client": "^0.1.5",
"@tauri-apps/api": "^2.2.0", "@tauri-apps/api": "^2.2.0",
"@tauri-apps/cli": "^2.2.2", "@tauri-apps/cli": "^2.2.2",
"@tauri-apps/plugin-deep-link": "^2.2.0", "@tauri-apps/plugin-deep-link": "^2.2.0",
@ -55,7 +57,7 @@
"@tauri-apps/plugin-shell": "^2.2.0", "@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-store": "^2.2.0", "@tauri-apps/plugin-store": "^2.2.0",
"@tauri-apps/plugin-updater": "^2.3.0", "@tauri-apps/plugin-updater": "^2.3.0",
"@tauri-apps/plugin-upload": "https://gitpkg.vercel.app/HuakunShen/tauri-plugins-workspace/plugins/upload?69b198b0ccba269fe7622a95ec6a33ae392bff03", "@tauri-apps/plugin-upload": "^2.2.1",
"kkrpc": "^0.0.13", "kkrpc": "^0.0.13",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",

View File

@ -0,0 +1,133 @@
import { describe, expect, test } from "bun:test";
import * as v from "valibot";
import { ExtPackageJson } from "../../../models/manifest";
import {
getAllVersionsOfJsrPackage,
getJsrNpmPackageVersionMetadata,
getJsrNpmPkgMetadata,
getJsrPackageGitHubRepo,
getJsrPackageHtml,
getJsrPackageMetadata,
getJsrPackageSrcFile,
getNpmPackageTarballUrl,
isSignedByGitHubAction,
jsrPackageExists,
splitRawJsrPkgName,
translateJsrToNpmPkgName,
} from "../index";
import { JsrPackageMetadata, NpmPkgMetadata } from "../models";
describe("Test the helper functions", () => {
test("Get Package Html", async () => {
const html = await getJsrPackageHtml("kunkun", "kkrpc");
expect(html).toBeDefined();
});
test("Signed By GitHub Action", async () => {
const kkrpcSigned = await isSignedByGitHubAction(
"kunkun",
"kkrpc",
"0.0.14",
);
expect(kkrpcSigned).toBe(true);
const kkrpcSignedVersion = await isSignedByGitHubAction(
"kunkun",
"kkrpc",
"0.0.14",
);
expect(kkrpcSignedVersion).toBe(true);
expect(kkrpcSignedVersion).toBe(true);
const kunkunApiSigned = await isSignedByGitHubAction(
"kunkun",
"api",
"0.0.47",
);
expect(kunkunApiSigned).toBe(false);
});
test("Get Linked GitHub Repo", async () => {
const repo = await getJsrPackageGitHubRepo("kunkun", "kkrpc");
expect(repo).toBeDefined();
expect(repo?.owner).toBe("kunkunsh");
expect(repo?.name).toBe("kkrpc");
});
test("Get Package Metadata", async () => {
const metadata = await getJsrPackageMetadata("kunkun", "api");
const parsed = v.parse(JsrPackageMetadata, metadata);
expect(parsed).toBeDefined();
});
test("Get Package's package.json", async () => {
const packageJson = await getJsrPackageSrcFile(
"kunkun",
"ext-image-processing",
"0.0.6",
"package.json",
);
expect(packageJson).toBeDefined();
const parsed = v.parse(ExtPackageJson, JSON.parse(packageJson!));
expect(parsed).toBeDefined();
});
test("Get Package's README.md", async () => {
const readme = await getJsrPackageSrcFile(
"kunkun",
"api",
"0.0.47",
"README.md",
);
expect(readme).toBeDefined();
});
test("Translate Jsr Package Name to Npm Package Name", () => {
const npmPkgName = translateJsrToNpmPkgName("kunkun", "api");
expect(npmPkgName).toBe("kunkun__api");
});
test("Split Jsr Package Name", async () => {
const { scope, name } = await splitRawJsrPkgName("@kunkun/api");
expect(scope).toBe("kunkun");
expect(name).toBe("api");
expect(splitRawJsrPkgName("kunkun/api")).rejects.toThrow();
});
test("Get Npm Package Metadata", async () => {
const metadata = await getJsrNpmPkgMetadata("kunkun", "api");
const parsed = v.parse(NpmPkgMetadata, metadata);
expect(parsed).toBeDefined();
});
test("Get Npm Package Version Metadata", async () => {
const metadata = await getJsrNpmPackageVersionMetadata(
"kunkun",
"api",
"0.0.47",
);
expect(metadata).toBeDefined();
});
test("Get Npm Package Tarball Url", async () => {
const url = await getNpmPackageTarballUrl("kunkun", "api", "0.0.47");
expect(url).toBeDefined();
});
test("Get All Versions Of Jsr Package", async () => {
const versions = await getAllVersionsOfJsrPackage("kunkun", "api");
expect(versions).toBeDefined();
// verify: versions should match npm api
const npmPkgMetadata = await getJsrNpmPkgMetadata("kunkun", "api");
expect(versions).toEqual(Object.keys(npmPkgMetadata.versions));
});
test("Jsr Package Exists", async () => {
expect(await jsrPackageExists("kunkun", "api")).toBe(true);
expect(await jsrPackageExists("hk", "non-existent-package")).toBe(
false,
);
expect(await jsrPackageExists("hk", "jsr-client", "0.1.2")).toBe(true);
expect(await jsrPackageExists("hk", "jsr-client", "0.1.500")).toBe(
false,
);
});
});

View File

@ -0,0 +1,71 @@
import { describe, expect, test } from "bun:test";
import { validateJsrPackageAsKunkunExtension } from "../index";
describe("Validate Jsr package as Kunkun extension", () => {
test("Package not signed by GitHub Actions", async () => {
expect(
(await validateJsrPackageAsKunkunExtension({
jsrPackage: {
scope: "kunkun",
name: "api",
version: "0.0.47",
},
githubUsername: "kunkunsh",
})).error,
).toBe("JSR package is not signed by GitHub Actions");
});
test("Non-existent package", async () => {
expect(
(await validateJsrPackageAsKunkunExtension({
jsrPackage: {
scope: "kunkun",
name: "non-existent-package",
version: "0.0.47",
},
githubUsername: "kunkunsh",
})).error,
).toBe("JSR package does not exist");
});
test("Package not linked to a GitHub repository", async () => {
expect(
(await validateJsrPackageAsKunkunExtension({
jsrPackage: {
scope: "hk",
name: "tauri-plugin-network-api",
version: "2.0.3-beta.1",
},
githubUsername: "kunkunsh",
})).error,
).toBe("JSR package is not linked to a GitHub repository");
});
test("GitHub repository owner does not match JSR package owner", async () => {
expect(
(await validateJsrPackageAsKunkunExtension({
jsrPackage: {
scope: "kunkun",
name: "ext-image-processing",
version: "0.0.6",
},
githubUsername: "kunkunsh", // should be HuakunShen
})).error,
).toBe(
"GitHub repository owner does not match JSR package owner: HuakunShen !== kunkunsh",
);
});
test("A valid extension package", async () => {
expect(
(await validateJsrPackageAsKunkunExtension({
jsrPackage: {
scope: "kunkun",
name: "ext-image-processing",
version: "0.0.6",
},
githubUsername: "HuakunShen",
})).data,
).toBeDefined();
});
});

View File

@ -0,0 +1,386 @@
import {
client,
getPackage,
getPackageVersion,
type GitHubRepository,
} from "@huakunshen/jsr-client/hey-api-client";
import * as v from "valibot";
import { ExtPackageJson } from "../../models/manifest";
import type { JsrPackageMetadata, NpmPkgMetadata } from "./models";
client.setConfig({
baseUrl: "https://api.jsr.io",
});
export function splitRawJsrPkgName(
packageName: string,
): Promise<{ scope: string; name: string }> {
return new Promise((resolve, reject) => {
// write a regex to match the scope and name
const regex = /^@([^@]+)\/([^@]+)$/;
const match = packageName.match(regex);
if (!match) {
return reject(new Error("Invalid Jsr package name"));
}
const [, rawScope, name] = match;
const scope = rawScope.startsWith("@") ? rawScope.slice(1) : rawScope;
return resolve({ scope, name });
});
}
/**
* Translate a Jsr package name to an npm package name
* All packages are under `@jsr` scope, thus the npm package name is `@jsr/scope__name`
* @param scope
* @param name
* @returns
*/
export const translateJsrToNpmPkgName = (scope: string, name: string) =>
`${scope}__${name}`;
/**
/**
* Get the html of a Jsr package main page
* @param scope
* @param name
* @param version
* @returns
*/
export function getJsrPackageHtml(
scope: string,
name: string,
version?: string,
) {
const url = `https://jsr.io/@${scope}/${name}${
version ? `@${version}` : ""
}`;
return fetch(url, {
headers: {
"sec-fetch-dest": "document",
},
}).then((res) => res.text());
}
/**
* Check if a Jsr package is signed by GitHub Actions
* @returns
*/
export async function isSignedByGitHubAction(
scope: string,
name: string,
version: string,
): Promise<boolean> {
const pkgVersion = await getPackageVersion({
path: {
scope,
package: name,
version,
},
});
return !!pkgVersion.data?.rekorLogId;
}
export async function getJsrPackageGitHubRepo(
scope: string,
name: string,
): Promise<GitHubRepository | null> {
const pkg = await getPackage({
path: {
scope,
package: name,
},
});
return pkg.data?.githubRepository ?? null;
}
/**
* Get the metadata of a Jsr package
* Data includes
* - latest version
* - versions (whether a version is yanked)
* @param scope
* @param name
* @returns
*/
export function getJsrPackageMetadata(
scope: string,
name: string,
): Promise<JsrPackageMetadata> {
const url = `https://jsr.io/@${scope}/${name}/meta.json`;
return fetch(url).then((res) => res.json());
}
/**
* Given a jsr package and path to the file, return the file content
* @param scope
* @param name
* @param version
* @param file
* @returns
*/
export function getJsrPackageSrcFile(
scope: string,
name: string,
version: string,
file: string,
): Promise<string | undefined> {
const url = `https://jsr.io/@${scope}/${name}/${version}/${file}`;
return fetch(url)
.then((res) => res.text())
.catch(() => undefined);
}
/**
* Jsr provides a npm compatible registry, so we can get the metadata of the npm package
* @param scope
* @param name
* @returns
*/
export function getJsrNpmPkgMetadata(
scope: string,
name: string,
): Promise<NpmPkgMetadata> {
// Sample: https://npm.jsr.io/@jsr/kunkun__api
const url = `https://npm.jsr.io/@jsr/${
translateJsrToNpmPkgName(scope, name)
}`;
return fetch(url).then((res) => res.json());
}
/**
* Get the metadata of a specific version of a Jsr package
* @param scope
* @param name
* @param version
* @returns
*/
export function getJsrNpmPackageVersionMetadata(
scope: string,
name: string,
version: string,
) {
return getJsrNpmPkgMetadata(scope, name).then((metadata) => {
return metadata.versions[version];
});
}
/**
* Get the tarball url of a Jsr package
* @param scope
* @param name
* @param version
* @returns
*/
export async function getNpmPackageTarballUrl(
scope: string,
name: string,
version: string,
): Promise<string | undefined> {
const metadata = await getJsrNpmPackageVersionMetadata(
scope,
name,
version,
);
const tarballUrl: string | undefined = metadata?.dist
.tarball;
return tarballUrl;
}
/**
* Get all versions of a Jsr package
* @param scope
* @param name
* @returns
*/
export async function getAllVersionsOfJsrPackage(
scope: string,
name: string,
): Promise<string[]> {
const metadata = await getJsrNpmPkgMetadata(scope, name);
return Object.keys(metadata.versions);
}
/**
* Check if a Jsr package exists
* @param scope
* @param name
* @returns
*/
export function jsrPackageExists(
scope: string,
name: string,
version?: string,
): Promise<boolean> {
if (version) {
return getPackageVersion({
path: {
scope,
package: name,
version,
},
}).then((res) => res.response.ok && res.response.status === 200);
}
return getPackage({
path: {
scope,
package: name,
},
}).then((res) => res.response.ok && res.response.status === 200);
}
/**
* Get the tarball size of a Jsr package
* @param url tarball url, can technically be any url
* @returns tarball size in bytes
*/
export function getTarballSize(url: string): Promise<number> {
return fetch(url, { method: "HEAD" }).then((res) => {
if (!(res.ok && res.status === 200)) {
throw new Error("Failed to fetch tarball size");
}
return Number(res.headers.get("Content-Length"));
});
}
/**
* Validate a Jsr package as a Kunkun extension
* - check if jsr pkg is linked to a github repo
* - check if jsr pkg is signed with github action
* - check if user's github username is the same as repo's owner name
* - check if jsr.json or deno.json has the same version as package.json
* - validate package.json format against latest schema
* @param payload
* @returns
*/
export async function validateJsrPackageAsKunkunExtension(payload: {
jsrPackage: {
scope: string;
name: string;
version: string;
};
githubUsername: string;
tarballSizeLimit?: number;
}): Promise<{
error?: string;
data?: {
pkgJson: ExtPackageJson;
tarballUrl: string;
shasum: string;
apiVersion: string;
tarballSize: number;
};
}> {
// check if jsr package exists
const jsrExists = await jsrPackageExists(
payload.jsrPackage.scope,
payload.jsrPackage.name,
payload.jsrPackage.version,
);
if (!jsrExists) {
return { error: "JSR package does not exist" };
}
/* -------------------------------------------------------------------------- */
/* check if jsr pkg is linked to a github repo */
/* -------------------------------------------------------------------------- */
const githubRepo = await getJsrPackageGitHubRepo(
payload.jsrPackage.scope,
payload.jsrPackage.name,
);
if (githubRepo === null) {
return { error: "JSR package is not linked to a GitHub repository" };
}
/* -------------------------------------------------------------------------- */
/* check if jsr pkg is signed with github action */
/* -------------------------------------------------------------------------- */
const signed = await isSignedByGitHubAction(
payload.jsrPackage.scope,
payload.jsrPackage.name,
payload.jsrPackage.version,
);
if (!signed) {
return { error: "JSR package is not signed by GitHub Actions" };
}
/* -------------------------------------------------------------------------- */
/* check if user's github username is the same as repo's owner name */
/* -------------------------------------------------------------------------- */
if (
githubRepo.owner?.toLowerCase() !== payload.githubUsername.toLowerCase()
) {
return {
error:
`GitHub repository owner does not match JSR package owner: ${githubRepo.owner} !== ${payload.githubUsername}`,
};
}
/* -------------------------------------------------------------------------- */
/* check if jsr.json or deno.json has the same version as package.json */
/* -------------------------------------------------------------------------- */
const packageJsonContent = await getJsrPackageSrcFile(
payload.jsrPackage.scope,
payload.jsrPackage.name,
payload.jsrPackage.version,
"package.json",
);
if (!packageJsonContent) {
return { error: "Could not find package.json in JSR package" };
}
let packageJson: any;
try {
packageJson = JSON.parse(packageJsonContent);
} catch (error) {
return { error: "Failed to parse package.json" };
}
if (packageJson.version !== payload.jsrPackage.version) {
// no need to fetch jsr.json or deno.json content, as we already know the version is valid with JSR API
return {
error:
"Package version in package.json does not match JSR package version",
};
}
/* -------------------------------------------------------------------------- */
/* validate package.json format against latest schema */
/* -------------------------------------------------------------------------- */
const parseResult = v.safeParse(ExtPackageJson, packageJson);
if (!parseResult.success) {
return { error: "package.json format not valid" };
}
const npmPkgVersionMetadata = await getJsrNpmPackageVersionMetadata(
payload.jsrPackage.scope,
payload.jsrPackage.name,
payload.jsrPackage.version,
);
const tarballUrl = npmPkgVersionMetadata.dist.tarball;
const shasum = npmPkgVersionMetadata.dist.shasum;
if (!tarballUrl) {
return { error: "Could not get tarball URL for JSR package" };
}
const tarballSize = await getTarballSize(tarballUrl);
const sizeLimit = payload.tarballSizeLimit ?? 50 * 1024 * 1024; // default to 50MB
if (tarballSize > sizeLimit) {
return {
error:
`Package tarball size (${tarballSize} bytes) exceeds limit of ${sizeLimit} bytes`,
};
}
/* -------------------------------------------------------------------------- */
/* get @kksh/api dependency version */
/* -------------------------------------------------------------------------- */
const apiVersion = parseResult.output.dependencies?.["@kksh/api"];
if (!apiVersion) {
return {
error:
`Extension ${packageJson.kunkun.identifier} doesn't not have @kksh/api as a dependency`,
};
}
return {
data: {
pkgJson: parseResult.output,
tarballUrl,
shasum,
apiVersion,
tarballSize,
},
};
}

View File

@ -0,0 +1,42 @@
import * as v from "valibot"
export const JsrPackageMetadata = v.object({
scope: v.string(),
name: v.string(),
latest: v.string(),
versions: v.record(
v.string(),
v.object({
yanked: v.optional(v.boolean())
})
)
})
export type JsrPackageMetadata = v.InferOutput<typeof JsrPackageMetadata>
export const NpmPkgMetadata = v.object({
name: v.string(),
description: v.optional(v.string()),
"dist-tags": v.record(v.string(), v.string()), // latest, next, beta, rc
versions: v.record(
v.string(),
v.object({
name: v.string(),
version: v.string(),
description: v.optional(v.string()),
dist: v.object({
tarball: v.string(),
shasum: v.string(),
integrity: v.string()
}),
dependencies: v.record(v.string(), v.string())
})
),
time: v.objectWithRest(
{
created: v.string(),
modified: v.string()
},
v.string()
)
})
export type NpmPkgMetadata = v.InferOutput<typeof NpmPkgMetadata>

View File

@ -1,36 +1,36 @@
import { FsPermissionSchema } from "tauri-api-adapter/permissions" import { FsPermissionSchema } from "tauri-api-adapter/permissions";
import * as v from "valibot" import * as v from "valibot";
import { import {
AllKunkunPermission, AllKunkunPermission,
FsPermissionScopedSchema, FsPermissionScopedSchema,
KunkunFsPermissionSchema, KunkunFsPermissionSchema,
KunkunManifestPermission, KunkunManifestPermission,
OpenPermissionScopedSchema, OpenPermissionScopedSchema,
ShellPermissionScopedSchema ShellPermissionScopedSchema,
} from "../permissions" } from "../permissions";
import { CmdType } from "./extension" import { CmdType } from "./extension";
import { Icon } from "./icon" import { Icon } from "./icon";
export enum OSPlatformEnum { export enum OSPlatformEnum {
linux = "linux", linux = "linux",
macos = "macos", macos = "macos",
windows = "windows" windows = "windows",
} }
export const OSPlatform = v.enum_(OSPlatformEnum) export const OSPlatform = v.enum_(OSPlatformEnum);
export type OSPlatform = v.InferOutput<typeof OSPlatform> export type OSPlatform = v.InferOutput<typeof OSPlatform>;
const allPlatforms = Object.values(OSPlatformEnum) const allPlatforms = Object.values(OSPlatformEnum);
export const TriggerCmd = v.object({ export const TriggerCmd = v.object({
type: v.union([v.literal("text"), v.literal("regex")]), type: v.union([v.literal("text"), v.literal("regex")]),
value: v.string() value: v.string(),
}) });
export type TriggerCmd = v.InferOutput<typeof TriggerCmd> export type TriggerCmd = v.InferOutput<typeof TriggerCmd>;
export enum TitleBarStyleEnum { export enum TitleBarStyleEnum {
"visible" = "visible", "visible" = "visible",
"transparent" = "transparent", "transparent" = "transparent",
"overlay" = "overlay" "overlay" = "overlay",
} }
export const TitleBarStyle = v.enum_(TitleBarStyleEnum) export const TitleBarStyle = v.enum_(TitleBarStyleEnum);
// JS new WebViewWindow only accepts lowercase, while manifest loaded from Rust is capitalized. I run toLowerCase() on the value before passing it to the WebViewWindow. // JS new WebViewWindow only accepts lowercase, while manifest loaded from Rust is capitalized. I run toLowerCase() on the value before passing it to the WebViewWindow.
// This lowercase title bar style schema is used to validate and set the type so TypeScript won't complaint // This lowercase title bar style schema is used to validate and set the type so TypeScript won't complaint
// export const TitleBarStyleAllLower = z.enum(["visible", "transparent", "overlay"]); // export const TitleBarStyleAllLower = z.enum(["visible", "transparent", "overlay"]);
@ -66,85 +66,101 @@ export const WindowConfig = v.object({
minimizable: v.optional(v.nullable(v.boolean())), minimizable: v.optional(v.nullable(v.boolean())),
closable: v.optional(v.nullable(v.boolean())), closable: v.optional(v.nullable(v.boolean())),
parent: v.optional(v.nullable(v.string())), parent: v.optional(v.nullable(v.string())),
visibleOnAllWorkspaces: v.optional(v.nullable(v.boolean())) visibleOnAllWorkspaces: v.optional(v.nullable(v.boolean())),
}) });
export type WindowConfig = v.InferOutput<typeof WindowConfig> export type WindowConfig = v.InferOutput<typeof WindowConfig>;
export const BaseCmd = v.object({ export const BaseCmd = v.object({
main: v.string("HTML file to load, e.g. dist/index.html"), main: v.string("HTML file to load, e.g. dist/index.html"),
description: v.optional(v.nullable(v.string("Description of the Command"), ""), ""), description: v.optional(
v.nullable(v.string("Description of the Command"), ""),
"",
),
name: v.string("Name of the command"), name: v.string("Name of the command"),
cmds: v.array(TriggerCmd, "Commands to trigger the UI"), cmds: v.array(TriggerCmd, "Commands to trigger the UI"),
icon: v.optional(Icon), icon: v.optional(Icon),
platforms: v.optional( platforms: v.optional(
v.nullable( v.nullable(
v.array(OSPlatform, "Platforms available on. Leave empty for all platforms."), v.array(
allPlatforms OSPlatform,
"Platforms available on. Leave empty for all platforms.",
),
allPlatforms,
), ),
allPlatforms allPlatforms,
) ),
}) });
export const CustomUiCmd = v.object({ export const CustomUiCmd = v.object({
...BaseCmd.entries, ...BaseCmd.entries,
type: v.optional(CmdType, CmdType.enum.UiIframe), type: v.optional(CmdType, CmdType.enum.UiIframe),
dist: v.string("Dist folder to load, e.g. dist, build, out"), dist: v.string("Dist folder to load, e.g. dist, build, out"),
devMain: v.string( devMain: v.string(
"URL to load in development to support live reload, e.g. http://localhost:5173/" "URL to load in development to support live reload, e.g. http://localhost:5173/",
), ),
window: v.optional(v.nullable(WindowConfig)) window: v.optional(v.nullable(WindowConfig)),
}) });
export type CustomUiCmd = v.InferOutput<typeof CustomUiCmd> export type CustomUiCmd = v.InferOutput<typeof CustomUiCmd>;
export const TemplateUiCmd = v.object({ export const TemplateUiCmd = v.object({
...BaseCmd.entries, ...BaseCmd.entries,
type: v.optional(CmdType, CmdType.enum.UiWorker), type: v.optional(CmdType, CmdType.enum.UiWorker),
window: v.optional(v.nullable(WindowConfig)) window: v.optional(v.nullable(WindowConfig)),
}) });
export const HeadlessCmd = v.object({ export const HeadlessCmd = v.object({
...BaseCmd.entries, ...BaseCmd.entries,
type: v.optional(CmdType, CmdType.enum.HeadlessWorker) type: v.optional(CmdType, CmdType.enum.HeadlessWorker),
}) });
export type HeadlessCmd = v.InferOutput<typeof HeadlessCmd> export type HeadlessCmd = v.InferOutput<typeof HeadlessCmd>;
export type TemplateUiCmd = v.InferOutput<typeof TemplateUiCmd> export type TemplateUiCmd = v.InferOutput<typeof TemplateUiCmd>;
export const PermissionUnion = v.union([ export const PermissionUnion = v.union([
KunkunManifestPermission, KunkunManifestPermission,
FsPermissionScopedSchema, FsPermissionScopedSchema,
OpenPermissionScopedSchema, OpenPermissionScopedSchema,
ShellPermissionScopedSchema ShellPermissionScopedSchema,
]) ]);
export type PermissionUnion = v.InferOutput<typeof PermissionUnion> export type PermissionUnion = v.InferOutput<typeof PermissionUnion>;
export const KunkunExtManifest = v.object({ export const KunkunExtManifest = v.object({
name: v.string("Name of the extension (Human Readable)"), name: v.string("Name of the extension (Human Readable)"),
shortDescription: v.string("Description of the extension (Will be displayed in store)"), shortDescription: v.string(
longDescription: v.string("Long description of the extension (Will be displayed in store)"), "Description of the extension (Will be displayed in store)",
),
longDescription: v.string(
"Long description of the extension (Will be displayed in store)",
),
identifier: v.string( identifier: v.string(
"Unique identifier for the extension, must be the same as extension folder name" "Unique identifier for the extension, must be the same as extension folder name",
), ),
icon: Icon, icon: Icon,
permissions: v.array( permissions: v.array(
PermissionUnion, PermissionUnion,
"Permissions Declared by the extension. e.g. clipboard-all. Not declared APIs will be blocked." "Permissions Declared by the extension. e.g. clipboard-all. Not declared APIs will be blocked.",
), ),
demoImages: v.array(v.string("Demo images for the extension")), demoImages: v.array(v.string("Demo images for the extension")),
customUiCmds: v.optional(v.array(CustomUiCmd, "Custom UI Commands")), customUiCmds: v.optional(v.array(CustomUiCmd, "Custom UI Commands")),
templateUiCmds: v.optional(v.array(TemplateUiCmd, "Template UI Commands")), templateUiCmds: v.optional(v.array(TemplateUiCmd, "Template UI Commands")),
headlessCmds: v.optional(v.array(HeadlessCmd, "Headless Commands")) headlessCmds: v.optional(v.array(HeadlessCmd, "Headless Commands")),
}) });
export type KunkunExtManifest = v.InferOutput<typeof KunkunExtManifest> export type KunkunExtManifest = v.InferOutput<typeof KunkunExtManifest>;
const Person = v.union([ const Person = v.union([
v.object({ v.object({
name: v.string("GitHub Username"), name: v.string("GitHub Username"),
email: v.string("Email of the person"), email: v.string("Email of the person"),
url: v.optional(v.nullable(v.string("URL of the person"))) url: v.optional(v.nullable(v.string("URL of the person"))),
}), }),
v.string("GitHub Username") v.string("GitHub Username"),
]) ]);
export const ExtPackageJson = v.object({ export const ExtPackageJson = v.object({
name: v.string("Package name for the extension (just a regular npm package name)"), name: v.string(
"Package name for the extension (just a regular npm package name)",
),
version: v.string("Version of the extension"), version: v.string("Version of the extension"),
author: v.optional(Person), author: v.optional(Person),
draft: v.optional(v.boolean("Whether the extension is a draft, draft will not be published")), draft: v.optional(
v.boolean(
"Whether the extension is a draft, draft will not be published",
),
),
contributors: v.optional(v.array(Person, "Contributors of the extension")), contributors: v.optional(v.array(Person, "Contributors of the extension")),
repository: v.optional( repository: v.optional(
v.union([ v.union([
@ -152,14 +168,17 @@ export const ExtPackageJson = v.object({
v.object({ v.object({
type: v.string("Type of the repository"), type: v.string("Type of the repository"),
url: v.string("URL of the repository"), url: v.string("URL of the repository"),
directory: v.string("Directory of the repository") directory: v.string("Directory of the repository"),
}) }),
]) ]),
), ),
dependencies: v.optional(v.record(v.string(), v.string())),
kunkun: KunkunExtManifest, kunkun: KunkunExtManifest,
files: v.array(v.string("Files to include in the extension. e.g. ['dist']")) files: v.array(
}) v.string("Files to include in the extension. e.g. ['dist']"),
export type ExtPackageJson = v.InferOutput<typeof ExtPackageJson> ),
});
export type ExtPackageJson = v.InferOutput<typeof ExtPackageJson>;
/** /**
* Extra fields for ExtPackageJson * Extra fields for ExtPackageJson
* e.g. path to the extension * e.g. path to the extension
@ -168,8 +187,8 @@ export const ExtPackageJsonExtra = v.object({
...ExtPackageJson.entries, ...ExtPackageJson.entries,
...{ ...{
extPath: v.string(), extPath: v.string(),
extFolderName: v.string() extFolderName: v.string(),
} },
}) });
export type ExtPackageJsonExtra = v.InferOutput<typeof ExtPackageJsonExtra> export type ExtPackageJsonExtra = v.InferOutput<typeof ExtPackageJsonExtra>;

View File

@ -1,268 +1,288 @@
export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[] export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export type Database = { export type Database = {
public: { public: {
Tables: { Tables: {
events: { events: {
Row: { Row: {
created_at: string created_at: string
data: Json | null data: Json | null
event_type: Database["public"]["Enums"]["event_type"] event_type: Database["public"]["Enums"]["event_type"]
id: number id: number
ip: string ip: string
} }
Insert: { Insert: {
created_at?: string created_at?: string
data?: Json | null data?: Json | null
event_type: Database["public"]["Enums"]["event_type"] event_type: Database["public"]["Enums"]["event_type"]
id?: number id?: number
ip: string ip: string
} }
Update: { Update: {
created_at?: string created_at?: string
data?: Json | null data?: Json | null
event_type?: Database["public"]["Enums"]["event_type"] event_type?: Database["public"]["Enums"]["event_type"]
id?: number id?: number
ip?: string ip?: string
} }
Relationships: [] Relationships: []
} }
ext_images: { ext_images: {
Row: { Row: {
created_at: string created_at: string
image_path: string image_path: string
sha512: string sha512: string
} }
Insert: { Insert: {
created_at?: string created_at?: string
image_path: string image_path: string
sha512: string sha512: string
} }
Update: { Update: {
created_at?: string created_at?: string
image_path?: string image_path?: string
sha512?: string sha512?: string
} }
Relationships: [] Relationships: []
} }
ext_publish: { ext_publish: {
Row: { Row: {
api_version: string | null api_version: string | null
cmd_count: number cmd_count: number
created_at: string created_at: string
demo_images: string[] demo_images: string[]
downloads: number downloads: number
id: number id: number
identifier: string identifier: string
manifest: Json manifest: Json
name: string metadata: Json | null
shasum: string name: string
size: number shasum: string
tarball_path: string size: number
version: string tarball_path: string
} version: string
Insert: { }
api_version?: string | null Insert: {
cmd_count: number api_version?: string | null
created_at?: string cmd_count: number
demo_images: string[] created_at?: string
downloads: number demo_images: string[]
id?: number downloads: number
identifier: string id?: number
manifest: Json identifier: string
name: string manifest: Json
shasum: string metadata?: Json | null
size: number name: string
tarball_path: string shasum: string
version: string size: number
} tarball_path: string
Update: { version: string
api_version?: string | null }
cmd_count?: number Update: {
created_at?: string api_version?: string | null
demo_images?: string[] cmd_count?: number
downloads?: number created_at?: string
id?: number demo_images?: string[]
identifier?: string downloads?: number
manifest?: Json id?: number
name?: string identifier?: string
shasum?: string manifest?: Json
size?: number metadata?: Json | null
tarball_path?: string name?: string
version?: string shasum?: string
} size?: number
Relationships: [ tarball_path?: string
{ version?: string
foreignKeyName: "ext_publish_identifier_fkey" }
columns: ["identifier"] Relationships: [
isOneToOne: false {
referencedRelation: "extensions" foreignKeyName: "ext_publish_identifier_fkey"
referencedColumns: ["identifier"] columns: ["identifier"]
} isOneToOne: false
] referencedRelation: "extensions"
} referencedColumns: ["identifier"]
extensions: { },
Row: { ]
api_version: string }
created_at: string extensions: {
downloads: number Row: {
icon: Json | null api_version: string
identifier: string author_id: string | null
long_description: string | null created_at: string
name: string downloads: number
readme: string | null icon: Json | null
short_description: string identifier: string
version: string long_description: string | null
} name: string
Insert: { readme: string | null
api_version: string short_description: string
created_at?: string version: string
downloads: number }
icon?: Json | null Insert: {
identifier: string api_version: string
long_description?: string | null author_id?: string | null
name: string created_at?: string
readme?: string | null downloads: number
short_description: string icon?: Json | null
version: string identifier: string
} long_description?: string | null
Update: { name: string
api_version?: string readme?: string | null
created_at?: string short_description: string
downloads?: number version: string
icon?: Json | null }
identifier?: string Update: {
long_description?: string | null api_version?: string
name?: string author_id?: string | null
readme?: string | null created_at?: string
short_description?: string downloads?: number
version?: string icon?: Json | null
} identifier?: string
Relationships: [] long_description?: string | null
} name?: string
} readme?: string | null
Views: { short_description?: string
[_ in never]: never version?: string
} }
Functions: { Relationships: []
get_aggregated_downloads: { }
Args: Record<PropertyKey, never> }
Returns: { Views: {
identifier: string [_ in never]: never
total_downloads: number }
}[] Functions: {
} get_aggregated_downloads: {
get_aggregated_downloads_with_details: { Args: Record<PropertyKey, never>
Args: Record<PropertyKey, never> Returns: {
Returns: { identifier: string
identifier: string total_downloads: number
total_downloads: number }[]
name: string }
short_description: string get_aggregated_downloads_with_details: {
}[] Args: Record<PropertyKey, never>
} Returns: {
increment_downloads: { identifier: string
Args: { total_downloads: number
t_identifier: string name: string
t_version: string short_description: string
} }[]
Returns: number }
} increment_downloads: {
} Args: {
Enums: { t_identifier: string
event_type: "download" | "updater" | "schema" | "nightly_schema" t_version: string
} }
CompositeTypes: { Returns: number
[_ in never]: never }
} }
} Enums: {
event_type: "download" | "updater" | "schema" | "nightly_schema"
}
CompositeTypes: {
[_ in never]: never
}
}
} }
type PublicSchema = Database[Extract<keyof Database, "public">] type PublicSchema = Database[Extract<keyof Database, "public">]
export type Tables< export type Tables<
PublicTableNameOrOptions extends PublicTableNameOrOptions extends
| keyof (PublicSchema["Tables"] & PublicSchema["Views"]) | keyof (PublicSchema["Tables"] & PublicSchema["Views"])
| { schema: keyof Database }, | { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database } TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"]) Database[PublicTableNameOrOptions["schema"]]["Views"])
: never = never : never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database } > = PublicTableNameOrOptions extends { schema: keyof Database }
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R Row: infer R
} }
? R ? R
: never : never
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & PublicSchema["Views"]) : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
? (PublicSchema["Tables"] & PublicSchema["Views"])[PublicTableNameOrOptions] extends { PublicSchema["Views"])
Row: infer R ? (PublicSchema["Tables"] &
} PublicSchema["Views"])[PublicTableNameOrOptions] extends {
? R Row: infer R
: never }
: never ? R
: never
: never
export type TablesInsert< export type TablesInsert<
PublicTableNameOrOptions extends keyof PublicSchema["Tables"] | { schema: keyof Database }, PublicTableNameOrOptions extends
TableName extends PublicTableNameOrOptions extends { schema: keyof Database } | keyof PublicSchema["Tables"]
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] | { schema: keyof Database },
: never = never TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database } > = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I Insert: infer I
} }
? I ? I
: never : never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"] : PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends { ? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Insert: infer I Insert: infer I
} }
? I ? I
: never : never
: never : never
export type TablesUpdate< export type TablesUpdate<
PublicTableNameOrOptions extends keyof PublicSchema["Tables"] | { schema: keyof Database }, PublicTableNameOrOptions extends
TableName extends PublicTableNameOrOptions extends { schema: keyof Database } | keyof PublicSchema["Tables"]
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] | { schema: keyof Database },
: never = never TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database } > = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U Update: infer U
} }
? U ? U
: never : never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"] : PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends { ? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Update: infer U Update: infer U
} }
? U ? U
: never : never
: never : never
export type Enums< export type Enums<
PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] | { schema: keyof Database }, PublicEnumNameOrOptions extends
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } | keyof PublicSchema["Enums"]
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] | { schema: keyof Database },
: never = never EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
: never = never,
> = PublicEnumNameOrOptions extends { schema: keyof Database } > = PublicEnumNameOrOptions extends { schema: keyof Database }
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
? PublicSchema["Enums"][PublicEnumNameOrOptions] ? PublicSchema["Enums"][PublicEnumNameOrOptions]
: never : never
export type CompositeTypes< export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends PublicCompositeTypeNameOrOptions extends
| keyof PublicSchema["CompositeTypes"] | keyof PublicSchema["CompositeTypes"]
| { schema: keyof Database }, | { schema: keyof Database },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof Database schema: keyof Database
} }
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
: never = never : never = never,
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } > = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"] : PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"]
? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] ? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
: never : never

View File

@ -21,7 +21,7 @@ export const breakingChangesVersionCheckpoints = [
const checkpointVersions = breakingChangesVersionCheckpoints.map((c) => c.version) const checkpointVersions = breakingChangesVersionCheckpoints.map((c) => c.version)
const sortedCheckpointVersions = sort(checkpointVersions) const sortedCheckpointVersions = sort(checkpointVersions)
export const version = "0.0.48" export const version = "0.0.52"
export function isVersionBetween(v: string, start: string, end: string) { export function isVersionBetween(v: string, start: string, end: string) {
const vCleaned = clean(v) const vCleaned = clean(v)

View File

@ -17,6 +17,7 @@
"@kksh/api": "workspace:*", "@kksh/api": "workspace:*",
"@kksh/supabase": "workspace:*", "@kksh/supabase": "workspace:*",
"@std/semver": "npm:@jsr/std__semver@^1.0.3", "@std/semver": "npm:@jsr/std__semver@^1.0.3",
"@tauri-apps/plugin-upload": "^2.2.1",
"uuid": "^11.0.3" "uuid": "^11.0.3"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -19,7 +19,13 @@ import { loadExtensionManifestFromDisk } from "./load"
* *
* @param tarballPath path to .tar.gz file * @param tarballPath path to .tar.gz file
*/ */
export async function installTarball(tarballPath: string, extsDir: string): Promise<string> { export async function installTarball(
tarballPath: string,
extsDir: string,
extras?: {
overwritePackageJson?: string
}
): Promise<string> {
const tempDirPath = await path.tempDir() const tempDirPath = await path.tempDir()
if (!extsDir) { if (!extsDir) {
return Promise.reject("Extension Folder Not Set") return Promise.reject("Extension Folder Not Set")
@ -32,7 +38,11 @@ export async function installTarball(tarballPath: string, extsDir: string): Prom
overwrite: true overwrite: true
} }
) )
return loadExtensionManifestFromDisk(await path.join(decompressDest, "package.json")) const pkgJsonPath = await path.join(decompressDest, "package.json")
if (extras?.overwritePackageJson) {
await fs.writeTextFile(pkgJsonPath, extras.overwritePackageJson)
}
return loadExtensionManifestFromDisk(pkgJsonPath)
.then(async (manifest) => { .then(async (manifest) => {
// The extension folder name will be the identifier // The extension folder name will be the identifier
const extInstallPath = await path.join(extsDir, manifest.kunkun.identifier) const extInstallPath = await path.join(extsDir, manifest.kunkun.identifier)
@ -62,8 +72,7 @@ export async function installTarball(tarballPath: string, extsDir: string): Prom
console.error(err) console.error(err)
throw new Error("Invalid Manifest or Extension") throw new Error("Invalid Manifest or Extension")
} }
console.log() console.error("installTarball error", err)
throw new Error(err) throw new Error(err)
}) })
} }
@ -74,13 +83,18 @@ export async function installTarball(tarballPath: string, extsDir: string): Prom
* @param extsDir Target directory to install the tarball * @param extsDir Target directory to install the tarball
* @returns * @returns
*/ */
export async function installTarballUrl(tarballUrl: string, extsDir: string): Promise<string> { export async function installTarballUrl(
tarballUrl: string,
extsDir: string,
extras?: { overwritePackageJson?: string }
): Promise<string> {
const filename = await path.basename(tarballUrl) const filename = await path.basename(tarballUrl)
if (filename) { if (filename) {
const tempDirPath = await path.tempDir() const tempDirPath = await path.tempDir()
let tarballPath = await path.join(tempDirPath, filename) let tarballPath = await path.join(tempDirPath, filename)
console.log("tarballPath", tarballPath)
await download(tarballUrl, tarballPath) await download(tarballUrl, tarballPath)
const extInstallPath = await installTarball(tarballPath, extsDir) const extInstallPath = await installTarball(tarballPath, extsDir, extras)
await fs.remove(tarballPath) await fs.remove(tarballPath)
return extInstallPath return extInstallPath
} else { } else {

View File

@ -48,35 +48,35 @@
}, },
"dependencies": { "dependencies": {
"@kksh/api": "workspace:*", "@kksh/api": "workspace:*",
"@kksh/svelte": "0.1.4", "@kksh/svelte5": "0.1.11",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-svelte": "^0.416.0", "lucide-svelte": "^0.469.0",
"mode-watcher": "^0.4.0", "mode-watcher": "^0.5.0",
"tailwind-merge": "^2.4.0", "tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.2.1" "tailwind-variants": "^0.3.0"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.15.2",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"svelte": "^5.0.0", "svelte": "^5.16.6",
"svelte-check": "^4.0.0", "svelte-check": "^4.1.1",
"typescript": "^5.0.0", "typescript": "^5.7.2",
"vite": "^5.0.3", "vite": "^6.0.7",
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.8",
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.16",
"@types/eslint": "^9.6.0", "@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.20",
"eslint": "^9.0.0", "eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0", "eslint-plugin-svelte": "^2.46.1",
"globals": "^15.0.0", "globals": "^15.14.0",
"postcss": "^8.4.38", "postcss": "^8.4.49",
"prettier": "^3.1.1", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.4", "prettier-plugin-tailwindcss": "^0.6.9",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.17",
"typescript-eslint": "^8.0.0-alpha.20" "typescript-eslint": "^8.19.1"
}, },
"type": "module", "type": "module",
"files": [ "files": [

View File

@ -1,4 +1,4 @@
@import url("@kksh/svelte/themes"); @import url("@kksh/svelte5/themes");
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { ThemeCustomizerButton, type ThemeConfig, updateTheme } from '@kksh/svelte'; import { ThemeCustomizerButton, type ThemeConfig, updateTheme } from '@kksh/svelte5';
import { ui } from '@kksh/api/ui/iframe'; import { ui } from '@kksh/api/ui/iframe';
import { onMount } from 'svelte'; import { onMount } from 'svelte';

View File

@ -1,7 +1,7 @@
<script> <script>
import '../app.css'; import '../app.css';
import { ModeWatcher } from 'mode-watcher'; import { ModeWatcher } from 'mode-watcher';
import { ThemeWrapper, updateTheme } from '@kksh/svelte'; import { ThemeWrapper, updateTheme } from '@kksh/svelte5';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { ui } from '@kksh/api/ui/iframe'; import { ui } from '@kksh/api/ui/iframe';

View File

@ -5,12 +5,11 @@
ModeToggle, ModeToggle,
Button, Button,
Command, Command,
CommandFooter,
ModeWatcher, ModeWatcher,
Separator, Separator,
ThemeWrapper, ThemeWrapper,
updateTheme updateTheme
} from '@kksh/svelte'; } from '@kksh/svelte5';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
onMount(() => { onMount(() => {

View File

@ -1,6 +1,6 @@
<script> <script>
import { base } from '$app/paths'; import { base } from '$app/paths';
import { Alert, Button, ThemeWrapper } from '@kksh/svelte'; import { Alert, Button, ThemeWrapper } from '@kksh/svelte5';
</script> </script>
<ThemeWrapper> <ThemeWrapper>

View File

@ -1,3 +1,3 @@
```bash ```bash
npx supabase gen types --lang=typescript --project-id $PROJECT_REF --schema public > src/types/database.types.ts npx supabase gen types --lang=typescript --project-id $PROJECT_REF --schema public > ../api/src/supabase/database.types.ts
``` ```

View File

@ -5,11 +5,13 @@
"prepare": "bun setup.ts" "prepare": "bun setup.ts"
}, },
"exports": { "exports": {
".": "./src/index.ts" ".": "./src/index.ts",
"./models": "./src/models.ts"
}, },
"dependencies": { "dependencies": {
"@kksh/api": "workspace:*", "@kksh/api": "workspace:*",
"@supabase/supabase-js": "^2.46.1" "@supabase/ssr": "^0.5.2",
"@supabase/supabase-js": "^2.47.12"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest" "@types/bun": "latest"

View File

@ -33,7 +33,7 @@ export class SupabaseAPI {
return this.supabase return this.supabase
.from("ext_publish") .from("ext_publish")
.select( .select(
"created_at, name, version, manifest, shasum, size, tarball_path, cmd_count, identifier, downloads, demo_images, api_version" "created_at, name, version, manifest, shasum, size, tarball_path, cmd_count, identifier, downloads, demo_images, api_version, metadata"
) )
.order("created_at", { ascending: false }) .order("created_at", { ascending: false })
.eq("identifier", identifier) .eq("identifier", identifier)
@ -70,6 +70,28 @@ export class SupabaseAPI {
}) })
} }
async publishExtFromJSR(payload: {
scope: string
version: string
name: string
}): Promise<void> {
return this.supabase.functions
.invoke("publish-jsr-ext", {
body: payload
})
.then(async ({ data, error }) => {
if (data && data.isValid) {
return
}
if (error?.name === "FunctionsHttpError") {
const errorMessage = await error.context.json()
throw new Error(errorMessage.error)
} else {
throw new Error(`Unknown error: ${error?.message}`)
}
})
}
translateExtensionFilePathToUrl(tarballPath: string): string { translateExtensionFilePathToUrl(tarballPath: string): string {
return this.supabase.storage.from("extensions").getPublicUrl(tarballPath).data.publicUrl return this.supabase.storage.from("extensions").getPublicUrl(tarballPath).data.publicUrl
} }

View File

@ -0,0 +1,18 @@
/**
* @module @kksh/supabase/models
* This module contains some models for supabase database that cannot be code generated, such as JSON fields.
*/
import * as v from "valibot";
export enum ExtPublishSourceTypeEnum {
jsr = "jsr",
npm = "npm",
}
export const ExtPublishMetadata = v.object({
source: v.optional(
v.string("Source of the extension (e.g. url to package)"),
),
sourceType: v.optional(v.enum(ExtPublishSourceTypeEnum)),
});
export type ExtPublishMetadata = v.InferOutput<typeof ExtPublishMetadata>;

View File

@ -40,23 +40,23 @@
}, },
"dependencies": { "dependencies": {
"@kksh/api": "workspace:*", "@kksh/api": "workspace:*",
"@kksh/svelte5": "0.1.10", "@kksh/svelte5": "0.1.11",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-svelte": "^0.460.1", "lucide-svelte": "^0.469.0",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.0" "tailwind-variants": "^0.3.0"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.1", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tsconfig/svelte": "^5.0.4", "@tsconfig/svelte": "^5.0.4",
"svelte": "^5.2.7", "svelte": "^5.16.6",
"svelte-check": "^4.0.9", "svelte-check": "^4.1.1",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "~5.6.3", "typescript": "~5.7.2",
"vite": "^5.4.11", "vite": "^6.0.7",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"tailwindcss": "^3.4.15" "tailwindcss": "^3.4.17"
}, },
"files": [ "files": [
"dist", "dist",

View File

@ -46,35 +46,35 @@
}, },
"dependencies": { "dependencies": {
"@kksh/api": "workspace:*", "@kksh/api": "workspace:*",
"@kksh/svelte5": "0.1.10", "@kksh/svelte5": "0.1.11",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-svelte": "^0.460.1", "lucide-svelte": "^0.469.0",
"mode-watcher": "^0.5.0", "mode-watcher": "^0.5.0",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.0" "tailwind-variants": "^0.3.0"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.3.1", "@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/kit": "^2.8.1", "@sveltejs/kit": "^2.15.2",
"@sveltejs/vite-plugin-svelte": "^4.0.1", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"svelte": "^5.2.7", "svelte": "^5.16.6",
"svelte-check": "^4.0.9", "svelte-check": "^4.1.1",
"typescript": "^5.6.3", "typescript": "^5.7.2",
"vite": "^5.4.11", "vite": "^6.0.7",
"@sveltejs/adapter-static": "^3.0.6", "@sveltejs/adapter-static": "^3.0.8",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.16",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.15.0", "eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.0", "eslint-plugin-svelte": "^2.46.1",
"globals": "^15.12.0", "globals": "^15.14.0",
"postcss": "^8.4.49", "postcss": "^8.4.49",
"prettier": "^3.3.3", "prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.2.8", "prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.9", "prettier-plugin-tailwindcss": "^0.6.9",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.17",
"typescript-eslint": "^8.15.0" "typescript-eslint": "^8.19.1"
}, },
"type": "module", "type": "module",
"files": [ "files": [

View File

@ -33,22 +33,25 @@
"scripts": { "scripts": {
"lint": "eslint ." "lint": "eslint ."
}, },
"peerDependencies": {
"svelte": "^5.0.0"
},
"devDependencies": { "devDependencies": {
"@iconify/svelte": "^4.1.0", "@iconify/svelte": "^4.2.0",
"@kksh/api": "workspace:*", "@kksh/api": "workspace:*",
"@kksh/svelte5": "^0.1.10", "@kksh/svelte5": "^0.1.12",
"@types/bun": "latest", "@types/bun": "latest",
"bits-ui": "1.0.0-next.72", "bits-ui": "1.0.0-next.77",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"formsnap": "2.0.0-next.1", "formsnap": "2.0.0-next.1",
"lucide-svelte": "^0.468.0", "lucide-svelte": "^0.469.0",
"mode-watcher": "^0.5.0", "mode-watcher": "^0.5.0",
"paneforge": "1.0.0-next.1", "paneforge": "0.0.6",
"shiki": "^1.24.2", "shiki": "^1.26.1",
"svelte-radix": "^2.0.1", "svelte-radix": "^2.0.1",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"sveltekit-superforms": "^2.22.1", "sveltekit-superforms": "^2.22.1",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.0", "tailwind-variants": "^0.3.0",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

View File

@ -0,0 +1,123 @@
<!-- Element Plus Style Alert (created because the original Shadcn Alert is not very good looking) -->
<script lang="ts" module>
export type AlertProps = {
title?: string
closable?: boolean
description?: string
variant?: "success" | "info" | "warning" | "error"
onClose?: () => void
class?: string
withIcon?: boolean
children?: Snippet
}
</script>
<script lang="ts">
import {
CircleAlertIcon,
CircleCheckBigIcon,
CircleHelpIcon,
CircleXIcon,
XIcon
} from "lucide-svelte"
import type { Snippet } from "svelte"
import { fade } from "svelte/transition"
import { cn } from "../../utils"
const config: Record<
"success" | "info" | "warning" | "error",
{
color: string
}
> = {
success: {
color: "green"
},
info: {
color: "blue"
},
warning: {
color: "yellow"
},
error: {
color: "red"
}
}
let {
title,
description,
class: className,
variant: type = "info",
closable,
withIcon,
onClose,
children
}: AlertProps = $props()
let show = $state(true)
</script>
{#if show}
<div
class={cn("flex items-center gap-3 rounded-xl px-3 py-3", className, {
"bg-red-500/10": type === "error",
"bg-blue-500/10": type === "info",
"bg-yellow-500/10": type === "warning",
"bg-green-500/10": type === "success"
})}
transition:fade
>
{#if withIcon}
{#if type === "success"}
<CircleCheckBigIcon class="h-6 w-6 shrink-0 text-green-400" />
{:else if type === "info"}
<CircleHelpIcon class="h-6 w-6 shrink-0 text-blue-400" />
{:else if type === "warning"}
<CircleAlertIcon class="h-6 w-6 shrink-0 text-yellow-400" />
{:else if type === "error"}
<CircleXIcon class="h-6 w-6 shrink-0 text-red-400" />
{/if}
{/if}
<div class="flex grow flex-col">
{#if title}
<span
class={cn("font-semibold", {
"text-green-400": type === "success",
"text-blue-400": type === "info",
"text-yellow-400": type === "warning",
"text-red-400": type === "error"
})}
>
{title}
</span>
{/if}
{#if description}
<small
class={cn("text-sm", {
"text-green-400/90": type === "success",
"text-blue-400/90": type === "info",
"text-yellow-400/90": type === "warning",
"text-red-400/90": type === "error"
})}
>
{description}
</small>
{/if}
{#if children}
{@render children()}
{/if}
</div>
{#if closable}
<XIcon
onclick={() => {
if (onClose) {
onClose()
} else {
}
show = false
}}
class="h-4 w-4 shrink-0 cursor-pointer self-start"
/>
{/if}
</div>
{/if}

View File

@ -3,4 +3,5 @@ export { default as IconSelector } from "./IconSelector.svelte"
export { default as StrikeSeparator } from "./StrikeSeparator.svelte" export { default as StrikeSeparator } from "./StrikeSeparator.svelte"
export { default as LoadingBar } from "./LoadingBar.svelte" export { default as LoadingBar } from "./LoadingBar.svelte"
export { default as TauriLink } from "./TauriLink.svelte" export { default as TauriLink } from "./TauriLink.svelte"
export { default as ElementAlert } from "./ElementAlert.svelte"
export * from "./date" export * from "./date"

View File

@ -1,4 +1,5 @@
export { default as ExtListItem } from "./ExtListItem.svelte" export { default as ExtListItem } from "./ExtListItem.svelte"
export { default as StoreExtDetail } from "./StoreExtDetail.svelte" export { default as StoreExtDetail } from "./StoreExtDetail.svelte"
export { default as PermissionInspector } from "./PermissionInspector.svelte" export { default as PermissionInspector } from "./PermissionInspector.svelte"
export { default as JsrPackageVersionTable } from "./publish/jsr/jsr-package-version-table.svelte"
export * as Templates from "./templates" export * as Templates from "./templates"

View File

@ -0,0 +1,93 @@
<script lang="ts">
import { Button, HoverCard, Table } from "@kksh/svelte5"
import { cn } from "../../../../utils"
type Version = {
scope: string
package?: string
version: string
yanked: boolean
rekorLogId?: string
}
let {
class: className,
githubOwnerMismatch,
versions,
onPublish,
publishedVersions
}: {
class?: string
githubOwnerMismatch: boolean
versions: Version[]
onPublish?: (version: Version) => void
publishedVersions: string[]
} = $props()
</script>
<Table.Root class={className}>
<Table.Caption>All versions of the package</Table.Caption>
<Table.Header>
<Table.Row>
<Table.Head class="w-[100px] text-center">Version</Table.Head>
<Table.Head class="text-center">Yanked</Table.Head>
<Table.Head class="text-center">Signed by GitHub Action</Table.Head>
<Table.Head class="text-center">Publish This Version</Table.Head>
<Table.Head class="text-center">Published</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body class="max-h-96">
{#each versions as version, i (i)}
{@const isPublished = publishedVersions.includes(version.version)}
<Table.Row>
<Table.Cell class="text-center font-medium">
<a
href={`https://jsr.io/@${version.scope}/${version.package}@${version.version}`}
target="_blank"
class="text-blue-500 underline"
>
{version.version}
</a>
</Table.Cell>
<Table.Cell
class={cn("text-center font-bold", {
"text-red-500": version.yanked,
"text-green-500": !version.yanked
})}
>
{version.yanked ? "Yes" : "No"}
</Table.Cell>
<Table.Cell class="text-center">{version.rekorLogId ? "✅" : "❌"}</Table.Cell>
<Table.Cell class="text-center">
{@const disabled =
version.yanked || !version.rekorLogId || githubOwnerMismatch || isPublished}
{#if disabled}
<HoverCard.Root>
<HoverCard.Trigger>
<Button size="sm" variant="outline" disabled={true}>Publish</Button>
</HoverCard.Trigger>
<HoverCard.Content>
{#if version.yanked}
Version is yanked
{:else if !version.rekorLogId}
Version is not signed by GitHub Action
{:else if githubOwnerMismatch}
Your GitHub account is not the owner of the package
{:else if isPublished}
Version is already published
{/if}
</HoverCard.Content>
</HoverCard.Root>
{:else}
<Button size="sm" variant="outline" onclick={() => onPublish?.(version)}>
Publish
</Button>
{/if}
</Table.Cell>
<Table.Cell class="text-center">
{isPublished ? "✅" : "❌"}
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
<Table.Footer></Table.Footer>
</Table.Root>

View File

@ -2,11 +2,13 @@
import { CmdTypeEnum, IconEnum, SysCommand } from "@kksh/api/models" import { CmdTypeEnum, IconEnum, SysCommand } from "@kksh/api/models"
import { Command } from "@kksh/svelte5" import { Command } from "@kksh/svelte5"
import { IconMultiplexer } from "@kksh/ui" import { IconMultiplexer } from "@kksh/ui"
import { confirm } from "@tauri-apps/plugin-dialog"
import { DraggableCommandGroup } from "../custom" import { DraggableCommandGroup } from "../custom"
import { CmdValue } from "./types" import { CmdValue } from "./types"
const { systemCommands }: { systemCommands: SysCommand[] } = $props() const {
systemCommands,
onConfirm
}: { systemCommands: SysCommand[]; onConfirm?: (cmd: SysCommand) => Promise<boolean> } = $props()
</script> </script>
<DraggableCommandGroup heading="System Commands"> <DraggableCommandGroup heading="System Commands">
@ -15,7 +17,7 @@
class="flex justify-between" class="flex justify-between"
onSelect={async () => { onSelect={async () => {
if (cmd.confirmRequired) { if (cmd.confirmRequired) {
const confirmed = await confirm(`Are you sure you want to run ${cmd.name}?`) const confirmed = onConfirm ? await onConfirm?.(cmd) : true
if (confirmed) { if (confirmed) {
cmd.function() cmd.function()
} }

Binary file not shown.

3385
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

@ -1 +1 @@
Subproject commit ab47fa355ba5c6088abd2d173efb742062202a8a Subproject commit 282b6df90c6e4fc596e1be33e923e13270174ee1