diff --git a/.github/workflows/utils-release.yml b/.github/workflows/utils-release.yml new file mode 100644 index 000000000..1bc5372c1 --- /dev/null +++ b/.github/workflows/utils-release.yml @@ -0,0 +1,58 @@ +name: Utils Publish + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: "Release tag (e.g. v1.2.3)" + required: false + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + publish: + runs-on: depot-ubuntu-24.04 + defaults: + run: + working-directory: ./src/typescript/utils + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Set version from tag + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + TAG_NAME=${{ github.event.inputs.tag }} + else + TAG_NAME=${GITHUB_REF_NAME} + fi + + if [ -z "$TAG_NAME" ]; then + echo "Error: No tag specified." + exit 1 + fi + + npm version "$TAG_NAME" --no-git-tag-version + + - name: Install API dependencies + run: npm --prefix ../api ci + - name: Build API dependency + run: npm --prefix ../api run build + - name: Install dependencies + run: npm ci + - name: Build package + run: npm run build + - uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c + with: + token: ${{ secrets.NPM_TOKEN }} + package: ./src/typescript/utils diff --git a/src/typescript/README.md b/src/typescript/README.md index 79f6fb932..448e0933e 100644 --- a/src/typescript/README.md +++ b/src/typescript/README.md @@ -4,6 +4,7 @@ More information can be obtained on each project by navigating to their respecti - `extension-manager`: a node process meant to be spawned by the main vicinae executable, ready to load and unload extension workers on-demand. - `@vicinae/api`: the vicinae SDK used to build extensions, as published on npm - `@vicinae/raycast-api-compat`: a private package that provides a raycast-compatible way to use the `@vicinae/api` package. It is a mere wrapper around it. +- `@vicinae/utils`: shared utilities for building extensions, published on npm. # Integration with the build system diff --git a/src/typescript/biome.json b/src/typescript/biome.json index d4a352afc..70cd4feea 100644 --- a/src/typescript/biome.json +++ b/src/typescript/biome.json @@ -4,6 +4,7 @@ "api/**", "extension-manager/**", "raycast-api-compat/**", + "utils/**", "!api/src/proto/**", "!api/src/api/proto/**", "!extension-manager/src/proto/**" diff --git a/src/typescript/utils/.gitignore b/src/typescript/utils/.gitignore new file mode 100644 index 000000000..bcda4e10e --- /dev/null +++ b/src/typescript/utils/.gitignore @@ -0,0 +1,4 @@ +*-debug.log +*-error.log +/dist +/node_modules \ No newline at end of file diff --git a/src/typescript/utils/.npmrc b/src/typescript/utils/.npmrc new file mode 100644 index 000000000..55c01c1bb --- /dev/null +++ b/src/typescript/utils/.npmrc @@ -0,0 +1,2 @@ +ignore-scripts=true +min-release-age=7 \ No newline at end of file diff --git a/src/typescript/utils/README.md b/src/typescript/utils/README.md new file mode 100644 index 000000000..7ae6e3c0b --- /dev/null +++ b/src/typescript/utils/README.md @@ -0,0 +1,9 @@ +# `@vicinae/utils` + +Shared utilities for building Vicinae extensions. + +## Installation + +```bash +npm install @vicinae/api @vicinae/utils +``` \ No newline at end of file diff --git a/src/typescript/utils/package-lock.json b/src/typescript/utils/package-lock.json new file mode 100644 index 000000000..e1f688e1e --- /dev/null +++ b/src/typescript/utils/package-lock.json @@ -0,0 +1,341 @@ +{ + "name": "@vicinae/utils", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@vicinae/utils", + "version": "0.0.0", + "dependencies": { + "@tanstack/query-async-storage-persister": "^5.101.4", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-persist-client": "^5.101.4" + }, + "devDependencies": { + "@biomejs/biome": "2.3.2", + "@types/react": "19.1.17", + "@vicinae/api": "file:../api", + "react": "19.1.1", + "typescript": "5.9.2" + }, + "peerDependencies": { + "@vicinae/api": "*", + "react": ">=19" + } + }, + "../api": { + "name": "@vicinae/api", + "version": "0.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/react": "19.1.17", + "chokidar": "^4.0.3", + "esbuild": "^0.25.2", + "react": "19.1.1", + "typescript": "5.9.2", + "zod": "^4.0.17" + }, + "bin": { + "vici": "dist/bin.js" + }, + "devDependencies": { + "@biomejs/biome": "2.3.2", + "@types/node": "24.3.0" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.2.tgz", + "integrity": "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.3.2", + "@biomejs/cli-darwin-x64": "2.3.2", + "@biomejs/cli-linux-arm64": "2.3.2", + "@biomejs/cli-linux-arm64-musl": "2.3.2", + "@biomejs/cli-linux-x64": "2.3.2", + "@biomejs/cli-linux-x64-musl": "2.3.2", + "@biomejs/cli-win32-arm64": "2.3.2", + "@biomejs/cli-win32-x64": "2.3.2" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.2.tgz", + "integrity": "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.2.tgz", + "integrity": "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.2.tgz", + "integrity": "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.2.tgz", + "integrity": "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.2.tgz", + "integrity": "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.2.tgz", + "integrity": "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.2.tgz", + "integrity": "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.2.tgz", + "integrity": "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@tanstack/query-async-storage-persister": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.101.4.tgz", + "integrity": "sha512-ROenNOVvxIZ1zKsBAiIvvslLo2xD8Me/vFz/tChdmuI7ciU+ET63yRrQ01W899Tx1ayQXdsGQJqYqjjVZk+OBQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.4", + "@tanstack/query-persist-client-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-persist-client-core": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.101.4.tgz", + "integrity": "sha512-Bmu+RfWhwYEyYZEwSeKC0gX4+KTkiEB3yO8oz9BfAY/Jfe3ndz9EnYVhihIZPmWIr8NXayF7JuraNQcrXQQCCg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-persist-client": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.101.4.tgz", + "integrity": "sha512-CY5i7PPKS/5B8OR2zAs9nCbLpgqI4jN8eBhtAxKRMNHyGcQhmyOy4PkkJrAAXT+ecGj++fBJTgQw6OpyCt/2SQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-persist-client-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.101.4", + "react": "^18 || ^19" + } + }, + "node_modules/@types/react": { + "version": "19.1.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", + "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@vicinae/api": { + "resolved": "../api", + "link": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/src/typescript/utils/package.json b/src/typescript/utils/package.json new file mode 100644 index 000000000..f76930f98 --- /dev/null +++ b/src/typescript/utils/package.json @@ -0,0 +1,35 @@ +{ + "name": "@vicinae/utils", + "version": "0.0.0", + "description": "Shared utilities for Vicinae extensions", + "scripts": { + "build": "tsc --outDir dist", + "pack": "npm run build && npm pack", + "format": "biome format --write" + }, + "files": [ + "dist" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@vicinae/api": "*", + "react": ">=19" + }, + "devDependencies": { + "@biomejs/biome": "2.3.2", + "@types/react": "19.1.17", + "@vicinae/api": "file:../api", + "react": "19.1.1", + "typescript": "5.9.2" + }, + "repository": "vicinaehq/vicinae", + "dependencies": { + "@tanstack/query-async-storage-persister": "^5.101.4", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-persist-client": "^5.101.4" + } +} diff --git a/src/typescript/utils/src/execute.ts b/src/typescript/utils/src/execute.ts new file mode 100644 index 000000000..5d65f0ac5 --- /dev/null +++ b/src/typescript/utils/src/execute.ts @@ -0,0 +1,22 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { useQuery } from "./query.js"; + +const executeAsync = promisify(execFile); + +async function execute(command: string, args: readonly string[]) { + try { + const { stdout } = await executeAsync(command, args); + return stdout; + } catch (error) { + const { stderr = "" } = error as Error & { stderr?: string }; + throw new Error(stderr.trim() || "Command failed"); + } +} + +export function useExec(command: string, args: readonly string[] = []) { + return useQuery({ + queryKey: ["execute", command, args], + queryFn: () => execute(command, args), + }); +} diff --git a/src/typescript/utils/src/index.ts b/src/typescript/utils/src/index.ts new file mode 100644 index 000000000..986e23dca --- /dev/null +++ b/src/typescript/utils/src/index.ts @@ -0,0 +1,3 @@ +export * from "./execute.js"; +export * from "./message.js"; +export * from "./query.js"; diff --git a/src/typescript/utils/src/message.tsx b/src/typescript/utils/src/message.tsx new file mode 100644 index 000000000..3d7d9a770 --- /dev/null +++ b/src/typescript/utils/src/message.tsx @@ -0,0 +1,44 @@ +import { Color, Icon, type ImageLike, List } from "@vicinae/api"; +import type { FC, ReactNode } from "react"; + +/** + * A full-page message for states that block the command's regular content. + * + * @category UI Components + * @public + */ +export namespace Message { + export type Status = "info" | "error"; + + export type Props = { + title: string; + description?: string; + navigationTitle?: string; + status?: Status; + icon?: ImageLike; + actions?: ReactNode; + }; +} + +export const Message: FC = ({ + actions, + icon, + status = "info", + navigationTitle, + ...emptyViewProps +}) => { + const defaultIcon: ImageLike = { + source: status === "error" ? Icon.XMarkCircle : Icon.Info01, + tintColor: status === "error" ? Color.Red : Color.PrimaryText, + }; + + return ( + + + + ); +}; diff --git a/src/typescript/utils/src/query.ts b/src/typescript/utils/src/query.ts new file mode 100644 index 000000000..c16adaf55 --- /dev/null +++ b/src/typescript/utils/src/query.ts @@ -0,0 +1,39 @@ +import { Cache } from "@vicinae/api"; +import { experimental_createQueryPersister } from "@tanstack/query-persist-client-core"; +import { + QueryClient, + type DefaultError, + type QueryKey, + type UseQueryOptions, + useQuery as useTanStackQuery, +} from "@tanstack/react-query"; + +const queryClient = new QueryClient(); +const cache = new Cache({ namespace: "tanstack-query" }); +const { persisterFn } = experimental_createQueryPersister({ + storage: { + getItem: cache.get, + setItem: cache.set, + removeItem: (key) => { + cache.remove(key); + }, + }, +}); + +export function useQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>(options: UseQueryOptions) { + return useTanStackQuery(options, queryClient); +} + +export function usePersistedQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>(options: UseQueryOptions) { + return useTanStackQuery({ ...options, persister: persisterFn }, queryClient); +} diff --git a/src/typescript/utils/tsconfig.json b/src/typescript/utils/tsconfig.json new file mode 100644 index 000000000..5fda26f95 --- /dev/null +++ b/src/typescript/utils/tsconfig.json @@ -0,0 +1,15 @@ +{ + "include": ["src"], + "compilerOptions": { + "jsx": "react-jsx", + "module": "nodenext", + "declaration": true, + "declarationMap": false, + "noEmit": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "types": ["node"], + "skipLibCheck": true + } +}