Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 21 additions & 4 deletions packages/shadcn/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions packages/shadcn/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./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",
Expand Down
229 changes: 229 additions & 0 deletions packages/shadcn/scripts/lib/publish-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
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 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' });
} 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<void> {
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 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<boolean> {
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,
baseBranch: string,
): { 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,
},
);

const branch = branchName(branchPrefix);
run(['git', 'checkout', '-b', branch, `origin/${baseBranch}`], { cwd: tmpDir });

return { tmpDir, branch };
}

async function confirmPrCreation(opts: {
repo: string;
cwd: string;
branch: string;
base: string;
title: string;
body: string;
}): Promise<boolean> {
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<string | undefined> {
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;
}
Loading
Loading