@stacksjs/buddy 0.75.108 → 0.75.109

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.
@@ -1,2 +1,2 @@
1
- import{existsSync,readFileSync}from"node:fs";const API="https://api.appstoreconnect.apple.com";export function appStoreConnectCredentials(env=process.env){const keyId=env.APP_STORE_CONNECT_KEY_ID?.trim(),issuerId=env.APP_STORE_CONNECT_ISSUER_ID?.trim();let privateKey=env.APP_STORE_CONNECT_PRIVATE_KEY?.trim();const keyPath=env.APP_STORE_CONNECT_PRIVATE_KEY_PATH?.trim();if(!privateKey&&keyPath&&existsSync(keyPath))privateKey=readFileSync(keyPath,"utf8").trim();if(!keyId||!issuerId||!privateKey)return null;return{keyId,issuerId,privateKey:privateKey.replace(/\\n/g,`
2
- `)}}function base64url(bytes){return(typeof bytes==="string"?Buffer.from(bytes):Buffer.from(bytes)).toString("base64").replace(/=+$/,"").replace(/\+/g,"-").replace(/\//g,"_")}export async function appStoreConnectToken(credentials,now=Date.now()){const pem=credentials.privateKey.replace(/-----(BEGIN|END) PRIVATE KEY-----/g,"").replace(/\s+/g,""),key=await crypto.subtle.importKey("pkcs8",Buffer.from(pem,"base64"),{name:"ECDSA",namedCurve:"P-256"},!1,["sign"]),iat=Math.floor(now/1000),header=base64url(JSON.stringify({alg:"ES256",kid:credentials.keyId,typ:"JWT"})),payload=base64url(JSON.stringify({iss:credentials.issuerId,iat,exp:iat+1200,aud:"appstoreconnect-v1"})),signature=await crypto.subtle.sign({name:"ECDSA",hash:"SHA-256"},key,Buffer.from(`${header}.${payload}`));return`${header}.${payload}.${base64url(new Uint8Array(signature))}`}export class AppStoreConnect{token;fetchImpl;constructor(token,fetchImpl=fetch){this.token=token;this.fetchImpl=fetchImpl}static async connect(credentials,fetchImpl){return new AppStoreConnect(await appStoreConnectToken(credentials),fetchImpl)}async request(path,init={}){const response=await this.fetchImpl(`${API}${path}`,{...init,headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json",...init.headers}});if(!response.ok){const body=await response.text().catch(()=>"");throw Error(`App Store Connect ${init.method??"GET"} ${path} answered ${response.status}${body?`: ${body.slice(0,300)}`:""}`)}return response.status===204?null:await response.json()}async appId(bundleId){const id=(await this.request(`/v1/apps?filter[bundleId]=${encodeURIComponent(bundleId)}&limit=1`))?.data?.[0]?.id;if(!id)throw Error(`App Store Connect has no app with the bundle id ${bundleId}`);return id}async latestXcodeCloudRun(appId){const productId=(await this.request(`/v1/ciProducts?filter[app]=${appId}&limit=1`))?.data?.[0]?.id;if(!productId)return null;const runs=await this.request(`/v1/ciProducts/${productId}/buildRuns?sort=-number&limit=1&fields[ciBuildRuns]=number`),number=Number(runs?.data?.[0]?.attributes?.number);return Number.isInteger(number)?number:null}async builds(appId){return((await this.request(`/v1/builds?filter[app]=${appId}&sort=-uploadedDate&limit=50&fields[builds]=version,uploadedDate,processingState,expired`))?.data??[]).map((build)=>({id:build.id,number:Number(build.attributes?.version),uploadedDate:String(build.attributes?.uploadedDate??""),processingState:String(build.attributes?.processingState??""),expired:Boolean(build.attributes?.expired)}))}async expire(buildId){await this.request(`/v1/builds/${buildId}`,{method:"PATCH",body:JSON.stringify({data:{type:"builds",id:buildId,attributes:{expired:!0}}})})}}export function buildsToExpire(builds,keep=1){return builds.filter((build)=>!build.expired&&build.processingState==="VALID").sort((a,b)=>b.number-a.number).slice(Math.max(0,keep))}export function tagForXcodeCloudRun(version,latestRun){return`v${version}-build.${latestRun+1}`}
1
+ import{existsSync,readFileSync}from"node:fs";import{nextBuildNumber}from"@stacksjs/bumpx";const API="https://api.appstoreconnect.apple.com";export function appStoreConnectCredentials(env=process.env){const keyId=env.APP_STORE_CONNECT_KEY_ID?.trim(),issuerId=env.APP_STORE_CONNECT_ISSUER_ID?.trim();let privateKey=env.APP_STORE_CONNECT_PRIVATE_KEY?.trim();const keyPath=env.APP_STORE_CONNECT_PRIVATE_KEY_PATH?.trim();if(!privateKey&&keyPath&&existsSync(keyPath))privateKey=readFileSync(keyPath,"utf8").trim();if(!keyId||!issuerId||!privateKey)return null;return{keyId,issuerId,privateKey:privateKey.replace(/\\n/g,`
2
+ `)}}function base64url(bytes){return(typeof bytes==="string"?Buffer.from(bytes):Buffer.from(bytes)).toString("base64").replace(/=+$/,"").replace(/\+/g,"-").replace(/\//g,"_")}export async function appStoreConnectToken(credentials,now=Date.now()){const pem=credentials.privateKey.replace(/-----(BEGIN|END) PRIVATE KEY-----/g,"").replace(/\s+/g,""),key=await crypto.subtle.importKey("pkcs8",Buffer.from(pem,"base64"),{name:"ECDSA",namedCurve:"P-256"},!1,["sign"]),iat=Math.floor(now/1000),header=base64url(JSON.stringify({alg:"ES256",kid:credentials.keyId,typ:"JWT"})),payload=base64url(JSON.stringify({iss:credentials.issuerId,iat,exp:iat+1200,aud:"appstoreconnect-v1"})),signature=await crypto.subtle.sign({name:"ECDSA",hash:"SHA-256"},key,Buffer.from(`${header}.${payload}`));return`${header}.${payload}.${base64url(new Uint8Array(signature))}`}export class AppStoreConnect{token;fetchImpl;constructor(token,fetchImpl=fetch){this.token=token;this.fetchImpl=fetchImpl}static async connect(credentials,fetchImpl){return new AppStoreConnect(await appStoreConnectToken(credentials),fetchImpl)}async request(path,init={}){const response=await this.fetchImpl(`${API}${path}`,{...init,headers:{Authorization:`Bearer ${this.token}`,"Content-Type":"application/json",...init.headers}});if(!response.ok){const body=await response.text().catch(()=>"");throw Error(`App Store Connect ${init.method??"GET"} ${path} answered ${response.status}${body?`: ${body.slice(0,300)}`:""}`)}return response.status===204?null:await response.json()}async appId(bundleId){const id=(await this.request(`/v1/apps?filter[bundleId]=${encodeURIComponent(bundleId)}&limit=1`))?.data?.[0]?.id;if(!id)throw Error(`App Store Connect has no app with the bundle id ${bundleId}`);return id}async latestXcodeCloudRun(appId){const productId=(await this.request(`/v1/ciProducts?filter[app]=${appId}&limit=1`))?.data?.[0]?.id;if(!productId)return null;const runs=await this.request(`/v1/ciProducts/${productId}/buildRuns?sort=-number&limit=1&fields[ciBuildRuns]=number`),number=Number(runs?.data?.[0]?.attributes?.number);return Number.isSafeInteger(number)&&number>=0?number:null}async builds(appId){return((await this.request(`/v1/builds?filter[app]=${appId}&sort=-uploadedDate&limit=50&fields[builds]=version,uploadedDate,processingState,expired`))?.data??[]).map((build)=>({id:build.id,number:Number(build.attributes?.version),uploadedDate:String(build.attributes?.uploadedDate??""),processingState:String(build.attributes?.processingState??""),expired:Boolean(build.attributes?.expired)}))}async expire(buildId){await this.request(`/v1/builds/${buildId}`,{method:"PATCH",body:JSON.stringify({data:{type:"builds",id:buildId,attributes:{expired:!0}}})})}}export function buildsToExpire(builds,keep=1){return builds.filter((build)=>!build.expired&&build.processingState==="VALID").sort((a,b)=>b.number-a.number).slice(Math.max(0,keep))}export function tagForXcodeCloudRun(version,latestRun){return`v${version}-build.${nextBuildNumber(latestRun)}`}
@@ -1,2 +1,2 @@
1
- import{execFileSync}from"node:child_process";import{existsSync,readFileSync,writeFileSync}from"node:fs";import{join}from"node:path";import{pathToFileURL}from"node:url";import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,italic,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{AppStoreConnect,appStoreConnectCredentials,buildsToExpire,tagForXcodeCloudRun}from"../app-store-connect";import{bumpedVersion,iosVersionIn,nextBuildTag,withIosVersion}from"../release-ios";import{resultFailed}from"../result";const descriptions={release:"Release a new version of your libraries/packages",project:"Target a specific project",dryRun:"Run the release without actually releasing",bump:"Non-interactive bump: patch | minor | major | prepatch | preminor | premajor | prerelease | calendar | x.y.z",verbose:"Enable verbose output"};export function release(buddy){buddy.command("release",descriptions.release).option("--dry-run",descriptions.dryRun,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--bump <type>",descriptions.bump).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy release` ...",options);if(options.dryRun)log.warn("Dry run enabled. No changes will be made or committed.");const startTime=await intro("buddy release"),result=await runAction(Action.Release,options);if(resultFailed(result)){await log.error("Failed to release",result.error);process.exit(ExitCode.FatalError)}await outro(options.dryRun?"Dry run complete. Nothing was committed, tagged or pushed.":"Triggered CI/CD Release via GitHub Actions",{startTime,useSeconds:!0});if(!options.dryRun)log.info(`Follow along: ${italic(resolveGitHubActionsUrl(readOriginRemote()))}`)});buddy.command("release:ios","Ship the iOS app: regenerate its project for production, commit, tag and push").option("--bump <type>","Raise the app version first: patch | minor | major | x.y.z (default: a new build of the current version)").option("--keep-builds <n>","TestFlight builds to keep besides the new one; older ones are expired (needs an App Store Connect API key)",{default:1}).option("--dry-run",descriptions.dryRun,{default:!1}).action(async(options)=>{const startTime=await intro("buddy release:ios");try{const tag=await releaseIos(options);await outro(options.dryRun?`Dry run complete: would release ${tag}`:`Released ${tag}`,{startTime,useSeconds:!0});if(!options.dryRun)log.info('A CI that builds tags (Xcode Cloud: Tag Changes, prefix "v") turns it into a TestFlight build now.')}catch(error){await log.error("Failed to release the iOS app",error);process.exit(ExitCode.FatalError)}});onUnknownSubcommand(buddy,"release")}function git(...args){return execFileSync("git",args,{encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()}async function releaseIos(options){const root=process.cwd(),configPath=join(root,"config/mobile.ts");if(!existsSync(configPath))throw Error("config/mobile.ts not found: run this in a Stacks app with a mobile config");const branch=git("rev-parse","--abbrev-ref","HEAD");if(branch!=="main")throw Error(`releases are cut from main; this is ${branch}`);if(git("status","--porcelain","--untracked-files=no"))throw Error("commit or stash your changes first, so the release is exactly what is on main");git("pull","--quiet","--rebase","origin","main");git("fetch","--quiet","--tags","origin");const source=readFileSync(configPath,"utf8"),current=iosVersionIn(source);if(!current)throw Error("config/mobile.ts declares no iOS `version`");const version=options.bump?bumpedVersion(current,options.bump):current,existingTags=git("tag","--list",`v${version}-build.*`).split(`
2
- `).filter(Boolean);let tag=nextBuildTag(version,existingTags);const credentials=appStoreConnectCredentials();let store=null;if(credentials){const bundleId=(await import(pathToFileURL(configPath).href)).default.ios?.bundleId;if(!bundleId)throw Error("config/mobile.ts declares no ios.bundleId");const api=await AppStoreConnect.connect(credentials);store={api,appId:await api.appId(bundleId)};const latestRun=await api.latestXcodeCloudRun(store.appId);if(latestRun!==null){const fromStore=tagForXcodeCloudRun(version,latestRun);if(!existingTags.includes(fromStore))tag=fromStore}}else log.warn("No App Store Connect API key (APP_STORE_CONNECT_KEY_ID, _ISSUER_ID, _PRIVATE_KEY_PATH): the tag counts releases, not the build number TestFlight will show, and older builds stay listed.");log.info(`Releasing ${tag}${version===current?"":` (version ${current} \u2192 ${version})`}`);const keep=Math.max(0,Number(options.keepBuilds??1)||0);if(options.dryRun){if(store){const retiring=buildsToExpire(await store.api.builds(store.appId),keep);log.info(retiring.length?`Would expire TestFlight builds ${retiring.map((b)=>b.number).join(", ")}`:"No TestFlight builds to expire")}return tag}const paths=["config/mobile.ts","storage/framework/mobile/ios"];if(version!==current){writeFileSync(configPath,withIosVersion(source,version));const pkgPath=join(root,"package.json");if(existsSync(pkgPath)){const pkg=readFileSync(pkgPath,"utf8");writeFileSync(pkgPath,pkg.replace(/("version"\s*:\s*)"[^"]*"/,`$1"${version}"`));paths.push("package.json")}}for(const key of["MOBILE_URL","IOS_APP_VERSION","IOS_BUILD_NUMBER"])delete process.env[key];const result=await runAction(Action.BuildIos,{});if(resultFailed(result))throw result.error??Error("the iOS build failed");git("add","--",...paths);if(git("diff","--cached","--name-only"))git("commit","--quiet","-m",`chore(release): ${tag}`);git("tag","-a",tag,"-m",tag);git("push","--quiet","--atomic","origin","main",tag);if(store)try{const retiring=buildsToExpire(await store.api.builds(store.appId),keep);for(const build of retiring)await store.api.expire(build.id);if(retiring.length)log.info(`Expired TestFlight builds ${retiring.map((b)=>b.number).join(", ")}`)}catch(error){log.warn(`Could not expire older TestFlight builds: ${error instanceof Error?error.message:String(error)}`)}return tag}export function resolveGitHubActionsUrl(remoteUrl,repository=process.env.GITHUB_REPOSITORY,serverUrl=process.env.GITHUB_SERVER_URL??"https://github.com"){const repo=repository?.trim()||remoteUrl?.trim().match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?$/)?.[1];return repo?`${serverUrl.replace(/\/$/,"")}/${repo}/actions`:"https://github.com/stacksjs/stacks/actions"}function readOriginRemote(){try{return execFileSync("git",["config","--get","remote.origin.url"],{encoding:"utf8"}).trim()}catch{return}}
1
+ import{execFileSync}from"node:child_process";import{existsSync,readFileSync,writeFileSync}from"node:fs";import{join}from"node:path";import{pathToFileURL}from"node:url";import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,italic,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{AppStoreConnect,appStoreConnectCredentials,buildsToExpire}from"../app-store-connect";import{bumpedVersion,iosVersionIn,nextBuildTag,withIosVersion}from"../release-ios";import{resultFailed}from"../result";const descriptions={release:"Release a new version of your libraries/packages",project:"Target a specific project",dryRun:"Run the release without actually releasing",bump:"Non-interactive bump: patch | minor | major | prepatch | preminor | premajor | prerelease | pre | release | build | calendar | x.y.z",verbose:"Enable verbose output"};export function release(buddy){buddy.command("release",descriptions.release).option("--dry-run",descriptions.dryRun,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--bump <type>",descriptions.bump).option("--preid <identifier>","Prerelease channel, for example beta or rc").option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy release` ...",options);if(options.dryRun)log.warn("Dry run enabled. No changes will be made or committed.");const startTime=await intro("buddy release"),result=await runAction(Action.Release,options);if(resultFailed(result)){await log.error("Failed to release",result.error);process.exit(ExitCode.FatalError)}await outro(options.dryRun?"Dry run complete. Nothing was committed, tagged or pushed.":"Triggered CI/CD Release via GitHub Actions",{startTime,useSeconds:!0});if(!options.dryRun)log.info(`Follow along: ${italic(resolveGitHubActionsUrl(readOriginRemote()))}`)});buddy.command("release:ios","Ship the iOS app: regenerate its project for production, commit, tag and push").option("--bump <type>","build | patch | minor | major | x.y.z (default: a new build of the current marketing version)").option("--keep-builds <n>","TestFlight builds to keep besides the new one; older ones are expired (needs an App Store Connect API key)",{default:1}).option("--dry-run",descriptions.dryRun,{default:!1}).action(async(options)=>{const startTime=await intro("buddy release:ios");try{const tag=await releaseIos(options);await outro(options.dryRun?`Dry run complete: would release ${tag}`:`Released ${tag}`,{startTime,useSeconds:!0});if(!options.dryRun)log.info('A CI that builds tags (Xcode Cloud: Tag Changes, prefix "v") turns it into a TestFlight build now.')}catch(error){await log.error("Failed to release the iOS app",error);process.exit(ExitCode.FatalError)}});onUnknownSubcommand(buddy,"release")}function git(...args){return execFileSync("git",args,{encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim()}async function releaseIos(options){const root=process.cwd(),configPath=join(root,"config/mobile.ts");if(!existsSync(configPath))throw Error("config/mobile.ts not found: run this in a Stacks app with a mobile config");const branch=git("rev-parse","--abbrev-ref","HEAD");if(branch!=="main")throw Error(`releases are cut from main; this is ${branch}`);if(git("status","--porcelain","--untracked-files=no"))throw Error("commit or stash your changes first, so the release is exactly what is on main");git("pull","--quiet","--rebase","origin","main");git("fetch","--quiet","--tags","origin");const source=readFileSync(configPath,"utf8"),current=iosVersionIn(source);if(!current)throw Error("config/mobile.ts declares no iOS `version`");const version=options.bump?bumpedVersion(current,options.bump):current,existingTags=git("tag","--list",`v${version}-build.*`).split(`
2
+ `).filter(Boolean);let tag=nextBuildTag(version,existingTags);const credentials=appStoreConnectCredentials();let store=null;if(credentials){const bundleId=(await import(pathToFileURL(configPath).href)).default.ios?.bundleId;if(!bundleId)throw Error("config/mobile.ts declares no ios.bundleId");const api=await AppStoreConnect.connect(credentials);store={api,appId:await api.appId(bundleId)};const latestRun=await api.latestXcodeCloudRun(store.appId);if(latestRun!==null)tag=nextBuildTag(version,existingTags,latestRun)}else log.warn("No App Store Connect API key (APP_STORE_CONNECT_KEY_ID, _ISSUER_ID, _PRIVATE_KEY_PATH): the tag counts releases, not the build number TestFlight will show, and older builds stay listed.");log.info(`Releasing ${tag}${version===current?"":` (version ${current} \u2192 ${version})`}`);const keep=Math.max(0,Number(options.keepBuilds??1)||0);if(options.dryRun){if(store){const retiring=buildsToExpire(await store.api.builds(store.appId),keep);log.info(retiring.length?`Would expire TestFlight builds ${retiring.map((b)=>b.number).join(", ")}`:"No TestFlight builds to expire")}return tag}const paths=["config/mobile.ts","storage/framework/mobile/ios"];if(version!==current){writeFileSync(configPath,withIosVersion(source,version));const pkgPath=join(root,"package.json");if(existsSync(pkgPath)){const pkg=readFileSync(pkgPath,"utf8");writeFileSync(pkgPath,pkg.replace(/("version"\s*:\s*)"[^"]*"/,`$1"${version}"`));paths.push("package.json")}}for(const key of["MOBILE_URL","IOS_APP_VERSION","IOS_BUILD_NUMBER"])delete process.env[key];const result=await runAction(Action.BuildIos,{});if(resultFailed(result))throw result.error??Error("the iOS build failed");git("add","--",...paths);if(git("diff","--cached","--name-only"))git("commit","--quiet","-m",`chore(release): ${tag}`);git("tag","-a",tag,"-m",tag);git("push","--quiet","--atomic","origin","main",tag);if(store)try{const retiring=buildsToExpire(await store.api.builds(store.appId),keep);for(const build of retiring)await store.api.expire(build.id);if(retiring.length)log.info(`Expired TestFlight builds ${retiring.map((b)=>b.number).join(", ")}`)}catch(error){log.warn(`Could not expire older TestFlight builds: ${error instanceof Error?error.message:String(error)}`)}return tag}export function resolveGitHubActionsUrl(remoteUrl,repository=process.env.GITHUB_REPOSITORY,serverUrl=process.env.GITHUB_SERVER_URL??"https://github.com"){const repo=repository?.trim()||remoteUrl?.trim().match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?$/)?.[1];return repo?`${serverUrl.replace(/\/$/,"")}/${repo}/actions`:"https://github.com/stacksjs/stacks/actions"}function readOriginRemote(){try{return execFileSync("git",["config","--get","remote.origin.url"],{encoding:"utf8"}).trim()}catch{return}}
@@ -5,4 +5,4 @@ export declare function withIosVersion(source: string, version: string): string;
5
5
  /** The version a bump leads to, or the one given outright. */
6
6
  export declare function bumpedVersion(current: string, bump: string): string;
7
7
  /** The next build's tag for `version`, after the tags that already exist. */
8
- export declare function nextBuildTag(version: string, tags: string[]): string;
8
+ export declare function nextBuildTag(version: string, tags: string[], latestRun?: number): string;
@@ -1 +1 @@
1
- const SEMVER=/^\d+\.\d+\.\d+$/;function iosBlock(source){const start=source.search(/\bios\s*:\s*\{/);if(start===-1)return null;const after=source.slice(start+1).search(/\n\s*(?:android|desktop|watch|web)\s*:\s*\{/);return{start,end:after===-1?source.length:start+1+after}}const VERSION_ENTRY=/(\bversion\s*:\s*(?:[^,\n]*?\?\?\s*)?)(['"])(\d+\.\d+\.\d+)\2/;export function iosVersionIn(source){const block=iosBlock(source);if(!block)return null;return VERSION_ENTRY.exec(source.slice(block.start,block.end))?.[3]??null}export function withIosVersion(source,version){const block=iosBlock(source);if(!block||!VERSION_ENTRY.test(source.slice(block.start,block.end)))throw Error("config/mobile.ts declares no iOS `version`");const ios=source.slice(block.start,block.end).replace(VERSION_ENTRY,(_,lead,quote)=>`${lead}${quote}${version}${quote}`);return source.slice(0,block.start)+ios+source.slice(block.end)}export function bumpedVersion(current,bump){if(SEMVER.test(bump))return bump;const[major=0,minor=0,patch=0]=current.split(".").map(Number);if(bump==="major")return`${major+1}.0.0`;if(bump==="minor")return`${major}.${minor+1}.0`;if(bump==="patch")return`${major}.${minor}.${patch+1}`;throw Error(`"${bump}" is not patch, minor, major or a version like 1.2.0`)}export function nextBuildTag(version,tags){const prefix=`v${version}-build.`,builds=tags.filter((tag)=>tag.startsWith(prefix)).map((tag)=>Number(tag.slice(prefix.length))).filter((n)=>Number.isInteger(n)&&n>0);return`${prefix}${Math.max(0,...builds)+1}`}
1
+ import{incrementVersion,nextBuildNumber,SemVer}from"@stacksjs/bumpx";function marketingVersion(version){const parsed=new SemVer(version);if(parsed.prerelease.length||parsed.build.length||parsed.toString()!==version)throw Error("An iOS marketing version must be a stable version such as 1.0.0");return parsed.toString()}function iosBlock(source){const start=source.search(/\bios\s*:\s*\{/);if(start===-1)return null;const after=source.slice(start+1).search(/\n\s*(?:android|desktop|watch|web)\s*:\s*\{/);return{start,end:after===-1?source.length:start+1+after}}const VERSION_ENTRY=/(\bversion\s*:\s*(?:[^,\n]*?\?\?\s*)?)(['"])(\d+\.\d+\.\d+)\2/;export function iosVersionIn(source){const block=iosBlock(source);if(!block)return null;return VERSION_ENTRY.exec(source.slice(block.start,block.end))?.[3]??null}export function withIosVersion(source,version){marketingVersion(version);const block=iosBlock(source);if(!block||!VERSION_ENTRY.test(source.slice(block.start,block.end)))throw Error("config/mobile.ts declares no iOS `version`");const ios=source.slice(block.start,block.end).replace(VERSION_ENTRY,(_,lead,quote)=>`${lead}${quote}${version}${quote}`);return source.slice(0,block.start)+ios+source.slice(block.end)}export function bumpedVersion(current,bump){marketingVersion(current);if(bump==="build")return current;if(["patch","minor","major"].includes(bump))return marketingVersion(incrementVersion(current,bump));return marketingVersion(bump)}export function nextBuildTag(version,tags,latestRun=0){marketingVersion(version);const prefix=`v${version}-build.`,builds=tags.filter((tag)=>tag.startsWith(prefix)).map((tag)=>tag.slice(prefix.length)).filter((number)=>/^[1-9]\d*$/.test(number)).map(Number).filter(Number.isSafeInteger);return`${prefix}${nextBuildNumber(latestRun,builds)}`}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.108",
5
+ "version": "0.75.109",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -95,68 +95,69 @@
95
95
  "prepublishOnly": "bun run build"
96
96
  },
97
97
  "dependencies": {
98
- "@stacksjs/actions": "^0.75.108",
99
- "@stacksjs/ai": "^0.75.108",
100
- "@stacksjs/alias": "^0.75.108",
101
- "@stacksjs/analytics": "^0.75.108",
102
- "@stacksjs/api": "^0.75.108",
103
- "@stacksjs/arrays": "^0.75.108",
104
- "@stacksjs/auth": "^0.75.108",
105
- "@stacksjs/browser-extension": "^0.75.108",
106
- "@stacksjs/build": "^0.75.108",
107
- "@stacksjs/cache": "^0.75.108",
108
- "@stacksjs/chat": "^0.75.108",
98
+ "@stacksjs/bumpx": "^0.2.20",
99
+ "@stacksjs/actions": "^0.75.109",
100
+ "@stacksjs/ai": "^0.75.109",
101
+ "@stacksjs/alias": "^0.75.109",
102
+ "@stacksjs/analytics": "^0.75.109",
103
+ "@stacksjs/api": "^0.75.109",
104
+ "@stacksjs/arrays": "^0.75.109",
105
+ "@stacksjs/auth": "^0.75.109",
106
+ "@stacksjs/browser-extension": "^0.75.109",
107
+ "@stacksjs/build": "^0.75.109",
108
+ "@stacksjs/cache": "^0.75.109",
109
+ "@stacksjs/chat": "^0.75.109",
109
110
  "@stacksjs/clapp": "^0.2.16",
110
- "@stacksjs/cli": "^0.75.108",
111
- "@stacksjs/cloud": "^0.75.108",
112
- "@stacksjs/cms": "^0.75.108",
113
- "@stacksjs/collections": "^0.75.108",
114
- "@stacksjs/commerce": "^0.75.108",
115
- "@stacksjs/config": "^0.75.108",
116
- "@stacksjs/database": "^0.75.108",
117
- "@stacksjs/desktop-build": "^0.75.108",
118
- "@stacksjs/dns": "^0.75.108",
111
+ "@stacksjs/cli": "^0.75.109",
112
+ "@stacksjs/cloud": "^0.75.109",
113
+ "@stacksjs/cms": "^0.75.109",
114
+ "@stacksjs/collections": "^0.75.109",
115
+ "@stacksjs/commerce": "^0.75.109",
116
+ "@stacksjs/config": "^0.75.109",
117
+ "@stacksjs/database": "^0.75.109",
118
+ "@stacksjs/desktop-build": "^0.75.109",
119
+ "@stacksjs/dns": "^0.75.109",
119
120
  "@stacksjs/dnsx": "^0.2.3",
120
- "@stacksjs/email": "^0.75.108",
121
- "@stacksjs/enums": "^0.75.108",
122
- "@stacksjs/env": "^0.75.108",
123
- "@stacksjs/error-handling": "^0.75.108",
124
- "@stacksjs/events": "^0.75.108",
125
- "@stacksjs/features": "^0.75.108",
126
- "@stacksjs/git": "^0.75.108",
121
+ "@stacksjs/email": "^0.75.109",
122
+ "@stacksjs/enums": "^0.75.109",
123
+ "@stacksjs/env": "^0.75.109",
124
+ "@stacksjs/error-handling": "^0.75.109",
125
+ "@stacksjs/events": "^0.75.109",
126
+ "@stacksjs/features": "^0.75.109",
127
+ "@stacksjs/git": "^0.75.109",
127
128
  "@stacksjs/gitit": "^0.2.5",
128
- "@stacksjs/health": "^0.75.108",
129
+ "@stacksjs/health": "^0.75.109",
129
130
  "@stacksjs/httx": "^0.1.10",
130
- "@stacksjs/image": "^0.75.108",
131
- "@stacksjs/lint": "^0.75.108",
132
- "@stacksjs/logging": "^0.75.108",
133
- "@stacksjs/notifications": "^0.75.108",
134
- "@stacksjs/objects": "^0.75.108",
135
- "@stacksjs/orm": "^0.75.108",
136
- "@stacksjs/path": "^0.75.108",
137
- "@stacksjs/payments": "^0.75.108",
138
- "@stacksjs/realtime": "^0.75.108",
139
- "@stacksjs/router": "^0.75.108",
131
+ "@stacksjs/image": "^0.75.109",
132
+ "@stacksjs/lint": "^0.75.109",
133
+ "@stacksjs/logging": "^0.75.109",
134
+ "@stacksjs/notifications": "^0.75.109",
135
+ "@stacksjs/objects": "^0.75.109",
136
+ "@stacksjs/orm": "^0.75.109",
137
+ "@stacksjs/path": "^0.75.109",
138
+ "@stacksjs/payments": "^0.75.109",
139
+ "@stacksjs/realtime": "^0.75.109",
140
+ "@stacksjs/router": "^0.75.109",
140
141
  "@stacksjs/rpx": "^0.11.54",
141
- "@stacksjs/scheduler": "^0.75.108",
142
- "@stacksjs/search-engine": "^0.75.108",
143
- "@stacksjs/security": "^0.75.108",
144
- "@stacksjs/server": "^0.75.108",
145
- "@stacksjs/sites": "^0.75.108",
146
- "@stacksjs/skills": "^0.75.108",
147
- "@stacksjs/socials": "^0.75.108",
148
- "@stacksjs/storage": "^0.75.108",
149
- "@stacksjs/strings": "^0.75.108",
142
+ "@stacksjs/scheduler": "^0.75.109",
143
+ "@stacksjs/search-engine": "^0.75.109",
144
+ "@stacksjs/security": "^0.75.109",
145
+ "@stacksjs/server": "^0.75.109",
146
+ "@stacksjs/sites": "^0.75.109",
147
+ "@stacksjs/skills": "^0.75.109",
148
+ "@stacksjs/socials": "^0.75.109",
149
+ "@stacksjs/storage": "^0.75.109",
150
+ "@stacksjs/strings": "^0.75.109",
150
151
  "@stacksjs/stx": "^0.2.422",
151
- "@stacksjs/testing": "^0.75.108",
152
- "@stacksjs/tinker": "^0.75.108",
152
+ "@stacksjs/testing": "^0.75.109",
153
+ "@stacksjs/tinker": "^0.75.109",
153
154
  "@stacksjs/tlsx": "^0.13.19",
154
155
  "@stacksjs/ts-cloud": "^0.16.47",
155
- "@stacksjs/tunnel": "^0.75.108",
156
- "@stacksjs/types": "^0.75.108",
157
- "@stacksjs/ui": "^0.75.108",
158
- "@stacksjs/utils": "^0.75.108",
159
- "@stacksjs/validation": "^0.75.108",
156
+ "@stacksjs/tunnel": "^0.75.109",
157
+ "@stacksjs/types": "^0.75.109",
158
+ "@stacksjs/ui": "^0.75.109",
159
+ "@stacksjs/utils": "^0.75.109",
160
+ "@stacksjs/validation": "^0.75.109",
160
161
  "ajv": "^8.20.0",
161
162
  "ajv-formats": "^3.0.1",
162
163
  "bun-plugin-stx": "^0.2.422",