@stacksjs/buddy 0.70.379 → 0.71.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1,4 @@
|
|
|
1
|
-
import process from"node:process";import{log}from"@stacksjs/cli";function parseSafariPlatforms(platform){if(!platform)return;if(platform==="all")return["macos","ios"];if(platform==="macos"||platform==="ios")return[platform];throw Error(`Invalid Safari platform ${platform}; use macos, ios, or all`)}export function extension(buddy){const load=async()=>{const{loadExtensionConfig}=await import("@stacksjs/browser-extension"),config=await loadExtensionConfig(process.cwd());if(!config){log.error("No extension config found. Create `config/extension.ts` exporting `defineExtension({ \u2026 })`.");process.exit(1)}const pkg=await Bun.file(`${process.cwd()}/package.json`).json().catch(()=>({}));return{config,version:pkg.version??"0.0.0"}};buddy.command("extension:build","Build the browser extension (Chrome + Firefox + Safari) from config/extension.ts").option("--target <target>","Build a single target (chrome | firefox | safari); omit to build all").option("--version <version>","Override the extension version (defaults to package.json)").action(async(options)=>{const{buildExtension,buildAllTargets}=await import("@stacksjs/browser-extension"),{config,version}=await load(),v=options.version??version;if(options.target){const{outdir}=await buildExtension(config,{target:options.target,version:v});log.success(`Built ${config.name} ${v} (${options.target}) \u2192 ${outdir}`)}else{await buildAllTargets(config,{version:v});log.success(`Built ${config.name} ${v} for ${(config.targets??["chrome","firefox"]).join(", ")}`)}});buddy.command("extension:init","Scaffold a Chrome, Firefox, or Safari extension, including the Safari Xcode app").option("--name <name>","Extension display name").option("--target <target>","Scaffold chrome, firefox, safari, or all (default all)").option("--bundle-id <id>","Safari container bundle identifier").option("--team-id <id>","Apple Developer team used for Safari signing").option("--platform <platform>","Safari platform: macos, ios, or all (default all)").option("--force","Overwrite existing starter and Safari scaffold files").action(async(options)=>{const target=options.target??"all";if(!["chrome","firefox","safari","all"].includes(target))throw Error(`Invalid extension target ${target}; use chrome, firefox, safari, or all`);const{scaffoldExtensionProject}=await import("@stacksjs/browser-extension"),result=await scaffoldExtensionProject({name:options.name,target,bundleId:options.bundleId,teamId:options.teamId,platforms:parseSafariPlatforms(options.platform)??["macos","ios"],force:Boolean(options.force)});for(const file of result.written)log.success(`created ${file}`);for(const file of result.skipped)log.info(`skip (exists): ${file}`);if(result.safari)log.success(`Scaffolded the Safari container app \u2192 ${result.safari.dir}`);log.info("Next: add icons under public/icons, then run `buddy extension:build`.")});buddy.command("extension:package","Build + zip the browser extension into store-ready archives").option("--target <target>","Package a single target (chrome | firefox | safari); omit to package all").option("--version <version>","Override the extension version (defaults to package.json)").action(async(options)=>{const{packageExtension}=await import("@stacksjs/browser-extension"),{config,version}=await load(),v=options.version??version,targets=options.target?[options.target]:config.targets??["chrome","firefox"];for(const target of targets){const out=await packageExtension(config,{target,version:v});log.success(`Packaged ${config.name} (${target}) \u2192 ${out}`)}});buddy.command("extension:chrome:status","Fetch the Chrome Web Store item status").option("--service-account-path <path>","Google service-account JSON key path").option("--access-token <token>","Short-lived Chrome Web Store OAuth access token").action(async(options)=>{const{ChromeWebStoreClient}=await import("@stacksjs/browser-extension"),{config}=await load();if(!config.chromeWebStore)throw Error("Chrome status needs chromeWebStore.publisherId and chromeWebStore.itemId in config/extension.ts");const status=await new ChromeWebStoreClient(options).fetchStatus(config.chromeWebStore);log.info(`Chrome Web Store item ${status.itemId}`);log.info(`published: ${status.publishedItemRevisionStatus?.state??"none"}`);log.info(`submitted: ${status.submittedItemRevisionStatus?.state??"none"}`);if(status.warned)log.warn("Chrome has warned this item for a policy violation.");if(status.takenDown)log.error("Chrome has taken this item down for a policy violation.")});buddy.command("extension:chrome:publish","Build, upload, and submit the Chrome extension through Web Store API v2").option("--version <version>","Override the extension version (defaults to package.json)").option("--service-account-path <path>","Google service-account JSON key path").option("--access-token <token>","Short-lived Chrome Web Store OAuth access token").option("--upload-only","Upload without submitting the item for review").option("--allow-warnings","Submit even when Chrome reports validation warnings").action(async(options)=>{const{publishChromeExtension}=await import("@stacksjs/browser-extension"),{config,version}=await load(),result=await publishChromeExtension(config,{version:options.version??version,serviceAccountPath:options.serviceAccountPath,accessToken:options.accessToken,uploadOnly:Boolean(options.uploadOnly),blockOnWarnings:!options.allowWarnings});if(result.deferred){log.warn(`${result.deferred.reason}. Version ${options.version??version} remains queued for the next automated retry.`);return}if(result.alreadyPublished){log.success(result.alreadyPublished.reason);return}log.success(`Uploaded Chrome package ${result.packagePath} (${result.upload?.crxVersion??"processing complete"})`);if(result.publish)log.success(`Submitted Chrome Web Store item ${result.publish.itemId}: ${result.publish.state}`)});buddy.command("extension:firefox:previews","Sync the Firefox listing screenshots declared in config/extension.ts").option("--api-key <issuer>","AMO JWT issuer").option("--api-secret <secret>","AMO JWT secret").option("--dry-run","Report what would change without touching the listing").action(async(options)=>{const{syncFirefoxPreviews}=await import("@stacksjs/browser-extension"),{config}=await load(),result=await syncFirefoxPreviews(config,{issuer:options.apiKey,secret:options.apiSecret,dryRun:options.dryRun});if(result.unchanged)log.info("Firefox listing screenshots already match config/extension.ts");else if(options.dryRun)log.info(`Would replace ${result.removed.length} Firefox listing screenshot(s)`);else log.success(`Synced ${result.uploaded.length} Firefox listing screenshot(s), removed ${result.removed.length}`)});buddy.command("extension:firefox:publish","Build and submit the Firefox extension through Mozilla Add-ons").option("--version <version>","Override the extension version (defaults to package.json)").option("--api-key <issuer>","AMO JWT issuer").option("--api-secret <secret>","AMO JWT secret").option("--source-code <path>","Human-readable source archive for AMO review").option("--approval-timeout <milliseconds>","How long to wait for human approval (default 0)").action(async(options)=>{const{publishFirefoxExtension}=await import("@stacksjs/browser-extension"),{config,version}=await load(),result=await publishFirefoxExtension(config,{version:options.version??version,issuer:options.apiKey,secret:options.apiSecret,sourceCodePath:options.sourceCode,approvalTimeout:options.approvalTimeout===void 0?void 0:Number(options.approvalTimeout)});log.success(`Submitted Firefox extension (${result.channel}) \u2192 ${result.artifactsDir}`);if(config.firefoxAddons?.screenshots?.length)try{const{syncFirefoxPreviews}=await import("@stacksjs/browser-extension"),previews=await syncFirefoxPreviews(config,{issuer:options.apiKey,secret:options.apiSecret});if(previews.unchanged)log.info("Firefox listing screenshots already match");else log.success(`Synced ${previews.uploaded.length} Firefox listing screenshot(s), removed ${previews.removed.length}`)}catch(error){log.warn(`[extension:firefox:publish] listing screenshots left as they were: ${error.message}`)}if(result.artifacts.length)log.info(`new artifacts: ${result.artifacts.join(", ")}`)});buddy.command("extension:safari:provision","Register Safari Bundle IDs and check the App Store Connect app record").option("--api-key-id <id>","App Store Connect API key ID").option("--api-issuer-id <id>","App Store Connect API issuer ID").option("--api-key-path <path>","Path to the App Store Connect AuthKey_*.p8 file").option("--check","Report missing resources without creating Bundle IDs").option("--version <version>","Create or align App Store versions (defaults to package.json)").option("--platform <platform>","Provision macos, ios, or all (defaults to config safariPlatforms)").action(async(options)=>{const{provisionSafariApp}=await import("@stacksjs/browser-extension"),{config,version}=await load(),result=await provisionSafariApp(config,{keyId:options.apiKeyId,issuerId:options.apiIssuerId,keyPath:options.apiKeyPath,checkOnly:Boolean(options.check),version:options.version??version,platforms:parseSafariPlatforms(options.platform)});for(const resource of[result.container,result.extension])if(resource.created)log.success(`Registered Bundle ID ${resource.identifier}`);else if(resource.exists)log.success(`Bundle ID exists: ${resource.identifier}`);else log.warn(`Bundle ID is missing: ${resource.identifier}`);if(result.appRecord.exists)log.success(`App Store Connect app record exists (${result.appRecord.id})`);else log.warn("App Store Connect app record is missing. Apple requires creating it in the App Store Connect website.");for(const appStoreVersion of result.appStoreVersions){const action=appStoreVersion.created?"Created":appStoreVersion.updated?"Updated":"Ready";log.success(`${action} Safari ${appStoreVersion.platform} App Store version ${appStoreVersion.version}`)}});buddy.command("extension:safari:init","Scaffold the Safari container app (Xcode project) from the template").option("--bundle-id <id>","Base bundle identifier (defaults to config safariBundleId)").option("--dir <dir>","Output directory for the Xcode project (default safari)").option("--force","Overwrite existing scaffold files").option("--team-id <id>","Apple Developer team used for signing").action(async(options)=>{const{scaffoldSafariApp}=await import("@stacksjs/browser-extension"),{config}=await load(),{dir,written,skipped}=await scaffoldSafariApp(config,{bundleId:options.bundleId,dir:options.dir,force:Boolean(options.force),teamId:options.teamId});log.success(`Scaffolded the Safari container app \u2192 ${dir} (${written.length} files)`);if(skipped.length)log.info(`kept ${skipped.length} existing files (use --force to overwrite)`)});buddy.command("extension:safari:app","Build the extension and its macOS, iPhone, and iPad Safari container apps").option("--release","Build the Release configuration (default Debug)").option("--signed","Sign locally against the Apple ID in Xcode (local builds only \u2014 see below)").option("--skip-xcodebuild","Only build + sync the extension payload").option("--version <version>","Override the extension version (defaults to package.json)").option("--platform <platform>","Build macos, ios, or all (defaults to config safariPlatforms)").action(async(options)=>{const{buildSafariApp,buildSafariUniversalApp}=await import("@stacksjs/browser-extension"),{config,version}=await load(),platforms=parseSafariPlatforms(options.platform)??config.safariPlatforms??["macos"];if(platforms.includes("ios")){const result=await buildSafariUniversalApp(config,{version:options.version??version,release:Boolean(options.release),signed:Boolean(options.signed),skipXcodebuild:Boolean(options.skipXcodebuild),platforms});for(const platform of platforms){const appPath=result.appPaths[platform];if(appPath)log.success(`Built Safari ${platform} app ${appPath}`)}if(options.skipXcodebuild)log.success(`Generated universal Safari project \u2192 ${result.project}`);if(result.appPaths.macos)log.info("Open the macOS app once, then enable the extension in Safari > Settings > Extensions.");if(result.appPaths.ios)log.info("Install the iOS app on an iPhone, iPad, or Simulator, then enable it in Settings > Apps > Safari > Extensions.");return}const{appPath,resources}=await buildSafariApp(config,{version:options.version??version,release:Boolean(options.release),signed:Boolean(options.signed),skipXcodebuild:Boolean(options.skipXcodebuild)});if(appPath){log.success(`Built ${appPath}`);log.info("Open the app once, then enable the extension in Safari > Settings > Extensions.")}else log.success(`Extension payload synced \u2192 ${resources}`)});buddy.command("extension:safari:publish","Archive and validate or upload the Safari app to App Store Connect").option("--version <version>","Override the marketing version (defaults to package.json)").option("--build-number <number>","CFBundleVersion (defaults to GITHUB_RUN_NUMBER or Unix time)").option("--team-id <id>","Apple Developer team (defaults to config safariTeamId)").option("--api-key-id <id>","App Store Connect API key ID").option("--api-issuer-id <id>","App Store Connect API issuer ID").option("--api-key-path <path>","Path to the App Store Connect AuthKey_*.p8 file").option("--validate-only","Create and validate the archive without uploading it").option("--platform <platform>","Publish macos, ios, or all (defaults to config safariPlatforms)").option("--skip-screenshots","Do not regenerate the App Store screenshot set before publishing").action(async(options)=>{const{publishSafariApp}=await import("@stacksjs/browser-extension"),{config,version}=await load();if(!options.skipScreenshots)try{const{generateProjectImages}=await import("@stacksjs/actions");await generateProjectImages({only:["app-store"]})}catch(error){log.warn(`[extension:safari:publish] using the committed screenshots \u2014 could not regenerate: ${error.message}`)}const result=await publishSafariApp(config,{version:options.version??version,buildNumber:options.buildNumber,teamId:options.teamId,keyId:options.apiKeyId,issuerId:options.apiIssuerId,keyPath:options.apiKeyPath,validateOnly:Boolean(options.validateOnly),platforms:parseSafariPlatforms(options.platform)});for(const deferred of result.deferred)log.warn(`${deferred.reason}. Version ${deferred.version} remains queued for the next automated retry.`);for(const published of result.alreadyPublished)log.success(`Safari ${published.platform} version ${published.version} is already published${published.state?` (${published.state})`:""}`);if(result.artifacts.length)log.success(options.validateOnly?`Validated Safari ${result.artifacts.map((artifact)=>artifact.platform).join(" + ")} archives (build ${result.buildNumber})`:`Uploaded and selected Safari ${result.attachments.map((attachment)=>attachment.platform).join(" + ")} build ${result.buildNumber} in App Store Connect`);if(result.appStoreSubmission?.reviewSubmissionIds.length)log.success(`Submitted ${result.appStoreSubmission.reviewSubmissionIds.length} Safari version(s) to App Review`)});buddy.command("extension:safari:submit","Synchronize metadata and submit an existing Safari version to App Review").option("--version <version>","Marketing version to submit (defaults to package.json)").option("--api-key-id <id>","App Store Connect API key ID").option("--api-issuer-id <id>","App Store Connect API issuer ID").option("--api-key-path <path>","Path to the App Store Connect AuthKey_*.p8 file").option("--platform <platform>","Submit macos, ios, or all (defaults to config safariPlatforms)").option("--prepare-only","Synchronize the listing without submitting it for review").action(async(options)=>{const{submitSafariAppStore}=await import("@stacksjs/browser-extension"),{config,version}=await load(),result=await submitSafariAppStore(config,{version:options.version??version,keyId:options.apiKeyId,issuerId:options.apiIssuerId,keyPath:options.apiKeyPath,platforms:parseSafariPlatforms(options.platform),submit:!options.prepareOnly});log.success(`Synchronized ${result.versions.map((item)=>item.platform).join(" + ")} App Store listings`);if(result.reviewSubmissionIds.length)log.success(`Submitted ${result.reviewSubmissionIds.length} Safari version(s) to App Review`)})}
|
|
1
|
+
import process from"node:process";import{log}from"@stacksjs/cli";function parseSafariPlatforms(platform){if(!platform)return;if(platform==="all")return["macos","ios"];if(platform==="macos"||platform==="ios")return[platform];throw Error(`Invalid Safari platform ${platform}; use macos, ios, or all`)}async function refreshAppStoreScreenshots(){try{const{generateProjectImages}=await import("@stacksjs/actions");await generateProjectImages({only:["app-store"]})}catch(error){log.warn(`[extension:publish] using the committed screenshots \u2014 could not regenerate: ${error.message}`)}}async function syncFirefoxListing(config,credentials){if(!config.firefoxAddons?.screenshots?.length)return;try{const{syncFirefoxPreviews}=await import("@stacksjs/browser-extension"),previews=await syncFirefoxPreviews(config,credentials);if(previews.unchanged)log.info("Firefox listing screenshots already match");else log.success(`Synced ${previews.uploaded.length} Firefox listing screenshot(s), removed ${previews.removed.length}`)}catch(error){log.warn(`[extension:publish] Firefox listing screenshots not synced: ${error.message}`)}}export function extension(buddy){const load=async()=>{const{loadExtensionConfig}=await import("@stacksjs/browser-extension"),config=await loadExtensionConfig(process.cwd());if(!config){log.error("No extension config found. Create `config/extension.ts` exporting `defineExtension({ \u2026 })`.");process.exit(1)}const pkg=await Bun.file(`${process.cwd()}/package.json`).json().catch(()=>({}));return{config,version:pkg.version??"0.0.0"}};buddy.command("extension:build","Build the browser extension (Chrome + Firefox + Safari) from config/extension.ts").option("--target <target>","Build a single target (chrome | firefox | safari); omit to build all").option("--version <version>","Override the extension version (defaults to package.json)").action(async(options)=>{const{buildExtension,buildAllTargets}=await import("@stacksjs/browser-extension"),{config,version}=await load(),v=options.version??version;if(options.target){const{outdir}=await buildExtension(config,{target:options.target,version:v});log.success(`Built ${config.name} ${v} (${options.target}) \u2192 ${outdir}`)}else{await buildAllTargets(config,{version:v});log.success(`Built ${config.name} ${v} for ${(config.targets??["chrome","firefox"]).join(", ")}`)}});buddy.command("extension:init","Scaffold a Chrome, Firefox, or Safari extension, including the Safari Xcode app").option("--name <name>","Extension display name").option("--target <target>","Scaffold chrome, firefox, safari, or all (default all)").option("--bundle-id <id>","Safari container bundle identifier").option("--team-id <id>","Apple Developer team used for Safari signing").option("--platform <platform>","Safari platform: macos, ios, or all (default all)").option("--force","Overwrite existing starter and Safari scaffold files").action(async(options)=>{const target=options.target??"all";if(!["chrome","firefox","safari","all"].includes(target))throw Error(`Invalid extension target ${target}; use chrome, firefox, safari, or all`);const{scaffoldExtensionProject}=await import("@stacksjs/browser-extension"),result=await scaffoldExtensionProject({name:options.name,target,bundleId:options.bundleId,teamId:options.teamId,platforms:parseSafariPlatforms(options.platform)??["macos","ios"],force:Boolean(options.force)});for(const file of result.written)log.success(`created ${file}`);for(const file of result.skipped)log.info(`skip (exists): ${file}`);if(result.safari)log.success(`Scaffolded the Safari container app \u2192 ${result.safari.dir}`);log.info("Next: add icons under public/icons, then run `buddy extension:build`.")});buddy.command("extension:package","Build + zip the browser extension into store-ready archives").option("--target <target>","Package a single target (chrome | firefox | safari); omit to package all").option("--version <version>","Override the extension version (defaults to package.json)").action(async(options)=>{const{packageExtension}=await import("@stacksjs/browser-extension"),{config,version}=await load(),v=options.version??version,targets=options.target?[options.target]:config.targets??["chrome","firefox"];for(const target of targets){const out=await packageExtension(config,{target,version:v});log.success(`Packaged ${config.name} (${target}) \u2192 ${out}`)}});buddy.command("extension:publish","Publish to every store this project is set up for \u2014 the release-tag entry point").option("--version <version>","Override the extension version (defaults to package.json)").option("--targets <targets>","Comma-separated subset of chrome,firefox,safari").option("--dry-run","Report the publish plan without uploading anything").action(async(options)=>{const{formatPublishPlan,planExtensionPublish,publishChromeExtension,publishFirefoxExtension,publishSafariApp}=await import("@stacksjs/browser-extension"),{config,version}=await load(),requested=options.targets?.split(",").map((value)=>value.trim()).filter(Boolean),plan=planExtensionPublish(config,process.env,requested?.length?requested:void 0);log.info(`Extension publish plan for v${options.version??version}:
|
|
2
|
+
${formatPublishPlan(plan)}`);const publishing=plan.filter((decision)=>decision.publish);if(options.dryRun||!publishing.length){if(!publishing.length)log.warn("No store is both configured and credentialed, so nothing was published.");return}const failures=[];for(const{target}of publishing)try{if(target==="chrome"){const result=await publishChromeExtension(config,{version:options.version??version,blockOnWarnings:!0});if(result.deferred)log.warn(`${result.deferred.reason}. Version ${options.version??version} remains queued for the next automated retry.`);else if(result.alreadyPublished)log.success(result.alreadyPublished.reason);else log.success(`Submitted Chrome Web Store item ${result.publish?.itemId??""}: ${result.publish?.state??"uploaded"}`)}else if(target==="firefox"){const result=await publishFirefoxExtension(config,{version:options.version??version});log.success(`Submitted Firefox extension (${result.channel}) \u2192 ${result.artifactsDir}`);await syncFirefoxListing(config,{})}else{await refreshAppStoreScreenshots();await publishSafariApp(config,{version:options.version??version});log.success("Uploaded Safari app to App Store Connect")}}catch(error){failures.push(`${target}: ${error.message}`);log.error(`[extension:publish] ${target} failed: ${error.message}`)}if(failures.length){log.error(`Failed to publish ${failures.length} of ${publishing.length} store(s):
|
|
3
|
+
${failures.join(`
|
|
4
|
+
`)}`);process.exit(1)}});buddy.command("extension:chrome:status","Fetch the Chrome Web Store item status").option("--service-account-path <path>","Google service-account JSON key path").option("--access-token <token>","Short-lived Chrome Web Store OAuth access token").action(async(options)=>{const{ChromeWebStoreClient}=await import("@stacksjs/browser-extension"),{config}=await load();if(!config.chromeWebStore)throw Error("Chrome status needs chromeWebStore.publisherId and chromeWebStore.itemId in config/extension.ts");const status=await new ChromeWebStoreClient(options).fetchStatus(config.chromeWebStore);log.info(`Chrome Web Store item ${status.itemId}`);log.info(`published: ${status.publishedItemRevisionStatus?.state??"none"}`);log.info(`submitted: ${status.submittedItemRevisionStatus?.state??"none"}`);if(status.warned)log.warn("Chrome has warned this item for a policy violation.");if(status.takenDown)log.error("Chrome has taken this item down for a policy violation.")});buddy.command("extension:chrome:publish","Build, upload, and submit the Chrome extension through Web Store API v2").option("--version <version>","Override the extension version (defaults to package.json)").option("--service-account-path <path>","Google service-account JSON key path").option("--access-token <token>","Short-lived Chrome Web Store OAuth access token").option("--upload-only","Upload without submitting the item for review").option("--allow-warnings","Submit even when Chrome reports validation warnings").action(async(options)=>{const{publishChromeExtension}=await import("@stacksjs/browser-extension"),{config,version}=await load(),result=await publishChromeExtension(config,{version:options.version??version,serviceAccountPath:options.serviceAccountPath,accessToken:options.accessToken,uploadOnly:Boolean(options.uploadOnly),blockOnWarnings:!options.allowWarnings});if(result.deferred){log.warn(`${result.deferred.reason}. Version ${options.version??version} remains queued for the next automated retry.`);return}if(result.alreadyPublished){log.success(result.alreadyPublished.reason);return}log.success(`Uploaded Chrome package ${result.packagePath} (${result.upload?.crxVersion??"processing complete"})`);if(result.publish)log.success(`Submitted Chrome Web Store item ${result.publish.itemId}: ${result.publish.state}`)});buddy.command("extension:firefox:previews","Sync the Firefox listing screenshots declared in config/extension.ts").option("--api-key <issuer>","AMO JWT issuer").option("--api-secret <secret>","AMO JWT secret").option("--dry-run","Report what would change without touching the listing").action(async(options)=>{const{syncFirefoxPreviews}=await import("@stacksjs/browser-extension"),{config}=await load(),result=await syncFirefoxPreviews(config,{issuer:options.apiKey,secret:options.apiSecret,dryRun:options.dryRun});if(result.unchanged)log.info("Firefox listing screenshots already match config/extension.ts");else if(options.dryRun)log.info(`Would replace ${result.removed.length} Firefox listing screenshot(s)`);else log.success(`Synced ${result.uploaded.length} Firefox listing screenshot(s), removed ${result.removed.length}`)});buddy.command("extension:firefox:publish","Build and submit the Firefox extension through Mozilla Add-ons").option("--version <version>","Override the extension version (defaults to package.json)").option("--api-key <issuer>","AMO JWT issuer").option("--api-secret <secret>","AMO JWT secret").option("--source-code <path>","Human-readable source archive for AMO review").option("--approval-timeout <milliseconds>","How long to wait for human approval (default 0)").action(async(options)=>{const{publishFirefoxExtension}=await import("@stacksjs/browser-extension"),{config,version}=await load(),result=await publishFirefoxExtension(config,{version:options.version??version,issuer:options.apiKey,secret:options.apiSecret,sourceCodePath:options.sourceCode,approvalTimeout:options.approvalTimeout===void 0?void 0:Number(options.approvalTimeout)});log.success(`Submitted Firefox extension (${result.channel}) \u2192 ${result.artifactsDir}`);if(config.firefoxAddons?.screenshots?.length)try{const{syncFirefoxPreviews}=await import("@stacksjs/browser-extension"),previews=await syncFirefoxPreviews(config,{issuer:options.apiKey,secret:options.apiSecret});if(previews.unchanged)log.info("Firefox listing screenshots already match");else log.success(`Synced ${previews.uploaded.length} Firefox listing screenshot(s), removed ${previews.removed.length}`)}catch(error){log.warn(`[extension:firefox:publish] listing screenshots left as they were: ${error.message}`)}if(result.artifacts.length)log.info(`new artifacts: ${result.artifacts.join(", ")}`)});buddy.command("extension:safari:provision","Register Safari Bundle IDs and check the App Store Connect app record").option("--api-key-id <id>","App Store Connect API key ID").option("--api-issuer-id <id>","App Store Connect API issuer ID").option("--api-key-path <path>","Path to the App Store Connect AuthKey_*.p8 file").option("--check","Report missing resources without creating Bundle IDs").option("--version <version>","Create or align App Store versions (defaults to package.json)").option("--platform <platform>","Provision macos, ios, or all (defaults to config safariPlatforms)").action(async(options)=>{const{provisionSafariApp}=await import("@stacksjs/browser-extension"),{config,version}=await load(),result=await provisionSafariApp(config,{keyId:options.apiKeyId,issuerId:options.apiIssuerId,keyPath:options.apiKeyPath,checkOnly:Boolean(options.check),version:options.version??version,platforms:parseSafariPlatforms(options.platform)});for(const resource of[result.container,result.extension])if(resource.created)log.success(`Registered Bundle ID ${resource.identifier}`);else if(resource.exists)log.success(`Bundle ID exists: ${resource.identifier}`);else log.warn(`Bundle ID is missing: ${resource.identifier}`);if(result.appRecord.exists)log.success(`App Store Connect app record exists (${result.appRecord.id})`);else log.warn("App Store Connect app record is missing. Apple requires creating it in the App Store Connect website.");for(const appStoreVersion of result.appStoreVersions){const action=appStoreVersion.created?"Created":appStoreVersion.updated?"Updated":"Ready";log.success(`${action} Safari ${appStoreVersion.platform} App Store version ${appStoreVersion.version}`)}});buddy.command("extension:safari:init","Scaffold the Safari container app (Xcode project) from the template").option("--bundle-id <id>","Base bundle identifier (defaults to config safariBundleId)").option("--dir <dir>","Output directory for the Xcode project (default safari)").option("--force","Overwrite existing scaffold files").option("--team-id <id>","Apple Developer team used for signing").action(async(options)=>{const{scaffoldSafariApp}=await import("@stacksjs/browser-extension"),{config}=await load(),{dir,written,skipped}=await scaffoldSafariApp(config,{bundleId:options.bundleId,dir:options.dir,force:Boolean(options.force),teamId:options.teamId});log.success(`Scaffolded the Safari container app \u2192 ${dir} (${written.length} files)`);if(skipped.length)log.info(`kept ${skipped.length} existing files (use --force to overwrite)`)});buddy.command("extension:safari:app","Build the extension and its macOS, iPhone, and iPad Safari container apps").option("--release","Build the Release configuration (default Debug)").option("--signed","Sign locally against the Apple ID in Xcode (local builds only \u2014 see below)").option("--skip-xcodebuild","Only build + sync the extension payload").option("--version <version>","Override the extension version (defaults to package.json)").option("--platform <platform>","Build macos, ios, or all (defaults to config safariPlatforms)").action(async(options)=>{const{buildSafariApp,buildSafariUniversalApp}=await import("@stacksjs/browser-extension"),{config,version}=await load(),platforms=parseSafariPlatforms(options.platform)??config.safariPlatforms??["macos"];if(platforms.includes("ios")){const result=await buildSafariUniversalApp(config,{version:options.version??version,release:Boolean(options.release),signed:Boolean(options.signed),skipXcodebuild:Boolean(options.skipXcodebuild),platforms});for(const platform of platforms){const appPath=result.appPaths[platform];if(appPath)log.success(`Built Safari ${platform} app ${appPath}`)}if(options.skipXcodebuild)log.success(`Generated universal Safari project \u2192 ${result.project}`);if(result.appPaths.macos)log.info("Open the macOS app once, then enable the extension in Safari > Settings > Extensions.");if(result.appPaths.ios)log.info("Install the iOS app on an iPhone, iPad, or Simulator, then enable it in Settings > Apps > Safari > Extensions.");return}const{appPath,resources}=await buildSafariApp(config,{version:options.version??version,release:Boolean(options.release),signed:Boolean(options.signed),skipXcodebuild:Boolean(options.skipXcodebuild)});if(appPath){log.success(`Built ${appPath}`);log.info("Open the app once, then enable the extension in Safari > Settings > Extensions.")}else log.success(`Extension payload synced \u2192 ${resources}`)});buddy.command("extension:safari:publish","Archive and validate or upload the Safari app to App Store Connect").option("--version <version>","Override the marketing version (defaults to package.json)").option("--build-number <number>","CFBundleVersion (defaults to GITHUB_RUN_NUMBER or Unix time)").option("--team-id <id>","Apple Developer team (defaults to config safariTeamId)").option("--api-key-id <id>","App Store Connect API key ID").option("--api-issuer-id <id>","App Store Connect API issuer ID").option("--api-key-path <path>","Path to the App Store Connect AuthKey_*.p8 file").option("--validate-only","Create and validate the archive without uploading it").option("--platform <platform>","Publish macos, ios, or all (defaults to config safariPlatforms)").option("--skip-screenshots","Do not regenerate the App Store screenshot set before publishing").action(async(options)=>{const{publishSafariApp}=await import("@stacksjs/browser-extension"),{config,version}=await load();if(!options.skipScreenshots)try{const{generateProjectImages}=await import("@stacksjs/actions");await generateProjectImages({only:["app-store"]})}catch(error){log.warn(`[extension:safari:publish] using the committed screenshots \u2014 could not regenerate: ${error.message}`)}const result=await publishSafariApp(config,{version:options.version??version,buildNumber:options.buildNumber,teamId:options.teamId,keyId:options.apiKeyId,issuerId:options.apiIssuerId,keyPath:options.apiKeyPath,validateOnly:Boolean(options.validateOnly),platforms:parseSafariPlatforms(options.platform)});for(const deferred of result.deferred)log.warn(`${deferred.reason}. Version ${deferred.version} remains queued for the next automated retry.`);for(const published of result.alreadyPublished)log.success(`Safari ${published.platform} version ${published.version} is already published${published.state?` (${published.state})`:""}`);if(result.artifacts.length)log.success(options.validateOnly?`Validated Safari ${result.artifacts.map((artifact)=>artifact.platform).join(" + ")} archives (build ${result.buildNumber})`:`Uploaded and selected Safari ${result.attachments.map((attachment)=>attachment.platform).join(" + ")} build ${result.buildNumber} in App Store Connect`);if(result.appStoreSubmission?.reviewSubmissionIds.length)log.success(`Submitted ${result.appStoreSubmission.reviewSubmissionIds.length} Safari version(s) to App Review`)});buddy.command("extension:safari:submit","Synchronize metadata and submit an existing Safari version to App Review").option("--version <version>","Marketing version to submit (defaults to package.json)").option("--api-key-id <id>","App Store Connect API key ID").option("--api-issuer-id <id>","App Store Connect API issuer ID").option("--api-key-path <path>","Path to the App Store Connect AuthKey_*.p8 file").option("--platform <platform>","Submit macos, ios, or all (defaults to config safariPlatforms)").option("--prepare-only","Synchronize the listing without submitting it for review").action(async(options)=>{const{submitSafariAppStore}=await import("@stacksjs/browser-extension"),{config,version}=await load(),result=await submitSafariAppStore(config,{version:options.version??version,keyId:options.apiKeyId,issuerId:options.apiIssuerId,keyPath:options.apiKeyPath,platforms:parseSafariPlatforms(options.platform),submit:!options.prepareOnly});log.success(`Synchronized ${result.versions.map((item)=>item.platform).join(" + ")} App Store listings`);if(result.reviewSubmissionIds.length)log.success(`Submitted ${result.reviewSubmissionIds.length} Safari version(s) to App Review`)})}
|
|
@@ -110,7 +110,7 @@ export declare function features(buddy: CLI): void;
|
|
|
110
110
|
* Mirrors Laravel's `php artisan passport:install` / `horizon:install`
|
|
111
111
|
* pattern: features are inert dead code on disk until installed.
|
|
112
112
|
*/
|
|
113
|
-
export declare const FEATURE_NAMES: readonly ['dashboard', 'commerce', 'cms', 'marketing', 'monitoring', 'realtime', 'queue'];
|
|
113
|
+
export declare const FEATURE_NAMES: readonly ['dashboard', 'commerce', 'cms', 'forms', 'marketing', 'monitoring', 'realtime', 'queue'];
|
|
114
114
|
/**
|
|
115
115
|
* Per-feature stamped file/directory manifest. Paths are relative to the
|
|
116
116
|
* project root and mirror the layout that `./buddy new` lays down. Entries
|
|
@@ -130,6 +130,7 @@ export declare const FEATURE_NAMES: readonly ['dashboard', 'commerce', 'cms', 'm
|
|
|
130
130
|
* @defaultValue
|
|
131
131
|
* ```ts
|
|
132
132
|
* {
|
|
133
|
+
* forms: [ 'app/Models/Forms/', ],
|
|
133
134
|
* cms: [ 'app/Actions/Cms/', 'app/Actions/Dashboard/Content/', 'app/Models/Content/', 'app/Models/Tag.ts', 'app/Models/Comment.ts', 'resources/views/dashboard/content/', ],
|
|
134
135
|
* commerce: [ 'app/Actions/Commerce/', 'app/Actions/Dashboard/Commerce/', 'app/Models/commerce/', 'resources/components/Dashboard/Commerce/', 'resources/views/dashboard/commerce/', ],
|
|
135
136
|
* dashboard: [ 'app/Actions/Dashboard/', 'resources/components/Dashboard/', 'resources/views/dashboard/', 'routes/dashboard.ts', 'routes/dashboard-api.ts', ],
|
|
@@ -163,8 +164,9 @@ export declare const FEATURE_FILES: Record<FeatureName, readonly string[]>;
|
|
|
163
164
|
* @defaultValue
|
|
164
165
|
* ```ts
|
|
165
166
|
* {
|
|
166
|
-
*
|
|
167
|
-
*
|
|
167
|
+
* forms: ['forms', 'form_fields', 'form_submissions'],
|
|
168
|
+
* cms: [ 'posts', 'pages', 'comments', 'tags', 'authors', 'categories', 'taggable_models', 'categorizable_models', 'commentables', 'page_revisions', 'redirects', 'menus', 'menu_items', ],
|
|
169
|
+
* commerce: [ 'products', 'product_variants', 'product_units', 'manufacturers', 'orders', 'order_items', 'order_idempotency', 'carts', 'cart_items', 'payments', 'payment_methods', 'payment_products', 'payment_transactions', 'customers', 'subscribers', 'subscriber_emails', 'subscriptions', 'gift_cards', 'coupons', 'transactions', 'reviews', 'drivers', 'driver_pings', 'delivery_routes', 'delivery_stops', 'digital_deliveries', 'shipping_methods', 'shipping_rates', 'shipping_zones', 'license_keys', 'loyalty_points', 'loyalty_rewards', 'print_devices', 'receipts', 'tax_rates', 'waitlist_products', 'waitlist_restaurants', 'auctions', 'auction_items', 'bids', 'pledges', ],
|
|
168
170
|
* dashboard: [ 'boards', 'board_columns', 'cards', 'card_labels', 'card_assignees', 'card_comments', 'labels', 'ci_run_states', 'ci_runner_samples', 'ci_runner_alert_states', 'requests', 'logs', ],
|
|
169
171
|
* marketing: [ 'campaigns', 'campaign_sends', 'email_lists', 'email_list_subscribers', 'social_posts', 'mail_preferences', ],
|
|
170
172
|
* monitoring: ['errors'],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync,readdirSync,readFileSync}from"node:fs";import{cp,rm}from"node:fs/promises";import{join}from"node:path";import process from"node:process";import{frameworkPath,projectPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";export const FEATURE_NAMES=["dashboard","commerce","cms","marketing","monitoring","realtime","queue"],FEATURE_FILES={cms:["app/Actions/Cms/","app/Actions/Dashboard/Content/","app/Models/Content/","app/Models/Tag.ts","app/Models/Comment.ts","resources/views/dashboard/content/"],commerce:["app/Actions/Commerce/","app/Actions/Dashboard/Commerce/","app/Models/commerce/","resources/components/Dashboard/Commerce/","resources/views/dashboard/commerce/"],dashboard:["app/Actions/Dashboard/","resources/components/Dashboard/","resources/views/dashboard/","routes/dashboard.ts","routes/dashboard-api.ts"],marketing:["app/Actions/Dashboard/Marketing/","app/Models/Campaign.ts","app/Models/CampaignSend.ts","app/Models/EmailList.ts","app/Models/EmailListSubscriber.ts","app/Models/SocialPost.ts","resources/components/Marketing/","resources/views/dashboard/marketing/"],monitoring:["app/Actions/Monitoring/","app/Actions/TestErrorAction.ts","app/Models/Error.ts","functions/monitoring/","resources/views/dashboard/monitoring/","resources/views/dashboard/errors/"],realtime:["app/Actions/Realtime/","app/Actions/Dashboard/Realtime/","app/Models/realtime/","app/Broadcasts/","functions/realtime/","resources/views/dashboard/realtime/"],queue:["app/Actions/Queue/","app/Actions/Dashboard/Jobs/","app/Jobs/","app/Models/Job.ts","app/Models/FailedJob.ts","functions/jobs.ts","resources/views/dashboard/queue/","resources/views/dashboard/jobs/"]},FEATURE_TABLES={cms:["posts","pages","comments","tags","authors","categories","taggable_models","categorizable_models","commentables"],commerce:["products","product_variants","product_units","manufacturers","orders","order_items","order_idempotency","carts","cart_items","payments","payment_methods","payment_products","payment_transactions","customers","subscribers","subscriber_emails","subscriptions","gift_cards","coupons","transactions","reviews","drivers","driver_pings","delivery_routes","delivery_stops","digital_deliveries","shipping_methods","shipping_rates","shipping_zones","license_keys","loyalty_points","loyalty_rewards","print_devices","receipts","tax_rates","waitlist_products","waitlist_restaurants"],dashboard:["boards","board_columns","cards","card_labels","card_assignees","card_comments","labels","ci_run_states","ci_runner_samples","ci_runner_alert_states","requests","logs"],marketing:["campaigns","campaign_sends","email_lists","email_list_subscribers","social_posts","mail_preferences"],monitoring:["errors"],realtime:["websockets"],queue:["jobs","failed_jobs"]};export function migrationTable(filename){const inMatch=filename.match(/-in-([a-z0-9_]+)\.sql$/i);if(inMatch)return inMatch[1]??null;const createMatch=filename.match(/-create-([a-z0-9_]+)-table\.sql$/i);if(createMatch)return createMatch[1]??null;const alterMatch=filename.match(/-alter-([a-z0-9_]+)-/i);if(alterMatch)return alterMatch[1]??null;return null}export function migrationFeature(filename){const table=migrationTable(filename);if(!table)return null;for(const f of FEATURE_NAMES)if(FEATURE_TABLES[f].includes(table))return f;return null}export function appModelClaimsTable(table,root=projectPath()){const modelsDir=join(root,"app/Models");if(!existsSync(modelsDir))return!1;const featureModelFiles=new Set(FEATURE_NAMES.flatMap((feature)=>FEATURE_FILES[feature]).filter((path)=>path.startsWith("app/Models/")&&!path.endsWith("/"))),escaped=table.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),declaration=new RegExp(`\\btable\\s*:\\s*['"]${escaped}['"]`);for(const entry of readdirSync(modelsDir,{withFileTypes:!0})){if(!entry.isFile()||!/\.[cm]?[jt]s$/.test(entry.name))continue;if(featureModelFiles.has(`app/Models/${entry.name}`))continue;if(declaration.test(readFileSync(join(modelsDir,entry.name),"utf8")))return!0}return!1}export function featurePathsPresent(feature,root=projectPath()){return FEATURE_FILES[feature].filter((rel)=>existsSync(`${root}/${rel}`))}export async function deleteFeatureFiles(feature,root=projectPath()){const removed=[];for(const rel of FEATURE_FILES[feature]){const full=`${root}/${rel}`;if(!existsSync(full))continue;await rm(full,{recursive:!0,force:!0});removed.push(rel)}return removed}export async function copyFeatureFiles(feature,options={}){const source=options.source??frameworkPath("defaults"),target=options.target??projectPath(),force=options.force===!0,copied=[],skipped=[];for(const rel of FEATURE_FILES[feature]){const sourceFull=join(source,rel);if(!existsSync(sourceFull)){skipped.push(rel);continue}const targetFull=join(target,rel);if(existsSync(targetFull)&&!force){skipped.push(rel);continue}await cp(sourceFull,targetFull,{recursive:!0,force});copied.push(rel)}return{copied,skipped}}const FEATURE_DESCRIPTIONS={dashboard:"Admin SPA shell + Activity/Log/Request/Deployment/Notification dashboards.",commerce:"Order/Cart/Product/Customer/Coupon/GiftCard/Shipping + storefront API.",cms:"Post/Page/Author/Comment/Tag models + content edit dashboards.",marketing:"/api/email/subscribe, /api/contact, Campaign/EmailList/SocialPost.",monitoring:"Error model + error-tracking views and actions.",realtime:"WebSocket broadcaster + Websocket model + realtime-stats actions.",queue:"Job + FailedJob models + queue dashboard pages."},STARTER_TEMPLATES={dashboard:`import type { DashboardConfig } from '@stacksjs/types'
|
|
1
|
+
import{existsSync,readdirSync,readFileSync}from"node:fs";import{cp,rm}from"node:fs/promises";import{join}from"node:path";import process from"node:process";import{frameworkPath,projectPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";export const FEATURE_NAMES=["dashboard","commerce","cms","forms","marketing","monitoring","realtime","queue"],FEATURE_FILES={forms:["app/Models/Forms/"],cms:["app/Actions/Cms/","app/Actions/Dashboard/Content/","app/Models/Content/","app/Models/Tag.ts","app/Models/Comment.ts","resources/views/dashboard/content/"],commerce:["app/Actions/Commerce/","app/Actions/Dashboard/Commerce/","app/Models/commerce/","resources/components/Dashboard/Commerce/","resources/views/dashboard/commerce/"],dashboard:["app/Actions/Dashboard/","resources/components/Dashboard/","resources/views/dashboard/","routes/dashboard.ts","routes/dashboard-api.ts"],marketing:["app/Actions/Dashboard/Marketing/","app/Models/Campaign.ts","app/Models/CampaignSend.ts","app/Models/EmailList.ts","app/Models/EmailListSubscriber.ts","app/Models/SocialPost.ts","resources/components/Marketing/","resources/views/dashboard/marketing/"],monitoring:["app/Actions/Monitoring/","app/Actions/TestErrorAction.ts","app/Models/Error.ts","functions/monitoring/","resources/views/dashboard/monitoring/","resources/views/dashboard/errors/"],realtime:["app/Actions/Realtime/","app/Actions/Dashboard/Realtime/","app/Models/realtime/","app/Broadcasts/","functions/realtime/","resources/views/dashboard/realtime/"],queue:["app/Actions/Queue/","app/Actions/Dashboard/Jobs/","app/Jobs/","app/Models/Job.ts","app/Models/FailedJob.ts","functions/jobs.ts","resources/views/dashboard/queue/","resources/views/dashboard/jobs/"]},FEATURE_TABLES={forms:["forms","form_fields","form_submissions"],cms:["posts","pages","comments","tags","authors","categories","taggable_models","categorizable_models","commentables","page_revisions","redirects","menus","menu_items"],commerce:["products","product_variants","product_units","manufacturers","orders","order_items","order_idempotency","carts","cart_items","payments","payment_methods","payment_products","payment_transactions","customers","subscribers","subscriber_emails","subscriptions","gift_cards","coupons","transactions","reviews","drivers","driver_pings","delivery_routes","delivery_stops","digital_deliveries","shipping_methods","shipping_rates","shipping_zones","license_keys","loyalty_points","loyalty_rewards","print_devices","receipts","tax_rates","waitlist_products","waitlist_restaurants","auctions","auction_items","bids","pledges"],dashboard:["boards","board_columns","cards","card_labels","card_assignees","card_comments","labels","ci_run_states","ci_runner_samples","ci_runner_alert_states","requests","logs"],marketing:["campaigns","campaign_sends","email_lists","email_list_subscribers","social_posts","mail_preferences"],monitoring:["errors"],realtime:["websockets"],queue:["jobs","failed_jobs"]};export function migrationTable(filename){const inMatch=filename.match(/-in-([a-z0-9_]+)\.sql$/i);if(inMatch)return inMatch[1]??null;const createMatch=filename.match(/-create-([a-z0-9_]+)-table\.sql$/i);if(createMatch)return createMatch[1]??null;const alterMatch=filename.match(/-alter-([a-z0-9_]+)-/i);if(alterMatch)return alterMatch[1]??null;return null}export function migrationFeature(filename){const table=migrationTable(filename);if(!table)return null;for(const f of FEATURE_NAMES)if(FEATURE_TABLES[f].includes(table))return f;return null}export function appModelClaimsTable(table,root=projectPath()){const modelsDir=join(root,"app/Models");if(!existsSync(modelsDir))return!1;const featureModelFiles=new Set(FEATURE_NAMES.flatMap((feature)=>FEATURE_FILES[feature]).filter((path)=>path.startsWith("app/Models/")&&!path.endsWith("/"))),escaped=table.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),declaration=new RegExp(`\\btable\\s*:\\s*['"]${escaped}['"]`);for(const entry of readdirSync(modelsDir,{withFileTypes:!0})){if(!entry.isFile()||!/\.[cm]?[jt]s$/.test(entry.name))continue;if(featureModelFiles.has(`app/Models/${entry.name}`))continue;if(declaration.test(readFileSync(join(modelsDir,entry.name),"utf8")))return!0}return!1}export function featurePathsPresent(feature,root=projectPath()){return FEATURE_FILES[feature].filter((rel)=>existsSync(`${root}/${rel}`))}export async function deleteFeatureFiles(feature,root=projectPath()){const removed=[];for(const rel of FEATURE_FILES[feature]){const full=`${root}/${rel}`;if(!existsSync(full))continue;await rm(full,{recursive:!0,force:!0});removed.push(rel)}return removed}export async function copyFeatureFiles(feature,options={}){const source=options.source??frameworkPath("defaults"),target=options.target??projectPath(),force=options.force===!0,copied=[],skipped=[];for(const rel of FEATURE_FILES[feature]){const sourceFull=join(source,rel);if(!existsSync(sourceFull)){skipped.push(rel);continue}const targetFull=join(target,rel);if(existsSync(targetFull)&&!force){skipped.push(rel);continue}await cp(sourceFull,targetFull,{recursive:!0,force});copied.push(rel)}return{copied,skipped}}const FEATURE_DESCRIPTIONS={dashboard:"Admin SPA shell + Activity/Log/Request/Deployment/Notification dashboards.",commerce:"Order/Cart/Product/Customer/Coupon/GiftCard/Shipping + storefront API.",cms:"Post/Page/Author/Comment/Tag models + content edit dashboards.",forms:"User-defined forms: builder models, conditional fields, public submit + CSV export.",marketing:"/api/email/subscribe, /api/contact, Campaign/EmailList/SocialPost.",monitoring:"Error model + error-tracking views and actions.",realtime:"WebSocket broadcaster + Websocket model + realtime-stats actions.",queue:"Job + FailedJob models + queue dashboard pages."},STARTER_TEMPLATES={dashboard:`import type { DashboardConfig } from '@stacksjs/types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* **Dashboard Configuration**
|
|
@@ -56,6 +56,17 @@ export default {
|
|
|
56
56
|
export default {
|
|
57
57
|
enabled: true,
|
|
58
58
|
} satisfies CmsConfig
|
|
59
|
+
`,forms:`import type { FormsConfig } from '@stacksjs/types'
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* **Forms Configuration**
|
|
63
|
+
*
|
|
64
|
+
* Controls the form-builder bundle (Form / FormField / FormSubmission
|
|
65
|
+
* models, public submit endpoints, CSV export).
|
|
66
|
+
*/
|
|
67
|
+
export default {
|
|
68
|
+
enabled: true,
|
|
69
|
+
} satisfies FormsConfig
|
|
59
70
|
`,marketing:`import type { MarketingConfig } from '@stacksjs/types'
|
|
60
71
|
|
|
61
72
|
/**
|
package/dist/commands/migrate.js
CHANGED
|
@@ -41,13 +41,13 @@ ${sample}${more}
|
|
|
41
41
|
2. (Optional) Export data from the current ${current} database.
|
|
42
42
|
3. ${switchBlocked?`Regenerate the migration files for ${target} FIRST \u2014 \`./buddy migrate\` will refuse until then.`:"Run `./buddy migrate` (or `migrate:fresh` to start clean)."}
|
|
43
43
|
4. The post-migrate FK audit will report any constraints that didn't replay.
|
|
44
|
-
`);await outro("Plan rendered. Re-run after updating .env to actually switch.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:regenerate [dialect]","Rebuild database/migrations from your models for a given dialect").option("--dry-run","Show what would change without writing anything",{default:!1}).option("-f, --force","Regenerate even though the database already has migrations recorded",{default:!1}).option("--replace-unmarked","Also delete migrations carrying no @generated marker (pre-marker corpora only)",{default:!1}).action(async(dialect,options)=>{const perf=await intro("buddy migrate:regenerate"),target=(dialect||process.env.DB_CONNECTION||"sqlite").toLowerCase();if(!new Set(["sqlite","mysql","vitess","postgres","singlestore"]).has(target)){log.syncError(`Unknown dialect "${target}". Allowed: sqlite, mysql, vitess, postgres, singlestore.`);process.exit(ExitCode.FatalError)}const{countAppliedMigrations,regenerateMigrationCorpus}=await import("@stacksjs/database");let applied=0;try{applied=await countAppliedMigrations()}catch{applied=0}if(applied>0&&!options.force&&!options.dryRun){log.syncError(`This database already has ${applied} migration(s) recorded.`);log.syncError("Regenerating renumbers every file, and the migrations table keys on the filename,");log.syncError("so already-applied migrations would look pending and run a second time.");log.syncError(" Point at an empty database, or re-run with --force if you know it is safe.");process.exit(ExitCode.FatalError)}const plan=await regenerateMigrationCorpus({dialect:target,dryRun:!0,replaceUnmarked:options.replaceUnmarked});if(resultFailed(plan)){log.syncError(plan.error.message);process.exit(ExitCode.FatalError)}const{files,removed,preserved,preservedOutOfScope,models,modelRoots}=plan.value,outOfScope=new Set(preservedOutOfScope),
|
|
45
|
-
\u2022 ${
|
|
46
|
-
${
|
|
44
|
+
`);await outro("Plan rendered. Re-run after updating .env to actually switch.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:regenerate [dialect]","Rebuild database/migrations from your models for a given dialect").option("--dry-run","Show what would change without writing anything",{default:!1}).option("-f, --force","Regenerate even though the database already has migrations recorded",{default:!1}).option("--replace-unmarked","Also delete migrations carrying no @generated marker (pre-marker corpora only)",{default:!1}).action(async(dialect,options)=>{const perf=await intro("buddy migrate:regenerate"),target=(dialect||process.env.DB_CONNECTION||"sqlite").toLowerCase();if(!new Set(["sqlite","mysql","vitess","postgres","singlestore"]).has(target)){log.syncError(`Unknown dialect "${target}". Allowed: sqlite, mysql, vitess, postgres, singlestore.`);process.exit(ExitCode.FatalError)}const{countAppliedMigrations,regenerateMigrationCorpus}=await import("@stacksjs/database");let applied=0;try{applied=await countAppliedMigrations()}catch{applied=0}if(applied>0&&!options.force&&!options.dryRun){log.syncError(`This database already has ${applied} migration(s) recorded.`);log.syncError("Regenerating renumbers every file, and the migrations table keys on the filename,");log.syncError("so already-applied migrations would look pending and run a second time.");log.syncError(" Point at an empty database, or re-run with --force if you know it is safe.");process.exit(ExitCode.FatalError)}const plan=await regenerateMigrationCorpus({dialect:target,dryRun:!0,replaceUnmarked:options.replaceUnmarked});if(resultFailed(plan)){log.syncError(plan.error.message);process.exit(ExitCode.FatalError)}const{files,removed,preserved,preservedOutOfScope,models,modelRoots}=plan.value,outOfScope=new Set(preservedOutOfScope),protectedHistory=preserved.filter((f)=>!outOfScope.has(f)),protectedHistoryBlock=protectedHistory.length===0?"":`
|
|
45
|
+
\u2022 ${protectedHistory.length} protected history file(s) will be KEPT:
|
|
46
|
+
${protectedHistory.map((f)=>` ${f}`).join(`
|
|
47
47
|
`)}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
These files either cannot be regenerated safely or follow a preserved
|
|
49
|
+
CREATE migration whose chronology must remain stable. --replace-unmarked
|
|
50
|
+
only replaces files that genuinely lack the @generated marker.`,OUT_OF_SCOPE_SHOWN=20,outOfScopeMore=preservedOutOfScope.length-OUT_OF_SCOPE_SHOWN,outOfScopeBlock=preservedOutOfScope.length===0?"":`
|
|
51
51
|
\u2022 ${preservedOutOfScope.length} file(s) describe tables this corpus does not rebuild, and will be KEPT:
|
|
52
52
|
${preservedOutOfScope.slice(0,OUT_OF_SCOPE_SHOWN).map((f)=>` ${f}`).join(`
|
|
53
53
|
`)}${outOfScopeMore>0?`
|
|
@@ -61,7 +61,7 @@ ${preservedOutOfScope.slice(0,OUT_OF_SCOPE_SHOWN).map((f)=>` ${f}`).join(`
|
|
|
61
61
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
62
62
|
\u2022 ${models} model(s) read from ${rootList}
|
|
63
63
|
\u2022 ${files.length} migration file(s) will be written
|
|
64
|
-
\u2022 ${removed.length} existing file(s) will be removed${
|
|
64
|
+
\u2022 ${removed.length} existing file(s) will be removed${protectedHistoryBlock}${outOfScopeBlock}
|
|
65
65
|
\u2022 These files are tracked in git, so review with \`git diff\` afterwards
|
|
66
66
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
67
67
|
`);if(options.dryRun){await outro("Dry run. Nothing was written.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}if(!isCI&&hasTTY&&process.stdin.isTTY){await log.flush();if(!await confirm({message:`Replace ${removed.length} migration file(s) with ${files.length} generated for ${target}?`,initial:!1})){await outro("Cancelled. Nothing was written.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}const result=await regenerateMigrationCorpus({dialect:target,replaceUnmarked:options.replaceUnmarked});if(resultFailed(result)){log.syncError(result.error.message);process.exit(ExitCode.FatalError)}log.success(`Wrote ${result.value.files.length} ${target} migration file(s) to database/migrations.`);if(applied>0){const{reconcileMigrationLedger}=await import("@stacksjs/database"),fixed=await reconcileMigrationLedger();if(fixed.remapped.length>0)log.info(`Repointed ${fixed.remapped.length} ledger row(s) at their renumbered file.`);if(fixed.recorded.length>0)log.info(`Recorded ${fixed.recorded.length} migration(s) already present in the schema.`);if(fixed.skipped.length>0)log.warn(`${fixed.skipped.length} ledger entr(ies) need a look \u2014 run \`./buddy migrate:status\`.`)}log.info("Review the change with `git diff`, then run `./buddy migrate`.");await outro("Regenerated.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:status","Compare database/migrations, the migrations ledger, and the live schema").option("--reconcile","Repair the ledger where the schema proves what happened",{default:!1}).option("--include-partial","With --reconcile, also record half-applied migrations",{default:!1}).option("--json","Emit the audit as JSON",{default:!1}).action(async(options)=>{const perf=options.json?void 0:await intro("buddy migrate:status"),{auditMigrationLedger,reconcileMigrationLedger}=await import("@stacksjs/database"),audit=await auditMigrationLedger();if(options.json){console.log(JSON.stringify(audit,(_k,v)=>v instanceof Set?[...v]:v,2));process.exit(audit.drift?ExitCode.FatalError:ExitCode.Success)}if(!audit.supported){log.info(`Dialect "${audit.dialect}" is not audited. Nothing to compare.`);await outro("Skipped.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const{counts,entries,orphans}=audit,list=(status)=>entries.filter((e)=>e.status===status).map((e)=>e.file),report=[],section=(heading,files)=>{if(files.length===0)return;if(heading)report.push(` ${heading}`);for(const file of files.slice(0,8))report.push(` ${file}`);if(files.length>8)report.push(` \u2026 +${files.length-8} more`);report.push("")};report.push("");report.push(` Migration status: ${audit.dialect}`);report.push(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");report.push(` ${entries.length} file(s) on disk \xB7 ${audit.recordedCount} recorded in the ledger`);report.push("");if(counts.applied>0)report.push(` ${counts.applied} applied - recorded, and present in the schema.`,"");if(counts.unverifiable>0)report.push(` ${counts.unverifiable} unverifiable - data migrations with no schema trace to check.`,"");section(`${counts.pending} pending - not applied yet, will run on the next \`buddy migrate\`:`,list("pending"));if(counts.stranded>0){report.push(` ${counts.stranded} STRANDED - already applied to the schema, but missing from the ledger.`);report.push(" These re-run on the next `buddy migrate`, which is unsafe for anything not idempotent.");section("",list("stranded"))}section(`${counts.partial} PARTIAL - some effects present, some missing. Needs a human:`,list("partial"));section(`${counts.reverted} REVERTED - recorded as applied, but the effects are gone from the schema:`,list("reverted"));if(orphans.length>0)section(`${orphans.length} orphaned ledger row(s) - recorded, but no such file on disk:`,orphans.map((o)=>`${o.migration}${o.renamedTo?` -> renumbered to ${o.renamedTo}`:" (no counterpart; migration deleted?)"}`));console.log(report.join(`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";import process from"node:process";import{installRequestContext,parseCookieHeader}from"@stacksjs/config";import{log}from"@stacksjs/logging";function productionRequestSnapshot(){const{__stxServeContext:snapshot,__stxServeCookies:legacyCookies,__stxServeSearch:legacySearch}=globalThis;if(!snapshot&&!legacyCookies&&legacySearch===void 0)return;return{...snapshot,cookies:snapshot?.cookies??legacyCookies??{},url:snapshot?.url||snapshot?.search||legacySearch||"",search:snapshot?.search??legacySearch??""}}installRequestContext(productionRequestSnapshot);function parseCookies(req){return parseCookieHeader(req.headers.get("cookie"))}export function resolveUserPartialsPath(cwd=process.cwd(),configuredDir){if(configuredDir){const configured=[join(cwd,"resources",configuredDir),join(cwd,configuredDir)].find((candidate)=>existsSync(candidate));if(configured)return configured}const existing=["resources/partials","resources/views/partials","partials","resources/components"].filter((candidate)=>existsSync(join(cwd,candidate)));if(existing.length===0)return;const populated=existing.find((candidate)=>containsTemplates(join(cwd,candidate)));return join(cwd,populated??existing[0])}function containsTemplates(dir){try{return[...new Bun.Glob("**/*.stx").scanSync({cwd:dir,onlyFiles:!0})].length>0}catch{return!1}}export async function loadStxPartialsDir(cwd=process.cwd()){const configPath=join(cwd,"config/stx.ts");if(!existsSync(configPath))return;try{const dir=(await import(configPath)).default?.partialsDir;return typeof dir==="string"&&dir.length>0?dir:void 0}catch{return}}function resolveCsrfMiddlewarePath(){const rel="app/Middleware/Csrf.ts",vendored=join(process.cwd(),"storage/framework/defaults",rel);if(existsSync(vendored))return vendored;try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());return`${pkgJson.slice(0,pkgJson.lastIndexOf("/"))}/${rel}`}catch{return vendored}}export function resolveApiBase(configuredPort,env=process.env){if(env.API_URL)return env.API_URL;const explicitPort=Number(env.PORT_API);if(explicitPort)return`http://127.0.0.1:${explicitPort}`;if(["production","staging","development"].includes((env.APP_ENV||"").toLowerCase()))return null;return`http://127.0.0.1:${configuredPort||3008}`}export async function startProductionServer(options){if(options?.port)process.env.PORT=String(options.port);process.env.APP_ENV=process.env.APP_ENV||"production";const port=Number(process.env.PORT)||3000,{config,overridesReady,resolveViewPatterns}=await import("@stacksjs/config");await overridesReady;const{describeApiProxyRules,describeRedirectRules,injectGlobalAutoImports,resolveApiProxyRules,resolveRedirectRules}=await import("@stacksjs/server"),{resolveDefaultsResources}=await import("@stacksjs/actions/dev/defaults-resources"),{stxPageAuthMiddleware}=await import("@stacksjs/auth");await injectGlobalAutoImports();let stxServe;const serveCandidates=[join(homedir(),"Code/Tools/stx/packages/bun-plugin/dist/serve.js"),join(process.cwd(),"pantry/bun-plugin-stx/dist/serve.js")];for(const entry of serveCandidates)try{if(existsSync(entry)){({serve:stxServe}=await import(entry));break}}catch{}if(!stxServe)({serve:stxServe}=await import("bun-plugin-stx/serve"));const stxModule=await resolveVendoredStxModule(),{site:siteConfig,i18n:i18nConfig}=await loadStxSiteConfig(),userViewsPath="resources/views",defaultsResources=resolveDefaultsResources(),defaultViewsPath=join(defaultsResources,"views"),userLayoutsPath=existsSync("resources/views/layouts")?"resources/views/layouts":"resources/layouts",userPartialsPath=resolveUserPartialsPath(process.cwd(),await loadStxPartialsDir()),apiBase=resolveApiBase(config.ports?.api),viewPatterns=resolveViewPatterns(userViewsPath,defaultViewsPath,config?.ui?.defaultViews);for(const name of viewPatterns.missing)log.warn(`ui.defaultViews lists "${name}", which does not exist under ${defaultViewsPath} \u2014 ignoring.`);const apiProxyRules=resolveApiProxyRules(config.server?.proxy);if(apiProxyRules.paths.length>0||apiProxyRules.prefixes.length>1)log.info(`API proxy: ${describeApiProxyRules(apiProxyRules)}`);const redirectRules=resolveRedirectRules(config.server?.redirects);if(redirectRules.size>0)log.info(`Redirects: ${describeRedirectRules(redirectRules)}`);log.info(`Starting production server on port ${port}...`);await stxServe({patterns:viewPatterns.patterns,port,autoIncrementPort:!1,reusePort:["production","staging","development"].includes((process.env.APP_ENV||"").toLowerCase()),componentsDir:join(defaultsResources,"components"),layoutsDir:userLayoutsPath,...userPartialsPath&&{partialsDir:userPartialsPath},fallbackLayoutsDir:join(defaultsResources,"layouts"),fallbackPartialsDir:defaultViewsPath,quiet:options?.verbose!==!0,...stxModule&&{stxModule},...i18nConfig&&{i18n:i18nConfig},...siteConfig?.url&&{site:siteConfig},middleware:stxPageAuthMiddleware(),onRequest:async(req)=>{const{maintenanceGate,isApiBoundRequest:isApiBound,proxyToBackend,resolveRedirect}=await import("@stacksjs/server"),gated=await maintenanceGate(req);if(gated)return gated;const url=new URL(req.url),redirected=resolveRedirect(url,redirectRules);if(redirected)return redirected;if(isApiBound(req,url.pathname,apiProxyRules)){if(!apiBase){log.error(`No API target configured for ${url.pathname}. This app shares its host with other `+"deployments, so there is no safe default port to guess \u2014 refusing to proxy. Set "+"PORT_API (or API_URL) for this site, and deploy an `api` site on its own port.");return new Response("Bad Gateway",{status:502})}try{return await proxyToBackend(req,apiBase)}catch(error){log.error(`API proxy to ${apiBase} failed: ${error.message}`);return new Response("Bad Gateway",{status:502})}}if(existsSync(join(process.cwd(),"resources/views/blog.stx"))){const{renderBlogFeed}=await import("@stacksjs/actions/blog"),feed=await renderBlogFeed(req);if(feed)return feed}globalThis.__stxServeSearch=url.search;globalThis.__stxServeCookies=parseCookies(req);return},onResponse:async(req,response)=>{const method=req.method.toUpperCase();if(method!=="GET"&&method!=="HEAD")return;try{const{seedCsrfCookieIfMissing}=await import(resolveCsrfMiddlewarePath());return seedCsrfCookieIfMissing(req,response)}catch(error){log.debug(`CSRF cookie seeding skipped: ${error.message}`)}}});log.success(`Production server listening on http://0.0.0.0:${port}`)}async function resolveVendoredStxModule(){const candidates=[join(homedir(),"Code/Tools/stx/packages/stx/dist/index.js"),join(process.cwd(),"pantry/@stacksjs/stx/dist/index.js")];for(const entry of candidates)try{if(existsSync(entry))return await import(entry)}catch{}try{return await import("@stacksjs/stx")}catch{}return}function fallbackI18nFromSite(site){const locales=site.i18n.locales,defaultLocale=site.i18n.defaultLocale??locales[0];return{locales,defaultLocale,labels:site.i18n.labels??Object.fromEntries(locales.map((c)=>[c,c.toUpperCase()])),translations:{},pickerSelector:site.i18n.pickerSelector??"#lang-picker"}}async function resolveSiteI18n(site){const resolverPaths=[join(homedir(),"Code/Tools/stx/packages/stx/src/site-builder/i18n.ts"),join(homedir(),"Code/Tools/stx/packages/stx/dist/index.js"),join(process.cwd(),"pantry/@stacksjs/stx/dist/index.js")];for(const resolverPath of resolverPaths)try{if(!existsSync(resolverPath))continue;const resolved=await import(resolverPath);if(typeof resolved.resolveI18n!=="function")continue;const i18n=resolved.resolveI18n(site,process.cwd());if(i18n)return i18n}catch{}try{const resolved=await import("@stacksjs/stx");if(typeof resolved.resolveI18n==="function"){const i18n=resolved.resolveI18n(site,process.cwd());if(i18n)return i18n}}catch{}return fallbackI18nFromSite(site)}async function loadStxSiteConfig(){const sitePath=join(process.cwd(),"site.config.ts");if(!existsSync(sitePath))return{};try{const site=(await import(sitePath)).default;if(!site)return{};if(!site.i18n)return{site};const i18n=await resolveSiteI18n(site);return{site,i18n}}catch{}return{}}
|
|
1
|
+
import{existsSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";import process from"node:process";import{installRequestContext,parseCookieHeader}from"@stacksjs/config";import{log}from"@stacksjs/logging";function productionRequestSnapshot(){const{__stxServeContext:snapshot,__stxServeCookies:legacyCookies,__stxServeSearch:legacySearch,__stxServeSite:site}=globalThis;if(!snapshot&&!legacyCookies&&legacySearch===void 0)return;return{...snapshot,cookies:snapshot?.cookies??legacyCookies??{},url:snapshot?.url||snapshot?.search||legacySearch||"",search:snapshot?.search??legacySearch??"",site:snapshot?.site??site??null}}installRequestContext(productionRequestSnapshot);function parseCookies(req){return parseCookieHeader(req.headers.get("cookie"))}export function resolveUserPartialsPath(cwd=process.cwd(),configuredDir){if(configuredDir){const configured=[join(cwd,"resources",configuredDir),join(cwd,configuredDir)].find((candidate)=>existsSync(candidate));if(configured)return configured}const existing=["resources/partials","resources/views/partials","partials","resources/components"].filter((candidate)=>existsSync(join(cwd,candidate)));if(existing.length===0)return;const populated=existing.find((candidate)=>containsTemplates(join(cwd,candidate)));return join(cwd,populated??existing[0])}function containsTemplates(dir){try{return[...new Bun.Glob("**/*.stx").scanSync({cwd:dir,onlyFiles:!0})].length>0}catch{return!1}}export async function loadStxPartialsDir(cwd=process.cwd()){const configPath=join(cwd,"config/stx.ts");if(!existsSync(configPath))return;try{const dir=(await import(configPath)).default?.partialsDir;return typeof dir==="string"&&dir.length>0?dir:void 0}catch{return}}function resolveCsrfMiddlewarePath(){const rel="app/Middleware/Csrf.ts",vendored=join(process.cwd(),"storage/framework/defaults",rel);if(existsSync(vendored))return vendored;try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());return`${pkgJson.slice(0,pkgJson.lastIndexOf("/"))}/${rel}`}catch{return vendored}}export function resolveApiBase(configuredPort,env=process.env){if(env.API_URL)return env.API_URL;const explicitPort=Number(env.PORT_API);if(explicitPort)return`http://127.0.0.1:${explicitPort}`;if(["production","staging","development"].includes((env.APP_ENV||"").toLowerCase()))return null;return`http://127.0.0.1:${configuredPort||3008}`}export async function startProductionServer(options){if(options?.port)process.env.PORT=String(options.port);process.env.APP_ENV=process.env.APP_ENV||"production";const port=Number(process.env.PORT)||3000,{config,overridesReady,resolveViewPatterns}=await import("@stacksjs/config");await overridesReady;const{describeApiProxyRules,describeRedirectRules,injectGlobalAutoImports,resolveApiProxyRules,resolveRedirectRules}=await import("@stacksjs/server"),{resolveDefaultsResources}=await import("@stacksjs/actions/dev/defaults-resources"),{stxPageAuthMiddleware}=await import("@stacksjs/auth");await injectGlobalAutoImports();let stxServe;const serveCandidates=[join(homedir(),"Code/Tools/stx/packages/bun-plugin/dist/serve.js"),join(process.cwd(),"pantry/bun-plugin-stx/dist/serve.js")];for(const entry of serveCandidates)try{if(existsSync(entry)){({serve:stxServe}=await import(entry));break}}catch{}if(!stxServe)({serve:stxServe}=await import("bun-plugin-stx/serve"));const stxModule=await resolveVendoredStxModule(),{site:siteConfig,i18n:i18nConfig}=await loadStxSiteConfig(),userViewsPath="resources/views",defaultsResources=resolveDefaultsResources(),defaultViewsPath=join(defaultsResources,"views"),userLayoutsPath=existsSync("resources/views/layouts")?"resources/views/layouts":"resources/layouts",userPartialsPath=resolveUserPartialsPath(process.cwd(),await loadStxPartialsDir()),apiBase=resolveApiBase(config.ports?.api),viewPatterns=resolveViewPatterns(userViewsPath,defaultViewsPath,config?.ui?.defaultViews);for(const name of viewPatterns.missing)log.warn(`ui.defaultViews lists "${name}", which does not exist under ${defaultViewsPath} \u2014 ignoring.`);const apiProxyRules=resolveApiProxyRules(config.server?.proxy);if(apiProxyRules.paths.length>0||apiProxyRules.prefixes.length>1)log.info(`API proxy: ${describeApiProxyRules(apiProxyRules)}`);const redirectRules=resolveRedirectRules(config.server?.redirects);if(redirectRules.size>0)log.info(`Redirects: ${describeRedirectRules(redirectRules)}`);log.info(`Starting production server on port ${port}...`);await stxServe({patterns:viewPatterns.patterns,port,autoIncrementPort:!1,reusePort:["production","staging","development"].includes((process.env.APP_ENV||"").toLowerCase()),componentsDir:join(defaultsResources,"components"),layoutsDir:userLayoutsPath,...userPartialsPath&&{partialsDir:userPartialsPath},fallbackLayoutsDir:join(defaultsResources,"layouts"),fallbackPartialsDir:defaultViewsPath,quiet:options?.verbose!==!0,...stxModule&&{stxModule},...i18nConfig&&{i18n:i18nConfig},...siteConfig?.url&&{site:siteConfig},middleware:stxPageAuthMiddleware(),onRequest:async(req)=>{const{maintenanceGate,isApiBoundRequest:isApiBound,proxyToBackend,resolveRedirect}=await import("@stacksjs/server"),gated=await maintenanceGate(req);if(gated)return gated;const url=new URL(req.url),redirected=resolveRedirect(url,redirectRules);if(redirected)return redirected;if(isApiBound(req,url.pathname,apiProxyRules)){if(!apiBase){log.error(`No API target configured for ${url.pathname}. This app shares its host with other `+"deployments, so there is no safe default port to guess \u2014 refusing to proxy. Set "+"PORT_API (or API_URL) for this site, and deploy an `api` site on its own port.");return new Response("Bad Gateway",{status:502})}try{return await proxyToBackend(req,apiBase)}catch(error){log.error(`API proxy to ${apiBase} failed: ${error.message}`);return new Response("Bad Gateway",{status:502})}}if(existsSync(join(process.cwd(),"resources/views/blog.stx"))){const{renderBlogFeed}=await import("@stacksjs/actions/blog"),feed=await renderBlogFeed(req);if(feed)return feed}globalThis.__stxServeSearch=url.search;globalThis.__stxServeCookies=parseCookies(req);if(config.analytics?.capturePageviews)import("@stacksjs/analytics").then(({recordPageview})=>recordPageview(req)).catch(()=>{});if(config.sites?.enabled){const sites=await import("@stacksjs/sites"),resolved=await sites.resolveSiteByHost(sites.requestHost(req.headers,sites.sitesOptions()));sites.setCurrentSite(resolved);globalThis.__stxServeSite=sites.toSiteSnapshot(resolved)}else globalThis.__stxServeSite=null;return},onResponse:async(req,response)=>{const method=req.method.toUpperCase();if(method!=="GET"&&method!=="HEAD")return;let current=response;if(response.status===404&&config.sites?.enabled)try{const{cmsNotFoundFallback}=await import("@stacksjs/cms"),cmsResponse=await cmsNotFoundFallback(req);if(cmsResponse)current=cmsResponse}catch(error){log.debug(`CMS fallback skipped: ${error.message}`)}try{const{seedCsrfCookieIfMissing}=await import(resolveCsrfMiddlewarePath());return await seedCsrfCookieIfMissing(req,current)??(current===response?void 0:current)}catch(error){log.debug(`CSRF cookie seeding skipped: ${error.message}`);return current===response?void 0:current}}});log.success(`Production server listening on http://0.0.0.0:${port}`)}async function resolveVendoredStxModule(){const candidates=[join(homedir(),"Code/Tools/stx/packages/stx/dist/index.js"),join(process.cwd(),"pantry/@stacksjs/stx/dist/index.js")];for(const entry of candidates)try{if(existsSync(entry))return await import(entry)}catch{}try{return await import("@stacksjs/stx")}catch{}return}function fallbackI18nFromSite(site){const locales=site.i18n.locales,defaultLocale=site.i18n.defaultLocale??locales[0];return{locales,defaultLocale,labels:site.i18n.labels??Object.fromEntries(locales.map((c)=>[c,c.toUpperCase()])),translations:{},pickerSelector:site.i18n.pickerSelector??"#lang-picker"}}async function resolveSiteI18n(site){const resolverPaths=[join(homedir(),"Code/Tools/stx/packages/stx/src/site-builder/i18n.ts"),join(homedir(),"Code/Tools/stx/packages/stx/dist/index.js"),join(process.cwd(),"pantry/@stacksjs/stx/dist/index.js")];for(const resolverPath of resolverPaths)try{if(!existsSync(resolverPath))continue;const resolved=await import(resolverPath);if(typeof resolved.resolveI18n!=="function")continue;const i18n=resolved.resolveI18n(site,process.cwd());if(i18n)return i18n}catch{}try{const resolved=await import("@stacksjs/stx");if(typeof resolved.resolveI18n==="function"){const i18n=resolved.resolveI18n(site,process.cwd());if(i18n)return i18n}}catch{}return fallbackI18nFromSite(site)}async function loadStxSiteConfig(){const sitePath=join(process.cwd(),"site.config.ts");if(!existsSync(sitePath))return{};try{const site=(await import(sitePath)).default;if(!site)return{};if(!site.i18n)return{site};const i18n=await resolveSiteI18n(site);return{site,i18n}}catch{}return{}}
|
package/dist/workflow-prune.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
const FRAMEWORK_ONLY_PATH=/storage\/framework\/(?:core|scripts\/publish-commit)/,FRAMEWORK_ONLY_SCRIPTS=new Set(["docs:buddy","docs:buddy:check","docs:artifacts","docs:artifacts:check","docs:links","docs:links:check"]);function invocations(line){return[...line.matchAll(/(?:bun\s+run|bun\s+buddy|\.\/buddy|bunx?\s+buddy)\s+["']?([\w:.-]+)/g)].map((match)=>match[1])}function referencesCore(line){if(line.trim().startsWith("#"))return!1;const code=line.replace(/\s#.*$/,"");return FRAMEWORK_ONLY_PATH.test(code)||invocations(code).some((name)=>FRAMEWORK_ONLY_SCRIPTS.has(name))}export const frameworkOnlyScripts=[...FRAMEWORK_ONLY_SCRIPTS];function jobStarts(lines){const out=[];let inJobs=!1;for(const[at,line]of lines.entries()){if(/^jobs:\s*$/.test(line)){inJobs=!0;continue}if(!inJobs)continue;if(/^\S/.test(line)&&!/^\s*#/.test(line))break;const match=line.match(/^ {2}([A-Za-z_][\w-]*):\s*$/);if(match)out.push({name:match[1],at})}return out}function blockEnd(lines,from,indent){const sibling=new RegExp(`^ {${indent}}(?:- |[A-Za-z_"'])`);for(let at=from+1;at<lines.length;at++){const line=lines[at];if(line.trim()==="")continue;const leading=line.length-line.trimStart().length;if(leading<indent)return at;if(leading===indent&&sibling.test(line))return at}return lines.length}const SETUP_STEP=[/uses:\s*actions\/checkout/,/uses:\s*actions\/cache/,/uses:\s*actions\/setup-/,/uses:\s*pantry-pm\/pantry/,/run:\s*(?:bun|pantry|npm|pnpm|yarn)\s+(?:install|ci)\b/];function isSetupStep(step){const code=step.filter((line)=>!line.trim().startsWith("#"));return SETUP_STEP.some((pattern)=>code.some((line)=>pattern.test(line)))}function withLeadingComments(lines,at){let start=at;while(start-1>=0){const previous=lines[start-1].trim();if(previous.startsWith("#")||previous==="")start--;else break}while(start<at&&lines[start].trim()==="")start++;return start}export function pruneVendoredCoreFromWorkflow(source){let lines=source.split(`
|
|
2
2
|
`);const removedJobs=[];let removedSteps=0;for(const job of jobStarts(lines).reverse()){const end=blockEnd(lines,job.at,2),body=lines.slice(job.at,end),steps=[];for(const[offset,line]of body.entries()){if(!/^ {6}- /.test(line))continue;const at=job.at+offset,stepEnd=blockEnd(lines,at,6),step=lines.slice(at,stepEnd);steps.push({at,end:stepEnd,core:step.some(referencesCore),setup:isSetupStep(step)})}if(steps.length===0||!steps.some((step)=>step.core))continue;if(steps.filter((step)=>!step.setup).every((step)=>step.core)){lines.splice(withLeadingComments(lines,job.at),end-withLeadingComments(lines,job.at));removedJobs.push(job.name);continue}for(const step of[...steps].reverse()){if(!step.core)continue;const from=withLeadingComments(lines,step.at);lines.splice(from,step.end-from);removedSteps++}}if(removedJobs.length>0)lines=lines.map((line)=>{const inline=line.match(/^(\s*needs:\s*)\[([^\]]*)\]\s*$/);if(inline){const kept=inline[2].split(",").map((name)=>name.trim()).filter((name)=>name&&!removedJobs.includes(name));return kept.length>0?`${inline[1]}[${kept.join(", ")}]`:""}const scalar=line.match(/^(\s*)needs:\s*([A-Za-z_][\w-]*)\s*$/);if(scalar&&removedJobs.includes(scalar[2]))return"";return line}).filter((line,at,all)=>!(line===""&&all[at-1]===""&&all[at+1]===""));return{yaml:lines.join(`
|
|
3
|
-
`),removedJobs,removedSteps}}export async function pruneVendoredCoreFromWorkflows(cwd){const{readdir,readFile,writeFile}=await import("node:fs/promises"),{join}=await import("node:path"),dir=join(cwd,".github","workflows"),pruned=[];let entries;try{entries=await readdir(dir)}catch{return pruned}for(const entry of entries.sort()){if(!/\.ya?ml$/.test(entry))continue;const file=join(dir,entry);try{const source=await readFile(file,"utf-8"),result=pruneVendoredCoreFromWorkflow(source);if(result.yaml===source)continue;await writeFile(file,result.yaml);pruned.push({file:`.github/workflows/${entry}`,removedJobs:result.removedJobs,removedSteps:result.removedSteps})}catch{continue}}return pruned}export function splitFrameworkTypecheckScript(scripts){if(!scripts.typecheck?.includes("tsconfig.framework.json")||!scripts["typecheck:app"])return null;return Object.fromEntries(Object.entries(scripts).flatMap(([name,script])=>name==="typecheck"?[["typecheck","bun run typecheck:app && bun run typecheck:framework"],["typecheck:framework",script]]:[[name,script]]))}
|
|
3
|
+
`),removedJobs,removedSteps}}export async function pruneVendoredCoreFromWorkflows(cwd){const{readdir,readFile,writeFile}=await import("node:fs/promises"),{join}=await import("node:path"),dir=join(cwd,".github","workflows"),pruned=[];let entries;try{entries=await readdir(dir)}catch{return pruned}for(const entry of entries.sort()){if(!/\.ya?ml$/.test(entry))continue;const file=join(dir,entry);try{const source=await readFile(file,"utf-8"),result=pruneVendoredCoreFromWorkflow(source);if(result.yaml===source)continue;await writeFile(file,result.yaml);pruned.push({file:`.github/workflows/${entry}`,removedJobs:result.removedJobs,removedSteps:result.removedSteps})}catch{continue}}return pruned}export function splitFrameworkTypecheckScript(scripts){if(!scripts.typecheck?.includes("tsconfig.framework.json")||!scripts["typecheck:app"])return null;return Object.fromEntries(Object.entries(scripts).flatMap(([name,script])=>name==="typecheck"?[["typecheck","bun run typecheck:app && bun run typecheck:framework"],["typecheck:framework",dropVendoredCoreSegments(script)]]:[[name,script]]))}function dropVendoredCoreSegments(script){const kept=splitTopLevelAnd(script).map((segment)=>segment.trim()).filter((segment)=>segment&&!segment.includes("storage/framework/core"));return kept.length>0?kept.join(" && "):script}function splitTopLevelAnd(script){const segments=[];let depth=0,current="";for(let i=0;i<script.length;i++){const char=script[i];if(char==="(")depth++;else if(char===")")depth=Math.max(0,depth-1);if(depth===0&&char==="&"&&script[i+1]==="&"){segments.push(current);current="";i++;continue}current+=char}segments.push(current);return segments}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/buddy",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.71.1",
|
|
6
6
|
"description": "Meet Buddy. The Stacks runtime.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -95,60 +95,62 @@
|
|
|
95
95
|
"prepublishOnly": "bun run build"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"@stacksjs/actions": "^0.
|
|
99
|
-
"@stacksjs/ai": "^0.
|
|
100
|
-
"@stacksjs/alias": "^0.
|
|
101
|
-
"@stacksjs/arrays": "^0.
|
|
102
|
-
"@stacksjs/auth": "^0.
|
|
103
|
-
"@stacksjs/build": "^0.
|
|
104
|
-
"@stacksjs/cache": "^0.
|
|
105
|
-
"@stacksjs/cli": "^0.
|
|
98
|
+
"@stacksjs/actions": "^0.71.1",
|
|
99
|
+
"@stacksjs/ai": "^0.71.1",
|
|
100
|
+
"@stacksjs/alias": "^0.71.1",
|
|
101
|
+
"@stacksjs/arrays": "^0.71.1",
|
|
102
|
+
"@stacksjs/auth": "^0.71.1",
|
|
103
|
+
"@stacksjs/build": "^0.71.1",
|
|
104
|
+
"@stacksjs/cache": "^0.71.1",
|
|
105
|
+
"@stacksjs/cli": "^0.71.1",
|
|
106
106
|
"@stacksjs/clapp": "^0.2.12",
|
|
107
|
-
"@stacksjs/cloud": "^0.
|
|
108
|
-
"@stacksjs/collections": "^0.
|
|
109
|
-
"@stacksjs/config": "^0.
|
|
110
|
-
"@stacksjs/database": "^0.
|
|
111
|
-
"@stacksjs/desktop-build": "^0.
|
|
112
|
-
"@stacksjs/dns": "^0.
|
|
113
|
-
"@stacksjs/email": "^0.
|
|
114
|
-
"@stacksjs/enums": "^0.
|
|
115
|
-
"@stacksjs/error-handling": "^0.
|
|
116
|
-
"@stacksjs/events": "^0.
|
|
117
|
-
"@stacksjs/git": "^0.
|
|
107
|
+
"@stacksjs/cloud": "^0.71.1",
|
|
108
|
+
"@stacksjs/collections": "^0.71.1",
|
|
109
|
+
"@stacksjs/config": "^0.71.1",
|
|
110
|
+
"@stacksjs/database": "^0.71.1",
|
|
111
|
+
"@stacksjs/desktop-build": "^0.71.1",
|
|
112
|
+
"@stacksjs/dns": "^0.71.1",
|
|
113
|
+
"@stacksjs/email": "^0.71.1",
|
|
114
|
+
"@stacksjs/enums": "^0.71.1",
|
|
115
|
+
"@stacksjs/error-handling": "^0.71.1",
|
|
116
|
+
"@stacksjs/events": "^0.71.1",
|
|
117
|
+
"@stacksjs/git": "^0.71.1",
|
|
118
118
|
"@stacksjs/gitit": "^0.2.5",
|
|
119
|
-
"@stacksjs/health": "^0.
|
|
119
|
+
"@stacksjs/health": "^0.71.1",
|
|
120
120
|
"@stacksjs/dnsx": "^0.2.3",
|
|
121
121
|
"@stacksjs/httx": "^0.1.10",
|
|
122
|
-
"@stacksjs/image": "^0.
|
|
123
|
-
"@stacksjs/lint": "^0.
|
|
124
|
-
"@stacksjs/logging": "^0.
|
|
125
|
-
"@stacksjs/notifications": "^0.
|
|
126
|
-
"@stacksjs/objects": "^0.
|
|
127
|
-
"@stacksjs/orm": "^0.
|
|
128
|
-
"@stacksjs/path": "^0.
|
|
129
|
-
"@stacksjs/skills": "^0.
|
|
130
|
-
"@stacksjs/payments": "^0.
|
|
131
|
-
"@stacksjs/realtime": "^0.
|
|
132
|
-
"@stacksjs/router": "^0.
|
|
122
|
+
"@stacksjs/image": "^0.71.1",
|
|
123
|
+
"@stacksjs/lint": "^0.71.1",
|
|
124
|
+
"@stacksjs/logging": "^0.71.1",
|
|
125
|
+
"@stacksjs/notifications": "^0.71.1",
|
|
126
|
+
"@stacksjs/objects": "^0.71.1",
|
|
127
|
+
"@stacksjs/orm": "^0.71.1",
|
|
128
|
+
"@stacksjs/path": "^0.71.1",
|
|
129
|
+
"@stacksjs/skills": "^0.71.1",
|
|
130
|
+
"@stacksjs/payments": "^0.71.1",
|
|
131
|
+
"@stacksjs/realtime": "^0.71.1",
|
|
132
|
+
"@stacksjs/router": "^0.71.1",
|
|
133
133
|
"@stacksjs/rpx": "^0.11.42",
|
|
134
|
-
"@stacksjs/search-engine": "^0.
|
|
135
|
-
"@stacksjs/security": "^0.
|
|
136
|
-
"@stacksjs/server": "^0.
|
|
137
|
-
"@stacksjs/
|
|
138
|
-
"@stacksjs/
|
|
139
|
-
"@stacksjs/
|
|
140
|
-
"@stacksjs/
|
|
141
|
-
"@stacksjs/
|
|
142
|
-
"@stacksjs/
|
|
143
|
-
"@stacksjs/
|
|
144
|
-
"@stacksjs/
|
|
145
|
-
"@stacksjs/
|
|
134
|
+
"@stacksjs/search-engine": "^0.71.1",
|
|
135
|
+
"@stacksjs/security": "^0.71.1",
|
|
136
|
+
"@stacksjs/server": "^0.71.1",
|
|
137
|
+
"@stacksjs/cms": "^0.71.1",
|
|
138
|
+
"@stacksjs/sites": "^0.71.1",
|
|
139
|
+
"@stacksjs/storage": "^0.71.1",
|
|
140
|
+
"@stacksjs/strings": "^0.71.1",
|
|
141
|
+
"@stacksjs/testing": "^0.71.1",
|
|
142
|
+
"@stacksjs/tunnel": "^0.71.1",
|
|
143
|
+
"@stacksjs/types": "^0.71.1",
|
|
144
|
+
"@stacksjs/ui": "^0.71.1",
|
|
145
|
+
"@stacksjs/utils": "^0.71.1",
|
|
146
|
+
"@stacksjs/validation": "^0.71.1",
|
|
147
|
+
"@stacksjs/ts-cloud": "^0.8.2",
|
|
146
148
|
"ajv": "^8.20.0",
|
|
147
149
|
"ajv-formats": "^3.0.1",
|
|
148
|
-
"ts-pantry": "^0.11.
|
|
150
|
+
"ts-pantry": "^0.11.27"
|
|
149
151
|
},
|
|
150
152
|
"devDependencies": {
|
|
151
|
-
"better-dx": "^0.2.
|
|
153
|
+
"better-dx": "^0.2.23"
|
|
152
154
|
},
|
|
153
155
|
"web-types": "./web-types.json"
|
|
154
156
|
}
|