#!/usr/bin/env bun /** * Publish all @shade/* packages to the Gitea npm registry. * * Expects these env vars: * GITEA_TOKEN — publish token from Gitea (Settings → Applications) * GITEA_USER — Gitea username that owns the registry (e.g. "Stian") * * Optional: * DRY_RUN=1 — build tarballs but don't publish * * Usage: * bun run scripts/publish-all.ts */ import { readFileSync, writeFileSync, existsSync } from 'fs'; import { join } from 'path'; import { $ } from 'bun'; // Order matters: each package only depends on packages above it. Publishing // in this order means a consumer fetching mid-publish never sees a manifest // pointing at an unpublished version. const PACKAGES = [ 'shade-core', 'shade-proto', 'shade-crypto-web', 'shade-storage-sqlite', 'shade-storage-postgres', 'shade-streams', 'shade-transport', 'shade-server', 'shade-transfer', 'shade-files', 'shade-recovery', 'shade-observer', 'shade-sdk', 'shade-widgets', 'shade-cli', ]; const REGISTRY_HOST = 'gt.zyon.no'; const ROOT = join(import.meta.dir, '..'); async function main() { const token = process.env.GITEA_TOKEN; const user = process.env.GITEA_USER ?? 'Stian'; const dryRun = process.env.DRY_RUN === '1'; if (!token && !dryRun) { console.error('GITEA_TOKEN is required (or set DRY_RUN=1)'); process.exit(1); } const registryUrl = `https://${REGISTRY_HOST}/api/packages/${user}/npm/`; console.log(`Target registry: ${registryUrl}`); console.log(`Dry run: ${dryRun ? 'yes' : 'no'}`); console.log(); // Write a temporary .npmrc at the root const npmrcPath = join(ROOT, '.npmrc.publish'); const npmrc = [ `@shade:registry=${registryUrl}`, dryRun ? '' : `//${REGISTRY_HOST}/api/packages/${user}/npm/:_authToken=${token}`, ].filter(Boolean).join('\n'); writeFileSync(npmrcPath, npmrc); // Build a name → version map across all workspace packages so we can rewrite // `workspace:*` (and friends) into concrete `^` specifiers before // publishing. Without this, the registry stores the literal `workspace:*` // string in published package.json, which then fails to resolve in any // consumer (e.g. Dispatch) outside the Shade monorepo. const versionByName = new Map(); for (const pkg of PACKAGES) { const pkgDir = join(ROOT, 'packages', pkg); if (!existsSync(join(pkgDir, 'package.json'))) continue; const json = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf-8')); versionByName.set(json.name, json.version); } let published = 0; let skipped = 0; let alreadyPublished = 0; let failed = 0; for (const pkg of PACKAGES) { const pkgDir = join(ROOT, 'packages', pkg); const pkgJsonPath = join(pkgDir, 'package.json'); if (!existsSync(pkgJsonPath)) { console.log(`⊘ ${pkg} — package.json not found, skipping`); skipped++; continue; } const originalPkgJson = readFileSync(pkgJsonPath, 'utf-8'); const pkgJson = JSON.parse(originalPkgJson); console.log(`→ ${pkgJson.name}@${pkgJson.version}`); rewriteWorkspaceSpecs(pkgJson, versionByName); writeFileSync(pkgJsonPath, `${JSON.stringify(pkgJson, null, 2)}\n`); try { if (dryRun) { await $`cd ${pkgDir} && bun pm pack --dry-run`.quiet(); } else { await $`cd ${pkgDir} && npm publish --registry=${registryUrl} --userconfig ${npmrcPath}`.quiet(); } published++; console.log(` ✓ ${dryRun ? 'packed' : 'published'}`); } catch (err) { const out = `${err instanceof Error ? err.message : String(err)} ${ (err as { stderr?: { toString(): string } }).stderr?.toString() ?? '' } ${(err as { stdout?: { toString(): string } }).stdout?.toString() ?? ''}`; // Gitea (and npm) report already-published versions as 409 / EPUBLISHCONFLICT. // Skip silently rather than failing the whole run — bumping the version // is the user's explicit decision via `bun run version `. if (/409|EPUBLISHCONFLICT|already exists|already been published/i.test(out)) { alreadyPublished++; console.log(` ⊙ already published — skipping`); } else { failed++; console.error(` ✗ failed: ${(err as Error).message}`); process.exitCode = 1; } } finally { // Always restore the original package.json so the workspace stays usable // for `bun install` after publish, regardless of success or failure. writeFileSync(pkgJsonPath, originalPkgJson); } } // Clean up temp npmrc try { await $`rm ${npmrcPath}`.quiet(); } catch {} console.log(); console.log( `Done: ${published} ${dryRun ? 'packed' : 'published'}, ${alreadyPublished} already published, ${skipped} skipped, ${failed} failed`, ); } /** * Rewrite `workspace:*` (and `workspace:^`, `workspace:~`, `workspace:`) * specifiers in dependency sections to concrete `^` specifiers. * Mutates the passed-in object. */ function rewriteWorkspaceSpecs( pkgJson: Record, versionByName: Map, ): void { const sections = ['dependencies', 'peerDependencies', 'optionalDependencies'] as const; for (const section of sections) { const deps = pkgJson[section]; if (!deps || typeof deps !== 'object') continue; for (const [name, spec] of Object.entries(deps as Record)) { if (typeof spec !== 'string' || !spec.startsWith('workspace:')) continue; const version = versionByName.get(name); if (!version) { throw new Error( `No workspace version known for ${name} (referenced from ${pkgJson.name as string}). ` + `Add it to PACKAGES or remove the workspace dependency.`, ); } (deps as Record)[name] = `^${version}`; } } } main().catch((err) => { console.error(err); process.exit(1); });