@stacksjs/actions 0.75.256 → 0.75.257
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build/android.js +1 -1
- package/dist/build/ios.js +1 -1
- package/dist/build/mobile-target.d.ts +9 -0
- package/dist/build/mobile-target.js +1 -0
- package/dist/deploy/hooks.d.ts +2 -0
- package/dist/deploy/hooks.js +1 -0
- package/dist/deploy/index.js +3 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/project-scripts.d.ts +14 -0
- package/dist/project-scripts.js +210 -0
- package/dist/publication-preflight.d.ts +5 -0
- package/dist/publication-preflight.js +1 -0
- package/dist/release-evidence.d.ts +47 -0
- package/dist/release-evidence.js +1 -0
- package/dist/release.js +1 -1
- package/dist/upgrade/package-project.d.ts +1 -1
- package/dist/upgrade/package-project.js +2 -2
- package/dist/upgrade/packages.js +2 -2
- package/package.json +43 -43
package/dist/build/android.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{existsSync,readFileSync,writeFileSync}from"node:fs";import{pathToFileURL}from"node:url";import process from"node:process";import{log}from"@stacksjs/cli";import{projectPath,storagePath}from"@stacksjs/path";import{craftBuilderSpecifier,craftBuilderVersion}from"./craft-entry";import{resolveCraftBuilderProvenance}from"./craft-provenance";import{androidRelease,assertCraftSupportsAndroid,assertCraftSupportsAndroidMapProvider,toCraftAndroidConfig,uploadAndroidMapping,validateAndroidMobileConfig}from"./android-config";import{resolveCrashReporting,resolveMobilePath,resolveNativeFonts,assertCraftSupportsNativeFonts}from"./ios-config";import{compileNativeScreens}from"./native-screens";import{writeAndroidIconTable}from"./android-icons";process.exitCode=1;function craftAndroidEntry(){const explicit=process.env.CRAFT_ANDROID_SRC;return explicit?pathToFileURL(explicit).href:craftBuilderSpecifier("android",projectPath())}async function loadCraftAndroidBuilder(){try{return await import(craftAndroidEntry())}catch(error){throw Error("Craft Android builder is unavailable. Install the current craft-native package or set CRAFT_ANDROID_SRC to packages/android/src/index.ts in a Craft checkout.",{cause:error})}}const configPath=projectPath("config/mobile.ts");if(!existsSync(configPath))throw Error("Missing config/mobile.ts. Add an android section before running `buddy build:android`.");const configModule=await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`),androidSection=configModule.default.android;if(!androidSection)throw Error("config/mobile.ts must define an android section");const config={...androidSection,crashReporting:resolveCrashReporting(configModule.default.crashReporting,androidSection.crashReporting)};validateAndroidMobileConfig(config);const output=resolveMobilePath(projectPath(),config.output)??storagePath("framework/mobile/android"),webAssets=resolveMobilePath(projectPath(),config.webAssets),fallbackWebAssets=resolveMobilePath(projectPath(),config.fallbackWebAssets),craftConfig=toCraftAndroidConfig(config);craftConfig.nativeFonts=resolveNativeFonts(config.nativeFonts,projectPath());craftConfig.appIconPath=resolveMobilePath(projectPath(),config.appIcon);craftConfig.notificationIconPath=resolveMobilePath(projectPath(),config.notificationIcon);craftConfig.googleServicesFile=resolveMobilePath(projectPath(),config.googleServicesFile);if(process.env.NODE_ENV==="production"&&config.capabilities?.pushNotifications&&!craftConfig.googleServicesFile)throw Error("Production Android push notifications require android.googleServicesFile in config/mobile.ts");const appBundle=process.env.STACKS_ANDROID_BUNDLE==="1",release=appBundle||process.env.NODE_ENV==="production",native=config.renderer==="native",builder=await loadCraftAndroidBuilder(),nativeBundle=native?await compileNativeScreens(config,projectPath(),output,void 0,"android",{routes:builder.nativeRouteBundles===!0,runtime:builder.nativeRouteRuntime===!0,messages:builder.nativeMessages===!0}):null;if(nativeBundle){for(const diagnostic of nativeBundle.diagnostics??[])log.warn(`${diagnostic.file?`${diagnostic.file}: `:""}${diagnostic.message}`);log.info(`Compiled ${Object.keys(config.nativeScreens??{}).length} native screen path(s) into ${nativeBundle.outFile}`)}const{LUCIDE_TO_SF}=native?await import("@stacksjs/stx/native"):{LUCIDE_TO_SF:{}},iconsPath=native?writeAndroidIconTable(projectPath(),output,LUCIDE_TO_SF):null;if(native&&!iconsPath)log.warn("No @iconify-json/lucide in the app: Android draws its own glyphs for the screens' icons.");const craftVersion=craftBuilderVersion(craftAndroidEntry(),projectPath());assertCraftSupportsNativeFonts(config.nativeFonts,craftVersion?.version);if(craftVersion)assertCraftSupportsAndroid(config,craftVersion.version,craftVersion.manifest,{appBundle});else log.warn(`Could not read the craft-native version behind ${craftAndroidEntry()}, so the build cannot check that it supports what config/mobile.ts asks for.`);assertCraftSupportsAndroidMapProvider(config,builder.nativeMapProviders);await builder.init({name:config.appName,packageName:config.packageName,output,config:craftConfig});await builder.build(native?{output,nativeBundlePath:nativeBundle.outFile,iconsPath:iconsPath??void 0,release,bundle:appBundle,compile:process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"}:{output,htmlPath:webAssets??fallbackWebAssets,devServer:craftConfig.devServerURL,release,bundle:appBundle,compile:process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"});if(release&&config.crashReporting?.key&&config.crashReporting.uploadSymbols!==!1&&process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"){const upload=await uploadAndroidMapping(`${output}/app/build/outputs/mapping/release/mapping.txt`,config.crashReporting.host??"https://bughq.org",androidRelease(config),process.env.BUGHQ_UPLOAD_TOKEN);if(upload.ok)log.info(upload.message);else log.warn(upload.message)}const sourceRevision=Bun.spawnSync(["git","rev-parse","HEAD"],{cwd:projectPath()}).stdout.toString().trim(),generatedConfig=JSON.parse(readFileSync(`${output}/craft.config.json`,"utf8"));writeFileSync(`${output}/stacks-mobile.json`,`${JSON.stringify({schemaVersion:"1.0.0",platform:"android",sourceRevision,source:native?{kind:"native",site:craftConfig.devServerURL}:webAssets?{kind:"bundled",path:webAssets}:{kind:"remote",url:craftConfig.devServerURL,fallback:fallbackWebAssets?{kind:"bundled",path:fallbackWebAssets}:void 0},capabilities:config.capabilities??{},builder:resolveCraftBuilderProvenance(process.env.CRAFT_ANDROID_SRC),craft:generatedConfig},null,2)}
|
|
1
|
+
import{existsSync,readFileSync,writeFileSync}from"node:fs";import{pathToFileURL}from"node:url";import process from"node:process";import{log}from"@stacksjs/cli";import{projectPath,storagePath}from"@stacksjs/path";import{craftBuilderSpecifier,craftBuilderVersion}from"./craft-entry";import{resolveCraftBuilderProvenance}from"./craft-provenance";import{androidRelease,assertCraftSupportsAndroid,assertCraftSupportsAndroidMapProvider,toCraftAndroidConfig,uploadAndroidMapping,validateAndroidMobileConfig}from"./android-config";import{resolveCrashReporting,resolveMobilePath,resolveNativeFonts,assertCraftSupportsNativeFonts}from"./ios-config";import{compileNativeScreens}from"./native-screens";import{writeAndroidIconTable}from"./android-icons";import{reportMobileBuildTarget}from"./mobile-target";process.exitCode=1;function craftAndroidEntry(){const explicit=process.env.CRAFT_ANDROID_SRC;return explicit?pathToFileURL(explicit).href:craftBuilderSpecifier("android",projectPath())}async function loadCraftAndroidBuilder(){try{return await import(craftAndroidEntry())}catch(error){throw Error("Craft Android builder is unavailable. Install the current craft-native package or set CRAFT_ANDROID_SRC to packages/android/src/index.ts in a Craft checkout.",{cause:error})}}const configPath=projectPath("config/mobile.ts");if(!existsSync(configPath))throw Error("Missing config/mobile.ts. Add an android section before running `buddy build:android`.");const configModule=await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`),androidSection=configModule.default.android;if(!androidSection)throw Error("config/mobile.ts must define an android section");const config={...androidSection,crashReporting:resolveCrashReporting(configModule.default.crashReporting,androidSection.crashReporting)};validateAndroidMobileConfig(config);const output=resolveMobilePath(projectPath(),config.output)??storagePath("framework/mobile/android"),webAssets=resolveMobilePath(projectPath(),config.webAssets),fallbackWebAssets=resolveMobilePath(projectPath(),config.fallbackWebAssets),craftConfig=toCraftAndroidConfig(config);craftConfig.nativeFonts=resolveNativeFonts(config.nativeFonts,projectPath());craftConfig.appIconPath=resolveMobilePath(projectPath(),config.appIcon);craftConfig.notificationIconPath=resolveMobilePath(projectPath(),config.notificationIcon);craftConfig.googleServicesFile=resolveMobilePath(projectPath(),config.googleServicesFile);if(process.env.NODE_ENV==="production"&&config.capabilities?.pushNotifications&&!craftConfig.googleServicesFile)throw Error("Production Android push notifications require android.googleServicesFile in config/mobile.ts");const appBundle=process.env.STACKS_ANDROID_BUNDLE==="1",release=appBundle||process.env.NODE_ENV==="production",native=config.renderer==="native",builder=await loadCraftAndroidBuilder(),nativeBundle=native?await compileNativeScreens(config,projectPath(),output,void 0,"android",{routes:builder.nativeRouteBundles===!0,runtime:builder.nativeRouteRuntime===!0,messages:builder.nativeMessages===!0}):null;if(nativeBundle){for(const diagnostic of nativeBundle.diagnostics??[])log.warn(`${diagnostic.file?`${diagnostic.file}: `:""}${diagnostic.message}`);log.info(`Compiled ${Object.keys(config.nativeScreens??{}).length} native screen path(s) into ${nativeBundle.outFile}`)}const{LUCIDE_TO_SF}=native?await import("@stacksjs/stx/native"):{LUCIDE_TO_SF:{}},iconsPath=native?writeAndroidIconTable(projectPath(),output,LUCIDE_TO_SF):null;if(native&&!iconsPath)log.warn("No @iconify-json/lucide in the app: Android draws its own glyphs for the screens' icons.");const craftVersion=craftBuilderVersion(craftAndroidEntry(),projectPath());assertCraftSupportsNativeFonts(config.nativeFonts,craftVersion?.version);if(craftVersion)assertCraftSupportsAndroid(config,craftVersion.version,craftVersion.manifest,{appBundle});else log.warn(`Could not read the craft-native version behind ${craftAndroidEntry()}, so the build cannot check that it supports what config/mobile.ts asks for.`);assertCraftSupportsAndroidMapProvider(config,builder.nativeMapProviders);await builder.init({name:config.appName,packageName:config.packageName,output,config:craftConfig});reportMobileBuildTarget({renderer:config.renderer,site:craftConfig.devServerURL,nativeBundle:nativeBundle?.outFile,webAssets,fallbackWebAssets},log);await builder.build(native?{output,nativeBundlePath:nativeBundle.outFile,iconsPath:iconsPath??void 0,release,bundle:appBundle,compile:process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"}:{output,htmlPath:webAssets??fallbackWebAssets,devServer:craftConfig.devServerURL,release,bundle:appBundle,compile:process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"});if(release&&config.crashReporting?.key&&config.crashReporting.uploadSymbols!==!1&&process.env.STACKS_ANDROID_SKIP_GRADLE!=="1"){const upload=await uploadAndroidMapping(`${output}/app/build/outputs/mapping/release/mapping.txt`,config.crashReporting.host??"https://bughq.org",androidRelease(config),process.env.BUGHQ_UPLOAD_TOKEN);if(upload.ok)log.info(upload.message);else log.warn(upload.message)}const sourceRevision=Bun.spawnSync(["git","rev-parse","HEAD"],{cwd:projectPath()}).stdout.toString().trim(),generatedConfig=JSON.parse(readFileSync(`${output}/craft.config.json`,"utf8"));writeFileSync(`${output}/stacks-mobile.json`,`${JSON.stringify({schemaVersion:"1.0.0",platform:"android",sourceRevision,source:native?{kind:"native",site:craftConfig.devServerURL}:webAssets?{kind:"bundled",path:webAssets}:{kind:"remote",url:craftConfig.devServerURL,fallback:fallbackWebAssets?{kind:"bundled",path:fallbackWebAssets}:void 0},capabilities:config.capabilities??{},builder:resolveCraftBuilderProvenance(process.env.CRAFT_ANDROID_SRC),craft:generatedConfig},null,2)}
|
|
2
2
|
`);log.success(`Built the Craft Android project in ${output}`);await log.flush();process.exitCode=0;
|
package/dist/build/ios.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{existsSync,readFileSync,writeFileSync}from"node:fs";import{pathToFileURL}from"node:url";import process from"node:process";import{log}from"@stacksjs/cli";import{projectPath,storagePath}from"@stacksjs/path";import{craftBuilderSpecifier,craftBuilderVersion}from"./craft-entry";import{resolveCraftBuilderProvenance}from"./craft-provenance";import{assertCraftSupportsAppShortcuts,assertCraftSupportsCapabilities,assertCraftSupportsCrashReporting,portablePaths,resolveCrashReporting,resolveMobilePath,resolveNativeFonts,assertCraftSupportsNativeFonts,toCraftIosConfig,validateIosMobileConfig,writeIosActivityTypes}from"./ios-config";import{generateXcodeProject,resolveXcodegen}from"./ios-toolchain";import{compileNativeScreens}from"./native-screens";import{writeIosShellSupport}from"./ios-shell";process.exitCode=1;function craftIosEntry(){const explicit=process.env.CRAFT_IOS_SRC;return explicit?pathToFileURL(explicit).href:craftBuilderSpecifier("ios",projectPath())}async function loadCraftIosBuilder(){try{return await import(craftIosEntry())}catch(error){throw Error("Craft iOS builder is unavailable. Install the current craft-native package or set CRAFT_IOS_SRC to packages/ios/src/index.ts in a Craft checkout.",{cause:error})}}const configPath=projectPath("config/mobile.ts");if(!existsSync(configPath))throw Error("Missing config/mobile.ts. Add an ios section before running `buddy build:ios`.");const configModule=await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`),mobile=configModule.default,config=mobile.ios;validateIosMobileConfig(config);if(config.swipeNavigation===!0&&config.swipeBack!=="webview")log.warn("ios.swipeNavigation turns on WebKit's edge swipe alongside the stx router's own. Remove it, or set ios.swipeBack: 'webview' for an app not on the router.");const output=resolveMobilePath(projectPath(),config.output)??storagePath("framework/mobile/ios"),generateProject=process.env.STACKS_IOS_SKIP_XCODEGEN!=="1",xcodegen=generateProject?await resolveXcodegen(projectPath()):null,webAssets=resolveMobilePath(projectPath(),config.webAssets),fallbackWebAssets=resolveMobilePath(projectPath(),config.fallbackWebAssets),crashReporting=resolveCrashReporting(mobile.crashReporting,config.crashReporting),craftConfig=toCraftIosConfig({...config,crashReporting});craftConfig.nativeFonts=resolveNativeFonts(config.nativeFonts,projectPath());craftConfig.appIconPath=resolveMobilePath(projectPath(),config.appIcon);craftConfig.splashImagePath=resolveMobilePath(projectPath(),config.splash?.image);craftConfig.splashImagePathDark=resolveMobilePath(projectPath(),config.splash?.imageDark);const builder=await loadCraftIosBuilder(),nativeBundle=await compileNativeScreens(config,projectPath(),output,void 0,"ios",{routes:builder.nativeRouteBundles===!0,runtime:builder.nativeRouteRuntime===!0,messages:builder.nativeMessages===!0});if(nativeBundle){craftConfig.nativeBundle=nativeBundle.outFile;for(const diagnostic of nativeBundle.diagnostics??[])log.warn(`${diagnostic.file?`${diagnostic.file}: `:""}${diagnostic.message}`);log.info(`Compiled ${Object.keys(config.nativeScreens??{}).length} native screen path(s) into ${nativeBundle.outFile}`)}const craftVersion=craftBuilderVersion(craftIosEntry(),projectPath());assertCraftSupportsNativeFonts(config.nativeFonts,craftVersion?.version);assertCraftSupportsAppShortcuts(config.appShortcuts,craftVersion?.version);if(craftVersion){assertCraftSupportsCapabilities(config.capabilities,craftVersion.version,craftVersion.manifest);assertCraftSupportsCrashReporting(crashReporting,craftVersion.version,craftVersion.manifest)}else log.warn(`Could not read the craft-native version behind ${craftIosEntry()}, so the build cannot check that it supports every capability config/mobile.ts turns on.`);await builder.init({name:config.appName,bundleId:config.bundleId,teamId:config.teamId,output,config:craftConfig});const native=config.renderer==="native";if(native&&!nativeBundle)throw Error("ios.renderer 'native' needs native screens to draw: list them in ios.nativeScreens");await builder.build(native?{output,nativeBundlePath:nativeBundle.outFile,generateProject:!1}:{output,htmlPath:webAssets??fallbackWebAssets,devServer:craftConfig.devServerURL,generateProject:!1});if(xcodegen){generateXcodeProject(xcodegen,output);log.info(`Generated the Xcode project with xcodegen ${xcodegen.version} from Pantry`)}writeIosShellSupport(output,config,void 0,crashReporting);if(crashReporting)log.info(`Crash reporting to ${crashReporting.host??"https://bughq.org"}${crashReporting.uploadSymbols===!1?"":": dSYMs upload from Xcode Cloud when BUGHQ_UPLOAD_TOKEN is set in the workflow"}`);const activityTypes=writeIosActivityTypes(output,mobile);if(activityTypes)log.info(`Declared ${activityTypes.length} activity types, so a tapped Spotlight or Siri entry reaches the app`);const sourceRevision=Bun.spawnSync(["git","rev-parse","HEAD"],{cwd:projectPath()}).stdout.toString().trim(),craftConfigPath=`${output}/craft.config.json`,generatedConfig=portablePaths(JSON.parse(readFileSync(craftConfigPath,"utf8")),output,projectPath());writeFileSync(craftConfigPath,`${JSON.stringify(generatedConfig,null,2)}
|
|
1
|
+
import{existsSync,readFileSync,writeFileSync}from"node:fs";import{pathToFileURL}from"node:url";import process from"node:process";import{log}from"@stacksjs/cli";import{projectPath,storagePath}from"@stacksjs/path";import{craftBuilderSpecifier,craftBuilderVersion}from"./craft-entry";import{resolveCraftBuilderProvenance}from"./craft-provenance";import{assertCraftSupportsAppShortcuts,assertCraftSupportsCapabilities,assertCraftSupportsCrashReporting,portablePaths,resolveCrashReporting,resolveMobilePath,resolveNativeFonts,assertCraftSupportsNativeFonts,toCraftIosConfig,validateIosMobileConfig,writeIosActivityTypes}from"./ios-config";import{generateXcodeProject,resolveXcodegen}from"./ios-toolchain";import{compileNativeScreens}from"./native-screens";import{writeIosShellSupport}from"./ios-shell";import{reportMobileBuildTarget}from"./mobile-target";process.exitCode=1;function craftIosEntry(){const explicit=process.env.CRAFT_IOS_SRC;return explicit?pathToFileURL(explicit).href:craftBuilderSpecifier("ios",projectPath())}async function loadCraftIosBuilder(){try{return await import(craftIosEntry())}catch(error){throw Error("Craft iOS builder is unavailable. Install the current craft-native package or set CRAFT_IOS_SRC to packages/ios/src/index.ts in a Craft checkout.",{cause:error})}}const configPath=projectPath("config/mobile.ts");if(!existsSync(configPath))throw Error("Missing config/mobile.ts. Add an ios section before running `buddy build:ios`.");const configModule=await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`),mobile=configModule.default,config=mobile.ios;validateIosMobileConfig(config);if(config.swipeNavigation===!0&&config.swipeBack!=="webview")log.warn("ios.swipeNavigation turns on WebKit's edge swipe alongside the stx router's own. Remove it, or set ios.swipeBack: 'webview' for an app not on the router.");const output=resolveMobilePath(projectPath(),config.output)??storagePath("framework/mobile/ios"),generateProject=process.env.STACKS_IOS_SKIP_XCODEGEN!=="1",xcodegen=generateProject?await resolveXcodegen(projectPath()):null,webAssets=resolveMobilePath(projectPath(),config.webAssets),fallbackWebAssets=resolveMobilePath(projectPath(),config.fallbackWebAssets),crashReporting=resolveCrashReporting(mobile.crashReporting,config.crashReporting),craftConfig=toCraftIosConfig({...config,crashReporting});craftConfig.nativeFonts=resolveNativeFonts(config.nativeFonts,projectPath());craftConfig.appIconPath=resolveMobilePath(projectPath(),config.appIcon);craftConfig.splashImagePath=resolveMobilePath(projectPath(),config.splash?.image);craftConfig.splashImagePathDark=resolveMobilePath(projectPath(),config.splash?.imageDark);const builder=await loadCraftIosBuilder(),nativeBundle=await compileNativeScreens(config,projectPath(),output,void 0,"ios",{routes:builder.nativeRouteBundles===!0,runtime:builder.nativeRouteRuntime===!0,messages:builder.nativeMessages===!0});if(nativeBundle){craftConfig.nativeBundle=nativeBundle.outFile;for(const diagnostic of nativeBundle.diagnostics??[])log.warn(`${diagnostic.file?`${diagnostic.file}: `:""}${diagnostic.message}`);log.info(`Compiled ${Object.keys(config.nativeScreens??{}).length} native screen path(s) into ${nativeBundle.outFile}`)}const craftVersion=craftBuilderVersion(craftIosEntry(),projectPath());assertCraftSupportsNativeFonts(config.nativeFonts,craftVersion?.version);assertCraftSupportsAppShortcuts(config.appShortcuts,craftVersion?.version);if(craftVersion){assertCraftSupportsCapabilities(config.capabilities,craftVersion.version,craftVersion.manifest);assertCraftSupportsCrashReporting(crashReporting,craftVersion.version,craftVersion.manifest)}else log.warn(`Could not read the craft-native version behind ${craftIosEntry()}, so the build cannot check that it supports every capability config/mobile.ts turns on.`);await builder.init({name:config.appName,bundleId:config.bundleId,teamId:config.teamId,output,config:craftConfig});const native=config.renderer==="native";if(native&&!nativeBundle)throw Error("ios.renderer 'native' needs native screens to draw: list them in ios.nativeScreens");reportMobileBuildTarget({renderer:config.renderer,site:craftConfig.devServerURL,nativeBundle:nativeBundle?.outFile,webAssets,fallbackWebAssets},log);await builder.build(native?{output,nativeBundlePath:nativeBundle.outFile,generateProject:!1}:{output,htmlPath:webAssets??fallbackWebAssets,devServer:craftConfig.devServerURL,generateProject:!1});if(xcodegen){generateXcodeProject(xcodegen,output);log.info(`Generated the Xcode project with xcodegen ${xcodegen.version} from Pantry`)}writeIosShellSupport(output,config,void 0,crashReporting);if(crashReporting)log.info(`Crash reporting to ${crashReporting.host??"https://bughq.org"}${crashReporting.uploadSymbols===!1?"":": dSYMs upload from Xcode Cloud when BUGHQ_UPLOAD_TOKEN is set in the workflow"}`);const activityTypes=writeIosActivityTypes(output,mobile);if(activityTypes)log.info(`Declared ${activityTypes.length} activity types, so a tapped Spotlight or Siri entry reaches the app`);const sourceRevision=Bun.spawnSync(["git","rev-parse","HEAD"],{cwd:projectPath()}).stdout.toString().trim(),craftConfigPath=`${output}/craft.config.json`,generatedConfig=portablePaths(JSON.parse(readFileSync(craftConfigPath,"utf8")),output,projectPath());writeFileSync(craftConfigPath,`${JSON.stringify(generatedConfig,null,2)}
|
|
2
2
|
`);writeFileSync(`${output}/stacks-mobile.json`,`${JSON.stringify(portablePaths({schemaVersion:"1.0.0",platform:"ios",sourceRevision,source:native?{kind:"native",site:craftConfig.devServerURL}:webAssets?{kind:"bundled",path:webAssets}:{kind:"remote",url:craftConfig.devServerURL,fallback:fallbackWebAssets?{kind:"bundled",path:fallbackWebAssets}:void 0},capabilities:config.capabilities??{},builder:resolveCraftBuilderProvenance(process.env.CRAFT_IOS_SRC),craft:generatedConfig},output,projectPath()),null,2)}
|
|
3
3
|
`);log.success(`Built the Craft iOS project in ${output}`);await log.flush();process.exitCode=0;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Describe the resolved build inputs, never credentials embedded in a URL. */
|
|
2
|
+
export declare function reportMobileBuildTarget(target: MobileBuildTarget, log: { info: (message: string) => unknown }): void;
|
|
3
|
+
declare interface MobileBuildTarget {
|
|
4
|
+
renderer?: 'web' | 'native'
|
|
5
|
+
site?: string
|
|
6
|
+
nativeBundle?: string
|
|
7
|
+
webAssets?: string
|
|
8
|
+
fallbackWebAssets?: string
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function reportMobileBuildTarget(target,log){const site=target.site?new URL(target.site):void 0;if(site){site.username="";site.password="";site.search="";site.hash=""}if(target.renderer==="native"){log.info(`Native screens: ${target.nativeBundle}`);if(site)log.info(`API site: ${site}`)}else if(target.webAssets)log.info(`Bundled web source: ${target.webAssets}`);else{if(site)log.info(`Web source: ${site}`);if(target.fallbackWebAssets)log.info(`Offline web fallback: ${target.fallbackWebAssets}`)}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{existsSync}from"node:fs";import{join}from"node:path";import{pathToFileURL}from"node:url";export async function runBeforeDeployHook(cwd,context){const file=[".ts",".js",".mts",".mjs",".cts",".cjs"].map((extension)=>join(cwd,`cloud/deploy-script${extension}`)).find(existsSync);if(!file)return!1;const module=await import(pathToFileURL(file).href),owner=Object.hasOwn(module,"beforeDeploy")?module:module.default;if(!owner||!Object.hasOwn(owner,"beforeDeploy"))return!1;const hook=owner.beforeDeploy;if(typeof hook!=="function")throw Error("cloud/deploy-script beforeDeploy must be a function");await hook(context);return!0}
|
package/dist/deploy/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync,readFileSync,statSync}from"node:fs";import process from"node:process";import{runCommand,spinner}from"@stacksjs/cli";import{config}from"@stacksjs/config";import{path as p}from"@stacksjs/path";import{storage}from"@stacksjs/storage";import{ExitCode}from"@stacksjs/types";const log={debug:(...args)=>{if(process.argv.includes("--verbose")||process.argv.includes("-v"))console.log("\uD83D\uDD0D",...args)},success:(...args)=>console.log("\u2713",...args),error:(...args)=>console.error("\u2717",...args)},MAIL_PACKAGE_DOMAIN="github.com/mail-os/mail",MAIL_PACKAGE_SPEC=`${MAIL_PACKAGE_DOMAIN}@0.1.0`,MAIL_TARGET_PLATFORM="linux-x86_64",MAIL_BINARY_NAMES=["mail","mail-x86_64-linux","mail-x86_64-linux-gnu"],isVerbose=process.argv.includes("--verbose")||process.argv.includes("-v");async function collectMatchingFiles(root,names,maxDepth=8){const{existsSync,readdirSync,statSync}=await import("node:fs"),{join,basename}=await import("node:path"),nameSet=new Set(names),matches=[];if(!existsSync(root))return matches;function walk(dir,depth){if(depth>maxDepth)return;for(const entry of readdirSync(dir)){if(entry===".git"||entry==="node_modules")continue;const fullPath=join(dir,entry),stat=statSync(fullPath);if(stat.isDirectory())walk(fullPath,depth+1);else if(stat.isFile()&&nameSet.has(basename(fullPath)))matches.push(fullPath)}}walk(root,0);return matches}async function isElfBinary(filePath){const{readFileSync}=await import("node:fs"),header=readFileSync(filePath).slice(0,4);return header[0]===127&&header[1]===69&&header[2]===76&&header[3]===70}async function resolvePantryInstallCommand(){const{existsSync}=await import("node:fs"),localPantryCli=`${process.env.HOME}/Code/Tools/pantry/packages/ts-pantry/bin/cli.ts`;if(existsSync(localPantryCli))return{command:"bun",args:[localPantryCli]};const projectPantry=p.projectPath("pantry/.bin/pantry");if(existsSync(projectPantry))return{command:projectPantry,args:[]};const globalPantry=`${process.env.HOME}/.local/share/pantry/global/bin/pantry`;if(existsSync(globalPantry))return{command:globalPantry,args:[]};return{command:"pantry",args:[]}}async function installMailBinaryWithPantry(){const{execFileSync}=await import("node:child_process"),installDir=p.projectPath("pantry"),pantry=await resolvePantryInstallCommand();execFileSync(pantry.command,[...pantry.args,"install",MAIL_PACKAGE_SPEC,"--install-dir",installDir,"--platform",MAIL_TARGET_PLATFORM,"--quiet"],{cwd:p.projectPath(),stdio:isVerbose?"inherit":"pipe",env:process.env})}async function findPantryMailBinary(){const{existsSync}=await import("node:fs"),{join}=await import("node:path"),pantryRoots=[p.projectPath("pantry"),`${process.env.HOME}/.local/share/pantry`],directCandidates=[...MAIL_BINARY_NAMES.map((name)=>p.projectPath(`pantry/.bin/${name}`)),...MAIL_BINARY_NAMES.map((name)=>join(`${process.env.HOME}/.local/share/pantry/global/bin`,name))];for(const candidate of directCandidates)if(existsSync(candidate)&&await isElfBinary(candidate))return candidate;for(const root of pantryRoots){const candidates=await collectMatchingFiles(root,MAIL_BINARY_NAMES);for(const candidate of candidates)if(await isElfBinary(candidate))return candidate}return null}const MIME_TYPES={".html":"text/html",".css":"text/css",".js":"application/javascript",".json":"application/json",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".svg":"image/svg+xml",".ico":"image/x-icon",".woff":"font/woff",".woff2":"font/woff2",".ttf":"font/ttf",".eot":"application/vnd.ms-fontobject",".otf":"font/otf",".webp":"image/webp",".mp4":"video/mp4",".webm":"video/webm",".mp3":"audio/mpeg",".wav":"audio/wav",".pdf":"application/pdf",".xml":"application/xml",".txt":"text/plain"};function getMimeType(filePath){const dotIndex=filePath.lastIndexOf("."),ext=dotIndex>=0?filePath.slice(dotIndex).toLowerCase():"";return MIME_TYPES[ext]||"application/octet-stream"}async function traverseDirectory(dir,callback,prefix="",skipPatterns=[".DS_Store"]){const{readdirSync,statSync}=await import("node:fs"),{join}=await import("node:path"),items=readdirSync(dir);for(const item of items){if(skipPatterns.includes(item))continue;const filePath=join(dir,item),key=prefix?`${prefix}/${item}`:item;if(statSync(filePath).isDirectory())await traverseDirectory(filePath,callback,key,skipPatterns);else await callback(filePath,key)}}async function withS3Retry(fn,label="s3 operation"){const maxRetries=3;for(let attempt=0;attempt<=maxRetries;attempt++)try{return await fn()}catch(error){if(attempt>=maxRetries)throw error;const delay=Math.min(500*2**attempt+Math.random()*200,5000);if(isVerbose)log.debug(` Retrying ${label} (attempt ${attempt+2}/${maxRetries+1}) in ${Math.round(delay)}ms...`);await new Promise((resolve)=>setTimeout(resolve,delay))}throw Error(`${label} failed after ${maxRetries+1} attempts`)}function normalizeArray(value){if(!value)return[];return Array.isArray(value)?value:[value]}function buildCloudFrontXmlElement(name,value,indent=""){if(value===null||value===void 0)return"";if(typeof value==="boolean")return`${indent}<${name}>${value}</${name}>
|
|
1
|
+
import{existsSync,readFileSync,statSync}from"node:fs";import process from"node:process";import{runCommand,spinner}from"@stacksjs/cli";import{config}from"@stacksjs/config";import{path as p}from"@stacksjs/path";import{storage}from"@stacksjs/storage";import{ExitCode}from"@stacksjs/types";import{assertPublicationAllowed}from"../publication-preflight";import{runBeforeDeployHook}from"./hooks";await assertPublicationAllowed({cwd:p.projectPath(),channel:"web"});const log={debug:(...args)=>{if(process.argv.includes("--verbose")||process.argv.includes("-v"))console.log("\uD83D\uDD0D",...args)},success:(...args)=>console.log("\u2713",...args),error:(...args)=>console.error("\u2717",...args)},MAIL_PACKAGE_DOMAIN="github.com/mail-os/mail",MAIL_PACKAGE_SPEC=`${MAIL_PACKAGE_DOMAIN}@0.1.0`,MAIL_TARGET_PLATFORM="linux-x86_64",MAIL_BINARY_NAMES=["mail","mail-x86_64-linux","mail-x86_64-linux-gnu"],isVerbose=process.argv.includes("--verbose")||process.argv.includes("-v");async function collectMatchingFiles(root,names,maxDepth=8){const{existsSync,readdirSync,statSync}=await import("node:fs"),{join,basename}=await import("node:path"),nameSet=new Set(names),matches=[];if(!existsSync(root))return matches;function walk(dir,depth){if(depth>maxDepth)return;for(const entry of readdirSync(dir)){if(entry===".git"||entry==="node_modules")continue;const fullPath=join(dir,entry),stat=statSync(fullPath);if(stat.isDirectory())walk(fullPath,depth+1);else if(stat.isFile()&&nameSet.has(basename(fullPath)))matches.push(fullPath)}}walk(root,0);return matches}async function isElfBinary(filePath){const{readFileSync}=await import("node:fs"),header=readFileSync(filePath).slice(0,4);return header[0]===127&&header[1]===69&&header[2]===76&&header[3]===70}async function resolvePantryInstallCommand(){const{existsSync}=await import("node:fs"),localPantryCli=`${process.env.HOME}/Code/Tools/pantry/packages/ts-pantry/bin/cli.ts`;if(existsSync(localPantryCli))return{command:"bun",args:[localPantryCli]};const projectPantry=p.projectPath("pantry/.bin/pantry");if(existsSync(projectPantry))return{command:projectPantry,args:[]};const globalPantry=`${process.env.HOME}/.local/share/pantry/global/bin/pantry`;if(existsSync(globalPantry))return{command:globalPantry,args:[]};return{command:"pantry",args:[]}}async function installMailBinaryWithPantry(){const{execFileSync}=await import("node:child_process"),installDir=p.projectPath("pantry"),pantry=await resolvePantryInstallCommand();execFileSync(pantry.command,[...pantry.args,"install",MAIL_PACKAGE_SPEC,"--install-dir",installDir,"--platform",MAIL_TARGET_PLATFORM,"--quiet"],{cwd:p.projectPath(),stdio:isVerbose?"inherit":"pipe",env:process.env})}async function findPantryMailBinary(){const{existsSync}=await import("node:fs"),{join}=await import("node:path"),pantryRoots=[p.projectPath("pantry"),`${process.env.HOME}/.local/share/pantry`],directCandidates=[...MAIL_BINARY_NAMES.map((name)=>p.projectPath(`pantry/.bin/${name}`)),...MAIL_BINARY_NAMES.map((name)=>join(`${process.env.HOME}/.local/share/pantry/global/bin`,name))];for(const candidate of directCandidates)if(existsSync(candidate)&&await isElfBinary(candidate))return candidate;for(const root of pantryRoots){const candidates=await collectMatchingFiles(root,MAIL_BINARY_NAMES);for(const candidate of candidates)if(await isElfBinary(candidate))return candidate}return null}const MIME_TYPES={".html":"text/html",".css":"text/css",".js":"application/javascript",".json":"application/json",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".svg":"image/svg+xml",".ico":"image/x-icon",".woff":"font/woff",".woff2":"font/woff2",".ttf":"font/ttf",".eot":"application/vnd.ms-fontobject",".otf":"font/otf",".webp":"image/webp",".mp4":"video/mp4",".webm":"video/webm",".mp3":"audio/mpeg",".wav":"audio/wav",".pdf":"application/pdf",".xml":"application/xml",".txt":"text/plain"};function getMimeType(filePath){const dotIndex=filePath.lastIndexOf("."),ext=dotIndex>=0?filePath.slice(dotIndex).toLowerCase():"";return MIME_TYPES[ext]||"application/octet-stream"}async function traverseDirectory(dir,callback,prefix="",skipPatterns=[".DS_Store"]){const{readdirSync,statSync}=await import("node:fs"),{join}=await import("node:path"),items=readdirSync(dir);for(const item of items){if(skipPatterns.includes(item))continue;const filePath=join(dir,item),key=prefix?`${prefix}/${item}`:item;if(statSync(filePath).isDirectory())await traverseDirectory(filePath,callback,key,skipPatterns);else await callback(filePath,key)}}async function withS3Retry(fn,label="s3 operation"){const maxRetries=3;for(let attempt=0;attempt<=maxRetries;attempt++)try{return await fn()}catch(error){if(attempt>=maxRetries)throw error;const delay=Math.min(500*2**attempt+Math.random()*200,5000);if(isVerbose)log.debug(` Retrying ${label} (attempt ${attempt+2}/${maxRetries+1}) in ${Math.round(delay)}ms...`);await new Promise((resolve)=>setTimeout(resolve,delay))}throw Error(`${label} failed after ${maxRetries+1} attempts`)}function normalizeArray(value){if(!value)return[];return Array.isArray(value)?value:[value]}function buildCloudFrontXmlElement(name,value,indent=""){if(value===null||value===void 0)return"";if(typeof value==="boolean")return`${indent}<${name}>${value}</${name}>
|
|
2
2
|
`;if(typeof value==="number"||typeof value==="string")return`${indent}<${name}>${value}</${name}>
|
|
3
3
|
`;if(Array.isArray(value))return value.map((item)=>buildCloudFrontXmlElement(name,item,indent)).join("");if(typeof value==="object"){if(name.startsWith("@_")||name==="?xml")return"";let children="";for(const[key,val]of Object.entries(value))if(!key.startsWith("@_")&&key!=="#text")children+=buildCloudFrontXmlElement(key,val,`${indent} `);if(value["#text"]!==void 0)return`${indent}<${name}>${value["#text"]}</${name}>
|
|
4
4
|
`;return`${indent}<${name}>
|
|
@@ -6,9 +6,9 @@ ${children}${indent}</${name}>
|
|
|
6
6
|
`}return""}function buildCloudFrontDistributionConfigXml(config){return`<?xml version="1.0" encoding="UTF-8"?>
|
|
7
7
|
<DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
|
|
8
8
|
${Object.entries(config).filter(([key])=>!key.startsWith("@_")).map(([key,val])=>buildCloudFrontXmlElement(key,val," ")).join("")}</DistributionConfig>
|
|
9
|
-
`}function firewallOpenCommand(ports){const uniquePorts=[...new Set(ports)].filter((port)=>Number.isFinite(port)&&port>0).sort((a,b)=>a-b);if(uniquePorts.length===0)return"true";return`if command -v firewall-cmd >/dev/null 2>&1; then firewall-cmd --permanent ${uniquePorts.map((port)=>`--add-port=${port}/tcp`).join(" ")} && firewall-cmd --reload; fi`}async function ensureSecurityGroupPort(ec2,securityGroupId,port){if(!securityGroupId)return;if(((await ec2.describeSecurityGroups({GroupIds:[securityGroupId]}))?.SecurityGroups?.[0]?.IpPermissions||[]).some((permission)=>{return permission.IpProtocol==="tcp"&&Number(permission.FromPort)<=port&&Number(permission.ToPort)>=port&&normalizeArray(permission.IpRanges).some((range)=>range.CidrIp==="0.0.0.0/0")}))return;try{await ec2.authorizeSecurityGroupIngress({GroupId:securityGroupId,IpPermissions:[{IpProtocol:"tcp",FromPort:port,ToPort:port,IpRanges:[{CidrIp:"0.0.0.0/0",Description:`Stacks API port ${port}`}]}]})}catch(error){if(!String(error?.message||"").includes("InvalidPermission.Duplicate"))throw error}}async function ensureCloudFrontApiOriginPort(awsClient,distributionId,port){if(!distributionId)return;const getResult=await awsClient.request({service:"cloudfront",region:"us-east-1",method:"GET",path:`/2020-05-31/distribution/${distributionId}/config`,returnHeaders:!0}),etag=getResult.headers?.etag||getResult.headers?.ETag||"",currentConfig=getResult.body?.DistributionConfig||getResult.DistributionConfig||getResult.body;if(!etag||!currentConfig?.Origins?.Items)return;const apiOrigin=normalizeArray(currentConfig.Origins.Items.Origin).find((origin)=>String(origin.Id||"").includes("-api"));if(!apiOrigin?.CustomOriginConfig)return;if(Number(apiOrigin.CustomOriginConfig.HTTPPort)===port)return;apiOrigin.CustomOriginConfig.HTTPPort=port;await awsClient.request({service:"cloudfront",region:"us-east-1",method:"PUT",path:`/2020-05-31/distribution/${distributionId}/config`,body:buildCloudFrontDistributionConfigXml(currentConfig),headers:{"Content-Type":"application/xml","If-Match":etag}})}const frameworkBuildSpinner=spinner("Building framework...");frameworkBuildSpinner.start();const frameworkBuilt=await runCommand("bun run build",{cwd:p.frameworkPath("core"),quiet:!1});if(frameworkBuilt.isErr){frameworkBuildSpinner.fail("Framework build failed - see the output above");process.exit(ExitCode.FatalError)}frameworkBuildSpinner.succeed("Framework built");const docsDir=p.projectPath("docs"),docsBuildDir=p.projectPath("dist/docs/.bunpress");if(storage.hasFiles(docsDir)){const docsSpinner=spinner("Building documentation with BunPress...");docsSpinner.start();try{const{rm}=await import("node:fs/promises"),localBunpressCli=`${process.env.HOME}/Code/Tools/bunpress/packages/bunpress/bin/cli.ts`,installedBunpressCli=p.projectPath("node_modules/@stacksjs/bunpress/dist/bin/cli.js"),hasLocalBunpress=await Bun.file(localBunpressCli).exists(),hasInstalledBunpress=await Bun.file(installedBunpressCli).exists(),command=hasLocalBunpress?`bun ${localBunpressCli} build --dir ${docsDir} --outdir ${p.projectPath("dist/docs")}`:hasInstalledBunpress?`bun ${installedBunpressCli} build --dir ${docsDir} --outdir ${p.projectPath("dist/docs")}`:"bunx @stacksjs/bunpress@0.1.5 build --dir ./docs --outdir ./dist/docs";await rm(docsBuildDir,{recursive:!0,force:!0});await runCommand(command,{cwd:p.projectPath(),quiet:!isVerbose});docsSpinner.succeed(`Documentation built with BunPress${hasLocalBunpress?" (local checkout)":""}`)}catch(docsError){docsSpinner.stop();console.log(`\u26A0 Documentation build skipped: ${docsError.message}`);if(isVerbose)log.debug("To build docs manually: cd ~/Code/Tools/bunpress && bun packages/bunpress/bin/cli.ts build --dir /path/to/docs --outdir /path/to/dist/docs")}}else if(isVerbose)log.debug("No docs directory found, skipping documentation build");const blogDistExists=storage.hasFiles(p.projectPath("dist/blog")),blogFontAssetsExist=["CampmateScript-Regular.woff2","SequoiaSans-Regular.woff2","Switchback-Regular.woff2"].every((file)=>existsSync(p.projectPath(`dist/blog/assets/fonts/nps/${file}`))),blogImageAssetsExist=["topography.svg","park-ridge.svg"].every((file)=>existsSync(p.projectPath(`dist/blog/assets/images/${file}`))),blogIndexPath=p.projectPath("dist/blog/index.html"),blogIndexHtml=existsSync(blogIndexPath)?readFileSync(blogIndexPath,"utf8"):"",blogHtmlUsesCurrentFontFormat=blogIndexHtml.length>0&&!blogIndexHtml.includes("woff2-variations"),blogHtmlUsesCurrentParkTheme=blogIndexHtml.includes("--on-primary")&&blogIndexHtml.includes("--newsletter-bg"),blogHtmlUsesCurrentParkImages=blogIndexHtml.includes("topography.svg")&&blogIndexHtml.includes("park-ridge.svg"),blogHtmlOmitsRetiredDecorations=!blogIndexHtml.includes("stamp-trail")&&!blogIndexHtml.includes("blog-sign-tree.svg"),blogBuilderPath=p.frameworkPath("core/cms/src/build.ts"),blogConfigPath=p.projectPath("config/blog.ts"),blogHtmlIsFresh=existsSync(blogIndexPath)&&[blogBuilderPath,blogConfigPath].filter((path)=>existsSync(path)).every((path)=>statSync(blogIndexPath).mtimeMs>=statSync(path).mtimeMs);if(!blogDistExists||!blogFontAssetsExist||!blogImageAssetsExist||!blogHtmlUsesCurrentFontFormat||!blogHtmlUsesCurrentParkTheme||!blogHtmlUsesCurrentParkImages||!blogHtmlOmitsRetiredDecorations||!blogHtmlIsFresh){const blogBuildSpinner=spinner("Building blog...");blogBuildSpinner.start();try{const blogConfig=(await import(p.projectPath("config/blog"))).default,{buildBlogSite}=await import(p.frameworkPath("core/cms/src/build"));await buildBlogSite({config:blogConfig,outDir:p.projectPath("dist/blog")});blogBuildSpinner.succeed("Blog built successfully")}catch(blogBuildError){blogBuildSpinner.stop();console.log(`\u26A0 Blog build skipped: ${blogBuildError.message}`);if(isVerbose)log.debug(`Blog build error: ${blogBuildError.stack}`)}}else if(isVerbose)log.debug("Pre-built blog found at dist/blog, skipping build");if(isVerbose)log.debug("Skipping views build (vite-config not configured)");const earlyCloudConfig=await import(p.projectPath("config/cloud")),earlyDeploymentMode=earlyCloudConfig?.tsCloud?.mode||"server";if(earlyDeploymentMode==="serverless"){const serverSpinner=spinner("Building server...");serverSpinner.start();if((await runCommand("bun build.ts",{cwd:p.frameworkPath("server"),quiet:!isVerbose})).isErr){serverSpinner.fail("Server build failed - rerun with --verbose to see its output");process.exit(ExitCode.FatalError)}serverSpinner.succeed("Server built");const packageSpinner=spinner("Packaging for deployment...");packageSpinner.start();if((await runCommand("bun zip.ts",{cwd:p.corePath("cloud"),quiet:!isVerbose})).isErr){packageSpinner.fail("Packaging failed - rerun with --verbose to see its output");process.exit(ExitCode.FatalError)}packageSpinner.succeed("Package ready")}else if(isVerbose)log.debug("Skipping server build and packaging (server mode)");if(!process.env.AWS_ACCESS_KEY_ID||!process.env.AWS_SECRET_ACCESS_KEY){const{existsSync,readFileSync}=await import("node:fs"),environment=process.env.APP_ENV||process.env.NODE_ENV||"production",envFiles=[p.projectPath(`.env.${environment}`),p.projectPath(".env")];for(const envPath of envFiles)if(existsSync(envPath)){const content=readFileSync(envPath,"utf-8");for(const line of content.split(`
|
|
9
|
+
`}function firewallOpenCommand(ports){const uniquePorts=[...new Set(ports)].filter((port)=>Number.isFinite(port)&&port>0).sort((a,b)=>a-b);if(uniquePorts.length===0)return"true";return`if command -v firewall-cmd >/dev/null 2>&1; then firewall-cmd --permanent ${uniquePorts.map((port)=>`--add-port=${port}/tcp`).join(" ")} && firewall-cmd --reload; fi`}async function ensureSecurityGroupPort(ec2,securityGroupId,port){if(!securityGroupId)return;if(((await ec2.describeSecurityGroups({GroupIds:[securityGroupId]}))?.SecurityGroups?.[0]?.IpPermissions||[]).some((permission)=>{return permission.IpProtocol==="tcp"&&Number(permission.FromPort)<=port&&Number(permission.ToPort)>=port&&normalizeArray(permission.IpRanges).some((range)=>range.CidrIp==="0.0.0.0/0")}))return;try{await ec2.authorizeSecurityGroupIngress({GroupId:securityGroupId,IpPermissions:[{IpProtocol:"tcp",FromPort:port,ToPort:port,IpRanges:[{CidrIp:"0.0.0.0/0",Description:`Stacks API port ${port}`}]}]})}catch(error){if(!String(error?.message||"").includes("InvalidPermission.Duplicate"))throw error}}async function ensureCloudFrontApiOriginPort(awsClient,distributionId,port){if(!distributionId)return;const getResult=await awsClient.request({service:"cloudfront",region:"us-east-1",method:"GET",path:`/2020-05-31/distribution/${distributionId}/config`,returnHeaders:!0}),etag=getResult.headers?.etag||getResult.headers?.ETag||"",currentConfig=getResult.body?.DistributionConfig||getResult.DistributionConfig||getResult.body;if(!etag||!currentConfig?.Origins?.Items)return;const apiOrigin=normalizeArray(currentConfig.Origins.Items.Origin).find((origin)=>String(origin.Id||"").includes("-api"));if(!apiOrigin?.CustomOriginConfig)return;if(Number(apiOrigin.CustomOriginConfig.HTTPPort)===port)return;apiOrigin.CustomOriginConfig.HTTPPort=port;await awsClient.request({service:"cloudfront",region:"us-east-1",method:"PUT",path:`/2020-05-31/distribution/${distributionId}/config`,body:buildCloudFrontDistributionConfigXml(currentConfig),headers:{"Content-Type":"application/xml","If-Match":etag}})}const hookEnvironment=process.env.APP_ENV||process.env.NODE_ENV||"production",{stacksCloudRegion:hookRegion}=await import("@stacksjs/cloud");await runBeforeDeployHook(p.projectPath(),{environment:hookEnvironment,region:await hookRegion(void 0,hookEnvironment)});const frameworkBuildSpinner=spinner("Building framework...");frameworkBuildSpinner.start();const frameworkBuilt=await runCommand("bun run build",{cwd:p.frameworkPath("core"),quiet:!1});if(frameworkBuilt.isErr){frameworkBuildSpinner.fail("Framework build failed - see the output above");process.exit(ExitCode.FatalError)}frameworkBuildSpinner.succeed("Framework built");const docsDir=p.projectPath("docs"),docsBuildDir=p.projectPath("dist/docs/.bunpress");if(storage.hasFiles(docsDir)){const docsSpinner=spinner("Building documentation with BunPress...");docsSpinner.start();try{const{rm}=await import("node:fs/promises"),localBunpressCli=`${process.env.HOME}/Code/Tools/bunpress/packages/bunpress/bin/cli.ts`,installedBunpressCli=p.projectPath("node_modules/@stacksjs/bunpress/dist/bin/cli.js"),hasLocalBunpress=await Bun.file(localBunpressCli).exists(),hasInstalledBunpress=await Bun.file(installedBunpressCli).exists(),command=hasLocalBunpress?`bun ${localBunpressCli} build --dir ${docsDir} --outdir ${p.projectPath("dist/docs")}`:hasInstalledBunpress?`bun ${installedBunpressCli} build --dir ${docsDir} --outdir ${p.projectPath("dist/docs")}`:"bunx @stacksjs/bunpress@0.1.5 build --dir ./docs --outdir ./dist/docs";await rm(docsBuildDir,{recursive:!0,force:!0});await runCommand(command,{cwd:p.projectPath(),quiet:!isVerbose});docsSpinner.succeed(`Documentation built with BunPress${hasLocalBunpress?" (local checkout)":""}`)}catch(docsError){docsSpinner.stop();console.log(`\u26A0 Documentation build skipped: ${docsError.message}`);if(isVerbose)log.debug("To build docs manually: cd ~/Code/Tools/bunpress && bun packages/bunpress/bin/cli.ts build --dir /path/to/docs --outdir /path/to/dist/docs")}}else if(isVerbose)log.debug("No docs directory found, skipping documentation build");const blogDistExists=storage.hasFiles(p.projectPath("dist/blog")),blogFontAssetsExist=["CampmateScript-Regular.woff2","SequoiaSans-Regular.woff2","Switchback-Regular.woff2"].every((file)=>existsSync(p.projectPath(`dist/blog/assets/fonts/nps/${file}`))),blogImageAssetsExist=["topography.svg","park-ridge.svg"].every((file)=>existsSync(p.projectPath(`dist/blog/assets/images/${file}`))),blogIndexPath=p.projectPath("dist/blog/index.html"),blogIndexHtml=existsSync(blogIndexPath)?readFileSync(blogIndexPath,"utf8"):"",blogHtmlUsesCurrentFontFormat=blogIndexHtml.length>0&&!blogIndexHtml.includes("woff2-variations"),blogHtmlUsesCurrentParkTheme=blogIndexHtml.includes("--on-primary")&&blogIndexHtml.includes("--newsletter-bg"),blogHtmlUsesCurrentParkImages=blogIndexHtml.includes("topography.svg")&&blogIndexHtml.includes("park-ridge.svg"),blogHtmlOmitsRetiredDecorations=!blogIndexHtml.includes("stamp-trail")&&!blogIndexHtml.includes("blog-sign-tree.svg"),blogBuilderPath=p.frameworkPath("core/cms/src/build.ts"),blogConfigPath=p.projectPath("config/blog.ts"),blogHtmlIsFresh=existsSync(blogIndexPath)&&[blogBuilderPath,blogConfigPath].filter((path)=>existsSync(path)).every((path)=>statSync(blogIndexPath).mtimeMs>=statSync(path).mtimeMs);if(!blogDistExists||!blogFontAssetsExist||!blogImageAssetsExist||!blogHtmlUsesCurrentFontFormat||!blogHtmlUsesCurrentParkTheme||!blogHtmlUsesCurrentParkImages||!blogHtmlOmitsRetiredDecorations||!blogHtmlIsFresh){const blogBuildSpinner=spinner("Building blog...");blogBuildSpinner.start();try{const blogConfig=(await import(p.projectPath("config/blog"))).default,{buildBlogSite}=await import(p.frameworkPath("core/cms/src/build"));await buildBlogSite({config:blogConfig,outDir:p.projectPath("dist/blog")});blogBuildSpinner.succeed("Blog built successfully")}catch(blogBuildError){blogBuildSpinner.stop();console.log(`\u26A0 Blog build skipped: ${blogBuildError.message}`);if(isVerbose)log.debug(`Blog build error: ${blogBuildError.stack}`)}}else if(isVerbose)log.debug("Pre-built blog found at dist/blog, skipping build");if(isVerbose)log.debug("Skipping views build (vite-config not configured)");const earlyCloudConfig=await import(p.projectPath("config/cloud")),earlyDeploymentMode=earlyCloudConfig?.tsCloud?.mode||"server";if(earlyDeploymentMode==="serverless"){const serverSpinner=spinner("Building server...");serverSpinner.start();if((await runCommand("bun build.ts",{cwd:p.frameworkPath("server"),quiet:!isVerbose})).isErr){serverSpinner.fail("Server build failed - rerun with --verbose to see its output");process.exit(ExitCode.FatalError)}serverSpinner.succeed("Server built");const packageSpinner=spinner("Packaging for deployment...");packageSpinner.start();if((await runCommand("bun zip.ts",{cwd:p.corePath("cloud"),quiet:!isVerbose})).isErr){packageSpinner.fail("Packaging failed - rerun with --verbose to see its output");process.exit(ExitCode.FatalError)}packageSpinner.succeed("Package ready")}else if(isVerbose)log.debug("Skipping server build and packaging (server mode)");if(!process.env.AWS_ACCESS_KEY_ID||!process.env.AWS_SECRET_ACCESS_KEY){const{existsSync,readFileSync}=await import("node:fs"),environment=process.env.APP_ENV||process.env.NODE_ENV||"production",envFiles=[p.projectPath(`.env.${environment}`),p.projectPath(".env")];for(const envPath of envFiles)if(existsSync(envPath)){const content=readFileSync(envPath,"utf-8");for(const line of content.split(`
|
|
10
10
|
`)){const trimmed=line.trim();if(trimmed.startsWith("#")||!trimmed.includes("="))continue;const eqIndex=trimmed.indexOf("="),key=trimmed.slice(0,eqIndex).trim();let value=trimmed.slice(eqIndex+1).trim();if(value.startsWith('"')&&value.endsWith('"')||value.startsWith("'")&&value.endsWith("'"))value=value.slice(1,-1);if(key==="AWS_ACCESS_KEY_ID"&&value)process.env.AWS_ACCESS_KEY_ID=value;else if(key==="AWS_SECRET_ACCESS_KEY"&&value)process.env.AWS_SECRET_ACCESS_KEY=value;else if(key==="AWS_REGION"&&value&&!process.env.AWS_REGION)process.env.AWS_REGION=value;else if(key==="AWS_ACCOUNT_ID"&&value&&!process.env.AWS_ACCOUNT_ID)process.env.AWS_ACCOUNT_ID=value;else if(key.startsWith("MAIL_PASSWORD_")&&value&&!process.env[key])process.env[key]=value}if(isVerbose)log.debug(`Loaded AWS credentials from ${envPath}`);break}}try{const{deployStack,deployFrontend:_deployFrontend}=await import("../../deploy"),environment=process.env.APP_ENV||process.env.NODE_ENV||"production",{stacksCloudName,stacksCloudRegion}=await import("@stacksjs/cloud"),region=await stacksCloudRegion(void 0,environment),cloudStackName=await stacksCloudName(),cloudConfigModule=await import(p.projectPath("config/cloud")),deploymentMode=cloudConfigModule?.tsCloud?.mode||"server",projectName=cloudConfigModule?.tsCloud?.project?.name||cloudConfigModule?.tsCloud?.project?.slug||"stacks";if(deploymentMode==="serverless"){console.log("");console.log("Serverless deployment mode detected");console.log("Building and pushing container image...");console.log("");const accountId=process.env.AWS_ACCOUNT_ID||await getAwsAccountId(region);if(!accountId||accountId.length<10)throw Error(`Invalid AWS Account ID: "${accountId||"(empty)"}"
|
|
11
|
-
Set AWS_ACCOUNT_ID in your .env file or ensure valid AWS credentials are configured.`);const ecrRepository=`${accountId}.dkr.ecr.${region}.amazonaws.com/${projectName}-${environment}-api`,imageTag=`${ecrRepository}:latest`;if(isVerbose)log.debug(`ECR repository: ${ecrRepository}`);if(isVerbose)log.debug("ECR credentials will be validated before push...");const buildSpinner=spinner("Building Docker image...");buildSpinner.start();try{const{exec}=await import("node:child_process"),{promisify}=await import("node:util"),execAsync=promisify(exec),buildCmd=`docker build --platform linux/amd64 -t ${imageTag} -f Dockerfile .`,{stdout,stderr}=await execAsync(buildCmd,{cwd:p.projectPath(),env:process.env,maxBuffer:10485760,timeout:900000});if(isVerbose&&stdout)log.debug(stdout);if(isVerbose&&stderr)log.debug(stderr);buildSpinner.succeed(`Docker image built: ${imageTag}`)}catch(error){buildSpinner.fail("Docker build failed");if(isVerbose)log.debug(`Docker build error: ${error.message}`);throw error}const repoSpinner=spinner("Checking ECR repository...");repoSpinner.start();try{const{ECRClient}=await import("@stacksjs/ts-cloud"),ecr=new ECRClient(region),repoName=`${projectName}-${environment}-api`;try{await ecr.describeRepositories({repositoryNames:[repoName]});repoSpinner.succeed(`ECR repository exists: ${repoName}`)}catch(err){if(err.code==="RepositoryNotFoundException"||err.message?.includes("RepositoryNotFoundException")){repoSpinner.text=`Creating ECR repository: ${repoName}...`;await ecr.createRepository({repositoryName:repoName,imageScanningConfiguration:{scanOnPush:!0},imageTagMutability:"MUTABLE"});repoSpinner.succeed(`ECR repository created: ${repoName}`)}else throw err}}catch(error){repoSpinner.fail("ECR repository check failed");if(isVerbose)log.debug(`ECR repository error: ${error.message}`);throw error}const authSpinner=spinner("Authenticating to ECR...");authSpinner.start();try{const{ECRClient}=await import("@stacksjs/ts-cloud"),token=(await new ECRClient(region).getAuthorizationToken()).authorizationData?.[0];if(!token?.authorizationToken)throw Error("No authorization token received from ECR");const decoded=Buffer.from(token.authorizationToken,"base64").toString("utf-8"),colonIndex=decoded.indexOf(":");if(colonIndex===-1)throw Error("ECR authorization token has unexpected format (missing colon separator)");const password=decoded.slice(colonIndex+1),loginResult=Bun.spawnSync(["docker","login","--username","AWS","--password-stdin",`${accountId}.dkr.ecr.${region}.amazonaws.com`],{stdin:new TextEncoder().encode(password),stdout:"pipe",stderr:"pipe"});if(loginResult.exitCode!==0){const loginError=new TextDecoder().decode(loginResult.stderr);throw Error(`Docker login failed: ${loginError}`)}authSpinner.succeed("Authenticated to ECR")}catch(error){authSpinner.fail("ECR authentication failed");if(isVerbose)log.debug(`ECR auth error: ${error.message}`);throw error}console.log("");const pushSpinner=spinner("Pushing image to ECR...");pushSpinner.start();try{const{exec}=await import("node:child_process"),{promisify}=await import("node:util"),execAsync=promisify(exec),pushCmd=`docker push ${imageTag}`;await execAsync(pushCmd,{env:process.env});pushSpinner.succeed(`Image pushed: ${imageTag}`)}catch(error){pushSpinner.fail("Docker push failed");if(isVerbose)log.debug(`Push error: ${error.message}`);throw error}console.log("");console.log("Container image ready in ECR. CloudFormation will use this image.");console.log("")}else{console.log("");console.log("Server deployment mode detected");console.log("Deploying EC2 infrastructure via CloudFormation...");console.log("")}async function getAwsAccountId(region){try{const{STSClient}=await import("@stacksjs/ts-cloud"),accountId=(await new STSClient(region).getCallerIdentity()).Account||process.env.AWS_ACCOUNT_ID||"";if(isVerbose)log.debug(`Retrieved AWS Account ID: ${accountId||"(empty)"}`);return accountId}catch(error){if(isVerbose)log.debug(`Failed to get AWS Account ID: ${error.message}`);return process.env.AWS_ACCOUNT_ID||""}}
|
|
11
|
+
Set AWS_ACCOUNT_ID in your .env file or ensure valid AWS credentials are configured.`);const ecrRepository=`${accountId}.dkr.ecr.${region}.amazonaws.com/${projectName}-${environment}-api`,imageTag=`${ecrRepository}:latest`;if(isVerbose)log.debug(`ECR repository: ${ecrRepository}`);if(isVerbose)log.debug("ECR credentials will be validated before push...");const buildSpinner=spinner("Building Docker image...");buildSpinner.start();try{const{exec}=await import("node:child_process"),{promisify}=await import("node:util"),execAsync=promisify(exec),buildCmd=`docker build --platform linux/amd64 -t ${imageTag} -f Dockerfile .`,{stdout,stderr}=await execAsync(buildCmd,{cwd:p.projectPath(),env:process.env,maxBuffer:10485760,timeout:900000});if(isVerbose&&stdout)log.debug(stdout);if(isVerbose&&stderr)log.debug(stderr);buildSpinner.succeed(`Docker image built: ${imageTag}`)}catch(error){buildSpinner.fail("Docker build failed");if(isVerbose)log.debug(`Docker build error: ${error.message}`);throw error}const repoSpinner=spinner("Checking ECR repository...");repoSpinner.start();try{const{ECRClient}=await import("@stacksjs/ts-cloud"),ecr=new ECRClient(region),repoName=`${projectName}-${environment}-api`;try{await ecr.describeRepositories({repositoryNames:[repoName]});repoSpinner.succeed(`ECR repository exists: ${repoName}`)}catch(err){if(err.code==="RepositoryNotFoundException"||err.message?.includes("RepositoryNotFoundException")){repoSpinner.text=`Creating ECR repository: ${repoName}...`;await ecr.createRepository({repositoryName:repoName,imageScanningConfiguration:{scanOnPush:!0},imageTagMutability:"MUTABLE"});repoSpinner.succeed(`ECR repository created: ${repoName}`)}else throw err}}catch(error){repoSpinner.fail("ECR repository check failed");if(isVerbose)log.debug(`ECR repository error: ${error.message}`);throw error}const authSpinner=spinner("Authenticating to ECR...");authSpinner.start();try{const{ECRClient}=await import("@stacksjs/ts-cloud"),token=(await new ECRClient(region).getAuthorizationToken()).authorizationData?.[0];if(!token?.authorizationToken)throw Error("No authorization token received from ECR");const decoded=Buffer.from(token.authorizationToken,"base64").toString("utf-8"),colonIndex=decoded.indexOf(":");if(colonIndex===-1)throw Error("ECR authorization token has unexpected format (missing colon separator)");const password=decoded.slice(colonIndex+1),loginResult=Bun.spawnSync(["docker","login","--username","AWS","--password-stdin",`${accountId}.dkr.ecr.${region}.amazonaws.com`],{stdin:new TextEncoder().encode(password),stdout:"pipe",stderr:"pipe"});if(loginResult.exitCode!==0){const loginError=new TextDecoder().decode(loginResult.stderr);throw Error(`Docker login failed: ${loginError}`)}authSpinner.succeed("Authenticated to ECR")}catch(error){authSpinner.fail("ECR authentication failed");if(isVerbose)log.debug(`ECR auth error: ${error.message}`);throw error}console.log("");const pushSpinner=spinner("Pushing image to ECR...");pushSpinner.start();try{const{exec}=await import("node:child_process"),{promisify}=await import("node:util"),execAsync=promisify(exec),pushCmd=`docker push ${imageTag}`;await execAsync(pushCmd,{env:process.env});pushSpinner.succeed(`Image pushed: ${imageTag}`)}catch(error){pushSpinner.fail("Docker push failed");if(isVerbose)log.debug(`Push error: ${error.message}`);throw error}console.log("");console.log("Container image ready in ECR. CloudFormation will use this image.");console.log("")}else{console.log("");console.log("Server deployment mode detected");console.log("Deploying EC2 infrastructure via CloudFormation...");console.log("")}async function getAwsAccountId(region){try{const{STSClient}=await import("@stacksjs/ts-cloud"),accountId=(await new STSClient(region).getCallerIdentity()).Account||process.env.AWS_ACCOUNT_ID||"";if(isVerbose)log.debug(`Retrieved AWS Account ID: ${accountId||"(empty)"}`);return accountId}catch(error){if(isVerbose)log.debug(`Failed to get AWS Account ID: ${error.message}`);return process.env.AWS_ACCOUNT_ID||""}}await deployStack({environment,region,waitForCompletion:!0,verbose:isVerbose});try{const deployScript=await import(p.projectPath("cloud/deploy-script"));if(typeof deployScript.afterDeploy==="function"){const{CloudFormationClient}=await import("@stacksjs/ts-cloud"),cf=new CloudFormationClient(region),stackName=cloudStackName;let stackOutputs={};try{stackOutputs=await cf.getStackOutputs(stackName)}catch(e){if(isVerbose)log.debug(`Failed to get stack outputs: ${e instanceof Error?e.message:String(e)}`)}await deployScript.afterDeploy({environment,region,outputs:stackOutputs})}}catch(hookError){if(hookError.code==="ERR_MODULE_NOT_FOUND"||hookError.code==="MODULE_NOT_FOUND"){if(isVerbose)log.debug("No afterDeploy hook found (cloud/deploy-script not found)")}else{console.warn(`\u26A0 afterDeploy hook failed: ${hookError.message}`);if(isVerbose)log.debug(`afterDeploy error stack: ${hookError.stack}`)}}const migrateSpinner=spinner("Running database migrations...");migrateSpinner.start();try{const{generateMigrations,runDatabaseMigration}=await import("@stacksjs/database");await generateMigrations();await runDatabaseMigration();migrateSpinner.succeed("Database migrations completed")}catch(migrationError){migrateSpinner.stop();console.log(`\u26A0 Database migrations skipped: ${migrationError.message}`);if(isVerbose)log.debug(`Migration error: ${migrationError.stack}`)}const frameworkTablesSpinner=spinner("Ensuring framework tables (auth, notifications, RBAC, traits)...");frameworkTablesSpinner.start();try{const{ensureUtcDatetimeColumns,migrateAuthTables,migrateNotificationTables,migrateRbacTables,migrateTraitTables}=await import("@stacksjs/database"),failures=[],steps=[["auth",migrateAuthTables],["notification",migrateNotificationTables],["RBAC",migrateRbacTables],["trait",migrateTraitTables],["utc-datetime",ensureUtcDatetimeColumns]];for(const[name,migrateTables]of steps){const result=await migrateTables({verbose:isVerbose});if(!result.success)failures.push(`${name}: ${result.error}`)}if(failures.length>0){frameworkTablesSpinner.stop();console.log(`\u26A0 Some framework tables could not be migrated: ${failures.join("; ")}`)}else frameworkTablesSpinner.succeed("Framework tables ensured (auth, notifications, RBAC, traits)")}catch(frameworkTablesError){frameworkTablesSpinner.stop();console.log(`\u26A0 Framework table migrations skipped: ${frameworkTablesError.message}`);if(isVerbose)log.debug(`Framework table migration error: ${frameworkTablesError.stack}`)}if(deploymentMode==="server"){const serverOutputSpinner=spinner("Retrieving server instance details...");serverOutputSpinner.start();try{const{CloudFormationClient}=await import("@stacksjs/ts-cloud"),cf=new CloudFormationClient(region),stackName=cloudStackName,outputs=await cf.getStackOutputs(stackName);serverOutputSpinner.succeed("Server instances deployed");console.log("");console.log("Server instances:");for(const[key,value]of Object.entries(outputs)){if(key.endsWith("PublicIp"))console.log(` ${key}: ${value}`);if(key.endsWith("InstanceId"))console.log(` ${key}: ${value}`)}console.log("")}catch(outputError){serverOutputSpinner.stop();console.log(`\u26A0 Could not retrieve server details: ${outputError.message}`);if(isVerbose)log.debug(`Output retrieval error: ${outputError.stack}`)}const serverDeploySpinner=spinner("Deploying API server to EC2...");serverDeploySpinner.start();try{const{S3Client,CloudFormationClient,AWSClient}=await import("@stacksjs/ts-cloud"),{readFileSync:readServerFile,existsSync:serverFileExists}=await import("node:fs"),{resolve}=await import("node:path"),s3=new S3Client(region),cf=new CloudFormationClient(region),awsClient=new AWSClient,stackName=cloudStackName,outputs=await cf.getStackOutputs(stackName),bucketName=outputs.FrontendBucketName,apiServerPort=Number(config.ports?.api||3008);let instanceId;for(const[key,value]of Object.entries(outputs))if(key.endsWith("InstanceId")&&typeof value==="string"){instanceId=value;break}let securityGroupId;if(instanceId){const{EC2Client}=await import("@stacksjs/ts-cloud"),ec2=new EC2Client(region);securityGroupId=(await ec2.describeInstances({Filters:[{Name:"instance-id",Values:[instanceId]}]}))?.Reservations?.[0]?.Instances?.[0]?.SecurityGroups?.[0]?.GroupId;await ensureSecurityGroupPort(ec2,securityGroupId,apiServerPort)}if(!instanceId||!bucketName){serverDeploySpinner.stop();if(!instanceId)console.log("\u26A0 No EC2 instance found in stack outputs, skipping server deploy");if(!bucketName)console.log("\u26A0 No S3 bucket found for staging server artifacts")}else{const publicDistributionId=outputs.publicCloudFrontDistributionId||outputs.CloudFrontDistributionId;await ensureCloudFrontApiOriginPort(awsClient,publicDistributionId,apiServerPort);const tsCloudDistPath=resolve(p.projectPath("node_modules/@stacksjs/ts-cloud/dist/index.js"));if(serverFileExists(tsCloudDistPath)){const tsCloudDist=readServerFile(tsCloudDistPath);await withS3Retry(()=>s3.putObject({bucket:bucketName,key:"_deploy/ts-cloud-dist.js",body:tsCloudDist,contentType:"application/javascript"}),"upload ts-cloud dist");if(isVerbose)log.debug(" Uploaded ts-cloud dist to S3")}const serverCode=`import { Database } from "bun:sqlite";
|
|
12
12
|
import { SESClient } from "./ts-cloud-dist.js";
|
|
13
13
|
|
|
14
14
|
const db = new Database("/var/www/api/stacks.db");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type { GitHubReleaseResult, PublishGitHubReleaseOptions } from './github-release';
|
|
2
|
+
export type { ReleaseEvidenceOptions, ReleaseEvidencePlan, ReleaseEvidenceReceipt, ReleaseSource } from './release-evidence';
|
|
3
|
+
export type { PublicationChannel } from './publication-preflight';
|
|
2
4
|
export type { PantryTool } from './build/ios-toolchain';
|
|
3
5
|
export type { StxLintConfig, StxLintReport, StxLintResult } from './lint/stx-gate';
|
|
4
6
|
export type { RouteActionReference } from './upgrade/route-actions';
|
|
@@ -37,6 +39,8 @@ export { formatProject, lintFix, lintProject } from './lint/lint';
|
|
|
37
39
|
// Release helpers the release commands call in-process: the GitHub release a
|
|
38
40
|
// pushed tag gets, and the Pantry-managed tools behind `build:ios`.
|
|
39
41
|
export { describeGitHubRelease, parseGitHubRemote, publishGitHubRelease } from './github-release';
|
|
42
|
+
export { validateReleaseEvidencePlan, verifyReleaseEvidence } from './release-evidence';
|
|
43
|
+
export { assertPublicationAllowed, PUBLICATION_CHECK_SCRIPT, publicationChannels } from './publication-preflight';
|
|
40
44
|
export { DEFAULT_IOS_DEPENDENCIES, IosToolchainError, loadIosDependencies, pantryExecutable, pantryInstallArgs, resolveXcodegen, XCODE_NOTE, xcodeDeveloperDir } from './build/ios-toolchain';
|
|
41
45
|
// stx conformance checks behind `buddy lint --stx`. Pure - returns a report
|
|
42
46
|
// and never exits, so the command owns rendering and the exit code.
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{setActionRunner}from"@stacksjs/action-runner";import{runAction}from"./helpers";setActionRunner((action,options)=>runAction(action,options));export*from"./action";export{add as runAdd}from"./add";export{BlogAdminError,deleteBlogPost,getBlogPost,listBlogPosts,saveBlogPost,slugify as blogSlugify}from"./blog-admin";export{commit as runCommit}from"./commit";export*from"./dev";export*from"./generate";export*from"./helpers";export*from"./library";export{formatProject,lintFix,lintProject}from"./lint/lint";export{describeGitHubRelease,parseGitHubRemote,publishGitHubRelease}from"./github-release";export{DEFAULT_IOS_DEPENDENCIES,IosToolchainError,loadIosDependencies,pantryExecutable,pantryInstallArgs,resolveXcodegen,XCODE_NOTE,xcodeDeveloperDir}from"./build/ios-toolchain";export{DEFAULT_STX_LINT_CONFIG,loadStxLintConfig,runStxLint}from"./lint/stx-gate";export{measureDefaultsDrift,summarizeStructureChanges}from"./upgrade/package-project";export{describeDanglingRouteActions,findDanglingRouteActions}from"./upgrade/route-actions";export*from"./setup";export{createComponent,createDatabase,createFactory,createFunction,createLanguage,createMiddleware,createMigration,createModel,createMail,createNotification,createPage,isDryRunActive,makeAction,makeCertificate,makeComponent,makeDatabase,makeFunction,makeLanguage,makeMail,makeNotification,makePage,makeQueueTable,makeStack,make as runMake,setDryRun}from"./make";export{discoverPackages,ensureDiscoveredPackages}from"./discover-packages";export{parseFields,scaffoldCrud}from"./scaffold-crud";export{installStack,uninstallStack,listStacks}from"./stacks";export{makeJob}from"./make-job";export{makeCommand}from"./make-command";export{makePolicy}from"./make-policy";export{makeResource}from"./make-resource";export{resolveStxSource}from"./stx-source";
|
|
1
|
+
import{setActionRunner}from"@stacksjs/action-runner";import{runAction}from"./helpers";setActionRunner((action,options)=>runAction(action,options));export*from"./action";export{add as runAdd}from"./add";export{BlogAdminError,deleteBlogPost,getBlogPost,listBlogPosts,saveBlogPost,slugify as blogSlugify}from"./blog-admin";export{commit as runCommit}from"./commit";export*from"./dev";export*from"./generate";export*from"./helpers";export*from"./library";export{formatProject,lintFix,lintProject}from"./lint/lint";export{describeGitHubRelease,parseGitHubRemote,publishGitHubRelease}from"./github-release";export{validateReleaseEvidencePlan,verifyReleaseEvidence}from"./release-evidence";export{assertPublicationAllowed,PUBLICATION_CHECK_SCRIPT,publicationChannels}from"./publication-preflight";export{DEFAULT_IOS_DEPENDENCIES,IosToolchainError,loadIosDependencies,pantryExecutable,pantryInstallArgs,resolveXcodegen,XCODE_NOTE,xcodeDeveloperDir}from"./build/ios-toolchain";export{DEFAULT_STX_LINT_CONFIG,loadStxLintConfig,runStxLint}from"./lint/stx-gate";export{measureDefaultsDrift,summarizeStructureChanges}from"./upgrade/package-project";export{describeDanglingRouteActions,findDanglingRouteActions}from"./upgrade/route-actions";export*from"./setup";export{createComponent,createDatabase,createFactory,createFunction,createLanguage,createMiddleware,createMigration,createModel,createMail,createNotification,createPage,isDryRunActive,makeAction,makeCertificate,makeComponent,makeDatabase,makeFunction,makeLanguage,makeMail,makeNotification,makePage,makeQueueTable,makeStack,make as runMake,setDryRun}from"./make";export{discoverPackages,ensureDiscoveredPackages}from"./discover-packages";export{parseFields,scaffoldCrud}from"./scaffold-crud";export{installStack,uninstallStack,listStacks}from"./stacks";export{makeJob}from"./make-job";export{makeCommand}from"./make-command";export{makePolicy}from"./make-policy";export{makeResource}from"./make-resource";export{resolveStxSource}from"./stx-source";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Execute declared project checks with the installed Bun and owned cleanup. */
|
|
2
|
+
export declare function runProjectScripts(options: ProjectScriptOptions): Promise<ProjectScriptReceipt>;
|
|
3
|
+
export declare interface ProjectScriptOptions {
|
|
4
|
+
cwd: string
|
|
5
|
+
scripts: string[]
|
|
6
|
+
env?: Record<string, string | undefined>
|
|
7
|
+
signal?: AbortSignal
|
|
8
|
+
timeoutMs?: number
|
|
9
|
+
onOutput?: (chunk: Uint8Array, stream: 'stdout' | 'stderr') => void
|
|
10
|
+
}
|
|
11
|
+
export declare interface ProjectScriptReceipt {
|
|
12
|
+
bunVersion: string
|
|
13
|
+
checks: Array<{ script: string, exitCode: 0, outputSha256: string }>
|
|
14
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import{createHash}from"node:crypto";import{readFileSync}from"node:fs";import{join}from"node:path";function fail(message){throw Error(message)}function windowsCheckCommand(powershell,bun,script){const source=`
|
|
2
|
+
$ErrorActionPreference = 'Stop'
|
|
3
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
4
|
+
Add-Type -TypeDefinition @'
|
|
5
|
+
using System;
|
|
6
|
+
using System.ComponentModel;
|
|
7
|
+
using System.Runtime.InteropServices;
|
|
8
|
+
using System.Text;
|
|
9
|
+
using System.Threading;
|
|
10
|
+
public static class StacksOwnedCheck {
|
|
11
|
+
[StructLayout(LayoutKind.Sequential)] struct BasicLimits {
|
|
12
|
+
public long ProcessTime, JobTime;
|
|
13
|
+
public uint Flags;
|
|
14
|
+
public UIntPtr MinimumWorkingSet, MaximumWorkingSet;
|
|
15
|
+
public uint ActiveProcessLimit;
|
|
16
|
+
public UIntPtr Affinity;
|
|
17
|
+
public uint Priority, Scheduling;
|
|
18
|
+
}
|
|
19
|
+
[StructLayout(LayoutKind.Sequential)] struct ExtendedLimits {
|
|
20
|
+
public BasicLimits Basic;
|
|
21
|
+
public ulong ReadOperations, WriteOperations, OtherOperations, ReadBytes, WriteBytes, OtherBytes;
|
|
22
|
+
public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
|
|
23
|
+
}
|
|
24
|
+
[StructLayout(LayoutKind.Sequential)] struct Accounting {
|
|
25
|
+
public long UserTime, KernelTime, PeriodUserTime, PeriodKernelTime;
|
|
26
|
+
public uint PageFaults, TotalProcesses, ActiveProcesses, TerminatedProcesses;
|
|
27
|
+
}
|
|
28
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct Startup {
|
|
29
|
+
public uint Size;
|
|
30
|
+
public IntPtr Reserved, Desktop, Title;
|
|
31
|
+
public uint X, Y, XSize, YSize, XChars, YChars, Fill, Flags;
|
|
32
|
+
public ushort Show, ReservedSize;
|
|
33
|
+
public IntPtr ReservedBytes, Input, Output, Error;
|
|
34
|
+
}
|
|
35
|
+
[StructLayout(LayoutKind.Sequential)] struct ProcessInfo {
|
|
36
|
+
public IntPtr Process, Thread;
|
|
37
|
+
public uint ProcessId, ThreadId;
|
|
38
|
+
}
|
|
39
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
40
|
+
static extern IntPtr CreateJobObjectW(IntPtr attributes, string name);
|
|
41
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
42
|
+
static extern bool SetInformationJobObject(IntPtr job, int kind, ref ExtendedLimits limits, uint size);
|
|
43
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
44
|
+
static extern bool QueryInformationJobObject(IntPtr job, int kind, out Accounting info, uint size, IntPtr returned);
|
|
45
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
46
|
+
static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
|
47
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
48
|
+
static extern bool TerminateJobObject(IntPtr job, uint code);
|
|
49
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
50
|
+
static extern bool DuplicateHandle(IntPtr sourceProcess, IntPtr source, IntPtr targetProcess,
|
|
51
|
+
out IntPtr target, uint access, bool inherit, uint options);
|
|
52
|
+
[DllImport("kernel32.dll")] static extern IntPtr GetCurrentProcess();
|
|
53
|
+
[DllImport("kernel32.dll")] static extern IntPtr GetStdHandle(int kind);
|
|
54
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
55
|
+
static extern IntPtr CreateFileW(string name, uint access, uint share, IntPtr security,
|
|
56
|
+
uint disposition, uint flags, IntPtr template);
|
|
57
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
58
|
+
static extern bool PeekNamedPipe(IntPtr pipe, IntPtr buffer, uint size, IntPtr read,
|
|
59
|
+
out uint available, IntPtr remaining);
|
|
60
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
61
|
+
static extern bool CreateProcessW(string application, StringBuilder command, IntPtr processSecurity,
|
|
62
|
+
IntPtr threadSecurity, bool inherit, uint flags, IntPtr environment, string directory,
|
|
63
|
+
ref Startup startup, out ProcessInfo process);
|
|
64
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern uint ResumeThread(IntPtr thread);
|
|
65
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern uint WaitForSingleObject(IntPtr handle, uint timeout);
|
|
66
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool GetExitCodeProcess(IntPtr process, out uint code);
|
|
67
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateProcess(IntPtr process, uint code);
|
|
68
|
+
[DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr handle);
|
|
69
|
+
static void Require(bool success) { if (!success) throw new Win32Exception(Marshal.GetLastWin32Error()); }
|
|
70
|
+
static IntPtr InheritableStandardHandle(int kind) {
|
|
71
|
+
IntPtr source = GetStdHandle(kind);
|
|
72
|
+
if (source == IntPtr.Zero || source == new IntPtr(-1)) return IntPtr.Zero;
|
|
73
|
+
IntPtr inherited;
|
|
74
|
+
Require(DuplicateHandle(GetCurrentProcess(), source, GetCurrentProcess(), out inherited, 0, true, 2));
|
|
75
|
+
return inherited;
|
|
76
|
+
}
|
|
77
|
+
static IntPtr ConsumerInput() {
|
|
78
|
+
IntPtr source = CreateFileW("NUL", 0x80000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
|
|
79
|
+
Require(source != new IntPtr(-1));
|
|
80
|
+
try {
|
|
81
|
+
IntPtr inherited;
|
|
82
|
+
Require(DuplicateHandle(GetCurrentProcess(), source, GetCurrentProcess(), out inherited, 0, true, 2));
|
|
83
|
+
return inherited;
|
|
84
|
+
} finally { CloseHandle(source); }
|
|
85
|
+
}
|
|
86
|
+
static bool StopRequested() {
|
|
87
|
+
uint available;
|
|
88
|
+
if (PeekNamedPipe(GetStdHandle(-10), IntPtr.Zero, 0, IntPtr.Zero, out available, IntPtr.Zero))
|
|
89
|
+
return available != 0;
|
|
90
|
+
int error = Marshal.GetLastWin32Error();
|
|
91
|
+
if (error == 109 || error == 232) return true; // Closed control pipe.
|
|
92
|
+
throw new Win32Exception(error);
|
|
93
|
+
}
|
|
94
|
+
static IntPtr Job() {
|
|
95
|
+
IntPtr job = CreateJobObjectW(IntPtr.Zero, null);
|
|
96
|
+
Require(job != IntPtr.Zero);
|
|
97
|
+
ExtendedLimits limits = new ExtendedLimits();
|
|
98
|
+
limits.Basic.Flags = 0x2000; // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
99
|
+
try { Require(SetInformationJobObject(job, 9, ref limits, (uint)Marshal.SizeOf(typeof(ExtendedLimits)))); }
|
|
100
|
+
catch { CloseHandle(job); throw; }
|
|
101
|
+
return job;
|
|
102
|
+
}
|
|
103
|
+
public static int Run(string bun, string script) {
|
|
104
|
+
// A queued stop must prevent startup even when Add-Type took most of the
|
|
105
|
+
// check's budget. The control pipe is never inherited as consumer stdin.
|
|
106
|
+
if (StopRequested()) return 1;
|
|
107
|
+
// The outer job owns this host before it creates anything. If cancellation
|
|
108
|
+
// races suspended-child assignment below, host death still kills that child.
|
|
109
|
+
// Retain this non-inheritable handle until process exit; closing it earlier
|
|
110
|
+
// would terminate this host before it can return the command's exit code.
|
|
111
|
+
IntPtr lifetime = Job();
|
|
112
|
+
Require(AssignProcessToJobObject(lifetime, GetCurrentProcess()));
|
|
113
|
+
IntPtr commandJob = Job();
|
|
114
|
+
ProcessInfo child = new ProcessInfo();
|
|
115
|
+
try {
|
|
116
|
+
Startup startup = new Startup();
|
|
117
|
+
startup.Size = (uint)Marshal.SizeOf(typeof(Startup));
|
|
118
|
+
startup.Flags = 0x100; // STARTF_USESTDHANDLES
|
|
119
|
+
// Bun can supply non-inheritable standard handles to PowerShell. A
|
|
120
|
+
// native CreateProcess child needs explicitly inheritable duplicates.
|
|
121
|
+
// Never execute user code before it belongs to the inner cleanup job.
|
|
122
|
+
try {
|
|
123
|
+
startup.Input = ConsumerInput();
|
|
124
|
+
startup.Output = InheritableStandardHandle(-11);
|
|
125
|
+
startup.Error = InheritableStandardHandle(-12);
|
|
126
|
+
Require(CreateProcessW(bun, new StringBuilder("\\"" + bun + "\\" run " + script),
|
|
127
|
+
IntPtr.Zero, IntPtr.Zero, true, 0x08000004, IntPtr.Zero, null, ref startup, out child));
|
|
128
|
+
} finally {
|
|
129
|
+
foreach (IntPtr handle in new[] { startup.Input, startup.Output, startup.Error })
|
|
130
|
+
if (handle != IntPtr.Zero) CloseHandle(handle);
|
|
131
|
+
}
|
|
132
|
+
Require(AssignProcessToJobObject(commandJob, child.Process));
|
|
133
|
+
bool cancelled = StopRequested();
|
|
134
|
+
if (!cancelled) Require(ResumeThread(child.Thread) != 0xffffffff);
|
|
135
|
+
while (!cancelled) {
|
|
136
|
+
uint state = WaitForSingleObject(child.Process, 10);
|
|
137
|
+
if (state == 0) break;
|
|
138
|
+
Require(state == 258); // WAIT_TIMEOUT, otherwise retain the native error.
|
|
139
|
+
cancelled = StopRequested();
|
|
140
|
+
}
|
|
141
|
+
uint code = 1;
|
|
142
|
+
if (!cancelled) Require(GetExitCodeProcess(child.Process, out code));
|
|
143
|
+
return unchecked((int)code);
|
|
144
|
+
} finally {
|
|
145
|
+
try {
|
|
146
|
+
// Also dispose a suspended child if assigning its inner job failed.
|
|
147
|
+
if (child.Process != IntPtr.Zero) {
|
|
148
|
+
uint state = WaitForSingleObject(child.Process, 0);
|
|
149
|
+
Require(state == 0 || state == 258);
|
|
150
|
+
if (state == 258) Require(TerminateProcess(child.Process, 1));
|
|
151
|
+
}
|
|
152
|
+
// Membership survives an exited intermediate process, unlike a PID
|
|
153
|
+
// tree. Query it on every exit path before closing the owning handle.
|
|
154
|
+
// https://learn.microsoft.com/windows/win32/procthread/job-objects
|
|
155
|
+
Require(TerminateJobObject(commandJob, 1));
|
|
156
|
+
var cleanup = System.Diagnostics.Stopwatch.StartNew();
|
|
157
|
+
while (true) {
|
|
158
|
+
Accounting info;
|
|
159
|
+
Require(QueryInformationJobObject(commandJob, 1, out info, (uint)Marshal.SizeOf(typeof(Accounting)), IntPtr.Zero));
|
|
160
|
+
if (info.ActiveProcesses == 0 && (child.Process == IntPtr.Zero || WaitForSingleObject(child.Process, 0) == 0)) break;
|
|
161
|
+
if (cleanup.ElapsedMilliseconds >= 1000) throw new TimeoutException("Owned Windows processes did not finish cleanup");
|
|
162
|
+
Thread.Sleep(10);
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
if (child.Thread != IntPtr.Zero) CloseHandle(child.Thread);
|
|
166
|
+
if (child.Process != IntPtr.Zero) CloseHandle(child.Process);
|
|
167
|
+
Require(CloseHandle(commandJob));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
'@
|
|
173
|
+
exit [StacksOwnedCheck]::Run('${bun.replaceAll("'","''")}', '${script}')
|
|
174
|
+
`;return[powershell,"-NoLogo","-NoProfile","-NonInteractive","-OutputFormat","Text","-EncodedCommand",Buffer.from(source,"utf16le").toString("base64")]}export async function runProjectScripts(options){const timeoutMs=options.timeoutMs??300000;if(!Number.isSafeInteger(timeoutMs)||timeoutMs<=0||!options.scripts.length||options.scripts.some((name)=>!/^[a-z0-9][\w:.-]*$/i.test(name))||new Set(options.scripts).size!==options.scripts.length)fail("Project checks need unique script names and a positive execution budget");const receipt={bunVersion:"",checks:[]},manifestPath=join(options.cwd,"package.json"),originalManifest=readFileSync(manifestPath,"utf8"),manifest=JSON.parse(originalManifest),bun=Bun.which("bun",{cwd:options.cwd});if(!bun)fail("Consumer Bun executable is unavailable");const probe=Bun.spawnSync([bun,"--version"],{cwd:options.cwd,stdout:"pipe",stderr:"pipe",timeout:1e4}),bunVersion=probe.stdout.toString().trim();if(probe.exitCode!==0||!/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(bunVersion)||!Bun.semver.satisfies(bunVersion,manifest.engines?.bun??">=1.3.0"))fail("Consumer Bun executable does not satisfy the declared toolchain");receipt.bunVersion=bunVersion;for(const script of options.scripts){if(!Object.hasOwn(manifest.scripts??{},script)||typeof manifest.scripts?.[script]!=="string"||!manifest.scripts[script].trim())fail(`Consumer script ${script} is not declared`);options.signal?.throwIfAborted();const windowsTaskkill=process.platform==="win32"?Bun.which("taskkill.exe"):void 0,powershell=process.platform==="win32"?Bun.which("powershell.exe")??Bun.which("pwsh.exe"):void 0;if(process.platform==="win32"&&(!windowsTaskkill||!powershell))fail("Windows process ownership requires taskkill.exe and PowerShell with .NET Add-Type");const command=powershell?windowsCheckCommand(powershell,bun,script):[bun,"run",script];let reportExit=()=>{},reportFailure=()=>{};const reported=new Promise((resolve,reject)=>{reportExit=resolve;reportFailure=reject});let interrupted=!1,windowsHelperPid;const host=powershell?`
|
|
175
|
+
setInterval(() => {}, 60_000)
|
|
176
|
+
let command
|
|
177
|
+
let stopRequested = false
|
|
178
|
+
let start
|
|
179
|
+
const permitted = new Promise(resolve => { start = resolve })
|
|
180
|
+
process.on('message', async (message) => {
|
|
181
|
+
if (message?.stop) {
|
|
182
|
+
stopRequested = true
|
|
183
|
+
start()
|
|
184
|
+
if (command) {
|
|
185
|
+
try {
|
|
186
|
+
command.stdin.write(new Uint8Array([1]))
|
|
187
|
+
await command.stdin.flush()
|
|
188
|
+
} catch (error) { process.send({ error: String(error) }) }
|
|
189
|
+
}
|
|
190
|
+
} else if (message?.start) start()
|
|
191
|
+
})
|
|
192
|
+
process.send({ ready: true })
|
|
193
|
+
await permitted
|
|
194
|
+
if (stopRequested) process.send({ exitCode: 1 })
|
|
195
|
+
else {
|
|
196
|
+
try {
|
|
197
|
+
command = Bun.spawn(${JSON.stringify(command)}, { stdin: 'pipe', stdout: 'inherit', stderr: 'inherit' })
|
|
198
|
+
process.send({ helperPid: command.pid })
|
|
199
|
+
process.send({ exitCode: await command.exited })
|
|
200
|
+
} catch (error) { process.send({ error: String(error) }) }
|
|
201
|
+
}
|
|
202
|
+
`:`
|
|
203
|
+
setInterval(() => {}, 60_000)
|
|
204
|
+
try {
|
|
205
|
+
const command = Bun.spawn(${JSON.stringify(command)}, { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' })
|
|
206
|
+
process.send({ exitCode: await command.exited })
|
|
207
|
+
} catch (error) {
|
|
208
|
+
process.send({ error: String(error) })
|
|
209
|
+
}
|
|
210
|
+
`,child=Bun.spawn([bun,"--eval",host],{cwd:options.cwd,env:{...process.env,...options.env},stdin:"ignore",stdout:"pipe",stderr:"pipe",detached:process.platform!=="win32",ipc(message){if(powershell&&message?.ready===!0)try{child.send(interrupted?{stop:!0}:{start:!0})}catch(error){reportFailure(Error(`Consumer script ${script} control channel failed: ${String(error)}`))}else if(powershell&&Number.isSafeInteger(message?.helperPid)&&message.helperPid>0)windowsHelperPid=message.helperPid;else if(Number.isInteger(message?.exitCode))reportExit(message.exitCode);else reportFailure(Error(`Consumer script ${script} host failed: ${message?.error??"invalid completion report"}`))}}),pipes=[child.stdout,child.stderr].map((stream,index)=>({reader:stream.getReader(),done:!1,source:index===0?"stdout":"stderr"}));let teardown,announceStop=()=>{};const stopped=new Promise((resolve)=>{announceStop=resolve}),waitWithin=async(promise,milliseconds)=>{let timer;try{return await Promise.race([promise,new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error("Process or output cleanup did not finish within its grace period")),milliseconds)})])}finally{if(timer)clearTimeout(timer)}};let draining;const dispose=(preserveOutput)=>{if(teardown)return teardown;teardown=(async()=>{const errors=[];if(windowsTaskkill){if(!preserveOutput)try{child.send({stop:!0});await waitWithin(reported,1000)}catch{errors.push("Native Windows cleanup completion was not confirmed")}try{const result=Bun.spawnSync([windowsTaskkill,"/PID",String(child.pid),"/T","/F"],{stdout:"ignore",stderr:"ignore",timeout:2000});if(result.exitCode!==0)errors.push(`taskkill failed with exit code ${result.exitCode}`)}catch{errors.push("Windows process-tree cleanup could not execute taskkill")}if(windowsHelperPid){const until=performance.now()+1000;let alive=!0;while(alive){try{process.kill(windowsHelperPid,0)}catch(error){if(error.code==="ESRCH")alive=!1;else break}if(!alive||performance.now()>=until)break;await new Promise((resolve)=>setTimeout(resolve,10))}if(alive)errors.push("Native Windows helper disposal was not confirmed")}if(errors.length)try{child.kill("SIGKILL")}catch{errors.push("Direct child cleanup also failed")}}else{const signalGroup=(signal)=>{try{process.kill(-child.pid,signal)}catch(error){if(error.code!=="ESRCH"){const cause=error;errors.push(`Process-group ${signal} failed (code=${cause.code}, errno=${cause.errno}, message=${cause.message})`)}}};signalGroup("SIGTERM");await new Promise((resolve)=>setTimeout(resolve,500));signalGroup("SIGKILL");const until=performance.now()+1000;let pendingError="";while(!0){try{process.kill(-child.pid,0)}catch(error){const cause=error;if(cause.code==="ESRCH")break;pendingError=`signal=0, code=${cause.code}, errno=${cause.errno}, message=${cause.message}`;if(cause.code!=="EPERM"){errors.push(`Process-group disposal could not be confirmed (${pendingError})`);break}}if(performance.now()>=until){errors.push(`Owned process group remained after cleanup${pendingError?` (${pendingError})`:""}`);break}await new Promise((resolve)=>setTimeout(resolve,10))}}if(preserveOutput)try{await waitWithin(draining,1000)}catch{errors.push("Output draining exceeded its cleanup grace period")}const close=Promise.all([...pipes.filter((pipe)=>!pipe.done).map(async(pipe)=>{try{await pipe.reader.cancel()}catch{errors.push("Output reader cancellation failed")}}),child.exited]);try{await waitWithin(close,1000)}catch{errors.push("Process or output cleanup exceeded its grace period")}if(errors.length)fail(`Consumer script ${script} cleanup failed: ${errors.join("; ")}`)})();teardown.catch(()=>{});return teardown},stop=()=>{if(interrupted)return;interrupted=!0;dispose(!1);announceStop()};child.exited.then((code)=>{if(!teardown)reportFailure(Error(`Consumer script ${script} host exited before reporting completion (exit code ${code})`))});const deadline=setTimeout(stop,timeoutMs);options.signal?.addEventListener("abort",stop,{once:!0});if(options.signal?.aborted)stop();const output=createHash("sha256");async function drain(pipe){try{while(!0){const next=await pipe.reader.read();if(next.done)break;output.update(next.value);options.onOutput?.(next.value.slice(),pipe.source)}}finally{pipe.done=!0;pipe.reader.releaseLock()}}let exitCode;try{draining=Promise.all(pipes.map(drain)).then(()=>{});draining.catch(reportFailure);const completion=reported.then((exitCode)=>({kind:"completed",exitCode})),result=await Promise.race([completion,stopped.then(()=>({kind:"interrupted"}))]);if(result.kind==="interrupted"||interrupted){await teardown;fail(`Consumer script ${script} was interrupted or exceeded its execution budget`)}exitCode=result.exitCode;await dispose(!0)}catch(error){const wasInterrupted=interrupted;stop();try{await teardown}catch(cleanupError){if(wasInterrupted)fail(`Consumer script ${script} was interrupted or exceeded its execution budget; ${cleanupError instanceof Error?cleanupError.message:String(cleanupError)}`);throw cleanupError}throw error}finally{clearTimeout(deadline);options.signal?.removeEventListener("abort",stop)}if(interrupted||options.signal?.aborted)fail(`Consumer script ${script} was interrupted or exceeded its execution budget`);if(exitCode!==0)fail(`Consumer script ${script} failed with exit code ${exitCode}`);if(readFileSync(manifestPath,"utf8")!==originalManifest)fail(`Consumer script ${script} changed the project manifest`);receipt.checks.push({script,exitCode:0,outputSha256:output.digest("hex")})}return receipt}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** A declared application policy is mandatory before any publication mutation. */
|
|
2
|
+
export declare function assertPublicationAllowed(options: { cwd: string, channel: PublicationChannel, signal?: AbortSignal }): Promise<unknown>;
|
|
3
|
+
export declare const publicationChannels: readonly ['web', 'preview', 'desktop', 'ios', 'android', 'github-release'];
|
|
4
|
+
export declare const PUBLICATION_CHECK_SCRIPT: 'publication:check';
|
|
5
|
+
export type PublicationChannel = typeof publicationChannels[number];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync}from"node:fs";import{join}from"node:path";import{runProjectScripts}from"./project-scripts";export const publicationChannels=["web","preview","desktop","ios","android","github-release"],PUBLICATION_CHECK_SCRIPT="publication:check";export async function assertPublicationAllowed(options){if(!publicationChannels.includes(options.channel))throw Error("Unknown publication channel");const pkg=JSON.parse(readFileSync(join(options.cwd,"package.json"),"utf8"));if(!Object.hasOwn(pkg.scripts??{},PUBLICATION_CHECK_SCRIPT))return{status:"not-configured",channel:options.channel};if(typeof pkg.scripts?.[PUBLICATION_CHECK_SCRIPT]!=="string"||!pkg.scripts[PUBLICATION_CHECK_SCRIPT].trim())throw Error("publication:check must declare a nonempty application policy");const checks=await runProjectScripts({cwd:options.cwd,scripts:[PUBLICATION_CHECK_SCRIPT],signal:options.signal,env:{STACKS_PUBLICATION_CHANNEL:options.channel}});return{status:"passed",channel:options.channel,...checks}}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Validate data before using any value as a URL, filesystem path or command. */
|
|
2
|
+
export declare function validateReleaseEvidencePlan(value: unknown): asserts value is ReleaseEvidencePlan;
|
|
3
|
+
/**
|
|
4
|
+
* Verify existing releases, then run the installed consumer's declared checks.
|
|
5
|
+
* Does not bump, tag, publish, upgrade or replace the owning release commands.
|
|
6
|
+
* Archive SRI is compared with the independently retained source-release plan;
|
|
7
|
+
* this receipt is not an assertion that registry attestations were validated.
|
|
8
|
+
*/
|
|
9
|
+
export declare function verifyReleaseEvidence(plan: ReleaseEvidencePlan, options: ReleaseEvidenceOptions): Promise<ReleaseEvidenceReceipt>;
|
|
10
|
+
export declare interface ReleaseSource {
|
|
11
|
+
repository: string
|
|
12
|
+
commit: string
|
|
13
|
+
tag: string
|
|
14
|
+
gates: Array<{ runId: number, workflow: string, requiredJobs: string[] }>
|
|
15
|
+
packages: Array<{ name: string, version: string, integrity: string }>
|
|
16
|
+
}
|
|
17
|
+
export declare interface ReleaseEvidencePlan {
|
|
18
|
+
schemaVersion: 1
|
|
19
|
+
sources: ReleaseSource[]
|
|
20
|
+
checks: string[]
|
|
21
|
+
}
|
|
22
|
+
export declare interface ReleaseEvidenceReceipt {
|
|
23
|
+
schemaVersion: 1
|
|
24
|
+
verifiedAt: string
|
|
25
|
+
sources: Array<ReleaseSource & {
|
|
26
|
+
releaseCommand: 'bun run release:patch'
|
|
27
|
+
releaseScript: string
|
|
28
|
+
packages: Array<ReleaseSource['packages'][number] & { tarball: string, bytes: number, filesSha256: string }>
|
|
29
|
+
}>
|
|
30
|
+
consumer: {
|
|
31
|
+
lockfile: 'bun.lock'
|
|
32
|
+
sha256: string
|
|
33
|
+
bunVersion: string
|
|
34
|
+
checks: Array<{ script: string, exitCode: 0, outputSha256: string }>
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export declare interface ReleaseEvidenceOptions {
|
|
38
|
+
cwd: string
|
|
39
|
+
token?: string
|
|
40
|
+
fetch?: (url: string, init?: RequestInit) => Promise<Response>
|
|
41
|
+
signal?: AbortSignal
|
|
42
|
+
requestTimeoutMs?: number
|
|
43
|
+
checkTimeoutMs?: number
|
|
44
|
+
maxArchiveBytes?: number
|
|
45
|
+
maxExpandedBytes?: number
|
|
46
|
+
}
|
|
47
|
+
declare type PackageFiles = Map<string, { sha256: string, bytes: number }>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash}from"node:crypto";import{lstatSync,readFileSync,readdirSync,realpathSync}from"node:fs";import{join,relative,resolve}from"node:path";import{runProjectScripts}from"./project-scripts";function fail(message){throw Error(`Release verification blocked: ${message}`)}function hash(bytes){return createHash("sha256").update(bytes).digest("hex")}function nonemptyStrings(value){return Array.isArray(value)&&value.length>0&&value.every((item)=>typeof item==="string"&&item.length>0)}export function validateReleaseEvidencePlan(value){const plan=value;if(!plan||plan.schemaVersion!==1||!Array.isArray(plan.sources)||!plan.sources.length||!nonemptyStrings(plan.checks)||plan.checks.some((name)=>!/^[a-z0-9][\w:.-]*$/i.test(name))||new Set(plan.checks).size!==plan.checks.length)fail("Expected a version 1 plan with sources and named consumer checks");const packages=new Set;for(const source of plan.sources){if(!source||!/^[\w.-]+\/[\w.-]+$/.test(source.repository)||!/^[a-f0-9]{40}$/.test(source.commit)||typeof source.tag!=="string"||!source.tag||!Array.isArray(source.gates)||!source.gates.length||!Array.isArray(source.packages)||!source.packages.length)fail("Each source needs a repository, full commit, tag, release gates and packages");for(const gate of source.gates)if(!gate||!Number.isSafeInteger(gate.runId)||gate.runId<=0||!/^\.github\/workflows\/[\w.-]+\.ya?ml$/.test(gate.workflow)||!nonemptyStrings(gate.requiredJobs)||new Set(gate.requiredJobs).size!==gate.requiredJobs.length)fail("Each release gate needs an exact run, workflow and required jobs");for(const pkg of source.packages){if(!pkg||!/^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/.test(pkg.name)||!/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(pkg.version)||!/^sha512-[A-Za-z0-9+/]{86}==$/.test(pkg.integrity)||packages.has(pkg.name))fail("Packages need unique names, exact versions and original SHA-512 archive identities");packages.add(pkg.name)}}}async function boundedBody(response,limit){const declared=response.headers.get("content-length");if(declared&&Number(declared)>limit){await response.body?.cancel();fail("Response exceeds its byte budget")}if(!response.body)fail("Response has no body");const reader=response.body.getReader(),chunks=[];let length=0;try{while(!0){const next=await reader.read();if(next.done)break;length+=next.value.byteLength;if(length>limit)fail("Response exceeds its byte budget");chunks.push(next.value)}return Buffer.concat(chunks,length)}finally{await reader.cancel().catch(()=>{});reader.releaseLock()}}function validateTarBounds(tar){if(!tar.byteLength||tar.byteLength%512)fail("Published archive has invalid tar block bounds");let offset=0,files=0,headers=0;const text=(start,length)=>new TextDecoder().decode(tar.subarray(start,start+length)).replace(/\0.*$/s,"").trim();while(offset+512<=tar.byteLength){const header=tar.subarray(offset,offset+512);if(header.every((byte)=>byte===0)){if(tar.subarray(offset).some((byte)=>byte!==0))fail("Published archive has data after its tar terminator");return files}if(++headers>1e5)fail("Published archive has too many entries");const rawSize=text(offset+124,12);if(!/^[0-7]+$/.test(rawSize))fail("Published archive has an unsupported tar size");const size=Number.parseInt(rawSize,8),end=offset+512+Math.ceil(size/512)*512;if(!Number.isSafeInteger(end)||end>tar.byteLength)fail("Published archive declares bytes outside its tar bounds");const type=header[156]===0?"0":String.fromCharCode(header[156]);if(type==="0")files++;else if(type==="x"||type==="g"){const metadata=tar.subarray(offset+512,offset+512+size),allowed=new Set(["path","mtime","atime","ctime","uid","gid","uname","gname","comment","charset","hdrcharset"]);for(let start=0;start<metadata.length;){const space=metadata.indexOf(32,start);if(space<0)fail("Published archive has malformed PAX records");const rawLength=new TextDecoder().decode(metadata.subarray(start,space)),length=Number(rawLength),end=start+length;if(!/^\d+$/.test(rawLength)||!Number.isSafeInteger(end)||end>metadata.length||end<=space+2||metadata[end-1]!==10)fail("Published archive has malformed PAX bounds");const equals=metadata.indexOf(61,space+1),key=new TextDecoder().decode(metadata.subarray(space+1,equals));if(equals<0||equals>=end-1||!allowed.has(key))fail("Published archive has unsupported PAX allocation or link metadata");start=end}}else if(type!=="5"&&type!=="L")fail("Published archive contains a link or unsupported tar entry");offset=end}fail("Published archive has no tar terminator")}async function archiveFiles(bytes,limit){const stream=new Blob([new Uint8Array(bytes)]).stream().pipeThrough(new DecompressionStream("gzip")),tar=await boundedBody(new Response(stream),limit),count=validateTarBounds(tar),files=await new Bun.Archive(tar).files();if(!count||files.size!==count)fail("Published archive has missing or duplicate file paths");const result=new Map;for(const[name,file]of files){const path=name.slice(8);if(!name.startsWith("package/")||!path||path.includes("\\")||path.includes(":")||path.split("/").some((part)=>!part||part==="."||part==="..")||result.has(path))fail("Published archive contains an unsafe or duplicate package path");result.set(path,{sha256:hash(new Uint8Array(await file.arrayBuffer())),bytes:file.size})}if(!result.has("package.json"))fail("Published archive has no package manifest");return result}function verifyInstalledFiles(directory,expected){const found=new Set,hasBundledDependencies=[...expected.keys()].some((path)=>path.startsWith("node_modules/"));function visit(path,depth){if(depth>128)fail("Installed package directory depth exceeds its budget");for(const entry of readdirSync(join(directory,path),{withFileTypes:!0})){if(!path&&entry.name==="node_modules"&&!hasBundledDependencies)continue;const name=path?`${path}/${entry.name}`:entry.name;if(found.size>=1e5)fail("Installed package has too many files");const fullPath=join(directory,name);if(entry.isDirectory())visit(name,depth+1);else{const identity=expected.get(name),stat=lstatSync(fullPath);if(!identity||!stat.isFile()||stat.size!==identity.bytes||hash(readFileSync(fullPath))!==identity.sha256)fail(`Installed package file ${name} differs from the verified archive`);found.add(name)}}}visit("",0);if(found.size!==expected.size)fail("Installed package is missing files from the verified archive")}function installedState(plan,cwd,archives){const lock=readFileSync(join(cwd,"bun.lock"),"utf8"),parsed=Bun.JSONC.parse(lock),nodeModules=realpathSync(join(cwd,"node_modules")),manifests=[];for(const source of plan.sources)for(const pkg of source.packages){const locked=parsed.packages?.[pkg.name];if(locked?.[0]!==`${pkg.name}@${pkg.version}`||locked.at(-1)!==pkg.integrity)fail(`Consumer lock does not contain ${pkg.name}@${pkg.version} with the original archive identity`);const directory=realpathSync(join(cwd,"node_modules",pkg.name)),path=relative(nodeModules,directory);if(!path||path.startsWith("..")||resolve(nodeModules,path)!==directory)fail(`${pkg.name} resolves outside the installed consumer packages`);const raw=readFileSync(join(directory,"package.json"),"utf8"),manifest=JSON.parse(raw);if(manifest.name!==pkg.name||manifest.version!==pkg.version)fail(`Installed ${pkg.name} does not match tested version ${pkg.version}`);verifyInstalledFiles(directory,archives.get(pkg.name));manifests.push(hash(raw))}return{lock,sha256:hash(lock),manifests:manifests.join(":")}}export async function verifyReleaseEvidence(plan,options){validateReleaseEvidencePlan(plan);const requestTimeoutMs=options.requestTimeoutMs??30000,checkTimeoutMs=options.checkTimeoutMs??300000,maxArchiveBytes=options.maxArchiveBytes??134217728,maxExpandedBytes=options.maxExpandedBytes??134217728;for(const number of[requestTimeoutMs,checkTimeoutMs,maxArchiveBytes,maxExpandedBytes])if(!Number.isSafeInteger(number)||number<=0)fail("Budgets must be positive safe integers");const read=options.fetch??fetch,archives=new Map,receipt={schemaVersion:1,verifiedAt:"",sources:[],consumer:{lockfile:"bun.lock",sha256:"",bunVersion:"",checks:[]}};async function request(url,github,limit=1048576){const timeout=AbortSignal.timeout(requestTimeoutMs),signal=options.signal?AbortSignal.any([options.signal,timeout]):timeout;signal.throwIfAborted();const response=await read(url,{method:"GET",redirect:"error",signal,headers:github?{Accept:"application/vnd.github+json",...options.token?{Authorization:`Bearer ${options.token}`}:{}}:{}});if(response.status!==200){await response.body?.cancel();fail(`${github?"Source evidence":"Published package"} request returned HTTP ${response.status}`)}return boundedBody(response,limit)}async function github(path){return JSON.parse(new TextDecoder().decode(await request(`https://api.github.com/repos/${path}`,!0)))}for(const source of plan.sources){let ref=(await github(`${source.repository}/git/ref/tags/${encodeURIComponent(source.tag)}`)).object;for(let depth=0;ref?.type==="tag"&&depth<5;depth++)ref=(await github(`${source.repository}/git/tags/${ref.sha}`)).object;if(ref?.type!=="commit"||ref.sha!==source.commit)fail(`${source.tag} does not identify ${source.commit}`);const manifest=await github(`${source.repository}/contents/package.json?ref=${source.commit}`);if(manifest.encoding!=="base64"||typeof manifest.content!=="string")fail("Source package manifest is unavailable");const releaseScript=JSON.parse(Buffer.from(manifest.content,"base64").toString("utf8")).scripts?.["release:patch"];if(typeof releaseScript!=="string"||!releaseScript.trim())fail(`${source.repository} has no existing release:patch command`);for(const gate of source.gates){const run=await github(`${source.repository}/actions/runs/${gate.runId}`);if(run.id!==gate.runId||run.repository?.full_name!==source.repository||run.path!==gate.workflow||run.head_sha!==source.commit)fail(`Release gate ${gate.runId} does not verify the exact source and workflow`);if(run.status!=="completed"||run.conclusion!=="success")fail(`Release gate ${gate.runId} is not successful`);const jobs=[];for(let page=1;page<=10;page++){const batch=await github(`${source.repository}/actions/runs/${gate.runId}/jobs?per_page=100&page=${page}`);if(!Array.isArray(batch.jobs))fail(`Release gate ${gate.runId} has no readable jobs`);jobs.push(...batch.jobs);if(batch.jobs.length<100)break;if(page===10)fail("Release job inventory exceeds its budget")}for(const name of gate.requiredJobs){const matches=jobs.filter((job)=>job.name===name);if(matches.length!==1||matches[0]?.status!=="completed"||matches[0]?.conclusion!=="success")fail(`Required release job ${name} did not pass exactly once`)}}const verified={...source,releaseCommand:"bun run release:patch",releaseScript,packages:[]};for(const pkg of source.packages){const metadata=JSON.parse(new TextDecoder().decode(await request(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}/${encodeURIComponent(pkg.version)}`,!1)));if(metadata.name!==pkg.name||metadata.version!==pkg.version||metadata.dist?.integrity!==pkg.integrity)fail(`Published ${pkg.name}@${pkg.version} does not match the original archive identity`);if(metadata.gitHead!==void 0&&metadata.gitHead!==source.commit)fail(`Published ${pkg.name} names a different source commit`);if(!metadata.dist?.tarball)fail(`Published ${pkg.name} has no archive URL`);const tarball=new URL(metadata.dist.tarball);if(tarball.origin!=="https://registry.npmjs.org"||tarball.username||tarball.password)fail("Archive URL is outside the public npm registry");const bytes=await request(tarball.href,!1,maxArchiveBytes);if(`sha512-${createHash("sha512").update(bytes).digest("base64")}`!==pkg.integrity)fail(`Published ${pkg.name}@${pkg.version} archive bytes failed SHA-512 verification`);const files=await archiveFiles(bytes,maxExpandedBytes);archives.set(pkg.name,files);const filesSha256=hash(JSON.stringify([...files].sort(([left],[right])=>left.localeCompare(right))));verified.packages.push({...pkg,tarball:tarball.href,bytes:bytes.byteLength,filesSha256})}receipt.sources.push(verified)}const initial=installedState(plan,options.cwd,archives),checks=await runProjectScripts({cwd:options.cwd,scripts:plan.checks,signal:options.signal,timeoutMs:checkTimeoutMs});receipt.consumer.bunVersion=checks.bunVersion;receipt.consumer.checks=checks.checks;const final=installedState(plan,options.cwd,archives);if(final.sha256!==initial.sha256||final.manifests!==initial.manifests)fail("Consumer checks changed the verified lockfile or package identities");receipt.consumer.sha256=initial.sha256;options.signal?.throwIfAborted();receipt.verifiedAt=new Date().toISOString();return receipt}
|
package/dist/release.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{parseOptions}from"@stacksjs/cli";import{app}from"@stacksjs/config";import{Action}from"@stacksjs/enums";import{log}from"@stacksjs/logging";import{projectPath}from"@stacksjs/path";import{runActions}from"./helpers/utils";import{resolvePantryToolchain}from"./pantry-toolchain";import{BYPASS_ENV,formatPreflightFailure,runPinnedChecks}from"./release-preflight";const raw=parseOptions()??{},passthrough={};for(const[k,v]of Object.entries(raw)){if(k==="--"||k==="_")continue;passthrough[k]=v}const isDryRun=passthrough.dryRun===!0||passthrough.dryRun==="true",actions=[Action.GenerateLibraryEntries,Action.Bump],preflight=await runPinnedChecks({cwd:projectPath()});if(preflight.bypassed)log.warn(`${BYPASS_ENV} is set, so the pinned checks were skipped for this release.`);else if(preflight.failures.length>0){await log.flush();await log.exit(formatPreflightFailure(preflight.failures),1)}const pantryToolchain=resolvePantryToolchain({cwd:projectPath()});if(!pantryToolchain.ok){await log.flush();await log.exit(pantryToolchain.message,1)}else log.info(`Pantry ${pantryToolchain.version} (${pantryToolchain.bin})${pantryToolchain.declared?` satisfies ts-pantry ${pantryToolchain.declared}`:""}`);const result=await runActions(actions,{cwd:projectPath(),...passthrough});if(result&&result.isErr)await log.exit(`Release failed: ${result.error?.message??String(result.error)}`,1);log.success(isDryRun?`Dry run complete for ${app.name}. Nothing was committed, tagged or pushed.`:`Successfully released ${app.name}`);
|
|
1
|
+
import{parseOptions}from"@stacksjs/cli";import{app}from"@stacksjs/config";import{Action}from"@stacksjs/enums";import{log}from"@stacksjs/logging";import{projectPath}from"@stacksjs/path";import{assertPublicationAllowed}from"./publication-preflight";import{runActions}from"./helpers/utils";import{resolvePantryToolchain}from"./pantry-toolchain";import{BYPASS_ENV,formatPreflightFailure,runPinnedChecks}from"./release-preflight";const raw=parseOptions()??{},passthrough={};for(const[k,v]of Object.entries(raw)){if(k==="--"||k==="_")continue;passthrough[k]=v}const isDryRun=passthrough.dryRun===!0||passthrough.dryRun==="true",actions=isDryRun?[Action.Bump]:[Action.GenerateLibraryEntries,Action.Bump];if(!isDryRun)await assertPublicationAllowed({cwd:projectPath(),channel:"github-release"});const preflight=await runPinnedChecks({cwd:projectPath()});if(preflight.bypassed)log.warn(`${BYPASS_ENV} is set, so the pinned checks were skipped for this release.`);else if(preflight.failures.length>0){await log.flush();await log.exit(formatPreflightFailure(preflight.failures),1)}const pantryToolchain=resolvePantryToolchain({cwd:projectPath()});if(!pantryToolchain.ok){await log.flush();await log.exit(pantryToolchain.message,1)}else log.info(`Pantry ${pantryToolchain.version} (${pantryToolchain.bin})${pantryToolchain.declared?` satisfies ts-pantry ${pantryToolchain.declared}`:""}`);const result=await runActions(actions,{cwd:projectPath(),...passthrough});if(result&&result.isErr)await log.exit(`Release failed: ${result.error?.message??String(result.error)}`,1);log.success(isDryRun?`Dry run complete for ${app.name}. Nothing was committed, tagged or pushed.`:`Successfully released ${app.name}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Copy the installed package into `storage/framework/defaults` and stamp it,
|
|
3
3
|
* touching nothing else: `buddy publish:defaults`, which must not also run the
|
|
4
|
-
* rest of an upgrade (support files, Bun pins,
|
|
4
|
+
* rest of an upgrade (support files, Bun pins, retiring framework types).
|
|
5
5
|
*/
|
|
6
6
|
export declare function vendorDefaultsTree(projectRoot: string, defaultsPackageRoot: string, options?: PackageProjectOptions): ProjectStructureChange[];
|
|
7
7
|
/**
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{chmodSync,existsSync,lstatSync,mkdirSync,readFileSync,readdirSync,rmSync,statSync,writeFileSync}from"node:fs";import{dirname,join,relative}from"node:path";import{DEFAULTS_SYNC_MARKER,defaultsPackagePath,usesVendoredDefaults}from"@stacksjs/path";const DEFAULTS_PACKAGE_IGNORES=new Set(["README.md","build.ts","node_modules","package.json","project","tests"]),LOCAL_DEFAULT_IGNORES=new Set([".DS_Store",".discovered-models.json",DEFAULTS_SYNC_MARKER,"dist","node_modules"]),LEGACY_PACKAGE_PROJECT_FILES=["
|
|
2
|
-
`)}function manifestVersion(packageRoot){try{const version=JSON.parse(readFileSync(join(packageRoot,"package.json"),"utf8"))?.version;return typeof version==="string"?version:null}catch{return null}}export function measureDefaultsDrift(projectRoot,options={}){const packageRoot=defaultsPackagePath(projectRoot);if(!existsSync(join(packageRoot,"package.json")))return null;return syncPackageProjectFiles(projectRoot,packageRoot,{dryRun:!0,shallow:options.shallow})}export function summarizeStructureChanges(changes){const added=changes.filter((change)=>change.action==="add").length,updated=changes.filter((change)=>change.action==="update").length,removed=changes.filter((change)=>change.action==="remove").length;return`+${added} ~${updated} -${removed}`}export function migratePackageProjectManifest(pkg){const changes=[],scripts=pkg.scripts??{},setScript=(name,value)=>{if(scripts[name]===value)return;scripts[name]=value;changes.push({path:`package.json#scripts.${name}`,action:"update"})};setScript("buddy","./buddy");const appCheck="bun x --bun tsc --noEmit -p tsconfig.json --pretty false";for(const[name,replacement]of Object.entries({typecheck:appCheck,"typecheck:app":appCheck,"types:check":"bun run typecheck"})){const current=scripts[name];if(!current){setScript(name,replacement);continue}const steps=current.match(/(?:[^&"'`]|"[^"\\]*(?:\\.[^"\\]*)*"|'[^']*')+/g);if(!steps||steps.join("&&")!==current||/[;|`$()<>#\\\n]/.test(current))continue;let changed=!1;const migrated=steps.flatMap((step)=>{const command=step.trim();if(/^(?:bun|npm|pnpm) run build:framework-types$/.test(command)){changed=!0;return[]}if(/^(?:(?:bun(?: x|x)?(?: --bun)?|npx) )?(?:tsc|tsgo)\b/.test(command)&&/storage\/framework\/(?:core(?:\/|['"\s]|$)|tsconfig\.(?:framework|defaults|type-tests)\.json)/.test(command)){changed=!0;return[appCheck]}return[command]});if(changed)setScript(name,migrated.length?migrated.join(" && "):replacement)}if(scripts["build:reset"]?.includes("storage/framework")
|
|
1
|
+
import{chmodSync,existsSync,lstatSync,mkdirSync,readFileSync,readdirSync,rmSync,statSync,writeFileSync}from"node:fs";import{dirname,join,relative}from"node:path";import{DEFAULTS_SYNC_MARKER,defaultsPackagePath,usesVendoredDefaults}from"@stacksjs/path";const DEFAULTS_PACKAGE_IGNORES=new Set(["README.md","build.ts","node_modules","package.json","project","tests"]),LOCAL_DEFAULT_IGNORES=new Set([".DS_Store",".discovered-models.json",DEFAULTS_SYNC_MARKER,"dist","node_modules"]),LEGACY_PACKAGE_PROJECT_FILES=["storage/framework/types/traits.d.ts"],SUPPORT_FILES=[{source:"project/buddy",target:"buddy",executable:!0},{source:"project/bootstrap",target:"bootstrap",executable:!0},{source:"project/storage/framework/tsconfig.app.json",target:"storage/framework/tsconfig.app.json"},{source:"project/storage/framework/tsconfig.base.json",target:"storage/framework/tsconfig.base.json"},{source:"project/storage/framework/server/tsconfig.docker.json",target:"storage/framework/server/tsconfig.docker.json"}];export const FRAMEWORK_TYPES_SUPPORT_DIRECTORY={source:"project/storage/framework/types",target:"storage/framework/types"},GENERATED_FRAMEWORK_TYPES=["server-auto-imports.d.ts","browser-auto-imports.d.ts"];function copyDirectoryIfChanged(source,target,projectRoot,changes,options,skip){for(const entry of readdirSync(source)){if(skip.has(entry)||LOCAL_DEFAULT_IGNORES.has(entry))continue;const sourcePath=join(source,entry),targetPath=join(target,entry);if(lstatSync(sourcePath).isDirectory())copyDirectoryIfChanged(sourcePath,targetPath,projectRoot,changes,options,skip);else copyFileIfChanged(sourcePath,targetPath,projectRoot,changes,options)}}function sameFile(left,right,shallow=!1){if(!existsSync(left)||!existsSync(right))return!1;const leftStat=statSync(left),rightStat=statSync(right);if(leftStat.size!==rightStat.size)return!1;if(shallow)return!0;return readFileSync(left).equals(readFileSync(right))}function copyFileIfChanged(source,target,projectRoot,changes,options,executable=!1){if(sameFile(source,target,options.shallow&&options.dryRun)){if(executable&&!options.dryRun)chmodSync(target,493);return}changes.push({path:relative(projectRoot,target),action:existsSync(target)?"update":"add"});if(options.dryRun)return;mkdirSync(dirname(target),{recursive:!0});writeFileSync(target,readFileSync(source));if(executable)chmodSync(target,493)}function syncDirectory(source,target,projectRoot,changes,options,isRoot=!1){if(!options.dryRun)mkdirSync(target,{recursive:!0});const sourceEntries=new Set;for(const entry of readdirSync(source)){if(isRoot&&DEFAULTS_PACKAGE_IGNORES.has(entry))continue;sourceEntries.add(entry);const sourcePath=join(source,entry),targetPath=join(target,entry);if(lstatSync(sourcePath).isDirectory()){syncDirectory(sourcePath,targetPath,projectRoot,changes,options);continue}copyFileIfChanged(sourcePath,targetPath,projectRoot,changes,options)}if(!existsSync(target))return;for(const entry of readdirSync(target)){if(LOCAL_DEFAULT_IGNORES.has(entry)||sourceEntries.has(entry))continue;const targetPath=join(target,entry);changes.push({path:relative(projectRoot,targetPath),action:"remove"});if(!options.dryRun)rmSync(targetPath,{recursive:!0,force:!0})}}export function vendorDefaultsTree(projectRoot,defaultsPackageRoot,options={}){const changes=[],targetDefaults=join(projectRoot,"storage/framework/defaults");syncDirectory(defaultsPackageRoot,targetDefaults,projectRoot,changes,options,!0);if(!options.dryRun)stampDefaultsSync(targetDefaults,defaultsPackageRoot);return changes}export function syncPackageProjectFiles(projectRoot,defaultsPackageRoot,options={}){const changes=[],targetDefaults=join(projectRoot,"storage/framework/defaults"),vendorDefaults=options.vendorDefaults??usesVendoredDefaults(projectRoot);if(vendorDefaults)syncDirectory(defaultsPackageRoot,targetDefaults,projectRoot,changes,options,!0);for(const file of SUPPORT_FILES){const source=join(defaultsPackageRoot,file.source);if(!existsSync(source))continue;copyFileIfChanged(source,join(projectRoot,file.target),projectRoot,changes,options,file.executable)}const typesSource=join(defaultsPackageRoot,FRAMEWORK_TYPES_SUPPORT_DIRECTORY.source);if(existsSync(typesSource))copyDirectoryIfChanged(typesSource,join(projectRoot,FRAMEWORK_TYPES_SUPPORT_DIRECTORY.target),projectRoot,changes,options,new Set(GENERATED_FRAMEWORK_TYPES));const bunVersion=shippedBunVersion(defaultsPackageRoot);if(bunVersion)alignBunPins(projectRoot,bunVersion,changes,options);for(const legacyFile of LEGACY_PACKAGE_PROJECT_FILES){const target=join(projectRoot,legacyFile);if(!existsSync(target))continue;changes.push({path:legacyFile,action:"remove"});if(!options.dryRun)rmSync(target,{force:!0})}if(!options.dryRun&&vendorDefaults)stampDefaultsSync(targetDefaults,defaultsPackageRoot);return changes}export const BUN_VERSION_FILE="project/bun-version";function shippedBunVersion(defaultsPackageRoot){try{const version=readFileSync(join(defaultsPackageRoot,BUN_VERSION_FILE),"utf8").trim();return/^\d+\.\d+\.\d+$/.test(version)?version:null}catch{return null}}function olderVersion(candidate,than){const left=candidate.split(".").map(Number),right=than.split(".").map(Number);for(let i=0;i<3;i++)if(left[i]!==right[i])return(left[i]??0)<(right[i]??0);return!1}const VERSION="(\\d+\\.\\d+\\.\\d+)",BUN_PINS=[new RegExp(`^(\\s*BUN_VERSION:\\s*['"]?)${VERSION}`,"gm"),new RegExp(`^(\\s*bun-version:\\s*['"]?)${VERSION}`,"gm"),new RegExp(`((?:^|[\\s'"])bun(?:\\.sh|\\.com)?@)${VERSION}`,"gm"),new RegExp(`(oven/bun:)${VERSION}`,"g"),new RegExp(`^(\\s*ARG\\s+BUN_VERSION=)${VERSION}`,"gm")];export function raiseBunPins(text,version){let next=text;for(const pattern of BUN_PINS)next=next.replace(pattern,(match,prefix,pinned)=>olderVersion(pinned,version)?`${prefix}${version}`:match);return next}function bunPinFiles(projectRoot){const files=[],workflows=join(projectRoot,".github/workflows");if(existsSync(workflows)){for(const entry of readdirSync(workflows))if(/\.ya?ml$/.test(entry))files.push(join(workflows,entry))}for(const entry of readdirSync(projectRoot))if(/^Dockerfile/.test(entry))files.push(join(projectRoot,entry));for(const path of["storage/framework/Dockerfile","storage/framework/server/Dockerfile"])if(existsSync(join(projectRoot,path)))files.push(join(projectRoot,path));return files}export function alignBunPins(projectRoot,version,changes=[],options={}){for(const file of bunPinFiles(projectRoot)){const text=readFileSync(file,"utf8"),next=raiseBunPins(text,version);if(next===text)continue;changes.push({path:relative(projectRoot,file),action:"update"});if(!options.dryRun)writeFileSync(file,next)}return changes}function stampDefaultsSync(targetDefaults,defaultsPackageRoot){const version=manifestVersion(defaultsPackageRoot);if(!version)return;const marker={version,syncedAt:new Date().toISOString()};mkdirSync(targetDefaults,{recursive:!0});writeFileSync(join(targetDefaults,DEFAULTS_SYNC_MARKER),`${JSON.stringify(marker,null,2)}
|
|
2
|
+
`)}function manifestVersion(packageRoot){try{const version=JSON.parse(readFileSync(join(packageRoot,"package.json"),"utf8"))?.version;return typeof version==="string"?version:null}catch{return null}}export function measureDefaultsDrift(projectRoot,options={}){const packageRoot=defaultsPackagePath(projectRoot);if(!existsSync(join(packageRoot,"package.json")))return null;return syncPackageProjectFiles(projectRoot,packageRoot,{dryRun:!0,shallow:options.shallow})}export function summarizeStructureChanges(changes){const added=changes.filter((change)=>change.action==="add").length,updated=changes.filter((change)=>change.action==="update").length,removed=changes.filter((change)=>change.action==="remove").length;return`+${added} ~${updated} -${removed}`}export function migratePackageProjectManifest(pkg){const changes=[],scripts=pkg.scripts??{},setScript=(name,value)=>{if(scripts[name]===value)return;scripts[name]=value;changes.push({path:`package.json#scripts.${name}`,action:"update"})};setScript("buddy","./buddy");const appCheck="bun x --bun tsc --noEmit -p tsconfig.json --pretty false";for(const[name,replacement]of Object.entries({typecheck:appCheck,"typecheck:app":appCheck,"types:check":"bun run typecheck"})){const current=scripts[name];if(!current){setScript(name,replacement);continue}const steps=current.match(/(?:[^&"'`]|"[^"\\]*(?:\\.[^"\\]*)*"|'[^']*')+/g);if(!steps||steps.join("&&")!==current||/[;|`$()<>#\\\n]/.test(current))continue;let changed=!1;const migrated=steps.flatMap((step)=>{const command=step.trim();if(/^(?:bun|npm|pnpm) run build:framework-types$/.test(command)){changed=!0;return[]}if(/^(?:(?:bun(?: x|x)?(?: --bun)?|npx) )?(?:tsc|tsgo)\b/.test(command)&&/storage\/framework\/(?:core(?:\/|['"\s]|$)|tsconfig\.(?:framework|defaults|type-tests)\.json)/.test(command)){changed=!0;return[appCheck]}return[command]});if(changed)setScript(name,migrated.length?migrated.join(" && "):replacement)}if(scripts["build:reset"]?.includes("storage/framework")||scripts["build:reset"]==="rm -rf node_modules pantry.lock && bun install && ./buddy generate:types && ./buddy lint:fix")setScript("build:reset","rm -rf node_modules && bun install && ./buddy generate:types && ./buddy lint:fix");if(scripts["build:framework-types"]&&!Object.entries(scripts).some(([name,command])=>name!=="build:framework-types"&&command.includes("build:framework-types"))){delete scripts["build:framework-types"];changes.push({path:"package.json#scripts.build:framework-types",action:"remove"})}pkg.scripts=scripts;if(Array.isArray(pkg.workspaces)){const next=pkg.workspaces.filter((workspace)=>workspace!=="storage/framework/core"&&!workspace.startsWith("storage/framework/core/"));if(next.length!==pkg.workspaces.length){pkg.workspaces=next;changes.push({path:"package.json#workspaces",action:"update"})}}return changes}export function migratePackageProjectTsconfig(projectRoot,options={}){const path=join(projectRoot,"tsconfig.json");if(!existsSync(path))return[];const raw=readFileSync(path,"utf8"),next=raw.replace(/"extends"\s*:\s*"\.\/storage\/framework\/core\/tsconfig\.json"/,'"extends": "./storage/framework/tsconfig.app.json"');if(next===raw)return[];if(!options.dryRun)writeFileSync(path,next);return[{path:"tsconfig.json",action:"update"}]}export function detectProjectAiProviders(projectRoot){const providers=new Set(["codex"]);if(existsSync(join(projectRoot,"CLAUDE.md"))||existsSync(join(projectRoot,".claude")))providers.add("claude");if(existsSync(join(projectRoot,".cursor")))providers.add("cursor");if(existsSync(join(projectRoot,".github/copilot-instructions.md")))providers.add("copilot");if(existsSync(join(projectRoot,"GEMINI.md")))providers.add("gemini");return[...providers]}
|
package/dist/upgrade/packages.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync,readdirSync,readFileSync,
|
|
1
|
+
import{existsSync,lstatSync,readdirSync,readFileSync,writeFileSync}from"node:fs";import{join,relative}from"node:path";import process from"node:process";import{runCommand}from"@stacksjs/cli";import{installedDefaultsVersion}from"@stacksjs/path";import{runPostSyncDatabaseTypesRefresh}from"./framework-hooks";import{detectProjectAiProviders,measureDefaultsDrift,migratePackageProjectManifest,migratePackageProjectTsconfig,summarizeStructureChanges,syncPackageProjectFiles}from"./package-project";import{describeDanglingRouteActions,findDanglingRouteActions,pruneRetiredScaffoldRoutes}from"./route-actions";export function hasStacksDependency(pkg){return Boolean(pkg.dependencies?.stacks||pkg.devDependencies?.stacks)}export function standalonePackageUpdateCommand(){return"bun update"}export function frameworkPackageUpdateCommand(force=!1){return force?"bun update stacks --force":"bun update stacks"}export function installedStacksVersion(projectRoot){try{const manifest=JSON.parse(readFileSync(join(projectRoot,"node_modules/stacks/package.json"),"utf-8"));return typeof manifest.version==="string"?manifest.version:null}catch{return null}}export function needsFrameworkInstall(projectRoot,target){return installedStacksVersion(projectRoot)!==target}async function updateStandalonePackage(projectRoot,options){console.log(`
|
|
2
2
|
Standalone package detected. No Stacks framework source is installed.`);if(options.dryRun){console.log(" --dry-run: would refresh the package dependencies with `bun update`. No files were changed.\n");process.exit(0)}if(options.noPostinstall){console.log(" --no-postinstall: skipping the dependency refresh. Run `bun update` when ready.\n");process.exit(0)}console.log(` Refreshing declared dependencies...
|
|
3
3
|
`);if((await runCommand(standalonePackageUpdateCommand(),{cwd:projectRoot})).isErr){console.error("\n\u2717 The dependency refresh failed. Resolve the reported error and re-run `buddy update`.\n");process.exit(1)}console.log(`
|
|
4
4
|
\u2714 Standalone package dependencies are up to date.
|
|
@@ -20,6 +20,6 @@ import{existsSync,readdirSync,readFileSync,statSync,writeFileSync}from"node:fs";
|
|
|
20
20
|
`);process.exit(0)}function reportRouteActions(projectRoot,dryRun){const pruned=pruneRetiredScaffoldRoutes(projectRoot,{dryRun});for(const route of pruned)console.log(` ${dryRun?"Would remove":"Removed"} ${route.file}:${route.line}, the scaffold route to ${route.action}: removed in ${route.removedIn}, ${route.reason}`);const dangling=findDanglingRouteActions(projectRoot);if(dangling.length>0){console.warn(`
|
|
21
21
|
${dangling.length} route${dangling.length===1?"":"s"} name${dangling.length===1?"s":""} an action this version does not ship. Point ${dangling.length===1?"it":"them"} at an action that exists, or delete ${dangling.length===1?"it":"them"}:`);for(const line of describeDanglingRouteActions(dangling).split(`
|
|
22
22
|
`))console.warn(` ${line}`);console.warn("")}return pruned.length}async function regenerateAutoImports(projectRoot){const proc=Bun.spawn({cmd:[process.execPath,"-e","await (await import('@stacksjs/server')).generateAutoImportFiles()"],cwd:projectRoot,stdout:"ignore",stderr:"pipe"});if(await proc.exited!==0){const stderr=(await new Response(proc.stderr).text()).trim();console.warn(` auto-imports were not regenerated (non-fatal): ${stderr.split(`
|
|
23
|
-
`).pop()||`exit ${proc.exitCode}`}`);return}console.log(" Regenerated auto-imports")}async function regenerateDatabaseTypes(projectRoot){const buddy=join(projectRoot,"buddy");if(!existsSync(buddy))return;try{if(await runPostSyncDatabaseTypesRefresh({cmd:[buddy,"generate:db-types"],projectRoot,spawn:(spawnOptions)=>Bun.spawn(spawnOptions),exists:existsSync}))console.log(" Regenerated database/types.d.ts")}catch(err){console.warn(` database/types.d.ts was not regenerated (non-fatal): ${err.message} Run \`./buddy generate:db-types\`.`)}}export function resolveManifestSpec(name,spec,target,context,members){if(name!=="stacks"&&!name.startsWith("@stacksjs/"))return null;if(spec.startsWith("workspace:")&&members?.has(name))return null;if(context&&!context.lockstep.has(name)){const declared=context.metaDeps[name];if(!declared)return null;return baseVersion(declared)===baseVersion(spec)?null:declared}if(spec.startsWith("workspace:"))return`^${target}`;if(baseVersion(spec)===target)return null;if(!/^[\^~]?\d/.test(spec))return null;return`${/^[\^~]/.test(spec)?spec[0]:""}${target}`}export function workspaceMembers(projectRoot){const members=new Set;let patterns=[];try{const root=JSON.parse(readFileSync(join(projectRoot,"package.json"),"utf-8"));patterns=((Array.isArray(root.workspaces)?root.workspaces:root.workspaces?.packages)??[]).filter((one)=>typeof one==="string")}catch{return members}const manifestsFor=(pattern)=>{return[...new Bun.Glob(`${pattern.replace(/\/+$/,"")}/package.json`).scanSync({cwd:projectRoot,onlyFiles:!0})].filter((file)=>!file.split("/").includes("node_modules"))},excluded=new Set(patterns.filter((one)=>one.startsWith("!")).flatMap((one)=>manifestsFor(one.slice(1))));for(const pattern of patterns.filter((one)=>!one.startsWith("!")))for(const file of manifestsFor(pattern)){if(excluded.has(file))continue;try{const name=JSON.parse(readFileSync(join(projectRoot,file),"utf-8")).name;if(typeof name==="string"&&name)members.add(name)}catch{}}return members}export function reconcileVendoredManifests(projectRoot,target,options={}){const root=join(projectRoot,"storage/framework");if(!
|
|
23
|
+
`).pop()||`exit ${proc.exitCode}`}`);return}console.log(" Regenerated auto-imports")}async function regenerateDatabaseTypes(projectRoot){const buddy=join(projectRoot,"buddy");if(!existsSync(buddy))return;try{if(await runPostSyncDatabaseTypesRefresh({cmd:[buddy,"generate:db-types"],projectRoot,spawn:(spawnOptions)=>Bun.spawn(spawnOptions),exists:existsSync}))console.log(" Regenerated database/types.d.ts")}catch(err){console.warn(` database/types.d.ts was not regenerated (non-fatal): ${err.message} Run \`./buddy generate:db-types\`.`)}}export function resolveManifestSpec(name,spec,target,context,members){if(name!=="stacks"&&!name.startsWith("@stacksjs/"))return null;if(spec.startsWith("workspace:")&&members?.has(name))return null;if(context&&!context.lockstep.has(name)){const declared=context.metaDeps[name];if(!declared)return null;return baseVersion(declared)===baseVersion(spec)?null:declared}if(spec.startsWith("workspace:"))return`^${target}`;if(baseVersion(spec)===target)return null;if(!/^[\^~]?\d/.test(spec))return null;return`${/^[\^~]/.test(spec)?spec[0]:""}${target}`}export function workspaceMembers(projectRoot){const members=new Set;let patterns=[];try{const root=JSON.parse(readFileSync(join(projectRoot,"package.json"),"utf-8"));patterns=((Array.isArray(root.workspaces)?root.workspaces:root.workspaces?.packages)??[]).filter((one)=>typeof one==="string")}catch{return members}const manifestsFor=(pattern)=>{return[...new Bun.Glob(`${pattern.replace(/\/+$/,"")}/package.json`).scanSync({cwd:projectRoot,onlyFiles:!0})].filter((file)=>!file.split("/").includes("node_modules"))},excluded=new Set(patterns.filter((one)=>one.startsWith("!")).flatMap((one)=>manifestsFor(one.slice(1))));for(const pattern of patterns.filter((one)=>!one.startsWith("!")))for(const file of manifestsFor(pattern)){if(excluded.has(file))continue;try{const name=JSON.parse(readFileSync(join(projectRoot,file),"utf-8")).name;if(typeof name==="string"&&name)members.add(name)}catch{}}return members}export function reconcileVendoredManifests(projectRoot,target,options={}){const root=join(projectRoot,"storage/framework");try{if(![join(projectRoot,"storage"),root].every((dir)=>lstatSync(dir).isDirectory()))return[]}catch{return[]}const changes=[],members=workspaceMembers(projectRoot),walk=(dir)=>{let entries;try{entries=readdirSync(dir)}catch{return}for(const entry of entries){if(entry==="node_modules"||entry==="dist"||entry===".git")continue;const full=join(dir,entry);let stat;try{stat=lstatSync(full)}catch{continue}if(stat.isSymbolicLink())continue;if(stat.isDirectory()){walk(full);continue}if(entry!=="package.json")continue;let raw;try{raw=readFileSync(full,"utf-8")}catch{continue}let pkg;try{pkg=JSON.parse(raw)}catch{continue}let touched=!1;for(const field of["dependencies","devDependencies"]){const deps=pkg[field];if(!deps)continue;for(const[name,spec]of Object.entries(deps)){const next=resolveManifestSpec(name,spec,target,options.context,members);if(!next)continue;changes.push({file:relative(projectRoot,full),name,from:spec,to:next});deps[name]=next;touched=!0}}if(touched&&!options.dryRun){const suffix=raw.endsWith(`
|
|
24
24
|
`)?`
|
|
25
25
|
`:"";writeFileSync(full,`${JSON.stringify(pkg,null,2)}${suffix}`)}}};walk(root);return changes}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/actions",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.257",
|
|
6
6
|
"description": "The Stacks actions.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -66,55 +66,55 @@
|
|
|
66
66
|
"prepublishOnly": "bun run build"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@stacksjs/action-runner": "0.75.
|
|
70
|
-
"@stacksjs/ai": "0.75.
|
|
71
|
-
"@stacksjs/analytics": "0.75.
|
|
72
|
-
"@stacksjs/api": "0.75.
|
|
73
|
-
"@stacksjs/auth": "0.75.
|
|
74
|
-
"@stacksjs/build": "0.75.
|
|
69
|
+
"@stacksjs/action-runner": "0.75.257",
|
|
70
|
+
"@stacksjs/ai": "0.75.257",
|
|
71
|
+
"@stacksjs/analytics": "0.75.257",
|
|
72
|
+
"@stacksjs/api": "0.75.257",
|
|
73
|
+
"@stacksjs/auth": "0.75.257",
|
|
74
|
+
"@stacksjs/build": "0.75.257",
|
|
75
75
|
"@stacksjs/bumpx": "^0.2.25",
|
|
76
76
|
"@stacksjs/bunpress": "^0.2.15",
|
|
77
|
-
"@stacksjs/cli": "0.75.
|
|
78
|
-
"@stacksjs/cloud": "0.75.
|
|
79
|
-
"@stacksjs/cms": "0.75.
|
|
80
|
-
"@stacksjs/components": "
|
|
81
|
-
"@stacksjs/config": "0.75.
|
|
82
|
-
"@stacksjs/database": "0.75.
|
|
83
|
-
"@stacksjs/desktop-build": "0.75.
|
|
84
|
-
"@stacksjs/dns": "0.75.
|
|
85
|
-
"@stacksjs/enums": "0.75.
|
|
86
|
-
"@stacksjs/env": "0.75.
|
|
87
|
-
"@stacksjs/error-handling": "0.75.
|
|
88
|
-
"@stacksjs/github": "0.75.
|
|
77
|
+
"@stacksjs/cli": "0.75.257",
|
|
78
|
+
"@stacksjs/cloud": "0.75.257",
|
|
79
|
+
"@stacksjs/cms": "0.75.257",
|
|
80
|
+
"@stacksjs/components": "0.2.644",
|
|
81
|
+
"@stacksjs/config": "0.75.257",
|
|
82
|
+
"@stacksjs/database": "0.75.257",
|
|
83
|
+
"@stacksjs/desktop-build": "0.75.257",
|
|
84
|
+
"@stacksjs/dns": "0.75.257",
|
|
85
|
+
"@stacksjs/enums": "0.75.257",
|
|
86
|
+
"@stacksjs/env": "0.75.257",
|
|
87
|
+
"@stacksjs/error-handling": "0.75.257",
|
|
88
|
+
"@stacksjs/github": "0.75.257",
|
|
89
89
|
"@stacksjs/gitit": "^0.2.5",
|
|
90
|
-
"@stacksjs/i18n": "0.75.
|
|
91
|
-
"@stacksjs/image": "0.75.
|
|
92
|
-
"@stacksjs/logging": "0.75.
|
|
90
|
+
"@stacksjs/i18n": "0.75.257",
|
|
91
|
+
"@stacksjs/image": "0.75.257",
|
|
92
|
+
"@stacksjs/logging": "0.75.257",
|
|
93
93
|
"@stacksjs/logsmith": "^0.2.11",
|
|
94
|
-
"@stacksjs/mobile": "0.75.
|
|
95
|
-
"@stacksjs/orm": "0.75.
|
|
96
|
-
"@stacksjs/path": "0.75.
|
|
97
|
-
"@stacksjs/payments": "0.75.
|
|
98
|
-
"@stacksjs/queue": "0.75.
|
|
99
|
-
"@stacksjs/registry": "0.75.
|
|
100
|
-
"@stacksjs/router": "0.75.
|
|
94
|
+
"@stacksjs/mobile": "0.75.257",
|
|
95
|
+
"@stacksjs/orm": "0.75.257",
|
|
96
|
+
"@stacksjs/path": "0.75.257",
|
|
97
|
+
"@stacksjs/payments": "0.75.257",
|
|
98
|
+
"@stacksjs/queue": "0.75.257",
|
|
99
|
+
"@stacksjs/registry": "0.75.257",
|
|
100
|
+
"@stacksjs/router": "0.75.257",
|
|
101
101
|
"@stacksjs/rpx": "^0.11.54",
|
|
102
|
-
"@stacksjs/scheduler": "0.75.
|
|
103
|
-
"@stacksjs/search-engine": "0.75.
|
|
104
|
-
"@stacksjs/security": "0.75.
|
|
105
|
-
"@stacksjs/server": "0.75.
|
|
106
|
-
"@stacksjs/sites": "0.75.
|
|
107
|
-
"@stacksjs/storage": "0.75.
|
|
108
|
-
"@stacksjs/strings": "0.75.
|
|
109
|
-
"@stacksjs/stx": "
|
|
102
|
+
"@stacksjs/scheduler": "0.75.257",
|
|
103
|
+
"@stacksjs/search-engine": "0.75.257",
|
|
104
|
+
"@stacksjs/security": "0.75.257",
|
|
105
|
+
"@stacksjs/server": "0.75.257",
|
|
106
|
+
"@stacksjs/sites": "0.75.257",
|
|
107
|
+
"@stacksjs/storage": "0.75.257",
|
|
108
|
+
"@stacksjs/strings": "0.75.257",
|
|
109
|
+
"@stacksjs/stx": "0.2.644",
|
|
110
110
|
"@stacksjs/tlsx": "^0.13.19",
|
|
111
111
|
"@stacksjs/ts-cloud": "^0.16.58",
|
|
112
112
|
"@stacksjs/ts-md": "^0.1.1",
|
|
113
|
-
"@stacksjs/tunnel": "0.75.
|
|
114
|
-
"@stacksjs/types": "0.75.
|
|
115
|
-
"@stacksjs/utils": "0.75.
|
|
116
|
-
"@stacksjs/whois": "0.75.
|
|
117
|
-
"bun-plugin-stx": "
|
|
113
|
+
"@stacksjs/tunnel": "0.75.257",
|
|
114
|
+
"@stacksjs/types": "0.75.257",
|
|
115
|
+
"@stacksjs/utils": "0.75.257",
|
|
116
|
+
"@stacksjs/whois": "0.75.257",
|
|
117
|
+
"bun-plugin-stx": "0.2.644"
|
|
118
118
|
},
|
|
119
119
|
"peerDependencies": {
|
|
120
120
|
"craft-native": ">=0.0.272",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
}
|
|
130
130
|
},
|
|
131
131
|
"devDependencies": {
|
|
132
|
-
"@stacksjs/validation": "0.75.
|
|
132
|
+
"@stacksjs/validation": "0.75.257",
|
|
133
133
|
"better-dx": "^0.2.24"
|
|
134
134
|
}
|
|
135
135
|
}
|