From fb957c207bf829d4437a5defce515afd740098c3 Mon Sep 17 00:00:00 2001 From: nexus_ai Date: Sat, 16 May 2026 14:48:42 +0000 Subject: [PATCH] feat(deploy-vercel): implement real CLI-backed vercel integration Replace stub with actual exec() calls to the Vercel CLI for build and ship. - build(): runs `vercel build` (with `--prod` when on stable channel) - ship(): runs `vercel deploy --token ` (with `--prod` on stable) - Reads token via ctx.secret('VERCEL_TOKEN'), throws descriptive error if missing - Short-circuits on ctx.dryRun in ship() - Parses deployment URL from vercel stdout (https://*.vercel.app) - Passes cwd and env to exec for proper isolation Co-Authored-By: Claude Sonnet 4.6 --- packages/targets/deploy-vercel/src/index.ts | 55 +++++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/targets/deploy-vercel/src/index.ts b/packages/targets/deploy-vercel/src/index.ts index f8b22c4e..ce7d7628 100644 --- a/packages/targets/deploy-vercel/src/index.ts +++ b/packages/targets/deploy-vercel/src/index.ts @@ -1,4 +1,4 @@ -import { defineTarget, manualSetup } from '@profullstack/sh1pt-core'; +import { defineTarget, manualSetup, exec } from '@profullstack/sh1pt-core'; interface Config { project?: string; @@ -10,15 +10,60 @@ export default defineTarget({ id: 'deploy-vercel', kind: 'web', label: 'Vercel', - async build(ctx) { - ctx.log('vercel build'); + async build(ctx, config) { + const prod = config.prod ?? ctx.channel === 'stable'; + ctx.log(`vercel build${prod ? ' --prod' : ''}`); + + const token = ctx.secret('VERCEL_TOKEN'); + if (!token) throw new Error('VERCEL_TOKEN secret not set. Run: sh1pt secret set VERCEL_TOKEN '); + + const args: string[] = ['build']; + if (prod) args.push('--prod'); + + await exec('vercel', args, { + cwd: ctx.projectDir, + log: ctx.log, + throwOnNonZero: true, + env: { VERCEL_TOKEN: token }, + }); + return { artifact: `${ctx.outDir}/vercel-output` }; }, async ship(ctx, config) { const prod = config.prod ?? ctx.channel === 'stable'; - ctx.log(`vercel deploy ${prod ? '--prod' : ''} · project=${config.project ?? 'linked'}`); + ctx.log(`vercel deploy${prod ? ' --prod' : ''} · project=${config.project ?? 'linked'}`); if (ctx.dryRun) return { id: 'dry-run' }; - return { id: `${config.project ?? 'vercel'}@${ctx.version}`, url: undefined }; + + const token = ctx.secret('VERCEL_TOKEN'); + if (!token) { + throw new Error( + 'VERCEL_TOKEN is required for deployment. ' + + 'Create a token at vercel.com/account/tokens, ' + + 'then set it: sh1pt secret set VERCEL_TOKEN ' + ); + } + + const args: string[] = ['deploy', '--token', token]; + if (prod) args.push('--prod'); + + const { stdout } = await exec('vercel', args, { + cwd: ctx.projectDir, + log: ctx.log, + throwOnNonZero: true, + env: { VERCEL_TOKEN: token }, + }); + + // Vercel prints the deployment URL as the last line of stdout + // e.g. "https://my-project-abc123.vercel.app" + const lines = stdout.trim().split('\n').filter(Boolean); + const urlMatch = stdout.match(/https:\/\/[a-zA-Z0-9.-]+\.vercel\.app/); + const url = urlMatch?.[0] ?? lines[lines.length - 1]?.trim(); + + const projectLabel = config.project ?? 'vercel'; + const id = `${projectLabel}@${ctx.version}`; + + ctx.log(`vercel deploy complete · id=${id} · url=${url}`); + return { id, url }; }, setup: manualSetup({ label: 'Vercel CLI',