#!/usr/bin/env bun /** * Headless publisher for all `@shade/*` packages. * * Use `scripts/publish-shade.sh` for the interactive human flow (token * prompt, conflict detection, version bump-on-conflict). This script is * the env-driven variant — designed for `DRY_RUN=1` smoke tests, CI * pipelines, and any context where prompts are not appropriate. * * Required env (when not DRY_RUN): * GITEA_TOKEN — publish token from Gitea (Settings → Applications) * GITEA_USER — Gitea username that owns the registry (default: Stian) * * Optional: * DRY_RUN=1 — pack tarballs but do not publish (no token required) */ import { readFileSync, writeFileSync, existsSync } from 'fs'; import { join } from 'path'; import { $ } from 'bun'; // Order matters: each package only depends on packages above it. // Mirrors the PACKAGES list in scripts/publish-shade.sh. const PACKAGES = [ 'shade-core', 'shade-proto', 'shade-crypto-web', 'shade-observability', 'shade-keychain', 'shade-key-transparency', 'shade-storage-sqlite', 'shade-storage-postgres', 'shade-storage-indexeddb', 'shade-storage-encrypted', 'shade-streams', 'shade-transport', 'shade-transport-bridge', 'shade-transport-webrtc', 'shade-server', 'shade-inbox-server', 'shade-inbox', 'shade-transfer', 'shade-files', 'shade-recovery', 'shade-observer', 'shade-dashboard', '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(); 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); 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() ?? ''}`; 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 { writeFileSync(pkgJsonPath, originalPkgJson); } } try { await $`rm ${npmrcPath}`.quiet(); } catch {} console.log(); console.log( `Done: ${published} ${dryRun ? 'packed' : 'published'}, ${alreadyPublished} already published, ${skipped} skipped, ${failed} failed`, ); } 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); });