Skip to content

Commit a0a1ac5

Browse files
committed
feat(cli): inline agent skills at scaffold time instead of plugin subprocess installs
create-vercel-shop already downloads the full repo tarball, so extract packages/plugin/skills into .agents/skills/ (with per-skill Claude Code symlinks in .claude/skills/, copy fallback) and packages/plugin/commands into .claude/commands/ from the same single fetch. - Removes the three post-scaffold 'npx plugins add' subprocesses, which ran even when the dependency install failed - Skills are now versioned with the scaffolded project and always as fresh as main at scaffold time - --no-template now inlines only the agent assets into an existing project and returns 1 cleanly when the download fails - Companion plugins (vercel/vercel-plugin, Shopify/shopify-ai-toolkit) are recommended via printed commands instead of auto-installed - Updated template AGENTS.md/README, init-vercel-shop, build-shop, and update-shop skills, and the docs pages describing the flow; ran the docs skill sync script
1 parent 02831c2 commit a0a1ac5

20 files changed

Lines changed: 301 additions & 150 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'create-vercel-shop': minor
3+
---
4+
5+
Inline agent skills at scaffold time instead of shelling out to `npx plugins add`. The CLI already downloads the full repo tarball, so it now extracts `packages/plugin/skills` into `.agents/skills/` (with per-skill Claude Code symlinks in `.claude/skills/`, falling back to copies where symlinks are unavailable) and `packages/plugin/commands` into `.claude/commands/` from the same single fetch. This removes the three post-install `npx plugins add` subprocesses — which ran even after a failed dependency install — and makes the skills versioned with the scaffolded project. `--no-template` now inlines only the agent assets into an existing project and fails cleanly if the download fails. The optional companion plugins (`vercel/vercel-plugin`, `Shopify/shopify-ai-toolkit`) are no longer auto-installed; the CLI prints the install commands instead.

apps/cli/index.mjs

Lines changed: 119 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
#!/usr/bin/env node
22
import { spawn } from 'node:child_process';
33
import { 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';
514
import { get } from 'node:https';
15+
import { tmpdir } from 'node:os';
616
import { join, resolve } from 'node:path';
717
import { createInterface } from 'node:readline/promises';
818
import { pathToFileURL } from 'node:url';
@@ -12,6 +22,8 @@ export const DEFAULT_PROJECT_NAME = 'my-shop';
1222
export const TEMPLATE_TARBALL_URL =
1323
'https://codeload.github.com/vercel/shop/tar.gz/refs/heads/main';
1424
export 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

1628
const PACKAGE_MANAGER_FLAGS = {
1729
'--use-bun': 'bun',
@@ -21,12 +33,6 @@ const PACKAGE_MANAGER_FLAGS = {
2133
};
2234
const 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-
3036
export 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+
137183
export 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

161223
export 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

218260
export 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

Comments
 (0)