@stacksjs/buddy 0.74.61 → 0.74.63

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.
@@ -125,17 +125,31 @@ if [ -n "$DOMAIN" ] && [ -f "$ENVF" ]; then
125
125
  # the obvious way to write this and the wrong one here: the separator would
126
126
  # have to be a newline written as \\n inside a JS template literal, and the
127
127
  # domain would become a regex whose dots match anything.
128
- OTHERS=""
128
+ #
129
+ # The entry is replaced where it stands, and appended only when absent. The
130
+ # list used to be rebuilt as "everyone else, then this domain", which moved
131
+ # the domain to the end. On a shared server only one tenant can be last, so
132
+ # every other tenant's deploy reordered the line, saw a change that was not
133
+ # one, and restarted mail for every domain on the box.
134
+ NEWEX=""
135
+ PLACED=0
129
136
  OLDIFS="$IFS"
130
137
  IFS=','
131
138
  for entry in $ex; do
132
139
  case "$entry" in
133
- "$DOMAIN:"*|"") : ;;
134
- *) OTHERS="\${OTHERS:+$OTHERS,}$entry" ;;
140
+ "") : ;;
141
+ "$DOMAIN:"*)
142
+ if [ "$PLACED" = 0 ]; then
143
+ NEWEX="\${NEWEX:+$NEWEX,}$ENTRY"
144
+ PLACED=1
145
+ fi ;;
146
+ *) NEWEX="\${NEWEX:+$NEWEX,}$entry" ;;
135
147
  esac
136
148
  done
137
149
  IFS="$OLDIFS"
138
- NEWEX="\${OTHERS:+$OTHERS,}$ENTRY"
150
+ if [ "$PLACED" = 0 ]; then
151
+ NEWEX="\${NEWEX:+$NEWEX,}$ENTRY"
152
+ fi
139
153
  if [ "$NEWEX" != "$ex" ]; then
140
154
  if grep -qE '^DKIM_EXTRA_KEYS=' "$ENVF"; then
141
155
  sed -i "s|^DKIM_EXTRA_KEYS=.*|DKIM_EXTRA_KEYS=$NEWEX|" "$ENVF"
