@stacksjs/buddy 0.74.6 → 0.74.8
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/cli.js +1 -1
- package/dist/commands/about.js +1 -1
- package/dist/commands/build.js +1 -1
- package/dist/commands/create.js +1 -1
- package/dist/commands/deploy.js +6 -6
- package/dist/commands/desktop-apple.js +1 -1
- package/dist/commands/dev.js +1 -1
- package/dist/commands/dns.js +1 -1
- package/dist/commands/extension.js +2 -2
- package/dist/commands/http.js +1 -1
- package/dist/commands/install.js +1 -1
- package/dist/commands/key.js +1 -1
- package/dist/commands/libs.js +1 -1
- package/dist/commands/link.js +1 -1
- package/dist/commands/lint.js +2 -2
- package/dist/commands/mail.js +8 -8
- package/dist/commands/maintenance.js +1 -1
- package/dist/commands/migrate-project.js +1 -1
- package/dist/commands/publish.js +4 -4
- package/dist/commands/queue.js +1 -1
- package/dist/commands/release.js +1 -1
- package/dist/commands/schedule.js +1 -1
- package/dist/commands/seed.js +1 -1
- package/dist/commands/serve.js +1 -1
- package/dist/commands/server.js +3 -3
- package/dist/commands/setup.js +2 -2
- package/dist/commands/share.js +1 -1
- package/dist/commands/stacks.js +1 -1
- package/dist/commands/telemetry.js +1 -1
- package/dist/commands/user.js +1 -1
- package/dist/result.d.ts +1 -1
- package/package.json +53 -53
|
@@ -98,6 +98,6 @@ jobs:
|
|
|
98
98
|
`);writeFileSync(join(appleDir,"apple-provenance.json"),`${JSON.stringify({schemaVersion:"1.0.0",appName:config.appName,bundleId:config.bundleId,teamId:config.teamId,version:config.version,buildNumber:config.buildNumber,minimumMacos:config.minimumMacos,package:{name:basename(packagePath),sha256:packageHash},sourceRevision:Bun.spawnSync(["git","rev-parse","HEAD"],{cwd:projectPath()}).stdout.toString().trim(),craft:{sha256:sha256(join(macosDir,"craft-runtime"))}},null,2)}
|
|
99
99
|
`);return packagePath}function validateOrUpload(packagePath,config,validateOnly){const errors=validateAppleDesktopConfig(config,!0);if(errors.length)throw Error(errors.join(`
|
|
100
100
|
`));const common=["--type","macos","--file",packagePath,"--apiKey",config.apiKeyId,"--apiIssuer",config.apiIssuerId];process.env.API_PRIVATE_KEYS_DIR=resolve(config.apiKeyPath,"..");command(["xcrun","altool","--validate-app",...common]);if(!validateOnly)command(["xcrun","altool","--upload-app",...common])}function fail(error){const message=error instanceof Error?error.message:String(error);process.stderr.write(`${message}
|
|
101
|
-
`);
|
|
101
|
+
`);console.error(message);process.exit(1)}export function desktopApple(buddy){buddy.command("desktop:apple:csr","Generate local private keys and CSRs for Mac App Store distribution certificates").option("--common-name <name>","Certificate request common name").option("--output <path>","Directory for private keys and certificate requests").action(async(options)=>{try{const metadata=packageMetadata(),outputDirectory=resolve(options.output||storagePath("framework/desktop-dist/apple/provisioning")),{generateMacCertificateRequests}=await import("ts-pantry");generateMacCertificateRequests({outputDirectory,commonName:options.commonName||metadata.name});log.success(`Generated Apple certificate requests and private keys in ${outputDirectory}`)}catch(error){fail(error)}});buddy.command("desktop:apple:provision","Plan or reconcile Apple Bundle ID, capabilities, certificates, and profile").option("--app-name <name>","Mac App Store display name").option("--bundle-id <id>","Reverse-DNS bundle identifier").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>","App Store Connect AuthKey .p8 file").option("--capabilities <types>","Comma-separated Apple capability types").option("--app-certificate-csr <path>","CSR for a missing Mac App Distribution certificate").option("--installer-certificate-csr <path>","CSR for a missing Mac Installer Distribution certificate").option("--profile-name <name>","Provisioning profile name").option("--output <path>","Directory for the plan and downloaded Apple assets").option("--plan","Report the idempotent Apple resource diff without mutating it").option("--apply","Apply the Apple resource diff without revoking existing certificates").action(async(options)=>{try{if(options.plan&&options.apply)throw Error("Choose either --plan or --apply");const config=resolveAppleDesktopConfig(options);if(!config.bundleId)throw Error("APPLE_BUNDLE_ID or --bundle-id is required");if(!config.apiKeyId||!config.apiIssuerId||!config.apiKeyPath||!existsSync(config.apiKeyPath))throw Error("App Store Connect API key ID, issuer ID, and existing .p8 key path are required");const readCsr=(file)=>{if(!file)return;const resolved=resolve(file);if(!existsSync(resolved))throw Error(`Apple certificate CSR does not exist: ${resolved}`);return readFileSync(resolved,"utf8")},{exportAppleCertificateP12,provisionMacApp}=await import("ts-pantry"),result=await provisionMacApp({identifier:config.bundleId,name:config.appName,capabilities:options.capabilities?.split(",").map((value)=>value.trim()).filter(Boolean),appCertificateCsr:readCsr(options.appCertificateCsr),installerCertificateCsr:readCsr(options.installerCertificateCsr),profileName:options.profileName,keyId:config.apiKeyId,issuerId:config.apiIssuerId,keyPath:config.apiKeyPath,checkOnly:!options.apply}),outputDirectory=resolve(options.output||storagePath("framework/desktop-dist/apple/provisioning"));mkdirSync(outputDirectory,{recursive:!0});for(const certificate of result.certificates){if(!certificate.certificateContent)continue;const fileName=certificate.type==="MAC_APP_DISTRIBUTION"?"mac-app-distribution.cer":"mac-installer-distribution.cer";writeFileSync(join(outputDirectory,fileName),Buffer.from(certificate.certificateContent,"base64"),{mode:384})}if(result.profile.profileContent)writeFileSync(join(outputDirectory,"mac-app-store.provisionprofile"),Buffer.from(result.profile.profileContent,"base64"),{mode:384});const certificatePassword=env("APPLE_CERTIFICATE_PASSWORD"),certificateExports=[{type:"MAC_APP_DISTRIBUTION",csr:options.appCertificateCsr,certificate:"mac-app-distribution.cer",output:"mac-app-distribution.p12",name:`${config.appName} Mac App Distribution`},{type:"MAC_INSTALLER_DISTRIBUTION",csr:options.installerCertificateCsr,certificate:"mac-installer-distribution.cer",output:"mac-installer-distribution.p12",name:`${config.appName} Mac Installer Distribution`}];for(const certificateExport of certificateExports){if(!result.certificates.find((item)=>item.type===certificateExport.type)?.certificateContent||!certificateExport.csr||!certificatePassword)continue;exportAppleCertificateP12({certificatePath:join(outputDirectory,certificateExport.certificate),privateKeyPath:resolve(certificateExport.csr.replace(/\.csr$/i,".key")),outputPath:join(outputDirectory,certificateExport.output),password:certificatePassword,name:certificateExport.name})}const certificates=result.certificates.map((item)=>{const certificate={...item};delete certificate.certificateContent;return certificate}),profile={...result.profile};delete profile.profileContent;const report={...result,certificates,profile},reportPath=join(outputDirectory,"provisioning-plan.json");writeFileSync(reportPath,`${JSON.stringify(report,null,2)}
|
|
102
102
|
`,{mode:384});log.success(`${options.apply?"Applied":"Planned"} Apple provisioning: ${reportPath}`);if(!result.appRecord.exists&&result.appRecord.manualAction)log.warn(result.appRecord.manualAction)}catch(error){fail(error)}});buddy.command("desktop:apple:doctor","Validate Mac App Store tooling, credentials, certificates, and project metadata").option("--app-name <name>","Mac App Store display name").option("--bundle-id <id>","Reverse-DNS bundle identifier").option("--team-id <id>","Apple Developer team ID").option("--app-signing-identity <identity>","Mac App Distribution signing identity").option("--installer-signing-identity <identity>","Mac Installer Distribution signing identity").option("--provisioning-profile <path>","Mac App Store provisioning profile").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>","App Store Connect AuthKey .p8 file").action((options)=>{try{const config=resolveAppleDesktopConfig(options),errors=validateAppleDesktopConfig(config,!0);if(!signingIdentityExists(config.appSigningIdentity))errors.push(`App signing identity is not installed: ${config.appSigningIdentity}`);if(!signingIdentityExists(config.installerSigningIdentity))errors.push(`Installer signing identity is not installed: ${config.installerSigningIdentity}`);if(!provisioningProfileMatches(config))errors.push(`Provisioning profile does not match ${config.teamId}.${config.bundleId}`);if(errors.length)throw Error(errors.join(`
|
|
103
103
|
`));log.success(`Mac App Store prerequisites are ready for ${config.bundleId}`)}catch(error){fail(error)}});buddy.command("desktop:apple:init","Create a GitHub Actions caller for the reusable Stacks Mac App Store workflow").option("--force","Replace an existing workflow").action((options)=>{try{const workflowPath=projectPath(".github/workflows/apple-app-store.yml");if(existsSync(workflowPath)&&!options.force)throw Error(`${workflowPath} already exists. Use --force to replace it.`);mkdirSync(resolve(workflowPath,".."),{recursive:!0});writeFileSync(workflowPath,renderAppleWorkflowCaller());log.success(`Created ${workflowPath}`)}catch(error){fail(error)}});const addSharedOptions=(commandBuilder)=>commandBuilder.option("--app-name <name>","Mac App Store display name").option("--bundle-id <id>","Reverse-DNS bundle identifier").option("--team-id <id>","Apple Developer team ID").option("--app-version <version>","Marketing version").option("--build-number <number>","Unique App Store build number").option("--minimum-macos <version>","Minimum supported macOS version").option("--category <category>","LSApplicationCategoryType value").option("--app-signing-identity <identity>","Mac App Distribution signing identity").option("--installer-signing-identity <identity>","Mac Installer Distribution signing identity").option("--provisioning-profile <path>","Mac App Store provisioning profile").option("--icon <path>","Optional .icns app icon").option("--skip-build","Package existing storage/framework/desktop-dist artifacts");addSharedOptions(buddy.command("desktop:apple:package","Build, sandbox, sign, and package a Mac App Store desktop app")).action(async(options)=>{try{const packagePath=await packageAppleDesktop(resolveAppleDesktopConfig(options),Boolean(options.skipBuild));log.success(`Created signed Mac App Store package ${packagePath}`)}catch(error){fail(error)}});addSharedOptions(buddy.command("desktop:apple:publish","Build and validate or upload a signed Mac App Store package")).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>","App Store Connect AuthKey .p8 file").option("--validate-only","Validate with App Store Connect without uploading").option("--package-only","Create the signed package without contacting App Store Connect").action(async(options)=>{try{const config=resolveAppleDesktopConfig(options),packagePath=await packageAppleDesktop(config,Boolean(options.skipBuild));if(!options.packageOnly)validateOrUpload(packagePath,config,Boolean(options.validateOnly));log.success(options.packageOnly?`Created signed Mac App Store package ${packagePath}`:options.validateOnly?`Validated ${packagePath} with App Store Connect`:`Uploaded ${packagePath} to App Store Connect`)}catch(error){fail(error)}})}
|
package/dist/commands/dev.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{execSync}from"node:child_process";import{existsSync,readdirSync,readFileSync}from"node:fs";import readline from"node:readline";import process from"node:process";import{bold,cyan,dim,green,intro,log,onUnknownSubcommand,outro,prompts,runCommand,yellow}from"@stacksjs/cli";import{homedir}from"node:os";import{dirname,join,relative}from"node:path";import{fileURLToPath}from"node:url";import{Action}from"@stacksjs/enums";import{inspectDefaultsProvenance,libsPath,projectPath,stxPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{version}from"../../package.json";import{resultFailed}from"../result";const DEV_BOOT_STARTING_LINE_COUNT=3;function eraseDevBootStartingLines(){if(!process.stdout.isTTY)return;for(let i=0;i<DEV_BOOT_STARTING_LINE_COUNT;i++){readline.moveCursor(process.stdout,0,-1);readline.clearLine(process.stdout,0)}}let developmentRpx;function resolveRpxEntryPath(){return[projectPath("node_modules/@stacksjs/rpx/dist/index.js"),join(homedir(),"Code/Tools/rpx/packages/rpx/dist/index.js"),projectPath("pantry/@stacksjs/rpx/dist/src/index.js")].find((entry)=>existsSync(entry))??null}async function importDevelopmentRpx(){if(developmentRpx)return developmentRpx;const entry=resolveRpxEntryPath();developmentRpx=entry?await import(entry):await import("@stacksjs/rpx");return developmentRpx}const activeRpxRegistryIds=[];let _actions;async function actions(){if(!_actions)_actions=await import("@stacksjs/actions");return _actions}async function warnOnStaleFrameworkDefaults(){try{const skew=inspectDefaultsProvenance(projectPath());if(skew.status==="not-applicable"||skew.status==="current")return;if(skew.status==="stale"){log.warn(`storage/framework/defaults is from ${skew.vendored}, but @stacksjs/defaults ${skew.installed} is installed.`);log.warn("Booting the installed package instead. Run `buddy upgrade` to bring the tree up to date.");return}const{measureDefaultsDrift}=await actions(),drift=measureDefaultsDrift(projectPath(),{shallow:!0});if(!drift||drift.length===0)return;log.warn(`storage/framework/defaults does not match the installed @stacksjs/defaults ${skew.installed}.`);log.warn("The app runs the vendored copy. Run `buddy upgrade` to sync it, or `buddy doctor` for the file counts.")}catch{}}export const interactiveDevChoices=[{value:"all",title:"All"},{value:"frontend",title:"Frontend"},{value:"api",title:"Backend"},{value:"dashboard",title:"Dashboard"},{value:"desktop",title:"Desktop"},{value:"native",title:"Native App"},{value:"components",title:"Components"},{value:"docs",title:"Documentation"}];export async function dispatchInteractiveDevSelection(selection,runners){if(!Object.hasOwn(runners,selection))return!1;await runners[selection]();return!0}export function resolvePrettyDevDomain(appUrl,nativeMode=!1){if(!appUrl||nativeMode)return null;try{const hostname=new URL(/^https?:\/\//i.test(appUrl)?appUrl:`https://${appUrl}`).hostname.toLowerCase();if(hostname==="localhost"||hostname==="127.0.0.1"||hostname==="::1"||hostname==="0.0.0.0")return null;return hostname}catch{return null}}export function shouldUsePrettyDevUrls(input){return input.domain!==null&&!input.localhostOnly&&(input.proxyManagedExternally||input.systemAuthorized)}const APPLICATION_VIEW_CANDIDATES=["app/index","app","dashboard/index","dashboard","home/index","home","composer","workspace/index","workspace","feed","portal","admin","account/index","account","profile/index","profile"],SITE_LAYOUT_NAMES=new Set(["default","guest","marketing","site"]);export function normalizeDevelopmentEntryPath(path){const value=path?.trim();if(!value||value==="/")return"/";try{const parsed=new URL(value,"http://stacks.localhost");return`${parsed.pathname}${parsed.search}${parsed.hash}`}catch{return"/"}}function configuredApplicationPath(root){if(process.env.APP_PATH)return process.env.APP_PATH;try{return readFileSync(join(root,"config/app.ts"),"utf8").match(/^[ \t]*appPath\s*:\s*(['"`])([^'"`]+)\1/m)?.[2]}catch{return}}function configuredDevelopmentLaunch(root){const environmentValue=process.env.STACKS_DEV_LAUNCH?.trim().toLowerCase();if(environmentValue==="browser"||environmentValue==="native")return environmentValue;try{const value=readFileSync(join(root,"config/app.ts"),"utf8").match(/\bdevLaunch\s*:\s*(['"`])(browser|native)\1/)?.[2];return value==="browser"||value==="native"?value:void 0}catch{return}}export function resolveDevelopmentLaunch(input={}){if(input.browser||input.site)return"browser";if(input.native)return"native";const root=input.root??projectPath();return input.configuredLaunch??configuredDevelopmentLaunch(root)??"browser"}function viewSource(root,route){const base=join(root,"resources/views",route);for(const extension of["stx","vue","html"]){const path=`${base}.${extension}`;if(existsSync(path))try{return readFileSync(path,"utf8")}catch{return""}}return}function usesApplicationLayout(source){const layout=source.match(/@extends\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1]??source.match(/@layout\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1];if(!layout)return!1;return!SITE_LAYOUT_NAMES.has(layout.split("/").at(-1)??layout)}export function resolveDevelopmentEntryPath(input={}){if(input.site)return"/";const root=input.root??projectPath(),configuredPath=input.configuredPath??configuredApplicationPath(root);if(configuredPath)return normalizeDevelopmentEntryPath(configuredPath);const loginExists=viewSource(root,"login")!==void 0||viewSource(root,"auth/login")!==void 0,candidates=APPLICATION_VIEW_CANDIDATES.map((route)=>({route,source:viewSource(root,route)})).filter((candidate)=>candidate.source!==void 0),appCandidate=candidates.find((candidate)=>usesApplicationLayout(candidate.source))??(loginExists?candidates[0]:void 0);if(appCandidate)return normalizeDevelopmentEntryPath(`/${appCandidate.route.replace(/\/index$/,"")}`);return loginExists?"/login":"/"}export function developmentUrl(baseUrl,entryPath){const normalized=normalizeDevelopmentEntryPath(entryPath);if(normalized==="/")return baseUrl.replace(/\/$/,"");return new URL(normalized,`${baseUrl.replace(/\/$/,"")}/`).toString()}export function developmentBrowserCommand(url,platform=process.platform){if(platform==="darwin")return["open",url];if(platform==="win32")return["cmd","/c","start","",url];return["xdg-open",url]}function openDevelopmentBrowser(url){if(!process.stdout.isTTY||process.env.CI||process.env.STACKS_DEV_NO_OPEN==="1")return;try{Bun.spawn(developmentBrowserCommand(url),{stdin:"ignore",stdout:"ignore",stderr:"ignore"}).unref()}catch{}}async function canStartPrettyDevProxy(){if(await waitForHttpsProxy(443,150))return!0;try{const{authorizeSystemAccess}=await importDevelopmentRpx();return authorizeSystemAccess({interactive:!1})}catch{return!1}}export function dev(buddy){const descriptions={dev:"Start development server",frontend:"Start the frontend development server",components:"Start the Components development server",desktop:"Start the Desktop App development server",native:"Start the app in a native Craft window",dashboard:"Start the Dashboard development server",api:"Start the local API development server",docs:"Start the Documentation development server",systemTray:"Start the System Tray development server",interactive:"Get asked which development server to start",select:"Which development server are you trying to start?",withLocalhost:"Include the localhost URL in the output",browser:"Open the application in a browser instead of its configured native window",site:"Open the marketing site instead of the application",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("dev [server]",descriptions.dev).option("-f, --frontend",descriptions.frontend).option("-a, --api",descriptions.api).option("-c, --components",descriptions.components).option("-d, --dashboard",descriptions.dashboard).option("-k, --desktop",descriptions.desktop).option("-n, --native",descriptions.native).option("-o, --docs",descriptions.docs).option("-s, --system-tray",descriptions.systemTray).option("-i, --interactive",descriptions.interactive,{default:!1}).option("-l, --with-localhost",descriptions.withLocalhost,{default:!1}).option("--browser",descriptions.browser,{default:!1}).option("--site",descriptions.site,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(server,options)=>{const perf=Bun.nanoseconds();process.env.STACKS_DEV_ENTRY_PATH=resolveDevelopmentEntryPath({site:options.site});await warnOnStaleFrameworkDefaults();const target=server||(options.frontend?"frontend":void 0)||(options.api?"api":void 0)||(options.components?"components":void 0)||(options.dashboard?"dashboard":void 0)||(options.desktop?"desktop":void 0)||(options.native?"native":void 0)||(options.systemTray||options["system-tray"]?"system-tray":void 0)||(options.docs?"docs":void 0);if(target){const serverOptions={...options},a=await actions();switch(target){case"native":await startDevelopmentServer({...serverOptions,native:!0},perf);break;case"frontend":await a.runFrontendDevServer(serverOptions);break;case"api":await a.runApiDevServer(serverOptions);break;case"components":await a.runComponentsDevServer(serverOptions);break;case"dashboard":await a.runDashboardDevServer(serverOptions);break;case"desktop":await startDevelopmentServer({...serverOptions,native:!0},perf);break;case"system-tray":await a.runSystemTrayDevServer(serverOptions);break;case"docs":await a.runDocsDevServer(serverOptions);break;default:log.error(`Unknown server: ${target}`);process.exit(ExitCode.InvalidArgument)}}else if(wantsInteractive(options)){const selectedValue=await prompts.select({message:descriptions.select,choices:interactiveDevChoices.map((choice)=>({value:choice.value,label:choice.title}))}),a=await actions();if(!await dispatchInteractiveDevSelection(selectedValue,{all:()=>startDevelopmentServer(options,perf),frontend:()=>a.runFrontendDevServer(options),api:()=>a.runApiDevServer(options),dashboard:()=>a.runDashboardDevServer(options),desktop:()=>startDevelopmentServer({...options,native:!0},perf),native:()=>startDevelopmentServer({...options,native:!0},perf),components:()=>a.runComponentsDevServer(options),docs:()=>a.runDocsDevServer(options)})){log.error("Invalid option during interactive mode");process.exit(ExitCode.InvalidArgument)}}else await startDevelopmentServer(options,perf);outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:components",descriptions.components).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:components"),result=await runCommand("bun run dev",{cwd:libsPath("components/stx")});if(options.verbose)log.info("buddy dev:components result",result);if(resultFailed(result)){await outro("While running the dev:components command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:docs",descriptions.docs).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:docs"),result=await(await actions()).runAction(Action.DevDocs,options);if(resultFailed(result)){await outro("While running the dev:docs command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:native",descriptions.native).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:native");await startDevelopmentServer({...options,native:!0},perf)});buddy.command("dev:desktop",descriptions.desktop).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:desktop");await startDevelopmentServer({...options,native:!0},perf)});buddy.command("dev:api",descriptions.api).option("-p, --project [project]",descriptions.project,{default:!1}).option("--no-watch-types","Skip the model/config type-regeneration watcher",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const a=await actions();if(options.watchTypes!==!1)a.watchTypes(options).catch((err)=>{log.warn("[dev:api] type watcher exited:",err)});await a.runApiDevServer(options)});buddy.command("dev:frontend",descriptions.frontend).alias("dev:pages").alias("dev:views").option("-p, --project [project]",descriptions.project,{default:!1}).option("--site",descriptions.site,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{process.env.STACKS_DEV_ENTRY_PATH=resolveDevelopmentEntryPath({site:options.site});await(await actions()).runFrontendDevServer(options)});buddy.command("dev:dashboard",descriptions.dashboard).alias("dev:admin").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await(await actions()).runDashboardDevServer(options)});buddy.command("dev:system-tray",descriptions.systemTray).alias("dev:tray").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await(await actions()).runSystemTrayDevServer(options)});onUnknownSubcommand(buddy,"dev")}export async function startDevelopmentServer(_options,_startTime){const options=_options,startedAt=_startTime,appUrl=process.env.APP_URL??"stacks.localhost",nativeMode=resolveDevelopmentLaunch({browser:options.browser,native:options.native,site:options.site})==="native",entryPath=resolveDevelopmentEntryPath({site:options.site});process.env.STACKS_DEV_ENTRY_PATH=entryPath;const proxyManagedExternally=process.env.STACKS_PROXY_MANAGED==="1",preferredFrontendPort=Number(process.env.PORT)||3000,preferredApiPort=Number(process.env.PORT_API)||3008,preferredDocsPort=Number(process.env.PORT_DOCS)||3006,preferredDashboardPort=Number(process.env.PORT_ADMIN)||3002;if(process.env.STACKS_DEV_NO_KILL!=="1")await cleanupStaleDevProcesses([preferredFrontendPort,preferredApiPort,preferredDocsPort,preferredDashboardPort]);const portShifts=[],claimPort=async(label,preferred)=>{if(proxyManagedExternally)return preferred;const port=await findAvailablePort(preferred);if(port!==preferred)portShifts.push(`${label} :${preferred} \u2192 :${port}`);return port},frontendPort=await claimPort("Frontend",preferredFrontendPort),apiPort=await claimPort("API",preferredApiPort),docsPort=await claimPort("Docs",preferredDocsPort),dashboardPort=await claimPort("Dashboard",preferredDashboardPort);process.env.PORT=String(frontendPort);process.env.PORT_API=String(apiPort);process.env.PORT_DOCS=String(docsPort);process.env.PORT_ADMIN=String(dashboardPort);const includeDashboard=process.env.STACKS_DEV_DASHBOARD==="1",domain=resolvePrettyDevDomain(appUrl,nativeMode),appLooksCustom=domain!==null,localhostOnly=process.env.STACKS_DEV_LOCALHOST==="1",prettyUrlsRequested=appLooksCustom&&!localhostOnly,systemAuthorized=!prettyUrlsRequested||proxyManagedExternally?!0:await canStartPrettyDevProxy(),usePrettyUrls=shouldUsePrettyDevUrls({domain,localhostOnly,proxyManagedExternally,systemAuthorized}),prettySetupRequired=prettyUrlsRequested&&!usePrettyUrls,hasCustomDomain=usePrettyUrls&&!proxyManagedExternally,displayedDomain=usePrettyUrls?domain:null,dashboardDomain=displayedDomain?`dashboard.${displayedDomain}`:null,frontendBaseUrl=displayedDomain?`https://${displayedDomain}`:`http://localhost:${frontendPort}`,frontendUrl=developmentUrl(frontendBaseUrl,entryPath),apiUrl=displayedDomain?`https://${displayedDomain}/api`:`http://localhost:${apiPort}`,docsUrl=displayedDomain?`https://${displayedDomain}/docs`:`http://localhost:${docsPort}`,dashboardUrl=dashboardDomain?`https://${dashboardDomain}`:`http://localhost:${dashboardPort}`;process.env.STACKS_PROXY_MANAGED="1";process.env.STACKS_DEV_QUIET="1";console.log();if(prettySetupRequired){console.log(` ${yellow("\u26A0")} ${yellow("Pretty URL setup required")} ${dim("- using localhost for this session")}`);console.log(` ${dim(" ")}${dim("Run `./buddy setup:ssl` once, then restart `./buddy dev`.")}`);console.log()}if(portShifts.length>0){console.log(` ${yellow("\u26A0")} ${yellow("Ports in use by another process")} ${dim(`- ${portShifts.join(", ")}`)}`);console.log()}console.log(` ${bold(cyan("stacks"))} ${dim(`v${version}`)} ${dim("starting\u2026")}`);console.log();const rpxTlsPreflight=hasCustomDomain&&domain?prepareRpxTlsForDev({domain,includeDashboard,options}):Promise.resolve();if(localhostOnly&&domain&&!proxyManagedExternally)removeStalePublicDomainOverrides(domain,includeDashboard,options.verbose??!1).catch(()=>{});rpxTlsPreflight.catch(()=>{});let isExiting=!1,closeNativeApp;const SHUTDOWN_GRACE_MS=1500,cleanup=()=>{if(isExiting)return;isExiting=!0;closeNativeApp?.();const rpxTeardown=(async()=>{try{await unregisterRpxProxies(activeRpxRegistryIds);activeRpxRegistryIds.length=0;if(hasCustomDomain&&domain)await removeStalePublicDomainOverrides(domain,includeDashboard,options.verbose??!1)}catch{}})(),teardownDeadline=new Promise((resolve)=>setTimeout(resolve,SHUTDOWN_GRACE_MS));Promise.race([rpxTeardown,teardownDeadline]).finally(()=>{try{process.kill(-process.pid,"SIGTERM")}catch{try{process.kill(0,"SIGTERM")}catch{}}setTimeout(()=>{try{process.kill(0,"SIGKILL")}catch{process.exit(1)}},SHUTDOWN_GRACE_MS).unref()})};process.on("SIGINT",cleanup);process.on("SIGTERM",cleanup);process.on("SIGHUP",cleanup);const quietOpts={...options,quiet:!0},a=await actions(),ports=[{name:"API",port:apiPort}],readinessTimeoutMs=30000;let readyAnnounced=!1;const nativeUrl=developmentUrl(`http://localhost:${frontendPort}`,entryPath),nativeWindowReady=nativeMode?waitForPort(frontendPort,readinessTimeoutMs).then(async(ready)=>{if(!ready){log.warn(`Native window skipped because the frontend did not answer within ${readinessTimeoutMs/1000}s`);return}closeNativeApp=await launchNativeAppWindow(nativeUrl,options)}):Promise.resolve();Promise.all(ports.map((p)=>waitForPort(p.port,readinessTimeoutMs))).then(async(results)=>{if(readyAnnounced)return;readyAnnounced=!0;const failed=results.map((ok,i)=>ok?null:ports[i]?.name??null).filter((x)=>x!==null);let proxyReachable=!1;if(hasCustomDomain&&domain){(async()=>{try{await rpxTlsPreflight;await registerRpxProxiesForDomain({domain,frontendPort,apiPort,docsPort,dashboardPort,includeDashboard,options})}catch{}})();proxyReachable=await waitForHttpsProxy(443,16000);if(!proxyReachable){console.log(` ${yellow("\u26A0")} ${yellow("HTTPS proxy not reachable on :443")} - serving ${cyan(`http://localhost:${frontendPort}`)} instead`);console.log(` ${dim(" ")}${dim("rpx needs a valid SUDO_PASSWORD in .env to bind :443; trust the local CA, then restart `./buddy dev`.")}`);if(options.verbose)console.log(` ${dim(" ")}${dim(`Trust CA: sh ${join(RPX_SSL_DIR,"trust-rpx-cert.sh")}`)}`)}}eraseDevBootStartingLines();printDevReadyBanner({options,nativeMode,hasCustomDomain:!!appLooksCustom&&!localhostOnly,proxyReachable:proxyManagedExternally?!0:proxyReachable,frontendUrl,frontendBaseUrl,entryPath,apiUrl,docsUrl,dashboardUrl,frontendPort,apiPort,docsPort,includeDashboard,domain,dashboardPort,dashboardDomain});if(!nativeMode){const browserUrl=hasCustomDomain&&(proxyManagedExternally||proxyReachable)?frontendUrl:developmentUrl(`http://localhost:${frontendPort}`,entryPath);openDevelopmentBrowser(browserUrl)}if(startedAt){const elapsedMs=(Bun.nanoseconds()-startedAt)/1e6,summary=failed.length?`ready in ${(elapsedMs/1000).toFixed(1)}s - ${failed.join(", ")} did not bind within ${readinessTimeoutMs/1000}s`:`ready in ${(elapsedMs/1000).toFixed(1)}s`;console.log(` ${dim(summary)}`);console.log();printDevEngineNotes();console.log()}if(process.env.STACKS_PRINT_ROUTES==="1")await printRegisteredRoutes(apiPort).catch(()=>{})}).catch(()=>{});await Promise.all([a.runFrontendDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Frontend: ${error}`)}),a.runApiDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`API: ${error}`)}),a.runDocsDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Docs: ${error}`)}),includeDashboard?a.runDashboardDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Dashboard: ${error}`)}):Promise.resolve(),nativeWindowReady.catch((error)=>{if(options.verbose)log.warn(`Native window: ${error}`)})])}function isComingSoonMode(){if(existsSync(projectPath("storage/framework/coming-soon")))return!0;try{const idx=projectPath("resources/views/index.stx");return existsSync(idx)&&/siteMode\s*=\s*['"]coming-soon['"]/.test(readFileSync(idx,"utf8"))}catch{return!1}}function blogIsConfigured(){try{const dir=projectPath("content/blog");return existsSync(dir)&&readdirSync(dir).some((f)=>f.endsWith(".md"))}catch{return!1}}function printDevReadyBanner(input){const{options,nativeMode,hasCustomDomain,proxyReachable,frontendUrl,frontendBaseUrl,entryPath,apiUrl,docsUrl,dashboardUrl,frontendPort,apiPort,docsPort,includeDashboard,domain,dashboardPort,dashboardDomain}=input,verbose=options.verbose??!1,useProxy=hasCustomDomain&&proxyReachable,feUrl=useProxy?frontendUrl:developmentUrl(`http://localhost:${frontendPort}`,entryPath),feBaseUrl=useProxy?frontendBaseUrl:`http://localhost:${frontendPort}`,apUrl=useProxy?apiUrl:`http://localhost:${apiPort}`,dcUrl=useProxy?docsUrl:`http://localhost:${docsPort}`,dbUrl=useProxy?dashboardUrl:`http://localhost:${dashboardPort}`,blogUrl=`${feBaseUrl}/blog`;console.log();const frontendLabel=entryPath==="/"?"Site":"App";console.log(` ${green("\u279C")} ${bold(frontendLabel.padEnd(8))}: ${cyan(feUrl)}`);if(entryPath!=="/")console.log(` ${dim("\u279C")} ${dim("Site")}: ${dim(feBaseUrl)}`);if(nativeMode)console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192 ${feUrl}`)}`);console.log(` ${green("\u279C")} ${bold("API")}: ${cyan(apUrl)}`);console.log(` ${green("\u279C")} ${bold("Docs")}: ${cyan(dcUrl)}`);if(blogIsConfigured())console.log(` ${green("\u279C")} ${bold("Blog")}: ${cyan(blogUrl)}`);if(includeDashboard)console.log(` ${green("\u279C")} ${bold("Dashboard")}: ${cyan(dbUrl)}`);if(useProxy){console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`${developmentUrl(`http://localhost:${frontendPort}`,entryPath)} (bypasses the proxy)`)}`);if(includeDashboard&&dashboardDomain)console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${dashboardPort} (dashboard, bypasses the proxy)`)}`)}if(isComingSoonMode()){console.log();console.log(` ${yellow("\u25CF")} ${bold(yellow("Coming soon mode"))} ${dim("- visitors see the holding page; bypass with the coming-soon secret.")}`)}if(verbose&&domain){console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${frontendPort} \u2192 ${domain}`)}`);console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/api \u2192 localhost:${apiPort}`)}`);console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/docs \u2192 localhost:${docsPort}`)}`);if(includeDashboard)console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${dashboardPort} \u2192 ${dashboardDomain}`)}`)}console.log()}function printDevEngineNotes(){const routesFile=stxPath("routes.ts"),routesLabel=relative(projectPath(),routesFile),crosswindConfig=join(projectPath(),"config/crosswind.ts");if(existsSync(crosswindConfig)||existsSync(join(projectPath(),"crosswind.config.ts")))console.log(` ${green("[Crosswind]")} ${dim("CSS engine loaded")}`);if(existsSync(routesFile))try{const routeCount=(readFileSync(routesFile,"utf8").match(/pattern:/g)??[]).length;if(routeCount>0)console.log(` ${green("[stx]")} ${dim(`Generated ${routeCount} routes \u2192 ${routesLabel}`)}`)}catch{console.log(` ${green("[stx]")} ${dim(`Routes manifest \u2192 ${routesLabel}`)}`)}}async function launchNativeAppWindow(url,options){let createApp;try{createApp=(await importCraftSdk()).createApp}catch(error){if(options.verbose)log.warn(`Native window unavailable: ${error}`);console.log(` ${dim("Native window unavailable. Install craft-native or set CRAFT_BIN to a Craft binary.")}
|
|
1
|
+
import{execSync}from"node:child_process";import{existsSync,readdirSync,readFileSync}from"node:fs";import readline from"node:readline";import process from"node:process";import{bold,cyan,dim,green,intro,log,onUnknownSubcommand,outro,prompts,runCommand,yellow}from"@stacksjs/cli";import{homedir}from"node:os";import{dirname,join,relative}from"node:path";import{fileURLToPath}from"node:url";import{Action}from"@stacksjs/enums";import{inspectDefaultsProvenance,libsPath,projectPath,stxPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{version}from"../../package.json";import{resultFailed}from"../result";const DEV_BOOT_STARTING_LINE_COUNT=3;function eraseDevBootStartingLines(){if(!process.stdout.isTTY)return;for(let i=0;i<DEV_BOOT_STARTING_LINE_COUNT;i++){readline.moveCursor(process.stdout,0,-1);readline.clearLine(process.stdout,0)}}let developmentRpx;function resolveRpxEntryPath(){return[projectPath("node_modules/@stacksjs/rpx/dist/index.js"),join(homedir(),"Code/Tools/rpx/packages/rpx/dist/index.js"),projectPath("pantry/@stacksjs/rpx/dist/src/index.js")].find((entry)=>existsSync(entry))??null}async function importDevelopmentRpx(){if(developmentRpx)return developmentRpx;const entry=resolveRpxEntryPath();developmentRpx=entry?await import(entry):await import("@stacksjs/rpx");return developmentRpx}const activeRpxRegistryIds=[];let _actions;async function actions(){if(!_actions)_actions=await import("@stacksjs/actions");return _actions}async function warnOnStaleFrameworkDefaults(){try{const skew=inspectDefaultsProvenance(projectPath());if(skew.status==="not-applicable"||skew.status==="current")return;if(skew.status==="stale"){log.warn(`storage/framework/defaults is from ${skew.vendored}, but @stacksjs/defaults ${skew.installed} is installed.`);log.warn("Booting the installed package instead. Run `buddy upgrade` to bring the tree up to date.");return}const{measureDefaultsDrift}=await actions(),drift=measureDefaultsDrift(projectPath(),{shallow:!0});if(!drift||drift.length===0)return;log.warn(`storage/framework/defaults does not match the installed @stacksjs/defaults ${skew.installed}.`);log.warn("The app runs the vendored copy. Run `buddy upgrade` to sync it, or `buddy doctor` for the file counts.")}catch{}}export const interactiveDevChoices=[{value:"all",title:"All"},{value:"frontend",title:"Frontend"},{value:"api",title:"Backend"},{value:"dashboard",title:"Dashboard"},{value:"desktop",title:"Desktop"},{value:"native",title:"Native App"},{value:"components",title:"Components"},{value:"docs",title:"Documentation"}];export async function dispatchInteractiveDevSelection(selection,runners){if(!Object.hasOwn(runners,selection))return!1;await runners[selection]();return!0}export function resolvePrettyDevDomain(appUrl,nativeMode=!1){if(!appUrl||nativeMode)return null;try{const hostname=new URL(/^https?:\/\//i.test(appUrl)?appUrl:`https://${appUrl}`).hostname.toLowerCase();if(hostname==="localhost"||hostname==="127.0.0.1"||hostname==="::1"||hostname==="0.0.0.0")return null;return hostname}catch{return null}}export function shouldUsePrettyDevUrls(input){return input.domain!==null&&!input.localhostOnly&&(input.proxyManagedExternally||input.systemAuthorized)}const APPLICATION_VIEW_CANDIDATES=["app/index","app","dashboard/index","dashboard","home/index","home","composer","workspace/index","workspace","feed","portal","admin","account/index","account","profile/index","profile"],SITE_LAYOUT_NAMES=new Set(["default","guest","marketing","site"]);export function normalizeDevelopmentEntryPath(path){const value=path?.trim();if(!value||value==="/")return"/";try{const parsed=new URL(value,"http://stacks.localhost");return`${parsed.pathname}${parsed.search}${parsed.hash}`}catch{return"/"}}function configuredApplicationPath(root){if(process.env.APP_PATH)return process.env.APP_PATH;try{return readFileSync(join(root,"config/app.ts"),"utf8").match(/^[ \t]*appPath\s*:\s*(['"`])([^'"`]+)\1/m)?.[2]}catch{return}}function configuredDevelopmentLaunch(root){const environmentValue=process.env.STACKS_DEV_LAUNCH?.trim().toLowerCase();if(environmentValue==="browser"||environmentValue==="native")return environmentValue;try{const value=readFileSync(join(root,"config/app.ts"),"utf8").match(/\bdevLaunch\s*:\s*(['"`])(browser|native)\1/)?.[2];return value==="browser"||value==="native"?value:void 0}catch{return}}export function resolveDevelopmentLaunch(input={}){if(input.browser||input.site)return"browser";if(input.native)return"native";const root=input.root??projectPath();return input.configuredLaunch??configuredDevelopmentLaunch(root)??"browser"}function viewSource(root,route){const base=join(root,"resources/views",route);for(const extension of["stx","vue","html"]){const path=`${base}.${extension}`;if(existsSync(path))try{return readFileSync(path,"utf8")}catch{return""}}return}function usesApplicationLayout(source){const layout=source.match(/@extends\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1]??source.match(/@layout\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1];if(!layout)return!1;return!SITE_LAYOUT_NAMES.has(layout.split("/").at(-1)??layout)}export function resolveDevelopmentEntryPath(input={}){if(input.site)return"/";const root=input.root??projectPath(),configuredPath=input.configuredPath??configuredApplicationPath(root);if(configuredPath)return normalizeDevelopmentEntryPath(configuredPath);const loginExists=viewSource(root,"login")!==void 0||viewSource(root,"auth/login")!==void 0,candidates=APPLICATION_VIEW_CANDIDATES.map((route)=>({route,source:viewSource(root,route)})).filter((candidate)=>candidate.source!==void 0),appCandidate=candidates.find((candidate)=>usesApplicationLayout(candidate.source))??(loginExists?candidates[0]:void 0);if(appCandidate)return normalizeDevelopmentEntryPath(`/${appCandidate.route.replace(/\/index$/,"")}`);return loginExists?"/login":"/"}export function developmentUrl(baseUrl,entryPath){const normalized=normalizeDevelopmentEntryPath(entryPath);if(normalized==="/")return baseUrl.replace(/\/$/,"");return new URL(normalized,`${baseUrl.replace(/\/$/,"")}/`).toString()}export function developmentBrowserCommand(url,platform=process.platform){if(platform==="darwin")return["open",url];if(platform==="win32")return["cmd","/c","start","",url];return["xdg-open",url]}function openDevelopmentBrowser(url){if(!process.stdout.isTTY||process.env.CI||process.env.STACKS_DEV_NO_OPEN==="1")return;try{Bun.spawn(developmentBrowserCommand(url),{stdin:"ignore",stdout:"ignore",stderr:"ignore"}).unref()}catch{}}async function canStartPrettyDevProxy(){if(await waitForHttpsProxy(443,150))return!0;try{const{authorizeSystemAccess}=await importDevelopmentRpx();return authorizeSystemAccess({interactive:!1})}catch{return!1}}export function dev(buddy){const descriptions={dev:"Start development server",frontend:"Start the frontend development server",components:"Start the Components development server",desktop:"Start the Desktop App development server",native:"Start the app in a native Craft window",dashboard:"Start the Dashboard development server",api:"Start the local API development server",docs:"Start the Documentation development server",systemTray:"Start the System Tray development server",interactive:"Get asked which development server to start",select:"Which development server are you trying to start?",withLocalhost:"Include the localhost URL in the output",browser:"Open the application in a browser instead of its configured native window",site:"Open the marketing site instead of the application",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("dev [server]",descriptions.dev).option("-f, --frontend",descriptions.frontend).option("-a, --api",descriptions.api).option("-c, --components",descriptions.components).option("-d, --dashboard",descriptions.dashboard).option("-k, --desktop",descriptions.desktop).option("-n, --native",descriptions.native).option("-o, --docs",descriptions.docs).option("-s, --system-tray",descriptions.systemTray).option("-i, --interactive",descriptions.interactive,{default:!1}).option("-l, --with-localhost",descriptions.withLocalhost,{default:!1}).option("--browser",descriptions.browser,{default:!1}).option("--site",descriptions.site,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(server,options)=>{const perf=Bun.nanoseconds();process.env.STACKS_DEV_ENTRY_PATH=resolveDevelopmentEntryPath({site:options.site});await warnOnStaleFrameworkDefaults();const target=server||(options.frontend?"frontend":void 0)||(options.api?"api":void 0)||(options.components?"components":void 0)||(options.dashboard?"dashboard":void 0)||(options.desktop?"desktop":void 0)||(options.native?"native":void 0)||(options.systemTray||options["system-tray"]?"system-tray":void 0)||(options.docs?"docs":void 0);if(target){const serverOptions={...options},a=await actions();switch(target){case"native":await startDevelopmentServer({...serverOptions,native:!0},perf);break;case"frontend":await a.runFrontendDevServer(serverOptions);break;case"api":await a.runApiDevServer(serverOptions);break;case"components":await a.runComponentsDevServer(serverOptions);break;case"dashboard":await a.runDashboardDevServer(serverOptions);break;case"desktop":await startDevelopmentServer({...serverOptions,native:!0},perf);break;case"system-tray":await a.runSystemTrayDevServer(serverOptions);break;case"docs":await a.runDocsDevServer(serverOptions);break;default:await log.error(`Unknown server: ${target}`);process.exit(ExitCode.InvalidArgument)}}else if(wantsInteractive(options)){const selectedValue=await prompts.select({message:descriptions.select,choices:interactiveDevChoices.map((choice)=>({value:choice.value,label:choice.title}))}),a=await actions();if(!await dispatchInteractiveDevSelection(selectedValue,{all:()=>startDevelopmentServer(options,perf),frontend:()=>a.runFrontendDevServer(options),api:()=>a.runApiDevServer(options),dashboard:()=>a.runDashboardDevServer(options),desktop:()=>startDevelopmentServer({...options,native:!0},perf),native:()=>startDevelopmentServer({...options,native:!0},perf),components:()=>a.runComponentsDevServer(options),docs:()=>a.runDocsDevServer(options)})){await log.error("Invalid option during interactive mode");process.exit(ExitCode.InvalidArgument)}}else await startDevelopmentServer(options,perf);outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:components",descriptions.components).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:components"),result=await runCommand("bun run dev",{cwd:libsPath("components/stx")});if(options.verbose)log.info("buddy dev:components result",result);if(resultFailed(result)){await outro("While running the dev:components command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:docs",descriptions.docs).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:docs"),result=await(await actions()).runAction(Action.DevDocs,options);if(resultFailed(result)){await outro("While running the dev:docs command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:native",descriptions.native).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:native");await startDevelopmentServer({...options,native:!0},perf)});buddy.command("dev:desktop",descriptions.desktop).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:desktop");await startDevelopmentServer({...options,native:!0},perf)});buddy.command("dev:api",descriptions.api).option("-p, --project [project]",descriptions.project,{default:!1}).option("--no-watch-types","Skip the model/config type-regeneration watcher",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const a=await actions();if(options.watchTypes!==!1)a.watchTypes(options).catch((err)=>{log.warn("[dev:api] type watcher exited:",err)});await a.runApiDevServer(options)});buddy.command("dev:frontend",descriptions.frontend).alias("dev:pages").alias("dev:views").option("-p, --project [project]",descriptions.project,{default:!1}).option("--site",descriptions.site,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{process.env.STACKS_DEV_ENTRY_PATH=resolveDevelopmentEntryPath({site:options.site});await(await actions()).runFrontendDevServer(options)});buddy.command("dev:dashboard",descriptions.dashboard).alias("dev:admin").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await(await actions()).runDashboardDevServer(options)});buddy.command("dev:system-tray",descriptions.systemTray).alias("dev:tray").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await(await actions()).runSystemTrayDevServer(options)});onUnknownSubcommand(buddy,"dev")}export async function startDevelopmentServer(_options,_startTime){const options=_options,startedAt=_startTime,appUrl=process.env.APP_URL??"stacks.localhost",nativeMode=resolveDevelopmentLaunch({browser:options.browser,native:options.native,site:options.site})==="native",entryPath=resolveDevelopmentEntryPath({site:options.site});process.env.STACKS_DEV_ENTRY_PATH=entryPath;const proxyManagedExternally=process.env.STACKS_PROXY_MANAGED==="1",preferredFrontendPort=Number(process.env.PORT)||3000,preferredApiPort=Number(process.env.PORT_API)||3008,preferredDocsPort=Number(process.env.PORT_DOCS)||3006,preferredDashboardPort=Number(process.env.PORT_ADMIN)||3002;if(process.env.STACKS_DEV_NO_KILL!=="1")await cleanupStaleDevProcesses([preferredFrontendPort,preferredApiPort,preferredDocsPort,preferredDashboardPort]);const portShifts=[],claimPort=async(label,preferred)=>{if(proxyManagedExternally)return preferred;const port=await findAvailablePort(preferred);if(port!==preferred)portShifts.push(`${label} :${preferred} \u2192 :${port}`);return port},frontendPort=await claimPort("Frontend",preferredFrontendPort),apiPort=await claimPort("API",preferredApiPort),docsPort=await claimPort("Docs",preferredDocsPort),dashboardPort=await claimPort("Dashboard",preferredDashboardPort);process.env.PORT=String(frontendPort);process.env.PORT_API=String(apiPort);process.env.PORT_DOCS=String(docsPort);process.env.PORT_ADMIN=String(dashboardPort);const includeDashboard=process.env.STACKS_DEV_DASHBOARD==="1",domain=resolvePrettyDevDomain(appUrl,nativeMode),appLooksCustom=domain!==null,localhostOnly=process.env.STACKS_DEV_LOCALHOST==="1",prettyUrlsRequested=appLooksCustom&&!localhostOnly,systemAuthorized=!prettyUrlsRequested||proxyManagedExternally?!0:await canStartPrettyDevProxy(),usePrettyUrls=shouldUsePrettyDevUrls({domain,localhostOnly,proxyManagedExternally,systemAuthorized}),prettySetupRequired=prettyUrlsRequested&&!usePrettyUrls,hasCustomDomain=usePrettyUrls&&!proxyManagedExternally,displayedDomain=usePrettyUrls?domain:null,dashboardDomain=displayedDomain?`dashboard.${displayedDomain}`:null,frontendBaseUrl=displayedDomain?`https://${displayedDomain}`:`http://localhost:${frontendPort}`,frontendUrl=developmentUrl(frontendBaseUrl,entryPath),apiUrl=displayedDomain?`https://${displayedDomain}/api`:`http://localhost:${apiPort}`,docsUrl=displayedDomain?`https://${displayedDomain}/docs`:`http://localhost:${docsPort}`,dashboardUrl=dashboardDomain?`https://${dashboardDomain}`:`http://localhost:${dashboardPort}`;process.env.STACKS_PROXY_MANAGED="1";process.env.STACKS_DEV_QUIET="1";console.log();if(prettySetupRequired){console.log(` ${yellow("\u26A0")} ${yellow("Pretty URL setup required")} ${dim("- using localhost for this session")}`);console.log(` ${dim(" ")}${dim("Run `./buddy setup:ssl` once, then restart `./buddy dev`.")}`);console.log()}if(portShifts.length>0){console.log(` ${yellow("\u26A0")} ${yellow("Ports in use by another process")} ${dim(`- ${portShifts.join(", ")}`)}`);console.log()}console.log(` ${bold(cyan("stacks"))} ${dim(`v${version}`)} ${dim("starting\u2026")}`);console.log();const rpxTlsPreflight=hasCustomDomain&&domain?prepareRpxTlsForDev({domain,includeDashboard,options}):Promise.resolve();if(localhostOnly&&domain&&!proxyManagedExternally)removeStalePublicDomainOverrides(domain,includeDashboard,options.verbose??!1).catch(()=>{});rpxTlsPreflight.catch(()=>{});let isExiting=!1,closeNativeApp;const SHUTDOWN_GRACE_MS=1500,cleanup=()=>{if(isExiting)return;isExiting=!0;closeNativeApp?.();const rpxTeardown=(async()=>{try{await unregisterRpxProxies(activeRpxRegistryIds);activeRpxRegistryIds.length=0;if(hasCustomDomain&&domain)await removeStalePublicDomainOverrides(domain,includeDashboard,options.verbose??!1)}catch{}})(),teardownDeadline=new Promise((resolve)=>setTimeout(resolve,SHUTDOWN_GRACE_MS));Promise.race([rpxTeardown,teardownDeadline]).finally(()=>{try{process.kill(-process.pid,"SIGTERM")}catch{try{process.kill(0,"SIGTERM")}catch{}}setTimeout(()=>{try{process.kill(0,"SIGKILL")}catch{process.exit(1)}},SHUTDOWN_GRACE_MS).unref()})};process.on("SIGINT",cleanup);process.on("SIGTERM",cleanup);process.on("SIGHUP",cleanup);const quietOpts={...options,quiet:!0},a=await actions(),ports=[{name:"API",port:apiPort}],readinessTimeoutMs=30000;let readyAnnounced=!1;const nativeUrl=developmentUrl(`http://localhost:${frontendPort}`,entryPath),nativeWindowReady=nativeMode?waitForPort(frontendPort,readinessTimeoutMs).then(async(ready)=>{if(!ready){log.warn(`Native window skipped because the frontend did not answer within ${readinessTimeoutMs/1000}s`);return}closeNativeApp=await launchNativeAppWindow(nativeUrl,options)}):Promise.resolve();Promise.all(ports.map((p)=>waitForPort(p.port,readinessTimeoutMs))).then(async(results)=>{if(readyAnnounced)return;readyAnnounced=!0;const failed=results.map((ok,i)=>ok?null:ports[i]?.name??null).filter((x)=>x!==null);let proxyReachable=!1;if(hasCustomDomain&&domain){(async()=>{try{await rpxTlsPreflight;await registerRpxProxiesForDomain({domain,frontendPort,apiPort,docsPort,dashboardPort,includeDashboard,options})}catch{}})();proxyReachable=await waitForHttpsProxy(443,16000);if(!proxyReachable){console.log(` ${yellow("\u26A0")} ${yellow("HTTPS proxy not reachable on :443")} - serving ${cyan(`http://localhost:${frontendPort}`)} instead`);console.log(` ${dim(" ")}${dim("rpx needs a valid SUDO_PASSWORD in .env to bind :443; trust the local CA, then restart `./buddy dev`.")}`);if(options.verbose)console.log(` ${dim(" ")}${dim(`Trust CA: sh ${join(RPX_SSL_DIR,"trust-rpx-cert.sh")}`)}`)}}eraseDevBootStartingLines();printDevReadyBanner({options,nativeMode,hasCustomDomain:!!appLooksCustom&&!localhostOnly,proxyReachable:proxyManagedExternally?!0:proxyReachable,frontendUrl,frontendBaseUrl,entryPath,apiUrl,docsUrl,dashboardUrl,frontendPort,apiPort,docsPort,includeDashboard,domain,dashboardPort,dashboardDomain});if(!nativeMode){const browserUrl=hasCustomDomain&&(proxyManagedExternally||proxyReachable)?frontendUrl:developmentUrl(`http://localhost:${frontendPort}`,entryPath);openDevelopmentBrowser(browserUrl)}if(startedAt){const elapsedMs=(Bun.nanoseconds()-startedAt)/1e6,summary=failed.length?`ready in ${(elapsedMs/1000).toFixed(1)}s - ${failed.join(", ")} did not bind within ${readinessTimeoutMs/1000}s`:`ready in ${(elapsedMs/1000).toFixed(1)}s`;console.log(` ${dim(summary)}`);console.log();printDevEngineNotes();console.log()}if(process.env.STACKS_PRINT_ROUTES==="1")await printRegisteredRoutes(apiPort).catch(()=>{})}).catch(()=>{});await Promise.all([a.runFrontendDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Frontend: ${error}`)}),a.runApiDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`API: ${error}`)}),a.runDocsDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Docs: ${error}`)}),includeDashboard?a.runDashboardDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Dashboard: ${error}`)}):Promise.resolve(),nativeWindowReady.catch((error)=>{if(options.verbose)log.warn(`Native window: ${error}`)})])}function isComingSoonMode(){if(existsSync(projectPath("storage/framework/coming-soon")))return!0;try{const idx=projectPath("resources/views/index.stx");return existsSync(idx)&&/siteMode\s*=\s*['"]coming-soon['"]/.test(readFileSync(idx,"utf8"))}catch{return!1}}function blogIsConfigured(){try{const dir=projectPath("content/blog");return existsSync(dir)&&readdirSync(dir).some((f)=>f.endsWith(".md"))}catch{return!1}}function printDevReadyBanner(input){const{options,nativeMode,hasCustomDomain,proxyReachable,frontendUrl,frontendBaseUrl,entryPath,apiUrl,docsUrl,dashboardUrl,frontendPort,apiPort,docsPort,includeDashboard,domain,dashboardPort,dashboardDomain}=input,verbose=options.verbose??!1,useProxy=hasCustomDomain&&proxyReachable,feUrl=useProxy?frontendUrl:developmentUrl(`http://localhost:${frontendPort}`,entryPath),feBaseUrl=useProxy?frontendBaseUrl:`http://localhost:${frontendPort}`,apUrl=useProxy?apiUrl:`http://localhost:${apiPort}`,dcUrl=useProxy?docsUrl:`http://localhost:${docsPort}`,dbUrl=useProxy?dashboardUrl:`http://localhost:${dashboardPort}`,blogUrl=`${feBaseUrl}/blog`;console.log();const frontendLabel=entryPath==="/"?"Site":"App";console.log(` ${green("\u279C")} ${bold(frontendLabel.padEnd(8))}: ${cyan(feUrl)}`);if(entryPath!=="/")console.log(` ${dim("\u279C")} ${dim("Site")}: ${dim(feBaseUrl)}`);if(nativeMode)console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192 ${feUrl}`)}`);console.log(` ${green("\u279C")} ${bold("API")}: ${cyan(apUrl)}`);console.log(` ${green("\u279C")} ${bold("Docs")}: ${cyan(dcUrl)}`);if(blogIsConfigured())console.log(` ${green("\u279C")} ${bold("Blog")}: ${cyan(blogUrl)}`);if(includeDashboard)console.log(` ${green("\u279C")} ${bold("Dashboard")}: ${cyan(dbUrl)}`);if(useProxy){console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`${developmentUrl(`http://localhost:${frontendPort}`,entryPath)} (bypasses the proxy)`)}`);if(includeDashboard&&dashboardDomain)console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${dashboardPort} (dashboard, bypasses the proxy)`)}`)}if(isComingSoonMode()){console.log();console.log(` ${yellow("\u25CF")} ${bold(yellow("Coming soon mode"))} ${dim("- visitors see the holding page; bypass with the coming-soon secret.")}`)}if(verbose&&domain){console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${frontendPort} \u2192 ${domain}`)}`);console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/api \u2192 localhost:${apiPort}`)}`);console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/docs \u2192 localhost:${docsPort}`)}`);if(includeDashboard)console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${dashboardPort} \u2192 ${dashboardDomain}`)}`)}console.log()}function printDevEngineNotes(){const routesFile=stxPath("routes.ts"),routesLabel=relative(projectPath(),routesFile),crosswindConfig=join(projectPath(),"config/crosswind.ts");if(existsSync(crosswindConfig)||existsSync(join(projectPath(),"crosswind.config.ts")))console.log(` ${green("[Crosswind]")} ${dim("CSS engine loaded")}`);if(existsSync(routesFile))try{const routeCount=(readFileSync(routesFile,"utf8").match(/pattern:/g)??[]).length;if(routeCount>0)console.log(` ${green("[stx]")} ${dim(`Generated ${routeCount} routes \u2192 ${routesLabel}`)}`)}catch{console.log(` ${green("[stx]")} ${dim(`Routes manifest \u2192 ${routesLabel}`)}`)}}async function launchNativeAppWindow(url,options){let createApp;try{createApp=(await importCraftSdk()).createApp}catch(error){if(options.verbose)log.warn(`Native window unavailable: ${error}`);console.log(` ${dim("Native window unavailable. Install craft-native or set CRAFT_BIN to a Craft binary.")}
|
|
2
2
|
`);return}if(!createApp){console.log(` ${dim("Native window unavailable. The Craft SDK did not export createApp.")}
|
|
3
3
|
`);return}const craftBinaryPath=resolveCraftBinaryPath();if(!craftBinaryPath){console.log(` ${dim("Native window unavailable. Set CRAFT_BIN to a Craft binary, or install Craft in ~/Code/Tools/craft.")}
|
|
4
4
|
`);return}const appIconPath=resolveNativeAppIconPath(),app=createApp({url,quiet:!options.verbose,craftPath:craftBinaryPath,window:{title:await resolveNativeAppTitle(),width:1280,height:860,titlebarHidden:!0,webSidebarMaterial:!0,webSidebarWidth:286,webSidebarMaterialOpacity:0.78,...appIconPath&&{icon:appIconPath}}});app.show().catch((error)=>{if(options.verbose)log.warn(`Native window exited: ${error}`)});return()=>app.close()}async function importCraftSdk(){const localCraftSdk=process.env.HOME?`${process.env.HOME}/Code/Tools/craft/packages/typescript/src/index.ts`:void 0;if(localCraftSdk&&existsSync(localCraftSdk))return await import(localCraftSdk);const packageNames=["craft-native","@craft-native/craft","@stacksjs/ts-craft"];let primaryError;for(const packageName of packageNames)try{return await import(packageName)}catch(error){primaryError??=error}throw primaryError}async function cleanupStaleDevProcesses(ports){const projectRoot=projectPath(),actionDevPath=projectPath("storage/framework/core/actions/src/dev/"),pids=new Set;for(const port of ports)for(const pid of await listenerPids(port)){if(pid===process.pid)continue;const command=await commandForPid(pid);if(!command.includes(projectRoot))continue;if(command.includes("storage/framework/core/buddy/src/cli.ts dev")||command.includes("storage/framework/core/actions/src/dev/")||command.includes(actionDevPath))pids.add(pid)}if(pids.size===0)return;for(const pid of pids)try{process.kill(pid,"SIGTERM")}catch{}await new Promise((r)=>setTimeout(r,250));for(const pid of pids)try{process.kill(pid,"SIGKILL")}catch{}await new Promise((r)=>setTimeout(r,150))}async function listenerPids(port){try{const proc=Bun.spawn(["lsof","-nP",`-iTCP:${port}`,"-sTCP:LISTEN","-t"],{stdout:"pipe",stderr:"ignore"}),output=await new Response(proc.stdout).text();await proc.exited;return output.split(/\s+/).map((value)=>Number(value)).filter((pid)=>Number.isInteger(pid)&&pid>0)}catch{return[]}}async function commandForPid(pid){try{const proc=Bun.spawn(["ps","-p",String(pid),"-o","command="],{stdout:"pipe",stderr:"ignore"}),output=await new Response(proc.stdout).text();await proc.exited;return output.trim()}catch{return""}}function resolveCraftBinaryPath(){const home=process.env.HOME;return[process.env.CRAFT_BIN,home?`${home}/Code/Tools/craft/craft`:void 0,home?`${home}/Code/Tools/craft/bin/craft`:void 0,home?`${home}/Code/Tools/craft/packages/zig/zig-out/bin/craft`:void 0,home?`${home}/Documents/Projects/craft/packages/zig/zig-out/bin/craft`:void 0].filter((candidate)=>Boolean(candidate)).find((candidate)=>existsSync(candidate))}function resolveNativeAppIconPath(){return[projectPath("resources/assets/images/app-icon.png"),projectPath("resources/assets/images/icon.png"),projectPath("public/icon.png")].find((candidate)=>existsSync(candidate))}async function resolveNativeAppTitle(){try{const pkg=await Bun.file(projectPath("package.json")).json(),name=typeof pkg.productName==="string"?pkg.productName:pkg.name;if(typeof name==="string"&&name.length>0)return name.split(/[-_\s]+/).filter(Boolean).map((part)=>part.charAt(0).toUpperCase()+part.slice(1)).join(" ")}catch{}return"Stacks App"}const METHOD_COLORS={GET:cyan,POST:green,PUT:cyan,PATCH:cyan,DELETE:cyan,OPTIONS:dim};async function printRegisteredRoutes(apiPort){try{const ac=new AbortController,t=setTimeout(()=>ac.abort(),1500),res=await fetch(`http://127.0.0.1:${apiPort}/__routes`,{signal:ac.signal}).catch(()=>null);clearTimeout(t);if(!res||!res.ok)return;const routes=await res.json();if(!Array.isArray(routes)||routes.length===0)return;const sorted=[...routes].sort((a,b)=>a.path.localeCompare(b.path)||a.method.localeCompare(b.method)),longestPath=Math.max(...sorted.map((r)=>r.path.length));console.log(` ${dim("Registered routes")}
|
package/dist/commands/dns.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import process from"node:process";import{log,onUnknownSubcommand}from"@stacksjs/cli";import{config}from"@stacksjs/config";import{renderDnsConfig,resolveLiveRecords,syncDnsConfig}from"@stacksjs/dns";import{ExitCode}from"@stacksjs/types";import{loadProjectDnsConfig}from"../config";export function dns(buddy){const descriptions={dns:"Lists the DNS records for a domain",query:"Host name or IP address to query",type:"Type of the DNS record being queried (A, MX, NS\u2026)",nameserver:"Address of the nameserver to send packets to",class:"Network class of the DNS record being queried (IN, CH, HS)",udp:"Use the DNS protocol over UDP",tcp:"Use the DNS protocol over TCP",tls:"Use the DNS-over-TLS protocol",https:"Use the DNS-over-HTTPS protocol",short:"Short mode: display nothing but the first result",json:"Display the output as JSON",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("dns [domain]",descriptions.dns).option("-q, --query <query>",descriptions.query).option("-t, --type <type>",descriptions.type,{default:"A"}).option("-n, --nameserver <nameserver>",descriptions.nameserver).option("--class <class>",descriptions.class).option("-U, --udp",descriptions.udp).option("-T, --tcp",descriptions.tcp).option("-S, --tls",descriptions.tls).option("-H, --https",descriptions.https).option("-1, --short",descriptions.short,{default:!1}).option("-J, --json",descriptions.json,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{log.debug("Running `buddy dns [domain]` ...",options);const targetDomain=domain||config.app.url;let DnsClient,formatOutput;try{const dnsx=await import("@stacksjs/dnsx");DnsClient=dnsx.DnsClient;formatOutput=dnsx.formatOutput}catch(err){log.error("`buddy dns` needs the @stacksjs/dnsx runtime, but only the type declarations are currently published. Install a build with the JS runtime (or wait for the next dnsx release) and re-run.");log.debug(`[dns] import failure: ${err instanceof Error?err.message:String(err)}`);process.exit(ExitCode.FatalError)}try{const client=new DnsClient({domains:[targetDomain],type:options.type,nameserver:options.nameserver,class:options.class,udp:options.udp,tcp:options.tcp,tls:options.tls,https:options.https,short:options.short,json:options.json,verbose:options.verbose}),startTime=performance.now(),responses=await client.query(),duration=performance.now()-startTime,output=formatOutput(responses,{json:options.json??!1,short:options.short??!1,showDuration:duration,colors:{enabled:!0},rawSeconds:!1});console.log(output)}catch(error){log.error(`DNS query failed: ${error instanceof Error?error.message:String(error)}`);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});const bareDomain=(input)=>(input||config.app.url||"").replace(/^[a-z]+:\/\//i,"").replace(/[/:].*$/,"");buddy.command("dns:pull [domain]","Print a domain's live DNS records as a config/dns.ts block").option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{const target=bareDomain(domain);log.debug(`Running \`buddy dns:pull ${target}\` ...`,options);const records=await resolveLiveRecords(target);if(!records.length){log.error(`No DNS records resolved for ${target}.`);process.exit(ExitCode.FatalError)}console.log(renderDnsConfig(target,records));process.exit(ExitCode.Success)});buddy.command("dns:diff [domain]","Show which config/dns.ts records are missing from the live zone").option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{const target=bareDomain(domain);log.debug(`Running \`buddy dns:diff ${target}\` ...`,options);const dnsConfig=await loadProjectDnsConfig(config.dns),{plan,provider}=await syncDnsConfig(target,dnsConfig,{dryRun:!0});for(const item of plan.items){const detail=item.record.type==="TXT"||item.record.type==="MX"?` ${item.record.content}`:` \u2192 ${item.record.content}`,label=item.action==="create"?"+ create":item.action==="skip"?"- skip ":" keep ",why=item.action==="skip"?` (${item.reason})`:"";console.log(` ${label} ${item.record.type.padEnd(5)} ${item.record.name}${detail}${why}`)}const skipped=plan.skip.length?`, ${plan.skip.length} unpublishable`:"";console.log(`
|
|
1
|
+
import process from"node:process";import{log,onUnknownSubcommand}from"@stacksjs/cli";import{config}from"@stacksjs/config";import{renderDnsConfig,resolveLiveRecords,syncDnsConfig}from"@stacksjs/dns";import{ExitCode}from"@stacksjs/types";import{loadProjectDnsConfig}from"../config";export function dns(buddy){const descriptions={dns:"Lists the DNS records for a domain",query:"Host name or IP address to query",type:"Type of the DNS record being queried (A, MX, NS\u2026)",nameserver:"Address of the nameserver to send packets to",class:"Network class of the DNS record being queried (IN, CH, HS)",udp:"Use the DNS protocol over UDP",tcp:"Use the DNS protocol over TCP",tls:"Use the DNS-over-TLS protocol",https:"Use the DNS-over-HTTPS protocol",short:"Short mode: display nothing but the first result",json:"Display the output as JSON",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("dns [domain]",descriptions.dns).option("-q, --query <query>",descriptions.query).option("-t, --type <type>",descriptions.type,{default:"A"}).option("-n, --nameserver <nameserver>",descriptions.nameserver).option("--class <class>",descriptions.class).option("-U, --udp",descriptions.udp).option("-T, --tcp",descriptions.tcp).option("-S, --tls",descriptions.tls).option("-H, --https",descriptions.https).option("-1, --short",descriptions.short,{default:!1}).option("-J, --json",descriptions.json,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{log.debug("Running `buddy dns [domain]` ...",options);const targetDomain=domain||config.app.url;let DnsClient,formatOutput;try{const dnsx=await import("@stacksjs/dnsx");DnsClient=dnsx.DnsClient;formatOutput=dnsx.formatOutput}catch(err){log.error("`buddy dns` needs the @stacksjs/dnsx runtime, but only the type declarations are currently published. Install a build with the JS runtime (or wait for the next dnsx release) and re-run.");log.debug(`[dns] import failure: ${err instanceof Error?err.message:String(err)}`);process.exit(ExitCode.FatalError)}try{const client=new DnsClient({domains:[targetDomain],type:options.type,nameserver:options.nameserver,class:options.class,udp:options.udp,tcp:options.tcp,tls:options.tls,https:options.https,short:options.short,json:options.json,verbose:options.verbose}),startTime=performance.now(),responses=await client.query(),duration=performance.now()-startTime,output=formatOutput(responses,{json:options.json??!1,short:options.short??!1,showDuration:duration,colors:{enabled:!0},rawSeconds:!1});console.log(output)}catch(error){await log.error(`DNS query failed: ${error instanceof Error?error.message:String(error)}`);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});const bareDomain=(input)=>(input||config.app.url||"").replace(/^[a-z]+:\/\//i,"").replace(/[/:].*$/,"");buddy.command("dns:pull [domain]","Print a domain's live DNS records as a config/dns.ts block").option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{const target=bareDomain(domain);log.debug(`Running \`buddy dns:pull ${target}\` ...`,options);const records=await resolveLiveRecords(target);if(!records.length){await log.error(`No DNS records resolved for ${target}.`);process.exit(ExitCode.FatalError)}console.log(renderDnsConfig(target,records));process.exit(ExitCode.Success)});buddy.command("dns:diff [domain]","Show which config/dns.ts records are missing from the live zone").option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{const target=bareDomain(domain);log.debug(`Running \`buddy dns:diff ${target}\` ...`,options);const dnsConfig=await loadProjectDnsConfig(config.dns),{plan,provider}=await syncDnsConfig(target,dnsConfig,{dryRun:!0});for(const item of plan.items){const detail=item.record.type==="TXT"||item.record.type==="MX"?` ${item.record.content}`:` \u2192 ${item.record.content}`,label=item.action==="create"?"+ create":item.action==="skip"?"- skip ":" keep ",why=item.action==="skip"?` (${item.reason})`:"";console.log(` ${label} ${item.record.type.padEnd(5)} ${item.record.name}${detail}${why}`)}const skipped=plan.skip.length?`, ${plan.skip.length} unpublishable`:"";console.log(`
|
|
2
2
|
${plan.create.length} to create, ${plan.keep.length} already present${skipped} (${provider?`registrar: ${provider}`:"public DNS"})`);process.exit(ExitCode.Success)});buddy.command("dns:sync [domain]","Additively sync config/dns.ts to the registrar (creates missing records; never deletes or overwrites)").option("--dry-run","Show the plan without writing any records",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{const target=bareDomain(domain);log.debug(`Running \`buddy dns:sync ${target}\` ...`,options);const dnsConfig=await loadProjectDnsConfig(config.dns),result=await syncDnsConfig(target,dnsConfig,{dryRun:options.dryRun});if(!result.provider&&!options.dryRun){log.warn(`No DNS provider credentials found (e.g. PORKBUN_API_KEY / PORKBUN_SECRET_KEY) - nothing was synced. ${result.plan.create.length} record(s) would be created.`);process.exit(ExitCode.Success)}const failedNames=new Set(result.failures.map((failure)=>`${failure.record.type} ${failure.record.name}`));for(const record of result.plan.create){const verb=!result.applied?"would create":failedNames.has(`${record.type} ${record.name}`)?"FAILED ":"created";console.log(` ${verb} ${record.type.padEnd(5)} ${record.name} \u2192 ${record.content}`)}for(const failure of result.failures)console.log(` ${failure.record.type} ${failure.record.name}: ${failure.reason}`);for(const skipped of result.skipped)console.log(` skipped ${skipped.record.type.padEnd(5)} ${skipped.record.name}: ${skipped.reason}`);const verb=result.applied?"created":"to create",count=result.applied?result.created:result.plan.create.length,skippedNote=result.skipped.length?`, ${result.skipped.length} unpublishable`:"";console.log(`
|
|
3
3
|
dns:sync ${target}: ${count} ${verb}, ${result.kept} kept${result.failed?`, ${result.failed} failed`:""}${skippedNote}${result.provider?` (${result.provider})`:""}`);process.exit(result.failed>0?ExitCode.FatalError:ExitCode.Success)});onUnknownSubcommand(buddy,"dns")}
|
|
@@ -1,4 +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`)}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 - 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 - 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):
|
|
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 - 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){await 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 - 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){await log.error(`Failed to publish ${failures.length} of ${publishing.length} store(s):
|
|
3
3
|
${failures.join(`
|
|
4
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 - 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 - 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`)})}
|
package/dist/commands/http.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{log}from"@stacksjs/cli";import{config}from"@stacksjs/config";import{HttxClient}from"@stacksjs/httx";import{ExitCode}from"@stacksjs/types";export function http(buddy){const descriptions={http:"Send an HTTP request to a domain",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("http [domain]",descriptions.http).option("-p, --project [project]",descriptions.project,{default:!1}).option("-v, --verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{log.debug("Running `buddy http [domain]` ...",options);const url=domain||config.app.url;if(!url)throw Error("No domain configured. Pass a domain or set config.app.url.");const client=new HttxClient({verbose:options.verbose});log.info(`GET ${url}`);(await client.request(url.startsWith("http")?url:`https://${url}`,{method:"GET"})).match({ok:(response)=>{log.info(`${response.status} ${response.statusText} (${response.timings.duration.toFixed(0)}ms)`);console.log(typeof response.data==="string"?response.data:JSON.stringify(response.data,null,2));process.exit(ExitCode.Success)},err:(error)=>{
|
|
1
|
+
import process from"node:process";import{log}from"@stacksjs/cli";import{config}from"@stacksjs/config";import{HttxClient}from"@stacksjs/httx";import{ExitCode}from"@stacksjs/types";export function http(buddy){const descriptions={http:"Send an HTTP request to a domain",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("http [domain]",descriptions.http).option("-p, --project [project]",descriptions.project,{default:!1}).option("-v, --verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{log.debug("Running `buddy http [domain]` ...",options);const url=domain||config.app.url;if(!url)throw Error("No domain configured. Pass a domain or set config.app.url.");const client=new HttxClient({verbose:options.verbose});log.info(`GET ${url}`);(await client.request(url.startsWith("http")?url:`https://${url}`,{method:"GET"})).match({ok:(response)=>{log.info(`${response.status} ${response.statusText} (${response.timings.duration.toFixed(0)}ms)`);console.log(typeof response.data==="string"?response.data:JSON.stringify(response.data,null,2));process.exit(ExitCode.Success)},err:(error)=>{console.error(`Request failed: ${error.message}`);process.exit(ExitCode.FatalError)}})})}
|
package/dist/commands/install.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{log,onUnknownSubcommand,runCommand}from"@stacksjs/cli";import{path as p}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function install(buddy){const descriptions={install:"Install your dependencies",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("install",descriptions.install).option("-p, --project [project]",descriptions.project,{default:!1}).option("-v, --verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy install` ...",options);const result=await runCommand("bun install",{...options,cwd:p.projectPath()});if(resultFailed(result)){log.error("bun install failed");process.exit(ExitCode.FatalError)}});onUnknownSubcommand(buddy,"install")}
|
|
1
|
+
import process from"node:process";import{log,onUnknownSubcommand,runCommand}from"@stacksjs/cli";import{path as p}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function install(buddy){const descriptions={install:"Install your dependencies",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("install",descriptions.install).option("-p, --project [project]",descriptions.project,{default:!1}).option("-v, --verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy install` ...",options);const result=await runCommand("bun install",{...options,cwd:p.projectPath()});if(resultFailed(result)){await log.error("bun install failed");process.exit(ExitCode.FatalError)}});onUnknownSubcommand(buddy,"install")}
|
package/dist/commands/key.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function key(buddy){const descriptions={command:"Generate & set the application key.",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("key:generate",descriptions.command).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy key:generate` ...",options);await intro("buddy key:generate");const result=await runAction(Action.KeyGenerate,options);if(resultFailed(result)){log.error("Failed to set random application key.",result.error);process.exit(ExitCode.FatalError)}await outro("Random application key set.")});onUnknownSubcommand(buddy,"key")}
|
|
1
|
+
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function key(buddy){const descriptions={command:"Generate & set the application key.",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("key:generate",descriptions.command).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy key:generate` ...",options);await intro("buddy key:generate");const result=await runAction(Action.KeyGenerate,options);if(resultFailed(result)){await log.error("Failed to set random application key.",result.error);process.exit(ExitCode.FatalError)}await outro("Random application key set.")});onUnknownSubcommand(buddy,"key")}
|
package/dist/commands/libs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{relative}from"node:path";import process from"node:process";import{runAction}from"@stacksjs/actions";import{bold,dim,log,onUnknownSubcommand}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function libs(buddy){const descriptions={list:"List the packages this project releases out of resources/functions and resources/components",build:"Build every configured library package",publish:"Publish the built library packages through pantry",json:"Print the resolved packages as JSON",dryRun:"Report what would be published without uploading anything",verbose:"Enable verbose output"};buddy.command("libs",descriptions.list).alias("libs:list").alias("libraries").option("--json",descriptions.json,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy libs").action(async(options)=>{const{resolveLibraryPackages,LibraryConfigError}=await import("@stacksjs/actions"),{library}=await import("@stacksjs/config");try{const packages=await resolveLibraryPackages(library);if(options.json){console.log(JSON.stringify(packages.map((pkg)=>({name:pkg.name,kind:pkg.kind,dir:relative(process.cwd(),pkg.dir),private:pkg.private,runtime:pkg.runtime,sources:pkg.sources.map((source)=>relative(process.cwd(),source))})),null,2));return}if(!packages.length){log.info("No library packages are configured. Add one to `packages` in config/library.ts.");return}for(const pkg of packages){console.log(`${bold(pkg.name)} ${dim(`(${pkg.kind}${pkg.private?", private":""})`)}`);console.log(dim(` \u2192 ${relative(process.cwd(),pkg.dir)}`));for(const source of pkg.sources)console.log(dim(` \xB7 ${relative(process.cwd(),source)}`))}}catch(error){if(error instanceof LibraryConfigError){await log.exit(error.message,1);return}throw error}});buddy.command("libs:build",descriptions.build).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const result=await runAction(Action.BuildLibs,options);if(resultFailed(result)){log.error("Failed to build the library packages.",result.error);process.exit(ExitCode.FatalError)}});buddy.command("libs:publish",descriptions.publish).option("--dry-run",descriptions.dryRun,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy libs:publish --dry-run").action(async(options)=>{const result=await runAction(Action.LibraryPublish,{...options,verbose:!0});if(resultFailed(result)){log.error("Failed to publish the library packages.",result.error);process.exit(ExitCode.FatalError)}});onUnknownSubcommand(buddy,"libs")}
|
|
1
|
+
import{relative}from"node:path";import process from"node:process";import{runAction}from"@stacksjs/actions";import{bold,dim,log,onUnknownSubcommand}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function libs(buddy){const descriptions={list:"List the packages this project releases out of resources/functions and resources/components",build:"Build every configured library package",publish:"Publish the built library packages through pantry",json:"Print the resolved packages as JSON",dryRun:"Report what would be published without uploading anything",verbose:"Enable verbose output"};buddy.command("libs",descriptions.list).alias("libs:list").alias("libraries").option("--json",descriptions.json,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy libs").action(async(options)=>{const{resolveLibraryPackages,LibraryConfigError}=await import("@stacksjs/actions"),{library}=await import("@stacksjs/config");try{const packages=await resolveLibraryPackages(library);if(options.json){console.log(JSON.stringify(packages.map((pkg)=>({name:pkg.name,kind:pkg.kind,dir:relative(process.cwd(),pkg.dir),private:pkg.private,runtime:pkg.runtime,sources:pkg.sources.map((source)=>relative(process.cwd(),source))})),null,2));return}if(!packages.length){log.info("No library packages are configured. Add one to `packages` in config/library.ts.");return}for(const pkg of packages){console.log(`${bold(pkg.name)} ${dim(`(${pkg.kind}${pkg.private?", private":""})`)}`);console.log(dim(` \u2192 ${relative(process.cwd(),pkg.dir)}`));for(const source of pkg.sources)console.log(dim(` \xB7 ${relative(process.cwd(),source)}`))}}catch(error){if(error instanceof LibraryConfigError){await log.exit(error.message,1);return}throw error}});buddy.command("libs:build",descriptions.build).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const result=await runAction(Action.BuildLibs,options);if(resultFailed(result)){await log.error("Failed to build the library packages.",result.error);process.exit(ExitCode.FatalError)}});buddy.command("libs:publish",descriptions.publish).option("--dry-run",descriptions.dryRun,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy libs:publish --dry-run").action(async(options)=>{const result=await runAction(Action.LibraryPublish,{...options,verbose:!0});if(resultFailed(result)){await log.error("Failed to publish the library packages.",result.error);process.exit(ExitCode.FatalError)}});onUnknownSubcommand(buddy,"libs")}
|
package/dist/commands/link.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import{existsSync,lstatSync,readdirSync,realpathSync}from"node:fs";import fs from"node:fs";import{homedir}from"node:os";import{join,resolve}from"node:path";import process from"node:process";import{italic,log}from"@stacksjs/cli";import{ExitCode}from"@stacksjs/types";function recordPath(){return join(process.cwd(),"storage/framework/runtime/linked-core.json")}function readRecord(){try{return JSON.parse(fs.readFileSync(recordPath(),"utf-8"))}catch{return null}}function writeRecord(record){fs.mkdirSync(join(process.cwd(),"storage/framework/runtime"),{recursive:!0});fs.writeFileSync(recordPath(),`${JSON.stringify(record,null,2)}
|
|
2
|
-
`)}function resolveFrameworkPath(explicit){const candidates=[explicit,process.env.STACKS_FRAMEWORK_PATH,resolve(process.cwd(),"../stacks"),join(homedir(),"Code/stacks")].filter(Boolean);for(const candidate of candidates){const full=resolve(candidate);if(existsSync(join(full,"storage/framework/core")))return full}return null}function corePackages(framework){const coreDir=join(framework,"storage/framework/core"),found=new Map;for(const entry of readdirSync(coreDir,{withFileTypes:!0})){if(!entry.isDirectory())continue;const dir=join(coreDir,entry.name),manifest=join(dir,"package.json");if(!existsSync(manifest))continue;try{const{name}=JSON.parse(fs.readFileSync(manifest,"utf-8"));if(name?.startsWith("@stacksjs/"))found.set(name,dir)}catch{}}return found}function normalize(name){return`@stacksjs/${name.replace(/^@stacksjs\//,"").replace(/^core\//,"")}`}function isSymlink(path){try{return lstatSync(path).isSymbolicLink()}catch{return!1}}export function link(buddy){buddy.command("link:core [...packages]","Point this app's @stacksjs/* at a local framework checkout").option("--path <path>","Framework checkout to link against").option("--all","Link every core package the app has installed",{default:!1}).example("buddy link:core auth router").example("buddy link:core --all --path ../stacks").action(async(packages,options)=>{const framework=resolveFrameworkPath(options?.path);if(!framework){log.error("No framework checkout found.");log.info("Pass one with `--path <dir>` or set STACKS_FRAMEWORK_PATH.");process.exit(ExitCode.FatalError)}const available=corePackages(framework),modulesDir=join(process.cwd(),"node_modules"),wanted=(packages?.length??0)>0?packages.map(normalize):[...available.keys()].filter((name)=>existsSync(join(modulesDir,name))||options?.all);if(wanted.length===0){log.info("Nothing to link: no installed @stacksjs/* packages found in this project.");await log.flush();process.exit(ExitCode.Success)}const linked=[],unbuilt=[];for(const name of wanted){const source=available.get(name);if(!source){log.warn(`${name} is not in ${italic(framework)} - skipped.`);continue}const target=join(modulesDir,name);if(isSymlink(target)&&realpathSync(target)===realpathSync(source)){linked.push(name);continue}fs.rmSync(target,{recursive:!0,force:!0});fs.mkdirSync(join(target,".."),{recursive:!0});fs.symlinkSync(source,target,"dir");linked.push(name);if(!existsSync(join(source,"dist")))unbuilt.push(name)}writeRecord({framework,packages:linked});log.success(`Linked ${linked.length} package${linked.length===1?"":"s"} to ${italic(framework)}`);if(unbuilt.length>0){log.warn(`Not built yet: ${unbuilt.join(", ")}`);log.info("An app imports the build output, so each linked package needs `bun build.ts` in its directory.")}log.info("Undo with `buddy unlink:core`.");await log.flush();process.exit(ExitCode.Success)});buddy.command("unlink:core [...packages]","Go back to the installed @stacksjs/* packages").option("--all","Unlink everything that was linked",{default:!1}).example("buddy unlink:core").action(async(packages,_options)=>{const record=readRecord(),modulesDir=join(process.cwd(),"node_modules"),wanted=(packages?.length??0)>0?packages.map(normalize):record?.packages??readdirSync(join(modulesDir,"@stacksjs"),{withFileTypes:!0}).filter((entry)=>entry.isSymbolicLink()).map((entry)=>`@stacksjs/${entry.name}`);let removed=0;const unlinked=new Set;for(const name of wanted){const target=join(modulesDir,name);if(!isSymlink(target))continue;fs.rmSync(target,{force:!0});unlinked.add(name);removed++}const survivors=(record?.packages??[]).filter((name)=>!unlinked.has(name));if(survivors.length>0)writeRecord({framework:record.framework,packages:survivors});else fs.rmSync(recordPath(),{force:!0});if(removed===0){log.info("Nothing was linked.");await log.flush();process.exit(ExitCode.Success)}log.info(`Unlinked ${removed} package${removed===1?"":"s"}; reinstalling the published copies...`);if(await Bun.spawn(["bun","install","--force"],{cwd:process.cwd(),stdout:"inherit",stderr:"inherit"}).exited!==0){log.error("`bun install` failed. The links are gone; re-run the install once the failure is resolved.");process.exit(ExitCode.FatalError)}for(const name of survivors){const source=join(record.framework,"storage/framework/core",name.replace("@stacksjs/","")),target=join(modulesDir,name);if(!existsSync(source))continue;fs.rmSync(target,{recursive:!0,force:!0});fs.symlinkSync(source,target,"dir")}if(survivors.length>0)log.success(`Unlinked ${removed}; ${survivors.length} package${survivors.length===1?"":"s"} still linked.`);else log.success("This project is back on the published packages.");await log.flush();process.exit(ExitCode.Success)})}
|
|
2
|
+
`)}function resolveFrameworkPath(explicit){const candidates=[explicit,process.env.STACKS_FRAMEWORK_PATH,resolve(process.cwd(),"../stacks"),join(homedir(),"Code/stacks")].filter(Boolean);for(const candidate of candidates){const full=resolve(candidate);if(existsSync(join(full,"storage/framework/core")))return full}return null}function corePackages(framework){const coreDir=join(framework,"storage/framework/core"),found=new Map;for(const entry of readdirSync(coreDir,{withFileTypes:!0})){if(!entry.isDirectory())continue;const dir=join(coreDir,entry.name),manifest=join(dir,"package.json");if(!existsSync(manifest))continue;try{const{name}=JSON.parse(fs.readFileSync(manifest,"utf-8"));if(name?.startsWith("@stacksjs/"))found.set(name,dir)}catch{}}return found}function normalize(name){return`@stacksjs/${name.replace(/^@stacksjs\//,"").replace(/^core\//,"")}`}function isSymlink(path){try{return lstatSync(path).isSymbolicLink()}catch{return!1}}export function link(buddy){buddy.command("link:core [...packages]","Point this app's @stacksjs/* at a local framework checkout").option("--path <path>","Framework checkout to link against").option("--all","Link every core package the app has installed",{default:!1}).example("buddy link:core auth router").example("buddy link:core --all --path ../stacks").action(async(packages,options)=>{const framework=resolveFrameworkPath(options?.path);if(!framework){await log.error("No framework checkout found.");log.info("Pass one with `--path <dir>` or set STACKS_FRAMEWORK_PATH.");process.exit(ExitCode.FatalError)}const available=corePackages(framework),modulesDir=join(process.cwd(),"node_modules"),wanted=(packages?.length??0)>0?packages.map(normalize):[...available.keys()].filter((name)=>existsSync(join(modulesDir,name))||options?.all);if(wanted.length===0){log.info("Nothing to link: no installed @stacksjs/* packages found in this project.");await log.flush();process.exit(ExitCode.Success)}const linked=[],unbuilt=[];for(const name of wanted){const source=available.get(name);if(!source){log.warn(`${name} is not in ${italic(framework)} - skipped.`);continue}const target=join(modulesDir,name);if(isSymlink(target)&&realpathSync(target)===realpathSync(source)){linked.push(name);continue}fs.rmSync(target,{recursive:!0,force:!0});fs.mkdirSync(join(target,".."),{recursive:!0});fs.symlinkSync(source,target,"dir");linked.push(name);if(!existsSync(join(source,"dist")))unbuilt.push(name)}writeRecord({framework,packages:linked});log.success(`Linked ${linked.length} package${linked.length===1?"":"s"} to ${italic(framework)}`);if(unbuilt.length>0){log.warn(`Not built yet: ${unbuilt.join(", ")}`);log.info("An app imports the build output, so each linked package needs `bun build.ts` in its directory.")}log.info("Undo with `buddy unlink:core`.");await log.flush();process.exit(ExitCode.Success)});buddy.command("unlink:core [...packages]","Go back to the installed @stacksjs/* packages").option("--all","Unlink everything that was linked",{default:!1}).example("buddy unlink:core").action(async(packages,_options)=>{const record=readRecord(),modulesDir=join(process.cwd(),"node_modules"),wanted=(packages?.length??0)>0?packages.map(normalize):record?.packages??readdirSync(join(modulesDir,"@stacksjs"),{withFileTypes:!0}).filter((entry)=>entry.isSymbolicLink()).map((entry)=>`@stacksjs/${entry.name}`);let removed=0;const unlinked=new Set;for(const name of wanted){const target=join(modulesDir,name);if(!isSymlink(target))continue;fs.rmSync(target,{force:!0});unlinked.add(name);removed++}const survivors=(record?.packages??[]).filter((name)=>!unlinked.has(name));if(survivors.length>0)writeRecord({framework:record.framework,packages:survivors});else fs.rmSync(recordPath(),{force:!0});if(removed===0){log.info("Nothing was linked.");await log.flush();process.exit(ExitCode.Success)}log.info(`Unlinked ${removed} package${removed===1?"":"s"}; reinstalling the published copies...`);if(await Bun.spawn(["bun","install","--force"],{cwd:process.cwd(),stdout:"inherit",stderr:"inherit"}).exited!==0){await log.error("`bun install` failed. The links are gone; re-run the install once the failure is resolved.");process.exit(ExitCode.FatalError)}for(const name of survivors){const source=join(record.framework,"storage/framework/core",name.replace("@stacksjs/","")),target=join(modulesDir,name);if(!existsSync(source))continue;fs.rmSync(target,{recursive:!0,force:!0});fs.symlinkSync(source,target,"dir")}if(survivors.length>0)log.success(`Unlinked ${removed}; ${survivors.length} package${survivors.length===1?"":"s"} still linked.`);else log.success("This project is back on the published packages.");await log.flush();process.exit(ExitCode.Success)})}
|
package/dist/commands/lint.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import process from"node:process";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{ExitCode}from"@stacksjs/types";async function runStyleAction(entry,label,options){const actions=await import("@stacksjs/actions"),{ok}=await actions[entry](options);if(!ok){log.error(`${label} reported failure`);process.exit(ExitCode.FatalError)}}async function runStxChecks(startTime){const{runStxLint}=await import("@stacksjs/actions"),report=await runStxLint(),out=[""];for(const r of report.results)if(r.status==="fail"){out.push(` FAIL ${r.label}`);out.push(` ${r.count} found, baseline ${r.baseline}${r.why?` (${r.why})`:""}`);for(const line of r.detail.slice(0,8))out.push(` ${line}`)}else if(r.status==="loosened")out.push(` DROP ${r.label}: ${r.count} < baseline ${r.baseline} - lower it in config/lint.ts`);else out.push(` ok ${r.label}${r.baseline>0?` (${r.count}, held)`:""}`);if(report.distMissing)out.push(""," note: no build output found - the dist checks did not run. Run `./buddy build` first.");if(report.loosened>0){out.push(""," Current counts, for config/lint.ts:");for(const[id,count]of Object.entries(report.counts).sort(([a],[b])=>a.localeCompare(b)))out.push(` '${id}': ${count},`)}console.log(out.join(`
|
|
2
|
-
`));if(report.failed>0){log.error(`${report.failed} stx check(s) failed.`);await outro("stx checks failed",{startTime,useSeconds:!0});process.exit(ExitCode.FatalError)}if(report.loosened>0){log.warn(`No regressions, but ${report.loosened} baseline(s) are now stale.`);await outro("stx baselines stale",{startTime,useSeconds:!0});process.exit(ExitCode.FatalError)}await outro("All stx checks pass",{startTime,useSeconds:!0})}export function lint(buddy){const descriptions={lint:"Automagically lints your project codebase",lintFix:"Automagically fixes all lint errors",format:"Format your project codebase",formatCheck:"Check formatting without making changes",project:"Target a specific project",stx:"Run the stx conformance checks instead of code style",verbose:"Enable verbose output"};buddy.command("lint",descriptions.lint).option("-f, --fix",descriptions.lintFix,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--stx",descriptions.stx,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy lint` ...",options);const startTime=await intro("buddy lint");if(options.stx){await runStxChecks(startTime);return}await runStyleAction(options.fix?"lintFix":"lintProject","lint");await outro("Linted your project",{startTime,useSeconds:!0})});buddy.command("lint:fix",descriptions.lintFix).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy lint:fix` ...",options);const startTime=await intro("buddy lint:fix");log.info("Fixing lint errors...");await runStyleAction("lintFix","lint:fix");await outro("Fixed lint errors",{startTime,useSeconds:!0})});buddy.command("format",descriptions.format).option("-w, --write","Write changes to files",{default:!1}).option("-c, --check",descriptions.formatCheck,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy format` ...",options);const startTime=await intro("buddy format");await runStyleAction("formatProject","format",options.check?{check:!0}:{write:!0});await outro("Formatted your project",{startTime,useSeconds:!0})});buddy.command("format:check",descriptions.formatCheck).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy format:check` ...",options);const startTime=await intro("buddy format:check");await runStyleAction("formatProject","format:check",{check:!0});await outro("Format check complete",{startTime,useSeconds:!0})});onUnknownSubcommand(buddy,"lint")}
|
|
1
|
+
import process from"node:process";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{ExitCode}from"@stacksjs/types";async function runStyleAction(entry,label,options){const actions=await import("@stacksjs/actions"),{ok}=await actions[entry](options);if(!ok){await log.error(`${label} reported failure`);process.exit(ExitCode.FatalError)}}async function runStxChecks(startTime){const{runStxLint}=await import("@stacksjs/actions"),report=await runStxLint(),out=[""];for(const r of report.results)if(r.status==="fail"){out.push(` FAIL ${r.label}`);out.push(` ${r.count} found, baseline ${r.baseline}${r.why?` (${r.why})`:""}`);for(const line of r.detail.slice(0,8))out.push(` ${line}`)}else if(r.status==="loosened")out.push(` DROP ${r.label}: ${r.count} < baseline ${r.baseline} - lower it in config/lint.ts`);else out.push(` ok ${r.label}${r.baseline>0?` (${r.count}, held)`:""}`);if(report.distMissing)out.push(""," note: no build output found - the dist checks did not run. Run `./buddy build` first.");if(report.loosened>0){out.push(""," Current counts, for config/lint.ts:");for(const[id,count]of Object.entries(report.counts).sort(([a],[b])=>a.localeCompare(b)))out.push(` '${id}': ${count},`)}console.log(out.join(`
|
|
2
|
+
`));if(report.failed>0){await log.error(`${report.failed} stx check(s) failed.`);await outro("stx checks failed",{startTime,useSeconds:!0});process.exit(ExitCode.FatalError)}if(report.loosened>0){log.warn(`No regressions, but ${report.loosened} baseline(s) are now stale.`);await outro("stx baselines stale",{startTime,useSeconds:!0});process.exit(ExitCode.FatalError)}await outro("All stx checks pass",{startTime,useSeconds:!0})}export function lint(buddy){const descriptions={lint:"Automagically lints your project codebase",lintFix:"Automagically fixes all lint errors",format:"Format your project codebase",formatCheck:"Check formatting without making changes",project:"Target a specific project",stx:"Run the stx conformance checks instead of code style",verbose:"Enable verbose output"};buddy.command("lint",descriptions.lint).option("-f, --fix",descriptions.lintFix,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--stx",descriptions.stx,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy lint` ...",options);const startTime=await intro("buddy lint");if(options.stx){await runStxChecks(startTime);return}await runStyleAction(options.fix?"lintFix":"lintProject","lint");await outro("Linted your project",{startTime,useSeconds:!0})});buddy.command("lint:fix",descriptions.lintFix).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy lint:fix` ...",options);const startTime=await intro("buddy lint:fix");log.info("Fixing lint errors...");await runStyleAction("lintFix","lint:fix");await outro("Fixed lint errors",{startTime,useSeconds:!0})});buddy.command("format",descriptions.format).option("-w, --write","Write changes to files",{default:!1}).option("-c, --check",descriptions.formatCheck,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy format` ...",options);const startTime=await intro("buddy format");await runStyleAction("formatProject","format",options.check?{check:!0}:{write:!0});await outro("Formatted your project",{startTime,useSeconds:!0})});buddy.command("format:check",descriptions.formatCheck).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy format:check` ...",options);const startTime=await intro("buddy format:check");await runStyleAction("formatProject","format:check",{check:!0});await outro("Format check complete",{startTime,useSeconds:!0})});onUnknownSubcommand(buddy,"lint")}
|