@stdout-design/cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,36 +1,19 @@
1
1
  #!/usr/bin/env bun
2
- import{createRequire}from"node:module";import{Command}from"commander";import{AppError,ErrorCode,importTemplateForBatch,loadConfig,openCache,runBatch,zodToJsonSchemaShape}from"@stdout-design/core";import path from"node:path";import{existsSync,readFileSync}from"node:fs";import{execSync,spawn,spawnSync}from"node:child_process";import{resolveCommand}from"package-manager-detector/commands";import{detect,getUserAgent}from"package-manager-detector/detect";import{z}from"zod";import{cp,mkdir,writeFile}from"node:fs/promises";const formatBytes=bytes=>bytes<1024?`${bytes} B`:bytes<1048576?`${(bytes/1024).toFixed(1)} KB`:bytes<1073741824?`${(bytes/1048576).toFixed(1)} MB`:`${(bytes/1073741824).toFixed(2)} GB`,formatCacheStats=stats=>[``,`Cache Statistics`,`━━━━━━━━━━━━━━━━`,` Location: ${stats.cacheDir}`,` Compiled: ${stats.compiled.entries} entries (in-memory compile cache)`,` Pixel: ${stats.pixels.entries} entries`,` Disk used: ${formatBytes(stats.pixels.sizeBytes)}`,` Disk limit: ${formatBytes(stats.pixels.maxSizeBytes)}`,``].join(`
2
+ import{createRequire}from"node:module";import{Command}from"commander";import{AppError,ErrorCode,importTemplateForBatch,loadConfig,openCache,runBatch,scriptOf,zodToJsonSchemaShape}from"@stdout-design/core";import path from"node:path";import{existsSync,readFileSync,readdirSync}from"node:fs";import{execSync,spawn,spawnSync}from"node:child_process";import{resolveCommand}from"package-manager-detector/commands";import{detect,getUserAgent}from"package-manager-detector/detect";import{z}from"zod";import{cp,mkdir,readFile,writeFile}from"node:fs/promises";const formatBytes=bytes=>bytes<1024?`${bytes} B`:bytes<1048576?`${(bytes/1024).toFixed(1)} KB`:bytes<1073741824?`${(bytes/1048576).toFixed(1)} MB`:`${(bytes/1073741824).toFixed(2)} GB`,formatCacheStats=stats=>[``,`Cache Statistics`,`━━━━━━━━━━━━━━━━`,` Location: ${stats.cacheDir}`,` Compiled: ${stats.compiled.entries} entries (in-memory compile cache)`,` Pixel: ${stats.pixels.entries} entries`,` Disk used: ${formatBytes(stats.pixels.sizeBytes)}`,` Disk limit: ${formatBytes(stats.pixels.maxSizeBytes)}`,``].join(`
3
3
  `),plural=(count,word)=>`${count} ${word}${count===1?``:`s`}`,formatManifestSummary=(manifest,cwd)=>{let{totalCount,succeeded,failed}=manifest,cacheHits=succeeded.filter(s=>s.cacheHit).length,display=filePath=>cwd&&path.relative(cwd,filePath)||filePath,lines=[``];if(succeeded.length>0){let cached=cacheHits>0?` (${cacheHits} from cache)`:``;lines.push(`✓ Rendered ${plural(succeeded.length,`image`)} of ${totalCount}${cached}`);for(let entry of succeeded.slice(0,10))lines.push(` ${display(entry.outputPath)}`);let hidden=succeeded.length-10;hidden>0&&lines.push(` … and ${hidden} more (see manifest.json in the output dir)`),lines.push(``)}if(failed.length>0){lines.push(`✗ ${plural(failed.length,`render`)} failed:`);for(let f of failed)lines.push(` [${f.preset}, ${f.locale}, row ${f.rowIndex}] ${f.error}`);lines.push(``)}if(manifest.status===`aborted`){let skipped=totalCount-succeeded.length-failed.length;lines.push(`Batch aborted after the first failure (--fail-fast); ${skipped} skipped.`,``)}return totalCount===0&&lines.push(`Nothing to render — the data file has no rows.`,``),lines.join(`
4
4
  `)},formatError=error=>error instanceof Error?`Error: ${error.message}`:`Error: ${String(error)}`,formatCacheCleanResult=freedBytes=>`Cache cleared. Freed ${formatBytes(freedBytes)}.`,CONFIG_FILE=`studio.config.ts`,findProjectRoot=(startDir=process.cwd())=>{let dir=path.resolve(startDir);for(;;){if(existsSync(path.join(dir,CONFIG_FILE)))return dir;let parent=path.dirname(dir);if(parent===dir)return path.resolve(startDir);dir=parent}},requireProjectRoot=(startDir=process.cwd())=>{let root=findProjectRoot(startDir);if(!existsSync(path.join(root,CONFIG_FILE)))throw Error(`No ${CONFIG_FILE} found in ${path.resolve(startDir)} or any parent directory.\nRun \`studio init\` to create a project, or cd into an existing one.`);return root},cacheStats=async(rootDir,options)=>{let cache=await openCache(rootDir??findProjectRoot()),stats=await cache.stats();cache.close(),options.json?console.log(JSON.stringify(stats,null,2)):console.log(formatCacheStats(stats))},cacheClean=async(rootDir,options)=>{let cache=await openCache(rootDir??findProjectRoot()),result=await cache.clean();cache.close(),options.json?console.log(JSON.stringify(result)):console.log(formatCacheCleanResult(result.freedBytes))},openInBrowser=url=>{let[command,args]={darwin:[`open`,[url]],win32:[`cmd`,[`/c`,`start`,``,url]]}[process.platform]??[`xdg-open`,[url]],child=spawn(command,args,{detached:!0,stdio:`ignore`});child.on(`error`,()=>{console.log(`Could not open a browser — visit ${url}`)}),child.unref()},findWebUiDist=()=>{try{let pkgJsonPath=createRequire(import.meta.url).resolve(`@stdout-design/web-ui/package.json`),pkgDir=path.dirname(pkgJsonPath);return path.join(pkgDir,`dist`)}catch{}},dev=async(rootDir,options)=>{let{startStandaloneServer}=await import(`@stdout-design/dev-server`).catch(()=>{throw Error(`studio dev requires a scaffolded project with @stdout-design/dev-server installed.
5
5
  Run \`studio init\` to create one, or install it manually:
6
6
 
7
7
  bun add @stdout-design/dev-server
8
8
 
9
- See https://stdout.design/docs for more information.`)}),resolvedRoot=requireProjectRoot(rootDir),port=Number(options.port??`3030`),webUiDist=findWebUiDist();console.log(`Starting studio dev server for ${resolvedRoot}...`),await startStandaloneServer({port,rootDir:resolvedRoot,webUiDist}),options.open&&openInBrowser(`http://localhost:${port}`)},DLX_MAP={bun:`bunx`,npm:`npx`,pnpm:`pnpm dlx`,yarn:`yarn dlx`},buildSkillListCommand=(pm,global=!1)=>{let parts=(DLX_MAP[pm]??`npx`).split(` `);return{args:[...parts.slice(1),`skills`,`list`,...global?[`-g`]:[],`--json`],command:parts[0]}},buildSkillInstallCommand=pm=>{let parts=(DLX_MAP[pm]??`npx`).split(` `);return{args:[...parts.slice(1),`skills`,`add`,`m7md1alaa/stdout-design`],command:parts[0]}},isSkillInstalledInScope=(pm,global=!1)=>{let{command,args}=buildSkillListCommand(pm,global),result=spawnSync(command,args,{encoding:`utf-8`,stdio:`pipe`});if(result.error||result.status!==0||!result.stdout)return!1;let{stdout}=result;try{return JSON.parse(stdout).some(skill=>skill.name===`studio`)}catch{return!1}},hasSkillInstalled=pm=>isSkillInstalledInScope(pm)||isSkillInstalledInScope(pm,!0),getSkillInstallCommand=pm=>{let{command,args}=buildSkillInstallCommand(pm);return`${command} ${args.join(` `)}`},maybeInstallSkill=async({packageManager,quiet=!1,shouldInstall})=>{if(shouldInstall===void 0&&!quiet&&hasSkillInstalled(packageManager))return!0;let wantsInstall=shouldInstall;if(wantsInstall===void 0){if(quiet)return!1;let{select,isCancel}=await import(`@clack/prompts`),installSkillResult=await select({message:`Do you want to install the studio agent skill?`,options:[{label:`Yes, install it`,value:`install`},{label:`No, I'll do it later`,value:`skip`}]});if(isCancel(installSkillResult))return!1;wantsInstall=installSkillResult===`install`}if(!wantsInstall)return!1;let{command,args}=buildSkillInstallCommand(packageManager),{spinner}=await import(`@clack/prompts`),s=spinner();quiet||s.start(`Installing the studio agent skill...`);let result=spawnSync(command,args,{stdio:`pipe`}),didInstall=!result.error&&result.status===0;return quiet||s.stop(didInstall?`Studio agent skill installed.`:`Couldn't install the studio agent skill automatically.`),didInstall},isInsideGitRepo=dir=>{let current=path.resolve(dir);for(;;){if(existsSync(path.join(current,`.git`)))return!0;let parent=path.dirname(current);if(parent===current)break;current=parent}return!1},tryGitInit=dir=>{try{execSync(`git --version`,{stdio:`ignore`})}catch{return!1}if(existsSync(path.join(dir,`.git`))||isInsideGitRepo(path.dirname(dir)))return!1;try{return execSync(`git init`,{cwd:dir,stdio:`ignore`}),execSync(`git checkout -b main`,{cwd:dir,stdio:`ignore`}),execSync(`git add .`,{cwd:dir,stdio:`ignore`}),execSync(`git commit -m "Initial commit from stdout-design"`,{cwd:dir,stdio:`ignore`}),!0}catch{if(existsSync(path.join(dir,`.git`)))try{execSync(`rm -rf .git`,{cwd:dir,stdio:`ignore`})}catch{}return!1}},projectNameSchema=z.string().min(1,`Project name cannot be empty.`).max(214,`Project name must be 214 characters or fewer.`).regex(/^[a-z0-9][a-z0-9._-]*$/u,`Project name must start with a lowercase letter or number, and can only contain letters, numbers, dots, hyphens, and underscores.`).refine(val=>!/[._-]$/u.test(val),`Project name must not end with a dot, hyphen, or underscore.`).refine(val=>!/__/u.test(val),`Project name must not contain consecutive underscores.`).refine(val=>!/[._-]{2,}/u.test(val),`Project name must not contain consecutive dots, hyphens, or underscores.`),validateProjectName=name=>{let result=projectNameSchema.safeParse(name);return result.success?null:result.error.issues[0]?.message??`Invalid project name.`},isCI=()=>process.env.CI===`true`||process.env.CI===`1`||process.env.CIRCLECI===`true`||!!process.env.GITHUB_ACTIONS||!!process.env.GITLAB_CI,ciOrYes=options=>options?.yes===!0||isCI(),promptOrSkip=async(prompt,clack)=>{let result=await prompt(clack);return clack.isCancel(result)&&(clack.cancel(`Cancelled.`),process.exit(0)),result},collectPrompts=async(clack,projectDir,options)=>{let cwd=process.cwd(),resolvedDir,projectName;projectDir?(resolvedDir=path.resolve(cwd,projectDir),projectName=path.basename(resolvedDir)):ciOrYes(options)?(projectName=`studio-project`,resolvedDir=path.resolve(cwd,projectName)):(projectName=(await promptOrSkip(p=>p.text({defaultValue:`studio-project`,message:`What's your project name?`,validate:value=>{let trimmed=value.trim();return trimmed.length===0?`Please enter a project name.`:validateProjectName(trimmed)}}),clack)).trim(),resolvedDir=path.resolve(cwd,projectName)),!ciOrYes(options)&&existsSync(resolvedDir)&&(await promptOrSkip(p=>p.confirm({initialValue:!1,message:`Directory "${projectName}" already exists. Overwrite?`}),clack)||(clack.cancel(`Cancelled.`),process.exit(0)));let templates=ciOrYes(options)?[`bento-feature`]:await promptOrSkip(p=>p.multiselect({message:`Which templates would you like to scaffold?`,options:[{hint:`Apple-style feature card`,label:`Bento Feature`,value:`bento-feature`}]}),clack),locales=ciOrYes(options)?[`en`,`ar`]:await promptOrSkip(p=>p.multiselect({message:`Which locales would you like to include?`,options:[{label:`English`,value:`en`},{label:`Arabic`,value:`ar`}]}),clack),parentDir=path.dirname(resolvedDir),alreadyInGitRepo=isInsideGitRepo(parentDir),wantsGit=!alreadyInGitRepo&&ciOrYes(options);!alreadyInGitRepo&&!ciOrYes(options)&&(wantsGit=await promptOrSkip(p=>p.confirm({initialValue:!0,message:`Initialize a git repository?`}),clack));let shouldInstall=ciOrYes(options)?!0:await promptOrSkip(p=>p.confirm({initialValue:!0,message:`Install dependencies?`}),clack);return{locales,projectName,resolvedDir,shouldInstall,templates,wantsGit}},getScaffoldVersion=()=>{try{return createRequire(import.meta.url)(`@stdout-design/cli/package.json`).version}catch{return`latest`}},getDefaults=()=>({defaultPreset:`instagram-square`,presets:[{height:1080,id:`instagram-square`,platform:`instagram`,width:1080},{height:675,id:`x-card`,platform:`x`,width:1200}],templates:{"bento-feature":{componentPath:`./templates/bento-feature`,description:`Apple-style bento feature card.`}}}),getConfigTemplate=version=>`import type { StudioConfig } from "@stdout-design/cli";
9
+ See https://stdout.design/docs for more information.`)}),resolvedRoot=requireProjectRoot(rootDir),port=Number(options.port??`3030`),webUiDist=findWebUiDist();console.log(`Starting studio dev server for ${resolvedRoot}...`),await startStandaloneServer({port,rootDir:resolvedRoot,webUiDist}),options.open&&openInBrowser(`http://localhost:${port}`)},DLX_MAP={bun:`bunx`,npm:`npx`,pnpm:`pnpm dlx`,yarn:`yarn dlx`},buildSkillListCommand=(pm,global=!1)=>{let parts=(DLX_MAP[pm]??`npx`).split(` `);return{args:[...parts.slice(1),`skills`,`list`,...global?[`-g`]:[],`--json`],command:parts[0]}},buildSkillInstallCommand=pm=>{let parts=(DLX_MAP[pm]??`npx`).split(` `);return{args:[...parts.slice(1),`skills`,`add`,`m7md1alaa/stdout-design`],command:parts[0]}},isSkillInstalledInScope=(pm,global=!1)=>{let{command,args}=buildSkillListCommand(pm,global),result=spawnSync(command,args,{encoding:`utf-8`,stdio:`pipe`});if(result.error||result.status!==0||!result.stdout)return!1;let{stdout}=result;try{return JSON.parse(stdout).some(skill=>skill.name===`studio`)}catch{return!1}},hasSkillInstalled=pm=>isSkillInstalledInScope(pm)||isSkillInstalledInScope(pm,!0),getSkillInstallCommand=pm=>{let{command,args}=buildSkillInstallCommand(pm);return`${command} ${args.join(` `)}`},maybeInstallSkill=async({packageManager,quiet=!1,shouldInstall})=>{if(shouldInstall===void 0&&!quiet&&hasSkillInstalled(packageManager))return!0;let wantsInstall=shouldInstall;if(wantsInstall===void 0){if(quiet)return!1;let{select,isCancel}=await import(`@clack/prompts`),installSkillResult=await select({message:`Do you want to install the studio agent skill?`,options:[{label:`Yes, install it`,value:`install`},{label:`No, I'll do it later`,value:`skip`}]});if(isCancel(installSkillResult))return!1;wantsInstall=installSkillResult===`install`}if(!wantsInstall)return!1;let{command,args}=buildSkillInstallCommand(packageManager),{spinner}=await import(`@clack/prompts`),s=spinner();quiet||s.start(`Installing the studio agent skill...`);let result=spawnSync(command,args,{stdio:`pipe`}),didInstall=!result.error&&result.status===0;return quiet||s.stop(didInstall?`Studio agent skill installed.`:`Couldn't install the studio agent skill automatically.`),didInstall},isInsideGitRepo=dir=>{let current=path.resolve(dir);for(;;){if(existsSync(path.join(current,`.git`)))return!0;let parent=path.dirname(current);if(parent===current)break;current=parent}return!1},tryGitInit=dir=>{try{execSync(`git --version`,{stdio:`ignore`})}catch{return!1}if(existsSync(path.join(dir,`.git`))||isInsideGitRepo(path.dirname(dir)))return!1;try{return execSync(`git init`,{cwd:dir,stdio:`ignore`}),execSync(`git checkout -b main`,{cwd:dir,stdio:`ignore`}),execSync(`git add .`,{cwd:dir,stdio:`ignore`}),execSync(`git commit -m "Initial commit from stdout-design"`,{cwd:dir,stdio:`ignore`}),!0}catch{if(existsSync(path.join(dir,`.git`)))try{execSync(`rm -rf .git`,{cwd:dir,stdio:`ignore`})}catch{}return!1}},FONTS=[{family:`Archivo Black`,script:`latin`,weights:[400]},{family:`Anton`,script:`latin`,weights:[400]},{family:`Bricolage Grotesque`,script:`latin`,weights:[400,700]},{family:`Instrument Serif`,script:`latin`,weights:[400]},{family:`Space Mono`,script:`latin`,weights:[400,700]},{family:`Lalezar`,script:`arabic`,weights:[400]},{family:`IBM Plex Sans Arabic`,script:`arabic`,weights:[400,700]},{family:`Amiri`,script:`arabic`,weights:[400,700]}],STARTERS=[{description:`Loud streetwear landing page with stickers and a stats strip.`,fonts:[`Archivo Black`,`Space Mono`,`Lalezar`,`IBM Plex Sans Arabic`],hint:`Landing page hero`,id:`launch-hero`,label:`Launch Hero`,since:`0.7.0`},{description:`Budgeting-app bento grid: metric, receipt, chart, and gauge tiles.`,fonts:[`Bricolage Grotesque`,`Space Mono`,`IBM Plex Sans Arabic`],hint:`Product bento grid`,id:`bento-feature`,label:`Bento Feature`,since:`0.1.0`},{description:`Retro print poster with a cassette, tape stripes, and a stamp.`,fonts:[`Anton`,`Archivo Black`,`Instrument Serif`,`Space Mono`,`Lalezar`,`IBM Plex Sans Arabic`,`Amiri`],hint:`Retro print poster`,id:`retro-poster`,label:`Retro Poster`,since:`0.7.0`},{description:`Editorial six-item grid with drawn specimens in each cell.`,fonts:[`Anton`,`Archivo Black`,`Instrument Serif`,`Space Mono`,`Lalezar`,`IBM Plex Sans Arabic`,`Amiri`],hint:`Editorial listicle`,id:`trend-poster`,label:`Trend Poster`,since:`0.7.0`}],STARTER_IDS=STARTERS.map(s=>s.id),isStarterId=id=>STARTER_IDS.includes(id),getStarter=id=>{let found=STARTERS.find(s=>s.id===id);if(!found)throw Error(`Unknown starter "${id}".`);return found},starterFonts=(ids,script)=>{let used=new Set(ids.flatMap(id=>getStarter(id).fonts));return FONTS.filter(font=>used.has(font.family)&&font.script===script).map(({family,weights})=>({family,source:`google`,weights:[...weights]}))},projectNameSchema=z.string().min(1,`Project name cannot be empty.`).max(214,`Project name must be 214 characters or fewer.`).regex(/^[a-z0-9][a-z0-9._-]*$/u,`Project name must start with a lowercase letter or number, and can only contain letters, numbers, dots, hyphens, and underscores.`).refine(val=>!/[._-]$/u.test(val),`Project name must not end with a dot, hyphen, or underscore.`).refine(val=>!/__/u.test(val),`Project name must not contain consecutive underscores.`).refine(val=>!/[._-]{2,}/u.test(val),`Project name must not contain consecutive dots, hyphens, or underscores.`),validateProjectName=name=>{let result=projectNameSchema.safeParse(name);return result.success?null:result.error.issues[0]?.message??`Invalid project name.`},isCI=()=>process.env.CI===`true`||process.env.CI===`1`||process.env.CIRCLECI===`true`||!!process.env.GITHUB_ACTIONS||!!process.env.GITLAB_CI,ciOrYes=options=>options?.yes===!0||isCI(),promptOrSkip=async(prompt,clack)=>{let result=await prompt(clack);return clack.isCancel(result)&&(clack.cancel(`Cancelled.`),process.exit(0)),result},collectPrompts=async(clack,projectDir,options)=>{let cwd=process.cwd(),resolvedDir,projectName;projectDir?(resolvedDir=path.resolve(cwd,projectDir),projectName=path.basename(resolvedDir)):ciOrYes(options)?(projectName=`studio-project`,resolvedDir=path.resolve(cwd,projectName)):(projectName=(await promptOrSkip(p=>p.text({defaultValue:`studio-project`,message:`What's your project name?`,validate:value=>{let trimmed=value.trim();return trimmed.length===0?`Please enter a project name.`:validateProjectName(trimmed)}}),clack)).trim(),resolvedDir=path.resolve(cwd,projectName)),!ciOrYes(options)&&existsSync(resolvedDir)&&(await promptOrSkip(p=>p.confirm({initialValue:!1,message:`Directory "${projectName}" already exists. Overwrite?`}),clack)||(clack.cancel(`Cancelled.`),process.exit(0)));let templates=ciOrYes(options)?STARTER_IDS:await promptOrSkip(p=>p.multiselect({initialValues:STARTER_IDS,message:`Which templates would you like to scaffold?`,options:STARTERS.map(t=>({hint:t.hint,label:t.label,value:t.id}))}),clack),locales=ciOrYes(options)?[`en`,`ar`]:await promptOrSkip(p=>p.multiselect({initialValues:[`en`,`ar`],message:`Which locales would you like to include?`,options:[{label:`English`,value:`en`},{label:`Arabic`,value:`ar`}]}),clack),parentDir=path.dirname(resolvedDir),alreadyInGitRepo=isInsideGitRepo(parentDir),wantsGit=!alreadyInGitRepo&&ciOrYes(options);!alreadyInGitRepo&&!ciOrYes(options)&&(wantsGit=await promptOrSkip(p=>p.confirm({initialValue:!0,message:`Initialize a git repository?`}),clack));let shouldInstall=ciOrYes(options)?!0:await promptOrSkip(p=>p.confirm({initialValue:!0,message:`Install dependencies?`}),clack);return{locales,projectName,resolvedDir,shouldInstall,templates,wantsGit}},getScaffoldVersion=()=>{try{return createRequire(import.meta.url)(`@stdout-design/cli/package.json`).version}catch{return`latest`}},compareVersions=(a,b)=>{let partsA=a.split(`.`).map(Number),partsB=b.split(`.`).map(Number),length=Math.max(partsA.length,partsB.length);for(let i=0;i<length;i+=1){let diff=(partsA[i]??0)-(partsB[i]??0);if(diff!==0)return diff}return 0},PRESETS=[{height:1080,id:`instagram-square`,platform:`instagram`,width:1080},{height:675,id:`x-card`,platform:`x`,width:1200}],fontsFor=({starters,locales})=>{let latin=starterFonts(starters,`latin`),arabic=starterFonts(starters,`arabic`),fonts={};if(latin.length>0&&(fonts.default=latin),arabic.length>0)for(let locale of locales.filter(l=>scriptOf(l)===`arabic`))fonts[locale]=[...latin,...arabic];return Object.keys(fonts).length>0?fonts:void 0},buildProjectConfig=(version,choices)=>({scaffoldVersion:version,defaultPreset:`instagram-square`,outDir:`./out`,locales:choices.locales.length>0?choices.locales:void 0,fonts:fontsFor(choices),presets:PRESETS,templates:Object.fromEntries(choices.starters.map(id=>[id,{componentPath:`./templates/${id}`,description:getStarter(id).description}]))}),startersSince=(scaffoldVersion,installedVersion)=>STARTER_IDS.filter(id=>{let{since}=getStarter(id),isNewToProject=compareVersions(since,scaffoldVersion??`0`)>0,isShipped=installedVersion===`latest`||compareVersions(since,installedVersion)<=0;return isNewToProject&&isShipped}),IDENTIFIER=/^[A-Za-z_$][\w$]*$/u,toLiteral=(value,depth=0)=>{if(typeof value!=`object`||!value)return JSON.stringify(value);let entries=Array.isArray(value)?value.map(item=>toLiteral(item,depth+1)):Object.entries(value).filter(([,item])=>item!==void 0).map(([key,item])=>`${IDENTIFIER.test(key)?key:JSON.stringify(key)}: ${toLiteral(item,depth+1)}`),[open,close]=Array.isArray(value)?[`[`,`]`]:[`{`,`}`];if(entries.length===0)return`${open}${close}`;let inline=Array.isArray(value)?`[${entries.join(`, `)}]`:`{ ${entries.join(`, `)} }`;if(inline.length<=80&&!inline.includes(`
10
+ `))return inline;let pad=` `.repeat(depth+1);return`${open}\n${entries.map(entry=>`${pad}${entry},`).join(`
11
+ `)}\n${` `.repeat(depth)}${close}`},serializeConfig=config=>`import type { StudioConfig } from "@stdout-design/cli";
10
12
 
11
- const config: StudioConfig = {
12
- scaffoldVersion: "${version}",
13
- defaultPreset: "instagram-square",
14
- outDir: "./out",
15
- presets: [
16
- { height: 1080, id: "instagram-square", platform: "instagram", width: 1080 },
17
- { height: 675, id: "x-card", platform: "x", width: 1200 },
18
- ],
19
- templates: {
20
- "bento-feature": {
21
- componentPath: "./templates/bento-feature",
22
- description: "Apple-style bento feature card.",
23
- },
24
- },
25
- };
13
+ const config: StudioConfig = ${toLiteral(config)};
26
14
 
27
15
  export default config;
28
- `,getPkgJson=(name,version)=>({name,private:!0,type:`module`,scripts:{dev:`studio dev`,render:`studio render`},dependencies:{"@stdout-design/cli":`^${version}`,"@stdout-design/dev-server":`^${version}`,"@stdout-design/web-ui":`^${version}`,"@types/react":`^19.2.17`,react:`^19.2.7`,"react-dom":`^19.2.7`,zod:`^4.4.3`}}),resolveTemplateDir=()=>{let fromDist=path.resolve(import.meta.dirname,`../template`);return existsSync(path.resolve(fromDist,`tsconfig.json`))?fromDist:path.resolve(import.meta.dirname,`../../../template`)},resolveTemplatesDir=()=>path.resolve(resolveTemplateDir(),`templates`),copyStaticAssets=async dir=>{let base=resolveTemplateDir();await Promise.all([cp(path.resolve(base,`tsconfig.json`),path.resolve(dir,`tsconfig.json`)),cp(path.resolve(base,`gitignore`),path.resolve(dir,`.gitignore`)),cp(path.resolve(base,`types.d.ts`),path.resolve(dir,`types.d.ts`))])},generateConfig=async(dir,version)=>{await writeFile(path.resolve(dir,`studio.config.ts`),getConfigTemplate(version))},generatePackageJson=async(dir,projectName,version)=>{await writeFile(path.resolve(dir,`package.json`),JSON.stringify(getPkgJson(projectName,version),null,2))},copyTemplateFiles=async(dir,templates)=>{if(templates.length===0)return;let dstTemplatesDir=path.resolve(dir,`templates`);await mkdir(dstTemplatesDir,{recursive:!0});let srcTemplatesDir=resolveTemplatesDir();for(let template of templates){let src=path.resolve(srcTemplatesDir,`${template}.tsx`);if(!existsSync(src))throw Error(`Template "${template}" not found at templates/${template}.tsx`)}await Promise.all(templates.map(template=>cp(path.resolve(srcTemplatesDir,`${template}.tsx`),path.resolve(dstTemplatesDir,`${template}.tsx`))))},createLocaleFiles=async(dir,locales)=>{if(locales.length===0)return;let localesDir=path.resolve(dir,`locales`);await mkdir(localesDir,{recursive:!0});let english={description:`A beautiful description goes here.`,headline:`Featured App`},localeData={ar:{description:`وصف جميل يظهر هنا.`,headline:`التطبيق المميز`},en:english};await Promise.all(locales.map(locale=>writeFile(path.resolve(localesDir,`${locale}.json`),JSON.stringify({"bento-feature":localeData[locale]??english},null,2))))},scaffold=async(dir,options)=>{if(options.projectName.trim().length===0)throw Error(`Project name cannot be empty.`);let version=getScaffoldVersion();existsSync(dir)||await mkdir(dir,{recursive:!0}),await copyStaticAssets(dir),await generateConfig(dir,version),await generatePackageJson(dir,options.projectName,version),await copyTemplateFiles(dir,options.templates),await createLocaleFiles(dir,options.locales)},detectPackageManager=async cwd=>{let detected=await detect({cwd});return detected?detected.name:getUserAgent()??`npm`},runInstall=(dir,pm)=>{let resolved=resolveCommand(pm,`install`,[]);if(!resolved)return!1;let{command,args}=resolved;return spawnSync(command,args,{cwd:dir,stdio:`inherit`}).status===0},init=async(projectDir,options)=>{let{intro,log,outro,spinner}=await import(`@clack/prompts`);intro(`studio init`);let cwd=process.cwd();if(existsSync(path.resolve(cwd,projectDir??`.`,`studio.config.ts`))){console.log(`studio.config.ts already exists — skipping scaffold.`),outro(`Done`);return}let clack=await import(`@clack/prompts`),collected=await collectPrompts(clack,projectDir,options),s=spinner();if(s.start(`Creating project...`),await scaffold(collected.resolvedDir,{git:collected.wantsGit,install:collected.shouldInstall,locales:collected.locales,projectName:collected.projectName,templates:collected.templates}),s.stop(`Project created`),collected.wantsGit){let gitSpinner=spinner();gitSpinner.start(`Initializing git...`);let initialized=tryGitInit(collected.resolvedDir);gitSpinner.stop(initialized?`Git initialized`:`Git repo already exists or git is not available`)}let pm=await detectPackageManager(cwd),installFailed=!1;collected.shouldInstall&&(log.step(`Installing dependencies with ${pm}...`),installFailed=!runInstall(collected.resolvedDir,pm),installFailed?log.error(`Dependency install failed. Fix the error above, then run \`${pm} install\` in the project.`):log.success(`Dependencies installed`)),await maybeInstallSkill({packageManager:pm,quiet:options?.yes??!1,shouldInstall:options?.installSkill}),console.log(``),console.log(` Created project at ${collected.resolvedDir}`),console.log(``);let relativeDir=path.relative(cwd,collected.resolvedDir);console.log(` Next steps:`),relativeDir&&console.log(` cd ${relativeDir}`),(installFailed||!collected.shouldInstall)&&console.log(` ${pm} install`),console.log(` ${pm} run dev # live studio`),console.log(` ${pm} run render bento-feature # render to ./out`),console.log(``),outro(`Done`),installFailed&&(process.exitCode=1)},lint=_rootDir=>{console.error(`Lint command not yet implemented. Coming in a future release.`),process.exit(1)},levenshteinDistance=(a,b)=>{let aLen=a.length,bLen=b.length;if(aLen===0)return bLen;if(bLen===0)return aLen;let prev=Array.from({length:aLen+1},(_,j)=>j),curr=Array.from({length:aLen+1}).fill(0);for(let j=1;j<=bLen;j+=1){curr[0]=j;for(let i=1;i<=aLen;i+=1){let cost=b[j-1]===a[i-1]?0:1;curr[i]=Math.min((prev[i]??0)+1,(curr[i-1]??0)+1,(prev[i-1]??0)+cost)}let tmp=prev;prev=curr,curr=tmp}return prev[aLen]??0},suggestClosest=(input,candidates,maxDistance=3)=>{let best,bestDistance=1/0;for(let candidate of candidates){let dist=levenshteinDistance(input.toLowerCase(),candidate.toLowerCase());dist<bestDistance&&dist<=maxDistance&&(bestDistance=dist,best=candidate)}return best},coerceValue=(value,type,_items)=>{switch(type){case`number`:{let n=Number(value);return Number.isNaN(n)?value:n}case`boolean`:return value===`true`||value===`1`?!0:value===`false`||value===`0`?!1:value;case`array`:if(value.startsWith(`[`)&&value.endsWith(`]`))try{return JSON.parse(value)}catch{return value.split(`,`).map(s=>s.trim())}return value.split(`,`).map(s=>s.trim());default:return value}},normalizePropArgs=args=>{let normalized=[];for(let i=0;i<args.length;i+=1){let arg=args[i]??``;if(!arg.startsWith(`--`)){normalized.push(arg);continue}let flag=arg.slice(2),next=args[i+1];flag.includes(`=`)||next===void 0||next.startsWith(`--`)?normalized.push(flag):(normalized.push(`${flag}=${next}`),i+=1)}return normalized},assertKnownProp=(key,known)=>{if(known.length===0||known.includes(key))return;let suggestion=suggestClosest(key,known),hint=suggestion?` Did you mean "${suggestion}"?`:``;throw Error(`Unknown prop "${key}".${hint} Available props: ${known.join(`, `)}`)},parsePropArgs=(args,schema)=>{let props={},properties=schema.properties??{},known=Object.keys(properties);for(let arg of normalizePropArgs(args)){let eqIndex=arg.indexOf(`=`);if(eqIndex===-1){assertKnownProp(arg,known),props[arg]=!0;continue}let key=arg.slice(0,eqIndex),value=arg.slice(eqIndex+1);assertKnownProp(key,known);let field=properties[key];props[key]=field?coerceValue(value,field.type,field.items):value}return props},mergeDefaultProps=(schema,props)=>{let properties=schema.properties??{},merged={};for(let[key,field]of Object.entries(properties))field.default!==void 0&&(merged[key]=field.default);return{...merged,...props}},resolveTemplate=(config,templateId)=>{let entry=config.templates[templateId];if(!entry){let candidates=Object.keys(config.templates),suggestion=suggestClosest(templateId,candidates),hint=suggestion?` Did you mean "${suggestion}"?`:``;throw new AppError(ErrorCode.TEMPLATE_NOT_FOUND,`Template "${templateId}" not found in studio.config.ts.${hint}`,{availableTemplates:candidates,templateId})}return{componentPath:entry.componentPath,templateId}},resolveBatchOptions=(config,options)=>{let allPresetIds=config.presets.map(p=>p.id),presetIds=config.defaultPreset?[config.defaultPreset]:allPresetIds;options?.preset===`all`?presetIds=allPresetIds:options?.preset&&(presetIds=options.preset.split(`,`).map(s=>s.trim()));let localeIds=options?.locale?options.locale.split(`,`).map(s=>s.trim()):void 0;return{concurrency:Number(options?.concurrency??`4`),dataFile:options?.data,failFast:options?.failFast,locales:localeIds,outDir:options?.outDir??config.outDir??`out`,presets:presetIds}},render=async(templateId,propArgs,options)=>{let rootDir=requireProjectRoot(),config=await loadConfig(rootDir),resolved=resolveTemplate(config,templateId),{module}=await importTemplateForBatch(rootDir,resolved.componentPath,templateId),schema=zodToJsonSchemaShape(module.propsSchema),parsedProps=parsePropArgs(propArgs,schema),mergedProps=mergeDefaultProps(schema,parsedProps),batchOpts=resolveBatchOptions(config,options),{manifest}=await runBatch({baseProps:mergedProps,concurrency:batchOpts.concurrency,dataFile:batchOpts.dataFile,failFast:batchOpts.failFast,locales:batchOpts.locales,outDir:batchOpts.outDir,presets:batchOpts.presets,rootDir,rows:batchOpts.dataFile?void 0:[{}],templateId});options.json?console.log(JSON.stringify(manifest,null,2)):console.log(formatManifestSummary(manifest,process.cwd())),manifest.failed.length>0&&process.exit(1)},skillInstall=async options=>{let{intro,outro,log}=await import(`@clack/prompts`);intro(`studio skill install`);let cwd=process.cwd(),packageManager=(await detect({cwd}))?.name??`npm`;await maybeInstallSkill({packageManager,quiet:options?.yes??!1,shouldInstall:options?.yes?!0:void 0})||options?.yes||log.step(`You can install it later with \`${getSkillInstallCommand(packageManager)}\`.`),outro(`Done`)},updatePackageJsonDeps=(contents,currentVersion)=>{let pkg=JSON.parse(contents);pkg.dependencies??={};let deps=pkg.dependencies,currentRange=`^${currentVersion}`;return deps[`@stdout-design/cli`]===currentRange?{changed:!1,updated:contents}:(deps[`@stdout-design/cli`]=currentRange,{changed:!0,updated:JSON.stringify(pkg,null,2)})},mergePresets=(existing,defaults,deprecatedPresetIds)=>{let existingIds=new Set(existing.presets.map(p=>p.id)),addedPresets=[],presets=[...existing.presets];for(let defaultPreset of defaults.presets)if(!existingIds.has(defaultPreset.id)){let marked=deprecatedPresetIds.has(defaultPreset.id)?{...defaultPreset,deprecated:!0}:{...defaultPreset};presets.push(marked),addedPresets.push(defaultPreset.id)}let presetsWithDeprecation=presets.map(p=>deprecatedPresetIds.has(p.id)&&!p.deprecated?{...p,deprecated:!0}:p);return{addedPresets,deprecatedPresets:[...new Set(presetsWithDeprecation.filter(p=>p.deprecated).map(p=>p.id))],presets:presetsWithDeprecation}},mergeTemplates=(existing,defaults)=>{let addedTemplates=[],templates={...existing.templates};for(let[key,template]of Object.entries(defaults.templates))key in templates||(templates[key]={...template},addedTemplates.push(key));return{addedTemplates,templates}},resolveDefaultPreset=(presets,existing,defaults)=>{let mergedIds=new Set(presets.map(p=>p.id)),current=existing.defaultPreset;return current&&mergedIds.has(current)?current:defaults.defaultPreset??defaults.presets[0]?.id},mergeConfig=(existing,defaults,currentVersion,deprecatedPresetIds)=>{let oldVersion=existing.scaffoldVersion??null,{presets,addedPresets,deprecatedPresets}=mergePresets(existing,defaults,deprecatedPresetIds),{templates,addedTemplates}=mergeTemplates(existing,defaults);return{config:{...existing,defaultPreset:resolveDefaultPreset(presets,existing,defaults),presets,scaffoldVersion:currentVersion,templates},summary:{addedPresets,addedTemplates,deprecatedPresets,newVersion:currentVersion,oldVersion}}},getLatestVersion=async()=>{try{let response=await fetch(`https://registry.npmjs.org/@stdout-design/cli/latest`);return response.status===200?(await response.json()).version:null}catch{return null}},DEPRECATED_PRESET_IDS=/* @__PURE__ */ new Set,compareVersions=(a,b)=>{let partsA=a.split(`.`).map(Number),partsB=b.split(`.`).map(Number),length=Math.max(partsA.length,partsB.length);for(let i=0;i<length;i+=1){let diff=(partsA[i]??0)-(partsB[i]??0);if(diff!==0)return diff}return 0},serializeConfig=config=>`import type { StudioConfig } from "@stdout-design/cli";
29
-
30
- const config: StudioConfig = ${JSON.stringify(config,null,2)};
31
-
32
- export default config;
33
- `,planUpdate=(existingConfig,defaults,installedVersion,deprecatedPresetIds)=>{let{config:mergedConfig,summary}=mergeConfig(existingConfig,defaults,installedVersion,deprecatedPresetIds);return{installedVersion,mergedConfig,summary}},resolveVersion=async deps=>{let installed=(deps?.getInstalledVersion??getScaffoldVersion)(),latest=await(deps?.getLatestVersion??getLatestVersion)();if(latest!==null&&installed!==`latest`&&compareVersions(latest,installed)>0)console.log(`CLI v${installed} is installed, but v${latest} is available.`),console.log("Run `npm install @stdout-design/cli@latest` first, then re-run `studio update`.");else return installed},printChanges=(summary,installedVersion)=>{console.log(``),summary.oldVersion!==summary.newVersion&&console.log(` scaffoldVersion ${summary.oldVersion??`(none)`} -> ${summary.newVersion}`);for(let id of summary.addedPresets)console.log(` presets +${id}`);for(let id of summary.deprecatedPresets)console.log(` presets ! ${id} (deprecated)`);for(let id of summary.addedTemplates)console.log(` templates +${id}`);console.log(` assets tsconfig.json, .gitignore, types.d.ts (overwrite)`),console.log(` deps @stdout-design/cli ^x -> ^${installedVersion}`),console.log(``)},applyUpdate=async(dir,plan)=>{let configPath=path.resolve(dir,`studio.config.ts`);await writeFile(configPath,serializeConfig(plan.mergedConfig)),await copyStaticAssets(dir);let pkgPath=path.resolve(dir,`package.json`);if(existsSync(pkgPath)){let pkgContent=readFileSync(pkgPath,`utf-8`),{updated,changed}=updatePackageJsonDeps(pkgContent,plan.installedVersion);changed&&await writeFile(pkgPath,updated)}},handlers={cache:cacheStats,cacheClean,cacheStats,dev,init,lint,render,skillInstall,update:async(projectDir,options,deps)=>{let cwd=process.cwd(),dir=path.resolve(cwd,projectDir??`.`),existingConfig=await loadConfig(dir),defaults=getDefaults(),installedVersion=await resolveVersion(deps);if(installedVersion===void 0)return;let plan=planUpdate(existingConfig,defaults,installedVersion,DEPRECATED_PRESET_IDS),{intro,outro,confirm}=await import(`@clack/prompts`);intro(`studio update`),plan.summary.addedPresets.length>0||plan.summary.deprecatedPresets.length>0||plan.summary.addedTemplates.length>0||plan.summary.oldVersion!==plan.summary.newVersion?(printChanges(plan.summary,plan.installedVersion),!options?.yes&&!await confirm({initialValue:!0,message:`Apply these changes?`})?(console.log(`Cancelled.`),outro(`Done`)):(await applyUpdate(dir,plan),console.log(``),outro(`Done`))):(console.log(`Project is already up to date.`),outro(`Done`))}},run=(moduleName,handlerName)=>async(...args)=>{try{let handler=handlerName?handlers[handlerName]:handlers[moduleName];if(!handler)throw Error(`Unknown command: ${handlerName??moduleName}`);await handler(...args)}catch(error){console.error(formatError(error)),process.exit(1)}},pkg=createRequire(import.meta.url)(`../package.json`),program=new Command().allowExcessArguments(!0);program.name(`studio`).description(`Social media design tool — render, preview, batch`).version(pkg.version,`-v, --version`),program.command(`dev`).description(`Start the studio dev server with live preview`).argument(`[rootDir]`,`Project root directory (default: cwd)`).option(`-p, --port <number>`,`Port to run on`,`3030`).option(`--open`,`Open browser on start`).action(run(`dev`)),program.command(`render`).description(`Render a template — single or batch with --data`).argument(`<template>`,`Template ID from studio.config.ts`).argument(`[props...]`,`Template props as key=value or --key value (e.g. headline="Hello")`).option(`--data <path>`,`CSV or JSON data file for batch rendering`).option(`--preset <ids>`,`Comma-separated preset IDs, or "all" (default: config defaultPreset)`).option(`--locale <locales>`,`Comma-separated locale codes`).option(`--out-dir <path>`,`Output directory`).option(`--concurrency <n>`,`Max concurrent renders in batch mode`,`4`).option(`--fail-fast`,`Stop batch on first error`).option(`--json`,`Output results as JSON`).allowUnknownOption().addHelpText(`after`,`
16
+ `,getPkgJson=(name,version)=>({name,private:!0,type:`module`,scripts:{dev:`studio dev`,render:`studio render`},dependencies:{"@stdout-design/cli":`^${version}`,"@stdout-design/dev-server":`^${version}`,"@stdout-design/web-ui":`^${version}`,"@types/react":`^19.2.17`,react:`^19.2.7`,"react-dom":`^19.2.7`,zod:`^4.4.3`}}),resolveTemplateDir=()=>{let fromDist=path.resolve(import.meta.dirname,`../template`);return existsSync(path.resolve(fromDist,`tsconfig.json`))?fromDist:path.resolve(import.meta.dirname,`../../../template`)},resolveTemplatesDir=()=>path.resolve(resolveTemplateDir(),`templates`),copyStaticAssets=async dir=>{let base=resolveTemplateDir();await Promise.all([cp(path.resolve(base,`tsconfig.json`),path.resolve(dir,`tsconfig.json`)),cp(path.resolve(base,`gitignore`),path.resolve(dir,`.gitignore`)),cp(path.resolve(base,`types.d.ts`),path.resolve(dir,`types.d.ts`))])},generateConfig=async(dir,version,choices)=>{await writeFile(path.resolve(dir,`studio.config.ts`),serializeConfig(buildProjectConfig(version,choices)))},generatePackageJson=async(dir,projectName,version)=>{await writeFile(path.resolve(dir,`package.json`),JSON.stringify(getPkgJson(projectName,version),null,2))},copyStarterFiles=async(dir,starters,{force})=>{if(starters.length===0)return;let dstTemplatesDir=path.resolve(dir,`templates`);await mkdir(dstTemplatesDir,{recursive:!0}),await Promise.all(starters.map(id=>cp(path.resolve(resolveTemplatesDir(),`${id}.tsx`),path.resolve(dstTemplatesDir,`${id}.tsx`),{errorOnExist:!1,force})))},readJson=async file=>JSON.parse(await readFile(file,`utf-8`)),readLocaleSource=locale=>{let file=path.resolve(resolveTemplateDir(),`locales`,`${locale}.json`),fallback=path.resolve(resolveTemplateDir(),`locales`,`en.json`);return readJson(existsSync(file)?file:fallback)},addStarterLocaleStrings=async(dir,locales,starters)=>{if(locales.length===0)return;let localesDir=path.resolve(dir,`locales`);await mkdir(localesDir,{recursive:!0}),await Promise.all(locales.map(async locale=>{let file=path.resolve(localesDir,`${locale}.json`),exists=existsSync(file),current=exists?await readJson(file):{},source=await readLocaleSource(locale),missing=starters.filter(id=>!(id in current)&&id in source);if(exists&&missing.length===0)return;let added=Object.fromEntries(missing.map(id=>[id,source[id]]));await writeFile(file,`${JSON.stringify({...current,...added},null,2)}\n`)}))},toStarterIds=templates=>templates.map(id=>{if(!isStarterId(id))throw Error(`Template "${id}" not found in the starter catalog. Choose from: ${STARTER_IDS.join(`, `)}.`);return id}),copyReadme=async(dir,projectName)=>{let readme=await readFile(path.resolve(resolveTemplateDir(),`README.md`),`utf-8`);await writeFile(path.resolve(dir,`README.md`),readme.replaceAll(`{{projectName}}`,projectName))},scaffold=async(dir,options)=>{if(options.projectName.trim().length===0)throw Error(`Project name cannot be empty.`);let choices={locales:options.locales,starters:toStarterIds(options.templates)},version=getScaffoldVersion();existsSync(dir)||await mkdir(dir,{recursive:!0}),await copyStaticAssets(dir),await generateConfig(dir,version,choices),await generatePackageJson(dir,options.projectName,version),await copyReadme(dir,options.projectName),await copyStarterFiles(dir,choices.starters,{force:!0}),await addStarterLocaleStrings(dir,choices.locales,choices.starters)},detectPackageManager=async cwd=>{let detected=await detect({cwd});return detected?detected.name:getUserAgent()??`npm`},runInstall=(dir,pm)=>{let resolved=resolveCommand(pm,`install`,[]);if(!resolved)return!1;let{command,args}=resolved;return spawnSync(command,args,{cwd:dir,stdio:`inherit`}).status===0},init=async(projectDir,options)=>{let{intro,log,outro,spinner}=await import(`@clack/prompts`);intro(`studio init`);let cwd=process.cwd();if(existsSync(path.resolve(cwd,projectDir??`.`,`studio.config.ts`))){console.log(`studio.config.ts already exists — skipping scaffold.`),outro(`Done`);return}let clack=await import(`@clack/prompts`),collected=await collectPrompts(clack,projectDir,options),s=spinner();if(s.start(`Creating project...`),await scaffold(collected.resolvedDir,{git:collected.wantsGit,install:collected.shouldInstall,locales:collected.locales,projectName:collected.projectName,templates:collected.templates}),s.stop(`Project created`),collected.wantsGit){let gitSpinner=spinner();gitSpinner.start(`Initializing git...`);let initialized=tryGitInit(collected.resolvedDir);gitSpinner.stop(initialized?`Git initialized`:`Git repo already exists or git is not available`)}let pm=await detectPackageManager(cwd),installFailed=!1;collected.shouldInstall&&(log.step(`Installing dependencies with ${pm}...`),installFailed=!runInstall(collected.resolvedDir,pm),installFailed?log.error(`Dependency install failed. Fix the error above, then run \`${pm} install\` in the project.`):log.success(`Dependencies installed`)),await maybeInstallSkill({packageManager:pm,quiet:options?.yes??!1,shouldInstall:options?.installSkill}),console.log(``),console.log(` Created project at ${collected.resolvedDir}`),console.log(``);let relativeDir=path.relative(cwd,collected.resolvedDir);console.log(` Next steps:`),relativeDir&&console.log(` cd ${relativeDir}`),(installFailed||!collected.shouldInstall)&&console.log(` ${pm} install`),console.log(` ${pm} run dev # live studio`);let[first]=collected.templates;first&&console.log(` ${pm} run render ${first} # render to ./out`),console.log(``),outro(`Done`),installFailed&&(process.exitCode=1)},lint=_rootDir=>{console.error(`Lint command not yet implemented. Coming in a future release.`),process.exit(1)},levenshteinDistance=(a,b)=>{let aLen=a.length,bLen=b.length;if(aLen===0)return bLen;if(bLen===0)return aLen;let prev=Array.from({length:aLen+1},(_,j)=>j),curr=Array.from({length:aLen+1}).fill(0);for(let j=1;j<=bLen;j+=1){curr[0]=j;for(let i=1;i<=aLen;i+=1){let cost=b[j-1]===a[i-1]?0:1;curr[i]=Math.min((prev[i]??0)+1,(curr[i-1]??0)+1,(prev[i-1]??0)+cost)}let tmp=prev;prev=curr,curr=tmp}return prev[aLen]??0},suggestClosest=(input,candidates,maxDistance=3)=>{let best,bestDistance=1/0;for(let candidate of candidates){let dist=levenshteinDistance(input.toLowerCase(),candidate.toLowerCase());dist<bestDistance&&dist<=maxDistance&&(bestDistance=dist,best=candidate)}return best},coerceValue=(value,type,_items)=>{switch(type){case`number`:{let n=Number(value);return Number.isNaN(n)?value:n}case`boolean`:return value===`true`||value===`1`?!0:value===`false`||value===`0`?!1:value;case`array`:if(value.startsWith(`[`)&&value.endsWith(`]`))try{return JSON.parse(value)}catch{return value.split(`,`).map(s=>s.trim())}return value.split(`,`).map(s=>s.trim());default:return value}},normalizePropArgs=args=>{let normalized=[];for(let i=0;i<args.length;i+=1){let arg=args[i]??``;if(!arg.startsWith(`--`)){normalized.push(arg);continue}let flag=arg.slice(2),next=args[i+1];flag.includes(`=`)||next===void 0||next.startsWith(`--`)?normalized.push(flag):(normalized.push(`${flag}=${next}`),i+=1)}return normalized},assertKnownProp=(key,known)=>{if(known.length===0||known.includes(key))return;let suggestion=suggestClosest(key,known),hint=suggestion?` Did you mean "${suggestion}"?`:``;throw Error(`Unknown prop "${key}".${hint} Available props: ${known.join(`, `)}`)},parsePropArgs=(args,schema)=>{let props={},properties=schema.properties??{},known=Object.keys(properties);for(let arg of normalizePropArgs(args)){let eqIndex=arg.indexOf(`=`);if(eqIndex===-1){assertKnownProp(arg,known),props[arg]=!0;continue}let key=arg.slice(0,eqIndex),value=arg.slice(eqIndex+1);assertKnownProp(key,known);let field=properties[key];props[key]=field?coerceValue(value,field.type,field.items):value}return props},mergeDefaultProps=(schema,props)=>{let properties=schema.properties??{},merged={};for(let[key,field]of Object.entries(properties))field.default!==void 0&&(merged[key]=field.default);return{...merged,...props}},resolveTemplate=(config,templateId)=>{let entry=config.templates[templateId];if(!entry){let candidates=Object.keys(config.templates),suggestion=suggestClosest(templateId,candidates),hint=suggestion?` Did you mean "${suggestion}"?`:``;throw new AppError(ErrorCode.TEMPLATE_NOT_FOUND,`Template "${templateId}" not found in studio.config.ts.${hint}`,{availableTemplates:candidates,templateId})}return{componentPath:entry.componentPath,templateId}},resolveBatchOptions=(config,options)=>{let allPresetIds=config.presets.map(p=>p.id),presetIds=config.defaultPreset?[config.defaultPreset]:allPresetIds;options?.preset===`all`?presetIds=allPresetIds:options?.preset&&(presetIds=options.preset.split(`,`).map(s=>s.trim()));let localeIds=options?.locale?options.locale.split(`,`).map(s=>s.trim()):void 0;return{concurrency:Number(options?.concurrency??`4`),dataFile:options?.data,failFast:options?.failFast,locales:localeIds,outDir:options?.outDir??config.outDir??`out`,presets:presetIds}},render=async(templateId,propArgs,options)=>{let rootDir=requireProjectRoot(),config=await loadConfig(rootDir),resolved=resolveTemplate(config,templateId),{module}=await importTemplateForBatch(rootDir,resolved.componentPath,templateId),schema=zodToJsonSchemaShape(module.propsSchema),parsedProps=parsePropArgs(propArgs,schema),mergedProps=mergeDefaultProps(schema,parsedProps),batchOpts=resolveBatchOptions(config,options),{manifest}=await runBatch({baseProps:mergedProps,concurrency:batchOpts.concurrency,dataFile:batchOpts.dataFile,failFast:batchOpts.failFast,locales:batchOpts.locales,outDir:batchOpts.outDir,presets:batchOpts.presets,rootDir,rows:batchOpts.dataFile?void 0:[{}],templateId});options.json?console.log(JSON.stringify(manifest,null,2)):console.log(formatManifestSummary(manifest,process.cwd())),manifest.failed.length>0&&process.exit(1)},skillInstall=async options=>{let{intro,outro,log}=await import(`@clack/prompts`);intro(`studio skill install`);let cwd=process.cwd(),packageManager=(await detect({cwd}))?.name??`npm`;await maybeInstallSkill({packageManager,quiet:options?.yes??!1,shouldInstall:options?.yes?!0:void 0})||options?.yes||log.step(`You can install it later with \`${getSkillInstallCommand(packageManager)}\`.`),outro(`Done`)},updatePackageJsonDeps=(contents,currentVersion)=>{let pkg=JSON.parse(contents);pkg.dependencies??={};let deps=pkg.dependencies,currentRange=`^${currentVersion}`;return deps[`@stdout-design/cli`]===currentRange?{changed:!1,updated:contents}:(deps[`@stdout-design/cli`]=currentRange,{changed:!0,updated:JSON.stringify(pkg,null,2)})},mergePresets=(existing,defaults,deprecatedPresetIds)=>{let existingIds=new Set(existing.presets.map(p=>p.id)),addedPresets=[],presets=[...existing.presets];for(let defaultPreset of defaults.presets)if(!existingIds.has(defaultPreset.id)){let marked=deprecatedPresetIds.has(defaultPreset.id)?{...defaultPreset,deprecated:!0}:{...defaultPreset};presets.push(marked),addedPresets.push(defaultPreset.id)}let presetsWithDeprecation=presets.map(p=>deprecatedPresetIds.has(p.id)&&!p.deprecated?{...p,deprecated:!0}:p);return{addedPresets,deprecatedPresets:[...new Set(presetsWithDeprecation.filter(p=>p.deprecated).map(p=>p.id))],presets:presetsWithDeprecation}},mergeTemplates=(existing,defaults)=>{let addedTemplates=[],templates={...existing.templates};for(let[key,template]of Object.entries(defaults.templates))key in templates||(templates[key]={...template},addedTemplates.push(key));return{addedTemplates,templates}},mergeFonts=(existing,defaults)=>{let addedFonts=[],fonts={...existing.fonts};for(let[locale,entries]of Object.entries(defaults.fonts??{})){let current=fonts[locale]??[],missing=entries.filter(entry=>!current.some(c=>c.family===entry.family));missing.length>0&&(fonts[locale]=[...current,...missing.map(entry=>({...entry}))],addedFonts.push(...missing.map(entry=>`${locale}: ${entry.family}`)))}return{addedFonts,fonts:addedFonts.length>0?fonts:existing.fonts}},resolveDefaultPreset=(presets,existing,defaults)=>{let mergedIds=new Set(presets.map(p=>p.id)),current=existing.defaultPreset;return current&&mergedIds.has(current)?current:defaults.defaultPreset??defaults.presets[0]?.id},mergeConfig=(existing,defaults,currentVersion,deprecatedPresetIds)=>{let oldVersion=existing.scaffoldVersion??null,{presets,addedPresets,deprecatedPresets}=mergePresets(existing,defaults,deprecatedPresetIds),{templates,addedTemplates}=mergeTemplates(existing,defaults),{fonts,addedFonts}=mergeFonts(existing,defaults),recordedLocales=existing.locales?[]:defaults.locales??[];return{config:{...existing,defaultPreset:resolveDefaultPreset(presets,existing,defaults),fonts,locales:existing.locales??defaults.locales,presets,scaffoldVersion:currentVersion,templates},summary:{addedFonts,addedPresets,addedTemplates,deprecatedPresets,newVersion:currentVersion,oldVersion,recordedLocales}}},hasChanges=summary=>summary.addedPresets.length>0||summary.deprecatedPresets.length>0||summary.addedTemplates.length>0||summary.addedFonts.length>0||summary.recordedLocales.length>0||summary.oldVersion!==summary.newVersion,getLatestVersion=async()=>{try{let response=await fetch(`https://registry.npmjs.org/@stdout-design/cli/latest`);return response.status===200?(await response.json()).version:null}catch{return null}},DEPRECATED_PRESET_IDS=/* @__PURE__ */ new Set,localesOnDisk=dir=>{let localesDir=path.resolve(dir,`locales`);return existsSync(localesDir)?readdirSync(localesDir).filter(file=>file.endsWith(`.json`)).map(file=>file.slice(0,-5)).toSorted():[]},planUpdate=(existingConfig,defaults,installedVersion,deprecatedPresetIds)=>{let{config:mergedConfig,summary}=mergeConfig(existingConfig,defaults,installedVersion,deprecatedPresetIds);return{installedVersion,locales:mergedConfig.locales??[],mergedConfig,starters:summary.addedTemplates.filter(isStarterId),summary}},resolveVersion=async deps=>{let installed=(deps?.getInstalledVersion??getScaffoldVersion)(),latest=await(deps?.getLatestVersion??getLatestVersion)();if(latest!==null&&installed!==`latest`&&compareVersions(latest,installed)>0)console.log(`CLI v${installed} is installed, but v${latest} is available.`),console.log("Run `npm install @stdout-design/cli@latest` first, then re-run `studio update`.");else return installed},printChanges=(summary,installedVersion)=>{console.log(``),summary.oldVersion!==summary.newVersion&&console.log(` scaffoldVersion ${summary.oldVersion??`(none)`} -> ${summary.newVersion}`);for(let id of summary.addedPresets)console.log(` presets +${id}`);for(let id of summary.deprecatedPresets)console.log(` presets ! ${id} (deprecated)`);for(let id of summary.addedTemplates)console.log(` templates +${id}`);for(let font of summary.addedFonts)console.log(` fonts +${font}`);summary.recordedLocales.length>0&&console.log(` locales ${summary.recordedLocales.join(`, `)} (recorded)`),console.log(` assets tsconfig.json, .gitignore, types.d.ts (overwrite)`),console.log(` deps @stdout-design/cli ^x -> ^${installedVersion}`),console.log(``)},applyUpdate=async(dir,plan)=>{await copyStarterFiles(dir,plan.starters,{force:!1}),await addStarterLocaleStrings(dir,plan.locales,plan.starters),await copyStaticAssets(dir),await writeFile(path.resolve(dir,`studio.config.ts`),serializeConfig(plan.mergedConfig));let pkgPath=path.resolve(dir,`package.json`);if(existsSync(pkgPath)){let pkgContent=readFileSync(pkgPath,`utf-8`),{updated,changed}=updatePackageJsonDeps(pkgContent,plan.installedVersion);changed&&await writeFile(pkgPath,updated)}},handlers={cache:cacheStats,cacheClean,cacheStats,dev,init,lint,render,skillInstall,update:async(projectDir,options,deps)=>{let cwd=process.cwd(),dir=path.resolve(cwd,projectDir??`.`),existingConfig=await loadConfig(dir),installedVersion=await resolveVersion(deps);if(installedVersion===void 0)return;let defaults=buildProjectConfig(installedVersion,{locales:existingConfig.locales??localesOnDisk(dir),starters:startersSince(existingConfig.scaffoldVersion,installedVersion)}),plan=planUpdate(existingConfig,defaults,installedVersion,DEPRECATED_PRESET_IDS),{intro,outro,confirm}=await import(`@clack/prompts`);intro(`studio update`),hasChanges(plan.summary)?(printChanges(plan.summary,plan.installedVersion),!options?.yes&&!await confirm({initialValue:!0,message:`Apply these changes?`})?(console.log(`Cancelled.`),outro(`Done`)):(await applyUpdate(dir,plan),console.log(``),outro(`Done`))):(console.log(`Project is already up to date.`),outro(`Done`))}},run=(moduleName,handlerName)=>async(...args)=>{try{let handler=handlerName?handlers[handlerName]:handlers[moduleName];if(!handler)throw Error(`Unknown command: ${handlerName??moduleName}`);await handler(...args)}catch(error){console.error(formatError(error)),process.exit(1)}},pkg=createRequire(import.meta.url)(`../package.json`),program=new Command().allowExcessArguments(!0);program.name(`studio`).description(`Social media design tool — render, preview, batch`).version(pkg.version,`-v, --version`),program.command(`dev`).description(`Start the studio dev server with live preview`).argument(`[rootDir]`,`Project root directory (default: cwd)`).option(`-p, --port <number>`,`Port to run on`,`3030`).option(`--open`,`Open browser on start`).action(run(`dev`)),program.command(`render`).description(`Render a template — single or batch with --data`).argument(`<template>`,`Template ID from studio.config.ts`).argument(`[props...]`,`Template props as key=value or --key value (e.g. headline="Hello")`).option(`--data <path>`,`CSV or JSON data file for batch rendering`).option(`--preset <ids>`,`Comma-separated preset IDs, or "all" (default: config defaultPreset)`).option(`--locale <locales>`,`Comma-separated locale codes`).option(`--out-dir <path>`,`Output directory`).option(`--concurrency <n>`,`Max concurrent renders in batch mode`,`4`).option(`--fail-fast`,`Stop batch on first error`).option(`--json`,`Output results as JSON`).allowUnknownOption().addHelpText(`after`,`
34
17
  Examples:
35
18
  $ studio render bento-feature
36
19
  $ studio render bento-feature headline="Hello" --preset all
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stdout-design/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "stdout-design CLI — shallow front door to @stdout-design/core and @stdout-design/dev-server",
5
5
  "keywords": [
6
6
  "cli",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@clack/prompts": "^1.8.1",
52
- "@stdout-design/core": "^0.6.0",
52
+ "@stdout-design/core": "^0.7.0",
53
53
  "commander": "^15.0.0",
54
54
  "package-manager-detector": "^1.9.0"
55
55
  },
@@ -0,0 +1,37 @@
1
+ # {{projectName}}
2
+
3
+ Social images as React components, rendered to PNG by [stdout design](https://github.com/m7md1alaa/stdout-design).
4
+
5
+ ## Run it
6
+
7
+ ```bash
8
+ npm run dev # live studio with every template and preset
9
+ npm run render launch-hero # one template, every locale, to ./out
10
+ npm run render launch-hero --preset all # every preset in studio.config.ts
11
+ npm run render launch-hero --locale ar # just the Arabic (RTL) version
12
+ ```
13
+
14
+ ## What's here
15
+
16
+ | Path | What it is |
17
+ | --- | --- |
18
+ | `templates/*.tsx` | One component per image. Its `propsSchema` is the form you see in the studio. |
19
+ | `locales/*.json` | Per-locale copy, keyed by template id. Strings here override the schema defaults. |
20
+ | `studio.config.ts` | Presets (canvas sizes), locales, registered templates, and fonts. |
21
+
22
+ ## The starters
23
+
24
+ - **launch-hero**: a streetwear landing page with stickers, tilted cards, and a stats strip.
25
+ - **bento-feature**: a product bento grid with metric, chart, receipt, and gauge tiles.
26
+ - **retro-poster**: a print poster with a drawn cassette, tape stripes, and a rubber stamp.
27
+ - **trend-poster**: an editorial six-item grid with a drawn specimen in each cell.
28
+
29
+ Each one lays out at both 1080×1080 and 1200×675 and mirrors itself for Arabic. Rewrite them or delete them: `studio update` never overwrites a template you already have, and never brings back a starter you left out.
30
+
31
+ ## Fonts
32
+
33
+ The `fonts` block in `studio.config.ts` pulls Google Fonts on the first render and caches them. It lists only the faces your starters use. `default` applies to every locale without its own entry, except Arabic: a Latin face has no Arabic glyphs, so Arabic gets its own entry with the Latin and Arabic faces together. Without one, Arabic falls back to Noto Sans Arabic. A template names its fonts in `fontFamily`, Latin face first: `"Archivo Black, Lalezar, sans-serif"`. Any glyph the first face lacks falls through to the next.
34
+
35
+ ## Writing RTL templates
36
+
37
+ `dir="rtl"` mirrors flex rows and text alignment for you. It does not mirror absolute `left`/`right` offsets or physical margins, so the starters pick a side with `const start = isRtl ? "right" : "left"`. Wrap brand names and numbers like `+18%` in `<span dir="ltr">` to keep bidi from reordering them.
@@ -0,0 +1,49 @@
1
+ {
2
+ "launch-hero": {
3
+ "badge": "الإصدار ٠٧",
4
+ "categories": ["تيشيرتات", "هوديز", "كابات"],
5
+ "cta": "تسوّق الآن",
6
+ "headline": "البسها بصوت عالٍ",
7
+ "links": ["الإصدارات", "الأرشيف", "النادي"],
8
+ "note": "شحن لكل العالم",
9
+ "stats": ["12 إصدارًا في السنة", "0 إعادة طرح", "100% قطن"],
10
+ "subheadline": "ملابس شارع بكميات محدودة، مطبوعة في مدينة لا تهمس أبدًا."
11
+ },
12
+ "bento-feature": {
13
+ "chartLabel": "المصاريف أسبوعيًا",
14
+ "description": "كل بطاقة وكل فاتورة، مرتّبة لحظة وصولها.",
15
+ "gaugeLabel": "المستخدم من الميزانية",
16
+ "headline": "اعرف أين يذهب كل دينار",
17
+ "metricDelta": "+18%",
18
+ "metricLabel": "وفّرت هذا الشهر (د.أ)",
19
+ "metricValue": "1,240",
20
+ "ratingLabel": "تقييم متجر التطبيقات",
21
+ "receipt": ["قهوة 4.50", "بقالة 62.10", "مترو 2.75"],
22
+ "receiptNote": "تصنيف تلقائي"
23
+ },
24
+ "retro-poster": {
25
+ "body": "تسعون دقيقة، وجهان، بلا تخطٍّ. أغانٍ مختارة باليد، وعناوين مكتوبة بخط اليد، وقلم رصاص لمّا يتشابك الشريط.",
26
+ "headline": "نادي الكاسيت",
27
+ "label": "مختارات صيف 89",
28
+ "stamp": "إصدار محدود",
29
+ "stampNote": "رقم 0042 / 5000",
30
+ "tagline": "تناظري منذ الأزل",
31
+ "title": "اضغط تشغيل"
32
+ },
33
+ "trend-poster": {
34
+ "aside": "لتقتبسها وتعيد مزجها",
35
+ "cta": "احفظها لاحقًا",
36
+ "footer": "خلّي الإبهام يتوقف.",
37
+ "headline": "طرق لإيقاف التمرير",
38
+ "issue": "العدد 07",
39
+ "items": [
40
+ "خط صاخب",
41
+ "نقاط الطباعة",
42
+ "ألوان متصادمة",
43
+ "كولاج خام",
44
+ "ملصقات",
45
+ "أشكال ضخمة"
46
+ ],
47
+ "strap": "دليل ميداني للتايملاين"
48
+ }
49
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "launch-hero": {
3
+ "badge": "Drop 07",
4
+ "categories": ["Tees", "Hoodies", "Caps"],
5
+ "cta": "Shop now",
6
+ "headline": "Wear It Loud",
7
+ "links": ["Drops", "Archive", "Club"],
8
+ "note": "Ships worldwide",
9
+ "stats": ["12 drops a year", "0 restocks", "100% cotton"],
10
+ "subheadline": "Small-batch streetwear, printed in a city that never whispers."
11
+ },
12
+ "bento-feature": {
13
+ "chartLabel": "Spending by week",
14
+ "description": "Every card, every bill, sorted the second it lands.",
15
+ "gaugeLabel": "Budget used",
16
+ "headline": "Know where every dollar goes",
17
+ "metricDelta": "+18%",
18
+ "metricLabel": "Saved this month",
19
+ "metricValue": "$1,240",
20
+ "ratingLabel": "App Store rating",
21
+ "receipt": ["Coffee 4.50", "Groceries 62.10", "Metro 2.75"],
22
+ "receiptNote": "Sorted automatically"
23
+ },
24
+ "retro-poster": {
25
+ "body": "Ninety minutes, two sides, zero skips. Hand-picked tracks, hand-written labels, and a pencil for when the tape gets tangled.",
26
+ "headline": "Mixtape Club",
27
+ "label": "Summer mix '89",
28
+ "stamp": "Limited edition",
29
+ "stampNote": "No. 0042 / 5000",
30
+ "tagline": "Analog since forever",
31
+ "title": "Press Play"
32
+ },
33
+ "trend-poster": {
34
+ "aside": "to steal & remix",
35
+ "cta": "Save for later",
36
+ "footer": "Make the thumb stop.",
37
+ "headline": "Ways to stop the scroll",
38
+ "issue": "Issue 07",
39
+ "items": [
40
+ "Loud type",
41
+ "Halftone",
42
+ "Clashing color",
43
+ "Raw collage",
44
+ "Stickers",
45
+ "Big shapes"
46
+ ],
47
+ "strap": "A field guide for the feed"
48
+ }
49
+ }