@@ -1,10 +1,10 @@
1
- import{existsSync,mkdirSync,realpathSync}from"node:fs";import{cp,readdir,stat}from"node:fs/promises";import{homedir}from"node:os";import{dirname,join,resolve}from"node:path";import process from"node:process";import{italic,log,onUnknownSubcommand}from"@stacksjs/cli";import{path}from"@stacksjs/path";import{fs,globSync}from"@stacksjs/storage";import{pruneVendoredCoreFromWorkflows,splitFrameworkTypecheckScript}from"../workflow-prune";import{detectInstaller,findCoreReferences,isDanglingLink,rewriteCoreCommandPaths,rewriteCoreSourceImports,rewriteSurvivingFrameworkManifests}from"../unvendor-rewrite";import{fetchPublishedVersions}from"../registry";import{ExitCode}from"@stacksjs/types";export function publish(buddy){const descriptions={command:"Publish a Stacks default into your userland (app/) directory so you can customize it",model:"Publish a default model from storage/framework/defaults/app/Models/ to app/Models/",controller:"Publish a default controller from storage/framework/defaults/app/Controllers/ to app/Controllers/",middleware:"Publish a default middleware from storage/framework/defaults/app/Middleware/ to app/Middleware/",action:"Publish a default action from storage/framework/defaults/app/Actions/ to app/Actions/",core:"Publish a framework package source from node_modules/@stacksjs/<pkg>/ into storage/framework/core/<pkg>/ for editing",unpublishCore:"Drop a vendored storage/framework/core/<pkg>/ and go back to the installed @stacksjs/<pkg>",all:"Unvendor the whole framework: remove storage/framework/core and resolve every @stacksjs package from the `stacks` dependency in package.json",publishAll:"Vendor the whole framework: copy storage/framework/core out of a local Stacks checkout and wire it up as a Bun workspace",frameworkPath:"The Stacks checkout to vendor from (defaults to $STACKS_FRAMEWORK_PATH, ../stacks, then ~/Code/stacks)",coreStatus:"Report whether this project runs on a vendored storage/framework/core or on the published packages",name:"The name of the resource to publish (e.g. Cart, User)",pkg:"The name of the framework package (e.g. router, orm, faker - without @stacksjs/ prefix)",force:"Overwrite an existing userland file",forceUnpublish:"Delete the vendored source even when it has uncommitted changes",verbose:"Enable verbose output"};buddy.command("publish:model <name>",descriptions.model).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"model",name,defaultsDir:path.frameworkPath("defaults/app/Models"),userDir:path.userModelsPath(),force:!!options.force})});buddy.command("publish:controller <name>",descriptions.controller).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"controller",name,defaultsDir:path.frameworkPath("defaults/app/Controllers"),userDir:path.userControllersPath(),force:!!options.force})});buddy.command("publish:middleware <name>",descriptions.middleware).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"middleware",name,defaultsDir:path.frameworkPath("defaults/app/Middleware"),userDir:path.userMiddlewarePath(),force:!!options.force})});buddy.command("publish:action <name>",descriptions.action).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"action",name,defaultsDir:path.frameworkPath("defaults/app/Actions"),userDir:path.userActionsPath(),force:!!options.force})});buddy.command("publish:core [pkg]",descriptions.core).option("--all",descriptions.publishAll,{default:!1}).option("--path <path>",descriptions.frameworkPath).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy publish:core router").example("buddy publish:core --all").example("buddy publish:core --all --path ../stacks").action(async(pkg,options)=>{if(options.all){await vendorFramework(options.path,!!options.force);return}if(!pkg){await log.error("Usage: buddy publish:core <pkg> (or --all to vendor the whole framework as a workspace)");process.exit(ExitCode.FatalError)}await publishCorePackage(pkg,!!options.force)});buddy.command("core:status",descriptions.coreStatus).alias("publish:core:status").option("--verbose",descriptions.verbose,{default:!1}).example("buddy core:status").action(async()=>{await reportCoreStatus()});buddy.command("unpublish:core [pkg]",descriptions.unpublishCore).option("--all",descriptions.all,{default:!1}).option("--force",descriptions.forceUnpublish,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(pkg,options)=>{if(options.all){await unvendorFramework(!!options.force);return}if(!pkg){await log.error("Usage: buddy unpublish:core <pkg> (or --all to move the whole framework to node_modules)");process.exit(ExitCode.FatalError)}await unpublishCorePackage(pkg,!!options.force)});buddy.command("publish [resource] [name]",descriptions.command).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(resource,name,options)=>{if(!resource||!name){await log.error("Usage: buddy publish:<resource> <Name> (e.g. buddy publish:model Cart)");process.exit(ExitCode.FatalError)}const handler={model:()=>publishResource({kind:"model",name,defaultsDir:path.frameworkPath("defaults/app/Models"),userDir:path.userModelsPath(),force:!!options.force}),controller:()=>publishResource({kind:"controller",name,defaultsDir:path.frameworkPath("defaults/app/Controllers"),userDir:path.userControllersPath(),force:!!options.force}),middleware:()=>publishResource({kind:"middleware",name,defaultsDir:path.frameworkPath("defaults/app/Middleware"),userDir:path.userMiddlewarePath(),force:!!options.force}),action:()=>publishResource({kind:"action",name,defaultsDir:path.frameworkPath("defaults/app/Actions"),userDir:path.userActionsPath(),force:!!options.force}),core:()=>publishCorePackage(name,!!options.force)}[resource.toLowerCase()];if(!handler){await log.error(`Unknown publishable resource: ${italic(resource)}`);log.info("Available: model, controller, middleware, action, core");await log.flush();process.exit(ExitCode.FatalError)}await handler()});onUnknownSubcommand(buddy,"publish")}async function publishCorePackage(pkg,force){const shortName=pkg.replace(/^@stacksjs\//,"").replace(/^core\//,""),fail=(msg,hint)=>{process.stderr.write(`${msg}
1
+ import{existsSync,mkdirSync,realpathSync}from"node:fs";import{cp,readdir,stat}from"node:fs/promises";import{homedir}from"node:os";import{dirname,join,resolve}from"node:path";import process from"node:process";import{italic,log,onUnknownSubcommand}from"@stacksjs/cli";import{path}from"@stacksjs/path";import{fs,globSync}from"@stacksjs/storage";import{pruneVendoredCoreFromWorkflows,splitFrameworkTypecheckScript}from"../workflow-prune";import{detectInstaller,findCoreReferences,INSTALL_RETRY_DELAYS_MS,installWithRetry,isDanglingLink,rewriteCoreCommandPaths,rewriteCoreSourceImports,rewriteSurvivingFrameworkManifests}from"../unvendor-rewrite";import{fetchPublishedVersions}from"../registry";import{ExitCode}from"@stacksjs/types";export function publish(buddy){const descriptions={command:"Publish a Stacks default into your userland (app/) directory so you can customize it",model:"Publish a default model from storage/framework/defaults/app/Models/ to app/Models/",controller:"Publish a default controller from storage/framework/defaults/app/Controllers/ to app/Controllers/",middleware:"Publish a default middleware from storage/framework/defaults/app/Middleware/ to app/Middleware/",action:"Publish a default action from storage/framework/defaults/app/Actions/ to app/Actions/",core:"Publish a framework package source from node_modules/@stacksjs/<pkg>/ into storage/framework/core/<pkg>/ for editing",unpublishCore:"Drop a vendored storage/framework/core/<pkg>/ and go back to the installed @stacksjs/<pkg>",all:"Unvendor the whole framework: remove storage/framework/core and resolve every @stacksjs package from the `stacks` dependency in package.json",publishAll:"Vendor the whole framework: copy storage/framework/core out of a local Stacks checkout and wire it up as a Bun workspace",frameworkPath:"The Stacks checkout to vendor from (defaults to $STACKS_FRAMEWORK_PATH, ../stacks, then ~/Code/stacks)",coreStatus:"Report whether this project runs on a vendored storage/framework/core or on the published packages",name:"The name of the resource to publish (e.g. Cart, User)",pkg:"The name of the framework package (e.g. router, orm, faker - without @stacksjs/ prefix)",force:"Overwrite an existing userland file",forceUnpublish:"Delete the vendored source even when it has uncommitted changes",verbose:"Enable verbose output"};buddy.command("publish:model <name>",descriptions.model).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"model",name,defaultsDir:path.frameworkPath("defaults/app/Models"),userDir:path.userModelsPath(),force:!!options.force})});buddy.command("publish:controller <name>",descriptions.controller).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"controller",name,defaultsDir:path.frameworkPath("defaults/app/Controllers"),userDir:path.userControllersPath(),force:!!options.force})});buddy.command("publish:middleware <name>",descriptions.middleware).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"middleware",name,defaultsDir:path.frameworkPath("defaults/app/Middleware"),userDir:path.userMiddlewarePath(),force:!!options.force})});buddy.command("publish:action <name>",descriptions.action).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"action",name,defaultsDir:path.frameworkPath("defaults/app/Actions"),userDir:path.userActionsPath(),force:!!options.force})});buddy.command("publish:core [pkg]",descriptions.core).option("--all",descriptions.publishAll,{default:!1}).option("--path <path>",descriptions.frameworkPath).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy publish:core router").example("buddy publish:core --all").example("buddy publish:core --all --path ../stacks").action(async(pkg,options)=>{if(options.all){await vendorFramework(options.path,!!options.force);return}if(!pkg){await log.error("Usage: buddy publish:core <pkg> (or --all to vendor the whole framework as a workspace)");process.exit(ExitCode.FatalError)}await publishCorePackage(pkg,!!options.force)});buddy.command("core:status",descriptions.coreStatus).alias("publish:core:status").option("--verbose",descriptions.verbose,{default:!1}).example("buddy core:status").action(async()=>{await reportCoreStatus()});buddy.command("unpublish:core [pkg]",descriptions.unpublishCore).option("--all",descriptions.all,{default:!1}).option("--force",descriptions.forceUnpublish,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(pkg,options)=>{if(options.all){await unvendorFramework(!!options.force);return}if(!pkg){await log.error("Usage: buddy unpublish:core <pkg> (or --all to move the whole framework to node_modules)");process.exit(ExitCode.FatalError)}await unpublishCorePackage(pkg,!!options.force)});buddy.command("publish [resource] [name]",descriptions.command).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(resource,name,options)=>{if(!resource||!name){await log.error("Usage: buddy publish:<resource> <Name> (e.g. buddy publish:model Cart)");process.exit(ExitCode.FatalError)}const handler={model:()=>publishResource({kind:"model",name,defaultsDir:path.frameworkPath("defaults/app/Models"),userDir:path.userModelsPath(),force:!!options.force}),controller:()=>publishResource({kind:"controller",name,defaultsDir:path.frameworkPath("defaults/app/Controllers"),userDir:path.userControllersPath(),force:!!options.force}),middleware:()=>publishResource({kind:"middleware",name,defaultsDir:path.frameworkPath("defaults/app/Middleware"),userDir:path.userMiddlewarePath(),force:!!options.force}),action:()=>publishResource({kind:"action",name,defaultsDir:path.frameworkPath("defaults/app/Actions"),userDir:path.userActionsPath(),force:!!options.force}),core:()=>publishCorePackage(name,!!options.force)}[resource.toLowerCase()];if(!handler){await log.error(`Unknown publishable resource: ${italic(resource)}`);log.info("Available: model, controller, middleware, action, core");await log.flush();process.exit(ExitCode.FatalError)}await handler()});onUnknownSubcommand(buddy,"publish")}async function publishCorePackage(pkg,force){const shortName=pkg.replace(/^@stacksjs\//,"").replace(/^core\//,""),fail=(msg,hint)=>{process.stderr.write(`${msg}
2
2
  `);if(hint)process.stderr.write(` ${hint}
