11#!/usr/bin/env node
22import { spawn } from 'node:child_process' ;
33import { realpathSync } from 'node:fs' ;
4- import { mkdir , readFile , writeFile } from 'node:fs/promises' ;
4+ import {
5+ cp ,
6+ mkdir ,
7+ mkdtemp ,
8+ readdir ,
9+ readFile ,
10+ rm ,
11+ symlink ,
12+ writeFile ,
13+ } from 'node:fs/promises' ;
514import { get } from 'node:https' ;
15+ import { tmpdir } from 'node:os' ;
616import { join , resolve } from 'node:path' ;
717import { createInterface } from 'node:readline/promises' ;
818import { pathToFileURL } from 'node:url' ;
@@ -12,6 +22,8 @@ export const DEFAULT_PROJECT_NAME = 'my-shop';
1222export const TEMPLATE_TARBALL_URL =
1323 'https://codeload.github.com/vercel/shop/tar.gz/refs/heads/main' ;
1424export const TEMPLATE_TARBALL_PREFIX = 'shop-main/apps/template' ;
25+ export const SKILLS_TARBALL_PREFIX = 'shop-main/packages/plugin/skills' ;
26+ export const COMMANDS_TARBALL_PREFIX = 'shop-main/packages/plugin/commands' ;
1527
1628const PACKAGE_MANAGER_FLAGS = {
1729 '--use-bun' : 'bun' ,
@@ -21,12 +33,6 @@ const PACKAGE_MANAGER_FLAGS = {
2133} ;
2234const INTERNAL_FLAGS = new Set ( [ NO_TEMPLATE_FLAG , ...Object . keys ( PACKAGE_MANAGER_FLAGS ) ] ) ;
2335
24- const pluginInstalls = [
25- [ 'vercel/shop' , '--scope' , 'project' , '--yes' ] ,
26- [ 'vercel/vercel-plugin' , '--scope' , 'project' , '--yes' ] ,
27- [ 'Shopify/shopify-ai-toolkit' , '--scope' , 'project' , '--yes' ] ,
28- ] ;
29-
3036export function explicitPackageManager ( args ) {
3137 for ( const arg of args ) {
3238 if ( PACKAGE_MANAGER_FLAGS [ arg ] ) return PACKAGE_MANAGER_FLAGS [ arg ] ;
@@ -134,28 +140,84 @@ function fetchResponse(url, depth = 0) {
134140 } ) ;
135141}
136142
143+ export async function inlineAgentAssets ( projectDir , stagingDir ) {
144+ const skillsSrc = join ( stagingDir , SKILLS_TARBALL_PREFIX ) ;
145+ const commandsSrc = join ( stagingDir , COMMANDS_TARBALL_PREFIX ) ;
146+
147+ const agentSkillsDir = join ( projectDir , '.agents' , 'skills' ) ;
148+ const claudeSkillsDir = join ( projectDir , '.claude' , 'skills' ) ;
149+ const claudeCommandsDir = join ( projectDir , '.claude' , 'commands' ) ;
150+
151+ await mkdir ( agentSkillsDir , { recursive : true } ) ;
152+ await mkdir ( claudeSkillsDir , { recursive : true } ) ;
153+ await mkdir ( claudeCommandsDir , { recursive : true } ) ;
154+
155+ const skillNames = ( await readdir ( skillsSrc , { withFileTypes : true } ) )
156+ . filter ( ( entry ) => entry . isDirectory ( ) )
157+ . map ( ( entry ) => entry . name )
158+ . sort ( ) ;
159+
160+ for ( const name of skillNames ) {
161+ const canonical = join ( agentSkillsDir , name ) ;
162+ await rm ( canonical , { force : true , recursive : true } ) ;
163+ await cp ( join ( skillsSrc , name ) , canonical , { recursive : true } ) ;
164+
165+ // Claude Code discovers project skills in .claude/skills. Link to the
166+ // canonical .agents/skills copy (same layout `npx skills add` produces);
167+ // fall back to a copy where symlinks are unavailable (e.g. Windows
168+ // without Developer Mode).
169+ const link = join ( claudeSkillsDir , name ) ;
170+ await rm ( link , { force : true , recursive : true } ) ;
171+ try {
172+ await symlink ( join ( '..' , '..' , '.agents' , 'skills' , name ) , link , 'junction' ) ;
173+ } catch {
174+ await cp ( join ( skillsSrc , name ) , link , { recursive : true } ) ;
175+ }
176+ }
177+
178+ await cp ( commandsSrc , claudeCommandsDir , { recursive : true } ) ;
179+
180+ return skillNames ;
181+ }
182+
137183export async function fetchTemplate (
138184 projectDir ,
139- { url = TEMPLATE_TARBALL_URL , prefix = TEMPLATE_TARBALL_PREFIX } = { } ,
185+ {
186+ includeTemplate = true ,
187+ prefix = TEMPLATE_TARBALL_PREFIX ,
188+ url = TEMPLATE_TARBALL_URL ,
189+ } = { } ,
140190) {
141- const stripComponents = prefix . split ( '/' ) . length ;
142- const tar = spawn (
143- 'tar' ,
144- [ '-xz' , `--strip-components=${ stripComponents } ` , '-C' , projectDir , prefix ] ,
145- { stdio : [ 'pipe' , 'inherit' , 'inherit' ] } ,
146- ) ;
191+ const stagingDir = await mkdtemp ( join ( tmpdir ( ) , 'create-vercel-shop-' ) ) ;
192+
193+ try {
194+ const extractPaths = [ SKILLS_TARBALL_PREFIX , COMMANDS_TARBALL_PREFIX ] ;
195+ if ( includeTemplate ) extractPaths . unshift ( prefix ) ;
147196
148- const tarClosed = new Promise ( ( resolveTar , rejectTar ) => {
149- tar . on ( 'error' , rejectTar ) ;
150- tar . on ( 'close' , ( code ) => {
151- if ( code === 0 ) resolveTar ( ) ;
152- else rejectTar ( new Error ( `tar exited with code ${ code } ` ) ) ;
197+ const tar = spawn ( 'tar' , [ '-xz' , '-C' , stagingDir , ...extractPaths ] , {
198+ stdio : [ 'pipe' , 'inherit' , 'inherit' ] ,
153199 } ) ;
154- } ) ;
155200
156- const response = await fetchResponse ( url ) ;
157- response . pipe ( tar . stdin ) ;
158- await tarClosed ;
201+ const tarClosed = new Promise ( ( resolveTar , rejectTar ) => {
202+ tar . on ( 'error' , rejectTar ) ;
203+ tar . on ( 'close' , ( code ) => {
204+ if ( code === 0 ) resolveTar ( ) ;
205+ else rejectTar ( new Error ( `tar exited with code ${ code } ` ) ) ;
206+ } ) ;
207+ } ) ;
208+
209+ const response = await fetchResponse ( url ) ;
210+ response . pipe ( tar . stdin ) ;
211+ await tarClosed ;
212+
213+ if ( includeTemplate ) {
214+ await cp ( join ( stagingDir , prefix ) , projectDir , { recursive : true } ) ;
215+ }
216+
217+ await inlineAgentAssets ( projectDir , stagingDir ) ;
218+ } finally {
219+ await rm ( stagingDir , { force : true , recursive : true } ) ;
220+ }
159221}
160222
161223export async function ensureProjectDir ( projectDir ) {
@@ -186,33 +248,13 @@ export function initGit(projectDir, run = runCommand) {
186248 return run ( 'git' , [ 'init' , '--quiet' ] , { cwd : projectDir } ) ;
187249}
188250
189- export async function installProjectPlugins ( projectDir , run = runCommand ) {
190- const failures = [ ] ;
191-
192- for ( const args of pluginInstalls ) {
193- const code = await run ( 'npx' , [ 'plugins' , 'add' , ...args ] , {
194- cwd : projectDir ,
195- } ) ;
196-
197- if ( code !== 0 ) {
198- failures . push ( args [ 0 ] ) ;
199- }
200- }
201-
202- return failures ;
203- }
204-
205- export function printRetryCommands ( projectDir , { scaffolded = true } = { } ) {
206- if ( scaffolded ) {
207- console . warn ( '\nVercel Shop scaffolded successfully, but one or more plugin installs failed.' ) ;
208- } else {
209- console . warn ( '\nProject plugin installation failed.' ) ;
210- }
211-
212- console . warn ( `Retry from ${ projectDir } :` ) ;
213- console . warn ( ' npx plugins add vercel/shop --scope project --yes' ) ;
214- console . warn ( ' npx plugins add vercel/vercel-plugin --scope project --yes' ) ;
215- console . warn ( ' npx plugins add Shopify/shopify-ai-toolkit --scope project --yes' ) ;
251+ export function printAgentSetupSummary ( ) {
252+ console . log (
253+ '\nAgent skills installed to .agents/skills (Claude Code links in .claude/skills, commands in .claude/commands).' ,
254+ ) ;
255+ console . log ( '\nRecommended companion plugins (optional, run from the project root):' ) ;
256+ console . log ( ' npx plugins add vercel/vercel-plugin --scope project --yes' ) ;
257+ console . log ( ' npx plugins add Shopify/shopify-ai-toolkit --scope project --yes' ) ;
216258}
217259
218260export async function main ( {
@@ -243,39 +285,45 @@ export async function main({
243285
244286 await ensureProjectDir ( projectDir ) ;
245287
246- if ( ! plan . noTemplate ) {
288+ if ( plan . noTemplate ) {
247289 try {
248- await scaffold ( projectDir ) ;
290+ await scaffold ( projectDir , { includeTemplate : false } ) ;
249291 } catch ( error ) {
250- console . error ( '\nFailed to download the Vercel Shop template .' ) ;
292+ console . error ( '\nFailed to download the Vercel Shop agent skills .' ) ;
251293 console . error ( error instanceof Error ? error . message : String ( error ) ) ;
252294 return 1 ;
253295 }
254296
255- try {
256- const templateVersion = await readTemplateVersion ( importMetaUrl ) ;
257- await writeBootstrapMetadata ( projectDir , templateVersion ) ;
258- } catch ( error ) {
259- console . warn ( '\nScaffold completed, but bootstrap metadata could not be written.' ) ;
260- console . warn ( error instanceof Error ? error . message : String ( error ) ) ;
261- }
262-
263- const installCode = await installDependencies ( projectDir , plan . packageManager , run ) ;
264- if ( installCode !== 0 ) {
265- console . warn (
266- `\n${ plan . packageManager } install failed. Re-run it from ${ projectDir } once resolved.` ,
267- ) ;
268- }
297+ printAgentSetupSummary ( ) ;
298+ return 0 ;
299+ }
269300
270- await initGit ( projectDir , run ) ;
301+ try {
302+ await scaffold ( projectDir ) ;
303+ } catch ( error ) {
304+ console . error ( '\nFailed to download the Vercel Shop template.' ) ;
305+ console . error ( error instanceof Error ? error . message : String ( error ) ) ;
306+ return 1 ;
271307 }
272308
273- const failedPlugins = await installProjectPlugins ( projectDir , run ) ;
309+ try {
310+ const templateVersion = await readTemplateVersion ( importMetaUrl ) ;
311+ await writeBootstrapMetadata ( projectDir , templateVersion ) ;
312+ } catch ( error ) {
313+ console . warn ( '\nScaffold completed, but bootstrap metadata could not be written.' ) ;
314+ console . warn ( error instanceof Error ? error . message : String ( error ) ) ;
315+ }
274316
275- if ( failedPlugins . length > 0 ) {
276- printRetryCommands ( projectDir , { scaffolded : ! plan . noTemplate } ) ;
317+ const installCode = await installDependencies ( projectDir , plan . packageManager , run ) ;
318+ if ( installCode !== 0 ) {
319+ console . warn (
320+ `\n${ plan . packageManager } install failed. Re-run it from ${ projectDir } once resolved.` ,
321+ ) ;
277322 }
278323
324+ await initGit ( projectDir , run ) ;
325+
326+ printAgentSetupSummary ( ) ;
279327 return 0 ;
280328}
281329
0 commit comments