Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
4d541de
Add curl installer scripts for standalone CLI distribution
bcotrim Apr 10, 2026
f77f9c4
Convert create-standalone-bundle from shell to TypeScript for cross-p…
bcotrim Apr 13, 2026
3fab289
Add cli:bundle npm script for standalone bundle creation
bcotrim Apr 13, 2026
2c988a6
Preserve config files during install (don't wipe ~/.studio)
bcotrim Apr 13, 2026
bac4423
Merge branch 'trunk' into stu-1525-add-curl-installer
bcotrim Apr 13, 2026
d8447c4
Use tar.gz for all platforms instead of zip for Windows
bcotrim Apr 13, 2026
7f43d56
Detect standalone CLI on Windows to prevent desktop app from overwrit…
bcotrim Apr 13, 2026
24fef1b
Merge branch 'trunk' into stu-1525-add-curl-installer
bcotrim Apr 13, 2026
da3ff4e
Simplify bundle script and fix Windows installer extraction
bcotrim Apr 13, 2026
6e97c91
Merge branch 'trunk' into stu-1525-add-curl-installer
bcotrim Apr 13, 2026
36d8cb6
Bundle CLI as a single executable binary for standalone and app distr…
bcotrim Apr 14, 2026
1c55a5c
Remove @yao-pkg/pkg and unused CLI launcher script
bcotrim Apr 14, 2026
100c9cd
Merge branch 'trunk' into stu-1525-add-curl-installer
bcotrim Apr 14, 2026
49a2c89
Fix lint errors in bundle entry point
bcotrim Apr 14, 2026
bfa005e
Clean up packaged bin directory and remove unused node binaries
bcotrim Apr 14, 2026
df41f34
Fix Windows tar path handling and clean up packaged bin directory
bcotrim Apr 14, 2026
4b9bdd2
Add --force-local to tar commands for Windows compatibility
bcotrim Apr 14, 2026
878c104
Only use --force-local tar flag on Windows (BSD tar doesn't support it)
bcotrim Apr 14, 2026
e52247c
Merge branch 'trunk' into stu-1525-add-curl-installer
bcotrim Apr 14, 2026
24fa120
Externalize trash package to fix site deletion in bundled binary
bcotrim Apr 14, 2026
570bfd6
Harden standalone CLI extraction: lockfile, auto-derived bundle-versi…
bcotrim Apr 17, 2026
9a20492
Verify SHA-256 checksum and tighten PATH matching in curl installers
bcotrim Apr 17, 2026
274ebde
Return nullable CLI binary path, handle Windows junction, add paths test
bcotrim Apr 17, 2026
801c67f
Merge branch 'trunk' into stu-1525-add-curl-installer
bcotrim Apr 20, 2026
1490d7c
Merge branch 'trunk' into stu-1525-add-curl-installer
bcotrim Apr 22, 2026
2d7dc87
Drop paths.test.ts per review feedback
bcotrim Apr 22, 2026
0e3735c
Pass --experimental-wasm-jspi when spawning bundled CLI binary
bcotrim Apr 22, 2026
fbbf904
Revert --experimental-wasm-jspi on SEA binary: flag can't be enabled …
bcotrim Apr 22, 2026
33b30aa
Drop unused --force-local tar flag (Windows bsdtar rejects it) and fi…
bcotrim Apr 22, 2026
a459ba0
Drive tar via stdio to avoid GNU tar mistaking 'C:' for a remote host
bcotrim Apr 22, 2026
b5bb876
Suggestion: build a single Vite file
fredrikekelund Apr 22, 2026
9ae2662
Rework SEA assets: embed main.mjs raw, split resources and node_modul…
bcotrim Apr 22, 2026
e534c6b
Remove redundant node_modules symlink from dev vite config
bcotrim Apr 22, 2026
95059d0
Update Windows launcher to detect bundled SEA binary
bcotrim Apr 22, 2026
78802e7
Use CLI package.json version as bundle marker instead of asset hash
bcotrim Apr 22, 2026
8880bdb
Download Node binary into bundle build dir instead of shipping bin/
bcotrim Apr 22, 2026
03e42b4
Merge branch 'trunk' into stu-1525-add-curl-installer
bcotrim Apr 23, 2026
df0d165
Await writeStream close in wpress extractor to fix CI flake
bcotrim Apr 23, 2026
1ffb582
Merge remote-tracking branch 'origin/trunk' into stu-1525-add-curl-in…
bcotrim Jun 2, 2026
569bbae
Fix standalone CLI sidecar bundling
bcotrim Jun 2, 2026
ff77dfb
Update E2E CLI artifact verification
bcotrim Jun 2, 2026
8f275ec
Merge trunk into stu-1525-add-curl-installer
bcotrim Jun 3, 2026
6b83c0a
Verify packaged CLI binary in E2E
bcotrim Jun 3, 2026
f54a266
Run desktop CLI via the bundled binary and harden first-run extraction
bcotrim Jun 3, 2026
8145e58
Merge remote-tracking branch 'origin/trunk' into stu-1525-add-curl-in…
bcotrim Jun 3, 2026
83adda9
Fix CLI review findings: postinst binary, standalone coexistence, ato…
bcotrim Jun 3, 2026
31d85f9
Treat external CLI as not app-managed; prevent partial/unverified CLI…
bcotrim Jun 3, 2026
daa3a09
Merge trunk into standalone CLI installer branch
bcotrim Jun 8, 2026
ea6055e
Merge trunk into standalone CLI installer branch
bcotrim Jun 9, 2026
91b8534
Merge branch 'trunk' into stu-1525-add-curl-installer
bcotrim Jun 9, 2026
f53e2ef
Add explicit esbuild dev dependency
bcotrim Jun 9, 2026
6ccd094
Merge remote-tracking branch 'origin/stu-1525-add-curl-installer' int…
bcotrim Jun 9, 2026
7571a7b
Address standalone CLI installer review feedback
bcotrim Jun 9, 2026
9eaa109
Merge branch 'trunk' into stu-1525-add-curl-installer
bcotrim Jun 9, 2026
92d8bc7
Externalize zod in standalone CLI bundle
bcotrim Jun 9, 2026
b7db5e6
Merge remote-tracking branch 'origin/stu-1525-add-curl-installer' int…
bcotrim Jun 9, 2026
eb3ed7a
Dispatch internal CLI commands before the yargs pipeline
bcotrim Jun 10, 2026
af45e69
Only accept main.mjs as first non-flag argument in child-mode detection
bcotrim Jun 10, 2026
4bad9d1
Report standalone CLI installs as installed on macOS and Linux
bcotrim Jun 10, 2026
47cc350
Preserve unexpanded user PATH entries in install.ps1
bcotrim Jun 10, 2026
8a15143
Declare require shim with var to fix dev CLI build
bcotrim Jun 10, 2026
29a6a4f
Remove dead dev fallback path and align child start guard
bcotrim Jun 10, 2026
49350b2
Merge trunk into stu-1525-add-curl-installer: port AI provider SDK pr…
bcotrim Jun 10, 2026
01563f4
Merge branch 'trunk' into stu-1525-add-curl-installer
fredrikekelund Jun 10, 2026
43f08a8
Rely on `module.builtinModules`
fredrikekelund Jun 11, 2026
bc7c7fb
Revert CLI bundling from Node SEA binary to bundled node + files
bcotrim Jun 11, 2026
71a6483
Distribute standalone CLI as a tarball with bundled node and launcher…
bcotrim Jun 11, 2026
c3e76d0
Detect standalone CLI on Windows via launcher and bundled node
bcotrim Jun 11, 2026
ffcb203
Merge remote-tracking branch 'origin/trunk' into stu-1525-add-curl-in…
bcotrim Jun 11, 2026
e5343e9
Restore trunk files dropped by trunk merge resolution
bcotrim Jun 11, 2026
cebb9fc
Revert single-file CLI build and internal command consolidation
bcotrim Jun 11, 2026
09fb44e
Drop explicit esbuild dev dependency restored transitively via @yao-p…
bcotrim Jun 11, 2026
a8a2748
Merge remote-tracking branch 'origin/trunk' into stu-1525-add-curl-in…
bcotrim Jun 11, 2026
8f62553
Drop packaged CLI smoke from e2e script, desktop bundling is unchanged
bcotrim Jun 11, 2026
ff98eab
Reclaim broken studio symlinks instead of treating them as externally…
bcotrim Jun 11, 2026
aa03891
Parameterize chrome-sandbox removal with ARCH in e2e script
bcotrim Jun 11, 2026
ad285dd
Reuse desktop CLI launcher scripts for the standalone bundle
bcotrim Jun 11, 2026
f4cc7d4
Stop a running standalone CLI before reinstalling over it
bcotrim Jun 15, 2026
ccc9916
Import download-node-binary directly in the standalone bundle script
bcotrim Jun 15, 2026
e23ebc1
Document PowerShell execution-policy bypass for local install testing
bcotrim Jun 15, 2026
15b1272
Merge branch 'trunk' into stu-1525-add-curl-installer
bcotrim Jun 15, 2026
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .buildkite/commands/run-e2e-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ if [ "$PLATFORM" = "linux" ]; then
# and Chromium aborts on the misconfigured helper. Removing the helper
# makes Chromium skip the SUID path and fall through to the user-
# namespace sandbox, which doesn't need setuid.
rm -f apps/studio/out/Studio-linux-x64/chrome-sandbox
rm -f "apps/studio/out/Studio-linux-${ARCH}/chrome-sandbox"

# Grant cap_net_bind_service to the bundled node so the proxy daemon can
# listen on privileged ports 80/443 without running as root — mirrors the
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Standalone CLI bundles
standalone-bundles/

# Logs
logs
*.log
Expand Down
42 changes: 42 additions & 0 deletions apps/studio/src/modules/cli/lib/linux-installation-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ export class LinuxCliInstallationManager implements StudioCliInstallationManager
return false;
}

// A standalone (curl) install is managed outside the app: report it as
// installed — matching the Windows manager — so the UI doesn't offer an
// install that would silently no-op. We never install over or uninstall it.
if ( await this.isExternallyManagedCli( cliSymlinkPath ) ) {
return true;
}

return await this.doesSymlinkLeadToPackagedCli( cliSymlinkPath );
}

