create-gamecn 0.0.5 → 0.0.6

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.
Files changed (2) hide show
  1. package/index.js +2 -2
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -379,8 +379,8 @@ a registry item and publish it via \`gamecn registry build\`.
379
379
  `}};function intersects(a,b3){for(const v of a)if(b3.includes(v))return true;return false}function predicateMatches(when,ctx){if(!when)return false;if(when.engine&&!when.engine.includes(ctx.engine))return false;if(when.framework&&!when.framework.includes(ctx.framework))return false;if(when.language&&!when.language.includes(ctx.language))return false;if(when.flow&&!when.flow.includes(ctx.flow))return false;if(when.templateTags){if(!intersects(when.templateTags,ctx.templateTags))return false}return true}function toRec(skill,ctx){const rec=skill.recommend;const def=rec?.default??"never";let alwaysRecommended=false;let defaultSelected=false;if(def==="always"){alwaysRecommended=true;defaultSelected=true}else if(def==="when"){defaultSelected=predicateMatches(rec?.when,ctx)}return{name:skill.name,label:rec?.label??skill.name,hint:rec?.hint??skill.description,alwaysRecommended,defaultSelected}}function recommendSkills(ctx,opts={}){const catalog=opts.catalog??loadCatalog();const recs=catalog.skills.map((s)=>toRec(s,ctx));recs.sort((a,b3)=>{if(a.alwaysRecommended!==b3.alwaysRecommended){return a.alwaysRecommended?-1:1}if(a.defaultSelected!==b3.defaultSelected){return a.defaultSelected?-1:1}return a.name.localeCompare(b3.name)});return recs}import{mkdir,rm,writeFile}from"node:fs/promises";import{dirname,join,resolve}from"node:path";var SKILL_TARGETS=[".claude/skills",".agent/skills"];async function installSkills(recs,targets){if(recs.length===0){return{installed:[],failed:[],writtenPaths:[]}}const bundle=targets.bundle??SKILLS_BUNDLE;const cwd=resolve(targets.cwd);const installed=[];const failed=[];const writtenPaths=[];for(const rec of recs){const files=bundle[rec.name];if(!files){failed.push({rec,reason:`skill "${rec.name}" not in bundle (catalog/bundle drift; re-run \`bun scripts/sync-skills.ts\`)`});continue}let allOk=true;const recPaths=[];for(const targetRoot of SKILL_TARGETS){const skillDir=join(cwd,targetRoot,rec.name);try{await rm(skillDir,{recursive:true,force:true});await mkdir(skillDir,{recursive:true});for(const[relPath,content]of Object.entries(files)){const dest=join(skillDir,relPath);await mkdir(dirname(dest),{recursive:true});await writeFile(dest,content,"utf8");recPaths.push(dest)}}catch(err){allOk=false;failed.push({rec,reason:`${skillDir}: ${err.message}`});break}}if(allOk)installed.push(rec);if(allOk)writtenPaths.push(...recPaths)}return{installed,failed,writtenPaths}}var import_picocolors2=__toESM(require_picocolors(),1);function applyPreselected(all,preselected){const byName=new Map(all.map((r2)=>[r2.name,r2]));const recs=[];const unknown2=[];for(const name of preselected){const rec=byName.get(name);if(rec)recs.push(rec);else unknown2.push(name)}return{recs,unknown:unknown2}}async function promptSkillsInstall(ctx,opts){if(opts.skip)return{recs:[],unknownPreselected:[]};const all=recommendSkills(ctx,opts.catalog?{catalog:opts.catalog}:{});if(opts.preselected&&opts.preselected.length>0){const{recs,unknown:unknown2}=applyPreselected(all,opts.preselected);return{recs,unknownPreselected:unknown2}}if(opts.yes){return{recs:all.filter((r2)=>r2.defaultSelected),unknownPreselected:[]}}if(all.length===0)return{recs:[],unknownPreselected:[]};const targetHint=opts.cwd?`${opts.cwd}/.claude/skills/<name>/ and ${opts.cwd}/.agent/skills/<name>/`:"<project>/.claude/skills/<name>/ and <project>/.agent/skills/<name>/";Me(`Recommended skills install to:
380
380
  ${targetHint}
381
381
  `+`For Claude Code, Codex, or other skill-aware agents. `+import_picocolors2.default.dim("Skip if you're not using one — press ESC."),"Claude Code / Codex skills");const impl=opts.multiselectImpl??fe;const choice=await impl({message:"Install recommended skills?",options:all.map((rec)=>({value:rec.name,label:rec.alwaysRecommended?`${rec.label} ${import_picocolors2.default.dim("(recommended)")}`:rec.label,hint:rec.hint})),initialValues:all.filter((r2)=>r2.defaultSelected).map((r2)=>r2.name),required:false});if(pD(choice))return{recs:[],unknownPreselected:[]};const selected=new Set(choice);return{recs:all.filter((r2)=>selected.has(r2.name)),unknownPreselected:[]}}var import_picocolors3=__toESM(require_picocolors(),1);function renderSkillsSummary(result,log=console.log){if(result.installed.length===0&&result.failed.length===0){return}if(result.installed.length>0){const names=result.installed.map((r2)=>r2.name).join(", ");log(import_picocolors3.default.green(`✓ Installed skills: ${names}`));log(import_picocolors3.default.dim(" Written to .claude/skills/<name>/ and .agent/skills/<name>/ under the project root."))}if(result.failed.length>0){log(import_picocolors3.default.yellow(`! ${result.failed.length} skill(s) failed to install:`));for(const fail of result.failed){log(import_picocolors3.default.yellow(` - ${fail.rec.name}: ${fail.reason}`))}log(import_picocolors3.default.dim(" Re-run with `--skills <name>` after fixing, or hand-copy from node_modules/@gamecn/skills/skills-bundle/."))}}function renderUnknownPreselected(names,log=console.log){if(names.length===0)return;log(import_picocolors3.default.yellow(`! Unknown skills (not in catalog, ignored): ${names.join(", ")}`))}import{readFile as readFile7}from"node:fs/promises";import{resolve as resolvePath}from"node:path";var ImageAssetSchema=object({id:string2().min(1),type:literal("image"),path:string2().min(1),width:number2().int().positive().optional(),height:number2().int().positive().optional()});var SpritesheetAssetSchema=object({id:string2().min(1),type:literal("spritesheet"),path:string2().min(1),width:number2().int().positive().optional(),height:number2().int().positive().optional(),frameWidth:number2().int().positive(),frameHeight:number2().int().positive(),animations:array(object({key:string2().min(1),frames:array(number2().int().nonnegative()),frameRate:number2().positive().optional(),repeat:number2().int().optional()})).optional()});var AudioAssetSchema=object({id:string2().min(1),type:literal("audio"),path:string2().min(1),duration:number2().nonnegative().optional(),formats:array(_enum(["wav","mp3","ogg","m4a","webm"])).optional()});var ModelAssetSchema=object({id:string2().min(1),type:literal("model"),path:string2().min(1),format:_enum(["glb","gltf","obj","fbx"]),triangles:number2().int().positive().optional(),animations:array(string2()).optional()});var ShaderAssetSchema=object({id:string2().min(1),type:literal("shader"),path:string2().min(1),stage:_enum(["vertex","fragment","compute"])});var TilesetAssetSchema=object({id:string2().min(1),type:literal("tileset"),path:string2().min(1),tileWidth:number2().int().positive(),tileHeight:number2().int().positive(),columns:number2().int().positive().optional(),rows:number2().int().positive().optional()});var AssetSchema=discriminatedUnion("type",[ImageAssetSchema,SpritesheetAssetSchema,AudioAssetSchema,ModelAssetSchema,ShaderAssetSchema,TilesetAssetSchema]);var RegistryConfigSchema=union([string2().min(1),object({url:string2().min(1),index:string2().min(1).optional(),headers:record(string2(),string2()).optional()})]);var DepSpecRegex=/^(?:@[a-z0-9][a-z0-9_-]*\/[a-z0-9][a-z0-9_./-]*|[a-z0-9][a-z0-9_-]*|https?:\/\/[^\s]+|gh:[^\s]+|github:[^\s]+|\.{1,2}\/[^\s]+)$/i;var GamecnConfigSchema=object({$schema:string2().optional(),engine:_enum(["phaser","three","pixi","vanilla"]),framework:_enum(["vanilla","react"]),language:_enum(["typescript","javascript"]),packageManager:_enum(["npm","pnpm","yarn","bun"]).optional().default("npm"),paths:object({src:string2().min(1),assets:string2().min(1),components:string2().optional(),systems:string2().optional(),scenes:string2().optional(),shaders:string2().optional(),ui:string2().optional()}),registries:record(string2(),RegistryConfigSchema).optional(),deps:array(string2().regex(DepSpecRegex,{error:"Each `deps[]` entry must be a registry spec — e.g. `event-bus`, `@main/event-bus`, or `https://...`."})).optional()});var ITEM_TYPES=["registry:template","registry:scene","registry:component","registry:system","registry:asset","registry:shader","registry:audio","registry:model","registry:tileset","registry:ui","registry:hook","registry:utility","registry:config","registry:recipe","registry:plugin"];var LockedFileSchema=object({path:string2().min(1),sha256:string2().regex(/^[a-f0-9]{64}$/,{error:"sha256 must be a 64-character lowercase hex string."})});var LockedItemSchema=object({name:string2().min(1),version:string2().min(1),registry:string2().min(1),resolved:string2().min(1),installedAt:string2(),files:array(LockedFileSchema)});var LockfileSchema=object({$schema:string2().optional(),version:literal(1),items:record(string2(),LockedItemSchema)});var ItemSummarySchema=object({name:string2().min(1),type:_enum(ITEM_TYPES),title:string2(),description:string2(),version:string2().min(1),engines:array(string2()).optional(),frameworks:array(string2()).optional(),languages:array(string2()).optional(),tags:array(string2()).optional()});var RegistryIndexSchema=object({$schema:string2().optional(),name:string2().min(1),homepage:url().optional(),items:array(ItemSummarySchema)});var GENRE_TAGS=["endless-runner","dodger","platformer","top-down-shooter","twin-stick","tower-defense","survivors","racing","puzzle","arcade","simulation","fighter","rpg","blank"];var STYLE_TAGS=["pixel","flat-2d","hand-drawn","low-poly-3d","voxel","photoreal-3d"];var PLATFORM_TAGS=["web","mobile-web","desktop"];var NAMESPACE_VALUES={genre:new Set(GENRE_TAGS),style:new Set(STYLE_TAGS),platform:new Set(PLATFORM_TAGS)};var CONTROLLED_PREFIXES=Object.keys(NAMESPACE_VALUES);function isControlledTag(tag){const idx=tag.indexOf(":");if(idx<=0)return false;const ns=tag.slice(0,idx);const value=tag.slice(idx+1);const allowed=NAMESPACE_VALUES[ns];return allowed!==undefined&&allowed.has(value)}function hasControlledPrefix(tag){for(const ns of CONTROLLED_PREFIXES){if(tag.startsWith(ns+":"))return true}return false}var semverRegex=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;var numericIdentifier="(?:0|[1-9]\\d*)";var prereleaseIdentifier=[numericIdentifier,"\\d*[a-zA-Z-][0-9a-zA-Z-]*"].join("|");var prerelease=`(?:${prereleaseIdentifier})(?:\\.(?:${prereleaseIdentifier}))*`;var build="[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*";var concretePatch=`${numericIdentifier}(?:-${prerelease})?(?:\\+${build})?`;var patch=`(?:[xX*]|${concretePatch})`;var minor=`(?:[xX*]|${numericIdentifier}(?:\\.${patch})?)`;var rangeVersion=`(?:[xX*]|${numericIdentifier}(?:\\.${minor})?)`;var comparatorOperator="(?:<=|>=|<|>|=|~>|~|\\^)";var comparator=`(?:(?:${comparatorOperator})\\s*)?${rangeVersion}`;var comparatorSet=`${comparator}(?:\\s+${comparator})*`;var hyphenRange=`${rangeVersion}\\s+-\\s+${rangeVersion}`;var simpleRange=`(?:${hyphenRange}|${comparatorSet})`;var semverRangeRegex=new RegExp(`^\\s*${simpleRange}(?:\\s*\\|\\|\\s*${simpleRange})*\\s*$`);var FILE_TYPES=["source","asset","documentation"];var integrityRegex=/^sha256-[A-Za-z0-9+/=]+$/;var FileSpecSchema=object({path:string2().min(1),target:string2().min(1),type:_enum(FILE_TYPES),language:_enum(["typescript","javascript"]).optional(),content:string2().optional(),url:url().optional(),integrity:string2().regex(integrityRegex,{error:"Integrity must be a Subresource Integrity string (sha256-...)."}).optional()}).refine((obj)=>!(obj.content!==undefined&&obj.url!==undefined),{error:"File spec cannot have both `content` and `url`."});var PreloadInstructionSchema=object({kind:string2().min(1),key:string2().min(1),path:string2().min(1),frameWidth:number2().int().positive().optional(),frameHeight:number2().int().positive().optional()}).catchall(unknown());var IntegrationBlockSchema=object({preload:array(PreloadInstructionSchema).optional(),usage:array(string2()).optional()});var AuthorSchema=union([string2().min(1),object({name:string2().min(1),url:url().optional(),email:string2().optional()})]);var RegistryItemSchema=object({$schema:string2().optional(),name:string2().min(1),type:_enum(ITEM_TYPES),title:string2(),description:string2(),version:string2().regex(semverRegex,{error:"Invalid SemVer string."}),license:string2().optional(),licenseUrl:url().optional(),author:AuthorSchema,tags:array(string2().refine((s)=>!hasControlledPrefix(s)||isControlledTag(s),{error:"Namespaced tag (genre:/style:/platform:) suffix is not in the controlled vocabulary."})).optional(),attributionRequired:boolean2().optional(),commercialUse:boolean2().optional(),redistribution:boolean2().optional(),aiGenerated:boolean2().optional(),compatibility:object({engines:record(string2(),string2().regex(semverRangeRegex)),frameworks:array(_enum(["vanilla","react"])),languages:array(_enum(["typescript","javascript"])),bundlers:array(string2()),libraries:record(string2(),string2().regex(semverRangeRegex)).optional(),platforms:array(string2()).optional()}).optional(),dependencies:object({npm:array(string2()).optional(),registry:array(string2()).optional()}).optional(),files:array(FileSpecSchema),assets:array(AssetSchema).optional(),integration:record(string2(),IntegrationBlockSchema).optional(),preview:object({thumbnail:url().optional(),demo:url().optional(),gif:url().optional()}).optional()});import{spawn}from"node:child_process";import{mkdir as mkdir4,readFile as readFile6,readdir,writeFile as writeFile5}from"node:fs/promises";import{resolve as resolve5}from"node:path";import{isAbsolute,join as join2,resolve as resolve2,sep}from"node:path";class GamecnError extends Error{constructor(message,name="GamecnError"){super(message);this.name=name}}class ItemNotFoundError extends GamecnError{spec;constructor(spec,reason){super(`Could not resolve item "${spec}": ${reason}`,"ItemNotFoundError");this.spec=spec}}class IntegrityError extends GamecnError{path;expected;actual;constructor(path,expected,actual){super(`Integrity check failed for ${path} (expected ${expected}, got ${actual})`,"IntegrityError");this.path=path;this.expected=expected;this.actual=actual}}class ConflictError extends GamecnError{path;constructor(path){super(`File already exists: ${path}`,"ConflictError");this.path=path}}class IncompatibleError extends GamecnError{constructor(message){super(message,"IncompatibleError")}}class IncompatibleVersionError extends GamecnError{itemName;engine;required;installed;constructor(itemName,engine,required2,installed){super(`${itemName} requires ${engine} ${required2}, but ${installed} is installed.`,"IncompatibleVersionError");this.itemName=itemName;this.engine=engine;this.required=required2;this.installed=installed}}class PathEscapeError extends GamecnError{constructor(target,resolved){super(`File target "${target}" resolves outside the project root (${resolved})`,"PathEscapeError")}}class UnsupportedSourceError extends GamecnError{constructor(message){super(message,"UnsupportedSourceError")}}class HttpError extends GamecnError{url;status;statusText;constructor(url2,status,statusText){super(`HTTP ${status} ${statusText}: ${url2}`,"HttpError");this.url=url2;this.status=status;this.statusText=statusText}}class MissingEnvVarError extends GamecnError{variable;constructor(variable){super(`Required environment variable not set: ${variable}`,"MissingEnvVarError");this.variable=variable}}function resolveTargetPath(config2,target,cwd){const paths=config2.paths;const placeholders={"{src}":paths.src,"{assets}":paths.assets,"{components}":paths.components??join2(paths.src,"components"),"{systems}":paths.systems??join2(paths.src,"systems"),"{scenes}":paths.scenes??join2(paths.src,"scenes"),"{shaders}":paths.shaders??join2(paths.src,"shaders"),"{ui}":paths.ui??join2(paths.src,"ui")};let interpolated=target;for(const[k3,v]of Object.entries(placeholders)){interpolated=interpolated.split(k3).join(v)}const absCwd=resolve2(cwd);const absTarget=isAbsolute(interpolated)?resolve2(interpolated):resolve2(absCwd,interpolated);if(absTarget!==absCwd&&!absTarget.startsWith(absCwd+sep)){throw new PathEscapeError(target,absTarget)}return absTarget}import{createHash}from"node:crypto";import{readFile,writeFile as writeFile2}from"node:fs/promises";import{join as join3}from"node:path";var LOCKFILE_NAME="gamecn-lock.json";async function loadLockfile(cwd){const path=join3(cwd,LOCKFILE_NAME);try{const raw=await readFile(path,"utf8");return LockfileSchema.parse(JSON.parse(raw))}catch(err){if(err.code==="ENOENT"){return{version:1,items:{}}}throw err}}async function saveLockfile(cwd,lock){const path=join3(cwd,LOCKFILE_NAME);await writeFile2(path,JSON.stringify(lock,null,2)+`
382
- `,"utf8")}function sha256OfBuffer(buf){const hash=createHash("sha256");hash.update(buf);return hash.digest("hex")}function recordInstall(lock,item,registry2,resolved,files){lock.items[item.name]={name:item.name,version:item.version,registry:registry2,resolved,installedAt:new Date().toISOString(),files}}var import_semver=__toESM(require_semver2(),1);import{readFile as readFile2,stat}from"node:fs/promises";import{join as join4,relative,resolve as resolve3}from"node:path";async function fileExists(path){try{await stat(path);return true}catch{return false}}function planFileSource(spec,baseDir,headers){if(spec.content!==undefined){return{kind:"inline",content:spec.content}}if(spec.url!==undefined){return{kind:"remote",url:spec.url,integrity:spec.integrity,...headers?{headers}:{}}}if(baseDir===undefined){throw new UnsupportedSourceError(`File "${spec.path}" has neither content nor url, and no baseDir is set for resolution.`)}return{kind:"local",absolutePath:resolve3(baseDir,spec.path)}}async function readPackageJson(cwd){try{const raw=await readFile2(join4(cwd,"package.json"),"utf8");return JSON.parse(raw)}catch{return}}function depName(spec){if(spec.startsWith("@")){const at2=spec.indexOf("@",1);return at2===-1?spec:spec.slice(0,at2)}const at=spec.indexOf("@");return at===-1?spec:spec.slice(0,at)}function normalizePath(p2){return p2.split(/[\\/]/).join("/")}async function plan(resolved,config2,lock,cwd){const item=resolved.item;const lockEntry=lock.items[item.name];const alreadyInstalled=lockEntry!==undefined&&lockEntry.version===item.version;const files=[];const conflicts=[];for(const spec of item.files){const target=resolveTargetPath(config2,spec.target,cwd);const source=planFileSource(spec,resolved.baseDir,resolved.headers);files.push({spec,source,target});if(await fileExists(target)){const targetRel=normalizePath(relative(cwd,target));const existingOwner=Object.values(lock.items).find((entry)=>entry.files.some((f)=>normalizePath(f.path)===targetRel));if(existingOwner===undefined||existingOwner.name!==item.name){conflicts.push({target,reason:"exists",ownedBy:existingOwner?.name})}}}const pkg=await readPackageJson(cwd);const allDeps={...pkg?.dependencies,...pkg?.devDependencies,...pkg?.peerDependencies};const npmDeps=(item.dependencies?.npm??[]).filter((d3)=>{const name=depName(d3);return!(name in allDeps)});const registryDeps=item.dependencies?.registry??[];const incompatibilities=checkEngineCompatibility(item,allDeps);return{item,resolved,files,conflicts,incompatibilities,npmDeps,registryDeps,alreadyInstalled}}function checkEngineCompatibility(item,allDeps){const required2=item.compatibility?.engines;if(!required2)return[];const out=[];for(const[engine,range]of Object.entries(required2)){const installed=allDeps[engine];if(!installed)continue;const minInstalled=import_semver.default.minVersion(installed);if(!minInstalled)continue;if(!import_semver.default.satisfies(minInstalled,range,{includePrerelease:true})){out.push({itemName:item.name,engine,required:range,installed,message:`${item.name} requires ${engine} ${range}, but ${installed} is installed.`})}}return out}import{randomBytes}from"node:crypto";import{mkdir as mkdir2,readFile as readFile3,rename,writeFile as writeFile3}from"node:fs/promises";import{dirname as dirname2,relative as relative2}from"node:path";async function readSource(file,fetcher){switch(file.source.kind){case"inline":return file.source.content;case"local":return readFile3(file.source.absolutePath);case"remote":if(!fetcher){throw new UnsupportedSourceError(`Remote file "${file.spec.path}" requires an AssetFetcher. Pass one to executePlan via the fetcher argument.`)}return fetcher.fetch(file.source.url,file.source.integrity,file.source.headers)}}async function writeAtomic(target,data){await mkdir2(dirname2(target),{recursive:true});const tmp=`${target}.tmp.${randomBytes(6).toString("hex")}`;await writeFile3(tmp,data);await rename(tmp,target)}function normalizeRelPath(absPath,cwd){return relative2(cwd,absPath).split(/[\\/]/).join("/")}async function executePlan(plan2,lock,cwd,options={},fetcher){const result={itemName:plan2.item.name,installed:[],skipped:[]};if(options.dryRun){return result}if(plan2.incompatibilities.length>0&&!options.force){const first=plan2.incompatibilities[0];throw new IncompatibleVersionError(first.itemName,first.engine,first.required,first.installed)}if(plan2.conflicts.length>0&&!options.force){const first=plan2.conflicts[0];throw new ConflictError(first.target)}for(const file of plan2.files){const data=await readSource(file,fetcher);await writeAtomic(file.target,data);const buf=typeof data==="string"?Buffer.from(data,"utf8"):data;const sha=sha256OfBuffer(buf);result.installed.push({path:normalizeRelPath(file.target,cwd),sha256:sha})}recordInstall(lock,plan2.item,plan2.resolved.registry,plan2.resolved.resolved,result.installed);return result}import{createHash as createHash3}from"node:crypto";import{mkdir as mkdir3,readFile as readFile4,rename as rename2,writeFile as writeFile4}from"node:fs/promises";import{homedir}from"node:os";import{dirname as dirname3,join as join5}from"node:path";import{createHash as createHash2}from"node:crypto";var SRI_RE=/^sha256-([A-Za-z0-9+/]+={0,2})$/;function parseSriSha256(sri){const m2=SRI_RE.exec(sri);if(!m2){throw new Error(`Invalid SRI string (expected "sha256-<base64>"): ${sri}`)}return Buffer.from(m2[1],"base64")}function sriOfBuffer(data){return"sha256-"+createHash2("sha256").update(data).digest("base64")}function hexOfSri(sri){return parseSriSha256(sri).toString("hex")}function verifyIntegrity(sri,data,label){const expected=parseSriSha256(sri);const actual=createHash2("sha256").update(data).digest();if(!expected.equals(actual)){throw new IntegrityError(label,sri,sriOfBuffer(data))}}function defaultCacheDir(){return process.env.GAMECN_CACHE_DIR??join5(homedir(),".gamecn","cache")}function sha1(s){return createHash3("sha1").update(s).digest("hex")}async function sleep(ms){return new Promise((res)=>setTimeout(res,ms))}async function fetchWithRetry(url2,init,opts={}){const attempts=opts.attempts??3;const initialDelayMs=opts.initialDelayMs??250;const fetcher=init.fetcher??fetch;const{fetcher:_3,...rest}=init;let lastErr;for(let i=0;i<attempts;i++){try{const res=await fetcher(url2,rest);if(res.status>=500&&res.status<600&&i<attempts-1){await sleep(initialDelayMs*2**i);continue}return res}catch(err){lastErr=err;if(i<attempts-1){await sleep(initialDelayMs*2**i);continue}}}throw lastErr??new Error(`fetch failed after ${attempts} attempts: ${url2}`)}async function readJsonIfExists(path){try{return JSON.parse(await readFile4(path,"utf8"))}catch(err){if(err.code==="ENOENT")return;throw err}}class ManifestCache{dir;fetcher;constructor(cacheDir,fetcher=fetch){this.dir=join5(cacheDir,"manifests");this.fetcher=fetcher}async getOrFetch(url2,headers={}){const key=sha1(url2);const file=join5(this.dir,`${key}.json`);const metaFile=join5(this.dir,`${key}.meta.json`);const meta2=await readJsonIfExists(metaFile);const reqHeaders={...headers};if(meta2?.etag)reqHeaders["If-None-Match"]=meta2.etag;const res=await fetchWithRetry(url2,{method:"GET",headers:reqHeaders,fetcher:this.fetcher});if(res.status===304){const cached2=await readJsonIfExists(file);if(cached2!==undefined)return cached2;const refetch=await fetchWithRetry(url2,{method:"GET",headers,fetcher:this.fetcher});return parseAndCache(refetch,url2,file,metaFile)}if(!res.ok){throw new HttpError(url2,res.status,res.statusText)}return parseAndCache(res,url2,file,metaFile)}}async function parseAndCache(res,url2,file,metaFile){const text=await res.text();let json;try{json=JSON.parse(text)}catch(err){throw new HttpError(url2,res.status,`invalid JSON: ${err.message}`)}await mkdir3(dirname3(file),{recursive:true});await writeFile4(file,text,"utf8");const meta2={etag:res.headers.get("etag")??undefined,fetchedAt:new Date().toISOString()};await writeFile4(metaFile,JSON.stringify(meta2),"utf8");return json}class AssetCache{dir;fetcher;constructor(cacheDir,fetcher=fetch){this.dir=join5(cacheDir,"assets");this.fetcher=fetcher}async getOrFetch(url2,integrity,headers={}){if(integrity){const hex2=hexOfSri(integrity);const cached2=join5(this.dir,hex2);try{const buf2=await readFile4(cached2);verifyIntegrity(integrity,buf2,cached2);return buf2}catch(err){if(err.code!=="ENOENT"){}}}const res=await fetchWithRetry(url2,{method:"GET",headers,fetcher:this.fetcher});if(!res.ok){throw new HttpError(url2,res.status,res.statusText)}const buf=Buffer.from(await res.arrayBuffer());if(integrity){verifyIntegrity(integrity,buf,url2)}const hex=createHash3("sha256").update(buf).digest("hex");const target=join5(this.dir,hex);await mkdir3(dirname3(target),{recursive:true});const tmp=`${target}.tmp.${createHash3("sha1").update(`${url2}${Date.now()}`).digest("hex").slice(0,8)}`;await writeFile4(tmp,buf);try{await rename2(tmp,target)}catch{}return buf}}import{isAbsolute as isAbsolute2}from"node:path";var ENV_VAR_RE=/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;function interpolateHeaders(headers,env=process.env){const out={};for(const[k3,v]of Object.entries(headers)){out[k3]=interpolate(v,env)}return out}function interpolate(template,env=process.env){return template.replace(ENV_VAR_RE,(_3,name)=>{const value=env[name];if(value===undefined){throw new MissingEnvVarError(name)}return value})}var DEFAULT_REGISTRY_URL="https://gamecn.dev/r/{name}.json";var BARE_NAME_RE=/^[a-z0-9][a-z0-9_-]{0,213}$/i;class DefaultRegistryResolver{config;cache;env;constructor(config2,cache,env=process.env){this.config=config2;this.cache=cache;this.env=env}canResolve(spec){if(spec.startsWith("@"))return false;if(spec.startsWith("http://")||spec.startsWith("https://"))return false;if(spec.startsWith("gh:")||spec.startsWith("github:"))return false;if(spec.startsWith("./")||spec.startsWith("../"))return false;if(isAbsolute2(spec))return false;if(spec.endsWith(".json"))return false;if(spec.includes("/")||spec.includes("\\"))return false;return BARE_NAME_RE.test(spec)}async resolve(spec,_ctx){const reg=this.config.registries?.["@main"];const urlTemplate=!reg?DEFAULT_REGISTRY_URL:typeof reg==="string"?reg:reg.url;const rawHeaders=typeof reg==="object"&&reg.headers?reg.headers:{};const headers=interpolateHeaders(rawHeaders,this.env);const url2=urlTemplate.replace(/\{name\}/g,spec);try{const json=await this.cache.getOrFetch(url2,headers);const item=RegistryItemSchema.parse(json);return{item,spec,registry:"@main",resolved:url2,headers:Object.keys(headers).length>0?headers:undefined}}catch(err){if(err instanceof HttpError&&err.status===404){throw new ItemNotFoundError(spec,`not found in the default @main registry (${url2}). Either configure 'registries["@main"]' in gamecn.json to point at a different registry, or use a fully-qualified spec (\`@scope/name\`, \`./local.json\`, \`gh:owner/repo/path\`, or \`https://...\`).`)}throw err}}}var GH_PREFIX_RE=/^(gh|github):/;function parseGitHubSpec(spec){if(!GH_PREFIX_RE.test(spec)){throw new ItemNotFoundError(spec,"not a GitHub spec")}const stripped=spec.replace(GH_PREFIX_RE,"");const hashIdx=stripped.indexOf("#");const pathPart=hashIdx===-1?stripped:stripped.slice(0,hashIdx);const ref=hashIdx===-1?"HEAD":stripped.slice(hashIdx+1)||"HEAD";const segs=pathPart.split("/").filter(Boolean);if(segs.length<3){throw new ItemNotFoundError(spec,"expected gh:owner/repo/path/to/item.json")}const[owner,repo,...rest]=segs;return{owner,repo,path:rest.join("/"),ref,hasExplicitRef:hashIdx!==-1}}function githubSpecToUrl(spec){const p2=parseGitHubSpec(spec);return`https://raw.githubusercontent.com/${p2.owner}/${p2.repo}/${p2.ref}/${p2.path}`}class GitHubResolver{cache;env;warn;constructor(cache,env=process.env,warn=(m2)=>console.warn(m2)){this.cache=cache;this.env=env;this.warn=warn}canResolve(spec){return GH_PREFIX_RE.test(spec)}async resolve(spec,_ctx){const parsed=parseGitHubSpec(spec);if(!parsed.hasExplicitRef){this.warn(`[gamecn] ${spec} has no ref; using HEAD (non-reproducible). Pin a ref with #v1.2.0 or #commit-sha.`)}const url2=githubSpecToUrl(spec);const headers={};const token=this.env["GITHUB_TOKEN"];if(token)headers["Authorization"]=`token ${token}`;const json=await this.cache.getOrFetch(url2,headers);const item=RegistryItemSchema.parse(json);return{item,spec,registry:"github",resolved:url2,headers:Object.keys(headers).length>0?headers:undefined}}}class HttpsResolver{cache;headers;constructor(cache,headers={}){this.cache=cache;this.headers=headers}canResolve(spec){return spec.startsWith("http://")||spec.startsWith("https://")}async resolve(spec,_ctx){const json=await this.cache.getOrFetch(spec,this.headers);const item=RegistryItemSchema.parse(json);return{item,spec,registry:"https",resolved:spec,headers:Object.keys(this.headers).length>0?this.headers:undefined}}}import{readFile as readFile5}from"node:fs/promises";import{dirname as dirname4,isAbsolute as isAbsolute3,resolve as resolve4}from"node:path";class LocalFileResolver{canResolve(spec){if(spec.startsWith("./")||spec.startsWith("../"))return true;if(spec.startsWith("/")||spec.startsWith("~/"))return true;if(/^[A-Za-z]:[\\/]/.test(spec))return true;if(spec.endsWith(".json"))return true;return false}async resolve(spec,ctx){const absSpec=isAbsolute3(spec)?spec:resolve4(ctx.cwd,spec);let raw;try{raw=await readFile5(absSpec,"utf8")}catch(err){const code=err.code;throw new ItemNotFoundError(spec,code==="ENOENT"?"file not found":`read failed: ${err.message}`)}let json;try{json=JSON.parse(raw)}catch(err){throw new ItemNotFoundError(spec,`JSON parse error: ${err.message}`)}const item=RegistryItemSchema.parse(json);return{item,spec,registry:"local",resolved:absSpec,baseDir:dirname4(absSpec)}}}var NAMESPACE_RE=/^(@[A-Za-z0-9_-]+)\/(.+)$/;class NamespaceResolver{config;cache;env;constructor(config2,cache,env=process.env){this.config=config2;this.cache=cache;this.env=env}canResolve(spec){return NAMESPACE_RE.test(spec)}async resolve(spec,_ctx){const m2=NAMESPACE_RE.exec(spec);if(!m2)throw new ItemNotFoundError(spec,"not a namespaced spec");const namespace=m2[1];const name=m2[2];const reg=this.config.registries?.[namespace];if(!reg){throw new ItemNotFoundError(spec,`unknown registry namespace "${namespace}". Add it to gamecn.json#registries.`)}const urlTemplate=typeof reg==="string"?reg:reg.url;const rawHeaders=typeof reg==="string"?{}:reg.headers??{};const headers=interpolateHeaders(rawHeaders,this.env);const url2=urlTemplate.replace(/\{name\}/g,name);const json=await this.cache.getOrFetch(url2,headers);const item=RegistryItemSchema.parse(json);return{item,spec,registry:namespace,resolved:url2,headers:Object.keys(headers).length>0?headers:undefined}}}function createResolver(config2,opts={}){const cacheDir=opts.cacheDir??defaultCacheDir();const fetcher=opts.fetcher??fetch;const env=opts.env??process.env;const manifestCache=new ManifestCache(cacheDir,fetcher);const resolvers=[new NamespaceResolver(config2,manifestCache,env),new GitHubResolver(manifestCache,env),new DefaultRegistryResolver(config2,manifestCache,env),new HttpsResolver(manifestCache),new LocalFileResolver];return{canResolve(spec){return resolvers.some((r2)=>r2.canResolve(spec))},async resolve(spec,ctx){for(const r2 of resolvers){if(r2.canResolve(spec))return r2.resolve(spec,ctx)}throw new ItemNotFoundError(spec,"no resolver matches this spec")}}}function createAssetFetcher(opts={}){const cacheDir=opts.cacheDir??defaultCacheDir();const fetcher=opts.fetcher??fetch;const cache=new AssetCache(cacheDir,fetcher);return{fetch:(url2,integrity,headers)=>cache.getOrFetch(url2,integrity,headers)}}var TEXT_EXTS=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs"]);var MAX_SCAN_BYTES=256*1024;var SKIP_DIRS=new Set(["node_modules",".git","dist",".turbo",".cache","registry-dist"]);var DEFAULT_ENDPOINT="https://api.gamecn.dev";var TIMEOUT_MS=2000;function isAnalyticsDisabled(){const env=process.env;return env.GAMECN_TELEMETRY==="0"||env.GAMECN_TELEMETRY==="false"||env.DO_NOT_TRACK==="1"||env.NODE_ENV==="test"}function resolveEndpoint(ctx){return ctx?.endpoint??process.env.GAMECN_ANALYTICS_ENDPOINT??DEFAULT_ENDPOINT}async function post(path,body,ctx){if(isAnalyticsDisabled())return;const url2=`${resolveEndpoint(ctx)}${path}`;const ctrl=new AbortController;const timer=setTimeout(()=>ctrl.abort(),TIMEOUT_MS);try{await fetch(url2,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(body),signal:ctrl.signal})}catch{}finally{clearTimeout(timer)}}async function trackTemplateInstall(event,ctx){await post("/analytics/install-template",{id:event.id,version:event.version,cliVersion:ctx?.cliVersion,packageManager:ctx?.packageManager},ctx)}async function trackProjectInit(event,ctx){await post("/analytics/project-init",{flow:event.flow,engine:event.engine,framework:event.framework,language:event.language,template:event.template,cliVersion:ctx?.cliVersion,packageManager:ctx?.packageManager},ctx)}var DEFAULT_REGISTRY="https://gamecn.dev/r/{name}.json";function detectPackageManager2(){const ua=process.env.npm_config_user_agent;if(ua){const name=ua.split(" ")[0]?.split("/")[0];if(name==="bun"||name==="pnpm"||name==="yarn"||name==="npm"){return name}}return"pnpm"}var NAME_RE=/^[a-z0-9][a-z0-9._-]{0,213}$/i;var RESERVED=new Set(["node_modules","favicon.ico"]);function validateProjectName(name){if(!NAME_RE.test(name)){throw new Error(`Invalid project name "${name}". Use lowercase letters, numbers, dot, underscore, or hyphen.`)}if(RESERVED.has(name)||name.startsWith(".")||name.startsWith("_")){throw new Error(`Reserved or invalid project name: "${name}".`)}}async function dirIsEmptyOrMissing(path){try{const entries=await readdir(path);return entries.length===0}catch(err){if(err.code==="ENOENT")return true;throw err}}function resolveTemplateSpec(template){if(template.startsWith("@")||template.startsWith("http://")||template.startsWith("https://")||template.startsWith("gh:")||template.startsWith("github:")||template.startsWith("./")||template.startsWith("/")||/^[A-Za-z]:[\\/]/.test(template)){return template}return`@main/${template}`}async function defaultInstallRunner(cwd,pm){await new Promise((res,rej)=>{const proc=spawn(pm,["install"],{cwd,stdio:"inherit",shell:process.platform==="win32"});proc.on("exit",(code)=>{if(code===0)res();else rej(new Error(`${pm} install exited with code ${code}`))});proc.on("error",rej)})}async function scaffold(opts){validateProjectName(opts.name);const parentDir=opts.parentDir??process.cwd();const cwd=resolve5(parentDir,opts.name);if(!await dirIsEmptyOrMissing(cwd)&&!opts.force){throw new Error(`Directory "${opts.name}" already exists and is not empty. Pass --force to overwrite.`)}await mkdir4(cwd,{recursive:true});const pm=opts.packageManager??detectPackageManager2();const config2={engine:"phaser",framework:"vanilla",language:"typescript",packageManager:pm,paths:{src:"src",assets:"public/assets"},registries:{"@main":opts.registry??DEFAULT_REGISTRY}};const resolverOpts=opts.cacheDir?{cacheDir:opts.cacheDir}:{};const resolver=createResolver(config2,resolverOpts);const fetcher=createAssetFetcher(resolverOpts);const spec=resolveTemplateSpec(opts.template);const resolved=await resolver.resolve(spec,{cwd,config:config2});if(resolved.item.type!=="registry:template"){throw new IncompatibleError(`"${spec}" is type ${resolved.item.type}, not registry:template.`)}const lock=await loadLockfile(cwd);const p2=await plan(resolved,config2,lock,cwd);const result=await executePlan(p2,lock,cwd,{force:opts.force??false},fetcher);await saveLockfile(cwd,lock);const pkgPath=resolve5(cwd,"package.json");try{const raw=await readFile6(pkgPath,"utf8");const pkg=JSON.parse(raw);pkg.name=opts.name;await writeFile5(pkgPath,JSON.stringify(pkg,null,2)+`
383
- `,"utf8")}catch(err){if(err.code!=="ENOENT")throw err}const postScaffoldDeps=await installPostScaffoldDeps(cwd,opts.registry,opts.cacheDir,opts.force??false);const templateTags=resolved.item.tags??[];await reportScaffoldTelemetry({cwd,templateName:resolved.item.name,templateVersion:resolved.item.version,packageManager:pm,cliVersion:opts.cliVersion});if(opts.onAfterDeps){await opts.onAfterDeps({cwd,templateTags})}let installed=false;if(!opts.skipInstall){const runner=opts.installRunner??defaultInstallRunner;await runner(cwd,pm);installed=true}return{cwd,packageManager:pm,installed,templateName:resolved.item.name,templateTags,filesWritten:result.installed.length+postScaffoldDeps.length,postScaffoldDeps:postScaffoldDeps.map((d3)=>d3.spec)}}async function reportScaffoldTelemetry(info){const ctx={cliVersion:info.cliVersion,packageManager:info.packageManager};let project={};try{const raw=await readFile6(resolve5(info.cwd,"gamecn.json"),"utf8");const parsed=GamecnConfigSchema.parse(JSON.parse(raw));project={engine:parsed.engine,framework:parsed.framework,language:parsed.language}}catch{}await Promise.allSettled([trackTemplateInstall({id:info.templateName,version:info.templateVersion},ctx),trackProjectInit({flow:"scaffold",template:info.templateName,...project},ctx)])}async function installPostScaffoldDeps(cwd,registryOverride,cacheDir,force){const cfgPath=resolve5(cwd,"gamecn.json");let raw;try{raw=await readFile6(cfgPath,"utf8")}catch(err){if(err.code==="ENOENT")return[];throw err}const projectConfig=GamecnConfigSchema.parse(JSON.parse(raw));const deps=projectConfig.deps??[];if(deps.length===0)return[];const config2=registryOverride?{...projectConfig,registries:{...projectConfig.registries,"@main":registryOverride}}:projectConfig;const resolverOpts=cacheDir?{cacheDir}:{};const resolver=createResolver(config2,resolverOpts);const fetcher=createAssetFetcher(resolverOpts);const lock=await loadLockfile(cwd);const installed=[];for(const spec of deps){const resolvedDep=await resolver.resolve(spec,{cwd,config:config2});if(resolvedDep.item.type==="registry:template"){throw new IncompatibleError(`Template's gamecn.json#deps entry "${spec}" resolved to a registry:template — deps[] is for components/systems/assets, not templates.`)}const p2=await plan(resolvedDep,config2,lock,cwd);const r2=await executePlan(p2,lock,cwd,{force},fetcher);installed.push({spec,fileCount:r2.installed.length})}await saveLockfile(cwd,lock);return installed}var CLI_VERSION="0.0.4-1";var TEMPLATES=[{value:"phaser-endless-runner",label:"Phaser Endless Runner",hint:"Vite + TypeScript + Phaser 3"}];async function pickTemplate(opts){if(opts.template)return opts.template;if(opts.yes){throw new Error("--template is required when --yes is set.")}const choice=await ve({message:"Template?",options:TEMPLATES});if(pD(choice)){xe("Cancelled.");process.exit(1)}return choice}function parseSkillsFlag(value){if(!value)return;return value.split(",").map((s)=>s.trim()).filter((s)=>s.length>0)}async function readProjectContext(cwd){try{const raw=await readFile7(resolvePath(cwd,"gamecn.json"),"utf8");const parsed=GamecnConfigSchema.parse(JSON.parse(raw));return{engine:parsed.engine,framework:parsed.framework,language:parsed.language}}catch{return{engine:"vanilla",framework:"vanilla",language:"typescript"}}}var program2=new Command;program2.name("create-gamecn").description("Scaffold a new gamecn project from a template").version("0.0.1").argument("<name>","project directory name").option("--template <name>","template name or full spec").option("--registry <url>","registry URL with {name} placeholder",DEFAULT_REGISTRY).option("--package-manager <pm>","package manager (npm|pnpm|yarn|bun); defaults to the one that invoked this command").option("--yes","skip prompts; installs always-recommended skills").option("--force","overwrite a non-empty target directory").option("--no-install","skip dependency install").option("--skills <list>","comma-separated skill names to install; `--no-skills` to skip entirely").option("--no-skills","skip the skills install step entirely").action(async(name,opts)=>{try{if(!opts.yes)Ie(import_picocolors4.default.cyan(`create-gamecn ${name}`));const template=await pickTemplate(opts);const pm=opts.packageManager??detectPackageManager2();const preselected=typeof opts.skills==="string"?parseSkillsFlag(opts.skills):undefined;const scaffoldOpts={name,template,registry:opts.registry,packageManager:pm,skipInstall:opts.install===false,force:opts.force,cliVersion:CLI_VERSION,onAfterDeps:async({cwd,templateTags})=>{const projCtx=await readProjectContext(cwd);const ctx={...projCtx,templateTags,flow:"scaffold"};const promptResult=await promptSkillsInstall(ctx,{yes:!!opts.yes,skip:opts.skills===false,preselected,cwd});renderUnknownPreselected(promptResult.unknownPreselected);if(promptResult.recs.length===0)return;const installResult=await installSkills(promptResult.recs,{cwd});renderSkillsSummary(installResult)}};const result=await scaffold(scaffoldOpts);const next=result.installed?`cd ${name}
382
+ `,"utf8")}function sha256OfBuffer(buf){const hash=createHash("sha256");hash.update(buf);return hash.digest("hex")}function recordInstall(lock,item,registry2,resolved,files){lock.items[item.name]={name:item.name,version:item.version,registry:registry2,resolved,installedAt:new Date().toISOString(),files}}var import_semver=__toESM(require_semver2(),1);import{readFile as readFile2,stat}from"node:fs/promises";import{join as join4,relative,resolve as resolve3}from"node:path";async function fileExists(path){try{await stat(path);return true}catch{return false}}function planFileSource(spec,baseDir,headers){if(spec.content!==undefined){return{kind:"inline",content:spec.content}}if(spec.url!==undefined){return{kind:"remote",url:spec.url,integrity:spec.integrity,...headers?{headers}:{}}}if(baseDir===undefined){throw new UnsupportedSourceError(`File "${spec.path}" has neither content nor url, and no baseDir is set for resolution.`)}return{kind:"local",absolutePath:resolve3(baseDir,spec.path)}}async function readPackageJson(cwd){try{const raw=await readFile2(join4(cwd,"package.json"),"utf8");return JSON.parse(raw)}catch{return}}function depName(spec){if(spec.startsWith("@")){const at2=spec.indexOf("@",1);return at2===-1?spec:spec.slice(0,at2)}const at=spec.indexOf("@");return at===-1?spec:spec.slice(0,at)}function normalizePath(p2){return p2.split(/[\\/]/).join("/")}async function plan(resolved,config2,lock,cwd){const item=resolved.item;const lockEntry=lock.items[item.name];const alreadyInstalled=lockEntry!==undefined&&lockEntry.version===item.version;const files=[];const conflicts=[];for(const spec of item.files){const target=resolveTargetPath(config2,spec.target,cwd);const source=planFileSource(spec,resolved.baseDir,resolved.headers);files.push({spec,source,target});if(await fileExists(target)){const targetRel=normalizePath(relative(cwd,target));const existingOwner=Object.values(lock.items).find((entry)=>entry.files.some((f)=>normalizePath(f.path)===targetRel));if(existingOwner===undefined||existingOwner.name!==item.name){conflicts.push({target,reason:"exists",ownedBy:existingOwner?.name})}}}const pkg=await readPackageJson(cwd);const allDeps={...pkg?.dependencies,...pkg?.devDependencies,...pkg?.peerDependencies};const npmDeps=(item.dependencies?.npm??[]).filter((d3)=>{const name=depName(d3);return!(name in allDeps)});const registryDeps=item.dependencies?.registry??[];const incompatibilities=checkEngineCompatibility(item,allDeps);return{item,resolved,files,conflicts,incompatibilities,npmDeps,registryDeps,alreadyInstalled}}function checkEngineCompatibility(item,allDeps){const required2=item.compatibility?.engines;if(!required2)return[];const out=[];for(const[engine,range]of Object.entries(required2)){const installed=allDeps[engine];if(!installed)continue;const minInstalled=import_semver.default.minVersion(installed);if(!minInstalled)continue;if(!import_semver.default.satisfies(minInstalled,range,{includePrerelease:true})){out.push({itemName:item.name,engine,required:range,installed,message:`${item.name} requires ${engine} ${range}, but ${installed} is installed.`})}}return out}import{randomBytes}from"node:crypto";import{mkdir as mkdir2,readFile as readFile3,rename,writeFile as writeFile3}from"node:fs/promises";import{dirname as dirname2,relative as relative2}from"node:path";async function readSource(file,fetcher){switch(file.source.kind){case"inline":return file.source.content;case"local":return readFile3(file.source.absolutePath);case"remote":if(!fetcher){throw new UnsupportedSourceError(`Remote file "${file.spec.path}" requires an AssetFetcher. Pass one to executePlan via the fetcher argument.`)}return fetcher.fetch(file.source.url,file.source.integrity,file.source.headers)}}async function writeAtomic(target,data){await mkdir2(dirname2(target),{recursive:true});const tmp=`${target}.tmp.${randomBytes(6).toString("hex")}`;await writeFile3(tmp,data);await rename(tmp,target)}function normalizeRelPath(absPath,cwd){return relative2(cwd,absPath).split(/[\\/]/).join("/")}async function executePlan(plan2,lock,cwd,options={},fetcher){const result={itemName:plan2.item.name,installed:[],skipped:[]};if(options.dryRun){return result}if(plan2.incompatibilities.length>0&&!options.force){const first=plan2.incompatibilities[0];throw new IncompatibleVersionError(first.itemName,first.engine,first.required,first.installed)}if(plan2.conflicts.length>0&&!options.force){const first=plan2.conflicts[0];throw new ConflictError(first.target)}for(const file of plan2.files){const data=await readSource(file,fetcher);await writeAtomic(file.target,data);const buf=typeof data==="string"?Buffer.from(data,"utf8"):data;const sha=sha256OfBuffer(buf);result.installed.push({path:normalizeRelPath(file.target,cwd),sha256:sha})}recordInstall(lock,plan2.item,plan2.resolved.registry,plan2.resolved.resolved,result.installed);return result}import{createHash as createHash3}from"node:crypto";import{mkdir as mkdir3,readFile as readFile4,rename as rename2,writeFile as writeFile4}from"node:fs/promises";import{homedir}from"node:os";import{dirname as dirname3,join as join5}from"node:path";import{createHash as createHash2}from"node:crypto";var SRI_RE=/^sha256-([A-Za-z0-9+/]+={0,2})$/;function parseSriSha256(sri){const m2=SRI_RE.exec(sri);if(!m2){throw new Error(`Invalid SRI string (expected "sha256-<base64>"): ${sri}`)}return Buffer.from(m2[1],"base64")}function sriOfBuffer(data){return"sha256-"+createHash2("sha256").update(data).digest("base64")}function hexOfSri(sri){return parseSriSha256(sri).toString("hex")}function verifyIntegrity(sri,data,label){const expected=parseSriSha256(sri);const actual=createHash2("sha256").update(data).digest();if(!expected.equals(actual)){throw new IntegrityError(label,sri,sriOfBuffer(data))}}function defaultCacheDir(){return process.env.GAMECN_CACHE_DIR??join5(homedir(),".gamecn","cache")}function sha1(s){return createHash3("sha1").update(s).digest("hex")}async function sleep(ms){return new Promise((res)=>setTimeout(res,ms))}async function fetchWithRetry(url2,init,opts={}){const attempts=opts.attempts??3;const initialDelayMs=opts.initialDelayMs??250;const fetcher=init.fetcher??fetch;const{fetcher:_3,...rest}=init;let lastErr;for(let i=0;i<attempts;i++){try{const res=await fetcher(url2,rest);if(res.status>=500&&res.status<600&&i<attempts-1){await sleep(initialDelayMs*2**i);continue}return res}catch(err){lastErr=err;if(i<attempts-1){await sleep(initialDelayMs*2**i);continue}}}throw lastErr??new Error(`fetch failed after ${attempts} attempts: ${url2}`)}async function readJsonIfExists(path){try{return JSON.parse(await readFile4(path,"utf8"))}catch(err){if(err.code==="ENOENT")return;throw err}}class ManifestCache{dir;fetcher;constructor(cacheDir,fetcher=fetch){this.dir=join5(cacheDir,"manifests");this.fetcher=fetcher}async getOrFetch(url2,headers={}){const key=sha1(url2);const file=join5(this.dir,`${key}.json`);const metaFile=join5(this.dir,`${key}.meta.json`);const meta2=await readJsonIfExists(metaFile);const reqHeaders={...headers};if(meta2?.etag)reqHeaders["If-None-Match"]=meta2.etag;const res=await fetchWithRetry(url2,{method:"GET",headers:reqHeaders,fetcher:this.fetcher});if(res.status===304){const cached2=await readJsonIfExists(file);if(cached2!==undefined)return cached2;const refetch=await fetchWithRetry(url2,{method:"GET",headers,fetcher:this.fetcher});return parseAndCache(refetch,url2,file,metaFile)}if(!res.ok){throw new HttpError(url2,res.status,res.statusText)}return parseAndCache(res,url2,file,metaFile)}}async function parseAndCache(res,url2,file,metaFile){const text=await res.text();let json;try{json=JSON.parse(text)}catch(err){throw new HttpError(url2,res.status,`invalid JSON: ${err.message}`)}await mkdir3(dirname3(file),{recursive:true});await writeFile4(file,text,"utf8");const meta2={etag:res.headers.get("etag")??undefined,fetchedAt:new Date().toISOString()};await writeFile4(metaFile,JSON.stringify(meta2),"utf8");return json}class AssetCache{dir;fetcher;constructor(cacheDir,fetcher=fetch){this.dir=join5(cacheDir,"assets");this.fetcher=fetcher}async getOrFetch(url2,integrity,headers={}){if(integrity){const hex2=hexOfSri(integrity);const cached2=join5(this.dir,hex2);try{const buf2=await readFile4(cached2);verifyIntegrity(integrity,buf2,cached2);return buf2}catch(err){if(err.code!=="ENOENT"){}}}const res=await fetchWithRetry(url2,{method:"GET",headers,fetcher:this.fetcher});if(!res.ok){throw new HttpError(url2,res.status,res.statusText)}const buf=Buffer.from(await res.arrayBuffer());if(integrity){verifyIntegrity(integrity,buf,url2)}const hex=createHash3("sha256").update(buf).digest("hex");const target=join5(this.dir,hex);await mkdir3(dirname3(target),{recursive:true});const tmp=`${target}.tmp.${createHash3("sha1").update(`${url2}${Date.now()}`).digest("hex").slice(0,8)}`;await writeFile4(tmp,buf);try{await rename2(tmp,target)}catch{}return buf}}import{isAbsolute as isAbsolute2}from"node:path";var ENV_VAR_RE=/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;function interpolateHeaders(headers,env=process.env){const out={};for(const[k3,v]of Object.entries(headers)){out[k3]=interpolate(v,env)}return out}function interpolate(template,env=process.env){return template.replace(ENV_VAR_RE,(_3,name)=>{const value=env[name];if(value===undefined){throw new MissingEnvVarError(name)}return value})}var DEFAULT_REGISTRY_URL="https://gamecn.dev/r/{name}.json";var BARE_NAME_RE=/^[a-z0-9][a-z0-9_-]{0,213}$/i;class DefaultRegistryResolver{config;cache;env;constructor(config2,cache,env=process.env){this.config=config2;this.cache=cache;this.env=env}canResolve(spec){if(spec.startsWith("@"))return false;if(spec.startsWith("http://")||spec.startsWith("https://"))return false;if(spec.startsWith("gh:")||spec.startsWith("github:"))return false;if(spec.startsWith("./")||spec.startsWith("../"))return false;if(isAbsolute2(spec))return false;if(spec.endsWith(".json"))return false;if(spec.includes("/")||spec.includes("\\"))return false;return BARE_NAME_RE.test(spec)}async resolve(spec,_ctx){const reg=this.config.registries?.["@main"];const urlTemplate=!reg?DEFAULT_REGISTRY_URL:typeof reg==="string"?reg:reg.url;const rawHeaders=typeof reg==="object"&&reg.headers?reg.headers:{};const headers=interpolateHeaders(rawHeaders,this.env);const url2=urlTemplate.replace(/\{name\}/g,spec);try{const json=await this.cache.getOrFetch(url2,headers);const item=RegistryItemSchema.parse(json);return{item,spec,registry:"@main",resolved:url2,headers:Object.keys(headers).length>0?headers:undefined}}catch(err){if(err instanceof HttpError&&err.status===404){throw new ItemNotFoundError(spec,`not found in the default @main registry (${url2}). Either configure 'registries["@main"]' in gamecn.json to point at a different registry, or use a fully-qualified spec (\`@scope/name\`, \`./local.json\`, \`gh:owner/repo/path\`, or \`https://...\`).`)}throw err}}}var GH_PREFIX_RE=/^(gh|github):/;function parseGitHubSpec(spec){if(!GH_PREFIX_RE.test(spec)){throw new ItemNotFoundError(spec,"not a GitHub spec")}const stripped=spec.replace(GH_PREFIX_RE,"");const hashIdx=stripped.indexOf("#");const pathPart=hashIdx===-1?stripped:stripped.slice(0,hashIdx);const ref=hashIdx===-1?"HEAD":stripped.slice(hashIdx+1)||"HEAD";const segs=pathPart.split("/").filter(Boolean);if(segs.length<3){throw new ItemNotFoundError(spec,"expected gh:owner/repo/path/to/item.json")}const[owner,repo,...rest]=segs;return{owner,repo,path:rest.join("/"),ref,hasExplicitRef:hashIdx!==-1}}function githubSpecToUrl(spec){const p2=parseGitHubSpec(spec);return`https://raw.githubusercontent.com/${p2.owner}/${p2.repo}/${p2.ref}/${p2.path}`}class GitHubResolver{cache;env;warn;constructor(cache,env=process.env,warn=(m2)=>console.warn(m2)){this.cache=cache;this.env=env;this.warn=warn}canResolve(spec){return GH_PREFIX_RE.test(spec)}async resolve(spec,_ctx){const parsed=parseGitHubSpec(spec);if(!parsed.hasExplicitRef){this.warn(`[gamecn] ${spec} has no ref; using HEAD (non-reproducible). Pin a ref with #v1.2.0 or #commit-sha.`)}const url2=githubSpecToUrl(spec);const headers={};const token=this.env["GITHUB_TOKEN"];if(token)headers["Authorization"]=`token ${token}`;const json=await this.cache.getOrFetch(url2,headers);const item=RegistryItemSchema.parse(json);return{item,spec,registry:"github",resolved:url2,headers:Object.keys(headers).length>0?headers:undefined}}}class HttpsResolver{cache;headers;constructor(cache,headers={}){this.cache=cache;this.headers=headers}canResolve(spec){return spec.startsWith("http://")||spec.startsWith("https://")}async resolve(spec,_ctx){const json=await this.cache.getOrFetch(spec,this.headers);const item=RegistryItemSchema.parse(json);return{item,spec,registry:"https",resolved:spec,headers:Object.keys(this.headers).length>0?this.headers:undefined}}}import{readFile as readFile5}from"node:fs/promises";import{dirname as dirname4,isAbsolute as isAbsolute3,resolve as resolve4}from"node:path";class LocalFileResolver{canResolve(spec){if(spec.startsWith("./")||spec.startsWith("../"))return true;if(spec.startsWith("/")||spec.startsWith("~/"))return true;if(/^[A-Za-z]:[\\/]/.test(spec))return true;if(spec.endsWith(".json"))return true;return false}async resolve(spec,ctx){const absSpec=isAbsolute3(spec)?spec:resolve4(ctx.cwd,spec);let raw;try{raw=await readFile5(absSpec,"utf8")}catch(err){const code=err.code;throw new ItemNotFoundError(spec,code==="ENOENT"?"file not found":`read failed: ${err.message}`)}let json;try{json=JSON.parse(raw)}catch(err){throw new ItemNotFoundError(spec,`JSON parse error: ${err.message}`)}const item=RegistryItemSchema.parse(json);return{item,spec,registry:"local",resolved:absSpec,baseDir:dirname4(absSpec)}}}var NAMESPACE_RE=/^(@[A-Za-z0-9_-]+)\/(.+)$/;class NamespaceResolver{config;cache;env;constructor(config2,cache,env=process.env){this.config=config2;this.cache=cache;this.env=env}canResolve(spec){return NAMESPACE_RE.test(spec)}async resolve(spec,_ctx){const m2=NAMESPACE_RE.exec(spec);if(!m2)throw new ItemNotFoundError(spec,"not a namespaced spec");const namespace=m2[1];const name=m2[2];const reg=this.config.registries?.[namespace];if(!reg){throw new ItemNotFoundError(spec,`unknown registry namespace "${namespace}". Add it to gamecn.json#registries.`)}const urlTemplate=typeof reg==="string"?reg:reg.url;const rawHeaders=typeof reg==="string"?{}:reg.headers??{};const headers=interpolateHeaders(rawHeaders,this.env);const url2=urlTemplate.replace(/\{name\}/g,name);const json=await this.cache.getOrFetch(url2,headers);const item=RegistryItemSchema.parse(json);return{item,spec,registry:namespace,resolved:url2,headers:Object.keys(headers).length>0?headers:undefined}}}function createResolver(config2,opts={}){const cacheDir=opts.cacheDir??defaultCacheDir();const fetcher=opts.fetcher??fetch;const env=opts.env??process.env;const manifestCache=new ManifestCache(cacheDir,fetcher);const resolvers=[new NamespaceResolver(config2,manifestCache,env),new GitHubResolver(manifestCache,env),new DefaultRegistryResolver(config2,manifestCache,env),new HttpsResolver(manifestCache),new LocalFileResolver];return{canResolve(spec){return resolvers.some((r2)=>r2.canResolve(spec))},async resolve(spec,ctx){for(const r2 of resolvers){if(r2.canResolve(spec))return r2.resolve(spec,ctx)}throw new ItemNotFoundError(spec,"no resolver matches this spec")}}}function createAssetFetcher(opts={}){const cacheDir=opts.cacheDir??defaultCacheDir();const fetcher=opts.fetcher??fetch;const cache=new AssetCache(cacheDir,fetcher);return{fetch:(url2,integrity,headers)=>cache.getOrFetch(url2,integrity,headers)}}var TEXT_EXTS=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs"]);var MAX_SCAN_BYTES=256*1024;var SKIP_DIRS=new Set(["node_modules",".git","dist",".turbo",".cache","registry-dist"]);var DEFAULT_ENDPOINT="https://api.gamecn.dev";var TIMEOUT_MS=2000;function isAnalyticsDisabled(){const env=process.env;return env.GAMECN_TELEMETRY==="0"||env.GAMECN_TELEMETRY==="false"||env.DO_NOT_TRACK==="1"||env.NODE_ENV==="test"}function resolveEndpoint(ctx){return ctx?.endpoint??process.env.GAMECN_ANALYTICS_ENDPOINT??DEFAULT_ENDPOINT}async function post(path,body,ctx){if(ctx?.disabled||isAnalyticsDisabled())return;const url2=`${resolveEndpoint(ctx)}${path}`;const ctrl=new AbortController;const timer=setTimeout(()=>ctrl.abort(),TIMEOUT_MS);try{await fetch(url2,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(body),signal:ctrl.signal})}catch{}finally{clearTimeout(timer)}}async function trackTemplateInstall(event,ctx){await post("/analytics/install-template",{id:event.id,version:event.version,cliVersion:ctx?.cliVersion,packageManager:ctx?.packageManager},ctx)}async function trackProjectInit(event,ctx){await post("/analytics/project-init",{flow:event.flow,engine:event.engine,framework:event.framework,language:event.language,template:event.template,cliVersion:ctx?.cliVersion,packageManager:ctx?.packageManager},ctx)}var DEFAULT_REGISTRY="https://gamecn.dev/r/{name}.json";function detectPackageManager2(){const ua=process.env.npm_config_user_agent;if(ua){const name=ua.split(" ")[0]?.split("/")[0];if(name==="bun"||name==="pnpm"||name==="yarn"||name==="npm"){return name}}return"pnpm"}var NAME_RE=/^[a-z0-9][a-z0-9._-]{0,213}$/i;var RESERVED=new Set(["node_modules","favicon.ico"]);function validateProjectName(name){if(!NAME_RE.test(name)){throw new Error(`Invalid project name "${name}". Use lowercase letters, numbers, dot, underscore, or hyphen.`)}if(RESERVED.has(name)||name.startsWith(".")||name.startsWith("_")){throw new Error(`Reserved or invalid project name: "${name}".`)}}async function dirIsEmptyOrMissing(path){try{const entries=await readdir(path);return entries.length===0}catch(err){if(err.code==="ENOENT")return true;throw err}}function resolveTemplateSpec(template){if(template.startsWith("@")||template.startsWith("http://")||template.startsWith("https://")||template.startsWith("gh:")||template.startsWith("github:")||template.startsWith("./")||template.startsWith("/")||/^[A-Za-z]:[\\/]/.test(template)){return template}return`@main/${template}`}async function defaultInstallRunner(cwd,pm){await new Promise((res,rej)=>{const proc=spawn(pm,["install"],{cwd,stdio:"inherit",shell:process.platform==="win32"});proc.on("exit",(code)=>{if(code===0)res();else rej(new Error(`${pm} install exited with code ${code}`))});proc.on("error",rej)})}async function scaffold(opts){validateProjectName(opts.name);const parentDir=opts.parentDir??process.cwd();const cwd=resolve5(parentDir,opts.name);if(!await dirIsEmptyOrMissing(cwd)&&!opts.force){throw new Error(`Directory "${opts.name}" already exists and is not empty. Pass --force to overwrite.`)}await mkdir4(cwd,{recursive:true});const pm=opts.packageManager??detectPackageManager2();const config2={engine:"phaser",framework:"vanilla",language:"typescript",packageManager:pm,paths:{src:"src",assets:"public/assets"},registries:{"@main":opts.registry??DEFAULT_REGISTRY}};const resolverOpts=opts.cacheDir?{cacheDir:opts.cacheDir}:{};const resolver=createResolver(config2,resolverOpts);const fetcher=createAssetFetcher(resolverOpts);const spec=resolveTemplateSpec(opts.template);const resolved=await resolver.resolve(spec,{cwd,config:config2});if(resolved.item.type!=="registry:template"){throw new IncompatibleError(`"${spec}" is type ${resolved.item.type}, not registry:template.`)}const lock=await loadLockfile(cwd);const p2=await plan(resolved,config2,lock,cwd);const result=await executePlan(p2,lock,cwd,{force:opts.force??false},fetcher);await saveLockfile(cwd,lock);const pkgPath=resolve5(cwd,"package.json");try{const raw=await readFile6(pkgPath,"utf8");const pkg=JSON.parse(raw);pkg.name=opts.name;await writeFile5(pkgPath,JSON.stringify(pkg,null,2)+`
383
+ `,"utf8")}catch(err){if(err.code!=="ENOENT")throw err}const postScaffoldDeps=await installPostScaffoldDeps(cwd,opts.registry,opts.cacheDir,opts.force??false);const templateTags=resolved.item.tags??[];await reportScaffoldTelemetry({cwd,templateName:resolved.item.name,templateVersion:resolved.item.version,packageManager:pm,cliVersion:opts.cliVersion,disabled:opts.disableAnalytics??false});if(opts.onAfterDeps){await opts.onAfterDeps({cwd,templateTags})}let installed=false;if(!opts.skipInstall){const runner=opts.installRunner??defaultInstallRunner;await runner(cwd,pm);installed=true}return{cwd,packageManager:pm,installed,templateName:resolved.item.name,templateTags,filesWritten:result.installed.length+postScaffoldDeps.length,postScaffoldDeps:postScaffoldDeps.map((d3)=>d3.spec)}}async function reportScaffoldTelemetry(info){if(info.disabled)return;const ctx={cliVersion:info.cliVersion,packageManager:info.packageManager};let project={};try{const raw=await readFile6(resolve5(info.cwd,"gamecn.json"),"utf8");const parsed=GamecnConfigSchema.parse(JSON.parse(raw));project={engine:parsed.engine,framework:parsed.framework,language:parsed.language}}catch{}await Promise.allSettled([trackTemplateInstall({id:info.templateName,version:info.templateVersion},ctx),trackProjectInit({flow:"scaffold",template:info.templateName,...project},ctx)])}async function installPostScaffoldDeps(cwd,registryOverride,cacheDir,force){const cfgPath=resolve5(cwd,"gamecn.json");let raw;try{raw=await readFile6(cfgPath,"utf8")}catch(err){if(err.code==="ENOENT")return[];throw err}const projectConfig=GamecnConfigSchema.parse(JSON.parse(raw));const deps=projectConfig.deps??[];if(deps.length===0)return[];const config2=registryOverride?{...projectConfig,registries:{...projectConfig.registries,"@main":registryOverride}}:projectConfig;const resolverOpts=cacheDir?{cacheDir}:{};const resolver=createResolver(config2,resolverOpts);const fetcher=createAssetFetcher(resolverOpts);const lock=await loadLockfile(cwd);const installed=[];for(const spec of deps){const resolvedDep=await resolver.resolve(spec,{cwd,config:config2});if(resolvedDep.item.type==="registry:template"){throw new IncompatibleError(`Template's gamecn.json#deps entry "${spec}" resolved to a registry:template — deps[] is for components/systems/assets, not templates.`)}const p2=await plan(resolvedDep,config2,lock,cwd);const r2=await executePlan(p2,lock,cwd,{force},fetcher);installed.push({spec,fileCount:r2.installed.length})}await saveLockfile(cwd,lock);return installed}var CLI_VERSION="0.0.4-1";var TEMPLATES=[{value:"phaser-endless-runner",label:"Phaser Endless Runner",hint:"Vite + TypeScript + Phaser 3"}];async function pickTemplate(opts){if(opts.template)return opts.template;if(opts.yes){throw new Error("--template is required when --yes is set.")}const choice=await ve({message:"Template?",options:TEMPLATES});if(pD(choice)){xe("Cancelled.");process.exit(1)}return choice}function parseSkillsFlag(value){if(!value)return;return value.split(",").map((s)=>s.trim()).filter((s)=>s.length>0)}async function readProjectContext(cwd){try{const raw=await readFile7(resolvePath(cwd,"gamecn.json"),"utf8");const parsed=GamecnConfigSchema.parse(JSON.parse(raw));return{engine:parsed.engine,framework:parsed.framework,language:parsed.language}}catch{return{engine:"vanilla",framework:"vanilla",language:"typescript"}}}var program2=new Command;program2.name("create-gamecn").description("Scaffold a new gamecn project from a template").version("0.0.1").argument("<name>","project directory name").option("--template <name>","template name or full spec").option("--registry <url>","registry URL with {name} placeholder",DEFAULT_REGISTRY).option("--package-manager <pm>","package manager (npm|pnpm|yarn|bun); defaults to the one that invoked this command").option("--yes","skip prompts; installs always-recommended skills").option("--force","overwrite a non-empty target directory").option("--no-install","skip dependency install").option("--skills <list>","comma-separated skill names to install; `--no-skills` to skip entirely").option("--no-skills","skip the skills install step entirely").option("--no-analytics","disable anonymous telemetry for this run").action(async(name,opts)=>{try{if(!opts.yes)Ie(import_picocolors4.default.cyan(`create-gamecn ${name}`));const template=await pickTemplate(opts);const pm=opts.packageManager??detectPackageManager2();const preselected=typeof opts.skills==="string"?parseSkillsFlag(opts.skills):undefined;const scaffoldOpts={name,template,registry:opts.registry,packageManager:pm,skipInstall:opts.install===false,force:opts.force,cliVersion:CLI_VERSION,disableAnalytics:opts.analytics===false,onAfterDeps:async({cwd,templateTags})=>{const projCtx=await readProjectContext(cwd);const ctx={...projCtx,templateTags,flow:"scaffold"};const promptResult=await promptSkillsInstall(ctx,{yes:!!opts.yes,skip:opts.skills===false,preselected,cwd});renderUnknownPreselected(promptResult.unknownPreselected);if(promptResult.recs.length===0)return;const installResult=await installSkills(promptResult.recs,{cwd});renderSkillsSummary(installResult)}};const result=await scaffold(scaffoldOpts);const next=result.installed?`cd ${name}
384
384
  ${result.packageManager} dev`:`cd ${name}
385
385
  ${result.packageManager} install
386
386
  ${result.packageManager} dev`;if(!opts.yes){Se(import_picocolors4.default.green(`Done!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-gamecn",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "bin": {