Skip to content

Commit 8b5597f

Browse files
committed
chore: wip
1 parent 9832ff0 commit 8b5597f

File tree

8 files changed

+416
-37
lines changed

8 files changed

+416
-37
lines changed

bun.lock

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
"devDependencies": {
77
"@stacksjs/docs": "^0.70.23",
88
"@stacksjs/eslint-config": "^4.14.0-beta.3",
9-
"@types/bun": "^1.2.19",
10-
"buddy-bot": "^0.7.7",
9+
"@stacksjs/launchpad": "^0.6.4",
10+
"@types/bun": "^1.2.20",
11+
"buddy-bot": "^0.7.8",
1112
"bumpp": "^10.2.2",
1213
"bun-plugin-dtsx": "0.9.5",
1314
"changelogen": "^0.6.2",
@@ -580,7 +581,7 @@
580581

581582
"@tybys/wasm-util": ["@tybys/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
582583

583-
"@types/bun": ["@types/[email protected].19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
584+
"@types/bun": ["@types/[email protected].20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
584585

585586
"@types/cacheable-request": ["@types/[email protected]", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="],
586587

@@ -848,7 +849,7 @@
848849

849850
"browserslist": ["[email protected]", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
850851

851-
"buddy-bot": ["[email protected].7", "", { "dependencies": { "@types/prompts": "^2.4.9", "bunfig": "^0.10.1", "cac": "6.7.13", "prompts": "^2.4.2", "ts-pkgx": "0.4.25" }, "bin": { "buddy-bot": "dist/bin/cli.js" } }, "sha512-A5XBWBcwGUVRBHaOw+Oe+PybhyQnX9AYZrzbPeZb8bAB32esuvIjxG80sq/7d8hTcOyXQbrwywXhAfFldup+RQ=="],
852+
"buddy-bot": ["[email protected].8", "", { "dependencies": { "@types/prompts": "^2.4.9", "bunfig": "^0.10.1", "cac": "6.7.13", "prompts": "^2.4.2", "ts-pkgx": "0.4.25" }, "bin": { "buddy-bot": "dist/bin/cli.js" } }, "sha512-rCi4RlfaCaKGnoR0gCNyQlQGuAryMV3RdjYj97vdr3/lBBxyNhu5WcvZiYj5nS5WnG5ZZr+hYJL5X153pKrmhA=="],
852853

853854
"buffer-crc32": ["[email protected]", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
854855

@@ -860,7 +861,7 @@
860861

861862
"bun-plugin-dtsx": ["[email protected]", "", { "dependencies": { "@stacksjs/dtsx": "0.9.4" } }, "sha512-PMGr8kna2C7rbN5NFKW+nqj8TyXjs05Yh2QM7Xjp9PN1/cJMyZML3JJAJT0Ne/6eOYCcubmLM91r+Rix/cqn8Q=="],
862863

863-
"bun-types": ["[email protected].19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
864+
"bun-types": ["[email protected].20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
864865

865866
"bundle-name": ["[email protected]", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
866867

deps.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
dependencies:
2-
bun.sh: ^1.2.20
2+
bun.sh: 1.2.19

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131
"devDependencies": {
3232
"@stacksjs/docs": "^0.70.23",
3333
"@stacksjs/eslint-config": "^4.14.0-beta.3",
34-
"@types/bun": "^1.2.19",
35-
"buddy-bot": "^0.7.7",
34+
"@stacksjs/launchpad": "workspace:*",
35+
"@types/bun": "^1.2.20",
36+
"buddy-bot": "^0.7.8",
3637
"bumpp": "^10.2.2",
3738
"bun-plugin-dtsx": "0.9.5",
3839
"changelogen": "^0.6.2",

packages/launchpad/src/dev/dump.ts

Lines changed: 188 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
742742
|| message.includes('🔧') // Extracting
743743
|| message.includes('🚀') // Service start messages
744744
|| message.includes('⏳') // Waiting messages
745+
|| message.includes('📌') // Pin/version update notices
745746
|| message.includes('🔍') // Verbose diagnostics
746747
|| message.includes('⏱') // Timing summaries (verbose)
747748
|| message.includes('✅') // Success messages
@@ -770,6 +771,7 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
770771
|| message.includes('🔧') // Extracting
771772
|| message.includes('🚀') // Service start messages
772773
|| message.includes('⏳') // Waiting messages
774+
|| message.includes('📌') // Pin/version update notices
773775
|| message.includes('🔍') // Verbose diagnostics
774776
|| message.includes('⏱') // Timing summaries (verbose)
775777
|| message.includes('✅') // Success messages
@@ -839,7 +841,15 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
839841
if (shellOutput) {
840842
// Generate hash for this project
841843
const projectHash = generateProjectHash(dir)
842-
const envDir = path.join(process.env.HOME || '', '.local', 'share', 'launchpad', 'envs', projectHash)
844+
// Compute dependency fingerprint to ensure env path reflects dependency versions
845+
let depSuffix = ''
846+
try {
847+
const depContent = fs.readFileSync(dependencyFile)
848+
const depHash = crypto.createHash('md5').update(depContent).digest('hex').slice(0, 8)
849+
depSuffix = `-d${depHash}`
850+
}
851+
catch {}
852+
const envDir = path.join(process.env.HOME || '', '.local', 'share', 'launchpad', 'envs', `${projectHash}${depSuffix}`)
843853
const globalEnvDir = path.join(process.env.HOME || '', '.local', 'share', 'launchpad', 'global')
844854

845855
// Check if environments exist first (quick filesystem check)
@@ -862,20 +872,107 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
862872
sniffResult = { pkgs: [], env: {} }
863873
}
864874

875+
// Merge raw constraints from deps.yaml to honor exact pins over any normalization
876+
try {
877+
const rawContent = fs.readFileSync(dependencyFile, 'utf8')
878+
const rawLines = rawContent.split(/\r?\n/)
879+
let inDeps = false
880+
const rawMap = new Map<string, string>()
881+
for (const raw of rawLines) {
882+
const indent = raw.length - raw.trimStart().length
883+
const line = raw.trim()
884+
if (!inDeps) {
885+
if (line.startsWith('dependencies:'))
886+
inDeps = true
887+
continue
888+
}
889+
if (indent === 0 && line.endsWith(':'))
890+
break
891+
if (!line || line.startsWith('#'))
892+
continue
893+
const m = line.match(/^([\w.\-/]+):\s*(\S.*)$/)
894+
if (m) {
895+
rawMap.set(m[1], m[2])
896+
continue
897+
}
898+
if (line.startsWith('- ')) {
899+
const spec = line.slice(2).trim()
900+
const [domain, constraint = '*'] = spec.split('@')
901+
if (domain)
902+
rawMap.set(domain, constraint)
903+
}
904+
}
905+
if (sniffResult && Array.isArray(sniffResult.pkgs) && rawMap.size > 0) {
906+
sniffResult.pkgs = sniffResult.pkgs.map((p: any) => {
907+
const domain = String(p.project || '')
908+
const rawC = rawMap.get(domain)
909+
if (rawC && rawC !== '*' && !rawC.startsWith('^') && !rawC.startsWith('~')) {
910+
return { ...p, constraint: rawC }
911+
}
912+
return p
913+
})
914+
}
915+
}
916+
catch {}
917+
918+
// Fallback: if sniff returned no packages, parse deps.yaml minimally for pins
919+
if ((!sniffResult.pkgs || sniffResult.pkgs.length === 0) && dependencyFile) {
920+
try {
921+
const content = fs.readFileSync(dependencyFile, 'utf8')
922+
const lines = content.split(/\r?\n/)
923+
let inDeps = false
924+
const pkgs: any[] = []
925+
for (const raw of lines) {
926+
const indent = raw.length - raw.trimStart().length
927+
const line = raw.trim()
928+
if (!inDeps) {
929+
if (line.startsWith('dependencies:')) {
930+
inDeps = true
931+
}
932+
continue
933+
}
934+
if (indent === 0 && line.endsWith(':'))
935+
break
936+
if (!line || line.startsWith('#'))
937+
continue
938+
const m = line.match(/^([\w.\-/]+):\s*(\S.*)$/)
939+
if (m) {
940+
const domain = m[1]
941+
const val = m[2].trim()
942+
if (domain && val && !val.startsWith('{')) {
943+
pkgs.push({ project: domain, constraint: val, global: false })
944+
}
945+
}
946+
else if (line.startsWith('- ')) {
947+
const spec = line.slice(2).trim()
948+
const [domain, constraint = '*'] = spec.split('@')
949+
if (domain)
950+
pkgs.push({ project: domain, constraint, global: false })
951+
}
952+
}
953+
if (pkgs.length > 0)
954+
sniffResult = { pkgs, env: sniffResult.env || {} }
955+
}
956+
catch {}
957+
}
958+
865959
// Quick constraint satisfaction check for already-existing environment
866960
const semverCompare = (a: string, b: string): number => {
867961
const pa = a.replace(/^v/, '').split('.').map(n => Number.parseInt(n, 10))
868962
const pb = b.replace(/^v/, '').split('.').map(n => Number.parseInt(n, 10))
869963
for (let i = 0; i < 3; i++) {
870964
const da = pa[i] || 0
871965
const db = pb[i] || 0
872-
if (da > db) return 1
873-
if (da < db) return -1
966+
if (da > db)
967+
return 1
968+
if (da < db)
969+
return -1
874970
}
875971
return 0
876972
}
877973
const satisfies = (installed: string, constraint?: string): boolean => {
878-
if (!constraint || constraint === '*' || constraint === '') return true
974+
if (!constraint || constraint === '*' || constraint === '')
975+
return true
879976
const c = constraint.trim()
880977
const ver = installed.replace(/^v/, '')
881978
const [cOp, cVerRaw] = c.startsWith('^') || c.startsWith('~') ? [c[0], c.slice(1)] : ['', c]
@@ -888,22 +985,59 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
888985
if (cOp === '~') {
889986
return vParts[0] === cParts[0] && vParts[1] === cParts[1] && cmp >= 0
890987
}
891-
// exact or >= style: treat as minimum version
892-
return cmp >= 0
988+
// No operator provided: treat as exact pin
989+
return cmp === 0
893990
}
991+
const pinInfo: Array<{ domain: string, desired: string, installed: string }> = []
992+
// Extra-robust exact-pin check for well-known tools (like bun.sh)
993+
const exactPins: Array<{ domain: string, version: string }> = []
994+
try {
995+
for (const pkg of sniffResult.pkgs || []) {
996+
const domain = String(pkg.project)
997+
const constraint = String(typeof pkg.constraint === 'string' ? pkg.constraint : (pkg.constraint || ''))
998+
if (constraint && !constraint.startsWith('^') && !constraint.startsWith('~') && constraint !== '*') {
999+
exactPins.push({ domain, version: constraint.replace(/^v/, '') })
1000+
}
1001+
}
1002+
}
1003+
catch {}
8941004
const needsUpgrade = (() => {
8951005
try {
8961006
for (const pkg of sniffResult.pkgs || []) {
8971007
const domain = pkg.project as string
8981008
const constraint = typeof pkg.constraint === 'string' ? pkg.constraint : String(pkg.constraint || '*')
8991009
const domainDir = path.join(envDir, domain)
900-
if (!fs.existsSync(domainDir)) return true
1010+
if (!fs.existsSync(domainDir))
1011+
return true
9011012
const versions = fs.readdirSync(domainDir, { withFileTypes: true })
9021013
.filter(e => e.isDirectory() && e.name.startsWith('v'))
9031014
.map(e => e.name)
904-
if (versions.length === 0) return true
1015+
if (versions.length === 0)
1016+
return true
9051017
const highest = versions.sort((a, b) => semverCompare(a.slice(1), b.slice(1))).slice(-1)[0]
906-
if (!satisfies(highest, constraint)) return true
1018+
if (!satisfies(highest, constraint)) {
1019+
pinInfo.push({ domain, desired: constraint, installed: highest.replace(/^v/, '') })
1020+
return true
1021+
}
1022+
}
1023+
// Additionally, ensure exact pins are active (symlinks point to pinned version)
1024+
for (const pin of exactPins) {
1025+
if (pin.domain === 'bun.sh') {
1026+
const pinDir = path.join(envDir, 'bun.sh', `v${pin.version}`)
1027+
if (!fs.existsSync(pinDir))
1028+
return true
1029+
const bunBin = path.join(envDir, 'bin', 'bun')
1030+
try {
1031+
if (fs.existsSync(bunBin)) {
1032+
const target = fs.realpathSync(bunBin)
1033+
if (!target.includes(path.join('bun.sh', `v${pin.version}`, 'bin', 'bun')))
1034+
return true
1035+
}
1036+
}
1037+
catch {
1038+
return true
1039+
}
1040+
}
9071041
}
9081042
}
9091043
catch {}
@@ -912,6 +1046,13 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
9121046

9131047
// If constraints are not satisfied, fall back to install path
9141048
if (needsUpgrade) {
1049+
if (isVerbose && pinInfo.length > 0) {
1050+
try {
1051+
const details = pinInfo.map(p => `${p.domain}@${p.desired} (installed ${p.installed})`).join(', ')
1052+
process.stderr.write(`📌 Updating to satisfy pins: ${details}\n`)
1053+
}
1054+
catch {}
1055+
}
9151056
const envBinPath = path.join(envDir, 'bin')
9161057
const envSbinPath = path.join(envDir, 'sbin')
9171058
const globalBinPath = path.join(globalEnvDir, 'bin')
@@ -922,7 +1063,8 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
9221063
for (const pkg of sniffResult.pkgs) {
9231064
const constraintStr = typeof pkg.constraint === 'string' ? pkg.constraint : String(pkg.constraint || '*')
9241065
const packageString = `${pkg.project}@${constraintStr}`
925-
if (pkg.global && !skipGlobal) globalPackages.push(packageString)
1066+
if (pkg.global && !skipGlobal)
1067+
globalPackages.push(packageString)
9261068
else localPackages.push(packageString)
9271069
}
9281070
const tInstallFast = tick()
@@ -937,7 +1079,10 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
9371079
if (shellOutput && summary) {
9381080
const msg = summary.replace(/"/g, '\\"')
9391081
const guard = isVerbose ? 'true' : 'false'
940-
try { process.stdout.write(`if ${guard}; then >&2 echo "${msg}"; fi\n`) } catch {}
1082+
try {
1083+
process.stdout.write(`if ${guard}; then >&2 echo "${msg}"; fi\n`)
1084+
}
1085+
catch {}
9411086
}
9421087
}
9431088
return
@@ -1126,7 +1271,15 @@ export async function dump(dir: string, options: DumpOptions = {}): Promise<void
11261271

11271272
// Generate hash for this project
11281273
const projectHash = generateProjectHash(dir)
1129-
const envDir = path.join(process.env.HOME || '', '.local', 'share', 'launchpad', 'envs', projectHash)
1274+
// Compute dependency fingerprint to ensure env path reflects dependency versions
1275+
let depSuffix = ''
1276+
try {
1277+
const depContent = fs.readFileSync(dependencyFile)
1278+
const depHash = crypto.createHash('md5').update(depContent).digest('hex').slice(0, 8)
1279+
depSuffix = `-d${depHash}`
1280+
}
1281+
catch {}
1282+
const envDir = path.join(process.env.HOME || '', '.local', 'share', 'launchpad', 'envs', `${projectHash}${depSuffix}`)
11301283
const globalEnvDir = path.join(process.env.HOME || '', '.local', 'share', 'launchpad', 'global')
11311284

11321285
// For shell output mode, check if we can skip expensive operations
@@ -1531,7 +1684,27 @@ function outputShellCode(dir: string, envBinPath: string, envSbinPath: string, p
15311684
process.stdout.write(`fi\n`)
15321685

15331686
// Build PATH with both project and global environments
1534-
const pathComponents = []
1687+
const pathComponents: string[] = []
1688+
1689+
// If exact pins exist, prepend their bin directories ahead of generic env bin
1690+
try {
1691+
if (sniffResult && Array.isArray(sniffResult.pkgs)) {
1692+
const envRoot = fs.existsSync(envBinPath) ? path.dirname(envBinPath) : ''
1693+
for (const pkg of sniffResult.pkgs) {
1694+
const domain = String(pkg.project || '')
1695+
const constraint = String(typeof pkg.constraint === 'string' ? pkg.constraint : (pkg.constraint || ''))
1696+
if (!domain || !constraint || constraint === '*' || constraint.startsWith('^') || constraint.startsWith('~'))
1697+
continue
1698+
if (!envRoot)
1699+
continue
1700+
const pinnedBin = path.join(envRoot, domain, `v${constraint.replace(/^v/, '')}`, 'bin')
1701+
if (fs.existsSync(pinnedBin) && !pathComponents.includes(pinnedBin)) {
1702+
pathComponents.push(pinnedBin)
1703+
}
1704+
}
1705+
}
1706+
}
1707+
catch {}
15351708

15361709
// Add project-specific paths first (highest priority - can override global versions)
15371710
if (fs.existsSync(envBinPath)) {
@@ -1737,6 +1910,8 @@ function outputShellCode(dir: string, envBinPath: string, envSbinPath: string, p
17371910
process.stdout.write(` ;;\n`)
17381911
process.stdout.write(` esac\n`)
17391912
process.stdout.write(`}\n`)
1913+
// Refresh the command hash so version switches take effect immediately
1914+
process.stdout.write(`hash -r 2>/dev/null || true\n`)
17401915
}
17411916

17421917
/**

0 commit comments

Comments
 (0)