Expand Down Expand Up @@ -155,6 +162,12 @@ export class LinuxCliInstallationManager implements StudioCliInstallationManager
}
}

// Never overwrite a CLI the app didn't install — e.g. a standalone curl
// install symlinking studio at <STUDIO_CLI_HOME>/bin/studio (any location).
if ( await this.isExternallyManagedCli( cliSymlinkPath ) ) {
return;
}

if ( await this.isCliInstalled() ) {
return;
}
Expand Down Expand Up @@ -188,6 +201,11 @@ export class LinuxCliInstallationManager implements StudioCliInstallationManager
throw error;
}

// Don't remove a CLI the app didn't install (e.g. a standalone curl install).
if ( await this.isExternallyManagedCli( cliSymlinkPath ) ) {
return;
}

await fs.promises.unlink( cliSymlinkPath );
}

Expand Down Expand Up @@ -226,6 +244,30 @@ export class LinuxCliInstallationManager implements StudioCliInstallationManager
}
}

private async isExternallyManagedCli( symlinkPath: string ): Promise< boolean > {
let isSymlink = false;
try {
isSymlink = ( await fs.promises.lstat( symlinkPath ) ).isSymbolicLink();
} catch {
return false;
}
if ( ! isSymlink ) {
return false;
}
// A dangling symlink (e.g. a standalone install that was deleted without
// removing the link) is broken, not externally managed — reclaim it so a
// reinstall can repair `studio` instead of refusing to touch it.
try {
await fs.promises.stat( symlinkPath );
} catch {
return false;
}
// A symlink that doesn't point at our packaged CLI is managed outside the
// app (e.g. a standalone curl install at <STUDIO_CLI_HOME>/bin/studio, any
// location). Treat it as installed and never overwrite it.
return ! ( await this.doesSymlinkLeadToPackagedCli( symlinkPath ) );
}

