@stacksjs/buddy 0.75.12 → 0.75.14
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/commands/doctor.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import{lstatSync,readlinkSync}from"node:fs";import{resolve}from"node:path";import process from"node:process";import{bold,dim,green,intro,log,onUnknownSubcommand,red,yellow}from"@stacksjs/cli";import{feature}from"@stacksjs/config";import{inspectDefaultsProvenance}from"@stacksjs/path";import{storage}from"@stacksjs/storage";import{isSupportedBunVersion,isVersionGreaterThanOrEqual,minimumBunVersion}from"@stacksjs/utils";import{FEATURE_NAMES,featurePathsPresent}from"./features";const minimumSqliteVersion="3.47.2";class ProbeWarning extends Error{}async function probe(checks,name,fn,timeoutMs=2000){const start=Date.now(),ac=new AbortController,timer=setTimeout(()=>ac.abort(),timeoutMs);try{const message=await Promise.race([fn(),new Promise((_,rej)=>ac.signal.addEventListener("abort",()=>rej(Error(`timed out (>${Math.round(timeoutMs/1000)}s)`))))]);checks.push({name,status:"pass",message:`${message} (${Date.now()-start}ms)`})}catch(err){checks.push({name,status:err instanceof ProbeWarning?"warn":"fail",message:err instanceof Error?err.message:String(err)})}finally{clearTimeout(timer)}}export function doctor(buddy){buddy.command("doctor","Run health checks on your Stacks installation").option("--no-fail","Print results but always exit 0 (CI ramp-up)").action(async(options)=>{log.debug("Running `buddy doctor` ...");await intro("buddy doctor");const checks=[],modulesPath=resolve(process.cwd(),"node_modules");try{if(lstatSync(modulesPath).isSymbolicLink()){const target=resolve(process.cwd(),readlinkSync(modulesPath)),insideProject=target.startsWith(`${resolve(process.cwd())}/`);checks.push({name:"Dependency tree",status:insideProject?"warn":"fail",message:insideProject?`node_modules is a symlink to ${target}`:`node_modules is a symlink to ${target}, which belongs to another project - installs here mutate it, and what runs locally is not what the lockfile resolves. Remove the link and run \`bun install\`.`})}else checks.push({name:"Dependency tree",status:"pass",message:"node_modules is this project's own"})}catch{checks.push({name:"Dependency tree",status:"warn",message:"node_modules is missing - run `bun install`"})}await probe(checks,"Framework defaults",async()=>{const{measureDefaultsDrift,summarizeStructureChanges}=await import("@stacksjs/actions"),root=resolve(process.cwd()),skew=inspectDefaultsProvenance(root);if(skew.status==="not-applicable")return"Not a package-managed project (nothing to compare)";const drift=measureDefaultsDrift(root)??[],synced=skew.syncedAt?`, synced ${skew.syncedAt.slice(0,10)}`:"";if(drift.length===0)return`Vendored tree matches @stacksjs/defaults ${skew.installed}${synced}`;const counts=summarizeStructureChanges(drift);if(skew.status==="stale")throw new ProbeWarning(`storage/framework/defaults is from ${skew.vendored} while @stacksjs/defaults ${skew.installed} is installed (${counts}). Boot resolves the package, so the tree on disk is not what runs. Run \`buddy upgrade\` to sync it.`);throw new ProbeWarning(`storage/framework/defaults differs from the installed @stacksjs/defaults ${skew.installed} (${counts}), and carries no record of what it was synced from. The app runs the vendored copy. Run \`buddy upgrade\` to sync it.`)},8000);const bunVersion=process.versions.bun;if(bunVersion&&isSupportedBunVersion(bunVersion))checks.push({name:"Bun Runtime",status:"pass",message:`v${bunVersion}`});else if(bunVersion)checks.push({name:"Bun Runtime",status:"fail",message:`v${bunVersion} (requires v${minimumBunVersion} or later, run: bun upgrade)`});else checks.push({name:"Bun Runtime",status:"fail",message:"Not found"});try{const pinnedBun=(await storage.readPackageJson("./package.json")).engines?.bun;if(pinnedBun&&bunVersion&&bunVersion!==pinnedBun)checks.push({name:"Pinned Bun",status:"warn",message:`v${bunVersion} is not the pinned v${pinnedBun} (engines.bun). Installing rewrites bun.lock - use \`./pantry/.bin/bun install\`, the pinned toolchain this checkout already carries, and \`git checkout -- bun.lock\` if it already changed.`});else if(pinnedBun&&bunVersion)checks.push({name:"Pinned Bun",status:"pass",message:`v${bunVersion} matches engines.bun`})}catch{}const nodeVersion=process.versions.node;if(nodeVersion)if(Number.parseInt(nodeVersion.split(".")[0]||"0",10)>=18)checks.push({name:"Node.js",status:"pass",message:`v${nodeVersion}`});else checks.push({name:"Node.js",status:"warn",message:`v${nodeVersion} (v18+ recommended)`});try{const pkg=await storage.readPackageJson("./package.json");if(pkg.name)checks.push({name:"package.json",status:"pass",message:`Found: ${pkg.name}`})}catch{checks.push({name:"package.json",status:"fail",message:"Not found in current directory"})}try{await storage.readTextFile(".env");checks.push({name:".env file",status:"pass",message:"Found"})}catch{checks.push({name:".env file",status:"warn",message:"Not found (optional)"})}const appKey=process.env.APP_KEY;if(appKey&&appKey.length>0)checks.push({name:"APP_KEY",status:appKey.length>=32?"pass":"warn",message:appKey.length>=32?"Set (\u226532 chars)":`Set but short (${appKey.length} chars; \u226532 recommended)`});else checks.push({name:"APP_KEY",status:"fail",message:"Not set - features that depend on it (encrypted columns, signed URLs, env decryption) will refuse to run. Run `buddy key:generate`."});await probe(checks,"SQLite Engine",async()=>{const{Database}=await import("bun:sqlite"),memory=new Database(":memory:");let version;try{const row=memory.query("SELECT sqlite_version() AS version").get();if(typeof row?.version==="string")version=row.version}finally{memory.close()}if(!version)throw new ProbeWarning("could not read sqlite_version() (skipped)");if(!isVersionGreaterThanOrEqual(version,minimumSqliteVersion))throw Error(`v${version} is below the required v${minimumSqliteVersion} (system.sqlite in package.json); upgrade Bun for a newer bundled SQLite`);return`v${version}`});await probe(checks,"Database",async()=>{const{db}=await import("@stacksjs/database"),unsafe=db.unsafe;if(typeof unsafe!=="function")throw new ProbeWarning("driver exposes no raw query method (skipped)");const statement=unsafe("SELECT 1");if(!statement||typeof statement.then!=="function"&&typeof statement.execute!=="function")throw new ProbeWarning("driver returned a non-executable statement (skipped)");const result=typeof statement.execute==="function"?await statement.execute():await statement;if(result===void 0||result===null)throw Error("probe query returned no result");if(Array.isArray(result)&&result.length===0)throw Error("probe query returned no rows");return"Reachable"});await probe(checks,"Database FKs",async()=>{const{auditForeignKeys}=await import("@stacksjs/database"),result=await auditForeignKeys();if(result.declared.length===0)return"No belongsTo declarations";const skipped=result.absentTable.length>0?`, ${result.absentTable.length} on tables not migrated`:"";if(result.missing.length===0)return`${result.declared.length} declared FKs all present${skipped}`;const sample=result.missing.slice(0,5).map((fk)=>`${fk.fromTable}.${fk.fromColumn} \u2192 ${fk.toTable}.${fk.toColumn}`).join(", "),more=result.missing.length>5?` (+${result.missing.length-5} more)`:"";throw Error(`${result.missing.length}/${result.declared.length} declared FKs missing from live schema: ${sample}${more}${skipped}. Run \`buddy migrate:fresh\` (will reset data) or \`buddy migrate\` against a clean DB.`)});await probe(checks,"Unique indexes",async()=>{const{auditUniqueIndexes}=await import("@stacksjs/database"),result=await auditUniqueIndexes();if(!result.supported)return"Dialect not audited (skipped)";const skipped=result.skippedTables.length>0?`, ${result.skippedTables.length} tables skipped (not migrated)`:"";if(result.missing.length===0)return`${result.declared.length} declared unique constraints all indexed${skipped}`;const sample=result.missing.slice(0,5).map((u)=>`${u.table}.${u.columns.join("+")}`).join(", "),more=result.missing.length>5?` (+${result.missing.length-5} more)`:"",first=result.missing[0];if(!first)throw Error("Unique-index audit reported missing entries without details");const example=`CREATE UNIQUE INDEX IF NOT EXISTS "${first.table}_${first.columns.join("_")}_unique" ON "${first.table}" ("${first.columns.join('", "')}")`;throw Error(`${result.missing.length}/${result.declared.length} declared unique constraints have no UNIQUE index: ${sample}${more}. Run \`buddy migrate\` (re-queues missing unique-index migrations, #1952) - dedupe duplicate rows first or migrate hard-fails; if no migration file exists run \`buddy generate:migrations\`, or create manually: ${example}`)},1e4);await probe(checks,"Migration ledger",async()=>{const{auditMigrationLedger}=await import("@stacksjs/database"),result=await auditMigrationLedger();if(!result.supported)return"Dialect not audited (skipped)";if(result.entries.length===0)return"No migration files";const{counts,orphans}=result;if(!result.drift)return`${result.entries.length} migrations, ledger consistent with the schema`;const parts=[];if(counts.stranded>0)parts.push(`${counts.stranded} applied but unrecorded (would re-run)`);if(counts.partial>0)parts.push(`${counts.partial} half-applied`);if(counts.reverted>0)parts.push(`${counts.reverted} recorded but missing from the schema`);if(orphans.length>0)parts.push(`${orphans.length} ledger row(s) with no file on disk`);const needsReapply=counts.reverted+counts.partial,repair=needsReapply>0?`\`buddy migrate:status --reconcile\` cannot repair ${needsReapply} of these - it never runs migration SQL, so it skips anything the schema is missing. Those need re-applying: \`buddy migrate:fresh\` (rebuilds from the corpus, RESETS DATA), or restore the database and re-run the missing files by hand.`:"Repair with `buddy migrate:status --reconcile`.";throw Error(`Migration ledger has drifted: ${parts.join(", ")}. Inspect with \`buddy migrate:status\`. ${repair}`)},1e4);await probe(checks,"FK orphans",async()=>{const{findFkOrphans}=await import("@stacksjs/database"),result=await findFkOrphans();if(!result.supported)return"Dialect not audited (skipped)";if(result.total===0)return"No orphan rows (PRAGMA foreign_key_check clean)";const sample=result.orphans.slice(0,5).map((o)=>`${o.table}.${o.column} \u2192 ${o.parent} (${o.count})`).join(", "),more=result.orphans.length>5?` (+${result.orphans.length-5} more)`:"",first=result.orphans[0];if(!first)throw Error("Foreign-key audit reported orphan rows without details");throw Error(`${result.total} orphan rows violate FKs: ${sample}${more}. Legacy rows written under foreign_keys=OFF (#1951). Review and clean manually, e.g. DELETE FROM ${first.table} WHERE ${first.column} IS NOT NULL AND ${first.column} NOT IN (SELECT id FROM ${first.parent}) - doctor never deletes data.`)},1e4);await probe(checks,"Cache",async()=>{const{cache}=await import("@stacksjs/cache"),k=`__doctor_${Date.now()}`;await cache.set(k,1,5);const v=await cache.get(k);await cache.del(k);if(v!==1)throw Error("round-trip failed");return"Round-trip ok"});await probe(checks,"Queue driver",async()=>{const{config}=await import("@stacksjs/config");return`Driver: ${config.queue?.default??"sync"}`});await probe(checks,"Mail driver",async()=>{const{config}=await import("@stacksjs/config");return`Driver: ${config.email?.default??"log"}`});{const{ensureRuntimeDirectories,projectPath}=await import("@stacksjs/path"),leftover=ensureRuntimeDirectories().filter((entry)=>!entry.cleared);checks.push(leftover.length===0?{name:"Runtime directories",status:"pass",message:"All under storage/"}:{name:"Runtime directories",status:"warn",message:`Still in the project root: ${leftover.map((entry)=>entry.legacy.slice(projectPath().length+1)).join(", ")} - remove them so state stays under storage/`})}const hasAwsCreds=Boolean(process.env.AWS_ACCESS_KEY_ID&&process.env.AWS_SECRET_ACCESS_KEY),hasAwsRole=Boolean(process.env.AWS_PROFILE)||Boolean(process.env.AWS_ROLE_ARN);if(hasAwsCreds||hasAwsRole)checks.push({name:"AWS credentials",status:"pass",message:hasAwsCreds?"Static keys in env":"IAM role configured"});else checks.push({name:"AWS credentials",status:"warn",message:"Not configured (cloud / SES / S3 commands will fail)"});await probe(checks,"Database backups",async()=>{const fs=await import("node:fs"),cloudConfig=resolve(process.cwd(),"config/cloud.ts");if(!fs.existsSync(cloudConfig))return"No config/cloud.ts (skipped)";const{findUnbackedManagedServices,hasOffsiteBackupDestination,unbackedDataMessage}=await import("../unbacked-data");let tsCloud;try{tsCloud=(await import(cloudConfig)).tsCloud}catch{throw new ProbeWarning("could not read config/cloud.ts (skipped)")}const unbacked=findUnbackedManagedServices(tsCloud,await hasOffsiteBackupDestination());if(unbacked.length===0)return"No unbacked managed data services";throw new ProbeWarning(unbackedDataMessage(unbacked))});await probe(checks,".env decryption",async()=>{const fs=await import("node:fs");if(!fs.existsSync(".env"))return"No .env (skipped)";const content=fs.readFileSync(".env","utf8");if(!/(?:^|=)(?:enc|encrypted):/m.test(content))return"No encrypted values";const privateKey=process.env.DOTENV_PRIVATE_KEY;if(!privateKey)throw Error("Encrypted values present but DOTENV_PRIVATE_KEY is unset");const{parse}=await import("@stacksjs/env"),{errors}=parse(content,{privateKey});if(errors.length>0)throw Error(errors.join("; "));return"All encrypted values decrypt cleanly"});try{const{filesystems}=await import("@stacksjs/config"),driver=filesystems.driver??"local";if(driver==="s3"){const bucket=process.env.S3_BUCKET??filesystems.s3?.bucket,accessKeyId=process.env.AWS_ACCESS_KEY_ID,secretAccessKey=process.env.AWS_SECRET_ACCESS_KEY,missing=[];if(!bucket)missing.push("S3_BUCKET");if(!accessKeyId)missing.push("AWS_ACCESS_KEY_ID");if(!secretAccessKey)missing.push("AWS_SECRET_ACCESS_KEY");if(missing.length>0)checks.push({name:"Storage credentials",status:"warn",message:`Default disk is 's3' but missing: ${missing.join(", ")}. Uploads will throw on first use.`});else checks.push({name:"Storage credentials",status:"pass",message:"s3 default disk has bucket + AWS credentials"})}else checks.push({name:"Storage credentials",status:"pass",message:`Default disk is '${driver}' (no remote credentials required)`})}catch(err){checks.push({name:"Storage credentials",status:"warn",message:`Could not audit storage config: ${err instanceof Error?err.message:String(err)}`})}try{if(process.platform!=="darwin")checks.push({name:"Dev domains (rpx)",status:"pass",message:"Not macOS (skipped)"});else{const fs=await import("node:fs"),os=await import("node:os"),path=await import("node:path"),isAlive=(pid)=>{try{process.kill(pid,0);return!0}catch(err){return err.code==="EPERM"}},registryDir=path.join(os.homedir(),".stacks","rpx","registry.d"),registered=new Set,deadRegistryFiles=[];if(fs.existsSync(registryDir))for(const file of fs.readdirSync(registryDir)){if(!file.endsWith(".json"))continue;try{const entry=JSON.parse(fs.readFileSync(path.join(registryDir,file),"utf8"));if(entry.to&&(entry.pid===void 0||isAlive(entry.pid)))registered.add(entry.to.toLowerCase());if(typeof entry.pid==="number"&&!isAlive(entry.pid))deadRegistryFiles.push(file)}catch{}}const staleHosts=new Set,staleResolvers=[],hostsPath="/etc/hosts",hostsLines=fs.existsSync(hostsPath)?fs.readFileSync(hostsPath,"utf8").split(`
|
|
1
|
+
import{lstatSync,readlinkSync}from"node:fs";import{resolve}from"node:path";import process from"node:process";import{bold,dim,green,intro,log,onUnknownSubcommand,red,yellow}from"@stacksjs/cli";import{feature}from"@stacksjs/config";import{inspectDefaultsProvenance}from"@stacksjs/path";import{storage}from"@stacksjs/storage";import{isSupportedBunVersion,isVersionGreaterThanOrEqual,minimumBunVersion}from"@stacksjs/utils";import{FEATURE_NAMES,featurePathsPresent}from"./features";const minimumSqliteVersion="3.47.2";class ProbeWarning extends Error{}async function probe(checks,name,fn,timeoutMs=2000){const start=Date.now(),ac=new AbortController,timer=setTimeout(()=>ac.abort(),timeoutMs);try{const message=await Promise.race([fn(),new Promise((_,rej)=>ac.signal.addEventListener("abort",()=>rej(Error(`timed out (>${Math.round(timeoutMs/1000)}s)`))))]);checks.push({name,status:"pass",message:`${message} (${Date.now()-start}ms)`})}catch(err){checks.push({name,status:err instanceof ProbeWarning?"warn":"fail",message:err instanceof Error?err.message:String(err)})}finally{clearTimeout(timer)}}export function doctor(buddy){buddy.command("doctor","Run health checks on your Stacks installation").option("--no-fail","Print results but always exit 0 (CI ramp-up)").action(async(options)=>{log.debug("Running `buddy doctor` ...");await intro("buddy doctor");const checks=[],modulesPath=resolve(process.cwd(),"node_modules");try{if(lstatSync(modulesPath).isSymbolicLink()){const target=resolve(process.cwd(),readlinkSync(modulesPath)),insideProject=target.startsWith(`${resolve(process.cwd())}/`);checks.push({name:"Dependency tree",status:insideProject?"warn":"fail",message:insideProject?`node_modules is a symlink to ${target}`:`node_modules is a symlink to ${target}, which belongs to another project - installs here mutate it, and what runs locally is not what the lockfile resolves. Remove the link and run \`bun install\`.`})}else checks.push({name:"Dependency tree",status:"pass",message:"node_modules is this project's own"})}catch{checks.push({name:"Dependency tree",status:"warn",message:"node_modules is missing - run `bun install`"})}await probe(checks,"Framework defaults",async()=>{const{measureDefaultsDrift,summarizeStructureChanges}=await import("@stacksjs/actions"),root=resolve(process.cwd()),skew=inspectDefaultsProvenance(root);if(skew.status==="not-applicable")return"Not a package-managed project (nothing to compare)";const drift=measureDefaultsDrift(root)??[],synced=skew.syncedAt?`, synced ${skew.syncedAt.slice(0,10)}`:"";if(drift.length===0)return`Vendored tree matches @stacksjs/defaults ${skew.installed}${synced}`;const counts=summarizeStructureChanges(drift);if(skew.status==="stale")throw new ProbeWarning(`storage/framework/defaults is from ${skew.vendored} while @stacksjs/defaults ${skew.installed} is installed (${counts}). Boot resolves the package, so the tree on disk is not what runs. Run \`buddy upgrade\` to sync it.`);throw new ProbeWarning(`storage/framework/defaults differs from the installed @stacksjs/defaults ${skew.installed} (${counts}), and carries no record of what it was synced from. The app runs the vendored copy. Run \`buddy upgrade\` to sync it.`)},8000);await probe(checks,"Route actions",async()=>{const{describeDanglingRouteActions,findDanglingRouteActions}=await import("@stacksjs/actions"),dangling=findDanglingRouteActions(resolve(process.cwd()));if(dangling.length===0)return"Every action a route names exists";throw new ProbeWarning(`${describeDanglingRouteActions(dangling).split(`
|
|
2
|
+
`).join("; ")}. Point each route at an action that exists, or delete it. \`buddy upgrade\` removes the ones the scaffold wrote.`)});const bunVersion=process.versions.bun;if(bunVersion&&isSupportedBunVersion(bunVersion))checks.push({name:"Bun Runtime",status:"pass",message:`v${bunVersion}`});else if(bunVersion)checks.push({name:"Bun Runtime",status:"fail",message:`v${bunVersion} (requires v${minimumBunVersion} or later, run: bun upgrade)`});else checks.push({name:"Bun Runtime",status:"fail",message:"Not found"});try{const pinnedBun=(await storage.readPackageJson("./package.json")).engines?.bun;if(pinnedBun&&bunVersion&&bunVersion!==pinnedBun)checks.push({name:"Pinned Bun",status:"warn",message:`v${bunVersion} is not the pinned v${pinnedBun} (engines.bun). Installing rewrites bun.lock - use \`./pantry/.bin/bun install\`, the pinned toolchain this checkout already carries, and \`git checkout -- bun.lock\` if it already changed.`});else if(pinnedBun&&bunVersion)checks.push({name:"Pinned Bun",status:"pass",message:`v${bunVersion} matches engines.bun`})}catch{}const nodeVersion=process.versions.node;if(nodeVersion)if(Number.parseInt(nodeVersion.split(".")[0]||"0",10)>=18)checks.push({name:"Node.js",status:"pass",message:`v${nodeVersion}`});else checks.push({name:"Node.js",status:"warn",message:`v${nodeVersion} (v18+ recommended)`});try{const pkg=await storage.readPackageJson("./package.json");if(pkg.name)checks.push({name:"package.json",status:"pass",message:`Found: ${pkg.name}`})}catch{checks.push({name:"package.json",status:"fail",message:"Not found in current directory"})}try{await storage.readTextFile(".env");checks.push({name:".env file",status:"pass",message:"Found"})}catch{checks.push({name:".env file",status:"warn",message:"Not found (optional)"})}const appKey=process.env.APP_KEY;if(appKey&&appKey.length>0)checks.push({name:"APP_KEY",status:appKey.length>=32?"pass":"warn",message:appKey.length>=32?"Set (\u226532 chars)":`Set but short (${appKey.length} chars; \u226532 recommended)`});else checks.push({name:"APP_KEY",status:"fail",message:"Not set - features that depend on it (encrypted columns, signed URLs, env decryption) will refuse to run. Run `buddy key:generate`."});await probe(checks,"SQLite Engine",async()=>{const{Database}=await import("bun:sqlite"),memory=new Database(":memory:");let version;try{const row=memory.query("SELECT sqlite_version() AS version").get();if(typeof row?.version==="string")version=row.version}finally{memory.close()}if(!version)throw new ProbeWarning("could not read sqlite_version() (skipped)");if(!isVersionGreaterThanOrEqual(version,minimumSqliteVersion))throw Error(`v${version} is below the required v${minimumSqliteVersion} (system.sqlite in package.json); upgrade Bun for a newer bundled SQLite`);return`v${version}`});await probe(checks,"Database",async()=>{const{db}=await import("@stacksjs/database"),unsafe=db.unsafe;if(typeof unsafe!=="function")throw new ProbeWarning("driver exposes no raw query method (skipped)");const statement=unsafe("SELECT 1");if(!statement||typeof statement.then!=="function"&&typeof statement.execute!=="function")throw new ProbeWarning("driver returned a non-executable statement (skipped)");const result=typeof statement.execute==="function"?await statement.execute():await statement;if(result===void 0||result===null)throw Error("probe query returned no result");if(Array.isArray(result)&&result.length===0)throw Error("probe query returned no rows");return"Reachable"});await probe(checks,"Database FKs",async()=>{const{auditForeignKeys}=await import("@stacksjs/database"),result=await auditForeignKeys();if(result.declared.length===0)return"No belongsTo declarations";const skipped=result.absentTable.length>0?`, ${result.absentTable.length} on tables not migrated`:"";if(result.missing.length===0)return`${result.declared.length} declared FKs all present${skipped}`;const sample=result.missing.slice(0,5).map((fk)=>`${fk.fromTable}.${fk.fromColumn} \u2192 ${fk.toTable}.${fk.toColumn}`).join(", "),more=result.missing.length>5?` (+${result.missing.length-5} more)`:"";throw Error(`${result.missing.length}/${result.declared.length} declared FKs missing from live schema: ${sample}${more}${skipped}. Run \`buddy migrate:fresh\` (will reset data) or \`buddy migrate\` against a clean DB.`)});await probe(checks,"Unique indexes",async()=>{const{auditUniqueIndexes}=await import("@stacksjs/database"),result=await auditUniqueIndexes();if(!result.supported)return"Dialect not audited (skipped)";const skipped=result.skippedTables.length>0?`, ${result.skippedTables.length} tables skipped (not migrated)`:"";if(result.missing.length===0)return`${result.declared.length} declared unique constraints all indexed${skipped}`;const sample=result.missing.slice(0,5).map((u)=>`${u.table}.${u.columns.join("+")}`).join(", "),more=result.missing.length>5?` (+${result.missing.length-5} more)`:"",first=result.missing[0];if(!first)throw Error("Unique-index audit reported missing entries without details");const example=`CREATE UNIQUE INDEX IF NOT EXISTS "${first.table}_${first.columns.join("_")}_unique" ON "${first.table}" ("${first.columns.join('", "')}")`;throw Error(`${result.missing.length}/${result.declared.length} declared unique constraints have no UNIQUE index: ${sample}${more}. Run \`buddy migrate\` (re-queues missing unique-index migrations, #1952) - dedupe duplicate rows first or migrate hard-fails; if no migration file exists run \`buddy generate:migrations\`, or create manually: ${example}`)},1e4);await probe(checks,"Migration ledger",async()=>{const{auditMigrationLedger}=await import("@stacksjs/database"),result=await auditMigrationLedger();if(!result.supported)return"Dialect not audited (skipped)";if(result.entries.length===0)return"No migration files";const{counts,orphans}=result;if(!result.drift)return`${result.entries.length} migrations, ledger consistent with the schema`;const parts=[];if(counts.stranded>0)parts.push(`${counts.stranded} applied but unrecorded (would re-run)`);if(counts.partial>0)parts.push(`${counts.partial} half-applied`);if(counts.reverted>0)parts.push(`${counts.reverted} recorded but missing from the schema`);if(orphans.length>0)parts.push(`${orphans.length} ledger row(s) with no file on disk`);const needsReapply=counts.reverted+counts.partial,repair=needsReapply>0?`\`buddy migrate:status --reconcile\` cannot repair ${needsReapply} of these - it never runs migration SQL, so it skips anything the schema is missing. Those need re-applying: \`buddy migrate:fresh\` (rebuilds from the corpus, RESETS DATA), or restore the database and re-run the missing files by hand.`:"Repair with `buddy migrate:status --reconcile`.";throw Error(`Migration ledger has drifted: ${parts.join(", ")}. Inspect with \`buddy migrate:status\`. ${repair}`)},1e4);await probe(checks,"FK orphans",async()=>{const{findFkOrphans}=await import("@stacksjs/database"),result=await findFkOrphans();if(!result.supported)return"Dialect not audited (skipped)";if(result.total===0)return"No orphan rows (PRAGMA foreign_key_check clean)";const sample=result.orphans.slice(0,5).map((o)=>`${o.table}.${o.column} \u2192 ${o.parent} (${o.count})`).join(", "),more=result.orphans.length>5?` (+${result.orphans.length-5} more)`:"",first=result.orphans[0];if(!first)throw Error("Foreign-key audit reported orphan rows without details");throw Error(`${result.total} orphan rows violate FKs: ${sample}${more}. Legacy rows written under foreign_keys=OFF (#1951). Review and clean manually, e.g. DELETE FROM ${first.table} WHERE ${first.column} IS NOT NULL AND ${first.column} NOT IN (SELECT id FROM ${first.parent}) - doctor never deletes data.`)},1e4);await probe(checks,"Cache",async()=>{const{cache}=await import("@stacksjs/cache"),k=`__doctor_${Date.now()}`;await cache.set(k,1,5);const v=await cache.get(k);await cache.del(k);if(v!==1)throw Error("round-trip failed");return"Round-trip ok"});await probe(checks,"Queue driver",async()=>{const{config}=await import("@stacksjs/config");return`Driver: ${config.queue?.default??"sync"}`});await probe(checks,"Mail driver",async()=>{const{config}=await import("@stacksjs/config");return`Driver: ${config.email?.default??"log"}`});{const{ensureRuntimeDirectories,projectPath}=await import("@stacksjs/path"),leftover=ensureRuntimeDirectories().filter((entry)=>!entry.cleared);checks.push(leftover.length===0?{name:"Runtime directories",status:"pass",message:"All under storage/"}:{name:"Runtime directories",status:"warn",message:`Still in the project root: ${leftover.map((entry)=>entry.legacy.slice(projectPath().length+1)).join(", ")} - remove them so state stays under storage/`})}const hasAwsCreds=Boolean(process.env.AWS_ACCESS_KEY_ID&&process.env.AWS_SECRET_ACCESS_KEY),hasAwsRole=Boolean(process.env.AWS_PROFILE)||Boolean(process.env.AWS_ROLE_ARN);if(hasAwsCreds||hasAwsRole)checks.push({name:"AWS credentials",status:"pass",message:hasAwsCreds?"Static keys in env":"IAM role configured"});else checks.push({name:"AWS credentials",status:"warn",message:"Not configured (cloud / SES / S3 commands will fail)"});await probe(checks,"Database backups",async()=>{const fs=await import("node:fs"),cloudConfig=resolve(process.cwd(),"config/cloud.ts");if(!fs.existsSync(cloudConfig))return"No config/cloud.ts (skipped)";const{findUnbackedManagedServices,hasOffsiteBackupDestination,unbackedDataMessage}=await import("../unbacked-data");let tsCloud;try{tsCloud=(await import(cloudConfig)).tsCloud}catch{throw new ProbeWarning("could not read config/cloud.ts (skipped)")}const unbacked=findUnbackedManagedServices(tsCloud,await hasOffsiteBackupDestination());if(unbacked.length===0)return"No unbacked managed data services";throw new ProbeWarning(unbackedDataMessage(unbacked))});await probe(checks,".env decryption",async()=>{const fs=await import("node:fs");if(!fs.existsSync(".env"))return"No .env (skipped)";const content=fs.readFileSync(".env","utf8");if(!/(?:^|=)(?:enc|encrypted):/m.test(content))return"No encrypted values";const privateKey=process.env.DOTENV_PRIVATE_KEY;if(!privateKey)throw Error("Encrypted values present but DOTENV_PRIVATE_KEY is unset");const{parse}=await import("@stacksjs/env"),{errors}=parse(content,{privateKey});if(errors.length>0)throw Error(errors.join("; "));return"All encrypted values decrypt cleanly"});try{const{filesystems}=await import("@stacksjs/config"),driver=filesystems.driver??"local";if(driver==="s3"){const bucket=process.env.S3_BUCKET??filesystems.s3?.bucket,accessKeyId=process.env.AWS_ACCESS_KEY_ID,secretAccessKey=process.env.AWS_SECRET_ACCESS_KEY,missing=[];if(!bucket)missing.push("S3_BUCKET");if(!accessKeyId)missing.push("AWS_ACCESS_KEY_ID");if(!secretAccessKey)missing.push("AWS_SECRET_ACCESS_KEY");if(missing.length>0)checks.push({name:"Storage credentials",status:"warn",message:`Default disk is 's3' but missing: ${missing.join(", ")}. Uploads will throw on first use.`});else checks.push({name:"Storage credentials",status:"pass",message:"s3 default disk has bucket + AWS credentials"})}else checks.push({name:"Storage credentials",status:"pass",message:`Default disk is '${driver}' (no remote credentials required)`})}catch(err){checks.push({name:"Storage credentials",status:"warn",message:`Could not audit storage config: ${err instanceof Error?err.message:String(err)}`})}try{if(process.platform!=="darwin")checks.push({name:"Dev domains (rpx)",status:"pass",message:"Not macOS (skipped)"});else{const fs=await import("node:fs"),os=await import("node:os"),path=await import("node:path"),isAlive=(pid)=>{try{process.kill(pid,0);return!0}catch(err){return err.code==="EPERM"}},registryDir=path.join(os.homedir(),".stacks","rpx","registry.d"),registered=new Set,deadRegistryFiles=[];if(fs.existsSync(registryDir))for(const file of fs.readdirSync(registryDir)){if(!file.endsWith(".json"))continue;try{const entry=JSON.parse(fs.readFileSync(path.join(registryDir,file),"utf8"));if(entry.to&&(entry.pid===void 0||isAlive(entry.pid)))registered.add(entry.to.toLowerCase());if(typeof entry.pid==="number"&&!isAlive(entry.pid))deadRegistryFiles.push(file)}catch{}}const staleHosts=new Set,staleResolvers=[],hostsPath="/etc/hosts",hostsLines=fs.existsSync(hostsPath)?fs.readFileSync(hostsPath,"utf8").split(`
|
|
2
3
|
`):[];for(let i=0;i<hostsLines.length;i++){const line=hostsLines[i];if(line.trim()==="# Added by rpx"){for(let j=i+1;j<hostsLines.length;j++){const blockLine=hostsLines[j].trim();if(blockLine===""||blockLine.startsWith("#"))break;const names=blockLine.split("#")[0]?.trim().split(/\s+/).slice(1)??[];for(const name of names)if(!registered.has(name.toLowerCase()))staleHosts.add(name)}continue}const hash=line.indexOf("#");if(hash===-1)continue;const marker=/^rpx(?::pid=(\d+))?$/.exec(line.slice(hash+1).trim());if(!marker)continue;const names=line.slice(0,hash).trim().split(/\s+/).slice(1),pid=marker[1]?Number.parseInt(marker[1],10):null;if(pid!==null?!isAlive(pid):names.every((n)=>!registered.has(n.toLowerCase())))for(const name of names)staleHosts.add(name)}const resolverDir="/etc/resolver";if(fs.existsSync(resolverDir))for(const file of fs.readdirSync(resolverDir))try{const content=fs.readFileSync(path.join(resolverDir,file),"utf8");if(!content.includes("127.0.0.1")||!content.includes("15353"))continue;const domain=file.toLowerCase();if(![...registered].some((host)=>host===domain||host.endsWith(`.${domain}`)))staleResolvers.push(file)}catch{}if(staleHosts.size>0||staleResolvers.length>0||deadRegistryFiles.length>0){const parts=[];if(staleHosts.size>0)parts.push(`hosts(${[...staleHosts].join(", ")})`);if(staleResolvers.length>0)parts.push(`resolver(${staleResolvers.join(", ")})`);if(deadRegistryFiles.length>0)parts.push(`registry(${deadRegistryFiles.join(", ")})`);checks.push({name:"Dev domains (rpx)",status:"warn",message:`Stale loopback overrides from dead dev sessions: ${parts.join(" ")}. These keep pointing the domain at 127.0.0.1. Remove with: sudo nano /etc/hosts; sudo rm /etc/resolver/<name>; rm ~/.stacks/rpx/registry.d/<file>. Updating @stacksjs/rpx lets the daemon sweep pid-stamped entries automatically.`})}else checks.push({name:"Dev domains (rpx)",status:"pass",message:"No stale dev-domain overrides"})}}catch(err){checks.push({name:"Dev domains (rpx)",status:"warn",message:`Could not audit dev-domain overrides: ${err instanceof Error?err.message:String(err)}`})}await probe(checks,"Dev ports",async()=>{const net=await import("node:net"),{config}=await import("@stacksjs/config"),configured=config.ports??{},targets=[{name:"frontend",key:"frontend",envVar:"PORT",fallback:3000},{name:"api",key:"api",envVar:"PORT_API",fallback:3008},{name:"docs",key:"docs",envVar:"PORT_DOCS",fallback:3006},{name:"dashboard",key:"admin",envVar:"PORT_ADMIN",fallback:3002}].map((t)=>({...t,port:Number(configured[t.key])||t.fallback})),canConnect=(port,host)=>new Promise((resolve)=>{const socket=net.createConnection({port,host});socket.setTimeout(400);const done=(occupied)=>{socket.destroy();resolve(occupied)};socket.once("connect",()=>done(!0));socket.once("timeout",()=>done(!1));socket.once("error",()=>done(!1))}),occupied=new Set;await Promise.all([...new Set(targets.map((t)=>t.port))].map(async(port)=>{if(await canConnect(port,"127.0.0.1")||await canConnect(port,"::1"))occupied.add(port)}));const busy=targets.filter((t)=>occupied.has(t.port));if(busy.length>0){const list=busy.map((t)=>`${t.name} :${t.port} (${t.envVar})`).join(", ");throw new ProbeWarning(`in use: ${list}. buddy dev will fail to bind; stop the process holding the port or set the override env var`)}return`All free: ${targets.map((t)=>`${t.name} :${t.port}`).join(", ")}`});try{const orphans=[];for(const name of FEATURE_NAMES){if(feature(name))continue;const present=featurePathsPresent(name);if(present.length>0)orphans.push({feature:name,count:present.length})}if(orphans.length>0){const summary=orphans.map((o)=>`${o.feature} (${o.count} path${o.count===1?"":"s"})`).join(", ");checks.push({name:"Feature scaffolding",status:"warn",message:`Stamped files remain for disabled features: ${summary}. Run \`./buddy <feature>:uninstall\` to remove or \`<feature>:install\` to re-enable.`})}else checks.push({name:"Feature scaffolding",status:"pass",message:"No orphan files for disabled features"})}catch(err){checks.push({name:"Feature scaffolding",status:"warn",message:`Could not audit feature scaffolding: ${err instanceof Error?err.message:String(err)}`})}log.info("");log.info(bold("Health Check Results:"));log.info(dim("\u2500".repeat(60)));log.info("");let hasFailures=!1,hasWarnings=!1;for(const check of checks){let statusIcon="",statusColor=(text)=>text;if(check.status==="pass"){statusIcon="\u2713";statusColor=green}else if(check.status==="warn"){statusIcon="\u26A0";statusColor=yellow;hasWarnings=!0}else{statusIcon="\u2717";statusColor=red;hasFailures=!0}log.info(`${statusColor(statusIcon)} ${bold(check.name.padEnd(20))} ${dim(check.message)}`)}log.info("");log.info(dim("\u2500".repeat(60)));log.info("");if(hasFailures){log.error("Some critical checks failed. Please address the issues above.");if(options?.fail!==!1){await log.flush();process.exit(1)}}else if(hasWarnings)log.info(yellow("Some checks have warnings. Your system should work but may have issues."));else log.success(green("All checks passed! Your Stacks installation looks healthy."));log.info("")});onUnknownSubcommand(buddy,"doctor")}
|
|
@@ -15,5 +15,14 @@ import type { CLI } from '@stacksjs/types';
|
|
|
15
15
|
* and only then discover it has nothing to rebuild them with.
|
|
16
16
|
*/
|
|
17
17
|
export declare function validateMigrationDialect(cwd?: unknown, options?: { driver?: string }): { valid: boolean, error?: string };
|
|
18
|
+
/**
|
|
19
|
+
* Whether this run is a preview. `--diff` is the migrate flag, but buddy also
|
|
20
|
+
* registers `--dry-run` for every command ("Preview actions without making
|
|
21
|
+
* changes"), and `migrate` used to ignore it: `buddy migrate --dry-run` ran a
|
|
22
|
+
* full migrate, generating files, advancing the model snapshot and applying
|
|
23
|
+
* everything. A flag promising to change nothing has to change nothing.
|
|
24
|
+
* `--pretend` is Laravel's name for the same thing.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isMigratePreview(options: { diff?: boolean, dryRun?: boolean, pretend?: boolean }, argv?: readonly string[]): boolean;
|
|
18
27
|
export declare function migrate(buddy: CLI): void;
|
|
19
28
|
declare type FreshGuard = 'allow' | 'confirm' | 'disabled';
|
package/dist/commands/migrate.js
CHANGED
|
@@ -6,17 +6,17 @@ The model-backed create migrations may be stale for this database - run \`./budd
|
|
|
6
6
|
`),more=result.orphans.length>5?`
|
|
7
7
|
+ ${result.orphans.length-5} more - run \`./buddy doctor\` for the full list.`:"",first=result.orphans[0];log.warn(`${result.total} row${result.total===1?"":"s"} violate foreign keys (orphaned parents), now that SQLite enforces \`foreign_keys = ON\`:
|
|
8
8
|
${sample}${more}
|
|
9
|
-
These were written under the old \`foreign_keys = OFF\` default (#1951). Review and clean up manually - e.g. DELETE FROM ${first.table} WHERE ${first.column} IS NOT NULL AND ${first.column} NOT IN (SELECT id FROM ${first.parent}). migrate never deletes data.`)}catch(err){log.debug(`[migrate] FK orphan scan skipped: ${err instanceof Error?err.message:String(err)}`)}}function describeOp(op){switch(op.kind){case"drop_table":return`drop table "${op.table}" (all rows lost)`;case"drop_column":return`drop column "${op.table}"."${op.column}" (column data lost)`;case"modify_column":return`change type of "${op.table}"."${op.column}" (possible data loss)`;case"rebuild_table":return`rebuild table "${op.table}" (SQLite rebuilds in place; compare the SQL below against the live schema)`;case"rename_column":return`rename "${op.table}"."${op.from}" \u2192 "${op.to}"`;case"rename_table":return`rename table "${op.from}" \u2192 "${op.to}"`;default:return`${op.kind} on "${op.table}"${op.column?`."${op.column}"`:""}`}}async function missingBaselineNote(){try{const{resolveSnapshotDirectory,snapshotDirectoryIsShared}=await import("@stacksjs/database"),dir=resolveSnapshotDirectory();if(existsSync(dir)&&readdirSync(dir).some((f)=>/^model-snapshot\.\w+\.json$/.test(f)))return;return`No model snapshot in ${dir}, so this change was derived without a baseline and will be proposed again on the next run.${snapshotDirectoryIsShared()?" DB_SNAPSHOT_PATH points there; check the directory exists and is writable.":" If each deploy runs from a new directory, point DB_SNAPSHOT_PATH at a shared one that survives the release."}`}catch(error){log.debug(`[migrate] baseline check skipped: ${error instanceof Error?error.message:String(error)}`);return}}async function confirmDestructiveMigrations(opts){let operations=[];try{const{previewPendingMigrations}=await import("@stacksjs/database");operations=await previewPendingMigrations({fromDb:opts.fromDb,applyRenames:opts.applyRenames})}catch(error){log.debug(`Migration preview unavailable: ${error instanceof Error?error.message:String(error)}`);return!0}const renames=operations.filter((o)=>o.kind==="rename_column"||o.kind==="rename_table");for(const r of renames)log.info(`Detected ${describeOp(r)} - applying as a rename (data preserved). Use --no-rename to drop + add instead.`);const destructive=operations.filter((o)=>o.destructive);if(destructive.length===0)return!0;log.warn(`This migration includes ${destructive.length} potentially destructive change${destructive.length===1?"":"s"}:`);for(const op of destructive)log.warn(` \u2022 ${describeOp(op)}`);const baseline=await missingBaselineNote();if(baseline)log.warn(baseline);if(opts.force)return!0;if(isCI||!hasTTY){log.error("Refusing to apply destructive changes in a non-interactive environment. Re-run with --force to proceed.");return!1}return confirm({message:"Apply these destructive changes?",initial:!1})}function parseGuardBool(raw){if(raw==null||raw==="")return;const v=raw.toLowerCase().trim();if(v==="1"||v==="true"||v==="yes"||v==="on")return!0;if(v==="0"||v==="false"||v==="no"||v==="off")return!1;return}function parseFreshGuard(raw){const v=raw?.toLowerCase().trim();return v==="allow"||v==="confirm"||v==="disabled"?v:void 0}async function resolveMigrationGuards(){const isProd=/^prod/i.test(process.env.APP_ENV||"local");let cfg={};try{const{awaitConfig}=await import("@stacksjs/config");cfg=(await awaitConfig()).database?.safety??{}}catch(error){log.debug(`[migrate] safety config unavailable, using defaults: ${error instanceof Error?error.message:String(error)}`)}const confirmMigrate=parseGuardBool(process.env.DB_MIGRATE_CONFIRM)??cfg.confirmMigrate??!0,migrateFresh=parseFreshGuard(process.env.DB_MIGRATE_FRESH)??cfg.migrateFresh??(isProd?"disabled":"allow");return{confirmMigrate,migrateFresh}}function currentDatabaseLabel(){if((process.env.DB_CONNECTION||"sqlite").toLowerCase()==="sqlite")return process.env.DB_DATABASE_PATH||"database/stacks.sqlite";return process.env.DB_DATABASE||"stacks"}export function migrate(buddy){const descriptions={migrate:"Migrates your database",fresh:"Drop all tables and re-run every migration (destroys all data)",project:"Target a specific project",verbose:"Enable verbose output",auth:"Also migrate auth tables (oauth_clients, oauth_access_tokens, oauth_refresh_tokens, password_resets)",force:"Apply destructive changes (drop column/table, lossy type change) without confirmation",fromDb:"Diff against the live database schema instead of the snapshot (self-heal drift)",noRename:"Treat renamed columns as drop + add instead of a data-preserving rename",noGenerate:"Apply committed migration files only; do not generate new ones from your models"};buddy.command("migrate",descriptions.migrate).alias("db:migrate").option("-d, --diff","Show the SQL that would be run",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("-a, --auth",descriptions.auth,{default:!0}).option("--no-auth","Skip auth/oauth table migrations").option("-f, --force",descriptions.force,{default:!1}).option("--create-database","Create the database if it does not exist, without asking",{default:!1}).option("--from-db",descriptions.fromDb,{default:!1}).option("--no-rename",descriptions.noRename).option("--no-generate",descriptions.noGenerate).option("--strict","Fail when the schema has drifted from the models or a migration is git-ignored (for CI)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy migrate` ...",options);const perf=await intro("buddy migrate"),validation=validateModelsExist();if(!validation.valid){console.error(`
|
|
9
|
+
These were written under the old \`foreign_keys = OFF\` default (#1951). Review and clean up manually - e.g. DELETE FROM ${first.table} WHERE ${first.column} IS NOT NULL AND ${first.column} NOT IN (SELECT id FROM ${first.parent}). migrate never deletes data.`)}catch(err){log.debug(`[migrate] FK orphan scan skipped: ${err instanceof Error?err.message:String(err)}`)}}function describeOp(op){switch(op.kind){case"drop_table":return`drop table "${op.table}" (all rows lost)`;case"drop_column":return`drop column "${op.table}"."${op.column}" (column data lost)`;case"modify_column":return`change type of "${op.table}"."${op.column}" (possible data loss)`;case"rebuild_table":return`rebuild table "${op.table}" (SQLite rebuilds in place; compare the SQL below against the live schema)`;case"rename_column":return`rename "${op.table}"."${op.from}" \u2192 "${op.to}"`;case"rename_table":return`rename table "${op.from}" \u2192 "${op.to}"`;default:return`${op.kind} on "${op.table}"${op.column?`."${op.column}"`:""}`}}async function missingBaselineNote(){try{const{resolveSnapshotDirectory,snapshotDirectoryIsShared}=await import("@stacksjs/database"),dir=resolveSnapshotDirectory();if(existsSync(dir)&&readdirSync(dir).some((f)=>/^model-snapshot\.\w+\.json$/.test(f)))return;return`No model snapshot in ${dir}, so this change was derived without a baseline and will be proposed again on the next run.${snapshotDirectoryIsShared()?" DB_SNAPSHOT_PATH points there; check the directory exists and is writable.":" If each deploy runs from a new directory, point DB_SNAPSHOT_PATH at a shared one that survives the release."}`}catch(error){log.debug(`[migrate] baseline check skipped: ${error instanceof Error?error.message:String(error)}`);return}}async function confirmDestructiveMigrations(opts){let operations=[];try{const{previewPendingMigrations}=await import("@stacksjs/database");operations=await previewPendingMigrations({fromDb:opts.fromDb,applyRenames:opts.applyRenames})}catch(error){log.debug(`Migration preview unavailable: ${error instanceof Error?error.message:String(error)}`);return!0}const renames=operations.filter((o)=>o.kind==="rename_column"||o.kind==="rename_table");for(const r of renames)log.info(`Detected ${describeOp(r)} - applying as a rename (data preserved). Use --no-rename to drop + add instead.`);const destructive=operations.filter((o)=>o.destructive);if(destructive.length===0)return!0;log.warn(`This migration includes ${destructive.length} potentially destructive change${destructive.length===1?"":"s"}:`);for(const op of destructive)log.warn(` \u2022 ${describeOp(op)}`);const baseline=await missingBaselineNote();if(baseline)log.warn(baseline);if(opts.force)return!0;if(isCI||!hasTTY){log.error("Refusing to apply destructive changes in a non-interactive environment. Re-run with --force to proceed.");return!1}return confirm({message:"Apply these destructive changes?",initial:!1})}export function isMigratePreview(options,argv=process.argv){return options.diff===!0||options.dryRun===!0||options.pretend===!0||argv.includes("--dry-run")||argv.includes("--pretend")}function parseGuardBool(raw){if(raw==null||raw==="")return;const v=raw.toLowerCase().trim();if(v==="1"||v==="true"||v==="yes"||v==="on")return!0;if(v==="0"||v==="false"||v==="no"||v==="off")return!1;return}function parseFreshGuard(raw){const v=raw?.toLowerCase().trim();return v==="allow"||v==="confirm"||v==="disabled"?v:void 0}async function resolveMigrationGuards(){const isProd=/^prod/i.test(process.env.APP_ENV||"local");let cfg={};try{const{awaitConfig}=await import("@stacksjs/config");cfg=(await awaitConfig()).database?.safety??{}}catch(error){log.debug(`[migrate] safety config unavailable, using defaults: ${error instanceof Error?error.message:String(error)}`)}const confirmMigrate=parseGuardBool(process.env.DB_MIGRATE_CONFIRM)??cfg.confirmMigrate??!0,migrateFresh=parseFreshGuard(process.env.DB_MIGRATE_FRESH)??cfg.migrateFresh??(isProd?"disabled":"allow");return{confirmMigrate,migrateFresh}}function currentDatabaseLabel(){if((process.env.DB_CONNECTION||"sqlite").toLowerCase()==="sqlite")return process.env.DB_DATABASE_PATH||"database/stacks.sqlite";return process.env.DB_DATABASE||"stacks"}export function migrate(buddy){const descriptions={migrate:"Migrates your database",fresh:"Drop all tables and re-run every migration (destroys all data)",project:"Target a specific project",verbose:"Enable verbose output",auth:"Also migrate auth tables (oauth_clients, oauth_access_tokens, oauth_refresh_tokens, password_resets)",force:"Apply destructive changes (drop column/table, lossy type change) without confirmation",fromDb:"Diff against the live database schema instead of the snapshot (self-heal drift)",noRename:"Treat renamed columns as drop + add instead of a data-preserving rename",noGenerate:"Apply committed migration files only; do not generate new ones from your models"};buddy.command("migrate",descriptions.migrate).alias("db:migrate").option("-d, --diff","Show the SQL that would be run",{default:!1}).option("--pretend","Same as --diff (and --dry-run): preview, change nothing",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("-a, --auth",descriptions.auth,{default:!0}).option("--no-auth","Skip auth/oauth table migrations").option("-f, --force",descriptions.force,{default:!1}).option("--create-database","Create the database if it does not exist, without asking",{default:!1}).option("--from-db",descriptions.fromDb,{default:!1}).option("--no-rename",descriptions.noRename).option("--no-generate",descriptions.noGenerate).option("--strict","Fail when the schema has drifted from the models or a migration is git-ignored (for CI)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy migrate` ...",options);const perf=await intro("buddy migrate"),validation=validateModelsExist();if(!validation.valid){console.error(`
|
|
10
10
|
\u274C Error: ${validation.error}
|
|
11
11
|
`);process.exit(ExitCode.FatalError)}const dialectCheck=validateMigrationDialect();if(!dialectCheck.valid){console.error(`
|
|
12
12
|
\u274C Error: ${dialectCheck.error}
|
|
13
|
-
`);process.exit(ExitCode.FatalError)}const applyRenames=options.rename===!1?!1:void 0;if(options.fromDb)process.env.STACKS_MIGRATE_FROM_DB="1";if(applyRenames===!1)process.env.STACKS_MIGRATE_NO_RENAME="1";if(options.generate===!1)process.env.STACKS_MIGRATE_NO_GENERATE="1";if(options
|
|
13
|
+
`);process.exit(ExitCode.FatalError)}const applyRenames=options.rename===!1?!1:void 0;if(options.fromDb)process.env.STACKS_MIGRATE_FROM_DB="1";if(applyRenames===!1)process.env.STACKS_MIGRATE_NO_RENAME="1";if(options.generate===!1)process.env.STACKS_MIGRATE_NO_GENERATE="1";if(isMigratePreview(options)){try{const{previewPendingMigrations}=await import("@stacksjs/database"),ops=await previewPendingMigrations({fromDb:options.fromDb,applyRenames});if(ops.length===0)log.info("No pending schema changes - your models match the database.");else{log.info(`${ops.length} pending change${ops.length===1?"":"s"}:`);for(const op of ops)log.info(` \u2022 ${describeOp(op)}${op.destructive?" [destructive]":""}`)}}catch(error){await log.error("Failed to preview migrations:",error)}await outro("Diff complete - no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}await preflightDatabase({createDatabase:options.createDatabase,command:"migrate"});if((await resolveMigrationGuards()).confirmMigrate&&!options.force)if(isCI||!hasTTY)log.debug("[migrate] confirmMigrate guard skipped - non-interactive environment.");else{const APP_ENV=process.env.APP_ENV||"local";await log.flush();if(!await confirm({message:`Run migrations against the ${APP_ENV} database "${currentDatabaseLabel()}"?`,initial:!0})){await outro("Migration cancelled - no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}await ensureDatabaseOrExit();const lock=acquireMigrationLock();if(!lock.acquired){log.syncError("Another migration is already running (storage/framework/runtime/migrations.lock exists). Wait for it to finish, or remove the lockfile if it is stale.");process.exit(ExitCode.FatalError)}try{const{catchUpFrameworkRenames}=await import("@stacksjs/database");await catchUpFrameworkRenames()}catch(error){lock.release();log.syncError(error instanceof Error?error.message:String(error));process.exit(ExitCode.FatalError)}if(options.generate!==!1){if(!await confirmDestructiveMigrations({force:options.force,fromDb:options.fromDb,applyRenames})){lock.release();await outro("Migration cancelled - no changes applied.",{startTime:perf,useSeconds:!0});process.exit(isCI||!hasTTY?ExitCode.FatalError:ExitCode.Success)}}if(options.auth!==!1){log.debug("Migrating auth tables...");try{const{migrateAuthTables}=await import("@stacksjs/database"),authResult=await migrateAuthTables({verbose:options.verbose});if(!authResult.success)log.error(`Failed to migrate auth tables: ${authResult.error}`)}catch(error){log.error("Failed to migrate auth tables:",error)}}const result=await runAction(Action.Migrate,options).finally(()=>lock.release());if(resultFailed(result))log.error("Model migrations failed - applying notification/RBAC table guarantees before exiting.");if(options.auth!==!1)try{const{ensureUtcDatetimeColumns,ensureUtcTimestampDefaults,migrateNotificationTables,migrateRbacTables,migrateTraitTables}=await import("@stacksjs/database"),notifResult=await migrateNotificationTables({verbose:options.verbose});if(!notifResult.success)log.error(`Failed to migrate notification tables: ${notifResult.error}`);const rbacResult=await migrateRbacTables({verbose:options.verbose});if(!rbacResult.success)log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);const traitResult=await migrateTraitTables({verbose:options.verbose});if(!traitResult.success)log.error(`Failed to migrate polymorphic trait tables: ${traitResult.error}`);const datetimeResult=await ensureUtcDatetimeColumns({verbose:options.verbose});if(!datetimeResult.success)log.error(`Failed to convert TIMESTAMP columns to DATETIME: ${datetimeResult.error}`);const defaultsResult=await ensureUtcTimestampDefaults({verbose:options.verbose});if(!defaultsResult.success)log.error(`Failed to pin timestamp defaults to UTC: ${defaultsResult.error}`)}catch(error){log.error("Failed to migrate notification/RBAC/trait tables:",error)}if(resultFailed(result)){await outro("While running the migrate command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}if(options.auth!==!1)try{const{ensureUsersAuthColumns,sqlHelpers}=await import("@stacksjs/database"),driver=process.env.DB_CONNECTION||"sqlite";await ensureUsersAuthColumns(sqlHelpers(driver),{verbose:options.verbose})}catch(error){log.error("Failed to ensure users auth columns post-migration:",error)}try{const{ensureUuidColumns,sqlHelpers}=await import("@stacksjs/database"),driver=process.env.DB_CONNECTION||"sqlite";await ensureUuidColumns(sqlHelpers(driver),{verbose:options.verbose})}catch(error){log.error("Failed to ensure uuid columns post-migration:",error)}await reportMissingForeignKeys();const schemaMatches=await reportSchemaDrift(),allCommittable=await reportIgnoredMigrations();if(strictMode(options)&&(!schemaMatches||!allCommittable)){console.error(`
|
|
14
14
|
\u274C --strict: ${!allCommittable?"a migration is git-ignored":"the live schema does not match the models"} (see above).
|
|
15
|
-
`);process.exit(ExitCode.FatalError)}await reportFkOrphans();const APP_ENV=process.env.APP_ENV||"local",marker=readMigrateMarker(),authSuffix=options.auth!==!1?" (including auth tables)":"",outroMessage=marker==null?`Migrated your ${APP_ENV} database.${authSuffix}`:marker.appliedCount===0?`Nothing to migrate - your ${APP_ENV} database is already up to date.${authSuffix}`:`Applied ${marker.appliedCount} migration${marker.appliedCount===1?"":"s"} to your ${APP_ENV} database.${authSuffix}`;await outro(outroMessage,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:fresh",descriptions.fresh).alias("db:fresh").option("-d, --diff","Show
|
|
15
|
+
`);process.exit(ExitCode.FatalError)}await reportFkOrphans();const APP_ENV=process.env.APP_ENV||"local",marker=readMigrateMarker(),authSuffix=options.auth!==!1?" (including auth tables)":"",outroMessage=marker==null?`Migrated your ${APP_ENV} database.${authSuffix}`:marker.appliedCount===0?`Nothing to migrate - your ${APP_ENV} database is already up to date.${authSuffix}`:`Applied ${marker.appliedCount} migration${marker.appliedCount===1?"":"s"} to your ${APP_ENV} database.${authSuffix}`;await outro(outroMessage,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:fresh",descriptions.fresh).alias("db:fresh").option("-d, --diff","Show what would be dropped and replayed, and change nothing",{default:!1}).option("--pretend","Same as --diff (and --dry-run): preview, change nothing",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("-s, --seed","Run database seeders after migration",{default:!1}).option("-a, --auth",descriptions.auth,{default:!0}).option("--no-auth","Skip auth/oauth table migrations").option("--create-database","Create the database if it does not exist, without asking",{default:!1}).option("-f, --force",'Skip the drop-database confirmation (only honored when the migrateFresh guard is "allow")',{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy migrate:fresh` ...",options);const perf=await intro("buddy migrate:fresh"),validation=validateModelsExist();if(!validation.valid){console.error(`
|
|
16
16
|
\u274C Error: ${validation.error}
|
|
17
17
|
`);process.exit(ExitCode.FatalError)}const dialectCheck=validateMigrationDialect();if(!dialectCheck.valid){console.error(`
|
|
18
18
|
\u274C Error: ${dialectCheck.error}
|
|
19
|
-
`);process.exit(ExitCode.FatalError)}const guards=await resolveMigrationGuards(),dbLabel=currentDatabaseLabel(),APP_ENV=process.env.APP_ENV||"local";if(guards.migrateFresh==="disabled"){await log.error(`\`buddy migrate:fresh\` is disabled by your migration safety guards (it DROPS every table).
|
|
19
|
+
`);process.exit(ExitCode.FatalError)}if(isMigratePreview(options)){let files=0;try{const dir=resolveMigrationDirectory(String(process.env.DB_CONNECTION||"sqlite").toLowerCase());files=readdirSync(dir).filter((file)=>file.endsWith(".sql")).length}catch{}log.info(`Would drop every table in the ${process.env.APP_ENV||"local"} database "${currentDatabaseLabel()}" and replay ${files} migration file${files===1?"":"s"}${options.seed?", then seed it":""}.`);await outro("Dry run - nothing was dropped or migrated.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const guards=await resolveMigrationGuards(),dbLabel=currentDatabaseLabel(),APP_ENV=process.env.APP_ENV||"local";if(guards.migrateFresh==="disabled"){await log.error(`\`buddy migrate:fresh\` is disabled by your migration safety guards (it DROPS every table).
|
|
20
20
|
Target: ${APP_ENV} database "${dbLabel}"
|
|
21
21
|
To allow it, set database.safety.migrateFresh to 'allow' in config/database.ts,
|
|
22
22
|
or run once with: DB_MIGRATE_FRESH=allow ./buddy migrate:fresh`);await outro('migrate:fresh refused - the migrateFresh guard is set to "disabled".',{startTime:perf,useSeconds:!0,type:"error"});process.exit(ExitCode.FatalError)}if(!(guards.migrateFresh==="allow"&&options.force===!0)){if(isCI||!hasTTY){const hint=guards.migrateFresh==="confirm"?'Guard is "confirm": migrate:fresh must be run interactively.':"Re-run with --force to drop the database non-interactively.";await log.error(`Refusing to drop the ${APP_ENV} database "${dbLabel}" in a non-interactive environment. ${hint}`);await outro("migrate:fresh cancelled.",{startTime:perf,useSeconds:!0,type:"warning"});process.exit(ExitCode.FatalError)}log.warn(`This will DROP ALL TABLES in the ${APP_ENV} database "${dbLabel}" and rebuild them from scratch. All data will be lost.`);await log.flush();if((await text({message:`Type the database name "${dbLabel}" to confirm (blank to cancel):`})).trim()!==dbLabel){await outro("migrate:fresh cancelled - confirmation did not match.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}await preflightDatabase({createDatabase:options.createDatabase,command:"migrate:fresh"});await ensureDatabaseOrExit();const result=await runAction(Action.MigrateFresh,options);if(resultFailed(result))log.error("Model migrations failed - applying auth/notification/RBAC table guarantees before exiting.");if(options.auth!==!1){log.debug("Migrating auth tables...");try{const{migrateAuthTables,migrateNotificationTables,migrateRbacTables,migrateTraitTables}=await import("@stacksjs/database"),authResult=await migrateAuthTables({verbose:options.verbose});if(!authResult.success)log.error(`Failed to migrate auth tables: ${authResult.error}`);const notifResult=await migrateNotificationTables({verbose:options.verbose});if(!notifResult.success)log.error(`Failed to migrate notification tables: ${notifResult.error}`);const rbacResult=await migrateRbacTables({verbose:options.verbose});if(!rbacResult.success)log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);const traitResult=await migrateTraitTables({verbose:options.verbose});if(!traitResult.success)log.error(`Failed to migrate polymorphic trait tables: ${traitResult.error}`)}catch(error){log.error("Failed to migrate auth/notification/RBAC/trait tables:",error)}}try{const{ensureUuidColumns,sqlHelpers}=await import("@stacksjs/database"),driver=process.env.DB_CONNECTION||"sqlite";await ensureUuidColumns(sqlHelpers(driver),{verbose:options.verbose})}catch(error){log.error("Failed to ensure uuid columns post-migration:",error)}if(resultFailed(result)){await outro("While running the migrate:fresh command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await reportMissingForeignKeys();await reportSchemaDrift();if(options.seed){log.info("Running database seeders...");try{const{runApplicationSeeders,seed}=await import("@stacksjs/database"),seedResult=await seed({verbose:options.verbose,fresh:!0}),applicationSeedResult=await runApplicationSeeders({verbose:options.verbose}),failures=seedResult.failed+applicationSeedResult.failed;if(failures>0){log.warn(`Seeding completed with ${failures} failure(s)`);for(const r of seedResult.results)if(!r.success)log.error(` - ${r.model}: ${r.error}`);for(const r of applicationSeedResult.results)if(!r.success)log.error(` - ${r.seeder}: ${r.error}`)}else log.success(`Seeded ${seedResult.successful} model(s) and ran ${applicationSeedResult.successful} application seeder(s)`)}catch(error){log.error("Failed to run seeders:",error)}}const parts=[];if(options.auth!==!1)parts.push("auth tables");if(options.seed)parts.push("seeded");const suffix=parts.length>0?` & ${parts.join(" & ")}`:"",marker=readMigrateMarker(),countPhrase=marker==null?"":marker.appliedCount===0?" (0 applied - no migration files found?)":` (${marker.appliedCount} migration${marker.appliedCount===1?"":"s"} applied)`;await outro(`All tables dropped successfully & migrated successfully${countPhrase}${suffix}`,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:switch <driver>","Pre-flight check + plan for switching DB_CONNECTION between sqlite / mysql / vitess / postgres").action(async(driver)=>{log.debug(`Running \`buddy migrate:switch ${driver}\` ...`);const perf=await intro("buddy migrate:switch"),target=driver.toLowerCase();if(!new Set(["sqlite","mysql","vitess","postgres"]).has(target)){console.log(`
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/buddy",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.14",
|
|
6
6
|
"description": "Meet Buddy. The Stacks runtime.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -95,69 +95,69 @@
|
|
|
95
95
|
"prepublishOnly": "bun run build"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"@stacksjs/actions": "^0.75.
|
|
99
|
-
"@stacksjs/ai": "^0.75.
|
|
100
|
-
"@stacksjs/alias": "^0.75.
|
|
101
|
-
"@stacksjs/analytics": "^0.75.
|
|
102
|
-
"@stacksjs/api": "^0.75.
|
|
103
|
-
"@stacksjs/arrays": "^0.75.
|
|
104
|
-
"@stacksjs/auth": "^0.75.
|
|
105
|
-
"@stacksjs/browser-extension": "^0.75.
|
|
106
|
-
"@stacksjs/build": "^0.75.
|
|
107
|
-
"@stacksjs/cache": "^0.75.
|
|
108
|
-
"@stacksjs/chat": "^0.75.
|
|
98
|
+
"@stacksjs/actions": "^0.75.14",
|
|
99
|
+
"@stacksjs/ai": "^0.75.14",
|
|
100
|
+
"@stacksjs/alias": "^0.75.14",
|
|
101
|
+
"@stacksjs/analytics": "^0.75.14",
|
|
102
|
+
"@stacksjs/api": "^0.75.14",
|
|
103
|
+
"@stacksjs/arrays": "^0.75.14",
|
|
104
|
+
"@stacksjs/auth": "^0.75.14",
|
|
105
|
+
"@stacksjs/browser-extension": "^0.75.14",
|
|
106
|
+
"@stacksjs/build": "^0.75.14",
|
|
107
|
+
"@stacksjs/cache": "^0.75.14",
|
|
108
|
+
"@stacksjs/chat": "^0.75.14",
|
|
109
109
|
"@stacksjs/clapp": "^0.2.12",
|
|
110
|
-
"@stacksjs/cli": "^0.75.
|
|
111
|
-
"@stacksjs/cloud": "^0.75.
|
|
112
|
-
"@stacksjs/cms": "^0.75.
|
|
113
|
-
"@stacksjs/collections": "^0.75.
|
|
114
|
-
"@stacksjs/config": "^0.75.
|
|
115
|
-
"@stacksjs/database": "^0.75.
|
|
116
|
-
"@stacksjs/desktop-build": "^0.75.
|
|
117
|
-
"@stacksjs/dns": "^0.75.
|
|
110
|
+
"@stacksjs/cli": "^0.75.14",
|
|
111
|
+
"@stacksjs/cloud": "^0.75.14",
|
|
112
|
+
"@stacksjs/cms": "^0.75.14",
|
|
113
|
+
"@stacksjs/collections": "^0.75.14",
|
|
114
|
+
"@stacksjs/config": "^0.75.14",
|
|
115
|
+
"@stacksjs/database": "^0.75.14",
|
|
116
|
+
"@stacksjs/desktop-build": "^0.75.14",
|
|
117
|
+
"@stacksjs/dns": "^0.75.14",
|
|
118
118
|
"@stacksjs/dnsx": "^0.2.3",
|
|
119
|
-
"@stacksjs/email": "^0.75.
|
|
120
|
-
"@stacksjs/enums": "^0.75.
|
|
121
|
-
"@stacksjs/env": "^0.75.
|
|
122
|
-
"@stacksjs/error-handling": "^0.75.
|
|
123
|
-
"@stacksjs/events": "^0.75.
|
|
124
|
-
"@stacksjs/features": "^0.75.
|
|
125
|
-
"@stacksjs/git": "^0.75.
|
|
119
|
+
"@stacksjs/email": "^0.75.14",
|
|
120
|
+
"@stacksjs/enums": "^0.75.14",
|
|
121
|
+
"@stacksjs/env": "^0.75.14",
|
|
122
|
+
"@stacksjs/error-handling": "^0.75.14",
|
|
123
|
+
"@stacksjs/events": "^0.75.14",
|
|
124
|
+
"@stacksjs/features": "^0.75.14",
|
|
125
|
+
"@stacksjs/git": "^0.75.14",
|
|
126
126
|
"@stacksjs/gitit": "^0.2.5",
|
|
127
|
-
"@stacksjs/health": "^0.75.
|
|
127
|
+
"@stacksjs/health": "^0.75.14",
|
|
128
128
|
"@stacksjs/httx": "^0.1.10",
|
|
129
|
-
"@stacksjs/image": "^0.75.
|
|
130
|
-
"@stacksjs/lint": "^0.75.
|
|
131
|
-
"@stacksjs/logging": "^0.75.
|
|
132
|
-
"@stacksjs/notifications": "^0.75.
|
|
133
|
-
"@stacksjs/objects": "^0.75.
|
|
134
|
-
"@stacksjs/orm": "^0.75.
|
|
135
|
-
"@stacksjs/path": "^0.75.
|
|
136
|
-
"@stacksjs/payments": "^0.75.
|
|
137
|
-
"@stacksjs/realtime": "^0.75.
|
|
138
|
-
"@stacksjs/router": "^0.75.
|
|
129
|
+
"@stacksjs/image": "^0.75.14",
|
|
130
|
+
"@stacksjs/lint": "^0.75.14",
|
|
131
|
+
"@stacksjs/logging": "^0.75.14",
|
|
132
|
+
"@stacksjs/notifications": "^0.75.14",
|
|
133
|
+
"@stacksjs/objects": "^0.75.14",
|
|
134
|
+
"@stacksjs/orm": "^0.75.14",
|
|
135
|
+
"@stacksjs/path": "^0.75.14",
|
|
136
|
+
"@stacksjs/payments": "^0.75.14",
|
|
137
|
+
"@stacksjs/realtime": "^0.75.14",
|
|
138
|
+
"@stacksjs/router": "^0.75.14",
|
|
139
139
|
"@stacksjs/rpx": "^0.11.54",
|
|
140
|
-
"@stacksjs/scheduler": "^0.75.
|
|
141
|
-
"@stacksjs/search-engine": "^0.75.
|
|
142
|
-
"@stacksjs/security": "^0.75.
|
|
143
|
-
"@stacksjs/server": "^0.75.
|
|
144
|
-
"@stacksjs/sites": "^0.75.
|
|
145
|
-
"@stacksjs/skills": "^0.75.
|
|
146
|
-
"@stacksjs/storage": "^0.75.
|
|
147
|
-
"@stacksjs/strings": "^0.75.
|
|
148
|
-
"@stacksjs/stx": "^0.2.
|
|
149
|
-
"@stacksjs/testing": "^0.75.
|
|
150
|
-
"@stacksjs/tinker": "^0.75.
|
|
140
|
+
"@stacksjs/scheduler": "^0.75.14",
|
|
141
|
+
"@stacksjs/search-engine": "^0.75.14",
|
|
142
|
+
"@stacksjs/security": "^0.75.14",
|
|
143
|
+
"@stacksjs/server": "^0.75.14",
|
|
144
|
+
"@stacksjs/sites": "^0.75.14",
|
|
145
|
+
"@stacksjs/skills": "^0.75.14",
|
|
146
|
+
"@stacksjs/storage": "^0.75.14",
|
|
147
|
+
"@stacksjs/strings": "^0.75.14",
|
|
148
|
+
"@stacksjs/stx": "^0.2.320",
|
|
149
|
+
"@stacksjs/testing": "^0.75.14",
|
|
150
|
+
"@stacksjs/tinker": "^0.75.14",
|
|
151
151
|
"@stacksjs/tlsx": "^0.13.19",
|
|
152
152
|
"@stacksjs/ts-cloud": "^0.16.14",
|
|
153
|
-
"@stacksjs/tunnel": "^0.75.
|
|
154
|
-
"@stacksjs/types": "^0.75.
|
|
155
|
-
"@stacksjs/ui": "^0.75.
|
|
156
|
-
"@stacksjs/utils": "^0.75.
|
|
157
|
-
"@stacksjs/validation": "^0.75.
|
|
153
|
+
"@stacksjs/tunnel": "^0.75.14",
|
|
154
|
+
"@stacksjs/types": "^0.75.14",
|
|
155
|
+
"@stacksjs/ui": "^0.75.14",
|
|
156
|
+
"@stacksjs/utils": "^0.75.14",
|
|
157
|
+
"@stacksjs/validation": "^0.75.14",
|
|
158
158
|
"ajv": "^8.20.0",
|
|
159
159
|
"ajv-formats": "^3.0.1",
|
|
160
|
-
"bun-plugin-stx": "^0.2.
|
|
160
|
+
"bun-plugin-stx": "^0.2.320",
|
|
161
161
|
"ts-pantry": "^0.11.35"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|