sparkbun 0.2.6 → 0.2.7
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/package.json +1 -1
- package/src/bun/core/Updater.ts +26 -11
- package/src/cli/index.ts +61 -13
- package/src/installer/installer-template.ts +33 -11
package/package.json
CHANGED
package/src/bun/core/Updater.ts
CHANGED
|
@@ -922,17 +922,16 @@ if %rmRetry% GEQ 10 goto rmfailed
|
|
|
922
922
|
timeout /t 2 /nobreak >nul
|
|
923
923
|
goto rmloop
|
|
924
924
|
:rmfailed
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
925
|
+
:: The script runs in a hidden window, so log instead of echo/pause (a hidden
|
|
926
|
+
:: pause would hang invisibly forever).
|
|
927
|
+
echo Update failed: could not remove "${runningAppWin}" after retries. Files may still be locked by a helper process. >> "%~dp0update-error.log"
|
|
928
928
|
exit /b 1
|
|
929
929
|
:rmdone
|
|
930
930
|
|
|
931
931
|
:: Move new app to current location (safe now that destination is gone)
|
|
932
932
|
move "${newAppWin}" "${runningAppWin}"
|
|
933
933
|
if not exist "${launcherPathWin}" (
|
|
934
|
-
echo Update failed: launcher not found at "${launcherPathWin}" after move.
|
|
935
|
-
pause
|
|
934
|
+
echo Update failed: launcher not found at "${launcherPathWin}" after move. >> "%~dp0update-error.log"
|
|
936
935
|
exit /b 1
|
|
937
936
|
)
|
|
938
937
|
|
|
@@ -947,24 +946,40 @@ for /f "tokens=1" %%t in ('schtasks /query /fo list ^| findstr /i "SparkBunUpdat
|
|
|
947
946
|
schtasks /delete /tn "%%t" /f >nul 2>&1
|
|
948
947
|
)
|
|
949
948
|
|
|
950
|
-
:: Delete this update script after a short delay
|
|
949
|
+
:: Delete this update script (and its hidden-window launcher) after a short delay
|
|
951
950
|
ping -n 2 127.0.0.1 >nul
|
|
951
|
+
del "%~dp0update.vbs" 2>nul
|
|
952
952
|
del "%~f0"
|
|
953
953
|
`;
|
|
954
954
|
|
|
955
955
|
await Bun.write(updateScriptPath, updateScript);
|
|
956
956
|
|
|
957
|
+
// VBS shim so the batch script runs with a HIDDEN window (style 0).
|
|
958
|
+
// schtasks can't hide a console app's window itself, and without
|
|
959
|
+
// this the whole update — wait loop included — sits in a visible
|
|
960
|
+
// cmd window. wscript.exe is a GUI-subsystem host, so nothing
|
|
961
|
+
// flashes. Errors are logged to update-error.log by the bat.
|
|
962
|
+
const scriptPathWin = updateScriptPath.replace(/\//g, "\\");
|
|
963
|
+
const vbsPath = join(parentDir, "update.vbs");
|
|
964
|
+
const vbsPathWin = vbsPath.replace(/\//g, "\\");
|
|
965
|
+
await Bun.write(
|
|
966
|
+
vbsPath,
|
|
967
|
+
`CreateObject("WScript.Shell").Run "cmd /c ""${scriptPathWin}""", 0, False\r\n`,
|
|
968
|
+
);
|
|
969
|
+
|
|
957
970
|
// Use Windows Task Scheduler to run the update script independently
|
|
958
971
|
// This ensures the script runs even after the app exits
|
|
959
|
-
const scriptPathWin = updateScriptPath.replace(/\//g, "\\");
|
|
960
972
|
const taskName = `SparkBunUpdate_${Date.now()}`;
|
|
961
973
|
|
|
962
|
-
// Create a scheduled task that runs immediately and deletes itself
|
|
974
|
+
// Create a scheduled task that runs immediately and deletes itself.
|
|
975
|
+
// windowsHide stops the schtasks invocations themselves from
|
|
976
|
+
// flashing console windows (the app is a GUI-subsystem process,
|
|
977
|
+
// so each unhidden child would allocate a fresh visible console).
|
|
963
978
|
execSync(
|
|
964
|
-
`schtasks /create /tn "${taskName}" /tr "
|
|
965
|
-
{ stdio: "ignore" },
|
|
979
|
+
`schtasks /create /tn "${taskName}" /tr "wscript.exe //B \\"${vbsPathWin}\\"" /sc once /st 00:00 /f`,
|
|
980
|
+
{ stdio: "ignore", windowsHide: true },
|
|
966
981
|
);
|
|
967
|
-
execSync(`schtasks /run /tn "${taskName}"`, { stdio: "ignore" });
|
|
982
|
+
execSync(`schtasks /run /tn "${taskName}"`, { stdio: "ignore", windowsHide: true });
|
|
968
983
|
// The task will be cleaned up by Windows after it runs, or we delete it in the batch script
|
|
969
984
|
|
|
970
985
|
// Use quit() for graceful shutdown - this closes all windows and processes
|
package/src/cli/index.ts
CHANGED
|
@@ -1313,6 +1313,30 @@ function escapeXml(str: string): string {
|
|
|
1313
1313
|
.replace(/'/g, "'");
|
|
1314
1314
|
}
|
|
1315
1315
|
|
|
1316
|
+
/**
|
|
1317
|
+
* Flip the PE subsystem from CONSOLE (3) to WINDOWS GUI (2) so the exe never
|
|
1318
|
+
* opens a console window. Byte-identical to `editbin /SUBSYSTEM:WINDOWS`.
|
|
1319
|
+
*
|
|
1320
|
+
* This exists because Bun's `compile.windows.hideConsole` is a silent no-op:
|
|
1321
|
+
* the only code that applies it sits after a switch in which every prong
|
|
1322
|
+
* returns early (oven-sh/bun#19916; fix PR oven-sh/bun#20338 unmerged as of
|
|
1323
|
+
* Bun 1.3.14). Bun's intended implementation writes the exact same byte at the
|
|
1324
|
+
* exact same offset, so when upstream merges the fix, `hideConsole: true`
|
|
1325
|
+
* takes over and this patch degrades to a no-op (the `current !== 2` check).
|
|
1326
|
+
*
|
|
1327
|
+
* Unlike Bun's flag, this also works when cross-compiling from macOS/Linux
|
|
1328
|
+
* (Bun only applies windows options when the build host is Windows).
|
|
1329
|
+
*
|
|
1330
|
+
* Must run AFTER all other PE edits (payload injection, rescle metadata) and
|
|
1331
|
+
* BEFORE any future code signing — mutating a signed exe invalidates the
|
|
1332
|
+
* signature.
|
|
1333
|
+
*
|
|
1334
|
+
* Historical note: a May 2026 commit (cf58be7) blamed hideConsole for breaking
|
|
1335
|
+
* the ShellExecuteW("runas") elevation flow. Since hideConsole never touched
|
|
1336
|
+
* the binary, that breakage came from elsewhere (likely the experimental
|
|
1337
|
+
* signature-stripping variant of this function that was being tested at the
|
|
1338
|
+
* same time and was reverted in the same commit).
|
|
1339
|
+
*/
|
|
1316
1340
|
function patchPeSubsystem(exePath: string): void {
|
|
1317
1341
|
const buf = readFileSync(exePath);
|
|
1318
1342
|
const peOffset = buf.readUInt32LE(0x3c);
|
|
@@ -2144,8 +2168,9 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
2144
2168
|
if (targetOS === "win") {
|
|
2145
2169
|
if (OS !== "win") {
|
|
2146
2170
|
console.warn(
|
|
2147
|
-
`\n⚠️ Cross-compiling for Windows: icon
|
|
2171
|
+
`\n⚠️ Cross-compiling for Windows: icon and PE metadata (title, version, publisher, etc.) will be ignored.\n` +
|
|
2148
2172
|
` Bun's Windows-specific compile options require building on a Windows host.\n` +
|
|
2173
|
+
` (The console window is still hidden — the PE subsystem patch runs on any host.)\n` +
|
|
2149
2174
|
` See: https://bun.com/docs/bundler/executables#windows-specific-flags\n`
|
|
2150
2175
|
);
|
|
2151
2176
|
} else {
|
|
@@ -3177,6 +3202,8 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3177
3202
|
hash,
|
|
3178
3203
|
config,
|
|
3179
3204
|
projectRoot,
|
|
3205
|
+
targetOS,
|
|
3206
|
+
currentTarget.arch,
|
|
3180
3207
|
);
|
|
3181
3208
|
artifactsToUpload.push(installerPath);
|
|
3182
3209
|
}
|
|
@@ -3199,8 +3226,10 @@ usageDescriptions : ""}${urlTypes ? "\n" + urlTypes : ""}${documentTypes ?
|
|
|
3199
3226
|
// the download button or display on your marketing site or in the app.
|
|
3200
3227
|
version: config.app.version,
|
|
3201
3228
|
hash: hash.toString(),
|
|
3202
|
-
platform
|
|
3203
|
-
|
|
3229
|
+
// The TARGET platform of these artifacts (host != target when
|
|
3230
|
+
// cross-compiling, e.g. --target=win-x64 on macOS).
|
|
3231
|
+
platform: targetOS,
|
|
3232
|
+
arch: currentTarget.arch,
|
|
3204
3233
|
// channel: buildEnvironment,
|
|
3205
3234
|
// baseUrl: config.release.baseUrl
|
|
3206
3235
|
});
|
|
@@ -3681,9 +3710,14 @@ ${archiveExports.join("\n")}
|
|
|
3681
3710
|
}
|
|
3682
3711
|
}
|
|
3683
3712
|
installerCompileOptions.windows = {
|
|
3684
|
-
//
|
|
3685
|
-
//
|
|
3686
|
-
//
|
|
3713
|
+
// Currently a no-op in Bun (oven-sh/bun#19916 — the subsystem edit is
|
|
3714
|
+
// unreachable code; fix PR #20338 unmerged as of 1.3.14), so the
|
|
3715
|
+
// console is actually hidden by patchPeSubsystem() below. Set it
|
|
3716
|
+
// anyway: it's harmless today and becomes the primary mechanism the
|
|
3717
|
+
// day Bun fixes it (the patch then no-ops). See patchPeSubsystem's
|
|
3718
|
+
// doc comment for why the old "conflicts with elevation" story here
|
|
3719
|
+
// was a misattribution.
|
|
3720
|
+
hideConsole: true,
|
|
3687
3721
|
...(icoPath && { icon: icoPath }),
|
|
3688
3722
|
title: `${installerName} Setup`,
|
|
3689
3723
|
version: installerVersion,
|
|
@@ -4318,9 +4352,16 @@ ${archiveExports.join("\n")}
|
|
|
4318
4352
|
hash: string,
|
|
4319
4353
|
config: any,
|
|
4320
4354
|
projectRoot: string,
|
|
4355
|
+
// The TARGET platform being built for — not the build host. These used to
|
|
4356
|
+
// be derived from the host OS/ARCH globals, so cross-compiling (e.g.
|
|
4357
|
+
// `--target=win-x64` on macOS) wrongly built a host-platform installer
|
|
4358
|
+
// whose extensionless outfile then collided with the app bundle directory
|
|
4359
|
+
// ("is a directory" build failure).
|
|
4360
|
+
targetOS: "win" | "linux" | "macos",
|
|
4361
|
+
targetArch: string,
|
|
4321
4362
|
): Promise<string> {
|
|
4322
|
-
const targetOSName =
|
|
4323
|
-
const isWindows =
|
|
4363
|
+
const targetOSName = targetOS === "macos" ? "darwin" : targetOS === "win" ? "windows" : "linux";
|
|
4364
|
+
const isWindows = targetOS === "win";
|
|
4324
4365
|
|
|
4325
4366
|
const setupFileName = isWindows
|
|
4326
4367
|
? getWindowsSetupFileName(config.app.name, buildEnvironment)
|
|
@@ -4337,9 +4378,9 @@ ${archiveExports.join("\n")}
|
|
|
4337
4378
|
// Copy archive
|
|
4338
4379
|
copyFileSync(compressedTarPath, join(stagingDir, "app-archive.tar.gz"));
|
|
4339
4380
|
|
|
4340
|
-
// Write metadata
|
|
4341
|
-
const platformConfig =
|
|
4342
|
-
:
|
|
4381
|
+
// Write metadata (platform config of the TARGET, not the build host)
|
|
4382
|
+
const platformConfig = targetOS === "macos" ? config.build?.mac
|
|
4383
|
+
: targetOS === "win" ? config.build?.win
|
|
4343
4384
|
: config.build?.linux;
|
|
4344
4385
|
const metadata = {
|
|
4345
4386
|
identifier: config.app.identifier,
|
|
@@ -4356,11 +4397,18 @@ ${archiveExports.join("\n")}
|
|
|
4356
4397
|
copyFileSync(templatePath, join(stagingDir, "installer.ts"));
|
|
4357
4398
|
|
|
4358
4399
|
const installerCompileOptions: any = {
|
|
4359
|
-
target: `bun-${targetOSName}-${
|
|
4400
|
+
target: `bun-${targetOSName}-${targetArch}`,
|
|
4360
4401
|
outfile: outputPath,
|
|
4361
4402
|
};
|
|
4362
4403
|
|
|
4363
|
-
if (isWindows) {
|
|
4404
|
+
if (isWindows && OS !== "win") {
|
|
4405
|
+
// Bun only applies windows compile options on a Windows host; the
|
|
4406
|
+
// console is still hidden by patchPeSubsystem below (works anywhere).
|
|
4407
|
+
console.warn(
|
|
4408
|
+
`\n⚠️ Cross-compiling the Windows installer: icon and PE metadata (title, version, publisher, etc.) will be ignored.\n` +
|
|
4409
|
+
` Build on a Windows host if the installer exe needs them.\n`
|
|
4410
|
+
);
|
|
4411
|
+
} else if (isWindows) {
|
|
4364
4412
|
let icoPath: string | undefined;
|
|
4365
4413
|
if (config.build.win?.icon) {
|
|
4366
4414
|
const iconSrc = config.build.win.icon.startsWith("/") || config.build.win.icon.match(/^[a-zA-Z]:/)
|
|
@@ -92,6 +92,36 @@ function getInstallDir(meta: Metadata): string {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Create a .lnk via WScript.Shell. windowsHide is essential: the installer is a
|
|
96
|
+
// GUI-subsystem exe, so without it each PowerShell child allocates its own
|
|
97
|
+
// visible console window mid-install. PowerShell is called by absolute path
|
|
98
|
+
// (PATH isn't reliable in every launch context), and the paths are passed
|
|
99
|
+
// through the environment ($env:SB_*) rather than interpolated into the script
|
|
100
|
+
// text, so an app name containing quotes can't break or inject into the command.
|
|
101
|
+
function createShortcut(lnkPath: string, targetPath: string, workingDir: string) {
|
|
102
|
+
const systemRoot = process.env.SystemRoot || process.env.windir || "C:\\Windows";
|
|
103
|
+
const powershell = join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
104
|
+
const script =
|
|
105
|
+
"$ws = New-Object -ComObject WScript.Shell;" +
|
|
106
|
+
" $s = $ws.CreateShortcut($env:SB_LNK);" +
|
|
107
|
+
" $s.TargetPath = $env:SB_TARGET;" +
|
|
108
|
+
" $s.WorkingDirectory = $env:SB_WORKDIR;" +
|
|
109
|
+
" $s.Save()";
|
|
110
|
+
try {
|
|
111
|
+
Bun.spawnSync([powershell, "-NoProfile", "-Command", script], {
|
|
112
|
+
stdout: "ignore",
|
|
113
|
+
stderr: "ignore",
|
|
114
|
+
windowsHide: true,
|
|
115
|
+
env: {
|
|
116
|
+
...process.env,
|
|
117
|
+
SB_LNK: lnkPath,
|
|
118
|
+
SB_TARGET: targetPath,
|
|
119
|
+
SB_WORKDIR: workingDir,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
|
|
95
125
|
async function createWindowsShortcuts(appDir: string, meta: Metadata) {
|
|
96
126
|
const binDir = join(appDir, "bin");
|
|
97
127
|
const exePath = join(binDir, `${meta.name.replace(/ /g, "")}.exe`);
|
|
@@ -100,19 +130,11 @@ async function createWindowsShortcuts(appDir: string, meta: Metadata) {
|
|
|
100
130
|
|
|
101
131
|
// Start Menu shortcut
|
|
102
132
|
const startMenu = join(process.env.APPDATA || "", "Microsoft", "Windows", "Start Menu", "Programs");
|
|
103
|
-
|
|
104
|
-
Bun.spawnSync(["powershell", "-NoProfile", "-Command",
|
|
105
|
-
`$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('${join(startMenu, meta.name + ".lnk")}'); $s.TargetPath = '${exePath}'; $s.WorkingDirectory = '${binDir}'; $s.Save()`
|
|
106
|
-
]);
|
|
107
|
-
} catch {}
|
|
133
|
+
createShortcut(join(startMenu, meta.name + ".lnk"), exePath, binDir);
|
|
108
134
|
|
|
109
135
|
// Desktop shortcut
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Bun.spawnSync(["powershell", "-NoProfile", "-Command",
|
|
113
|
-
`$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('${join(desktop, meta.name + ".lnk")}'); $s.TargetPath = '${exePath}'; $s.WorkingDirectory = '${binDir}'; $s.Save()`
|
|
114
|
-
]);
|
|
115
|
-
} catch {}
|
|
136
|
+
const desktop = join(process.env.USERPROFILE || "", "Desktop");
|
|
137
|
+
createShortcut(join(desktop, meta.name + ".lnk"), exePath, binDir);
|
|
116
138
|
|
|
117
139
|
console.log("Created Start Menu and Desktop shortcuts");
|
|
118
140
|
}
|