From e89d4a850a676fc0c5362ae1d9a654e859814b84 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Thu, 23 Jul 2026 17:33:40 -0400 Subject: [PATCH 01/10] 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 b595946b3679a24280e5f564e0fb2addd1d4084c Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 24 Jul 2026 14:15:37 -0400 Subject: [PATCH 02/10] 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 fde0e14338147c893dcbecd2d2578cc2fb9a8ce8 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Fri, 24 Jul 2026 16:09:41 -0400 Subject: [PATCH 03/10] 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", From 6b703f41f421a5fd01cca5a79ccd3c62c6d31168 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Wed, 29 Jul 2026 14:37:39 -0400 Subject: [PATCH 04/10] fix: tighten shadcn downstream publishing --- .../agents-ui/agent-audio-visualizer-bar.tsx | 12 ---------- .../agents-ui/agent-audio-visualizer-grid.tsx | 12 ---------- .../agent-audio-visualizer-radial.tsx | 12 ---------- packages/shadcn/scripts/lib/publish-utils.ts | 10 ++++++-- .../scripts/publish-agent-starter-react.ts | 23 +++++++++++-------- packages/shadcn/scripts/publish-downstream.ts | 11 ++++++--- .../shadcn/scripts/publish-livekit-web.ts | 23 ++++++++++++++----- 7 files changed, 46 insertions(+), 57 deletions(-) diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx index f38ac261f..97a5b3210 100644 --- a/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx @@ -19,18 +19,6 @@ import { import { useAgentAudioVisualizerBarAnimator } from '@/hooks/agents-ui/use-agent-audio-visualizer-bar'; import { cn } from '@/lib/utils'; -/** - * Resizes an array of per-band volume values to exactly `count` entries. - * Excess values are trimmed from the end; if there are too few, the last - * value is duplicated to fill the remainder. An empty array is padded with 0s. - */ -function normalizeVolumeBands(bands: number[], count: number): number[] { - if (bands.length === count) return bands; - if (bands.length > count) return bands.slice(0, count); - const lastValue = bands[bands.length - 1] ?? 0; - return [...bands, ...new Array(count - bands.length).fill(lastValue)]; -} - function cloneSingleChild( children: ReactNode | ReactNode[], props?: Record, diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx index 84603b947..4f2d8e972 100644 --- a/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx @@ -23,18 +23,6 @@ import { } from '@/hooks/agents-ui/use-agent-audio-visualizer-grid'; import { cn } from '@/lib/utils'; -/** - * Resizes an array of per-band volume values to exactly `count` entries. - * Excess values are trimmed from the end; if there are too few, the last - * value is duplicated to fill the remainder. An empty array is padded with 0s. - */ -function normalizeVolumeBands(bands: number[], count: number): number[] { - if (bands.length === count) return bands; - if (bands.length > count) return bands.slice(0, count); - const lastValue = bands[bands.length - 1] ?? 0; - return [...bands, ...new Array(count - bands.length).fill(lastValue)]; -} - function cloneSingleChild( children: ReactNode | ReactNode[], props?: Record, diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-radial.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-radial.tsx index 1676f992b..93bb10ea1 100644 --- a/packages/shadcn/components/agents-ui/agent-audio-visualizer-radial.tsx +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-radial.tsx @@ -11,18 +11,6 @@ import { import { cn } from '@/lib/utils'; import { useAgentAudioVisualizerRadialAnimator } from '@/hooks/agents-ui/use-agent-audio-visualizer-radial'; -/** - * Resizes an array of per-band volume values to exactly `count` entries. - * Excess values are trimmed from the end; if there are too few, the last - * value is duplicated to fill the remainder. An empty array is padded with 0s. - */ -function normalizeVolumeBands(bands: number[], count: number): number[] { - if (bands.length === count) return bands; - if (bands.length > count) return bands.slice(0, count); - const lastValue = bands[bands.length - 1] ?? 0; - return [...bands, ...new Array(count - bands.length).fill(lastValue)]; -} - export const AgentAudioVisualizerRadialVariants = cva( [ 'relative flex items-center justify-center', diff --git a/packages/shadcn/scripts/lib/publish-utils.ts b/packages/shadcn/scripts/lib/publish-utils.ts index 9f43dcc07..bfad6011c 100644 --- a/packages/shadcn/scripts/lib/publish-utils.ts +++ b/packages/shadcn/scripts/lib/publish-utils.ts @@ -22,16 +22,22 @@ export function hasDiff(cwd: string): boolean { return runCapture(['git', 'status', '--porcelain'], { cwd }).length > 0; } +export function buildRegistry(shadcnPkgDir: string): void { + console.log('--------------------------------'); + console.log('Building registry'); + run(['pnpm', 'shadcn:build'], { cwd: shadcnPkgDir }); +} + export function installDependencies(cwd: string): void { console.log('--------------------------------'); console.log('Installing dependencies'); run(['pnpm', 'install'], { cwd }); } -export function formatChanges(cwd: string): void { +export function formatChanges(cwd: string, cmd: string[] = ['pnpm', 'format']): void { console.log('--------------------------------'); console.log('Formatting changes'); - run(['pnpm', 'format'], { cwd }); + run(cmd, { cwd }); } export function ghAuthPreflight(): void { diff --git a/packages/shadcn/scripts/publish-agent-starter-react.ts b/packages/shadcn/scripts/publish-agent-starter-react.ts index 4bbcfb3d9..ab3fe17f5 100644 --- a/packages/shadcn/scripts/publish-agent-starter-react.ts +++ b/packages/shadcn/scripts/publish-agent-starter-react.ts @@ -3,6 +3,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { + buildRegistry, cloneAndCreateBranch, commitPushAndOpenPr, formatChanges, @@ -17,6 +18,10 @@ import { waitForHttp, } from './lib/publish-utils.ts'; +type PublishOptions = { + registryAlreadyBuilt?: boolean; +}; + const __dirname = path.dirname(fileURLToPath(import.meta.url)); const SHADCN_PKG_DIR = path.join(__dirname, '..'); const REPO = 'livekit-examples/agent-starter-react'; @@ -27,12 +32,6 @@ 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}`); @@ -59,8 +58,10 @@ function installComponents(tmpDir: string): void { run(['pnpm', 'install'], { cwd: tmpDir }); console.log('--------------------------------'); - console.log('Running shadcn:install'); - run(['pnpm', 'shadcn:install'], { cwd: tmpDir }); + console.log('Installing agents-ui components'); + run(['pnpm', 'dlx', 'shadcn@latest', 'add', '--yes', '--overwrite', '@agents-ui/all'], { + cwd: tmpDir, + }); } function revertComponentsJson(tmpDir: string): void { @@ -73,7 +74,7 @@ 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<{ +export async function publishAgentStarterReact(options: PublishOptions = {}): Promise<{ success: boolean; skipped?: boolean; prUrl?: string; @@ -94,7 +95,9 @@ export async function publishAgentStarterReact(): Promise<{ const unregisterInterruptHandler = onInterrupt(cleanup); try { - buildRegistry(); + if (!options.registryAlreadyBuilt) { + buildRegistry(SHADCN_PKG_DIR); + } server = await serveRegistry(); const cloned = cloneAndCreateBranch(REPO, 'agent-starter-react-', BRANCH_PREFIX, BASE_BRANCH); diff --git a/packages/shadcn/scripts/publish-downstream.ts b/packages/shadcn/scripts/publish-downstream.ts index 24c4d4f3c..bc00ac571 100644 --- a/packages/shadcn/scripts/publish-downstream.ts +++ b/packages/shadcn/scripts/publish-downstream.ts @@ -1,12 +1,16 @@ -import { pathToFileURL } from 'node:url'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { buildRegistry } from './lib/publish-utils.ts'; 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 }; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const SHADCN_PKG_DIR = path.join(__dirname, '..'); async function runAgentStarterReact(): Promise { try { - const result = await publishAgentStarterReact(); + const result = await publishAgentStarterReact({ registryAlreadyBuilt: true }); return { name: 'agent-starter-react', ...result }; } catch (err) { return { name: 'agent-starter-react', success: false, error: String(err) }; @@ -15,7 +19,7 @@ async function runAgentStarterReact(): Promise { async function runLivekitWeb(): Promise { try { - const result = await publishLivekitWeb(); + const result = await publishLivekitWeb({ registryAlreadyBuilt: true }); return { name: 'livekit-web', ...result }; } catch (err) { return { name: 'livekit-web', success: false, error: String(err) }; @@ -34,6 +38,7 @@ function printSummary(results: Result[]): void { } async function publishDownstream(): Promise { + buildRegistry(SHADCN_PKG_DIR); const results: Result[] = []; results.push(await runAgentStarterReact()); results.push(await runLivekitWeb()); diff --git a/packages/shadcn/scripts/publish-livekit-web.ts b/packages/shadcn/scripts/publish-livekit-web.ts index 5b719e44c..cfdc40dd1 100644 --- a/packages/shadcn/scripts/publish-livekit-web.ts +++ b/packages/shadcn/scripts/publish-livekit-web.ts @@ -14,6 +14,10 @@ import { runCapture, } from './lib/publish-utils.ts'; +type PublishOptions = { + registryAlreadyBuilt?: boolean; +}; + 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'); @@ -34,10 +38,17 @@ function writeTempEnvLocal(tmpDir: string): void { ); } -function deployRegistry(): void { +function deployRegistry(registryAlreadyBuilt: boolean): void { console.log('--------------------------------'); - console.log('Running shadcn:deploy'); - run(['pnpm', 'shadcn:deploy'], { cwd: SHADCN_PKG_DIR }); + if (registryAlreadyBuilt) { + console.log('Deploying already-built registry'); + run(['node', '--experimental-strip-types', '--env-file=.env.local', './scripts/update.ts'], { + cwd: SHADCN_PKG_DIR, + }); + } else { + console.log('Building and deploying registry'); + run(['pnpm', 'shadcn:deploy'], { cwd: SHADCN_PKG_DIR }); + } } function restoreEnvLocal(hadEnvLocal: boolean, envBackup: string | null): void { @@ -52,7 +63,7 @@ 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<{ +export async function publishLivekitWeb(options: PublishOptions = {}): Promise<{ success: boolean; skipped?: boolean; prUrl?: string; @@ -73,9 +84,9 @@ export async function publishLivekitWeb(): Promise<{ tmpDir = cloned.tmpDir; writeTempEnvLocal(tmpDir); - deployRegistry(); + deployRegistry(options.registryAlreadyBuilt ?? false); installDependencies(tmpDir); - formatChanges(tmpDir); + formatChanges(tmpDir, ['pnpm', '--filter', 'www', '--filter', 'docs', 'format']); if (!hasDiff(tmpDir)) { console.log('No changes after sync — skipping PR'); From f531986617ac8c0539017c1fd1d618e9a9372e33 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Wed, 29 Jul 2026 17:18:34 -0400 Subject: [PATCH 05/10] feat(shadcn): add -y/--yes auto-approve flag to publish scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add hasCliFlag helper and thread an autoApprove option through confirmPrCreation/commitPushAndOpenPr so the confirmation prompt can be skipped without removing the printed repo/branch/title/body/diff summary - Wire -y/--yes parsing into the standalone entry points for publish-agent-starter-react.ts, publish-livekit-web.ts, and publish-downstream.ts - Build @repo/prettier-plugin-markdoc in the tmp livekit/web clone before running its format step — pnpm install alone doesn't build workspace packages, and apps/docs' format script needs this plugin's dist/ to exist Co-Authored-By: Claude Sonnet 5 --- package.json | 2 +- packages/shadcn/scripts/lib/publish-utils.ts | 11 +++++++++++ .../scripts/publish-agent-starter-react.ts | 5 ++++- packages/shadcn/scripts/publish-downstream.ts | 19 ++++++++++--------- .../shadcn/scripts/publish-livekit-web.ts | 12 +++++++++++- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 01959345f..2e8065a89 100644 --- a/package.json +++ b/package.json @@ -19,7 +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...", + "shadcn:publish:all": "TURBO_UI=true 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/scripts/lib/publish-utils.ts b/packages/shadcn/scripts/lib/publish-utils.ts index bfad6011c..100163f54 100644 --- a/packages/shadcn/scripts/lib/publish-utils.ts +++ b/packages/shadcn/scripts/lib/publish-utils.ts @@ -124,6 +124,10 @@ export function removeTempDir(dir: string | undefined): void { } } +export function hasCliFlag(...flags: string[]): boolean { + return process.argv.slice(2).some((arg) => flags.includes(arg)); +} + export function onInterrupt(cleanup: () => void): () => void { const handler = () => { cleanup(); @@ -184,6 +188,7 @@ async function confirmPrCreation(opts: { base: string; title: string; body: string; + autoApprove?: boolean; }): Promise { const diffStat = runCapture(['git', 'diff', '--stat'], { cwd: opts.cwd }); @@ -198,6 +203,11 @@ async function confirmPrCreation(opts: { for (const line of diffStat.split('\n')) console.log(` ${line}`); console.log('--------------------------------'); + if (opts.autoApprove) { + console.log('Auto-approved (-y)'); + return true; + } + return promptYesNo('Create this PR?'); } @@ -209,6 +219,7 @@ export async function commitPushAndOpenPr(opts: { title: string; body: string; commitMessage: string; + autoApprove?: boolean; }): Promise { const confirmed = await confirmPrCreation(opts); if (!confirmed) { diff --git a/packages/shadcn/scripts/publish-agent-starter-react.ts b/packages/shadcn/scripts/publish-agent-starter-react.ts index ab3fe17f5..fb34a4617 100644 --- a/packages/shadcn/scripts/publish-agent-starter-react.ts +++ b/packages/shadcn/scripts/publish-agent-starter-react.ts @@ -8,6 +8,7 @@ import { commitPushAndOpenPr, formatChanges, ghAuthPreflight, + hasCliFlag, hasDiff, isPortInUse, killServer, @@ -20,6 +21,7 @@ import { type PublishOptions = { registryAlreadyBuilt?: boolean; + autoApprove?: boolean; }; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -122,6 +124,7 @@ export async function publishAgentStarterReact(options: PublishOptions = {}): Pr title: PR_TITLE, body: buildPrBody(sourceSha), commitMessage: COMMIT_MESSAGE, + autoApprove: options.autoApprove, }); if (!prUrl) { @@ -135,7 +138,7 @@ export async function publishAgentStarterReact(options: PublishOptions = {}): Pr } if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { - publishAgentStarterReact() + publishAgentStarterReact({ autoApprove: hasCliFlag('-y', '--yes') }) .then((result) => { console.log('Done', result); }) diff --git a/packages/shadcn/scripts/publish-downstream.ts b/packages/shadcn/scripts/publish-downstream.ts index bc00ac571..f56d3534b 100644 --- a/packages/shadcn/scripts/publish-downstream.ts +++ b/packages/shadcn/scripts/publish-downstream.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { buildRegistry } from './lib/publish-utils.ts'; +import { buildRegistry, hasCliFlag } from './lib/publish-utils.ts'; import { publishAgentStarterReact } from './publish-agent-starter-react.ts'; import { publishLivekitWeb } from './publish-livekit-web.ts'; @@ -8,18 +8,18 @@ type Result = { name: string; success: boolean; skipped?: boolean; prUrl?: strin const __dirname = path.dirname(fileURLToPath(import.meta.url)); const SHADCN_PKG_DIR = path.join(__dirname, '..'); -async function runAgentStarterReact(): Promise { +async function runAgentStarterReact(autoApprove: boolean): Promise { try { - const result = await publishAgentStarterReact({ registryAlreadyBuilt: true }); + const result = await publishAgentStarterReact({ registryAlreadyBuilt: true, autoApprove }); return { name: 'agent-starter-react', ...result }; } catch (err) { return { name: 'agent-starter-react', success: false, error: String(err) }; } } -async function runLivekitWeb(): Promise { +async function runLivekitWeb(autoApprove: boolean): Promise { try { - const result = await publishLivekitWeb({ registryAlreadyBuilt: true }); + const result = await publishLivekitWeb({ registryAlreadyBuilt: true, autoApprove }); return { name: 'livekit-web', ...result }; } catch (err) { return { name: 'livekit-web', success: false, error: String(err) }; @@ -37,16 +37,17 @@ function printSummary(results: Result[]): void { } } -async function publishDownstream(): Promise { +async function publishDownstream(autoApprove: boolean): Promise { buildRegistry(SHADCN_PKG_DIR); const results: Result[] = []; - results.push(await runAgentStarterReact()); - results.push(await runLivekitWeb()); + results.push(await runAgentStarterReact(autoApprove)); + results.push(await runLivekitWeb(autoApprove)); return results; } if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { - const results = await publishDownstream(); + const autoApprove = hasCliFlag('-y', '--yes'); + const results = await publishDownstream(autoApprove); 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 index cfdc40dd1..21baf3044 100644 --- a/packages/shadcn/scripts/publish-livekit-web.ts +++ b/packages/shadcn/scripts/publish-livekit-web.ts @@ -6,6 +6,7 @@ import { commitPushAndOpenPr, formatChanges, ghAuthPreflight, + hasCliFlag, hasDiff, installDependencies, onInterrupt, @@ -16,6 +17,7 @@ import { type PublishOptions = { registryAlreadyBuilt?: boolean; + autoApprove?: boolean; }; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -51,6 +53,12 @@ function deployRegistry(registryAlreadyBuilt: boolean): void { } } +function buildFormatDependencies(tmpDir: string): void { + console.log('--------------------------------'); + console.log('Building @repo/prettier-plugin-markdoc (required by apps/docs format script)'); + run(['pnpm', '--filter', '@repo/prettier-plugin-markdoc', 'build'], { cwd: tmpDir }); +} + function restoreEnvLocal(hadEnvLocal: boolean, envBackup: string | null): void { if (hadEnvLocal && envBackup !== null) { fs.writeFileSync(ENV_LOCAL_PATH, envBackup); @@ -86,6 +94,7 @@ export async function publishLivekitWeb(options: PublishOptions = {}): Promise<{ writeTempEnvLocal(tmpDir); deployRegistry(options.registryAlreadyBuilt ?? false); installDependencies(tmpDir); + buildFormatDependencies(tmpDir); formatChanges(tmpDir, ['pnpm', '--filter', 'www', '--filter', 'docs', 'format']); if (!hasDiff(tmpDir)) { @@ -102,6 +111,7 @@ export async function publishLivekitWeb(options: PublishOptions = {}): Promise<{ title: PR_TITLE, body: buildPrBody(sourceSha), commitMessage: COMMIT_MESSAGE, + autoApprove: options.autoApprove, }); if (!prUrl) { @@ -115,7 +125,7 @@ export async function publishLivekitWeb(options: PublishOptions = {}): Promise<{ } if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { - publishLivekitWeb() + publishLivekitWeb({ autoApprove: hasCliFlag('-y', '--yes') }) .then((result) => { console.log('Done', result); }) From c70b3a9bfe1b5454124dd3670744d0789934f7e2 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Wed, 29 Jul 2026 17:50:22 -0400 Subject: [PATCH 06/10] fix(shadcn): restore normalizeVolumeBands in agent visualizer components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An earlier commit removed the shared lib/utils.ts implementation and its import from these components, and a later commit removed what looked like now-redundant local copies — but those were the only remaining implementation, leaving normalizeVolumeBands undefined and throwing a ReferenceError in agent-audio-visualizer-bar/grid/radial. Restore normalizeVolumeBands as a local, exported function in each component (not reintroducing the lib/utils.ts dependency), and import it directly in each component's test file to cover the trim/pad/empty cases that were previously tested against the deleted lib/utils.ts version. Co-Authored-By: Claude Sonnet 5 --- .../agents-ui/agent-audio-visualizer-bar.tsx | 12 +++++++++ .../agents-ui/agent-audio-visualizer-grid.tsx | 12 +++++++++ .../agent-audio-visualizer-radial.tsx | 12 +++++++++ .../tests/agent-audio-visualizer-bar.test.tsx | 27 ++++++++++++++++++- .../agent-audio-visualizer-grid.test.tsx | 27 ++++++++++++++++++- .../agent-audio-visualizer-radial.test.tsx | 27 ++++++++++++++++++- 6 files changed, 114 insertions(+), 3 deletions(-) diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx index 97a5b3210..b049b6081 100644 --- a/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-bar.tsx @@ -19,6 +19,18 @@ import { import { useAgentAudioVisualizerBarAnimator } from '@/hooks/agents-ui/use-agent-audio-visualizer-bar'; import { cn } from '@/lib/utils'; +/** + * Resizes an array of per-band volume values to exactly `count` entries. + * Excess values are trimmed from the end; if there are too few, the last + * value is duplicated to fill the remainder. An empty array is padded with 0s. + */ +export function normalizeVolumeBands(bands: number[], count: number): number[] { + if (bands.length === count) return bands; + if (bands.length > count) return bands.slice(0, count); + const lastValue = bands[bands.length - 1] ?? 0; + return [...bands, ...new Array(count - bands.length).fill(lastValue)]; +} + function cloneSingleChild( children: ReactNode | ReactNode[], props?: Record, diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx index 4f2d8e972..808be9302 100644 --- a/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx @@ -23,6 +23,18 @@ import { } from '@/hooks/agents-ui/use-agent-audio-visualizer-grid'; import { cn } from '@/lib/utils'; +/** + * Resizes an array of per-band volume values to exactly `count` entries. + * Excess values are trimmed from the end; if there are too few, the last + * value is duplicated to fill the remainder. An empty array is padded with 0s. + */ +export function normalizeVolumeBands(bands: number[], count: number): number[] { + if (bands.length === count) return bands; + if (bands.length > count) return bands.slice(0, count); + const lastValue = bands[bands.length - 1] ?? 0; + return [...bands, ...new Array(count - bands.length).fill(lastValue)]; +} + function cloneSingleChild( children: ReactNode | ReactNode[], props?: Record, diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-radial.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-radial.tsx index 93bb10ea1..2961a6079 100644 --- a/packages/shadcn/components/agents-ui/agent-audio-visualizer-radial.tsx +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-radial.tsx @@ -11,6 +11,18 @@ import { import { cn } from '@/lib/utils'; import { useAgentAudioVisualizerRadialAnimator } from '@/hooks/agents-ui/use-agent-audio-visualizer-radial'; +/** + * Resizes an array of per-band volume values to exactly `count` entries. + * Excess values are trimmed from the end; if there are too few, the last + * value is duplicated to fill the remainder. An empty array is padded with 0s. + */ +export function normalizeVolumeBands(bands: number[], count: number): number[] { + if (bands.length === count) return bands; + if (bands.length > count) return bands.slice(0, count); + const lastValue = bands[bands.length - 1] ?? 0; + return [...bands, ...new Array(count - bands.length).fill(lastValue)]; +} + export const AgentAudioVisualizerRadialVariants = cva( [ 'relative flex items-center justify-center', diff --git a/packages/shadcn/tests/agent-audio-visualizer-bar.test.tsx b/packages/shadcn/tests/agent-audio-visualizer-bar.test.tsx index cdb0857f0..b106d1f51 100644 --- a/packages/shadcn/tests/agent-audio-visualizer-bar.test.tsx +++ b/packages/shadcn/tests/agent-audio-visualizer-bar.test.tsx @@ -1,6 +1,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; -import { AgentAudioVisualizerBar } from '@/components/agents-ui/agent-audio-visualizer-bar'; +import { + AgentAudioVisualizerBar, + normalizeVolumeBands, +} from '@/components/agents-ui/agent-audio-visualizer-bar'; import * as LiveKitComponents from '@livekit/components-react'; // Mock the @livekit/components-react hooks @@ -149,3 +152,25 @@ describe('AgentAudioVisualizerBar', () => { ).toThrow('AgentAudioVisualizerBar children must be a single element.'); }); }); + +describe('normalizeVolumeBands', () => { + it('returns the array unchanged when the length already matches', () => { + expect(normalizeVolumeBands([0.1, 0.2, 0.3], 3)).toEqual([0.1, 0.2, 0.3]); + }); + + it('trims excess trailing values', () => { + expect(normalizeVolumeBands([0.1, 0.2, 0.3, 0.4], 2)).toEqual([0.1, 0.2]); + }); + + it('pads by duplicating the last value', () => { + expect(normalizeVolumeBands([0.1, 0.2], 4)).toEqual([0.1, 0.2, 0.2, 0.2]); + }); + + it('pads a single-element array by duplicating it', () => { + expect(normalizeVolumeBands([0.5], 3)).toEqual([0.5, 0.5, 0.5]); + }); + + it('pads an empty array with 0s', () => { + expect(normalizeVolumeBands([], 3)).toEqual([0, 0, 0]); + }); +}); diff --git a/packages/shadcn/tests/agent-audio-visualizer-grid.test.tsx b/packages/shadcn/tests/agent-audio-visualizer-grid.test.tsx index 4f2548f58..9eefd814d 100644 --- a/packages/shadcn/tests/agent-audio-visualizer-grid.test.tsx +++ b/packages/shadcn/tests/agent-audio-visualizer-grid.test.tsx @@ -1,6 +1,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; -import { AgentAudioVisualizerGrid } from '@/components/agents-ui/agent-audio-visualizer-grid'; +import { + AgentAudioVisualizerGrid, + normalizeVolumeBands, +} from '@/components/agents-ui/agent-audio-visualizer-grid'; import { useMultibandTrackVolume } from '@livekit/components-react'; import { useAgentAudioVisualizerGridAnimator } from '@/hooks/agents-ui/use-agent-audio-visualizer-grid'; @@ -202,3 +205,25 @@ describe('AgentAudioVisualizerGrid', () => { ).toThrow('AgentAudioVisualizerGrid children must be a single element.'); }); }); + +describe('normalizeVolumeBands', () => { + it('returns the array unchanged when the length already matches', () => { + expect(normalizeVolumeBands([0.1, 0.2, 0.3], 3)).toEqual([0.1, 0.2, 0.3]); + }); + + it('trims excess trailing values', () => { + expect(normalizeVolumeBands([0.1, 0.2, 0.3, 0.4], 2)).toEqual([0.1, 0.2]); + }); + + it('pads by duplicating the last value', () => { + expect(normalizeVolumeBands([0.1, 0.2], 4)).toEqual([0.1, 0.2, 0.2, 0.2]); + }); + + it('pads a single-element array by duplicating it', () => { + expect(normalizeVolumeBands([0.5], 3)).toEqual([0.5, 0.5, 0.5]); + }); + + it('pads an empty array with 0s', () => { + expect(normalizeVolumeBands([], 3)).toEqual([0, 0, 0]); + }); +}); diff --git a/packages/shadcn/tests/agent-audio-visualizer-radial.test.tsx b/packages/shadcn/tests/agent-audio-visualizer-radial.test.tsx index 955f01097..2b4e2f642 100644 --- a/packages/shadcn/tests/agent-audio-visualizer-radial.test.tsx +++ b/packages/shadcn/tests/agent-audio-visualizer-radial.test.tsx @@ -1,6 +1,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; -import { AgentAudioVisualizerRadial } from '@/components/agents-ui/agent-audio-visualizer-radial'; +import { + AgentAudioVisualizerRadial, + normalizeVolumeBands, +} from '@/components/agents-ui/agent-audio-visualizer-radial'; import * as LiveKitComponents from '@livekit/components-react'; // Mock hooks vi.mock('@livekit/components-react', async () => { @@ -108,3 +111,25 @@ describe('AgentAudioVisualizerRadial', () => { expect(bars[2]).not.toHaveStyle({ height: '0px' }); }); }); + +describe('normalizeVolumeBands', () => { + it('returns the array unchanged when the length already matches', () => { + expect(normalizeVolumeBands([0.1, 0.2, 0.3], 3)).toEqual([0.1, 0.2, 0.3]); + }); + + it('trims excess trailing values', () => { + expect(normalizeVolumeBands([0.1, 0.2, 0.3, 0.4], 2)).toEqual([0.1, 0.2]); + }); + + it('pads by duplicating the last value', () => { + expect(normalizeVolumeBands([0.1, 0.2], 4)).toEqual([0.1, 0.2, 0.2, 0.2]); + }); + + it('pads a single-element array by duplicating it', () => { + expect(normalizeVolumeBands([0.5], 3)).toEqual([0.5, 0.5, 0.5]); + }); + + it('pads an empty array with 0s', () => { + expect(normalizeVolumeBands([], 3)).toEqual([0, 0, 0]); + }); +}); From e6cbfbe24daee6ba7fc6b106b36b2d0754a7bb65 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Wed, 29 Jul 2026 18:02:37 -0400 Subject: [PATCH 07/10] apply tailwind uppercase to control bar --- packages/shadcn/components/agents-ui/agent-control-bar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shadcn/components/agents-ui/agent-control-bar.tsx b/packages/shadcn/components/agents-ui/agent-control-bar.tsx index f64cc116f..ea39cdd91 100644 --- a/packages/shadcn/components/agents-ui/agent-control-bar.tsx +++ b/packages/shadcn/components/agents-ui/agent-control-bar.tsx @@ -397,8 +397,8 @@ export function AgentControlBar({ 'bg-destructive/10 dark:bg-destructive/10 text-destructive hover:bg-destructive/20 dark:hover:bg-destructive/20 focus:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/4 rounded-full font-mono text-xs font-bold tracking-wider', )} > - End call - End + End call + End )} From bacb040e2e6c5fa58ca500fd989b6274403ab876 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Thu, 30 Jul 2026 10:34:36 -0400 Subject: [PATCH 08/10] fix(shadcn): resolve port race and double-prompt hang in publish scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add waitForPortFree to poll instead of failing immediately: killServer() sends SIGTERM without waiting for exit, so livekit-web's port check could fire before agent-starter-react's server had actually released it when run back-to-back via publish-downstream.ts - Share one readline interface across a process instead of creating/closing one per prompt — a second readline.createInterface() on process.stdin after closing the first one never receives input, which could hang shadcn:publish:all on its second confirmation prompt - Actually install @agents-ui components into apps/www and apps/docs (both have their own shadcn:install script) instead of only copying the raw registry JSON + prop-types.json, by pointing each app's components.json at the local dev registry before running shadcn:install and reverting it after - Document the -y/--yes auto-approve flag and dry-run usage in the README Co-Authored-By: Claude Sonnet 5 --- packages/shadcn/README.md | 31 ++++- .../agents-ui/agent-audio-visualizer-grid.tsx | 2 +- .../agents-ui/agent-audio-visualizer-wave.tsx | 2 +- .../agents-ui/agent-disconnect-button.tsx | 2 +- .../agents-ui/agent-session-provider.tsx | 2 +- .../agents-ui/agent-track-control.tsx | 3 +- .../components/agents-ui/react-shader-toy.tsx | 4 - .../agents-ui/start-audio-button.tsx | 2 +- packages/shadcn/scripts/lib/publish-utils.ts | 108 ++++++++++++++---- .../scripts/publish-agent-starter-react.ts | 52 +++------ packages/shadcn/scripts/publish-downstream.ts | 3 +- .../shadcn/scripts/publish-livekit-web.ts | 62 +++++++--- 12 files changed, 183 insertions(+), 90 deletions(-) diff --git a/packages/shadcn/README.md b/packages/shadcn/README.md index 4bbaeffc9..b12da9d76 100644 --- a/packages/shadcn/README.md +++ b/packages/shadcn/README.md @@ -166,13 +166,42 @@ repositories, and open GitHub PRs. # 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 +# Sync the hosted registry, prop docs, and installed @agents-ui components +# in apps/www and apps/docs into livekit/web pnpm shadcn:publish:livekit-web # Run both downstream publish scripts pnpm shadcn:publish:all ``` +Each script clones the target repo into a temp directory, makes the change, and prints the +repo, branch, title, body, and a `git diff --stat` summary before asking `Create this PR? +(y/N)`. Pass `-y`/`--yes` to skip that prompt and auto-approve (still prints the summary +first): + +```bash +# From packages/shadcn — no -- needed +pnpm shadcn:publish:livekit-web -y + +# From the repo root — shadcn:publish:all goes through turbo, which requires +# -- to forward flags to the underlying task +pnpm shadcn:publish:all -- -y +``` + +#### Dry run + +To see the full diff a run would produce — including the confirmation summary — without +ever committing, pushing, or opening a PR, just answer `n` (or let it default) at the +prompt. This still does all the real work (build, clone, install, format), it just stops +before touching anything remote: + +```bash +echo "n" | pnpm shadcn:publish:livekit-web +``` + +The temp clone, local registry server, and any `.env.local` changes are cleaned up +automatically whether you confirm, decline, or `Ctrl-C` out mid-run. + ### Environment Variables Create a `.env.local` file with: diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx index 808be9302..3b0ca9b12 100644 --- a/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-grid.tsx @@ -11,7 +11,7 @@ import React, { useMemo, } from 'react'; import { type VariantProps, cva } from 'class-variance-authority'; -import { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client'; +import type { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client'; import { type AgentState, type TrackReferenceOrPlaceholder, diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx index 1d2190674..9eb790d12 100644 --- a/packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-wave.tsx @@ -7,7 +7,7 @@ import { type AgentState, type TrackReferenceOrPlaceholder } from '@livekit/comp import { ReactShaderToy } from '@/components/agents-ui/react-shader-toy'; import { useAgentAudioVisualizerWave } from '@/hooks/agents-ui/use-agent-audio-visualizer-wave'; import { cn } from '@/lib/utils'; -import { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client'; +import type { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client'; const DEFAULT_COLOR = '#1FD5F9'; diff --git a/packages/shadcn/components/agents-ui/agent-disconnect-button.tsx b/packages/shadcn/components/agents-ui/agent-disconnect-button.tsx index d4e2f9044..976e7bc0c 100644 --- a/packages/shadcn/components/agents-ui/agent-disconnect-button.tsx +++ b/packages/shadcn/components/agents-ui/agent-disconnect-button.tsx @@ -1,7 +1,7 @@ 'use client'; import { type ComponentProps } from 'react'; -import { Button, buttonVariants } from '@/components/ui/button'; +import { Button, type buttonVariants } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { useSessionContext } from '@livekit/components-react'; import { type VariantProps } from 'class-variance-authority'; diff --git a/packages/shadcn/components/agents-ui/agent-session-provider.tsx b/packages/shadcn/components/agents-ui/agent-session-provider.tsx index 54ffe7465..d9c0af47c 100644 --- a/packages/shadcn/components/agents-ui/agent-session-provider.tsx +++ b/packages/shadcn/components/agents-ui/agent-session-provider.tsx @@ -5,7 +5,7 @@ import { type SessionProviderProps, type RoomAudioRendererProps, } from '@livekit/components-react'; -import { Room } from 'livekit-client'; +import { type Room } from 'livekit-client'; /** * Props for the AgentSessionProvider component. diff --git a/packages/shadcn/components/agents-ui/agent-track-control.tsx b/packages/shadcn/components/agents-ui/agent-track-control.tsx index 21e3050c7..d74c3e147 100644 --- a/packages/shadcn/components/agents-ui/agent-track-control.tsx +++ b/packages/shadcn/components/agents-ui/agent-track-control.tsx @@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from 'react'; import { type VariantProps, cva } from 'class-variance-authority'; -import { LocalAudioTrack, LocalVideoTrack } from 'livekit-client'; import { type TrackReferenceOrPlaceholder, useMaybeRoomContext, @@ -17,7 +16,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; -import { toggleVariants } from '@/components/ui/toggle'; +import { type toggleVariants } from '@/components/ui/toggle'; import { cn } from '@/lib/utils'; const selectVariants = cva( diff --git a/packages/shadcn/components/agents-ui/react-shader-toy.tsx b/packages/shadcn/components/agents-ui/react-shader-toy.tsx index 8d05a2e9a..e46e36161 100644 --- a/packages/shadcn/components/agents-ui/react-shader-toy.tsx +++ b/packages/shadcn/components/agents-ui/react-shader-toy.tsx @@ -171,10 +171,6 @@ const uniformTypeToGLSLType = (t: string) => { const LinearFilter = 9729; const NearestFilter = 9728; const LinearMipMapLinearFilter = 9987; -const NearestMipMapLinearFilter = 9986; -const LinearMipMapNearestFilter = 9985; -const NearestMipMapNearestFilter = 9984; -const MirroredRepeatWrapping = 33648; const ClampToEdgeWrapping = 33071; const RepeatWrapping = 10497; diff --git a/packages/shadcn/components/agents-ui/start-audio-button.tsx b/packages/shadcn/components/agents-ui/start-audio-button.tsx index d8f74dd83..7ab90d139 100644 --- a/packages/shadcn/components/agents-ui/start-audio-button.tsx +++ b/packages/shadcn/components/agents-ui/start-audio-button.tsx @@ -1,7 +1,7 @@ import { type ComponentProps } from 'react'; import { useEnsureRoom, useStartAudio } from '@livekit/components-react'; import { Button } from '@/components/ui/button'; -import { Room } from 'livekit-client'; +import { type Room } from 'livekit-client'; /** * Props for the StartAudioButton component. diff --git a/packages/shadcn/scripts/lib/publish-utils.ts b/packages/shadcn/scripts/lib/publish-utils.ts index 100163f54..72730bec2 100644 --- a/packages/shadcn/scripts/lib/publish-utils.ts +++ b/packages/shadcn/scripts/lib/publish-utils.ts @@ -1,4 +1,4 @@ -import { execFileSync, type ChildProcess } from 'node:child_process'; +import { execFileSync, spawn, type ChildProcess } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -40,6 +40,31 @@ export function formatChanges(cwd: string, cmd: string[] = ['pnpm', 'format']): run(cmd, { cwd }); } +export async function serveRegistry(shadcnPkgDir: string, port: number): Promise { + console.log('--------------------------------'); + console.log(`Serving registry on port ${port}`); + const server = spawn('python3', ['-m', 'http.server', String(port), '-d', './dist'], { + cwd: shadcnPkgDir, + stdio: 'ignore', + }); + await waitForHttp(`http://localhost:${port}/r/registry.json`); + return server; +} + +export function pointRegistryAt(componentsJsonPath: string, registryUrl: string): void { + console.log('--------------------------------'); + console.log(`Pointing ${componentsJsonPath} at ${registryUrl}`); + const componentsJson = JSON.parse(fs.readFileSync(componentsJsonPath, 'utf-8')); + componentsJson.registries['@agents-ui'] = registryUrl; + fs.writeFileSync(componentsJsonPath, JSON.stringify(componentsJson, null, 2) + '\n'); +} + +export function revertFile(cwd: string, relativePath: string): void { + console.log('--------------------------------'); + console.log(`Reverting temporary change to ${relativePath}`); + run(['git', 'checkout', '--', relativePath], { cwd }); +} + export function ghAuthPreflight(): void { try { execFileSync('gh', ['auth', 'status'], { stdio: 'ignore' }); @@ -61,6 +86,24 @@ export function isPortInUse(port: number): boolean { } } +/** + * killServer() sends SIGTERM without waiting for the process to actually exit, so a + * server killed at the end of one publish run may still hold the port for a moment + * when the next run starts (e.g. back-to-back in publish-downstream.ts). Poll instead + * of failing immediately on the first check. + */ +export async function waitForPortFree(port: number, timeoutMs = 5_000): Promise { + const start = Date.now(); + while (isPortInUse(port)) { + if (Date.now() - start > timeoutMs) { + throw new Error( + `Port ${port} is already in use. Stop any running shadcn:serve process and try again.`, + ); + } + await new Promise((resolve) => setTimeout(resolve, 200)); + } +} + export async function waitForHttp(url: string, timeoutMs = 10_000): Promise { const start = Date.now(); while (Date.now() - start < timeoutMs) { @@ -141,14 +184,30 @@ export function onInterrupt(cleanup: () => void): () => void { }; } +// A second readline.createInterface() on process.stdin after closing the first one +// never receives further input (it hangs indefinitely) — so when a single process +// prompts more than once (e.g. publish-downstream.ts running both publish scripts +// in-process), all prompts must share one interface, closed exactly once at exit +// via closePromptInterface(). +let sharedPromptInterface: readline.Interface | undefined; + +function getPromptInterface(): readline.Interface { + sharedPromptInterface ??= readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + return sharedPromptInterface; +} + +export function closePromptInterface(): void { + sharedPromptInterface?.close(); + sharedPromptInterface = undefined; +} + 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(); - } + const rl = getPromptInterface(); + const answer = await rl.question(`${question} (y/N): `); + return ['y', 'yes'].includes(answer.trim().toLowerCase()); } export function cloneAndCreateBranch( @@ -159,21 +218,24 @@ export function cloneAndCreateBranch( ): { tmpDir: string; branch: string } { const tmpDir = mkTempClone(tmpDirPrefix); console.log('--------------------------------'); - console.log(`Cloning ${repo} into ${tmpDir}`); - run(['gh', 'repo', 'clone', repo, tmpDir]); - console.log(`Fetching latest origin/${baseBranch}`); - run( - [ - 'git', - 'fetch', - '--prune', - 'origin', - `+refs/heads/${baseBranch}:refs/remotes/origin/${baseBranch}`, - ], - { - cwd: tmpDir, - }, - ); + console.log(`Cloning ${repo} (${baseBranch}, depth 1) into ${tmpDir}`); + // Shallow, single-branch clone: this helper is only ever used to branch off the + // tip of baseBranch, diff, commit, and push — never git log/blame/tags. Don't + // reuse this path for a caller that needs history beyond HEAD. + run([ + 'gh', + 'repo', + 'clone', + repo, + tmpDir, + '--', + '--depth', + '1', + '--single-branch', + '--branch', + baseBranch, + '--no-tags', + ]); const branch = branchName(branchPrefix); run(['git', 'checkout', '-b', branch, `origin/${baseBranch}`], { cwd: tmpDir }); diff --git a/packages/shadcn/scripts/publish-agent-starter-react.ts b/packages/shadcn/scripts/publish-agent-starter-react.ts index fb34a4617..7249a7347 100644 --- a/packages/shadcn/scripts/publish-agent-starter-react.ts +++ b/packages/shadcn/scripts/publish-agent-starter-react.ts @@ -1,5 +1,4 @@ -import { spawn, type ChildProcess } from 'node:child_process'; -import fs from 'node:fs'; +import { type ChildProcess } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { @@ -7,16 +6,19 @@ import { cloneAndCreateBranch, commitPushAndOpenPr, formatChanges, + closePromptInterface, ghAuthPreflight, hasCliFlag, hasDiff, - isPortInUse, killServer, onInterrupt, + pointRegistryAt, removeTempDir, + revertFile, run, runCapture, - waitForHttp, + serveRegistry, + waitForPortFree, } from './lib/publish-utils.ts'; type PublishOptions = { @@ -34,26 +36,6 @@ 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'; -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'); @@ -66,12 +48,6 @@ function installComponents(tmpDir: string): void { }); } -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\`.`; } @@ -82,11 +58,7 @@ export async function publishAgentStarterReact(options: PublishOptions = {}): Pr prUrl?: string; }> { ghAuthPreflight(); - if (isPortInUse(PORT)) { - throw new Error( - `Port ${PORT} is already in use. Stop any running shadcn:serve process and try again.`, - ); - } + await waitForPortFree(PORT); let server: ChildProcess | undefined; let tmpDir: string | undefined; @@ -100,14 +72,15 @@ export async function publishAgentStarterReact(options: PublishOptions = {}): Pr if (!options.registryAlreadyBuilt) { buildRegistry(SHADCN_PKG_DIR); } - server = await serveRegistry(); + server = await serveRegistry(SHADCN_PKG_DIR, PORT); const cloned = cloneAndCreateBranch(REPO, 'agent-starter-react-', BRANCH_PREFIX, BASE_BRANCH); tmpDir = cloned.tmpDir; - pointComponentsJsonAtLocalRegistry(tmpDir); + const componentsJsonPath = path.join(tmpDir, 'components.json'); + pointRegistryAt(componentsJsonPath, REGISTRY_URL); installComponents(tmpDir); - revertComponentsJson(tmpDir); + revertFile(tmpDir, 'components.json'); formatChanges(tmpDir); if (!hasDiff(tmpDir)) { @@ -145,5 +118,6 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { .catch((err) => { console.error(err); process.exitCode = 1; - }); + }) + .finally(() => closePromptInterface()); } diff --git a/packages/shadcn/scripts/publish-downstream.ts b/packages/shadcn/scripts/publish-downstream.ts index f56d3534b..58f19c6a5 100644 --- a/packages/shadcn/scripts/publish-downstream.ts +++ b/packages/shadcn/scripts/publish-downstream.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { buildRegistry, hasCliFlag } from './lib/publish-utils.ts'; +import { buildRegistry, closePromptInterface, hasCliFlag } from './lib/publish-utils.ts'; import { publishAgentStarterReact } from './publish-agent-starter-react.ts'; import { publishLivekitWeb } from './publish-livekit-web.ts'; @@ -48,6 +48,7 @@ async function publishDownstream(autoApprove: boolean): Promise { if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { const autoApprove = hasCliFlag('-y', '--yes'); const results = await publishDownstream(autoApprove); + closePromptInterface(); 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 index 21baf3044..fd349eca5 100644 --- a/packages/shadcn/scripts/publish-livekit-web.ts +++ b/packages/shadcn/scripts/publish-livekit-web.ts @@ -1,18 +1,25 @@ +import { type ChildProcess } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { + buildRegistry, cloneAndCreateBranch, commitPushAndOpenPr, - formatChanges, + closePromptInterface, ghAuthPreflight, hasCliFlag, hasDiff, installDependencies, + killServer, onInterrupt, + pointRegistryAt, removeTempDir, + revertFile, run, runCapture, + serveRegistry, + waitForPortFree, } from './lib/publish-utils.ts'; type PublishOptions = { @@ -24,10 +31,13 @@ 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 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(agents-ui): update registry + prop-types from @livekit/agents-ui'; const PR_TITLE = 'chore(agents-ui): update registry + prop-types'; +const APPS = ['www', 'docs']; function writeTempEnvLocal(tmpDir: string): void { console.log('--------------------------------'); @@ -40,17 +50,12 @@ function writeTempEnvLocal(tmpDir: string): void { ); } -function deployRegistry(registryAlreadyBuilt: boolean): void { +function copyRegistryFiles(): void { console.log('--------------------------------'); - if (registryAlreadyBuilt) { - console.log('Deploying already-built registry'); - run(['node', '--experimental-strip-types', '--env-file=.env.local', './scripts/update.ts'], { - cwd: SHADCN_PKG_DIR, - }); - } else { - console.log('Building and deploying registry'); - run(['pnpm', 'shadcn:deploy'], { cwd: SHADCN_PKG_DIR }); - } + console.log('Copying built registry into the tmp livekit/web checkout'); + run(['node', '--experimental-strip-types', '--env-file=.env.local', './scripts/update.ts'], { + cwd: SHADCN_PKG_DIR, + }); } function buildFormatDependencies(tmpDir: string): void { @@ -59,6 +64,24 @@ function buildFormatDependencies(tmpDir: string): void { run(['pnpm', '--filter', '@repo/prettier-plugin-markdoc', 'build'], { cwd: tmpDir }); } +function installAgentsUiComponents(tmpDir: string): void { + const componentsJsonPaths = APPS.map((app) => path.join(tmpDir, 'apps', app, 'components.json')); + + for (const componentsJsonPath of componentsJsonPaths) { + pointRegistryAt(componentsJsonPath, REGISTRY_URL); + } + + for (const app of APPS) { + console.log('--------------------------------'); + console.log(`Installing agents-ui components in apps/${app}`); + run(['pnpm', '--filter', app, 'shadcn:install'], { cwd: tmpDir }); + } + + for (const app of APPS) { + revertFile(tmpDir, path.join('apps', app, 'components.json')); + } +} + function restoreEnvLocal(hadEnvLocal: boolean, envBackup: string | null): void { if (hadEnvLocal && envBackup !== null) { fs.writeFileSync(ENV_LOCAL_PATH, envBackup); @@ -68,7 +91,7 @@ function restoreEnvLocal(hadEnvLocal: boolean, envBackup: string | null): void { } 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\`.`; + return `Updates \`apps/www/agents-ui-registry\`, \`apps/docs/lib/shadcn/prop-types.json\`, and the installed \`@agents-ui\` components in \`apps/www\` and \`apps/docs\` 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(options: PublishOptions = {}): Promise<{ @@ -77,25 +100,33 @@ export async function publishLivekitWeb(options: PublishOptions = {}): Promise<{ prUrl?: string; }> { ghAuthPreflight(); + await waitForPortFree(PORT); + let server: ChildProcess | undefined; let tmpDir: string | undefined; const hadEnvLocal = fs.existsSync(ENV_LOCAL_PATH); const envBackup = hadEnvLocal ? fs.readFileSync(ENV_LOCAL_PATH, 'utf-8') : null; const cleanup = () => { + killServer(server); restoreEnvLocal(hadEnvLocal, envBackup); removeTempDir(tmpDir); }; const unregisterInterruptHandler = onInterrupt(cleanup); try { + if (!options.registryAlreadyBuilt) { + buildRegistry(SHADCN_PKG_DIR); + } + server = await serveRegistry(SHADCN_PKG_DIR, PORT); + const cloned = cloneAndCreateBranch(REPO, 'livekit-web-', BRANCH_PREFIX, BASE_BRANCH); tmpDir = cloned.tmpDir; writeTempEnvLocal(tmpDir); - deployRegistry(options.registryAlreadyBuilt ?? false); + copyRegistryFiles(); installDependencies(tmpDir); buildFormatDependencies(tmpDir); - formatChanges(tmpDir, ['pnpm', '--filter', 'www', '--filter', 'docs', 'format']); + installAgentsUiComponents(tmpDir); if (!hasDiff(tmpDir)) { console.log('No changes after sync — skipping PR'); @@ -132,5 +163,6 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) { .catch((err) => { console.error(err); process.exitCode = 1; - }); + }) + .finally(() => closePromptInterface()); } From 3e469d9f78909aeed58204cd051c7fffaa1258c4 Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Thu, 30 Jul 2026 11:03:50 -0400 Subject: [PATCH 09/10] fix(shadcn): preserve catalog: versions after shadcn:install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit shadcn add doesn't understand pnpm's catalog: protocol — installing @agents-ui components overwrote @livekit/components-react and livekit-client in apps/www and apps/docs package.json with concrete pinned versions (e.g. catalog: -> ^2.9.21), forking them off the workspace catalog. Restore catalog: for both after shadcn:install runs and reinstall so pnpm-lock.yaml stays consistent. Co-Authored-By: Claude Sonnet 5 --- .../shadcn/scripts/publish-livekit-web.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/shadcn/scripts/publish-livekit-web.ts b/packages/shadcn/scripts/publish-livekit-web.ts index fd349eca5..504750296 100644 --- a/packages/shadcn/scripts/publish-livekit-web.ts +++ b/packages/shadcn/scripts/publish-livekit-web.ts @@ -38,6 +38,10 @@ 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'; const APPS = ['www', 'docs']; +// shadcn add doesn't know about pnpm's catalog: protocol — it overwrites these with a +// concrete version pinned to whatever the @agents-ui registry entry declares. Restore +// the workspace's catalog reference afterward so we don't fork these off the catalog. +const CATALOG_DEPENDENCIES = ['@livekit/components-react', 'livekit-client']; function writeTempEnvLocal(tmpDir: string): void { console.log('--------------------------------'); @@ -64,6 +68,28 @@ function buildFormatDependencies(tmpDir: string): void { run(['pnpm', '--filter', '@repo/prettier-plugin-markdoc', 'build'], { cwd: tmpDir }); } +function restoreCatalogDependencies(packageJsonPath: string): void { + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + let changed = false; + + for (const section of ['dependencies', 'devDependencies', 'peerDependencies']) { + const deps = pkg[section]; + if (!deps) continue; + for (const name of CATALOG_DEPENDENCIES) { + if (name in deps && deps[name] !== 'catalog:') { + deps[name] = 'catalog:'; + changed = true; + } + } + } + + if (changed) { + console.log('--------------------------------'); + console.log(`Restoring catalog: versions in ${packageJsonPath}`); + fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n'); + } +} + function installAgentsUiComponents(tmpDir: string): void { const componentsJsonPaths = APPS.map((app) => path.join(tmpDir, 'apps', app, 'components.json')); @@ -80,6 +106,19 @@ function installAgentsUiComponents(tmpDir: string): void { for (const app of APPS) { revertFile(tmpDir, path.join('apps', app, 'components.json')); } + + let restoredAnyCatalogVersion = false; + for (const app of APPS) { + const packageJsonPath = path.join(tmpDir, 'apps', app, 'package.json'); + const before = fs.readFileSync(packageJsonPath, 'utf-8'); + restoreCatalogDependencies(packageJsonPath); + if (fs.readFileSync(packageJsonPath, 'utf-8') !== before) { + restoredAnyCatalogVersion = true; + } + } + if (restoredAnyCatalogVersion) { + installDependencies(tmpDir); + } } function restoreEnvLocal(hadEnvLocal: boolean, envBackup: string | null): void { From 1e271d6051f219f39fb7362091d8ffae9b38cdcc Mon Sep 17 00:00:00 2001 From: Thomas Yuill Date: Thu, 30 Jul 2026 12:07:29 -0400 Subject: [PATCH 10/10] move license position --- .../components/agents-ui/agent-audio-visualizer-aura.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx b/packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx index c01e39cc3..305fed9a7 100644 --- a/packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx +++ b/packages/shadcn/components/agents-ui/agent-audio-visualizer-aura.tsx @@ -1,3 +1,5 @@ +'use client'; + /** * @license * @@ -10,8 +12,6 @@ * © 2026 UNCRN LLC */ -'use client'; - import React, { useMemo, type ComponentProps } from 'react'; import { type VariantProps, cva } from 'class-variance-authority'; import { type LocalAudioTrack, type RemoteAudioTrack } from 'livekit-client';