3
3
  `);process.exit(ExitCode.FatalError)};if(!shortName||shortName.includes("/")||shortName.includes(".."))fail(`Invalid package name: ${pkg}`,"Use a short name like `router` or the fully qualified `@stacksjs/router`.");const sourceDir=resolve(process.cwd(),"node_modules","@stacksjs",shortName),targetDir=path.frameworkPath(`core/${shortName}`);try{if(!(await stat(sourceDir)).isDirectory())fail(`${sourceDir} is not a directory.`)}catch{fail(`Could not find @stacksjs/${shortName} in node_modules.`,"Run `bun install` first, or check the package name.")}if(existsSync(targetDir)&&!force)fail(`Already published: ${targetDir.replace(`${process.cwd()}/`,"")}`,"Pass --force to overwrite.");mkdirSync(dirname(targetDir),{recursive:!0});const SKIP=new Set(["node_modules","dist",".bun",".cache","tsconfig.tsbuildinfo"]);let copied=0;await copyTreeFiltered(sourceDir,targetDir,SKIP,()=>copied++);log.success(`Published @stacksjs/${shortName} \u2192 ${italic(targetDir.replace(`${process.cwd()}/`,""))} (${copied} files)`);log.info("Edit freely - local changes win over the installed package.")}async function copyTreeFiltered(sourceDir,targetDir,skip,onFile){mkdirSync(targetDir,{recursive:!0});const entries=await readdir(sourceDir,{withFileTypes:!0});for(const entry of entries){if(skip.has(entry.name))continue;const src=`${sourceDir}/${entry.name}`,dst=`${targetDir}/${entry.name}`;if(entry.isDirectory()){await copyTreeFiltered(src,dst,skip,onFile);continue}await cp(src,dst,{force:!0,dereference:!0});onFile()}}async function publishResource(ctx){const{kind,name,defaultsDir,userDir,force}=ctx,fileName=name.endsWith(".ts")?name:`${name}.ts`,matches=globSync([`${defaultsDir}/**/${fileName}`],{absolute:!0});if(!matches.length){await log.error(`Could not find default ${kind}: ${italic(fileName)}`);log.info(`Looked under: ${italic(defaultsDir)}`);await log.flush();process.exit(ExitCode.FatalError)}if(matches.length>1){log.warn(`Multiple defaults match ${italic(fileName)}; using the first:`);for(const m of matches)log.info(` ${m}`)}const sourcePath=matches[0];if(!sourcePath)throw Error(`Could not resolve default ${kind}: ${fileName}`);const targetPath=`${userDir.replace(/\/$/,"")}/${fileName}`;if(existsSync(targetPath)&&!force){await log.error(`Already exists: ${italic(targetPath)}`);log.info("Pass --force to overwrite.");await log.flush();process.exit(ExitCode.FatalError)}mkdirSync(dirname(targetPath),{recursive:!0});await fs.promises.copyFile(sourcePath,targetPath);const carried=await carryRelativeImports(sourcePath,targetPath);log.success(`Published ${kind} ${italic(name)} \u2192 ${italic(targetPath.replace(`${process.cwd()}/`,""))}`);for(const file of carried)log.info(` + ${italic(file.replace(`${process.cwd()}/`,""))} (imported by it)`)}export async function carryRelativeImports(sourcePath,targetPath,seen=new Set){if(seen.has(sourcePath))return[];seen.add(sourcePath);const source=await fs.promises.readFile(sourcePath,"utf-8"),written=[],root=realpathSync(process.cwd()),targetDir=realpathSync(dirname(targetPath)),specifiers=[...source.matchAll(/from\s+['"](\.[^'"]+)['"]/g)].map((match)=>match[1]);for(const specifier of new Set(specifiers)){if(!specifier)continue;const candidates=specifier.endsWith(".ts")?[specifier]:[`${specifier}.ts`,`${specifier}/index.ts`];for(const candidate of candidates){const from=resolve(dirname(sourcePath),candidate),to=resolve(targetDir,candidate);if(!existsSync(from))continue;if(!to.startsWith(`${root}/`))break;if(!existsSync(to)){mkdirSync(dirname(to),{recursive:!0});await fs.promises.copyFile(from,to);written.push(to)}written.push(...await carryRelativeImports(from,to,seen));break}}return written}async function vendorFramework(explicitPath,force){const coreDir=path.frameworkPath("core"),rel=(p)=>p.replace(`${process.cwd()}/`,"");if(existsSync(coreDir)&&!force){log.info(`Already vendored: ${italic(rel(coreDir))}`);log.info("Pass --force to replace it with a fresh copy of the checkout.");return}const framework=resolveFrameworkCheckout(explicitPath);if(!framework){await log.error("No Stacks checkout found to vendor from.");log.info("Pass one with `--path <dir>`, or set STACKS_FRAMEWORK_PATH.");log.info("A checkout is required: the published packages ship `dist` only, so a copy of them would not be editable.");await log.flush();process.exit(ExitCode.FatalError)}const sourceCore=join(framework,"storage/framework/core"),corePkgPath=resolve(sourceCore,"package.json");if(!existsSync(corePkgPath)){await log.error(`${sourceCore} has no package.json - that does not look like a Stacks checkout.`);process.exit(ExitCode.FatalError)}const depName=JSON.parse(await fs.promises.readFile(corePkgPath,"utf-8")).name??"stacks";log.info(`Vendoring the framework from ${italic(framework)}...`);if(existsSync(coreDir))await fs.promises.rm(coreDir,{recursive:!0,force:!0});const SKIP=new Set(["node_modules",".bun",".cache","tsconfig.tsbuildinfo"]);let copied=0;await copyTreeFiltered(sourceCore,coreDir,SKIP,()=>copied++);const rootPkgPath=resolve(process.cwd(),"package.json"),rootPkg=JSON.parse(await fs.promises.readFile(rootPkgPath,"utf-8")),provided=new Set([depName]);for(const entry of await readdir(coreDir,{withFileTypes:!0})){if(!entry.isDirectory())continue;if(existsSync(resolve(coreDir,entry.name,"package.json")))provided.add(`@stacksjs/${entry.name}`)}let repointed=0;for(const field of["dependencies","devDependencies"]){const deps=rootPkg[field];if(!deps)continue;for(const name of Object.keys(deps)){if(!provided.has(name)||deps[name].startsWith("workspace:"))continue;deps[name]="workspace:*";repointed++}}if(!rootPkg.dependencies?.[depName]&&!rootPkg.devDependencies?.[depName])rootPkg.dependencies={...rootPkg.dependencies,[depName]:"workspace:*"};const workspaces=rootPkg.workspaces??[];let addedGlobs=0;for(const glob of["storage/framework/core","storage/framework/core/*"]){if(workspaces.some((existing)=>existing.replace(/^\.\//,"").replace(/\/$/,"")===glob))continue;workspaces.push(glob);addedGlobs++}rootPkg.workspaces=workspaces;await fs.promises.writeFile(rootPkgPath,`${JSON.stringify(rootPkg,null,2)}
