From d9f29d317bdcc2f44d349675e5e10003ee435f9d Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Thu, 23 Jul 2026 17:33:40 -0400 Subject: [PATCH 1/3] feat(shadcn): add scripts to publish registry updates downstream Adds maintainer scripts to sync the @agents-ui shadcn registry into livekit-examples/agent-starter-react and livekit/web via PRs, so a registry rebuild doesn't require manually cloning, branching, and opening PRs against both downstream repos by hand. - publish-agent-starter-react.ts / publish-livekit-web.ts / publish-downstream.ts: clone each repo into a tmp dir, point it at a freshly built local registry, run the existing install/update mechanism, and open a PR only after the user confirms a printed summary (repo, branch, title, body, diffstat) with a y/n prompt - Rename registry:* package scripts to shadcn:* for consistency (registry:update -> shadcn:deploy, to avoid colliding with the existing shadcn:update script) - Add build:deps/pretest (packages/shadcn) and build:deps/predev (docs/storybook) hooks so workspace dependencies are always built before tests or the storybook dev server run - Harden scripts/update.ts to use fs APIs instead of shelling out, and guard against cleaning an unsafe DEST_REGISTRY_PATH - Add a root-level pnpm shadcn:publish:all script wired through turbo Co-Authored-By: Claude Sonnet 5 --- docs/storybook/package.json | 2 + package.json | 1 + packages/shadcn/README.md | 25 ++- packages/shadcn/package.json | 13 +- packages/shadcn/scripts/lib/publish-utils.ts | 192 ++++++++++++++++++ .../scripts/publish-agent-starter-react.ts | 134 ++++++++++++ packages/shadcn/scripts/publish-downstream.ts | 47 +++++ .../shadcn/scripts/publish-livekit-web.ts | 104 ++++++++++ packages/shadcn/scripts/update.ts | 54 +++-- turbo.json | 5 +- 10 files changed, 554 insertions(+), 23 deletions(-) create mode 100644 packages/shadcn/scripts/lib/publish-utils.ts create mode 100644 packages/shadcn/scripts/publish-agent-starter-react.ts create mode 100644 packages/shadcn/scripts/publish-downstream.ts create mode 100644 packages/shadcn/scripts/publish-livekit-web.ts diff --git a/docs/storybook/package.json b/docs/storybook/package.json index 9087cb986..e9ef91c25 100644 --- a/docs/storybook/package.json +++ b/docs/storybook/package.json @@ -5,6 +5,8 @@ "license": "Apache-2.0", "main": "index.js", "scripts": { + "build:deps": "pnpm --filter @livekit/component-docs-storybook^... build", + "predev": "pnpm build:deps", "build": "storybook build", "clean": "rm -rf .turbo && rm -rf node_modules", "dev": "storybook dev -p 6006", diff --git a/package.json b/package.json index 320f3fe3a..01959345f 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "gen:docs:watch": "nodemon --watch \"tooling/api-documenter/src/**/*\" --watch \"packages/react/src/**/*\" --ignore \"packages/react/src/assets/**/*\" -e js,jsx,ts,tsx -x \"pnpm gen:docs\"", "lint": "turbo run lint -- --quiet", "preinstall": "npx only-allow pnpm", + "shadcn:publish:all": "turbo run shadcn:publish:all --filter=@livekit/agents-ui...", "ci:publish": "turbo run build --filter=./packages/* && pnpm gen:docs && changeset publish", "start:next": "turbo run start --filter=@livekit/component-example-next...", "test": "turbo run test", diff --git a/packages/shadcn/README.md b/packages/shadcn/README.md index a9b7d772a..4bbaeffc9 100644 --- a/packages/shadcn/README.md +++ b/packages/shadcn/README.md @@ -144,16 +144,33 @@ You can find the components in [Storybook](http://localhost:6006) under the `Age ```bash # Build the shadcn registry -pnpm registry:build +pnpm shadcn:build # Serve the registry locally (http://localhost:3210) -pnpm registry:serve +pnpm shadcn:serve # Generate prop documentation -pnpm registry:doc-gen +pnpm shadcn:doc-gen # Build and deploy to configured destination paths -pnpm registry:update +pnpm shadcn:deploy +``` + +### Publishing downstream updates + +These scripts are maintainer-only release helpers. They require an authenticated `gh` +CLI session with access to the target repositories, push branches to those +repositories, and open GitHub PRs. + +```bash +# Sync the registry into livekit-examples/agent-starter-react +pnpm shadcn:publish:agent-starter-react + +# Sync the hosted registry and prop docs into livekit/web +pnpm shadcn:publish:livekit-web + +# Run both downstream publish scripts +pnpm shadcn:publish:all ``` ### Environment Variables diff --git a/packages/shadcn/package.json b/packages/shadcn/package.json index 2d5ad653a..802484851 100644 --- a/packages/shadcn/package.json +++ b/packages/shadcn/package.json @@ -6,10 +6,15 @@ "type": "module", "main": "index.ts", "scripts": { - "registry:build": "pnpm test && rm -rf dist && shadcn build --output ./dist/r && pnpm registry:doc-gen", - "registry:serve": "python3 -m http.server 3210 -d ./dist", - "registry:doc-gen": "node --experimental-strip-types --env-file=.env.local ./scripts/doc-gen.ts", - "registry:update": "pnpm registry:build && node --experimental-strip-types --env-file=.env.local ./scripts/update.ts", + "build:deps": "pnpm --filter @livekit/agents-ui^... build", + "pretest": "pnpm build:deps", + "shadcn:build": "pnpm test && rm -rf dist && shadcn build --output ./dist/r && pnpm shadcn:doc-gen", + "shadcn:serve": "pnpm shadcn:build && python3 -m http.server 3210 -d ./dist", + "shadcn:doc-gen": "node --experimental-strip-types --env-file=.env.local ./scripts/doc-gen.ts", + "shadcn:deploy": "pnpm shadcn:build && node --experimental-strip-types --env-file=.env.local ./scripts/update.ts", + "shadcn:publish:agent-starter-react": "node --experimental-strip-types ./scripts/publish-agent-starter-react.ts", + "shadcn:publish:livekit-web": "node --experimental-strip-types ./scripts/publish-livekit-web.ts", + "shadcn:publish:all": "node --experimental-strip-types ./scripts/publish-downstream.ts", "shadcn:update": "pnpm dlx shadcn@latest add alert button button-group select separator sonner toggle tooltip message-scroller message bubble && pnpm format", "test": "vitest --run", "test:watch": "vitest", diff --git a/packages/shadcn/scripts/lib/publish-utils.ts b/packages/shadcn/scripts/lib/publish-utils.ts new file mode 100644 index 000000000..dbc390e84 --- /dev/null +++ b/packages/shadcn/scripts/lib/publish-utils.ts @@ -0,0 +1,192 @@ +import { execFileSync, type ChildProcess } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import readline from 'node:readline/promises'; + +export function run(cmd: string[], opts: { cwd?: string } = {}): void { + execFileSync(cmd[0], cmd.slice(1), { cwd: opts.cwd, stdio: 'inherit' }); +} + +export function runCapture(cmd: string[], opts: { cwd?: string } = {}): string { + return execFileSync(cmd[0], cmd.slice(1), { cwd: opts.cwd, stdio: ['ignore', 'pipe', 'pipe'] }) + .toString() + .trim(); +} + +export function mkTempClone(prefix: string): string { + return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); +} + +export function hasDiff(cwd: string): boolean { + return runCapture(['git', 'status', '--porcelain'], { cwd }).length > 0; +} + +export function ghAuthPreflight(): void { + try { + execFileSync('gh', ['auth', 'status'], { stdio: 'ignore' }); + } catch { + throw new Error('gh CLI is not authenticated. Run `gh auth login` and try again.'); + } +} + +export function isPortInUse(port: number): boolean { + try { + const pids = execFileSync('lsof', ['-ti', `tcp:${port}`], { + stdio: ['ignore', 'pipe', 'ignore'], + }) + .toString() + .trim(); + return pids.length > 0; + } catch { + return false; + } +} + +export async function waitForHttp(url: string, timeoutMs = 10_000): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + try { + const res = await fetch(url); + if (res.ok) return; + } catch { + // server not up yet + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`Timed out waiting for ${url} to respond`); +} + +export function branchName(prefix: string): string { + const timestamp = new Date() + .toISOString() + .replace(/[-:]/g, '') + .replace(/\..+/, '') + .replace('T', '-'); + return `${prefix}-${timestamp}`; +} + +export function ghPrCreate(opts: { + repo: string; + cwd: string; + title: string; + body: string; + head: string; + base: string; +}): string { + return runCapture( + [ + 'gh', + 'pr', + 'create', + '--repo', + opts.repo, + '--title', + opts.title, + '--body', + opts.body, + '--head', + opts.head, + '--base', + opts.base, + ], + { cwd: opts.cwd }, + ); +} + +export function killServer(server: ChildProcess | undefined): void { + if (server && !server.killed) { + server.kill(); + } +} + +export function removeTempDir(dir: string | undefined): void { + if (dir) { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +export async function promptYesNo(question: string): Promise { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + try { + const answer = await rl.question(`${question} (y/N): `); + return ['y', 'yes'].includes(answer.trim().toLowerCase()); + } finally { + rl.close(); + } +} + +export function cloneAndCreateBranch( + repo: string, + tmpDirPrefix: string, + branchPrefix: string, +): { tmpDir: string; branch: string } { + const tmpDir = mkTempClone(tmpDirPrefix); + console.log('--------------------------------'); + console.log(`Cloning ${repo} into ${tmpDir}`); + run(['gh', 'repo', 'clone', repo, tmpDir]); + run(['git', 'fetch', 'origin', 'main'], { cwd: tmpDir }); + run(['git', 'reset', '--hard', 'origin/main'], { cwd: tmpDir }); + + const branch = branchName(branchPrefix); + run(['git', 'checkout', '-b', branch], { cwd: tmpDir }); + + return { tmpDir, branch }; +} + +async function confirmPrCreation(opts: { + repo: string; + cwd: string; + branch: string; + base: string; + title: string; + body: string; +}): Promise { + const diffStat = runCapture(['git', 'diff', '--stat'], { cwd: opts.cwd }); + + console.log('--------------------------------'); + console.log('About to open a pull request:'); + console.log(` Repo: ${opts.repo}`); + console.log(` Branch: ${opts.branch} -> ${opts.base}`); + console.log(` Title: ${opts.title}`); + console.log(' Body:'); + for (const line of opts.body.split('\n')) console.log(` ${line}`); + console.log(' Changes:'); + for (const line of diffStat.split('\n')) console.log(` ${line}`); + console.log('--------------------------------'); + + return promptYesNo('Create this PR?'); +} + +export async function commitPushAndOpenPr(opts: { + repo: string; + cwd: string; + branch: string; + base: string; + title: string; + body: string; + commitMessage: string; +}): Promise { + const confirmed = await confirmPrCreation(opts); + if (!confirmed) { + console.log('PR creation cancelled by user'); + return undefined; + } + + run(['git', 'add', '-A'], { cwd: opts.cwd }); + run(['git', 'commit', '-m', opts.commitMessage], { cwd: opts.cwd }); + run(['git', 'push', '-u', 'origin', opts.branch], { cwd: opts.cwd }); + + const prUrl = ghPrCreate({ + repo: opts.repo, + cwd: opts.cwd, + title: opts.title, + body: opts.body, + head: opts.branch, + base: opts.base, + }); + + console.log('--------------------------------'); + console.log(`Opened PR: ${prUrl}`); + return prUrl; +} diff --git a/packages/shadcn/scripts/publish-agent-starter-react.ts b/packages/shadcn/scripts/publish-agent-starter-react.ts new file mode 100644 index 000000000..25e3d3b15 --- /dev/null +++ b/packages/shadcn/scripts/publish-agent-starter-react.ts @@ -0,0 +1,134 @@ +import { spawn, type ChildProcess } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { + cloneAndCreateBranch, + commitPushAndOpenPr, + ghAuthPreflight, + hasDiff, + isPortInUse, + killServer, + removeTempDir, + run, + runCapture, + waitForHttp, +} from './lib/publish-utils.ts'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SHADCN_PKG_DIR = path.join(__dirname, '..'); +const REPO = 'livekit-examples/agent-starter-react'; +const PORT = 3210; +const REGISTRY_URL = `http://localhost:${PORT}/r/{name}.json`; +const BRANCH_PREFIX = 'chore/update-agents-ui-registry'; +const COMMIT_MESSAGE = 'chore: sync agents-ui components from livekit/components-js'; +const PR_TITLE = 'chore: sync agents-ui components'; + +function buildRegistry(): void { + console.log('--------------------------------'); + console.log('Building registry'); + run(['pnpm', 'shadcn:build'], { cwd: SHADCN_PKG_DIR }); +} + +async function serveRegistry(): Promise { + console.log('--------------------------------'); + console.log(`Serving registry on port ${PORT}`); + const server = spawn('python3', ['-m', 'http.server', String(PORT), '-d', './dist'], { + cwd: SHADCN_PKG_DIR, + stdio: 'ignore', + }); + await waitForHttp(`http://localhost:${PORT}/r/registry.json`); + return server; +} + +function pointComponentsJsonAtLocalRegistry(tmpDir: string): void { + console.log('--------------------------------'); + console.log('Pointing components.json at the local registry'); + const componentsJsonPath = path.join(tmpDir, 'components.json'); + const componentsJson = JSON.parse(fs.readFileSync(componentsJsonPath, 'utf-8')); + componentsJson.registries['@agents-ui'] = REGISTRY_URL; + fs.writeFileSync(componentsJsonPath, JSON.stringify(componentsJson, null, 2) + '\n'); +} + +function installComponents(tmpDir: string): void { + console.log('--------------------------------'); + console.log('Installing dependencies'); + run(['pnpm', 'install'], { cwd: tmpDir }); + + console.log('--------------------------------'); + console.log('Running shadcn:install'); + run(['pnpm', 'shadcn:install'], { cwd: tmpDir }); +} + +function revertComponentsJson(tmpDir: string): void { + console.log('--------------------------------'); + console.log('Reverting temporary components.json change'); + run(['git', 'checkout', '--', 'components.json'], { cwd: tmpDir }); +} + +function buildPrBody(sourceSha: string): string { + return `Syncs \`@agents-ui\` components from the latest build of [livekit/components-js](https://github.com/livekit/components-js) at ${sourceSha}.\n\nGenerated by \`packages/shadcn/scripts/publish-agent-starter-react.ts\`.`; +} + +export async function publishAgentStarterReact(): Promise<{ + success: boolean; + skipped?: boolean; + prUrl?: string; +}> { + ghAuthPreflight(); + if (isPortInUse(PORT)) { + throw new Error( + `Port ${PORT} is already in use. Stop any running shadcn:serve process and try again.`, + ); + } + + let server: ChildProcess | undefined; + let tmpDir: string | undefined; + + try { + buildRegistry(); + server = await serveRegistry(); + + const cloned = cloneAndCreateBranch(REPO, 'agent-starter-react-', BRANCH_PREFIX); + tmpDir = cloned.tmpDir; + + pointComponentsJsonAtLocalRegistry(tmpDir); + installComponents(tmpDir); + revertComponentsJson(tmpDir); + + if (!hasDiff(tmpDir)) { + console.log('No changes after sync — skipping PR'); + return { success: true, skipped: true }; + } + + const sourceSha = runCapture(['git', 'rev-parse', 'HEAD'], { cwd: SHADCN_PKG_DIR }); + const prUrl = await commitPushAndOpenPr({ + repo: REPO, + cwd: tmpDir, + branch: cloned.branch, + base: 'main', + title: PR_TITLE, + body: buildPrBody(sourceSha), + commitMessage: COMMIT_MESSAGE, + }); + + if (!prUrl) { + return { success: true, skipped: true }; + } + return { success: true, prUrl }; + } finally { + killServer(server); + removeTempDir(tmpDir); + } +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { + publishAgentStarterReact() + .then((result) => { + console.log('Done', result); + }) + .catch((err) => { + console.error(err); + process.exitCode = 1; + }); +} diff --git a/packages/shadcn/scripts/publish-downstream.ts b/packages/shadcn/scripts/publish-downstream.ts new file mode 100644 index 000000000..24c4d4f3c --- /dev/null +++ b/packages/shadcn/scripts/publish-downstream.ts @@ -0,0 +1,47 @@ +import { pathToFileURL } from 'node:url'; +import { publishAgentStarterReact } from './publish-agent-starter-react.ts'; +import { publishLivekitWeb } from './publish-livekit-web.ts'; + +type Result = { name: string; success: boolean; skipped?: boolean; prUrl?: string; error?: string }; + +async function runAgentStarterReact(): Promise { + try { + const result = await publishAgentStarterReact(); + return { name: 'agent-starter-react', ...result }; + } catch (err) { + return { name: 'agent-starter-react', success: false, error: String(err) }; + } +} + +async function runLivekitWeb(): Promise { + try { + const result = await publishLivekitWeb(); + return { name: 'livekit-web', ...result }; + } catch (err) { + return { name: 'livekit-web', success: false, error: String(err) }; + } +} + +function printSummary(results: Result[]): void { + console.log('--------------------------------'); + console.log('Summary'); + for (const result of results) { + const status = !result.success ? '❌' : result.skipped ? '⏭ skipped' : '✅'; + console.log( + `${status} ${result.name}${result.prUrl ? ` — ${result.prUrl}` : ''}${result.error ? ` — ${result.error}` : ''}`, + ); + } +} + +async function publishDownstream(): Promise { + const results: Result[] = []; + results.push(await runAgentStarterReact()); + results.push(await runLivekitWeb()); + return results; +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { + const results = await publishDownstream(); + printSummary(results); + process.exitCode = results.some((r) => !r.success) ? 1 : 0; +} diff --git a/packages/shadcn/scripts/publish-livekit-web.ts b/packages/shadcn/scripts/publish-livekit-web.ts new file mode 100644 index 000000000..948ca5049 --- /dev/null +++ b/packages/shadcn/scripts/publish-livekit-web.ts @@ -0,0 +1,104 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { + cloneAndCreateBranch, + commitPushAndOpenPr, + ghAuthPreflight, + hasDiff, + removeTempDir, + run, + runCapture, +} from './lib/publish-utils.ts'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SHADCN_PKG_DIR = path.join(__dirname, '..'); +const ENV_LOCAL_PATH = path.join(SHADCN_PKG_DIR, '.env.local'); +const REPO = 'livekit/web'; +const BRANCH_PREFIX = 'chore/update-agents-ui-registry'; +const COMMIT_MESSAGE = 'chore(agents-ui): update registry + prop-types from @livekit/agents-ui'; +const PR_TITLE = 'chore(agents-ui): update registry + prop-types'; + +function writeTempEnvLocal(tmpDir: string): void { + console.log('--------------------------------'); + console.log('Pointing .env.local at the tmp livekit/web checkout'); + const destRegistryPath = path.join(tmpDir, 'apps/www/agents-ui-registry'); + const destPropTypesPath = path.join(tmpDir, 'apps/docs/lib/shadcn/prop-types.json'); + fs.writeFileSync( + ENV_LOCAL_PATH, + `DEST_REGISTRY_PATH=${destRegistryPath}\nDEST_PROP_TYPES_PATH=${destPropTypesPath}\n`, + ); +} + +function deployRegistry(): void { + console.log('--------------------------------'); + console.log('Running shadcn:deploy'); + run(['pnpm', 'shadcn:deploy'], { cwd: SHADCN_PKG_DIR }); +} + +function restoreEnvLocal(hadEnvLocal: boolean, envBackup: string | null): void { + if (hadEnvLocal && envBackup !== null) { + fs.writeFileSync(ENV_LOCAL_PATH, envBackup); + } else { + fs.rmSync(ENV_LOCAL_PATH, { force: true }); + } +} + +function buildPrBody(sourceSha: string): string { + return `Updates \`apps/www/agents-ui-registry\` and \`apps/docs/lib/shadcn/prop-types.json\` from the latest build of [livekit/components-js](https://github.com/livekit/components-js) at ${sourceSha}.\n\nGenerated by \`packages/shadcn/scripts/publish-livekit-web.ts\`.`; +} + +export async function publishLivekitWeb(): Promise<{ + success: boolean; + skipped?: boolean; + prUrl?: string; +}> { + ghAuthPreflight(); + + let tmpDir: string | undefined; + const hadEnvLocal = fs.existsSync(ENV_LOCAL_PATH); + const envBackup = hadEnvLocal ? fs.readFileSync(ENV_LOCAL_PATH, 'utf-8') : null; + + try { + const cloned = cloneAndCreateBranch(REPO, 'livekit-web-', BRANCH_PREFIX); + tmpDir = cloned.tmpDir; + + writeTempEnvLocal(tmpDir); + deployRegistry(); + + if (!hasDiff(tmpDir)) { + console.log('No changes after sync — skipping PR'); + return { success: true, skipped: true }; + } + + const sourceSha = runCapture(['git', 'rev-parse', 'HEAD'], { cwd: SHADCN_PKG_DIR }); + const prUrl = await commitPushAndOpenPr({ + repo: REPO, + cwd: tmpDir, + branch: cloned.branch, + base: 'main', + title: PR_TITLE, + body: buildPrBody(sourceSha), + commitMessage: COMMIT_MESSAGE, + }); + + if (!prUrl) { + return { success: true, skipped: true }; + } + return { success: true, prUrl }; + } finally { + restoreEnvLocal(hadEnvLocal, envBackup); + removeTempDir(tmpDir); + } +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { + publishLivekitWeb() + .then((result) => { + console.log('Done', result); + }) + .catch((err) => { + console.error(err); + process.exitCode = 1; + }); +} diff --git a/packages/shadcn/scripts/update.ts b/packages/shadcn/scripts/update.ts index 9ad0e051e..1b82fe7a9 100644 --- a/packages/shadcn/scripts/update.ts +++ b/packages/shadcn/scripts/update.ts @@ -1,24 +1,50 @@ -import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -// copy .env.example to .env.local and edit DEST_REGISTRY_PATH -if (!process.env.DEST_REGISTRY_PATH) { - throw new Error('DEST_REGISTRY_PATH is not set'); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SHADCN_PKG_DIR = path.join(__dirname, '..'); + +function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`${name} is not set`); + } + return value; } -console.log('--------------------------------'); -console.log(`Cleaning ${process.env.DEST_REGISTRY_PATH}`); -execSync(`rm -rf ${process.env.DEST_REGISTRY_PATH}/*`); -console.log('--------------------------------'); -console.log(`Copying dist/r to ${process.env.DEST_REGISTRY_PATH}`); -execSync(`cp -r dist/r/* ${process.env.DEST_REGISTRY_PATH}`); +const destRegistryPath = requireEnv('DEST_REGISTRY_PATH'); +const destPropTypesPath = requireEnv('DEST_PROP_TYPES_PATH'); +const registrySourcePath = path.join(SHADCN_PKG_DIR, 'dist/r'); +const propTypesSourcePath = path.join(SHADCN_PKG_DIR, 'dist/prop-types.json'); + +function assertSafeCleanTarget(targetPath: string): void { + const resolvedPath = path.resolve(targetPath); + if ( + resolvedPath === path.parse(resolvedPath).root || + resolvedPath === SHADCN_PKG_DIR || + resolvedPath === path.dirname(SHADCN_PKG_DIR) + ) { + throw new Error(`Refusing to clean unsafe DEST_REGISTRY_PATH: ${targetPath}`); + } +} -if (!process.env.DEST_PROP_TYPES_PATH) { - throw new Error('DEST_PROP_TYPES_PATH is not set'); +console.log('--------------------------------'); +console.log(`Cleaning ${destRegistryPath}`); +assertSafeCleanTarget(destRegistryPath); +fs.mkdirSync(destRegistryPath, { recursive: true }); +for (const entry of fs.readdirSync(destRegistryPath)) { + fs.rmSync(path.join(destRegistryPath, entry), { recursive: true, force: true }); } console.log('--------------------------------'); -console.log(`Copying dist/prop-types.json to ${process.env.DEST_PROP_TYPES_PATH}`); -execSync(`cp dist/prop-types.json ${process.env.DEST_PROP_TYPES_PATH}`); +console.log(`Copying dist/r to ${destRegistryPath}`); +fs.cpSync(registrySourcePath, destRegistryPath, { recursive: true }); + +console.log('--------------------------------'); +console.log(`Copying dist/prop-types.json to ${destPropTypesPath}`); +fs.mkdirSync(path.dirname(destPropTypesPath), { recursive: true }); +fs.copyFileSync(propTypesSourcePath, destPropTypesPath); console.log('--------------------------------'); console.log('Done'); diff --git a/turbo.json b/turbo.json index 8349aff41..734b7cee8 100644 --- a/turbo.json +++ b/turbo.json @@ -36,6 +36,9 @@ "cache": false, "dependsOn": ["^build"] }, - "api-check:update": {} + "api-check:update": {}, + "shadcn:publish:all": { + "cache": false + } } } From 7116747d49624515d5ce92a7b009e6699d6266af Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 24 Jul 2026 14:15:37 -0400 Subject: [PATCH 2/3] fix(shadcn): make publish scripts resilient to stalls and interrupts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mark the root shadcn:publish:all turbo task interactive so stdin is forwarded to the confirmation prompt (turbo disconnects stdin by default, which previously made the prompt hang indefinitely) - Drop the unused --env-file=.env.local flag from shadcn:doc-gen — doc-gen.ts never reads process.env, but the flag made Node hard-fail whenever .env.local didn't exist, breaking shadcn:build for publish-agent-starter-react.ts - Add an onInterrupt helper that registers SIGINT/SIGTERM handlers so Ctrl-C during the confirmation prompt still restores .env.local, kills the local registry server, and removes the tmp clone, instead of leaking state past the bypassed finally block Co-Authored-By: Claude Sonnet 5 --- packages/shadcn/package.json | 2 +- packages/shadcn/scripts/lib/publish-utils.ts | 13 +++++++++++++ .../shadcn/scripts/publish-agent-starter-react.ts | 10 ++++++++-- packages/shadcn/scripts/publish-livekit-web.ts | 10 ++++++++-- turbo.json | 4 +++- 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/shadcn/package.json b/packages/shadcn/package.json index 802484851..77e03d664 100644 --- a/packages/shadcn/package.json +++ b/packages/shadcn/package.json @@ -10,7 +10,7 @@ "pretest": "pnpm build:deps", "shadcn:build": "pnpm test && rm -rf dist && shadcn build --output ./dist/r && pnpm shadcn:doc-gen", "shadcn:serve": "pnpm shadcn:build && python3 -m http.server 3210 -d ./dist", - "shadcn:doc-gen": "node --experimental-strip-types --env-file=.env.local ./scripts/doc-gen.ts", + "shadcn:doc-gen": "node --experimental-strip-types ./scripts/doc-gen.ts", "shadcn:deploy": "pnpm shadcn:build && node --experimental-strip-types --env-file=.env.local ./scripts/update.ts", "shadcn:publish:agent-starter-react": "node --experimental-strip-types ./scripts/publish-agent-starter-react.ts", "shadcn:publish:livekit-web": "node --experimental-strip-types ./scripts/publish-livekit-web.ts", diff --git a/packages/shadcn/scripts/lib/publish-utils.ts b/packages/shadcn/scripts/lib/publish-utils.ts index dbc390e84..8d83df596 100644 --- a/packages/shadcn/scripts/lib/publish-utils.ts +++ b/packages/shadcn/scripts/lib/publish-utils.ts @@ -106,6 +106,19 @@ export function removeTempDir(dir: string | undefined): void { } } +export function onInterrupt(cleanup: () => void): () => void { + const handler = () => { + cleanup(); + process.exit(1); + }; + process.once('SIGINT', handler); + process.once('SIGTERM', handler); + return () => { + process.off('SIGINT', handler); + process.off('SIGTERM', handler); + }; +} + export async function promptYesNo(question: string): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); try { diff --git a/packages/shadcn/scripts/publish-agent-starter-react.ts b/packages/shadcn/scripts/publish-agent-starter-react.ts index 25e3d3b15..d5dde939a 100644 --- a/packages/shadcn/scripts/publish-agent-starter-react.ts +++ b/packages/shadcn/scripts/publish-agent-starter-react.ts @@ -9,6 +9,7 @@ import { hasDiff, isPortInUse, killServer, + onInterrupt, removeTempDir, run, runCapture, @@ -84,6 +85,11 @@ export async function publishAgentStarterReact(): Promise<{ let server: ChildProcess | undefined; let tmpDir: string | undefined; + const cleanup = () => { + killServer(server); + removeTempDir(tmpDir); + }; + const unregisterInterruptHandler = onInterrupt(cleanup); try { buildRegistry(); @@ -117,8 +123,8 @@ export async function publishAgentStarterReact(): Promise<{ } return { success: true, prUrl }; } finally { - killServer(server); - removeTempDir(tmpDir); + unregisterInterruptHandler(); + cleanup(); } } diff --git a/packages/shadcn/scripts/publish-livekit-web.ts b/packages/shadcn/scripts/publish-livekit-web.ts index 948ca5049..16873a5a8 100644 --- a/packages/shadcn/scripts/publish-livekit-web.ts +++ b/packages/shadcn/scripts/publish-livekit-web.ts @@ -6,6 +6,7 @@ import { commitPushAndOpenPr, ghAuthPreflight, hasDiff, + onInterrupt, removeTempDir, run, runCapture, @@ -58,6 +59,11 @@ export async function publishLivekitWeb(): Promise<{ let tmpDir: string | undefined; const hadEnvLocal = fs.existsSync(ENV_LOCAL_PATH); const envBackup = hadEnvLocal ? fs.readFileSync(ENV_LOCAL_PATH, 'utf-8') : null; + const cleanup = () => { + restoreEnvLocal(hadEnvLocal, envBackup); + removeTempDir(tmpDir); + }; + const unregisterInterruptHandler = onInterrupt(cleanup); try { const cloned = cloneAndCreateBranch(REPO, 'livekit-web-', BRANCH_PREFIX); @@ -87,8 +93,8 @@ export async function publishLivekitWeb(): Promise<{ } return { success: true, prUrl }; } finally { - restoreEnvLocal(hadEnvLocal, envBackup); - removeTempDir(tmpDir); + unregisterInterruptHandler(); + cleanup(); } } diff --git a/turbo.json b/turbo.json index 734b7cee8..967ca825b 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,6 @@ { "$schema": "https://turborepo.org/schema.json", + "ui": "tui", "tasks": { "build": { "dependsOn": ["^build"], @@ -38,7 +39,8 @@ }, "api-check:update": {}, "shadcn:publish:all": { - "cache": false + "cache": false, + "interactive": true } } } From d83201232517a5d5eda90675ffd7694709fa35e2 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 24 Jul 2026 16:09:41 -0400 Subject: [PATCH 3/3] chore: harden shadcn downstream publish scripts --- packages/shadcn/scripts/lib/publish-utils.ts | 30 +++++++++++++++++-- .../scripts/publish-agent-starter-react.ts | 7 +++-- .../shadcn/scripts/publish-livekit-web.ts | 9 ++++-- packages/shadcn/tsconfig.json | 1 + 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/shadcn/scripts/lib/publish-utils.ts b/packages/shadcn/scripts/lib/publish-utils.ts index 8d83df596..9f43dcc07 100644 --- a/packages/shadcn/scripts/lib/publish-utils.ts +++ b/packages/shadcn/scripts/lib/publish-utils.ts @@ -22,6 +22,18 @@ export function hasDiff(cwd: string): boolean { return runCapture(['git', 'status', '--porcelain'], { cwd }).length > 0; } +export function installDependencies(cwd: string): void { + console.log('--------------------------------'); + console.log('Installing dependencies'); + run(['pnpm', 'install'], { cwd }); +} + +export function formatChanges(cwd: string): void { + console.log('--------------------------------'); + console.log('Formatting changes'); + run(['pnpm', 'format'], { cwd }); +} + export function ghAuthPreflight(): void { try { execFileSync('gh', ['auth', 'status'], { stdio: 'ignore' }); @@ -133,16 +145,28 @@ export function cloneAndCreateBranch( repo: string, tmpDirPrefix: string, branchPrefix: string, + baseBranch: string, ): { tmpDir: string; branch: string } { const tmpDir = mkTempClone(tmpDirPrefix); console.log('--------------------------------'); console.log(`Cloning ${repo} into ${tmpDir}`); run(['gh', 'repo', 'clone', repo, tmpDir]); - run(['git', 'fetch', 'origin', 'main'], { cwd: tmpDir }); - run(['git', 'reset', '--hard', 'origin/main'], { cwd: tmpDir }); + console.log(`Fetching latest origin/${baseBranch}`); + run( + [ + 'git', + 'fetch', + '--prune', + 'origin', + `+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`, + ], + { + cwd: tmpDir, + }, + ); const branch = branchName(branchPrefix); - run(['git', 'checkout', '-b', branch], { cwd: tmpDir }); + run(['git', 'checkout', '-b', branch, `origin/${baseBranch}`], { cwd: tmpDir }); return { tmpDir, branch }; } diff --git a/packages/shadcn/scripts/publish-agent-starter-react.ts b/packages/shadcn/scripts/publish-agent-starter-react.ts index d5dde939a..4bbcfb3d9 100644 --- a/packages/shadcn/scripts/publish-agent-starter-react.ts +++ b/packages/shadcn/scripts/publish-agent-starter-react.ts @@ -5,6 +5,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; import { cloneAndCreateBranch, commitPushAndOpenPr, + formatChanges, ghAuthPreflight, hasDiff, isPortInUse, @@ -21,6 +22,7 @@ const SHADCN_PKG_DIR = path.join(__dirname, '..'); const REPO = 'livekit-examples/agent-starter-react'; const PORT = 3210; const REGISTRY_URL = `http://localhost:${PORT}/r/{name}.json`; +const BASE_BRANCH = 'main'; const BRANCH_PREFIX = 'chore/update-agents-ui-registry'; const COMMIT_MESSAGE = 'chore: sync agents-ui components from livekit/components-js'; const PR_TITLE = 'chore: sync agents-ui components'; @@ -95,12 +97,13 @@ export async function publishAgentStarterReact(): Promise<{ buildRegistry(); server = await serveRegistry(); - const cloned = cloneAndCreateBranch(REPO, 'agent-starter-react-', BRANCH_PREFIX); + const cloned = cloneAndCreateBranch(REPO, 'agent-starter-react-', BRANCH_PREFIX, BASE_BRANCH); tmpDir = cloned.tmpDir; pointComponentsJsonAtLocalRegistry(tmpDir); installComponents(tmpDir); revertComponentsJson(tmpDir); + formatChanges(tmpDir); if (!hasDiff(tmpDir)) { console.log('No changes after sync — skipping PR'); @@ -112,7 +115,7 @@ export async function publishAgentStarterReact(): Promise<{ repo: REPO, cwd: tmpDir, branch: cloned.branch, - base: 'main', + base: BASE_BRANCH, title: PR_TITLE, body: buildPrBody(sourceSha), commitMessage: COMMIT_MESSAGE, diff --git a/packages/shadcn/scripts/publish-livekit-web.ts b/packages/shadcn/scripts/publish-livekit-web.ts index 16873a5a8..5b719e44c 100644 --- a/packages/shadcn/scripts/publish-livekit-web.ts +++ b/packages/shadcn/scripts/publish-livekit-web.ts @@ -4,8 +4,10 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; import { cloneAndCreateBranch, commitPushAndOpenPr, + formatChanges, ghAuthPreflight, hasDiff, + installDependencies, onInterrupt, removeTempDir, run, @@ -16,6 +18,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const SHADCN_PKG_DIR = path.join(__dirname, '..'); const ENV_LOCAL_PATH = path.join(SHADCN_PKG_DIR, '.env.local'); const REPO = 'livekit/web'; +const BASE_BRANCH = 'main'; const BRANCH_PREFIX = 'chore/update-agents-ui-registry'; const COMMIT_MESSAGE = 'chore(agents-ui): update registry + prop-types from @livekit/agents-ui'; const PR_TITLE = 'chore(agents-ui): update registry + prop-types'; @@ -66,11 +69,13 @@ export async function publishLivekitWeb(): Promise<{ const unregisterInterruptHandler = onInterrupt(cleanup); try { - const cloned = cloneAndCreateBranch(REPO, 'livekit-web-', BRANCH_PREFIX); + const cloned = cloneAndCreateBranch(REPO, 'livekit-web-', BRANCH_PREFIX, BASE_BRANCH); tmpDir = cloned.tmpDir; writeTempEnvLocal(tmpDir); deployRegistry(); + installDependencies(tmpDir); + formatChanges(tmpDir); if (!hasDiff(tmpDir)) { console.log('No changes after sync — skipping PR'); @@ -82,7 +87,7 @@ export async function publishLivekitWeb(): Promise<{ repo: REPO, cwd: tmpDir, branch: cloned.branch, - base: 'main', + base: BASE_BRANCH, title: PR_TITLE, body: buildPrBody(sourceSha), commitMessage: COMMIT_MESSAGE, diff --git a/packages/shadcn/tsconfig.json b/packages/shadcn/tsconfig.json index 47b54192e..656e2d593 100644 --- a/packages/shadcn/tsconfig.json +++ b/packages/shadcn/tsconfig.json @@ -9,6 +9,7 @@ "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", + "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve",