I created two scripts that do the same thing but in different ways. The first one uses jszip to extract a ZIP and sends each file to V0 by using the v0.chats.updateVersion() function, and in the end sends a message. The second one uses v0.chats.init() with a ZIP URL as a parameter, then sends a message.
Both of these scripts has the same goal:
- init() a chat
- upload a template project
- send a message
Both scripts are using init(). (shit code, both scripts is just to show the behavior)
// v0-init.ts
import 'dotenv/config';
import { createClient } from 'v0-sdk';
import JSZip from 'jszip';
const isTextFile = (filename: string) => {
const textExtensions = [
'.ts',
'.tsx',
'.js',
'.jsx',
'.json',
'.css',
'.scss',
'.html',
'.md',
'.txt',
'.svg',
'.yaml',
'.yml',
'.toml',
'.env',
'.gitignore',
'.eslintrc',
'.prettierrc',
'.mjs',
'.tsbuildinfo'
];
const basename = filename.split('/').pop() ?? '';
return (
textExtensions.some((ext) => filename.endsWith(ext)) ||
basename.startsWith('.') ||
!basename.includes('.')
);
};
const main = async () => {
const apiKey = process.env.V0_API_KEY;
const v0 = createClient({ apiKey });
const initResult = await v0.chats.init({
name: 'init test with no files',
type: 'files',
files: [{ name: '.gitkeep', content: '' }]
});
const chatId = initResult.id;
const versionId = initResult.latestVersion?.id;
if (!versionId) {
throw new Error(`Init returned chat=${chatId} without latestVersion.id`);
}
const response = await fetch(
'.../heads/main.zip'
);
if (!response.ok) {
throw new Error(`Failed to fetch zip: ${response.status}`);
}
const zipBuffer = await response.arrayBuffer();
const zip = await JSZip.loadAsync(zipBuffer);
const files: { name: string; content: string }[] = [];
const entries = Object.keys(zip.files);
const rootFolder = (entries[0]?.split('/')[0] ?? '') + '/';
for (const [path, zipEntry] of Object.entries(zip.files)) {
if (zipEntry.dir) continue;
const relativePath = path.startsWith(rootFolder) ? path.slice(rootFolder.length) : path;
if (!relativePath) continue;
if (!isTextFile(relativePath)) {
console.log(`Skipping binary file: ${relativePath}`);
continue;
}
const content = await zipEntry.async('string');
files.push({ name: relativePath, content });
}
await v0.chats.updateVersion({
chatId,
versionId,
files
});
await v0.chats.sendMessage({
chatId,
message: 'make it darkmode only'
});
console.log('uwu done!');
console.log(`chatUrl=https://v0.app/chat/${chatId}`);
};
main().catch((err) => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});
// v0-init-zip.ts
import 'dotenv/config';
import { createClient } from 'v0-sdk';
const main = async () => {
const apiKey = process.env.V0_API_KEY;
if (!apiKey) {
throw new Error('V0_API_KEY is missing from environment.');
}
const v0 = createClient({ apiKey });
const initResult = await v0.chats.init({
name: 'init test from zip',
type: 'zip',
zip: {
url: '.../heads/main.zip'
}
});
const chatId = initResult.id;
const versionId = initResult.latestVersion?.id;
if (!versionId) {
throw new Error(`Init returned chat=${chatId} without latestVersion.id`);
}
await v0.chats.sendMessage({
chatId,
message: 'make it darkmode only'
});
console.log('uwu done!');
console.log(`chatUrl=https://v0.app/chat/${chatId}`);
};
main().catch((err) => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});
Both script runs, but the one using init() with the ZIP file as a parameter, created a chat with no file:
While the one doing the workaround by extracting the ZIP, using the updateVersion etc., did everything as expected:
Also, the version with no files, when it runs the init(), it creates a version, but this version remains on “pending” forever.
Please let me know if I’m doing something wrong!