private async doesSymlinkLeadToPackagedCli( symlinkPath: string ): Promise< boolean > {
try {
const symlinkDestination = await fs.promises.readlink( symlinkPath );
Expand Down
42 changes: 42 additions & 0 deletions apps/studio/src/modules/cli/lib/macos-installation-manager.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can tackle this in a follow-up PR, but these changes will make it so the Studio CLI for terminal toggle in the app's settings modal is always enabled if the standalone CLI installed. If the user tries to uninstall it, it would silently fail.

I'd argue the ideal behavior would be to disable the UI toggle when the CLI is externally managed.

Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ export class MacOSCliInstallationManager implements StudioCliInstallationManager
return false;
}

// A standalone (curl) install is managed outside the app: report it as
// installed — matching the Windows manager — so the UI doesn't offer an
// install that would silently no-op. We never install over or uninstall it.
if ( await this.isExternallyManagedCli( cliSymlinkPath ) ) {
return true;
}

return await this.doesSymlinkLeadToPackagedCli( cliSymlinkPath );
}

Expand Down Expand Up @@ -158,6 +165,12 @@ export class MacOSCliInstallationManager implements StudioCliInstallationManager
}
}

// Never overwrite a CLI the app didn't install — e.g. a standalone curl
// install symlinking studio at <STUDIO_CLI_HOME>/bin/studio (any location).
if ( await this.isExternallyManagedCli( cliSymlinkPath ) ) {
return;
}

