This guide is for contributors who want to build the rushstack repository with the opt-in Rush daemon
(rush-client, from apps/rush-cli-client) before a published
Rush release contains it. Ordinary rush, rushx, and CI are unaffected: they keep running in-process
unless you opt in explicitly.
Nothing described here is published yet, so the client is built from source. The daemon must not run
directly from the workspace's build outputs: rebuilding @microsoft/rush-lib, @rushstack/rush-daemon, or
@rushstack/rush-cli-client would replace modules that the long-lived daemon is executing. Instead,
rush deploy extracts a stable, self-contained snapshot of the built client closure into the gitignored
common/temp/rush-daemon-dogfood folder, and the daemon runs from that snapshot.
- A supported Node.js version (see
nodeSupportedVersionRangeinrush.json) and Git. - A completed install:
node common/scripts/install-run-rush.js install. - No
.envfile in the repository root or in~/.rush-user/. The daemon does not support.envinitialization and would fall back to in-process Rush. - The source-built Rush version must equal the repository's
rushVersion. Compare the output ofnode -p "require('./apps/rush/package.json').version"with therushVersionfield inrush.json. If they differ, see Version skew.
Do not export
RUSH_DAEMONin a shell that also runs ordinaryrush. The published Rush release thatrush.jsoncurrently selects rejects unrecognizedRUSH_environment variables, sonode common/scripts/install-run-rush.js ...fails withThe following environment variables were found with the "RUSH_" prefix, but they are not recognized. The shell functions below setRUSH_DAEMON=1only forrush-clientinvocations.Do not add a
daemonblock torush.jsonyet. The published schema rejects it, which would break ordinaryrushand CI. Opting in with the environment variable is sufficient.
node common/scripts/install-run-rush.js build --to @rushstack/rush-cli-clientIf a daemon from an earlier snapshot is running, stop it first (see Refresh).
Then extract the snapshot with the rush-daemon-dogfood deployment scenario
(common/config/rush/deploy-rush-daemon-dogfood.json):
node common/scripts/install-run-rush.js deploy --scenario rush-daemon-dogfood --target-folder common/temp/rush-daemon-dogfood --overwriteThe snapshot contains copies of the built Rush projects and their npm dependencies, with links that stay
inside the snapshot. To confirm that the client resolves its engine from the snapshot rather than from the
workspace, run the following from the repository root. It must print a path under
common/temp/rush-daemon-dogfood:
node -p "require('fs').realpathSync(require.resolve('@microsoft/rush-lib', { paths: [require('path').resolve('common/temp/rush-daemon-dogfood/apps/rush-cli-client')] }))"The snapshot also contains Rush's built-in cloud build-cache plugins (amazon-s3, azure-blob-storage and
http), next to the @microsoft/rush-lib links of @microsoft/rush, rush-client and the daemon. A source-built rush-lib loads them
from there, and it points _RUSH_LIB_PATH at that same link. So plugins that resolve @microsoft/rush-lib by name
from _RUSH_LIB_PATH work in both the daemon and the in-process fallback. This repository's own build cache is
local-only, so it doesn't need them.
Define a rush-client function in the terminal you will use, from the repository root.
Bash:
DOGFOOD_SNAPSHOT="$PWD/common/temp/rush-daemon-dogfood"
rush-client() { RUSH_DAEMON=1 node "$DOGFOOD_SNAPSHOT/apps/rush-cli-client/bin/rush-client" "$@"; }PowerShell:
$DogfoodSnapshot = "$PWD\common\temp\rush-daemon-dogfood"
function rush-client {
$env:RUSH_DAEMON = '1'
try { node "$DogfoodSnapshot\apps\rush-cli-client\bin\rush-client" @args } finally { Remove-Item Env:RUSH_DAEMON }
}Then build as usual. The first request starts the daemon and constructs the all-project graph; later compatible requests from the same terminal reuse it:
rush-client build --to @rushstack/tree-pattern
rush-client build --to @rushstack/tree-patternAny project selection that rush build accepts works, including projects in the build-tests-subspace
subspace. rush-client rebuild is also supported, as are the phased commands of command-line.json
(for example rush-client test). Commands share the warm graph where they can: a test graph also serves
build and rebuild, and a build graph serves rebuild. The first rush-client test after a build reloads
the graph once; later builds reuse the test graph. A custom command whose graph could not serve the current
graph's command either, such as retest (which is not incremental) after a build, runs in-process instead, so
that the two commands don't reload the graph on every switch. With daemon.usePersistentIpcRunners, a
command that is not incremental never runs on another command's graph, because persistent IPC runners serve
only incremental commands. After a rush-client test, for example, rebuild reloads the graph and retest
runs in-process.
Enabling the daemon is not proof that a build used it: unsupported requests silently or explicitly use in-process Rush. Check all of the following:
- No fallback. When the client does not use the daemon, it runs native Rush, which prints the
Rush Multi-Project Build Toolbanner. If the daemon was selected but could not serve the request, stderr also contains a line starting withrush-client:and ending withusing in-process Rush.A daemon-served build prints neither. - A warm graph.
rush-client daemon statusprints one JSON object.workspace.graphInitializedmust betrue. - The same process.
pidandworkspace.generationTokenstay the same across requests from the same terminal.workspace.lastReloadTieris0when the request reused the existing graph, and1after an in-process reload (for example, the first request, or a configuration change). The command line of thatpidrunsSelectedDaemonBootstrap.jsfromcommon/temp/rush-daemon-dogfood, not from the workspace. - Parity. For the same selection, ordinary Rush produces the same outputs. For example, after a
daemon-served build,
node common/scripts/install-run-rush.js rebuild --only @rushstack/tree-patternleaves the files inlib-commonjs,lib-dts,lib-esm, anddistunchanged.
A warm request whose selected operations are all up to date finishes without printing anything, and exits
with code 0. Unlike ordinary Rush, the daemon client does not yet print a summary, so "no output" after a
build that already ran means everything was up to date; rush-client daemon status confirms that it was served.
For example, in one Bash terminal after steps 1–3:
rush-client build --to @rushstack/tree-pattern 2>&1 | tee /tmp/dogfood-build.log
grep -E 'in-process Rush|Rush Multi-Project Build Tool' /tmp/dogfood-build.log && echo 'NOT served by the daemon'
rush-client daemon status # graphInitialized: true; note pid and generationToken
cp libraries/tree-pattern/src/index.ts /tmp/tree-pattern-index.ts # keeps any edits you already made
echo "// throwaway" >> libraries/tree-pattern/src/index.ts
rush-client build --to @rushstack/tree-pattern # rebuilds tree-pattern in the same daemon
rush-client daemon status # same pid and generationToken
cp /tmp/tree-pattern-index.ts libraries/tree-pattern/src/index.ts # restores the file as it wasIn PowerShell, use rush-client build --to @rushstack/tree-pattern *>&1 | Tee-Object dogfood-build.log and
Select-String -Path dogfood-build.log -Pattern 'in-process Rush', 'Rush Multi-Project Build Tool'.
Pass --no-daemon to run a single rush-client command in-process:
rush-client build --no-daemon --to @rushstack/tree-patternOrdinary node common/scripts/install-run-rush.js ... commands also keep working while the daemon is
running. The daemon holds the Rush lock only while it prepares or executes a request, not while idle.
The snapshot does not pick up later source changes to Rush itself; after changing or pulling changes to
libraries/rush-lib, libraries/rush-daemon, libraries/rush-client-core, or apps/rush-cli-client, refresh it.
Stop the daemon first, because --overwrite deletes the files that a running daemon executes:
rush-client daemon stop
node common/scripts/install-run-rush.js build --to @rushstack/rush-cli-client
node common/scripts/install-run-rush.js deploy --scenario rush-daemon-dogfood --target-folder common/temp/rush-daemon-dogfood --overwriteOn Windows, the first daemon start from a new or refreshed snapshot can take longer than the client's 15-second
startup deadline while Windows scans the newly written files. Because the startup helper is still waiting for the
daemon, the client says so and keeps trying for another 15 seconds, then uses the daemon once it is ready. If it is
still not ready, the request fails with exit code 1 and a rush-client: ... message that says Rush was not run
in-process, rather than running native Rush next to the starting daemon. The daemon finishes starting in the
background, so rerun the command (rush-client daemon status shows when it is ready).
The daemon's identity is the canonical repository root plus the selected Rush version, so each checkout or
worktree has its own daemon, and rush-client daemon ... commands address the daemon for the checkout that
contains the current directory.
rush-client daemon stop # prints state "shutdownAccepted"
rush-client daemon status # exits with code 1: no daemon is listening
rush-client daemon logs # launcher log, available even after the daemon stoppedRemove the snapshot with rm -rf common/temp/rush-daemon-dogfood (or rush purge, which clears all of
common/temp). Stop the daemon before purging or reinstalling.
- Environment identity. The request environment is a daemon input, apart from per-shell bookkeeping
(such as
PWD,SHLVL,TERM,TERM_SESSION_ID,WSL_INTEROP, and SSH or tmux session handles) and the client's ownRUSH_DAEMONrouting variables. Any other difference, such as a differentWT_SESSIONorVSCODE_*value from another terminal window, or a changedPATH, restarts the daemon before anything runs, and the new process then serves the request. This is not a fallback, but it costs a cold start and changespid. Run related requests from the same terminal. - Only phased commands use the warm engine:
build,rebuild, and the phased commands of command-line.json. Global commands,rush start(which always watches),--watch,--install,--variant, and--node-diagnostic-dirstay native, as do build event-hook scripts (forbuildandrebuild, the only commands that run them) and reporter controls such as--output. Keep using ordinary Rush forinstall,update, and other commands.rushx-clientkeeps scripts attached to a TTY in-process. - No persistent Heft or TypeScript workers by default. Each operation still starts its Heft process; the
daemon saves Rush startup and graph construction, not compilation. The
usePersistentIpcRunners/daemonIpcmode requires a bundled, self-contained worker entry point, and Heft is not packaged that way. Seedaemon.warmWorkersbelow for Heft workers that stay alive between builds. :incrementalscripts. Withdaemon.incrementalBuilds(on by default), an operation whose project defines a_phase:<name>:incrementalscript runs it instead of the initial script when only files that the operation builds were edited since its last successful run in the daemon. The operation log then saysInvoking (incremental): .... An added, deleted or renamed input, a configuration, tool, environment or command-line change, a change to its output folders, bundled outputs, a cache restore or a nativerushcommand make it run the initial script, and the log says why (Not using the incremental command because ...). Incremental results are never written to the build cache, and neither are the results of operations built against them. SetRUSH_DAEMON_INCREMENTAL_BUILDS=0to always run the initial script.- Warm workers. With
daemon.warmWorkers(orRUSH_DAEMON_WARM_WORKERS=1, off by default) andincrementalBuilds, an operation whose project defines a_phase:<name>:incremental:ipcscript (for Heft,heft run-watch --only <phase> --) and whoserush-project.jsonoperation settings set"allowDaemonWarmWorker": trueruns its incremental builds in a worker started from that script. Set it only if the script, run in watch mode, runs every task and check that the initial script runs: the daemon reports a run on the worker as if the initial script had run, andrush startmay run the same script with fewer tasks on purpose. Heft skips lint and API Extractor in watch mode unless the lint task sets heft-lint-plugin'slintInWatchModeoption andconfig/api-extractor-task.jsonsetsrunInWatchMode; without them, a lint error does not fail the build, and consumers read a stale.d.tsrollup. A rig can set all three for its projects. The worker stays alive between builds and keeps its last build in memory, so the next build sends it another run instead of starting Heft again. The operation log saysInvoking (incremental): ..., followed byStarting a warm worker for it.orSending run <n> to the warm worker (pid <pid>).. When the rules above require the initial script, the worker is closed first (Closing the warm worker (pid <pid>), because ...), and that line is written even if the build cache then restores the operation. The initial script then runs as usual, or in a new worker if the project defines_phase:<name>:ipc(for Heft,heft run-watch --only <phase> -- --clean). A worker also accepts bundled outputs, because it keeps its bundler state in memory; if a run changes which output files exist, the initial script runs after it. If the run added or removed a file with a content hash in its name, as a bundler does when a chunk's content changes, the operation runs only its initial script from then on, until a request replaces the engine, because each later edit would rename the chunk again and leave the old one behind. A worker starts withTSC_WATCHFILE=UseFsEventsOnParentDirectoryunless the operation's environment setsTSC_WATCHFILE. On Linux and macOS, TypeScript's default file watcher follows each file's inode, and after Git replaces a file it can lose track of that file for the life of the process, so the worker would miss every later edit to the file and keep its stale outputs, with no error. TypeScript reads the variable only iftsconfig.jsondoes not setwatchOptions.watchFile. The variable's watcher fails loudly instead: on Linux, if a source file is missing when a run on the worker updates the TypeScript program, for example because a checkout deleted it during the build and then restored it, the run usually fails with errorTS6053(file not found), or withENOENTif a whole folder was missing, and like any failed run it closes the worker (see below), so it costs a failed run but leaves no stale outputs. With TypeScript 3.8 or later, Heft's TypeScript plugin reports the file as created when it comes back; with TypeScript 3.7 or older, or an older release of the plugin, TypeScript never finds that file again in the process. But if Heft sees the change while the run is in progress, it runs its tasks again, and the last pass decides the result. The run can then succeed with warnings, which fails the build unless the phase setsallowWarningsOnSuccess, but nothing runs again and the worker stays alive. The next build does not reuse that worker, with or without the build cache, because input files that changed while an operation ran make its result unverifiable. A watcher of a folder can also miss every later change in it once the folder is deleted and created again, for example by a branch switch that removed the folder and then restored it. So the next build runs the initial script if a folder that held the operation's input files was deleted or recreated since the last run on the worker started (Not using the incremental command because folders that held its input files were deleted or recreated since its last run (...)), even if the files kept their content. The daemon compares each folder's inode and creation time; a file that an editor saves by renaming a new file over it does not count. The message names only the top folder of a recreated tree, and an operation whose own build recreates such a folder never reuses its worker. A worker's outputs are only as current as Heft's watchers. A change that a watcher misses is not built, and because the run still succeeds, later builds report the operation as up to date and keep the stale outputs until the file is edited again. The checks above cover the cases found in testing: a file that Git replaces, a folder that is recreated, and a file that is missing while a run updates TypeScript. A case that none of them covers would leave stale outputs without an error. A reused worker can fail where the initial script passes, for example after Git rewrites thepackage.jsonof a dependency, because some tools keep state between runs. So if a run on a reused worker fails, or the worker exits during a run, the worker is closed and the initial script runs in the same build (Running the initial command, because the run on the warm worker failed.), and the operation fails only if the initial script fails too. A genuine error then costs an extra run of the initial script. The failed first run of a new worker is reported as it stands, because that run had no earlier state. The build cache is not read for a run on a live worker. A worker is closed after its operation fails, because the next build runs the initial script, and after 25 runs or once its memory doubles. Builds that follow each other within seconds can double a worker's memory in a few runs without a leak, because V8 collects each run's garbage later, while the worker is idle: with API Extractor in watch mode, a small package's worker grew by 120 to 180 MB per run and was replaced every 4 or 5 runs, but with 20 seconds between builds it stayed flat. Workers count towardwarmMemoryBudgetMBandwarmSetMaxProjects. Over either limit, or once no build has included a project forwarmIdleTimeoutSeconds, the warm set closes the project's workers and drops its last results, so its next build runs the initial script (The warm worker (pid <pid>) was closed after the operation last ran.). The default budget, 512 MiB, includes the daemon itself and leaves room for few workers, so raise both limits when you turn workers on. A request that replaces the engine, e.g. arebuildafter abuild, closes every worker without a note, and each operation's next build runs the initial script. - Plugins. This repository's only configured plugin,
@rushstack/rush-published-versions-json-plugin, is associated only withrecord-published-versionsand is inert for builds. A plugin withoutassociatedCommands, a plugin associated withbuildorrebuild, or a plugin command-line that defines the command, one of its phases, or a parameter for either would make those builds fall back to native Rush, unless the plugin is declared daemon-compatible. See Rush plugins in daemon engines. - Windows. Native Windows validation of the daemon code saw unresolved, intermittent failures in which
Git
hash-object --stdin-pathsexited with0xC0000142(DLL initialization failed) while the daemon captured workspace snapshots under Jest. Direct fixtures did not reproduce it, and it did not occur while this workflow was validated on native Windows. The cause is unresolved, so treat it as an open risk: if a daemon-served build fails this way, retry it with--no-daemonand report the failure. The daemon runs without a console, so it starts its tools (Git, tar and operation shells) with hidden windows. Daemons started from a snapshot older than that change open a visible terminal window for each tool; if you see that, refresh the snapshot. - CI stays in-process unless
RUSH_DAEMON=1is set; do not set it in CI workflows.
With "daemon": { "deferCacheWrites": true } in rush.json, or RUSH_DAEMON_DEFER_CACHE_WRITES=1 (off by
default), an operation that writes a build cache entry completes as soon as the daemon has cloned its output files
into common/temp/build-cache-staging/<pid>/. The daemon then writes the entry from the clones in the background,
two at a time, so the operations that depend on it, and the command, don't wait for tar. On a file system that can
clone files, such as Btrfs, XFS or APFS, the clones take well under a second, even for thousands of files, because
a clone shares its data with the output file until either changes. That is also why later changes to the outputs
don't change the entry. Like the daemon's other settings, the variable is part of its environment identity, so
changing it restarts the daemon.
- The operation's build cache log says
Sealed <n> output files (<x.y> MB) in <ms> ms; writing the build cache entry in the background.After each command with background activity, legacy output printsBuild cache entries written in the background since the previous command: <q> queued, <w> written (<x.y> MB), <f> failed; <p> pending.The line is a warning (on stderr) if any of those writes failed, and agent output prints it only then. Either way, the next command counts only the writes since this one. The daemon log has a line for each entry that it writes, fails to write or drops. - The clones take longer while the file system writes back what the build just wrote, which can take seconds. An
operation waits for them for at most half a second. The rest of its files are then cloned in the background, and
its log says
Sealing <n> output files (<x.y> MB) in the background after <ms> ms; the build cache entry is written once they're sealed.An output file that changes before it is cloned, for example because the next build rewrites it, would make the entry inconsistent, so the entry is dropped instead: the daemon log saysDropped the build cache entry <id> for <operation>, because <path> changed before it was sealed., and the command's line ends with<f> failed, <d> dropped; <p> pending.A dropped entry isn't a failure: like an entry that isn't written yet, it makes a restore of its cache key miss, so the operation runs again. - If the files can't be cloned, for example on ext4 or tmpfs, the entry is written before the operation completes,
as in native Rush (
Unable to clone the output files (<code>), so the build cache entry is written now.). Once the file system has refused a clone, the daemon doesn't try again until it restarts. - Cobuilds always write an entry before its operation completes, because other agents read it as soon as the operation completes.
- An entry that fails to be written in the background doesn't change the operation's status. The daemon log and the next command's warning report it.
- Until an entry is written, a restore of its cache key misses. For example, a native build that starts right after the daemon build runs the operation again.
rush-client daemon stopand a daemon restart drop the entries that aren't written yet.- Hard links among the output files are archived as separate files.
A daemon engine applies each Rush plugin once and then serves many requests from one long-lived process.
A plugin written for a single native command can therefore misbehave, so the daemon serves a build or
rebuild that a configured plugin participates in only if the plugin is declared daemon-compatible. A plugin
participates if it has no associatedCommands (Rush initializes it for every command), is associated with
the command, or its command-line.json defines the command, one of its phases, or a parameter for either.
Any other plugin is inert for the build and needs no declaration.
Declare a plugin in one of these ways:
- The plugin author sets
"daemonCompatible": trueon the plugin's entry inrush-plugin-manifest.json. Releases whose schemas predate this setting reject such a manifest, and then every command fails, not only builds. Set it only in plugin versions that are installed with a Rush release that accepts it, as with the@rushstackplugins, which are versioned with Rush; otherwise leave the declaration to repositories. - The repository lists the plugin's
pluginNamefromrush-plugins.jsonin therush.jsonsetting"daemon": { "compatiblePlugins": [...] }, after verifying the plugin against the contract below. - One shell sets
RUSH_DAEMON_COMPATIBLE_PLUGINSto a comma-separated list of plugin names, which replaces therush.jsonlist; an empty value lists no plugins. Releases whose schemas predate these settings reject the manifest andrush.jsonkeys, so a repository that still selects such a release can only use this variable, and only forrush-client. The value is part of the daemon's environment identity, so changing it starts a new daemon.
A listed name that matches no configured plugin has no effect; the request that creates the daemon's engine prints a warning, which the daemon's launcher log also keeps. An undeclared participating plugin makes the request fall back to native Rush, with a message that names the plugin and each reason.
The engine lifecycle that a declared plugin must support:
- Once per engine: the plugin's
apply(),initialize,runAnyPhasedCommand,runPhasedCommand.for(<command>),createOperationsAsyncandonGraphCreatedAsync. The engine builds the graph for every project, withisWatchfalse; each request then selects operations from it. A request whose parameters differ from the engine's (other than project selection,--include-phase-deps,--ignore-hooks,--verbose,--parallelismand--timeline) or a changed configuration file replaces the engine, and the new engine applies the plugin again in the same process. Module-level state therefore outlives an engine. An engine serves another command, such asrebuildon abuildengine, only if the same plugins are associated with both commands, no plugin taps therunPhasedCommandhook of either command, and every plugin that tapsinitializeorrunAnyPhasedCommandis declared command-agnostic (below), because Rush calls these hooks only with the command that created the engine. - Once per iteration: the operation graph hooks, such as
configureIteration,beforeExecuteIterationAsync,before/afterExecuteOperationAsync,createEnvironmentForOperationandafterExecuteIterationAsync. One iteration can serve several concurrent requests. If every operation that a request selects is up to date, the daemon aborts the iteration before it runs anything, sobeforeExecuteIterationAsyncand the operation hooks don't fire. Don't rely on iteration hooks for per-request work. - Disposal: the engine aborts
IOperationGraph.abortController, cancels the current iteration, and then closes every operation runner. Release resources from the abort signal or the runner'scloseAsync().
Rules for a daemon-compatible plugin:
- Don't change process globals: no writes to
process.env,process.exitCode, the current directory orprocess.argv, and no patching or clearing of global functions such assetTimeout. They belong to the daemon and to every other request. - Keep request-specific state per iteration, not per process or per engine (session IDs, start times, performance marks).
- Never prompt. The daemon has no terminal; fail fast with a message that the requesting client sees, and continue without the feature where possible.
- Read the operation's environment (from
createEnvironmentForOperation), not the daemon'sprocess.env. - Write output through the session's logger. Output written while an iteration runs reaches that iteration's clients, and output written while the engine is created reaches the request that created it; the daemon drops output written between iterations.
A plugin that taps initialize or runAnyPhasedCommand keeps an engine from serving any command other than the
one that created it, unless the plugin is also declared command-agnostic: what it does from those hooks doesn't
depend on the command. Its callbacks there, and the command hooks that they tap in turn (such as
createOperationsAsync and onGraphCreatedAsync), don't read the command's name, its parameters, its phase
selection or isIncrementalBuildAllowed. A plugin that only reads isWatch, for example, is command-agnostic.
Rush's built-in build cache plugins declare this in their manifests. Declare a plugin the same three ways as
above: "daemonCommandAgnostic": true in the manifest, the rush.json setting
"daemon": { "commandAgnosticPlugins": [...] }, or RUSH_DAEMON_COMMAND_AGNOSTIC_PLUGINS in one shell. The
same release rules apply: releases whose schemas predate these settings reject them, and then only the
variable works, only for rush-client, and only with an engine that recognizes it. The declaration covers only
the taps that the plugin's apply() adds; a tap added later, an interceptor on the hook, or a tap of
runPhasedCommand still keeps the engine to its own command. When a plugin keeps an engine to its own command,
a request of another command replaces the engine, or, for a rush-client custom command that neither engine
could serve, falls back to native Rush with a message that names the plugin.
Node.js loads a plugin's code only once, so the daemon treats the installed package folder of every configured
plugin as part of its implementation: a change to a plugin's .js or .json files, including through a
link: dependency, starts a new daemon for the next request. Declaration (lib-dts) and ES module (lib-esm)
output folders are left out, because a plugin's CommonJS entry point doesn't load them.
rush-client runs the daemon with its bundled engine only if that engine's version equals the selected Rush
version (RUSH_PREVIEW_VERSION, otherwise rushVersion in rush.json). Today both are the same, so the
snapshot's engine is used. If a release bumps apps/rush and libraries/rush-lib without updating
rushVersion, the client instead looks for a daemon-capable published release of the selected version.
None exists, so every build falls back to native Rush, with a rush-client: ...; using in-process Rush.
message (see Confirm).
To keep dogfooding in that state, select the snapshot's exact version for rush-client invocations only:
DOGFOOD_VERSION=$(node -p "require('./common/temp/rush-daemon-dogfood/libraries/rush-lib/package.json').version")
rush-client() { RUSH_DAEMON=1 RUSH_PREVIEW_VERSION="$DOGFOOD_VERSION" node "$DOGFOOD_SNAPSHOT/apps/rush-cli-client/bin/rush-client" "$@"; }Side effects:
- The build runs with the snapshot's engine rather than the repository's selected release.
RUSH_PREVIEW_VERSIONis part of the daemon's identity and environment, sorush-client daemon statusandrush-client daemon stopaddress the matching daemon only when the same value is set.- A native fallback, including
--no-daemon, also selects that version. Because it equals the snapshot's own frontend version, the snapshot's source-built Rush runs in-process, printing theRUSH_PREVIEW_VERSIONwarning banner, rather than the repository's selected release. Use ordinarynode common/scripts/install-run-rush.js ...for builds with the selected release. - Never export
RUSH_PREVIEW_VERSIONto ordinaryrush; it would try to install that version from npm.
Once a published Rush release includes the daemon, the standalone client, and the plugin narrowing that lets this repository's command-scoped plugin coexist with daemon builds:
- Update
rushVersioninrush.jsonto that release (or a later one). - Optionally add
"daemon": { "enabled": true }torush.json, so thatrush-clientuses the daemon withoutRUSH_DAEMON=1. CI still stays in-process unless it setsRUSH_DAEMON=1, andRUSH_DAEMON=0opts a shell out. - Install the published client whose engine matches
rushVersion, for examplenpm install --global @rushstack/rush-cli-client, and runrush-clientdirectly instead of the snapshot. - Stop any snapshot daemon and delete
common/temp/rush-daemon-dogfood. Therush-daemon-dogfooddeployment scenario remains useful for trying unreleased daemon changes.