import { createCli, type TrpcCliMeta } from "trpc-cli"; import { initTRPC } from "@trpc/server"; import { GithubCredentialsManager } from "./credential-manager"; import * as v from "valibot"; import pc from "picocolors"; import { readFile, stat, unlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { x } from "tinyexec"; import { API_URL_BASE, REGISTRY_URL_BASE, GITHUB_CLIENT_ID, CLI_CONFIG_PATH, SAVED_CONFIG, writeCliConfig, } from "./config"; import { inspect } from "node:util"; import { createHash } from "node:crypto"; import { glob } from "glob"; import open from "open"; import * as prompts from "@clack/prompts"; import { execSync, spawn } from "node:child_process"; type PackageJson = { name?: string; version?: string; private?: boolean; dependencies?: Record; devDependencies?: Record; optionalDependencies?: Record; peerDependencies?: Record; [key: string]: any; }; const t = initTRPC.meta().create(); const PackageManager = v.picklist(["pnpm", "bun", "yarn", "npm"]); type PackageManager = v.InferOutput; const router = t.router({ login: t.procedure.mutation(async () => { if (await GithubCredentialsManager.getCredentials()) { prompts.log.info("You are already logged in"); return; } else { await GithubCredentialsManager.login(); } }), logout: t.procedure.mutation(async () => { await GithubCredentialsManager.logout(); }), browserLogin: t.procedure.mutation(async () => { const token = await GithubCredentialsManager.getCredentialsWithGhFallback(); if (!token) { return prompts.log.error( "Please login with `pkgdrop login` (or approve gh credential reuse) before using browser login", ); } const s1 = prompts.spinner(); s1.start("Generating login URL..."); const loginTokenRes = await fetch(`${API_URL_BASE}/token-auth`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ githubToken: token.token, }), }); if (!loginTokenRes.ok) return s1.error("Failed to generate login url: " + (await loginTokenRes.text())); s1.stop("Login URL generated"); const { loginToken } = await loginTokenRes.json(); const url = `${API_URL_BASE}/token-auth?token=${loginToken}`; prompts.outro( `Open this URL in your browser: ${pc.underline(url)}, this will be only valid for 5 minutes`, ); await open(url); }), configure: t.procedure .input( v.tuple([ v.object({ apiUrl: v.optional(v.string()), registryUrl: v.optional(v.string()), githubClientId: v.optional(v.string()), }), ]), ) .mutation(async ({ input }) => { const current = { apiUrl: SAVED_CONFIG.apiUrl || API_URL_BASE, registryUrl: SAVED_CONFIG.registryUrl || REGISTRY_URL_BASE, githubClientId: SAVED_CONFIG.githubClientId || GITHUB_CLIENT_ID, }; const [flags] = input; const apiUrl = flags.apiUrl ?? (await prompts.text({ message: "API base URL", placeholder: current.apiUrl, initialValue: current.apiUrl, })); if (prompts.isCancel(apiUrl)) { prompts.outro(pc.yellow("Configuration cancelled")); return; } const registryUrl = flags.registryUrl ?? (await prompts.text({ message: "Registry base URL", placeholder: current.registryUrl, initialValue: current.registryUrl, })); if (prompts.isCancel(registryUrl)) { prompts.outro(pc.yellow("Configuration cancelled")); return; } const githubClientId = flags.githubClientId ?? (await prompts.text({ message: "GitHub OAuth Client ID", placeholder: current.githubClientId, initialValue: current.githubClientId, })); if (prompts.isCancel(githubClientId)) { prompts.outro(pc.yellow("Configuration cancelled")); return; } const next = writeCliConfig({ apiUrl: String(apiUrl).trim(), registryUrl: String(registryUrl).trim(), githubClientId: String(githubClientId).trim(), }); prompts.note( [ `API URL: ${pc.cyan(next.apiUrl ?? "")}`, `Registry URL: ${pc.cyan(next.registryUrl ?? "")}`, `GitHub Client ID: ${pc.cyan(next.githubClientId ?? "")}`, pc.dim(`Saved to ${CLI_CONFIG_PATH}`), ].join("\n"), "Saved configuration", ); prompts.outro(pc.green("Configuration updated")); }), search: t.procedure .input( v.tuple([ v.pipe(v.optional(v.string(), ""), v.description("query")), v.object({ limit: v.optional( v.pipe( v.union([v.string(), v.number()]), v.transform((value) => Number(value)), v.integer(), ), 20, ), }), ]), ) .mutation(async ({ input }) => { const [query, options] = input; const limit = Math.max(1, Math.min(options.limit, 100)); const res = await fetch(`${API_URL_BASE}/packages?limit=100`); if (!res.ok) { prompts.outro(pc.red(`Failed to search packages: ${res.status} ${res.statusText}`)); return; } const data = (await res.json()) as { packages: Array<{ username: string; org?: string; packageName: string; description?: string; downloads: number; latestVersion?: string; }>; }; const q = query.trim().toLowerCase(); const filtered = data.packages .filter((pkg) => { if (!q) return true; const displayName = pkg.org ? `@${pkg.org}/${pkg.packageName}` : pkg.packageName; const full = `${pkg.username}/${displayName}`.toLowerCase(); const desc = (pkg.description ?? "").toLowerCase(); return full.includes(q) || desc.includes(q); }) .slice(0, limit); if (filtered.length === 0) { prompts.outro(pc.yellow(`No packages found${q ? ` for "${query}"` : ""}`)); return; } const lines = filtered.map((pkg) => { const displayName = pkg.org ? `@${pkg.org}/${pkg.packageName}` : pkg.packageName; const latest = pkg.latestVersion ? pc.dim(`@${pkg.latestVersion}`) : ""; const desc = pkg.description ? pc.dim(` - ${pkg.description}`) : ""; return `${pc.cyan(`${pkg.username}/${displayName}`)}${latest}${desc}`; }); prompts.note( lines.join("\n"), pc.bold(`Found ${filtered.length} package${filtered.length > 1 ? "s" : ""}`), ); prompts.outro(pc.dim("Install with: pkgdrop install /")); }), install: t.procedure .input( v.tuple([ v.pipe(v.string(), v.nonEmpty(), v.description("package")), v.object({ packer: v.optional(PackageManager, detectPackageManager()), }), ]), ) .mutation(async ({ input }) => { const [target, options] = input; const parsed = parseInstallTarget(target); if (!parsed) { prompts.outro( pc.red( "Invalid package target. Use: / or /", ), ); return; } const packageSpec = hasExplicitVersion(parsed.packageSpec) ? parsed.packageSpec : `${parsed.packageSpec}@latest`; const manifestRes = await fetch( `${REGISTRY_URL_BASE}/${parsed.username}/${encodeURIComponent(packageSpec)}`, ); if (!manifestRes.ok) { prompts.outro( pc.red(`Failed to resolve package: ${manifestRes.status} ${manifestRes.statusText}`), ); return; } const manifest = (await manifestRes.json()) as { name?: string; version?: string; dist?: { tarball?: string }; }; const tarballUrl = manifest.dist?.tarball; if (!tarballUrl) { prompts.outro(pc.red("Registry response missing dist.tarball")); return; } const cmd = packageManagerInstallArgs(options.packer, tarballUrl); prompts.log.info( pc.bold( `Installing ${pc.cyan(manifest.name ?? parsed.packageSpec)}@${pc.green(manifest.version ?? "unknown")}`, ), ); await x(cmd.bin, cmd.args, { nodeOptions: { cwd: process.cwd(), stdio: "inherit", }, }); prompts.outro(pc.green("Install complete")); }), // Mostly based on pkg.pr.new's publish command publish: t.procedure .input( v.tuple([ v.pipe(v.optional(v.array(v.string()), []), v.description("paths")), v.object({ packer: v.optional(PackageManager, detectPackageManager()), version: v.optional(v.string()), tag: v.optional(v.string(), "latest"), }), ]), ) .mutation(async ({ input }) => { prompts.intro(pc.bold(pc.bgBlueBright(pc.black(" pkgdrop publish ")))); const credentials = await GithubCredentialsManager.getCredentialsWithGhFallback(); if (!credentials) { prompts.log.error( "Please login with `pkgdrop login` (or approve gh credential reuse) before publishing packages.", ); prompts.outro(pc.red("Authentication required")); return; } // Expand paths using glob to handle directory patterns const s1 = prompts.spinner(); s1.start("Scanning for packages..."); const paths = input[0]?.length > 0 ? ( await Promise.all( input[0].map((pattern) => glob(pattern, { withFileTypes: false, absolute: true, }), ), ) ) .flat() .filter((p) => p) // Filter out any empty results : [process.cwd()]; const publishingVersion = input[1].version ?? String(Math.floor(Date.now() / 1000)); const username = await GithubCredentialsManager.getUsername(); // PASS 1: Read all package.json files and build dependency map const deps = new Map(); const publishTargets = new Map(); const packageInfos: Array<{ path: string; pJson: PackageJson }> = []; for (const p of paths) { const pJsonPath = join(p, "package.json"); const pJson = await readPackageJson(pJsonPath); if (!pJson) { prompts.log.warn(`Skipping ${p}: package.json not found`); continue; } if (!pJson.name) { prompts.log.warn(`Skipping ${p}: package name not defined`); continue; } if (pJson.private) { prompts.log.warn(`Skipping ${p}: package is private`); continue; } if (!pJson.version) { prompts.log.warn(`Skipping ${p}: package version not defined`); continue; } packageInfos.push({ path: p, pJson }); const publishSpec = `${pJson.name}@${publishingVersion}`; const publishUrl = `${REGISTRY_URL_BASE}/${username}/${encodeURIComponent(publishSpec)}`; const dependencyTarballUrl = buildTarballUrl( REGISTRY_URL_BASE, username, pJson.name, publishingVersion, ); deps.set(pJson.name, dependencyTarballUrl); publishTargets.set(pJson.name, publishUrl); } s1.stop("Package scan complete"); if (packageInfos.length === 0) { prompts.log.error("No valid packages found to publish"); prompts.outro(pc.red("No packages to publish")); return; } prompts.note( [ pc.bold(`Version: ${pc.green(publishingVersion)}`), pc.bold(`Tag: ${pc.yellow(input[1].tag)}`), pc.bold(`Package Manager: ${pc.blue(input[1].packer)}`), "", pc.bold("Packages to publish:"), ...packageInfos.map( (info) => ` ${pc.cyan(info.pJson.name!)} ${pc.dim(`v${info.pJson.version}`)}`, ), ].join("\n"), ); // PASS 2: Modify package.json files to replace workspace dependencies const restoreMap = new Map Promise>(); for (const { path: p, pJson } of packageInfos) { const pJsonPath = join(p, "package.json"); const originalContents = await readFile(pJsonPath, "utf-8"); const restore = await writeDeps(pJsonPath, originalContents, pJson, deps); restoreMap.set(p, restore); } // PASS 3: Pack all packages and collect results const packedPackages: Array<{ path: string; pJson: PackageJson; packageIdentifier: string; packResult: { filename: string; file: Buffer; sha256: string; size: number; output: string; }; }> = []; for (const { path: p, pJson } of packageInfos) { const packageIdentifier = `${pJson .name!.replace("@", "") .replace("/", "-")}-${pJson.version}`; const packResult = await pack({ packageManager: input[1].packer, cwd: p, packageIdentifier, }); packedPackages.push({ path: p, pJson, packageIdentifier, packResult, }); } // PASS 4: Restore all package.json files for (const restore of restoreMap.values()) { await restore(); } // PASS 5: Upload all packed packages const uploadResults: Array<{ pJson: PackageJson; tarballUrl: string; status: "success" | "exists" | "error"; errorMessage?: string; }> = []; await prompts.tasks( packedPackages.map(({ pJson, packResult }) => ({ title: pc.bold(`Publishing ${pc.cyan(pJson.name!)}@${pc.green(publishingVersion)}`), task: async () => { const publishUrl = publishTargets.get(pJson.name!)!; const tarballUrl = deps.get(pJson.name!)!; try { const form = new FormData(); form.append("tarball", new File([packResult.file], packResult.filename)); form.append("sha256", packResult.sha256); form.append("tag", input[1].tag); form.append("customVersion", String(Boolean(input[1].version))); const uploadRes = await fetch(publishUrl, { method: "POST", body: form, headers: { Authorization: `Bearer ${credentials.token}`, }, }); const response = v.safeParse( v.pipe( v.string(), v.parseJson(), v.union([ v.looseObject({ message: v.string(), }), v.looseObject({ error: v.string(), }), v.looseObject({ error: v.string(), sameAsLatest: v.boolean(), latestVersion: v.string(), sha256: v.string(), previousVersionUrl: v.string(), }), ]), ), await uploadRes.text(), ); if (!response.success) { const errorDetails = inspect(v.flatten(response.issues), { depth: null, colors: false, }); uploadResults.push({ pJson, tarballUrl, status: "error", errorMessage: `Failed to parse response: ${errorDetails}`, }); return pc.bold(pc.red("Failed to parse response")); } if (uploadRes.status === 409) { if ("sameAsLatest" in response.output && response.output.sameAsLatest) { const previousUrl = typeof response.output.previousVersionUrl === "string" ? response.output.previousVersionUrl : tarballUrl; const latestVersion = typeof response.output.latestVersion === "string" ? response.output.latestVersion : "latest"; uploadResults.push({ pJson, tarballUrl: previousUrl, status: "exists", }); return pc.bold(pc.dim(`Same as latest version ${latestVersion} (skipped)`)); } if ("sha256" in response.output) { if (response.output.sha256 === packResult.sha256) { uploadResults.push({ pJson, tarballUrl, status: "exists" }); return pc.bold(pc.dim("Package already exists")); } else { uploadResults.push({ pJson, tarballUrl, status: "error", errorMessage: `Same version exists with different SHA-256 checksum\nExpected: ${response.output.sha256}\nActual: ${packResult.sha256}`, }); return pc.bold(pc.red("Version conflict")); } } } if (!uploadRes.ok) { const errorDetails = inspect(response.output, { depth: null, colors: false, }); uploadResults.push({ pJson, tarballUrl, status: "error", errorMessage: errorDetails, }); return pc.bold(pc.red(`Upload failed: ${uploadRes.statusText}`)); } uploadResults.push({ pJson, tarballUrl, status: "success" }); return pc.bold(pc.green(`Published ${pJson.name!}`)); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); uploadResults.push({ pJson, tarballUrl, status: "error", errorMessage, }); return pc.bold(pc.red(errorMessage)); } }, })), ); // Display any error details const errorUploads = uploadResults.filter((r) => r.status === "error"); if (errorUploads.length > 0) { prompts.log.info(""); for (const { pJson, errorMessage } of errorUploads) { if (errorMessage) { prompts.log.error(`${pJson.name!}:`); prompts.log.info(errorMessage); } } } const successfulUploads = uploadResults.filter((r) => r.status === "success"); const existingPackages = uploadResults.filter((r) => r.status === "exists"); if (successfulUploads.length > 0 || existingPackages.length > 0) { prompts.log.info(pc.bold("Installation Commands:")); prompts.log.success( successfulUploads .concat(existingPackages) .map((r) => formatInstallationCommand(r.tarballUrl, input[1].packer)) .join("\n"), ); } if (successfulUploads.length > 0) { prompts.outro( pc.green( `✅ Successfully published ${successfulUploads.length} package${ successfulUploads.length > 1 ? "s" : "" }!`, ), ); } else if (existingPackages.length > 0) { prompts.outro(pc.yellow(`All packages already exist with the same content`)); } else { prompts.outro(pc.red("No packages were published")); } }), remove: t.procedure .input( v.tuple([ v.pipe(v.string(), v.nonEmpty(), v.description("package")), v.object({ yes: v.optional(v.boolean(), false), }), ]), ) .mutation(async ({ input }) => { const [packageSpec, options] = input; const credentials = await GithubCredentialsManager.getCredentialsWithGhFallback(); if (!credentials) { prompts.log.error( "Please login with `pkgdrop login` (or approve gh credential reuse) before removing packages.", ); return; } const username = await GithubCredentialsManager.getUsername(); const removingVersion = hasExplicitVersion(packageSpec); if (!options.yes) { const action = removingVersion ? "version" : "package"; const confirmation = await prompts.confirm({ message: `Delete ${action} ${pc.cyan(packageSpec)} from ${pc.cyan(username)}?`, initialValue: false, }); if (!confirmation || prompts.isCancel(confirmation)) { prompts.outro(pc.yellow("Deletion cancelled")); return; } } const res = await fetch( `${API_URL_BASE}/manage/${username}/${encodeURIComponent(packageSpec)}`, { method: "DELETE", headers: { Authorization: `Bearer ${credentials.token}`, }, }, ); if (!res.ok) { const errorText = await res.text(); prompts.outro(pc.red(`Failed to delete: ${errorText}`)); return; } const body = (await res.json()) as { deleted?: { type?: string; version?: string; packageDeleted?: boolean }; }; if (body.deleted?.type === "version") { const suffix = body.deleted.packageDeleted ? " (package removed, no versions left)" : ""; prompts.outro(pc.green(`Deleted version ${body.deleted.version}${suffix}`)); return; } prompts.outro(pc.green(`Deleted package ${packageSpec}`)); }), ...(process.env.USE_MOCK_GITHUB_AUTH === "true" ? { mockLogin: t.procedure .input( v.object({ username: v.string(), }), ) .mutation(async ({ input }) => { if (process.env.USE_MOCK_GITHUB_AUTH !== "true") { prompts.log.error( "Mock auth is disabled. Set USE_MOCK_GITHUB_AUTH=true to enable it.", ); return; } await GithubCredentialsManager.mockLogin(input.username); }), } : {}), ...(process.env.INCLUDE_WEB_BUNDLE === "true" ? { start: t.procedure.mutation(async () => { const webBundle = join(import.meta.dirname, "web"); console.log(`Starting web bundle from ${webBundle}`); const pm = detectPackageManager(); console.log(`Using ${pm} to install dependencies`); execSync(`${pm} install`, { cwd: join(webBundle, "server"), stdio: "inherit" }); const server = spawn("node", [join(webBundle, "server/index.mjs")], { stdio: "inherit", }); await new Promise((resolve) => server.on("exit", resolve)); process.exit(0); }), } : {}), }); type PackOptions = { packageManager: PackageManager; cwd: string; packageIdentifier: string; keepFile?: boolean; }; async function pack(options: PackOptions) { const { packageManager, cwd, packageIdentifier, keepFile = false } = options; const packArgs = ["pack"]; // Bun & Yarn, the problem children if (packageManager === "bun") packArgs.unshift("pm"); if (packageManager === "yarn") packArgs.push("--filename", `${packageIdentifier}.tgz`); const res = await x(packageManager, packArgs, { nodeOptions: { cwd, stdio: "inherit" }, }); const output = (res.stdout + res.stderr).trim(); if (res.exitCode !== 0) { throw new Error(`Failed to pack ${cwd} with ${packageManager}${output ? `:\n${output}` : ""}`); } const filename = join(cwd, `${packageIdentifier}.tgz`); const file = await readFile(filename).catch(() => null); const stats = await stat(filename); if (!file) { throw new Error( `Pack command returned success but no output file was found, this is likely a bug`, ); } const sha256 = createHash("sha256").update(file).digest("hex"); // Cleanup the file once we have everything we need if (!keepFile) await unlink(filename); return { filename, file, sha256, size: stats.size, output }; } async function readPackageJson(path: string): Promise { try { const contents = await readFile(path, "utf-8"); return JSON.parse(contents) as PackageJson; } catch { return null; } } async function writeDeps( pJsonPath: string, originalContents: string, pJson: PackageJson, deps: Map, ) { // Hijack dependencies to point to our published URLs hijackDeps(deps, pJson.dependencies); hijackDeps(deps, pJson.devDependencies); hijackDeps(deps, pJson.optionalDependencies); // Write the modified package.json await writeFile(pJsonPath, JSON.stringify(pJson, null, 2)); // Return a restore function that restores the original contents return () => writeFile(pJsonPath, originalContents); } function hijackDeps(newDeps: Map, oldDeps?: Record) { if (!oldDeps) { return; } for (const [newDep, url] of newDeps) { if (newDep in oldDeps) { oldDeps[newDep] = url; } } } function detectPackageManager(): PackageManager { const packageManager = process.env.npm_config_user_agent; if (packageManager?.includes("pnpm")) return "pnpm"; if (packageManager?.includes("bun")) return "bun"; if (packageManager?.includes("yarn")) return "yarn"; return "npm"; } function formatInstallationCommand(packageUrl: string, packageManager: PackageManager) { const tarballUrl = packageUrl; return [ pc.green(packageManager), pc.green(packageManager === "yarn" ? "add" : "install"), pc.underline(pc.blue(tarballUrl)), ].join(" "); } function buildTarballUrl( registryBaseUrl: string, username: string, packageName: string, version: string, ) { if (packageName.startsWith("@")) { const slashIndex = packageName.indexOf("/"); const scope = packageName.slice(0, slashIndex); const name = packageName.slice(slashIndex + 1); return `${registryBaseUrl}/${username}/${scope}/${name}/-/${name}-${version}.tgz`; } return `${registryBaseUrl}/${username}/${packageName}/-/${packageName}-${version}.tgz`; } function hasExplicitVersion(spec: string) { if (spec.startsWith("@")) { const slashIndex = spec.indexOf("/"); if (slashIndex === -1) return false; return spec.slice(slashIndex + 1).includes("@"); } return spec.includes("@"); } function parseInstallTarget(target: string): { username: string; packageSpec: string } | null { const firstSlash = target.indexOf("/"); if (firstSlash <= 0 || firstSlash === target.length - 1) return null; const username = target.slice(0, firstSlash).trim(); const packageSpec = target.slice(firstSlash + 1).trim(); if (!/^[a-zA-Z0-9-]{1,39}$/.test(username)) return null; if (!packageSpec) return null; return { username, packageSpec }; } function packageManagerInstallArgs(packageManager: PackageManager, tarballUrl: string) { if (packageManager === "yarn") { return { bin: "yarn", args: ["add", tarballUrl] }; } if (packageManager === "npm") { return { bin: "npm", args: ["install", tarballUrl] }; } if (packageManager === "bun") { return { bin: "bun", args: ["add", tarballUrl] }; } return { bin: "pnpm", args: ["add", tarballUrl] }; } createCli({ router }).run({ formatError(error) { return pc.bold(pc.red(error instanceof Error ? error.message : String(error))); }, });