4
4
  `);const bunfigPath=resolve(process.cwd(),"bunfig.toml");let rewrittenPreloads=0;if(existsSync(bunfigPath)){const bunfig=await fs.promises.readFile(bunfigPath,"utf-8"),next=bunfig.replace(/(["'])@stacksjs\/([\w-]+)\/([^"']+?)\.js\1/g,(match,quote,pkgName,subpath)=>{if(!provided.has(`@stacksjs/${pkgName}`))return match;rewrittenPreloads++;return`${quote}./storage/framework/core/${pkgName}/${subpath}.ts${quote}`});if(next!==bunfig)await fs.promises.writeFile(bunfigPath,next)}const tsconfigPath=resolve(process.cwd(),"tsconfig.json");let rewroteTsconfig=!1;if(existsSync(tsconfigPath)){const raw=await fs.promises.readFile(tsconfigPath,"utf-8"),next=raw.replace(/"extends"\s*:\s*"\.\/storage\/framework\/tsconfig\.app\.json"/,'"extends": "./storage/framework/core/tsconfig.json"');if(next!==raw){await fs.promises.writeFile(tsconfigPath,next);rewroteTsconfig=!0}}log.success(`Vendored ${copied} files into ${italic(rel(coreDir))}`);log.info(`package.json now depends on ${depName}@workspace:*`);if(repointed>0)log.info(`Repointed ${repointed} version range${repointed===1?"":"s"} to workspace:*`);if(addedGlobs>0)log.info(`Added ${addedGlobs} workspace glob${addedGlobs===1?"":"s"} for the vendored packages`);if(rewrittenPreloads>0)log.info(`Rewrote ${rewrittenPreloads} bunfig.toml preload path${rewrittenPreloads===1?"":"s"} to the vendored source`);if(rewroteTsconfig)log.info("tsconfig.json now extends storage/framework/core/tsconfig.json");log.info("Linking the workspace...");if(await Bun.spawn(["bun","install"],{cwd:process.cwd(),stdout:"inherit",stderr:"inherit"}).exited!==0){await log.error("`bun install` failed. The files are in place; re-run the install once the failure is resolved.");process.exit(ExitCode.FatalError)}log.success("This project now runs on the vendored framework - edits under storage/framework/core are live.");log.info("Go back to the published packages any time with `buddy unpublish:core --all`.")}function resolveFrameworkCheckout(explicit){const candidates=[explicit,process.env.STACKS_FRAMEWORK_PATH,resolve(process.cwd(),"../stacks"),join(homedir(),"Code/stacks")].filter(Boolean);for(const candidate of candidates){const full=resolve(candidate);if(full===resolve(process.cwd()))continue;if(existsSync(join(full,"storage/framework/core/package.json")))return full}return null}async function reportCoreStatus(){const coreDir=path.frameworkPath("core"),rel=(p)=>p.replace(`${process.cwd()}/`,""),vendored=existsSync(resolve(coreDir,"package.json")),rootPkgPath=resolve(process.cwd(),"package.json"),rootPkg=existsSync(rootPkgPath)?JSON.parse(await fs.promises.readFile(rootPkgPath,"utf-8")):{},declared=rootPkg.dependencies?.stacks??rootPkg.devDependencies?.stacks;if(vendored){const corePkg=JSON.parse(await fs.promises.readFile(resolve(coreDir,"package.json"),"utf-8")),packages=(await readdir(coreDir,{withFileTypes:!0})).filter((entry)=>entry.isDirectory()&&existsSync(resolve(coreDir,entry.name,"package.json")));log.info(`Layout: vendored - ${italic(rel(coreDir))} (${packages.length} packages, v${corePkg.version??"unknown"})`);log.info(`Declared: stacks@${declared??"(not declared)"}`);if(declared&&!declared.startsWith("workspace:"))log.warn(`The vendored copy is not linked: stacks is declared as ${declared}, not workspace:*. Run \`buddy publish:core --all --force\` to relink, or \`buddy unpublish:core --all\` to remove it.`);log.info("Move to the published packages with `buddy unpublish:core --all`.");return}const installed=resolve(process.cwd(),"node_modules/@stacksjs/buddy/package.json"),installedVersion=existsSync(installed)?JSON.parse(await fs.promises.readFile(installed,"utf-8")).version:void 0;log.info("Layout: published packages - no storage/framework/core in this project");log.info(`Declared: stacks@${declared??"(not declared)"}`);log.info(`Installed: ${installedVersion?`v${installedVersion}`:"(run bun install)"}`);log.info("Vendor the framework for local development with `buddy publish:core --all`.")}async function unpublishCorePackage(pkg,force){const shortName=normalizeCoreName(pkg),targetDir=path.frameworkPath(`core/${shortName}`),rel=(p)=>p.replace(`${process.cwd()}/`,"");if(!existsSync(targetDir)){log.info(`Not vendored: ${italic(rel(targetDir))} - nothing to do.`);return}const installed=resolve(process.cwd(),"node_modules","@stacksjs",shortName);if(!existsSync(installed)&&!force){await log.error(`@stacksjs/${shortName} is not installed, so removing the vendored copy would leave nothing to resolve.`);log.info(`Run \`bun add @stacksjs/${shortName}\` first, or pass --force to remove it anyway.`);await log.flush();process.exit(ExitCode.FatalError)}await assertNoUncommittedChanges(targetDir,force);await fs.promises.rm(targetDir,{recursive:!0,force:!0});log.success(`Unpublished ${italic(rel(targetDir))} - @stacksjs/${shortName} now resolves from node_modules.`)}export function packagesPreloadFilesFallBackTo(source){const found=new Set;for(const match of source.matchAll(/'@stacksjs\/'\s*\+\s*'([a-z0-9-]+)'/g))found.add(`@stacksjs/${match[1]}`);return found}export function rewriteBunfigPreloads(bunfig){const packages=new Set;let rewritten=0;return{next:bunfig.replace(/(["'])\.?\/?storage\/framework\/core\/([\w-]+)\/([^"']+?)\.ts\1/g,(_match,quote,pkgName,subpath)=>{rewritten++;packages.add(`@stacksjs/${pkgName}`);return`${quote}@stacksjs/${pkgName}/${subpath}.js${quote}`}),packages,rewritten}}async function unvendorFramework(force){const coreDir=path.frameworkPath("core"),rel=(p)=>p.replace(`${process.cwd()}/`,"");if(!existsSync(coreDir)){log.info("No storage/framework/core in this project - already on the installed packages.");return}const corePkgPath=resolve(coreDir,"package.json");if(!existsSync(corePkgPath)){await log.error(`${rel(coreDir)} has no package.json, so its version cannot be determined.`);log.info("Unvendor the packages individually with `buddy unpublish:core <pkg>` instead.");await log.flush();process.exit(ExitCode.FatalError)}const corePkg=JSON.parse(await fs.promises.readFile(corePkgPath,"utf-8")),version=corePkg.version;if(!version){await log.error(`${rel(corePkgPath)} has no version field.`);process.exit(ExitCode.FatalError)}await assertNoUncommittedChanges(coreDir,force);const depName=corePkg.name??"stacks",range=`^${await resolvePublishedVersion(depName,version)}`,rootPkgPath=resolve(process.cwd(),"package.json"),rootPkgRaw=await fs.promises.readFile(rootPkgPath,"utf-8"),rootPkg=JSON.parse(rootPkgRaw),provided=new Set([depName,...Object.keys(corePkg.dependencies??{}).filter((name)=>name.startsWith("@stacksjs/"))]);for(const entry of await readdir(coreDir,{withFileTypes:!0}))if(entry.isDirectory())provided.add(`@stacksjs/${entry.name}`);let repointed=0;const repointWorkspaceRanges=(pkg)=>{let touched=!1;for(const field of["dependencies","devDependencies","peerDependencies"]){const deps=pkg[field];if(!deps)continue;for(const[name,spec]of Object.entries(deps)){if(!spec.startsWith("workspace:")||!provided.has(name))continue;deps[name]=range;touched=!0;repointed++}}return touched};repointWorkspaceRanges(rootPkg);if(!rootPkg.dependencies?.[depName]&&!rootPkg.devDependencies?.[depName])rootPkg.dependencies={...rootPkg.dependencies,[depName]:range};let rewrittenScripts=0;for(const[name,script]of Object.entries(rootPkg.scripts??{})){if(!/storage\/framework\/core\/buddy\/src\/cli\.ts/.test(script))continue;rootPkg.scripts[name]=script.replace(/\bbunx?\s+(?:--bun\s+)?\.?\/?storage\/framework\/core\/buddy\/src\/cli\.ts/g,"./buddy");rewrittenScripts++}if(Array.isArray(rootPkg.workspaces)){rootPkg.workspaces=rootPkg.workspaces.filter((glob)=>!isCoreWorkspaceGlob(glob));if(rootPkg.workspaces.length===0)delete rootPkg.workspaces}await fs.promises.writeFile(rootPkgPath,`${JSON.stringify(rootPkg,null,2)}
5
5
  `);for(const glob of rootPkg.workspaces??[])for(const memberPkgPath of globSync(`${glob.replace(/\/$/,"")}/package.json`,{cwd:process.cwd(),absolute:!0})){const raw=await fs.promises.readFile(memberPkgPath,"utf-8"),memberPkg=JSON.parse(raw);if(repointWorkspaceRanges(memberPkg))await fs.promises.writeFile(memberPkgPath,`${JSON.stringify(memberPkg,null,2)}
6
6
  `)}const survivingManifests=await rewriteSurvivingFrameworkManifests(process.cwd(),provided,range),bunfigPath=resolve(process.cwd(),"bunfig.toml");let rewrittenPreloads=0;if(existsSync(bunfigPath)){const bunfig=await fs.promises.readFile(bunfigPath,"utf-8"),{next,packages:preloadedPackages,rewritten}=rewriteBunfigPreloads(bunfig);rewrittenPreloads+=rewritten;if(next!==bunfig)await fs.promises.writeFile(bunfigPath,next);for(const match of next.matchAll(/["'](\.\/[^"']+\.ts)["']/g)){const preloadPath=resolve(process.cwd(),match[1]);if(!existsSync(preloadPath))continue;for(const pkgName of packagesPreloadFilesFallBackTo(await fs.promises.readFile(preloadPath,"utf-8")))preloadedPackages.add(pkgName)}let declaredForPreload=0;for(const pkgName of preloadedPackages){if(rootPkg.dependencies?.[pkgName]||rootPkg.devDependencies?.[pkgName])continue;rootPkg.dependencies={...rootPkg.dependencies,[pkgName]:range};declaredForPreload++}if(declaredForPreload>0)await fs.promises.writeFile(rootPkgPath,`${JSON.stringify(rootPkg,null,2)}
7
7
  `)}const tsconfigPath=resolve(process.cwd(),"tsconfig.json");let rewroteTsconfig=!1,rewroteTypecheck=!1;if(existsSync(tsconfigPath)){const raw=await fs.promises.readFile(tsconfigPath,"utf-8"),next=raw.replace(/"extends"\s*:\s*"\.\/storage\/framework\/core\/tsconfig\.json"/,'"extends": "./storage/framework/tsconfig.app.json"');if(next!==raw){await fs.promises.writeFile(tsconfigPath,next);rewroteTsconfig=!0}}const splitTypecheck=splitFrameworkTypecheckScript(rootPkg.scripts??{});if(splitTypecheck){rootPkg.scripts=splitTypecheck;rewroteTypecheck=!0;await fs.promises.writeFile(rootPkgPath,`${JSON.stringify(rootPkg,null,2)}
8
- `)}const rewrittenCommands=await rewriteCoreCommandPaths(process.cwd()),prunedWorkflows=await pruneVendoredCoreFromWorkflows(process.cwd()),rewrittenImports=await rewriteCoreSourceImports(process.cwd());await fs.promises.rm(coreDir,{recursive:!0,force:!0});const pantryLock=resolve(process.cwd(),"pantry.lock"),removedPantryLock=existsSync(pantryLock);await fs.promises.rm(pantryLock,{force:!0});let danglingRemoved=0;for(const depsDir of["node_modules","pantry"]){const scopedDir=resolve(process.cwd(),depsDir,"@stacksjs");if(!existsSync(scopedDir))continue;for(const entry of await readdir(scopedDir,{withFileTypes:!0})){if(!entry.isSymbolicLink())continue;const link=resolve(scopedDir,entry.name);if(existsSync(link))continue;await fs.promises.rm(link,{force:!0});danglingRemoved++}const rootLink=resolve(process.cwd(),depsDir,depName);if(existsSync(dirname(rootLink))&&isDanglingLink(rootLink)){await fs.promises.rm(rootLink,{force:!0});danglingRemoved++}}log.success(`Removed ${italic(rel(coreDir))}`);log.info(`package.json now depends on ${depName}@${range}`);if(repointed>0)log.info(`Repointed ${repointed} workspace: range${repointed===1?"":"s"} to ${range}`);if(survivingManifests.ranges>0)log.info(`Repointed ${survivingManifests.ranges} workspace: range${survivingManifests.ranges===1?"":"s"} across ${survivingManifests.files.length} surviving framework manifest${survivingManifests.files.length===1?"":"s"}`);if(removedPantryLock)log.info("Removed the legacy Pantry workspace lock; the install will resolve a package-layout graph");if(danglingRemoved>0)log.info(`Removed ${danglingRemoved} node_modules symlink${danglingRemoved===1?"":"s"} left pointing into it`);if(rewrittenScripts>0)log.info(`Repointed ${rewrittenScripts} package.json script${rewrittenScripts===1?"":"s"} to ./buddy`);if(rewrittenPreloads>0)log.info(`Rewrote ${rewrittenPreloads} bunfig.toml preload path${rewrittenPreloads===1?"":"s"} to package specifiers`);if(rewroteTsconfig)log.info("tsconfig.json now extends storage/framework/tsconfig.app.json");if(rewroteTypecheck)log.info("`typecheck` now checks this app as well as the framework files it still ships");for(const pruned of prunedWorkflows){const parts=[pruned.removedJobs.length>0?`${pruned.removedJobs.length} job${pruned.removedJobs.length===1?"":"s"} (${pruned.removedJobs.join(", ")})`:"",pruned.removedSteps>0?`${pruned.removedSteps} step${pruned.removedSteps===1?"":"s"}`:""].filter(Boolean);log.info(`${pruned.file}: removed ${parts.join(" and ")} that ran against the vendored core`)}if(rewrittenCommands.length>0){log.info(`Repointed vendored-CLI commands to ./buddy in ${rewrittenCommands.length} file${rewrittenCommands.length===1?"":"s"}:`);for(const file of rewrittenCommands)log.info(` ${file}`)}if(rewrittenImports.length>0){log.info(`Repointed vendored-source imports to package specifiers in ${rewrittenImports.length} file${rewrittenImports.length===1?"":"s"}:`);for(const file of rewrittenImports)log.info(` ${file}`)}const stragglers=await findCoreReferences(process.cwd());if(stragglers.length>0){log.warn(`${stragglers.length} file${stragglers.length===1?"":"s"} still reference storage/framework/core, which no longer exists:`);for(const{file,line,text}of stragglers)log.warn(` ${file}:${line} ${text}`);log.info("Run those through `./buddy <command>` or a package specifier before deploying.")}const installer=detectInstaller(process.cwd());log.info(`Installing the published packages with \`${installer.join(" ")}\`...`);if(await Bun.spawn(installer,{cwd:process.cwd(),stdout:"inherit",stderr:"inherit"}).exited!==0){await log.error(`\`${installer.join(" ")}\` failed. package.json and bunfig.toml were updated; re-run the install once the failure is resolved.`);process.exit(ExitCode.FatalError)}log.success("This project now runs on the published Stacks packages.");log.info("Vendor an individual package again any time with `buddy publish:core <pkg>`.")}async function resolvePublishedVersion(depName,vendored){let published;try{published=await fetchPublishedVersions(depName)}catch(error){log.warn(`Could not reach the npm registry to check ${depName} versions (${error instanceof Error?error.message:String(error)}).`);log.info(`Pinning the vendored version, ${depName}@^${vendored}.`);return vendored}if(published.versions.has(vendored))return vendored;if(!published.latest){log.warn(`${depName}@${vendored} is not published and the registry reports no latest version.`);return vendored}log.warn(`${depName}@${vendored} is not published yet - the vendored copy is ahead of npm.`);log.info(`Pinning the newest published version instead, ${depName}@^${published.latest}.`);return published.latest}function normalizeCoreName(pkg){const shortName=pkg.replace(/^@stacksjs\//,"").replace(/^core\//,"");if(!shortName||shortName.includes("/")||shortName.includes("..")){process.stderr.write(`Invalid package name: ${pkg}
8
+ `)}const rewrittenCommands=await rewriteCoreCommandPaths(process.cwd()),prunedWorkflows=await pruneVendoredCoreFromWorkflows(process.cwd()),rewrittenImports=await rewriteCoreSourceImports(process.cwd());await fs.promises.rm(coreDir,{recursive:!0,force:!0});const pantryLock=resolve(process.cwd(),"pantry.lock"),removedPantryLock=existsSync(pantryLock);await fs.promises.rm(pantryLock,{force:!0});let danglingRemoved=0;for(const depsDir of["node_modules","pantry"]){const scopedDir=resolve(process.cwd(),depsDir,"@stacksjs");if(!existsSync(scopedDir))continue;for(const entry of await readdir(scopedDir,{withFileTypes:!0})){if(!entry.isSymbolicLink())continue;const link=resolve(scopedDir,entry.name);if(existsSync(link))continue;await fs.promises.rm(link,{force:!0});danglingRemoved++}const rootLink=resolve(process.cwd(),depsDir,depName);if(existsSync(dirname(rootLink))&&isDanglingLink(rootLink)){await fs.promises.rm(rootLink,{force:!0});danglingRemoved++}}log.success(`Removed ${italic(rel(coreDir))}`);log.info(`package.json now depends on ${depName}@${range}`);if(repointed>0)log.info(`Repointed ${repointed} workspace: range${repointed===1?"":"s"} to ${range}`);if(survivingManifests.ranges>0)log.info(`Repointed ${survivingManifests.ranges} workspace: range${survivingManifests.ranges===1?"":"s"} across ${survivingManifests.files.length} surviving framework manifest${survivingManifests.files.length===1?"":"s"}`);if(removedPantryLock)log.info("Removed the legacy Pantry workspace lock; the install will resolve a package-layout graph");if(danglingRemoved>0)log.info(`Removed ${danglingRemoved} node_modules symlink${danglingRemoved===1?"":"s"} left pointing into it`);if(rewrittenScripts>0)log.info(`Repointed ${rewrittenScripts} package.json script${rewrittenScripts===1?"":"s"} to ./buddy`);if(rewrittenPreloads>0)log.info(`Rewrote ${rewrittenPreloads} bunfig.toml preload path${rewrittenPreloads===1?"":"s"} to package specifiers`);if(rewroteTsconfig)log.info("tsconfig.json now extends storage/framework/tsconfig.app.json");if(rewroteTypecheck)log.info("`typecheck` now checks this app as well as the framework files it still ships");for(const pruned of prunedWorkflows){const parts=[pruned.removedJobs.length>0?`${pruned.removedJobs.length} job${pruned.removedJobs.length===1?"":"s"} (${pruned.removedJobs.join(", ")})`:"",pruned.removedSteps>0?`${pruned.removedSteps} step${pruned.removedSteps===1?"":"s"}`:""].filter(Boolean);log.info(`${pruned.file}: removed ${parts.join(" and ")} that ran against the vendored core`)}if(rewrittenCommands.length>0){log.info(`Repointed vendored-CLI commands to ./buddy in ${rewrittenCommands.length} file${rewrittenCommands.length===1?"":"s"}:`);for(const file of rewrittenCommands)log.info(` ${file}`)}if(rewrittenImports.length>0){log.info(`Repointed vendored-source imports to package specifiers in ${rewrittenImports.length} file${rewrittenImports.length===1?"":"s"}:`);for(const file of rewrittenImports)log.info(` ${file}`)}const stragglers=await findCoreReferences(process.cwd());if(stragglers.length>0){log.warn(`${stragglers.length} file${stragglers.length===1?"":"s"} still reference storage/framework/core, which no longer exists:`);for(const{file,line,text}of stragglers)log.warn(` ${file}:${line} ${text}`);log.info("Run those through `./buddy <command>` or a package specifier before deploying.")}const installer=detectInstaller(process.cwd());log.info(`Installing the published packages with \`${installer.join(" ")}\`...`);if(await installWithRetry(()=>Bun.spawn(installer,{cwd:process.cwd(),stdout:"inherit",stderr:"inherit"}).exited,{onRetry:(attempt,delayMs)=>{log.warn(`The install failed. A release published minutes ago can list versions npm cannot serve yet, so retrying in ${Math.round(delayMs/1000)}s (attempt ${attempt} of ${INSTALL_RETRY_DELAYS_MS.length+1})...`)}})!==0){await log.error(`\`${installer.join(" ")}\` failed. package.json and bunfig.toml were updated; re-run the install once the failure is resolved.`);process.exit(ExitCode.FatalError)}log.success("This project now runs on the published Stacks packages.");log.info("Vendor an individual package again any time with `buddy publish:core <pkg>`.")}async function resolvePublishedVersion(depName,vendored){let published;try{published=await fetchPublishedVersions(depName)}catch(error){log.warn(`Could not reach the npm registry to check ${depName} versions (${error instanceof Error?error.message:String(error)}).`);log.info(`Pinning the vendored version, ${depName}@^${vendored}.`);return vendored}if(published.versions.has(vendored))return vendored;if(!published.latest){log.warn(`${depName}@${vendored} is not published and the registry reports no latest version.`);return vendored}log.warn(`${depName}@${vendored} is not published yet - the vendored copy is ahead of npm.`);log.info(`Pinning the newest published version instead, ${depName}@^${published.latest}.`);return published.latest}function normalizeCoreName(pkg){const shortName=pkg.replace(/^@stacksjs\//,"").replace(/^core\//,"");if(!shortName||shortName.includes("/")||shortName.includes("..")){process.stderr.write(`Invalid package name: ${pkg}
9
9
  `);process.stderr.write(" Use a short name like `router` or the fully qualified `@stacksjs/router`.\n");process.exit(ExitCode.FatalError)}return shortName}function isCoreWorkspaceGlob(glob){const normalized=glob.replace(/^\.\//,"").replace(/\/$/,"");return normalized==="storage/framework/core"||normalized.startsWith("storage/framework/core/")}async function assertNoUncommittedChanges(dir,force){if(force)return;try{const proc=Bun.spawn(["git","status","--porcelain","--",dir],{cwd:process.cwd(),stdout:"pipe",stderr:"ignore"}),output=await new Response(proc.stdout).text();if(await proc.exited!==0)return;const changed=output.split(`
10
10
  `).filter(Boolean);if(changed.length===0)return;log.error(`${changed.length} uncommitted change${changed.length===1?"":"s"} under ${italic(dir.replace(`${process.cwd()}/`,""))}:`);for(const line of changed.slice(0,10))log.info(` ${line}`);if(changed.length>10)log.info(` ... and ${changed.length-10} more`);log.info("Commit or stash them first, or pass --force to delete them anyway.");await log.flush();process.exit(ExitCode.FatalError)}catch{}}
@@ -6,6 +6,19 @@
6
6
  */
7
7
  export declare function isDanglingLink(target: string): boolean;
8
8
  export declare function detectInstaller(cwd: string): string[];
9
+ /**
10
+ * Run an install, retrying a failure after each of `delaysMs`.
11
+ *
12
+ * Every failure is treated as retryable: a registry 404, a reset connection
13
+ * and a real resolution error look identical from an exit code, and one more
14
+ * attempt costs a few seconds against a scaffold that would otherwise need
15
+ * finishing by hand. Returns the last exit code, so a real error still fails.
16
+ */
17
+ export declare function installWithRetry(run: () => Promise<number>, options?: {
18
+ delaysMs?: number[]
19
+ sleep?: (ms: number) => Promise<void>
20
+ onRetry?: (attempt: number, delayMs: number) => void
21
+ }): Promise<number>;
9
22
  /**
10
23
  * Repoint package manifests that remain under `storage/framework` after core
11
24
  * is removed. These directories are standalone build/runtime packages rather
@@ -16,3 +29,19 @@ export declare function rewriteSurvivingFrameworkManifests(cwd: string, provided
16
29
  export declare function rewriteCoreCommandPaths(cwd: string): Promise<string[]>;
17
30
  export declare function rewriteCoreSourceImports(cwd: string): Promise<string[]>;
18
31
  export declare function findCoreReferences(cwd: string): Promise<{ file: string, line: number, text: string }[]>;
32
+ /**
33
+ * How long to keep retrying an install that failed, between attempts.
34
+ *
35
+ * A release lists its new versions in the registry metadata before every
36
+ * tarball can be fetched: `@stacksjs/cms@0.74.61` was in the packument at
37
+ * 17:29 UTC and its tarball answered 404 until 17:36, while the Releaser was
38
+ * still publishing the rest of the set. A caret range resolves
39
+ * to the newest listed version, so an app scaffolded in that window fails its
40
+ * install on a version that exists and cannot be downloaded yet, and is left
41
+ * half converted. The Releaser's own smoke install already retries for this
42
+ * reason (`Verify published framework install`, 30 attempts); this gives
43
+ * `buddy new` the same tolerance: eight minutes of waiting in all, past the
44
+ * seven that window lasted.
45
+ * @defaultValue `[15_000, 30_000, 60_000, 90_000, 120_000, 180_000]`
46
+ */
47
+ export declare const INSTALL_RETRY_DELAYS_MS: unknown[];
@@ -1,3 +1,3 @@
1
- import{existsSync,lstatSync,readFileSync}from"node:fs";import{readdir}from"node:fs/promises";import{join,resolve}from"node:path";import{log}from"@stacksjs/cli";import{fs}from"@stacksjs/storage";export function isDanglingLink(target){try{return lstatSync(target).isSymbolicLink()&&!existsSync(target)}catch{return!1}}function hoistedLinker(cwd){try{const bunfig=readFileSync(resolve(cwd,"bunfig.toml"),"utf-8");return/^\s*linker\s*=\s*["']hoisted["']/m.test(bunfig)}catch{return!1}}export function detectInstaller(cwd){const has=(rel)=>existsSync(resolve(cwd,rel)),usesPantry=has("pantry")||has("pantry.lock")&&!has("node_modules");if(usesPantry&&Bun.which("pantry")){if(hoistedLinker(cwd))return["pantry","install","--linker","hoisted"];return["pantry","install"]}if(usesPantry)log.warn("This project installs with pantry, but no `pantry` binary is on PATH \u2014 falling back to `bun install`.");return["bun","install"]}export async function rewriteSurvivingFrameworkManifests(cwd,provided,range){const framework=resolve(cwd,"storage/framework"),files=[];let ranges=0;const walk=async(dir)=>{let entries;try{entries=await readdir(dir,{withFileTypes:!0})}catch{return}for(const entry of entries){if(entry.name==="core"||entry.name==="node_modules"||entry.name==="dist")continue;const full=join(dir,entry.name);if(entry.isDirectory()){await walk(full);continue}if(!entry.isFile()||entry.name!=="package.json")continue;const raw=await fs.promises.readFile(full,"utf-8"),pkg=JSON.parse(raw);let touched=!1;for(const field of["dependencies","devDependencies","peerDependencies"]){const dependencies=pkg[field];if(!dependencies)continue;for(const[name,spec]of Object.entries(dependencies)){if(!spec.startsWith("workspace:")||!provided.has(name))continue;dependencies[name]=range;ranges++;touched=!0}}if(!touched)continue;await fs.promises.writeFile(full,`${JSON.stringify(pkg,null,2)}
1
+ import{existsSync,lstatSync,readFileSync}from"node:fs";import{readdir}from"node:fs/promises";import{join,resolve}from"node:path";import{log}from"@stacksjs/cli";import{fs}from"@stacksjs/storage";export function isDanglingLink(target){try{return lstatSync(target).isSymbolicLink()&&!existsSync(target)}catch{return!1}}function hoistedLinker(cwd){try{const bunfig=readFileSync(resolve(cwd,"bunfig.toml"),"utf-8");return/^\s*linker\s*=\s*["']hoisted["']/m.test(bunfig)}catch{return!1}}export function detectInstaller(cwd){const has=(rel)=>existsSync(resolve(cwd,rel)),usesPantry=has("pantry")||has("pantry.lock")&&!has("node_modules");if(usesPantry&&Bun.which("pantry")){if(hoistedLinker(cwd))return["pantry","install","--linker","hoisted"];return["pantry","install"]}if(usesPantry)log.warn("This project installs with pantry, but no `pantry` binary is on PATH \u2014 falling back to `bun install`.");return["bun","install"]}export const INSTALL_RETRY_DELAYS_MS=[15000,30000,60000,90000,120000,180000];export async function installWithRetry(run,options={}){const delays=options.delaysMs??INSTALL_RETRY_DELAYS_MS,sleep=options.sleep??((ms)=>Bun.sleep(ms));let code=await run();for(const[i,delay]of delays.entries()){if(code===0)break;options.onRetry?.(i+2,delay);await sleep(delay);code=await run()}return code}export async function rewriteSurvivingFrameworkManifests(cwd,provided,range){const framework=resolve(cwd,"storage/framework"),files=[];let ranges=0;const walk=async(dir)=>{let entries;try{entries=await readdir(dir,{withFileTypes:!0})}catch{return}for(const entry of entries){if(entry.name==="core"||entry.name==="node_modules"||entry.name==="dist")continue;const full=join(dir,entry.name);if(entry.isDirectory()){await walk(full);continue}if(!entry.isFile()||entry.name!=="package.json")continue;const raw=await fs.promises.readFile(full,"utf-8"),pkg=JSON.parse(raw);let touched=!1;for(const field of["dependencies","devDependencies","peerDependencies"]){const dependencies=pkg[field];if(!dependencies)continue;for(const[name,spec]of Object.entries(dependencies)){if(!spec.startsWith("workspace:")||!provided.has(name))continue;dependencies[name]=range;ranges++;touched=!0}}if(!touched)continue;await fs.promises.writeFile(full,`${JSON.stringify(pkg,null,2)}
2
2
  `);files.push(full.replace(`${cwd}/`,""))}};await walk(framework);return{files,ranges}}const SCANNED_EXTENSIONS=new Set([".ts",".tsx",".js",".mjs",".cjs",".json",".sh",".yml",".yaml",".toml"]),SKIPPED_DIRECTORIES=new Set(["node_modules","pantry","dist","storage","public","coverage","temp","tmp",".git",".cache",".claude",".idea",".stx",".vscode"]);async function*projectFiles(cwd){const walk=async function*(dir){let entries;try{entries=await readdir(dir,{withFileTypes:!0})}catch{return}for(const entry of entries){const full=join(dir,entry.name);if(entry.isDirectory()){if(SKIPPED_DIRECTORIES.has(entry.name))continue;yield*walk(full);continue}if(!entry.isFile())continue;const dot=entry.name.lastIndexOf("."),ext=dot===-1?"":entry.name.slice(dot);if(SCANNED_EXTENSIONS.has(ext)||entry.name==="Dockerfile"||entry.name.startsWith("Dockerfile."))yield full}};yield*walk(cwd)}const CORE_SOURCE_IMPORT=/(\bfrom\s+|\bimport\s*\(\s*|\brequire\s*\(\s*)(['"])(?:\.{1,2}\/)*storage\/framework\/core\/([\w.-]+)\/src\/([^'"]+?)(?:\.ts)?\2/g;function toPackageSpecifier(pkg,rest){const subpath=rest.replace(/\/index$/,"");return subpath==="index"?`@stacksjs/${pkg}`:`@stacksjs/${pkg}/${subpath}`}const CORE_ACTION_COMMANDS={"actions/src/migrate/database":"migrate","actions/src/migrate/fresh":"migrate:fresh","actions/src/database/seed":"seed","actions/src/auth/setup":"auth:setup","actions/src/dev/api":"dev:api","actions/src/key-generate":"key:generate"},CORE_COMMAND_PATH=/\bbunx?\s+(?:-{1,2}[\w=.-]+\s+)*(?:\.\/)?storage\/framework\/core\/([\w/.-]+?)\.ts\b/g;export async function rewriteCoreCommandPaths(cwd){const touched=[];for await(const file of projectFiles(cwd)){const raw=await fs.promises.readFile(file,"utf-8");if(!raw.includes("storage/framework/core/"))continue;const next=raw.replace(CORE_COMMAND_PATH,(match,entry)=>{if(entry==="buddy/src/cli")return"./buddy";const command=CORE_ACTION_COMMANDS[entry];return command?`./buddy ${command}`:match});if(next!==raw){await fs.promises.writeFile(file,next);touched.push(file.replace(`${cwd}/`,""))}}return touched}export async function rewriteCoreSourceImports(cwd){const touched=[];for await(const file of projectFiles(cwd)){const raw=await fs.promises.readFile(file,"utf-8");if(!raw.includes("storage/framework/core/"))continue;const next=raw.replace(CORE_SOURCE_IMPORT,(_match,lead,quote,pkg,rest)=>`${lead}${quote}${toPackageSpecifier(pkg,rest)}${quote}`);if(next!==raw){await fs.promises.writeFile(file,next);touched.push(file.replace(`${cwd}/`,""))}}return touched}export async function findCoreReferences(cwd){const hits=[];for await(const file of projectFiles(cwd)){const raw=await fs.promises.readFile(file,"utf-8");if(!raw.includes("storage/framework/core"))continue;raw.split(`
3
3
  `).forEach((text,index)=>{if(!text.includes("storage/framework/core"))return;const trimmed=text.trim();if(/^(?:\/\/|\*|#|<!--)/.test(trimmed))return;hits.push({file:file.replace(`${cwd}/`,""),line:index+1,text:trimmed.slice(0,120)})})}return hits}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.61",
5
+ "version": "0.74.63",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -95,66 +95,66 @@
95
95
  "prepublishOnly": "bun run build"
96
96
  },
97
97
  "dependencies": {
98
- "@stacksjs/actions": "^0.74.61",
99
- "@stacksjs/ai": "^0.74.61",
100
- "@stacksjs/alias": "^0.74.61",
101
- "@stacksjs/analytics": "^0.74.61",
102
- "@stacksjs/api": "^0.74.61",
103
- "@stacksjs/arrays": "^0.74.61",
104
- "@stacksjs/auth": "^0.74.61",
105
- "@stacksjs/browser-extension": "^0.74.61",
106
- "@stacksjs/build": "^0.74.61",
107
- "@stacksjs/cache": "^0.74.61",
108
- "@stacksjs/chat": "^0.74.61",
98
+ "@stacksjs/actions": "^0.74.63",
99
+ "@stacksjs/ai": "^0.74.63",
100
+ "@stacksjs/alias": "^0.74.63",
101
+ "@stacksjs/analytics": "^0.74.63",
102
+ "@stacksjs/api": "^0.74.63",
103
+ "@stacksjs/arrays": "^0.74.63",
104
+ "@stacksjs/auth": "^0.74.63",
105
+ "@stacksjs/browser-extension": "^0.74.63",
106
+ "@stacksjs/build": "^0.74.63",
107
+ "@stacksjs/cache": "^0.74.63",
108
+ "@stacksjs/chat": "^0.74.63",
109
109
  "@stacksjs/clapp": "^0.2.12",
110
- "@stacksjs/cli": "^0.74.61",
111
- "@stacksjs/cloud": "^0.74.61",
112
- "@stacksjs/cms": "^0.74.61",
113
- "@stacksjs/collections": "^0.74.61",
114
- "@stacksjs/config": "^0.74.61",
115
- "@stacksjs/database": "^0.74.61",
116
- "@stacksjs/desktop-build": "^0.74.61",
117
- "@stacksjs/dns": "^0.74.61",
110
+ "@stacksjs/cli": "^0.74.63",
111
+ "@stacksjs/cloud": "^0.74.63",
112
+ "@stacksjs/cms": "^0.74.63",
113
+ "@stacksjs/collections": "^0.74.63",
114
+ "@stacksjs/config": "^0.74.63",
115
+ "@stacksjs/database": "^0.74.63",
116
+ "@stacksjs/desktop-build": "^0.74.63",
117
+ "@stacksjs/dns": "^0.74.63",
118
118
  "@stacksjs/dnsx": "^0.2.3",
119
- "@stacksjs/email": "^0.74.61",
120
- "@stacksjs/enums": "^0.74.61",
121
- "@stacksjs/env": "^0.74.61",
122
- "@stacksjs/error-handling": "^0.74.61",
123
- "@stacksjs/events": "^0.74.61",
124
- "@stacksjs/features": "^0.74.61",
125
- "@stacksjs/git": "^0.74.61",
119
+ "@stacksjs/email": "^0.74.63",
120
+ "@stacksjs/enums": "^0.74.63",
121
+ "@stacksjs/env": "^0.74.63",
122
+ "@stacksjs/error-handling": "^0.74.63",
123
+ "@stacksjs/events": "^0.74.63",
124
+ "@stacksjs/features": "^0.74.63",
125
+ "@stacksjs/git": "^0.74.63",
126
126
  "@stacksjs/gitit": "^0.2.5",
127
- "@stacksjs/health": "^0.74.61",
127
+ "@stacksjs/health": "^0.74.63",
128
128
  "@stacksjs/httx": "^0.1.10",
129
- "@stacksjs/image": "^0.74.61",
130
- "@stacksjs/lint": "^0.74.61",
131
- "@stacksjs/logging": "^0.74.61",
132
- "@stacksjs/notifications": "^0.74.61",
133
- "@stacksjs/objects": "^0.74.61",
134
- "@stacksjs/orm": "^0.74.61",
135
- "@stacksjs/path": "^0.74.61",
136
- "@stacksjs/payments": "^0.74.61",
137
- "@stacksjs/realtime": "^0.74.61",
138
- "@stacksjs/router": "^0.74.61",
129
+ "@stacksjs/image": "^0.74.63",
130
+ "@stacksjs/lint": "^0.74.63",
131
+ "@stacksjs/logging": "^0.74.63",
132
+ "@stacksjs/notifications": "^0.74.63",
133
+ "@stacksjs/objects": "^0.74.63",
134
+ "@stacksjs/orm": "^0.74.63",
135
+ "@stacksjs/path": "^0.74.63",
136
+ "@stacksjs/payments": "^0.74.63",
137
+ "@stacksjs/realtime": "^0.74.63",
138
+ "@stacksjs/router": "^0.74.63",
139
139
  "@stacksjs/rpx": "^0.11.54",
140
- "@stacksjs/scheduler": "^0.74.61",
141
- "@stacksjs/search-engine": "^0.74.61",
142
- "@stacksjs/security": "^0.74.61",
143
- "@stacksjs/server": "^0.74.61",
144
- "@stacksjs/sites": "^0.74.61",
145
- "@stacksjs/skills": "^0.74.61",
146
- "@stacksjs/storage": "^0.74.61",
147
- "@stacksjs/strings": "^0.74.61",
140
+ "@stacksjs/scheduler": "^0.74.63",
141
+ "@stacksjs/search-engine": "^0.74.63",
142
+ "@stacksjs/security": "^0.74.63",
143
+ "@stacksjs/server": "^0.74.63",
144
+ "@stacksjs/sites": "^0.74.63",
145
+ "@stacksjs/skills": "^0.74.63",
146
+ "@stacksjs/storage": "^0.74.63",
147
+ "@stacksjs/strings": "^0.74.63",
148
148
  "@stacksjs/stx": "^0.2.286",
149
- "@stacksjs/testing": "^0.74.61",
150
- "@stacksjs/tinker": "^0.74.61",
149
+ "@stacksjs/testing": "^0.74.63",
150
+ "@stacksjs/tinker": "^0.74.63",
151
151
  "@stacksjs/tlsx": "^0.13.19",
152
152
  "@stacksjs/ts-cloud": "^0.16.0",
153
- "@stacksjs/tunnel": "^0.74.61",
154
- "@stacksjs/types": "^0.74.61",
155
- "@stacksjs/ui": "^0.74.61",
156
- "@stacksjs/utils": "^0.74.61",
157
- "@stacksjs/validation": "^0.74.61",
153
+ "@stacksjs/tunnel": "^0.74.63",
154
+ "@stacksjs/types": "^0.74.63",
155
+ "@stacksjs/ui": "^0.74.63",
156
+ "@stacksjs/utils": "^0.74.63",
157
+ "@stacksjs/validation": "^0.74.63",
158
158
  "ajv": "^8.20.0",
159
159
  "ajv-formats": "^3.0.1",
160
160
  "bun-plugin-stx": "^0.2.286",