if ( await this.isCliInstalled() ) {
return;
}
Expand Down Expand Up @@ -192,6 +205,11 @@ export class MacOSCliInstallationManager implements StudioCliInstallationManager
throw error;
}

// Don't remove a CLI the app didn't install (e.g. a standalone curl install).
if ( await this.isExternallyManagedCli( cliSymlinkPath ) ) {
return;
}

await fs.promises.unlink( cliSymlinkPath );
}

Expand Down Expand Up @@ -250,6 +268,30 @@ export class MacOSCliInstallationManager implements StudioCliInstallationManager
}
}

private async isExternallyManagedCli( symlinkPath: string ): Promise< boolean > {
let isSymlink = false;
try {
isSymlink = ( await fs.promises.lstat( symlinkPath ) ).isSymbolicLink();
} catch {
return false;
}
if ( ! isSymlink ) {
return false;
}
// A dangling symlink (e.g. a standalone install that was deleted without
// removing the link) is broken, not externally managed — reclaim it so a
// reinstall can repair `studio` instead of refusing to touch it.
try {
await fs.promises.stat( symlinkPath );
} catch {
return false;
}
// A symlink that doesn't point at our packaged CLI is managed outside the
// app (e.g. a standalone curl install at <STUDIO_CLI_HOME>/bin/studio, any
// location). Treat it as installed and never overwrite it.
return ! ( await this.doesSymlinkLeadToPackagedCli( symlinkPath ) );
}

private async doesSymlinkLeadToPackagedCli( symlinkPath: string ): Promise< boolean > {
try {
const symlinkDestination = await fs.promises.readlink( symlinkPath );
Expand Down
37 changes: 37 additions & 0 deletions apps/studio/src/modules/cli/lib/windows-installation-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { loadUserData, updateAppdata } from 'src/storage/user-data';

// `STABLE_BIN_DIR_PATH` resolves to C:\Users\<USERNAME>\AppData\Local\studio\bin
export const STABLE_BIN_DIR_PATH = path.resolve( path.dirname( app.getPath( 'exe' ) ), '../bin' );

const PATH_KEY = 'Path';

const currentUserRegistry = new Registry( {
Expand All @@ -27,9 +28,13 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag

/**
* Check if the stable bin directory has been created and if it's contained in the registry PATH.
* Also detects standalone CLI installed via install.ps1.
*/
async isCliInstalled(): Promise< boolean > {
try {
if ( await this.isStandaloneCli() ) {
return true;
}
const isStudioCliDirInPath = await this.isStudioCliDirInPath();
return isStudioCliDirInPath && existsSync( STABLE_BIN_DIR_PATH );
} catch ( error ) {
Expand Down Expand Up @@ -207,7 +212,39 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag
}
}

/**
* Check if a standalone CLI (installed via install.ps1) is present.
* Detects the standalone launcher and its bundled Node binary in the
* default or custom install path. Requiring both files means a broken
* install (e.g. a launcher without its runtime) is not treated as
* standalone, so the app can still install its own CLI.
*/
private async isStandaloneCli(): Promise< boolean > {
const currentPath = await this.getPathFromRegistry();
const pathDirs = currentPath
.split( ';' )
.map( ( item ) => item.trim() )
.filter( Boolean );

for ( const dir of pathDirs ) {
// Skip the app's own bin directory
if ( dir.toLowerCase() === STABLE_BIN_DIR_PATH.toLowerCase() ) {
continue;
}
const launcherPath = path.join( dir, 'studio.cmd' );
const bundledNodePath = path.join( dir, 'node.exe' );
if ( existsSync( launcherPath ) && existsSync( bundledNodePath ) ) {
return true;
}
}
return false;
}

private async installCli(): Promise< void > {
// Don't overwrite standalone CLI installed via install.ps1
if ( await this.isStandaloneCli() ) {
return;
}
await this.installPath();
await this.installProxyBatFile();
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"cli:build:prod": "npm -w wp-studio run build:prod",
"cli:build:npm": "npm -w wp-studio run build:npm",
"cli:package": "npm -w wp-studio run package",
"cli:bundle": "tsx scripts/create-standalone-bundle.ts",
"cli:watch": "npm -w wp-studio run watch",
"app:install:bundle": "npm -w studio-app run install:bundle",
"compare:perf": "npm -w compare-perf run compare",
Expand Down
Loading