From 4581fcf8b01a0dace7b6d76015a1de0fd849018b Mon Sep 17 00:00:00 2001 From: mojaza Date: Mon, 17 Aug 2026 18:06:09 -0700 Subject: [PATCH 1/4] [rush-daemon] Add daemon host bootstrap Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apps/rush/bin/rushd | 2 + apps/rush/package.json | 1 + apps/rush/src/RushCommandSelector.ts | 15 +- ...aemon-host-bootstrap_2026-08-17-23-30.json | 11 ++ ...aemon-host-bootstrap_2026-08-17-23-30.json | 11 ++ ...aemon-host-bootstrap_2026-08-17-23-30.json | 11 ++ .../rush/browser-approved-packages.json | 6 +- .../config/subspaces/default/pnpm-lock.yaml | 10 ++ .../reviews/api/rush-daemon-protocol.api.md | 8 +- common/reviews/api/rush-daemon.api.md | 28 ++++ common/reviews/api/rush-lib.api.md | 2 + .../src/ControlMessageValidation.ts | 14 +- .../src/DaemonControlMessage.ts | 11 +- .../src/DaemonPongMessage.ts | 14 ++ .../src/DaemonProtocolVersion.ts | 2 +- libraries/rush-daemon-protocol/src/index.ts | 3 +- .../src/test/ControlFrame.test.ts | 10 +- libraries/rush-daemon/README.md | 6 +- libraries/rush-daemon/package.json | 4 + .../rush-daemon/src/DaemonControlSession.ts | 117 ++++++++++++++ libraries/rush-daemon/src/RushDaemonHost.ts | 108 +++++++++++++ libraries/rush-daemon/src/index.ts | 2 + libraries/rush-daemon/src/serveRushDaemon.ts | 63 ++++++++ .../src/test/RushDaemonHost.test.ts | 149 ++++++++++++++++++ libraries/rush-lib/package.json | 1 + libraries/rush-lib/src/api/Rush.ts | 14 ++ .../rush-lib/src/cli/RushDaemonCommandLine.ts | 28 ++++ 27 files changed, 627 insertions(+), 24 deletions(-) create mode 100644 apps/rush/bin/rushd create mode 100644 common/changes/@microsoft/rush/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json create mode 100644 common/changes/@rushstack/rush-daemon-protocol/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json create mode 100644 common/changes/@rushstack/rush-daemon/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json create mode 100644 libraries/rush-daemon-protocol/src/DaemonPongMessage.ts create mode 100644 libraries/rush-daemon/src/DaemonControlSession.ts create mode 100644 libraries/rush-daemon/src/RushDaemonHost.ts create mode 100644 libraries/rush-daemon/src/serveRushDaemon.ts create mode 100644 libraries/rush-daemon/src/test/RushDaemonHost.test.ts create mode 100644 libraries/rush-lib/src/cli/RushDaemonCommandLine.ts diff --git a/apps/rush/bin/rushd b/apps/rush/bin/rushd new file mode 100644 index 00000000000..eef2fc27066 --- /dev/null +++ b/apps/rush/bin/rushd @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../lib-commonjs/start.js'); diff --git a/apps/rush/package.json b/apps/rush/package.json index 38b775062e6..7e5ee96b83a 100644 --- a/apps/rush/package.json +++ b/apps/rush/package.json @@ -32,6 +32,7 @@ "bin": { "rush": "./bin/rush", "rush-pnpm": "./bin/rush-pnpm", + "rushd": "./bin/rushd", "rushx": "./bin/rushx" }, "license": "MIT", diff --git a/apps/rush/src/RushCommandSelector.ts b/apps/rush/src/RushCommandSelector.ts index d85f00c5a91..8bddae10ee0 100644 --- a/apps/rush/src/RushCommandSelector.ts +++ b/apps/rush/src/RushCommandSelector.ts @@ -6,7 +6,7 @@ import * as path from 'node:path'; import type { ILaunchOptions } from '@microsoft/rush-lib/lib/index'; import { Colorize } from '@rushstack/terminal'; -type CommandName = 'rush' | 'rush-pnpm' | 'rushx' | undefined; +type CommandName = 'rush' | 'rush-pnpm' | 'rushd' | 'rushx' | undefined; /** * Both "rush" and "rushx" share the same src/start.ts entry point. This makes it @@ -39,7 +39,15 @@ export class RushCommandSelector { const commandName: CommandName = _getCommandName(); - if (commandName === 'rush-pnpm') { + if (commandName === 'rushd') { + if (!Rush.launchRushDaemon) { + _failWithError( + `This repository is using Rush version ${Rush.version}` + + ` which does not support the "rushd" command` + ); + } + Rush.launchRushDaemon(); + } else if (commandName === 'rush-pnpm') { if (!Rush.launchRushPnpm) { _failWithError( `This repository is using Rush version ${Rush.version}` + @@ -81,6 +89,9 @@ function _getCommandName(): CommandName { if (basename === 'RUSH-PNPM') { return 'rush-pnpm'; } + if (basename === 'RUSHD') { + return 'rushd'; + } if (basename === 'RUSHX') { return 'rushx'; } diff --git a/common/changes/@microsoft/rush/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json b/common/changes/@microsoft/rush/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json new file mode 100644 index 00000000000..63e3c246750 --- /dev/null +++ b/common/changes/@microsoft/rush/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add an opt-in rushd launcher through the version-selected rush-lib entry point.", + "type": "minor" + } + ], + "packageName": "@microsoft/rush", + "email": "mojazayeri@users.noreply.github.com" +} diff --git a/common/changes/@rushstack/rush-daemon-protocol/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json b/common/changes/@rushstack/rush-daemon-protocol/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json new file mode 100644 index 00000000000..f75faa8561a --- /dev/null +++ b/common/changes/@rushstack/rush-daemon-protocol/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@rushstack/rush-daemon-protocol", + "comment": "Include daemon and protocol version metadata in pong control messages.", + "type": "minor" + } + ], + "packageName": "@rushstack/rush-daemon-protocol", + "email": "mojazayeri@users.noreply.github.com" +} diff --git a/common/changes/@rushstack/rush-daemon/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json b/common/changes/@rushstack/rush-daemon/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json new file mode 100644 index 00000000000..25a8b014b72 --- /dev/null +++ b/common/changes/@rushstack/rush-daemon/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@rushstack/rush-daemon", + "comment": "Add workspace-keyed daemon host bootstrap with handshake, liveness, readiness, and clean shutdown lifecycle.", + "type": "minor" + } + ], + "packageName": "@rushstack/rush-daemon", + "email": "mojazayeri@users.noreply.github.com" +} diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index 772dfea4928..2d9f54468d5 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -46,13 +46,17 @@ "name": "@rushstack/problem-matcher", "allowedCategories": [ "libraries" ] }, + { + "name": "@rushstack/rush-daemon", + "allowedCategories": [ "libraries" ] + }, { "name": "@rushstack/rush-daemon-protocol", "allowedCategories": [ "libraries", "tests" ] }, { "name": "@rushstack/rush-daemon-transport", - "allowedCategories": [ "tests" ] + "allowedCategories": [ "libraries", "tests" ] }, { "name": "@rushstack/rush-serve-dashboard", diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index edeffd5d185..ff74ebec2f1 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -4078,6 +4078,13 @@ importers: version: 9.37.0 ../../../libraries/rush-daemon: + dependencies: + '@rushstack/rush-daemon-protocol': + specifier: workspace:* + version: link:../rush-daemon-protocol + '@rushstack/rush-daemon-transport': + specifier: workspace:* + version: link:../rush-daemon-transport devDependencies: '@rushstack/heft': specifier: workspace:* @@ -4161,6 +4168,9 @@ importers: '@rushstack/rig-package': specifier: workspace:* version: link:../rig-package + '@rushstack/rush-daemon': + specifier: workspace:* + version: link:../rush-daemon '@rushstack/rush-pnpm-kit-v10': specifier: workspace:* version: link:../rush-pnpm-kit-v10 diff --git a/common/reviews/api/rush-daemon-protocol.api.md b/common/reviews/api/rush-daemon-protocol.api.md index 54d3ad5bfed..c050c040a8e 100644 --- a/common/reviews/api/rush-daemon-protocol.api.md +++ b/common/reviews/api/rush-daemon-protocol.api.md @@ -49,13 +49,13 @@ export const DAEMON_PROTOCOL_VERSION: IDaemonProtocolVersion; // @beta export type DaemonControlMessage = IDaemonHelloMessage | IDaemonHelloAckMessage | IDaemonSubscribeMessage | IDaemonUnsubscribeMessage | IDaemonPingMessage | IDaemonPongMessage | IDaemonErrorMessage; -// @beta +// @beta (undocumented) export type DaemonControlMessageKind = (typeof DAEMON_CONTROL_MESSAGE_KINDS)[number]; // @beta export type DaemonDiagnosticSeverity = 'debug' | 'info' | 'warning' | 'error'; -// @beta +// @beta (undocumented) export type DaemonEmptyPayload = Record; // @beta @@ -292,6 +292,8 @@ export interface IDaemonPongMessage { readonly kind: 'pong'; // (undocumented) readonly payload: { + readonly daemonVersion: string; + readonly protocolVersion: IDaemonProtocolVersion; readonly uptimeMs: number; }; } @@ -326,7 +328,7 @@ export interface IDaemonUnsubscribeMessage { // @beta export function isDaemonControlMessageKind(value: unknown): value is DaemonControlMessageKind; -// @beta +// @beta (undocumented) export function isDaemonControlRecord(value: unknown): value is Record; // @beta diff --git a/common/reviews/api/rush-daemon.api.md b/common/reviews/api/rush-daemon.api.md index e32d01c6bc8..ef178340ec5 100644 --- a/common/reviews/api/rush-daemon.api.md +++ b/common/reviews/api/rush-daemon.api.md @@ -6,6 +6,8 @@ /// +import type { IDaemonPaths } from '@rushstack/rush-daemon-transport'; + // @public export interface IRequestLease { // (undocumented) @@ -23,6 +25,21 @@ export interface IRequestSchedulerAcquireOptions { waitTimeoutMs?: number; } +// @beta +export interface IRushDaemonHostOptions { + readonly daemonVersion: string; + readonly onError?: (error: Error) => void; + readonly repoRoot: string; + readonly rushVersion: string; + readonly startupOptions?: Readonly>; +} + +// @beta +export interface IRushDaemonServeOptions extends IRushDaemonHostOptions { + readonly onReady?: (host: RushDaemonHost) => void | Promise; + readonly shutdownSignal?: AbortSignal; +} + // @public export enum RequestExclusivityClass { // (undocumented) @@ -57,6 +74,17 @@ export enum RequestSchedulerErrorCode { WaitTimeout = "WAIT_TIMEOUT" } +// @beta +export class RushDaemonHost { + closeAsync(): Promise; + // (undocumented) + readonly paths: IDaemonPaths; + static startAsync(options: IRushDaemonHostOptions): Promise; +} + +// @beta +export function serveRushDaemonAsync(options: IRushDaemonServeOptions): Promise; + // (No @packageDocumentation comment for this package) ``` diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index ea311a232d8..26fc91f8181 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1372,6 +1372,8 @@ export class RepoStateFile { // @public export class Rush { static launch(launcherVersion: string, options: ILaunchOptions): void; + // @internal + static launchRushDaemon(): void; static launchRushPnpm(launcherVersion: string, options: ILaunchOptions): void; static launchRushX(launcherVersion: string, options: ILaunchOptions): void; // (undocumented) diff --git a/libraries/rush-daemon-protocol/src/ControlMessageValidation.ts b/libraries/rush-daemon-protocol/src/ControlMessageValidation.ts index 8f3f6dcfacc..e2704ed10c6 100644 --- a/libraries/rush-daemon-protocol/src/ControlMessageValidation.ts +++ b/libraries/rush-daemon-protocol/src/ControlMessageValidation.ts @@ -5,7 +5,7 @@ import { isDaemonControlMessageKind } from './DaemonControlMessage'; import { DaemonProtocolError } from './DaemonProtocolError'; import { isDaemonVerbosity } from './DaemonVerbosity'; -/** Returns `true` when `value` is a plain object. @beta */ +/** @beta */ export function isDaemonControlRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; } @@ -45,6 +45,12 @@ function validateHelloAck(payload: Record): void { requireStringField(payload, 'sessionId'); } +function validatePong(payload: Record): void { + requireStringField(payload, 'daemonVersion'); + requireVersion(payload); + requireNumberField(payload, 'uptimeMs'); +} + function validateSubscribe(payload: Record): void { if (typeof payload.isTTY !== 'boolean') { fail('Subscribe message payload.isTTY must be a boolean.'); @@ -73,13 +79,11 @@ const VALIDATORS_BY_KIND: Record = { subscribe: validateSubscribe, unsubscribe: noopValidator, ping: noopValidator, - pong: (payload: Record) => requireNumberField(payload, 'uptimeMs'), + pong: validatePong, error: validateError }; -/** - * Structurally validates a parsed control message. - * +/** Structurally validates a parsed control message. * @throws {@link DaemonProtocolError} when the value is not a well-formed control message. * * @beta diff --git a/libraries/rush-daemon-protocol/src/DaemonControlMessage.ts b/libraries/rush-daemon-protocol/src/DaemonControlMessage.ts index 36b592b5e3b..2fc0535d7e1 100644 --- a/libraries/rush-daemon-protocol/src/DaemonControlMessage.ts +++ b/libraries/rush-daemon-protocol/src/DaemonControlMessage.ts @@ -2,10 +2,11 @@ // See LICENSE in the project root for license information. import type { IDaemonClientCaps } from './DaemonClientCaps'; +import type { IDaemonPongMessage } from './DaemonPongMessage'; import type { DaemonProtocolErrorCode } from './DaemonProtocolError'; import type { IDaemonProtocolVersion } from './DaemonProtocolVersion'; -/** The empty payload of control messages that carry no data. @beta */ +/** @beta */ export type DaemonEmptyPayload = Record; /** The first frame a client sends on a new connection. @beta */ @@ -41,12 +42,6 @@ export interface IDaemonPingMessage { readonly payload: DaemonEmptyPayload; } -/** The liveness reply. @beta */ -export interface IDaemonPongMessage { - readonly kind: 'pong'; - readonly payload: { readonly uptimeMs: number }; -} - /** A protocol error sent on the wire. @beta */ export interface IDaemonErrorMessage { readonly kind: 'error'; @@ -89,7 +84,7 @@ export const DAEMON_CONTROL_MESSAGE_KINDS: readonly [ 'error' ] = ['hello', 'helloAck', 'subscribe', 'unsubscribe', 'ping', 'pong', 'error']; -/** The union of control message `kind` discriminants, derived from the list. @beta */ +/** @beta */ export type DaemonControlMessageKind = (typeof DAEMON_CONTROL_MESSAGE_KINDS)[number]; const CONTROL_KIND_SET: ReadonlySet = new Set(DAEMON_CONTROL_MESSAGE_KINDS); diff --git a/libraries/rush-daemon-protocol/src/DaemonPongMessage.ts b/libraries/rush-daemon-protocol/src/DaemonPongMessage.ts new file mode 100644 index 00000000000..9368bd7c40d --- /dev/null +++ b/libraries/rush-daemon-protocol/src/DaemonPongMessage.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IDaemonProtocolVersion } from './DaemonProtocolVersion'; + +/** The liveness reply. @beta */ +export interface IDaemonPongMessage { + readonly kind: 'pong'; + readonly payload: { + readonly daemonVersion: string; + readonly protocolVersion: IDaemonProtocolVersion; + readonly uptimeMs: number; + }; +} diff --git a/libraries/rush-daemon-protocol/src/DaemonProtocolVersion.ts b/libraries/rush-daemon-protocol/src/DaemonProtocolVersion.ts index 1bc51b155fb..e4053bdc795 100644 --- a/libraries/rush-daemon-protocol/src/DaemonProtocolVersion.ts +++ b/libraries/rush-daemon-protocol/src/DaemonProtocolVersion.ts @@ -34,7 +34,7 @@ export interface IDaemonProtocolVersion { */ export const DAEMON_PROTOCOL_VERSION: IDaemonProtocolVersion = { major: 0, - minor: 1 + minor: 2 }; /** diff --git a/libraries/rush-daemon-protocol/src/index.ts b/libraries/rush-daemon-protocol/src/index.ts index 4de98d0bb1b..4ef7a7107c7 100644 --- a/libraries/rush-daemon-protocol/src/index.ts +++ b/libraries/rush-daemon-protocol/src/index.ts @@ -30,7 +30,8 @@ export type { IDaemonClientCaps } from './DaemonClientCaps'; export { DAEMON_CONTROL_MESSAGE_KINDS, isDaemonControlMessageKind } from './DaemonControlMessage'; export type { DaemonControlMessage, DaemonControlMessageKind, DaemonEmptyPayload } from './DaemonControlMessage'; export type { IDaemonErrorMessage, IDaemonHelloAckMessage, IDaemonHelloMessage } from './DaemonControlMessage'; -export type { IDaemonPingMessage, IDaemonPongMessage, IDaemonSubscribeMessage, IDaemonUnsubscribeMessage } from './DaemonControlMessage'; +export type { IDaemonPingMessage, IDaemonSubscribeMessage, IDaemonUnsubscribeMessage } from './DaemonControlMessage'; +export type { IDaemonPongMessage } from './DaemonPongMessage'; export { isDaemonControlRecord, validateDaemonControlMessage } from './ControlMessageValidation'; export { decodeDaemonControlMessage, encodeDaemonControlMessage } from './ControlFrameCodec'; export { decodeDaemonLogChunk, encodeDaemonLogChunk, type IDaemonLogChunk } from './LogFrameCodec'; diff --git a/libraries/rush-daemon-protocol/src/test/ControlFrame.test.ts b/libraries/rush-daemon-protocol/src/test/ControlFrame.test.ts index 7a73101e744..803e2f6da72 100644 --- a/libraries/rush-daemon-protocol/src/test/ControlFrame.test.ts +++ b/libraries/rush-daemon-protocol/src/test/ControlFrame.test.ts @@ -9,6 +9,7 @@ import { captureProtocolError } from './TestVectors'; const UPTIME_MS: number = 42; const COLUMNS: number = 120; +const DAEMON_VERSION: string = '5.178.1'; const MESSAGES: readonly DaemonControlMessage[] = [ { kind: 'hello', payload: { protocolVersion: DAEMON_PROTOCOL_VERSION } }, @@ -16,7 +17,14 @@ const MESSAGES: readonly DaemonControlMessage[] = [ { kind: 'subscribe', payload: { isTTY: true, verbosity: 'verbose', columns: COLUMNS } }, { kind: 'unsubscribe', payload: {} }, { kind: 'ping', payload: {} }, - { kind: 'pong', payload: { uptimeMs: UPTIME_MS } }, + { + kind: 'pong', + payload: { + daemonVersion: DAEMON_VERSION, + protocolVersion: DAEMON_PROTOCOL_VERSION, + uptimeMs: UPTIME_MS + } + }, { kind: 'error', payload: { code: 'malformedPayload', message: 'bad' } } ]; diff --git a/libraries/rush-daemon/README.md b/libraries/rush-daemon/README.md index 5dba14abaf5..b61e1336212 100644 --- a/libraries/rush-daemon/README.md +++ b/libraries/rush-daemon/README.md @@ -1,5 +1,7 @@ # @rushstack/rush-daemon -The long-lived Rush workspace daemon host. +The long-lived Rush workspace daemon host, including workspace-keyed listener bootstrap, +protocol handshake and liveness control, and explicit serve/shutdown lifecycle APIs. -This package is under active development and is not yet integrated with the Rush command line. +The host is available from the opt-in `rushd` command. It does not change the default behavior of +`rush`, `rushx`, or `rush-pnpm`. diff --git a/libraries/rush-daemon/package.json b/libraries/rush-daemon/package.json index ae76afb22b6..251d5a85514 100644 --- a/libraries/rush-daemon/package.json +++ b/libraries/rush-daemon/package.json @@ -41,6 +41,10 @@ "_phase:build": "heft run --only build -- --clean", "_phase:test": "heft run --only test -- --clean" }, + "dependencies": { + "@rushstack/rush-daemon-protocol": "workspace:*", + "@rushstack/rush-daemon-transport": "workspace:*" + }, "devDependencies": { "@rushstack/heft": "workspace:*", "eslint": "~9.37.0", diff --git a/libraries/rush-daemon/src/DaemonControlSession.ts b/libraries/rush-daemon/src/DaemonControlSession.ts new file mode 100644 index 00000000000..85c38211322 --- /dev/null +++ b/libraries/rush-daemon/src/DaemonControlSession.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { randomUUID } from 'node:crypto'; + +import { + DAEMON_PROTOCOL_VERSION, + DaemonFrameType, + DaemonProtocolError, + decodeDaemonControlMessage, + encodeDaemonControlMessage, + negotiateDaemonHello +} from '@rushstack/rush-daemon-protocol'; +import type { + DaemonControlMessage, + IDaemonErrorMessage, + IDaemonFrame, + IDaemonPongMessage +} from '@rushstack/rush-daemon-protocol'; +import type { DaemonFrameConnection } from '@rushstack/rush-daemon-transport'; + +export interface IDaemonControlSessionOptions { + readonly daemonVersion: string; + readonly startedAtMs: number; + readonly onClosed: (session: DaemonControlSession, error: Error | undefined) => void; + readonly onError: (error: Error) => void; +} + +export class DaemonControlSession { + private readonly _connection: DaemonFrameConnection; + private readonly _options: IDaemonControlSessionOptions; + private _handshakeComplete: boolean = false; + private _sendQueue: Promise = Promise.resolve(); + + public constructor(connection: DaemonFrameConnection, options: IDaemonControlSessionOptions) { + this._connection = connection; + this._options = options; + connection.onFrame((frame: IDaemonFrame) => this._onFrame(frame)); + connection.onClosed((error: Error | undefined) => options.onClosed(this, error)); + } + + public closeAsync(): Promise { + return this._connection.closeAsync(); + } + + private _onFrame(frame: IDaemonFrame): void { + if (frame.kind !== DaemonFrameType.controlJson) { + throw new DaemonProtocolError( + 'malformedControlMessage', + 'A daemon control connection only accepts control frames.' + ); + } + const message: DaemonControlMessage = decodeDaemonControlMessage(frame.payload); + if (!this._handshakeComplete) { + this._handleHello(message); + } else if (message.kind === 'ping') { + this._send(this._createPong()); + } else { + throw new DaemonProtocolError( + 'malformedControlMessage', + `Control message "${message.kind}" is not valid in this daemon host state.` + ); + } + } + + private _handleHello(message: DaemonControlMessage): void { + if (message.kind !== 'hello') { + throw new DaemonProtocolError( + 'malformedControlMessage', + 'The first control message on a connection must be hello.' + ); + } + const outcome: ReturnType = negotiateDaemonHello( + message, + DAEMON_PROTOCOL_VERSION, + randomUUID() + ); + if (outcome.accepted) { + this._handshakeComplete = true; + this._send(outcome.ack); + } else { + const errorMessage: IDaemonErrorMessage = { + kind: 'error', + payload: { code: outcome.error.code, message: outcome.error.message } + }; + this._send(errorMessage, true); + } + } + + private _createPong(): IDaemonPongMessage { + return { + kind: 'pong', + payload: { + daemonVersion: this._options.daemonVersion, + protocolVersion: DAEMON_PROTOCOL_VERSION, + uptimeMs: Date.now() - this._options.startedAtMs + } + }; + } + + private _send(message: DaemonControlMessage, closeAfterSend: boolean = false): void { + const frame: IDaemonFrame = { + kind: DaemonFrameType.controlJson, + payload: encodeDaemonControlMessage(message) + }; + this._sendQueue = this._sendQueue + .then(() => this._connection.sendFrameAsync(frame)) + .then(() => (closeAfterSend ? this._connection.closeAsync() : undefined)) + .catch((error: unknown) => this._handleSendErrorAsync(error)); + } + + private async _handleSendErrorAsync(error: unknown): Promise { + const normalizedError: Error = error instanceof Error ? error : new Error(String(error)); + this._options.onError(normalizedError); + await this._connection.closeAsync(); + } +} diff --git a/libraries/rush-daemon/src/RushDaemonHost.ts b/libraries/rush-daemon/src/RushDaemonHost.ts new file mode 100644 index 00000000000..f3a9cd42510 --- /dev/null +++ b/libraries/rush-daemon/src/RushDaemonHost.ts @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { realpath } from 'node:fs/promises'; + +import { DAEMON_PROTOCOL_VERSION } from '@rushstack/rush-daemon-protocol'; +import { + computeDaemonWorkspaceKey, + DaemonFrameListener, + resolveDaemonPathsFromProcess +} from '@rushstack/rush-daemon-transport'; +import type { + DaemonFrameConnection, + IDaemonPaths +} from '@rushstack/rush-daemon-transport'; + +import { DaemonControlSession } from './DaemonControlSession'; + +/** + * Options for starting one workspace daemon host. + * + * @beta + */ +export interface IRushDaemonHostOptions { + /** The daemon implementation version reported by `pong`. */ + readonly daemonVersion: string; + /** Reports connection-level failures. */ + readonly onError?: (error: Error) => void; + /** The repository root containing rush.json. */ + readonly repoRoot: string; + /** The selected Rush version used to isolate the workspace transport. */ + readonly rushVersion: string; + /** Additional stable options that distinguish daemon instances. */ + readonly startupOptions?: Readonly>; +} + +/** + * A bound, workspace-keyed Rush daemon host. + * + * @beta + */ +export class RushDaemonHost { + private readonly _listener: DaemonFrameListener; + private readonly _sessions: Set; + private readonly _lifecycle: { closing: boolean }; + public readonly paths: IDaemonPaths; + private _closePromise: Promise | undefined; + + private constructor( + listener: DaemonFrameListener, + paths: IDaemonPaths, + sessions: Set, + lifecycle: { closing: boolean } + ) { + this._listener = listener; + this.paths = paths; + this._sessions = sessions; + this._lifecycle = lifecycle; + } + + /** Resolves only after the transport is bound and its lockfile has been written. */ + public static async startAsync(options: IRushDaemonHostOptions): Promise { + const canonicalRepoRoot: string = await realpath(options.repoRoot); + const workspaceKey: string = computeDaemonWorkspaceKey({ + canonicalRepoRoot, + rushVersion: options.rushVersion, + startupOptions: options.startupOptions + }); + const paths: IDaemonPaths = resolveDaemonPathsFromProcess(workspaceKey); + const sessions: Set = new Set(); + const lifecycle: { closing: boolean } = { closing: false }; + const startedAtMs: number = Date.now(); + const listener: DaemonFrameListener = await DaemonFrameListener.listenAsync(paths, { + protocolVersion: DAEMON_PROTOCOL_VERSION, + startedAt: new Date(startedAtMs).toISOString(), + onConnection: (connection: DaemonFrameConnection) => { + const session: DaemonControlSession = new DaemonControlSession(connection, { + daemonVersion: options.daemonVersion, + startedAtMs, + onClosed: (closedSession: DaemonControlSession, error: Error | undefined) => { + sessions.delete(closedSession); + if (error) { + options.onError?.(error); + } + }, + onError: (error: Error) => options.onError?.(error) + }); + sessions.add(session); + if (lifecycle.closing) { + void session.closeAsync(); + } + } + }); + return new RushDaemonHost(listener, paths, sessions, lifecycle); + } + + /** Closes active connections, stops listening, and removes transport artifacts. */ + public closeAsync(): Promise { + this._closePromise ??= this._closeOnceAsync(); + return this._closePromise; + } + + private async _closeOnceAsync(): Promise { + this._lifecycle.closing = true; + await Promise.all(Array.from(this._sessions, (session: DaemonControlSession) => session.closeAsync())); + await this._listener.closeAsync(); + } +} diff --git a/libraries/rush-daemon/src/index.ts b/libraries/rush-daemon/src/index.ts index 1da4d820f98..a9178e97499 100644 --- a/libraries/rush-daemon/src/index.ts +++ b/libraries/rush-daemon/src/index.ts @@ -11,3 +11,5 @@ export { RequestSchedulerError, RequestSchedulerErrorCode } from './RequestScheduler'; +export { RushDaemonHost, type IRushDaemonHostOptions } from './RushDaemonHost'; +export { serveRushDaemonAsync, type IRushDaemonServeOptions } from './serveRushDaemon'; diff --git a/libraries/rush-daemon/src/serveRushDaemon.ts b/libraries/rush-daemon/src/serveRushDaemon.ts new file mode 100644 index 00000000000..a5970586863 --- /dev/null +++ b/libraries/rush-daemon/src/serveRushDaemon.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { RushDaemonHost } from './RushDaemonHost'; +import type { IRushDaemonHostOptions } from './RushDaemonHost'; + +/** + * Options for the daemon serve lifecycle. + * + * @beta + */ +export interface IRushDaemonServeOptions extends IRushDaemonHostOptions { + /** Called after the listener is bound and the lockfile is available. */ + readonly onReady?: (host: RushDaemonHost) => void | Promise; + /** Requests a clean shutdown. Process signals are used when omitted. */ + readonly shutdownSignal?: AbortSignal; +} + +/** + * Starts a daemon host, signals readiness, and serves until shutdown is requested. + * + * @beta + */ +export async function serveRushDaemonAsync(options: IRushDaemonServeOptions): Promise { + const signalRegistration: IShutdownSignalRegistration = options.shutdownSignal + ? { signal: options.shutdownSignal, dispose: () => undefined } + : createProcessShutdownSignal(); + let host: RushDaemonHost | undefined; + try { + host = await RushDaemonHost.startAsync(options); + await options.onReady?.(host); + await waitForAbortAsync(signalRegistration.signal); + } finally { + signalRegistration.dispose(); + await host?.closeAsync(); + } +} + +interface IShutdownSignalRegistration { + readonly signal: AbortSignal; + readonly dispose: () => void; +} + +function createProcessShutdownSignal(): IShutdownSignalRegistration { + const controller: AbortController = new AbortController(); + const onSignal: () => void = () => controller.abort(); + process.once('SIGINT', onSignal); + process.once('SIGTERM', onSignal); + return { + signal: controller.signal, + dispose: () => { + process.off('SIGINT', onSignal); + process.off('SIGTERM', onSignal); + } + }; +} + +function waitForAbortAsync(signal: AbortSignal): Promise { + if (signal.aborted) { + return Promise.resolve(); + } + return new Promise((resolve: () => void) => signal.addEventListener('abort', () => resolve(), { once: true })); +} diff --git a/libraries/rush-daemon/src/test/RushDaemonHost.test.ts b/libraries/rush-daemon/src/test/RushDaemonHost.test.ts new file mode 100644 index 00000000000..768d95e10e6 --- /dev/null +++ b/libraries/rush-daemon/src/test/RushDaemonHost.test.ts @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { + DAEMON_PROTOCOL_VERSION, + DaemonFrameType, + createDaemonHello, + decodeDaemonControlMessage, + encodeDaemonControlMessage +} from '@rushstack/rush-daemon-protocol'; +import type { + DaemonControlMessage, + IDaemonFrame +} from '@rushstack/rush-daemon-protocol'; +import { + connectDaemonAsync, + readDaemonLockfile +} from '@rushstack/rush-daemon-transport'; +import type { + DaemonFrameConnection, + IDaemonPaths +} from '@rushstack/rush-daemon-transport'; + +import { RushDaemonHost } from '../RushDaemonHost'; +import { serveRushDaemonAsync } from '../serveRushDaemon'; + +const RUSH_VERSION: string = '5.178.1'; +const DAEMON_VERSION: string = '0.1.0-test'; +const WINDOWS_PIPE_PREFIX: string = '\\\\.\\pipe\\rushd-'; +const REPO_PREFIX: string = 'rush-daemon-host-test-'; + +const testRepoRoots: Set = new Set(); + +afterEach(() => { + for (const repoRoot of testRepoRoots) { + fs.rmSync(repoRoot, { force: true, recursive: true }); + } + testRepoRoots.clear(); +}); + +function createTestRepoRoot(): string { + const repoRoot: string = fs.mkdtempSync(path.join(os.tmpdir(), REPO_PREFIX)); + testRepoRoots.add(repoRoot); + return repoRoot; +} + +function createHostOptions(repoRoot: string): { + daemonVersion: string; + repoRoot: string; + rushVersion: string; +} { + return { daemonVersion: DAEMON_VERSION, repoRoot, rushVersion: RUSH_VERSION }; +} + +async function exchangeControlAsync( + connection: DaemonFrameConnection, + message: DaemonControlMessage +): Promise { + const response: Promise = new Promise( + (resolve: (message: DaemonControlMessage) => void) => { + connection.onFrame((frame: IDaemonFrame) => resolve(decodeDaemonControlMessage(frame.payload))); + } + ); + await connection.sendFrameAsync({ + kind: DaemonFrameType.controlJson, + payload: encodeDaemonControlMessage(message) + }); + return response; +} + +describe(RushDaemonHost.name, () => { + it('binds the workspace transport and handles hello plus ping', async () => { + const host: RushDaemonHost = await RushDaemonHost.startAsync( + createHostOptions(createTestRepoRoot()) + ); + const client: DaemonFrameConnection = await connectDaemonAsync(host.paths.socketPath); + try { + expect(readDaemonLockfile(host.paths.lockfilePath)).toMatchObject({ + pid: process.pid, + protocolVersion: DAEMON_PROTOCOL_VERSION, + socketPath: host.paths.socketPath + }); + if (process.platform === 'win32') { + expect(host.paths.socketPath.startsWith(WINDOWS_PIPE_PREFIX)).toBe(true); + } + const helloAck: DaemonControlMessage = await exchangeControlAsync( + client, + createDaemonHello(DAEMON_PROTOCOL_VERSION) + ); + expect(helloAck).toMatchObject({ + kind: 'helloAck', + payload: { protocolVersion: DAEMON_PROTOCOL_VERSION } + }); + const pong: DaemonControlMessage = await exchangeControlAsync(client, { + kind: 'ping', + payload: {} + }); + expect(pong).toMatchObject({ + kind: 'pong', + payload: { + daemonVersion: DAEMON_VERSION, + protocolVersion: DAEMON_PROTOCOL_VERSION + } + }); + } finally { + await client.closeAsync(); + await host.closeAsync(); + } + }); + + it('signals readiness only after the listener and lockfile are available', async () => { + const controller: AbortController = new AbortController(); + let readyPaths: IDaemonPaths | undefined; + const onReadyAsync: (host: RushDaemonHost) => Promise = async (host: RushDaemonHost) => { + readyPaths = host.paths; + expect(readDaemonLockfile(host.paths.lockfilePath)).toBeDefined(); + const client: DaemonFrameConnection = await connectDaemonAsync(host.paths.socketPath); + await client.closeAsync(); + controller.abort(); + }; + await serveRushDaemonAsync({ + ...createHostOptions(createTestRepoRoot()), + shutdownSignal: controller.signal, + onReady: onReadyAsync + }); + if (!readyPaths) { + throw new Error('The daemon did not signal readiness.'); + } + expect(readDaemonLockfile(readyPaths.lockfilePath)).toBeUndefined(); + }); + + it('closes active connections and removes transport artifacts', async () => { + const host: RushDaemonHost = await RushDaemonHost.startAsync( + createHostOptions(createTestRepoRoot()) + ); + const client: DaemonFrameConnection = await connectDaemonAsync(host.paths.socketPath); + const closed: Promise = new Promise((resolve: () => void) => client.onClosed(() => resolve())); + await host.closeAsync(); + await closed; + expect(readDaemonLockfile(host.paths.lockfilePath)).toBeUndefined(); + await expect(connectDaemonAsync(host.paths.socketPath)).rejects.toMatchObject({ + code: 'connectionRefused' + }); + }); +}); diff --git a/libraries/rush-lib/package.json b/libraries/rush-lib/package.json index c1e072cd268..82238adf8e4 100644 --- a/libraries/rush-lib/package.json +++ b/libraries/rush-lib/package.json @@ -50,6 +50,7 @@ "@rushstack/package-deps-hash": "workspace:*", "@rushstack/package-extractor": "workspace:*", "@rushstack/rig-package": "workspace:*", + "@rushstack/rush-daemon": "workspace:*", "@rushstack/rush-pnpm-kit-v10": "workspace:*", "@rushstack/rush-pnpm-kit-v8": "workspace:*", "@rushstack/rush-pnpm-kit-v9": "workspace:*", diff --git a/libraries/rush-lib/src/api/Rush.ts b/libraries/rush-lib/src/api/Rush.ts index 64e06354047..5d1d4f548c3 100644 --- a/libraries/rush-lib/src/api/Rush.ts +++ b/libraries/rush-lib/src/api/Rush.ts @@ -15,6 +15,7 @@ import { CommandLineMigrationAdvisor } from '../cli/CommandLineMigrationAdvisor' import { EnvironmentVariableNames } from './EnvironmentConfiguration'; import type { IBuiltInPluginConfiguration } from '../pluginFramework/PluginLoader/BuiltInPluginLoader'; import { RushPnpmCommandLine } from '../cli/RushPnpmCommandLine'; +import { RushDaemonCommandLine } from '../cli/RushDaemonCommandLine'; import { measureAsyncFn } from '../utilities/performance'; /** @@ -121,6 +122,19 @@ export class Rush { RushPnpmCommandLine.launch(launcherVersion, { ...options }); } + /** + * This API is used by the `@microsoft/rush` front end to launch the opt-in "rushd" command. + * + * @internal + */ + public static launchRushDaemon(): void { + _assignRushInvokedFolder(); + RushDaemonCommandLine.launchAsync().catch((error: Error) => { + process.stderr.write(`${error.stack ?? error.message}\n`); + process.exitCode = 1; + }); + } + /** * The currently executing version of the "rush-lib" library. * This is the same as the Rush tool version for that release. diff --git a/libraries/rush-lib/src/cli/RushDaemonCommandLine.ts b/libraries/rush-lib/src/cli/RushDaemonCommandLine.ts new file mode 100644 index 00000000000..f968a24989b --- /dev/null +++ b/libraries/rush-lib/src/cli/RushDaemonCommandLine.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { serveRushDaemonAsync } from '@rushstack/rush-daemon'; +import type { RushDaemonHost } from '@rushstack/rush-daemon'; + +import { RushConfiguration } from '../api/RushConfiguration'; +import { Rush } from '../api/Rush'; + +/** + * Internal entry point for the version-selected `rushd` command. + * + * @internal + */ +export class RushDaemonCommandLine { + public static async launchAsync(): Promise { + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromDefaultLocation(); + await serveRushDaemonAsync({ + daemonVersion: Rush.version, + repoRoot: rushConfiguration.rushJsonFolder, + rushVersion: Rush.version, + onError: (error: Error) => process.stderr.write(`${error.stack ?? error.message}\n`), + onReady: (host: RushDaemonHost) => { + process.stdout.write(`rushd ready at ${host.paths.socketPath}\n`); + } + }); + } +} From 795bde63b5beea030ccd106d8417f0e90b7daaf8 Mon Sep 17 00:00:00 2001 From: mojaza Date: Mon, 17 Aug 2026 18:21:32 -0700 Subject: [PATCH 2/4] [rush-daemon] Refresh injected dependency metadata Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../build-tests-subspace/pnpm-lock.yaml | 35 +++++++++++++++++++ .../build-tests-subspace/repo-state.json | 4 +-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml index ff2150b8065..b82d447fad4 100644 --- a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml +++ b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml @@ -995,6 +995,25 @@ packages: '@rushstack/rig-package@file:../../../libraries/rig-package': resolution: {directory: ../../../libraries/rig-package, type: directory} + '@rushstack/rush-daemon-protocol@file:../../../libraries/rush-daemon-protocol': + resolution: {directory: ../../../libraries/rush-daemon-protocol, type: directory} + + '@rushstack/rush-daemon-transport@file:../../../libraries/rush-daemon-transport': + resolution: {directory: ../../../libraries/rush-daemon-transport, type: directory} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rush-daemon@file:../../../libraries/rush-daemon': + resolution: {directory: ../../../libraries/rush-daemon, type: directory} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + '@rushstack/rush-pnpm-kit-v10@file:../../../libraries/rush-pnpm-kit-v10': resolution: {directory: ../../../libraries/rush-pnpm-kit-v10, type: directory} @@ -4326,6 +4345,7 @@ snapshots: '@rushstack/package-deps-hash': file:../../../libraries/package-deps-hash(@types/node@20.17.19) '@rushstack/package-extractor': file:../../../libraries/package-extractor(@types/node@20.17.19) '@rushstack/rig-package': file:../../../libraries/rig-package + '@rushstack/rush-daemon': file:../../../libraries/rush-daemon(@types/node@20.17.19) '@rushstack/rush-pnpm-kit-v10': file:../../../libraries/rush-pnpm-kit-v10 '@rushstack/rush-pnpm-kit-v8': file:../../../libraries/rush-pnpm-kit-v8 '@rushstack/rush-pnpm-kit-v9': file:../../../libraries/rush-pnpm-kit-v9 @@ -4922,6 +4942,21 @@ snapshots: jju: 1.4.0 resolve: 1.22.11 + '@rushstack/rush-daemon-protocol@file:../../../libraries/rush-daemon-protocol': {} + + '@rushstack/rush-daemon-transport@file:../../../libraries/rush-daemon-transport(@types/node@20.17.19)': + dependencies: + '@rushstack/rush-daemon-protocol': file:../../../libraries/rush-daemon-protocol + optionalDependencies: + '@types/node': 20.17.19 + + '@rushstack/rush-daemon@file:../../../libraries/rush-daemon(@types/node@20.17.19)': + dependencies: + '@rushstack/rush-daemon-protocol': file:../../../libraries/rush-daemon-protocol + '@rushstack/rush-daemon-transport': file:../../../libraries/rush-daemon-transport(@types/node@20.17.19) + optionalDependencies: + '@types/node': 20.17.19 + '@rushstack/rush-pnpm-kit-v10@file:../../../libraries/rush-pnpm-kit-v10': dependencies: '@pnpm/dependency-path-pnpm-v10': '@pnpm/dependency-path@1000.0.9' diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index fb5e86342d9..a3c32b7030e 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -1,6 +1,6 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "4e7dc2a53df7dd2ee77e903f8f67c29f817ecd81", + "pnpmShrinkwrapHash": "f54344248a6c82a599b6890690b640b41d7645ce", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9", - "packageJsonInjectedDependenciesHash": "dacc324b7b6d9e35baf57460cae419ef162589d6" + "packageJsonInjectedDependenciesHash": "05d372d173bdf63b8f3f50e61de7e9bc41919c2c" } From 0379b52a712c8420a45ac384feecc1a8d2f92a82 Mon Sep 17 00:00:00 2001 From: mojaza Date: Mon, 17 Aug 2026 18:26:07 -0700 Subject: [PATCH 3/4] [rush-daemon-protocol] Preserve pong compatibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- common/reviews/api/rush-daemon-protocol.api.md | 10 +++++----- .../src/ControlMessageValidation.ts | 6 +++--- .../rush-daemon-protocol/src/DaemonControlMessage.ts | 4 ++-- .../rush-daemon-protocol/src/DaemonPongMessage.ts | 6 ++++-- .../rush-daemon-protocol/src/test/ControlFrame.test.ts | 1 + 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/common/reviews/api/rush-daemon-protocol.api.md b/common/reviews/api/rush-daemon-protocol.api.md index c050c040a8e..37834b8ba5e 100644 --- a/common/reviews/api/rush-daemon-protocol.api.md +++ b/common/reviews/api/rush-daemon-protocol.api.md @@ -49,13 +49,13 @@ export const DAEMON_PROTOCOL_VERSION: IDaemonProtocolVersion; // @beta export type DaemonControlMessage = IDaemonHelloMessage | IDaemonHelloAckMessage | IDaemonSubscribeMessage | IDaemonUnsubscribeMessage | IDaemonPingMessage | IDaemonPongMessage | IDaemonErrorMessage; -// @beta (undocumented) +// @beta export type DaemonControlMessageKind = (typeof DAEMON_CONTROL_MESSAGE_KINDS)[number]; // @beta export type DaemonDiagnosticSeverity = 'debug' | 'info' | 'warning' | 'error'; -// @beta (undocumented) +// @beta export type DaemonEmptyPayload = Record; // @beta @@ -292,8 +292,8 @@ export interface IDaemonPongMessage { readonly kind: 'pong'; // (undocumented) readonly payload: { - readonly daemonVersion: string; - readonly protocolVersion: IDaemonProtocolVersion; + readonly daemonVersion?: string; + readonly protocolVersion?: IDaemonProtocolVersion; readonly uptimeMs: number; }; } @@ -328,7 +328,7 @@ export interface IDaemonUnsubscribeMessage { // @beta export function isDaemonControlMessageKind(value: unknown): value is DaemonControlMessageKind; -// @beta (undocumented) +// @beta export function isDaemonControlRecord(value: unknown): value is Record; // @beta diff --git a/libraries/rush-daemon-protocol/src/ControlMessageValidation.ts b/libraries/rush-daemon-protocol/src/ControlMessageValidation.ts index e2704ed10c6..49d87c195a1 100644 --- a/libraries/rush-daemon-protocol/src/ControlMessageValidation.ts +++ b/libraries/rush-daemon-protocol/src/ControlMessageValidation.ts @@ -5,7 +5,7 @@ import { isDaemonControlMessageKind } from './DaemonControlMessage'; import { DaemonProtocolError } from './DaemonProtocolError'; import { isDaemonVerbosity } from './DaemonVerbosity'; -/** @beta */ +/** Returns `true` when `value` is a non-null object usable as a control record. @beta */ export function isDaemonControlRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; } @@ -46,8 +46,8 @@ function validateHelloAck(payload: Record): void { } function validatePong(payload: Record): void { - requireStringField(payload, 'daemonVersion'); - requireVersion(payload); + if (payload.daemonVersion !== undefined) requireStringField(payload, 'daemonVersion'); + if (payload.protocolVersion !== undefined) requireVersion(payload); requireNumberField(payload, 'uptimeMs'); } diff --git a/libraries/rush-daemon-protocol/src/DaemonControlMessage.ts b/libraries/rush-daemon-protocol/src/DaemonControlMessage.ts index 2fc0535d7e1..c20a04a3a5b 100644 --- a/libraries/rush-daemon-protocol/src/DaemonControlMessage.ts +++ b/libraries/rush-daemon-protocol/src/DaemonControlMessage.ts @@ -6,7 +6,7 @@ import type { IDaemonPongMessage } from './DaemonPongMessage'; import type { DaemonProtocolErrorCode } from './DaemonProtocolError'; import type { IDaemonProtocolVersion } from './DaemonProtocolVersion'; -/** @beta */ +/** The empty payload of control messages that carry no data. @beta */ export type DaemonEmptyPayload = Record; /** The first frame a client sends on a new connection. @beta */ @@ -84,7 +84,7 @@ export const DAEMON_CONTROL_MESSAGE_KINDS: readonly [ 'error' ] = ['hello', 'helloAck', 'subscribe', 'unsubscribe', 'ping', 'pong', 'error']; -/** @beta */ +/** The union of control message `kind` discriminants, derived from the runtime list. @beta */ export type DaemonControlMessageKind = (typeof DAEMON_CONTROL_MESSAGE_KINDS)[number]; const CONTROL_KIND_SET: ReadonlySet = new Set(DAEMON_CONTROL_MESSAGE_KINDS); diff --git a/libraries/rush-daemon-protocol/src/DaemonPongMessage.ts b/libraries/rush-daemon-protocol/src/DaemonPongMessage.ts index 9368bd7c40d..0538bf91890 100644 --- a/libraries/rush-daemon-protocol/src/DaemonPongMessage.ts +++ b/libraries/rush-daemon-protocol/src/DaemonPongMessage.ts @@ -7,8 +7,10 @@ import type { IDaemonProtocolVersion } from './DaemonProtocolVersion'; export interface IDaemonPongMessage { readonly kind: 'pong'; readonly payload: { - readonly daemonVersion: string; - readonly protocolVersion: IDaemonProtocolVersion; + /** The daemon implementation version, when reported by protocol 0.2 or newer. */ + readonly daemonVersion?: string; + /** The daemon wire protocol version, when reported by protocol 0.2 or newer. */ + readonly protocolVersion?: IDaemonProtocolVersion; readonly uptimeMs: number; }; } diff --git a/libraries/rush-daemon-protocol/src/test/ControlFrame.test.ts b/libraries/rush-daemon-protocol/src/test/ControlFrame.test.ts index 803e2f6da72..11a854a0ba0 100644 --- a/libraries/rush-daemon-protocol/src/test/ControlFrame.test.ts +++ b/libraries/rush-daemon-protocol/src/test/ControlFrame.test.ts @@ -17,6 +17,7 @@ const MESSAGES: readonly DaemonControlMessage[] = [ { kind: 'subscribe', payload: { isTTY: true, verbosity: 'verbose', columns: COLUMNS } }, { kind: 'unsubscribe', payload: {} }, { kind: 'ping', payload: {} }, + { kind: 'pong', payload: { uptimeMs: UPTIME_MS } }, { kind: 'pong', payload: { From 387a196933507dc96b745f089dad135a58011eae Mon Sep 17 00:00:00 2001 From: mojaza Date: Tue, 18 Aug 2026 09:51:27 -0700 Subject: [PATCH 4/4] [rush-daemon] Move rushd entrypoint into daemon package Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- apps/rush/bin/rushd | 2 - apps/rush/package.json | 1 - apps/rush/src/RushCommandSelector.ts | 15 +---- ...aemon-host-bootstrap_2026-08-17-23-30.json | 11 ---- ...aemon-host-bootstrap_2026-08-17-23-30.json | 2 +- .../rush/browser-approved-packages.json | 4 -- .../build-tests-subspace/pnpm-lock.yaml | 35 ----------- .../build-tests-subspace/repo-state.json | 4 +- .../config/subspaces/default/pnpm-lock.yaml | 6 +- common/reviews/api/rush-lib.api.md | 2 - libraries/rush-daemon/README.md | 5 +- libraries/rush-daemon/package.json | 4 ++ .../rush-daemon/src/RushDaemonCommandLine.ts | 61 +++++++++++++++++++ libraries/rush-daemon/src/start.ts | 11 ++++ .../src/test/RushDaemonCommandLine.test.ts | 51 ++++++++++++++++ libraries/rush-lib/package.json | 1 - libraries/rush-lib/src/api/Rush.ts | 14 ----- .../rush-lib/src/cli/RushDaemonCommandLine.ts | 28 --------- 18 files changed, 138 insertions(+), 119 deletions(-) delete mode 100644 apps/rush/bin/rushd delete mode 100644 common/changes/@microsoft/rush/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json create mode 100644 libraries/rush-daemon/src/RushDaemonCommandLine.ts create mode 100644 libraries/rush-daemon/src/start.ts create mode 100644 libraries/rush-daemon/src/test/RushDaemonCommandLine.test.ts delete mode 100644 libraries/rush-lib/src/cli/RushDaemonCommandLine.ts diff --git a/apps/rush/bin/rushd b/apps/rush/bin/rushd deleted file mode 100644 index eef2fc27066..00000000000 --- a/apps/rush/bin/rushd +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -require('../lib-commonjs/start.js'); diff --git a/apps/rush/package.json b/apps/rush/package.json index 7e5ee96b83a..38b775062e6 100644 --- a/apps/rush/package.json +++ b/apps/rush/package.json @@ -32,7 +32,6 @@ "bin": { "rush": "./bin/rush", "rush-pnpm": "./bin/rush-pnpm", - "rushd": "./bin/rushd", "rushx": "./bin/rushx" }, "license": "MIT", diff --git a/apps/rush/src/RushCommandSelector.ts b/apps/rush/src/RushCommandSelector.ts index 8bddae10ee0..d85f00c5a91 100644 --- a/apps/rush/src/RushCommandSelector.ts +++ b/apps/rush/src/RushCommandSelector.ts @@ -6,7 +6,7 @@ import * as path from 'node:path'; import type { ILaunchOptions } from '@microsoft/rush-lib/lib/index'; import { Colorize } from '@rushstack/terminal'; -type CommandName = 'rush' | 'rush-pnpm' | 'rushd' | 'rushx' | undefined; +type CommandName = 'rush' | 'rush-pnpm' | 'rushx' | undefined; /** * Both "rush" and "rushx" share the same src/start.ts entry point. This makes it @@ -39,15 +39,7 @@ export class RushCommandSelector { const commandName: CommandName = _getCommandName(); - if (commandName === 'rushd') { - if (!Rush.launchRushDaemon) { - _failWithError( - `This repository is using Rush version ${Rush.version}` + - ` which does not support the "rushd" command` - ); - } - Rush.launchRushDaemon(); - } else if (commandName === 'rush-pnpm') { + if (commandName === 'rush-pnpm') { if (!Rush.launchRushPnpm) { _failWithError( `This repository is using Rush version ${Rush.version}` + @@ -89,9 +81,6 @@ function _getCommandName(): CommandName { if (basename === 'RUSH-PNPM') { return 'rush-pnpm'; } - if (basename === 'RUSHD') { - return 'rushd'; - } if (basename === 'RUSHX') { return 'rushx'; } diff --git a/common/changes/@microsoft/rush/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json b/common/changes/@microsoft/rush/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json deleted file mode 100644 index 63e3c246750..00000000000 --- a/common/changes/@microsoft/rush/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "packageName": "@microsoft/rush", - "comment": "Add an opt-in rushd launcher through the version-selected rush-lib entry point.", - "type": "minor" - } - ], - "packageName": "@microsoft/rush", - "email": "mojazayeri@users.noreply.github.com" -} diff --git a/common/changes/@rushstack/rush-daemon/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json b/common/changes/@rushstack/rush-daemon/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json index 25a8b014b72..10842acf1e0 100644 --- a/common/changes/@rushstack/rush-daemon/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json +++ b/common/changes/@rushstack/rush-daemon/mojazayeri-rush-daemon-host-bootstrap_2026-08-17-23-30.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@rushstack/rush-daemon", - "comment": "Add workspace-keyed daemon host bootstrap with handshake, liveness, readiness, and clean shutdown lifecycle.", + "comment": "Add the rushd executable and workspace-keyed daemon host bootstrap with handshake, liveness, readiness, and clean shutdown lifecycle.", "type": "minor" } ], diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index 2d9f54468d5..b8fdee53a10 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -46,10 +46,6 @@ "name": "@rushstack/problem-matcher", "allowedCategories": [ "libraries" ] }, - { - "name": "@rushstack/rush-daemon", - "allowedCategories": [ "libraries" ] - }, { "name": "@rushstack/rush-daemon-protocol", "allowedCategories": [ "libraries", "tests" ] diff --git a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml index b82d447fad4..ff2150b8065 100644 --- a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml +++ b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml @@ -995,25 +995,6 @@ packages: '@rushstack/rig-package@file:../../../libraries/rig-package': resolution: {directory: ../../../libraries/rig-package, type: directory} - '@rushstack/rush-daemon-protocol@file:../../../libraries/rush-daemon-protocol': - resolution: {directory: ../../../libraries/rush-daemon-protocol, type: directory} - - '@rushstack/rush-daemon-transport@file:../../../libraries/rush-daemon-transport': - resolution: {directory: ../../../libraries/rush-daemon-transport, type: directory} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - - '@rushstack/rush-daemon@file:../../../libraries/rush-daemon': - resolution: {directory: ../../../libraries/rush-daemon, type: directory} - peerDependencies: - '@types/node': '*' - peerDependenciesMeta: - '@types/node': - optional: true - '@rushstack/rush-pnpm-kit-v10@file:../../../libraries/rush-pnpm-kit-v10': resolution: {directory: ../../../libraries/rush-pnpm-kit-v10, type: directory} @@ -4345,7 +4326,6 @@ snapshots: '@rushstack/package-deps-hash': file:../../../libraries/package-deps-hash(@types/node@20.17.19) '@rushstack/package-extractor': file:../../../libraries/package-extractor(@types/node@20.17.19) '@rushstack/rig-package': file:../../../libraries/rig-package - '@rushstack/rush-daemon': file:../../../libraries/rush-daemon(@types/node@20.17.19) '@rushstack/rush-pnpm-kit-v10': file:../../../libraries/rush-pnpm-kit-v10 '@rushstack/rush-pnpm-kit-v8': file:../../../libraries/rush-pnpm-kit-v8 '@rushstack/rush-pnpm-kit-v9': file:../../../libraries/rush-pnpm-kit-v9 @@ -4942,21 +4922,6 @@ snapshots: jju: 1.4.0 resolve: 1.22.11 - '@rushstack/rush-daemon-protocol@file:../../../libraries/rush-daemon-protocol': {} - - '@rushstack/rush-daemon-transport@file:../../../libraries/rush-daemon-transport(@types/node@20.17.19)': - dependencies: - '@rushstack/rush-daemon-protocol': file:../../../libraries/rush-daemon-protocol - optionalDependencies: - '@types/node': 20.17.19 - - '@rushstack/rush-daemon@file:../../../libraries/rush-daemon(@types/node@20.17.19)': - dependencies: - '@rushstack/rush-daemon-protocol': file:../../../libraries/rush-daemon-protocol - '@rushstack/rush-daemon-transport': file:../../../libraries/rush-daemon-transport(@types/node@20.17.19) - optionalDependencies: - '@types/node': 20.17.19 - '@rushstack/rush-pnpm-kit-v10@file:../../../libraries/rush-pnpm-kit-v10': dependencies: '@pnpm/dependency-path-pnpm-v10': '@pnpm/dependency-path@1000.0.9' diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index a3c32b7030e..fb5e86342d9 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -1,6 +1,6 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "f54344248a6c82a599b6890690b640b41d7645ce", + "pnpmShrinkwrapHash": "4e7dc2a53df7dd2ee77e903f8f67c29f817ecd81", "preferredVersionsHash": "550b4cee0bef4e97db6c6aad726df5149d20e7d9", - "packageJsonInjectedDependenciesHash": "05d372d173bdf63b8f3f50e61de7e9bc41919c2c" + "packageJsonInjectedDependenciesHash": "dacc324b7b6d9e35baf57460cae419ef162589d6" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index ff74ebec2f1..a0167a937a8 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -4079,6 +4079,9 @@ importers: ../../../libraries/rush-daemon: dependencies: + '@rushstack/node-core-library': + specifier: workspace:* + version: link:../node-core-library '@rushstack/rush-daemon-protocol': specifier: workspace:* version: link:../rush-daemon-protocol @@ -4168,9 +4171,6 @@ importers: '@rushstack/rig-package': specifier: workspace:* version: link:../rig-package - '@rushstack/rush-daemon': - specifier: workspace:* - version: link:../rush-daemon '@rushstack/rush-pnpm-kit-v10': specifier: workspace:* version: link:../rush-pnpm-kit-v10 diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 26fc91f8181..ea311a232d8 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1372,8 +1372,6 @@ export class RepoStateFile { // @public export class Rush { static launch(launcherVersion: string, options: ILaunchOptions): void; - // @internal - static launchRushDaemon(): void; static launchRushPnpm(launcherVersion: string, options: ILaunchOptions): void; static launchRushX(launcherVersion: string, options: ILaunchOptions): void; // (undocumented) diff --git a/libraries/rush-daemon/README.md b/libraries/rush-daemon/README.md index b61e1336212..3e895e2ee26 100644 --- a/libraries/rush-daemon/README.md +++ b/libraries/rush-daemon/README.md @@ -3,5 +3,6 @@ The long-lived Rush workspace daemon host, including workspace-keyed listener bootstrap, protocol handshake and liveness control, and explicit serve/shutdown lifecycle APIs. -The host is available from the opt-in `rushd` command. It does not change the default behavior of -`rush`, `rushx`, or `rush-pnpm`. +The package provides an opt-in `rushd` executable. Run it from a Rush workspace to start the host +for the nearest `rush.json`; it does not change the default behavior of `rush`, `rushx`, or +`rush-pnpm`. diff --git a/libraries/rush-daemon/package.json b/libraries/rush-daemon/package.json index 251d5a85514..7daacb9ae7d 100644 --- a/libraries/rush-daemon/package.json +++ b/libraries/rush-daemon/package.json @@ -5,6 +5,9 @@ "main": "./lib-commonjs/index.js", "module": "./lib-esm/index.js", "types": "./dist/rush-daemon.d.ts", + "bin": { + "rushd": "./lib-commonjs/start.js" + }, "exports": { ".": { "types": "./dist/rush-daemon.d.ts", @@ -42,6 +45,7 @@ "_phase:test": "heft run --only test -- --clean" }, "dependencies": { + "@rushstack/node-core-library": "workspace:*", "@rushstack/rush-daemon-protocol": "workspace:*", "@rushstack/rush-daemon-transport": "workspace:*" }, diff --git a/libraries/rush-daemon/src/RushDaemonCommandLine.ts b/libraries/rush-daemon/src/RushDaemonCommandLine.ts new file mode 100644 index 00000000000..097f9330274 --- /dev/null +++ b/libraries/rush-daemon/src/RushDaemonCommandLine.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'node:path'; + +import { FileSystem, JsonFile, PackageJsonLookup } from '@rushstack/node-core-library'; +import type { IPackageJson } from '@rushstack/node-core-library'; + +import { serveRushDaemonAsync } from './serveRushDaemon'; + +const RUSH_JSON_FILENAME: string = 'rush.json'; + +export interface IRushDaemonWorkspace { + readonly repoRoot: string; + readonly rushVersion: string; +} + +export function resolveRushDaemonWorkspace(startingFolder: string): IRushDaemonWorkspace { + const rushJsonPath: string = findRushJsonPath(startingFolder); + const rushJson: { rushVersion?: unknown } = JsonFile.load(rushJsonPath); + if (typeof rushJson.rushVersion !== 'string') { + throw new Error(`The "rushVersion" field in "${rushJsonPath}" must be a string.`); + } + return { + repoRoot: path.dirname(rushJsonPath), + rushVersion: rushJson.rushVersion + }; +} + +export async function launchRushDaemonAsync(startingFolder: string = process.cwd()): Promise { + const workspace: IRushDaemonWorkspace = resolveRushDaemonWorkspace(startingFolder); + const packageJson: IPackageJson | undefined = + PackageJsonLookup.instance.tryLoadPackageJsonFor(__dirname); + if (!packageJson) { + throw new Error('Unable to determine the @rushstack/rush-daemon package version.'); + } + await serveRushDaemonAsync({ + daemonVersion: packageJson.version, + repoRoot: workspace.repoRoot, + rushVersion: workspace.rushVersion, + onError: (error: Error) => process.stderr.write(`${error.stack ?? error.message}\n`), + onReady: (host) => { + process.stdout.write(`rushd ready at ${host.paths.socketPath}\n`); + } + }); +} + +function findRushJsonPath(startingFolder: string): string { + let currentFolder: string = path.resolve(startingFolder); + while (true) { + const candidatePath: string = path.join(currentFolder, RUSH_JSON_FILENAME); + if (FileSystem.exists(candidatePath)) { + return candidatePath; + } + const parentFolder: string = path.dirname(currentFolder); + if (parentFolder === currentFolder) { + throw new Error(`Unable to find ${RUSH_JSON_FILENAME} in "${startingFolder}" or its parents.`); + } + currentFolder = parentFolder; + } +} diff --git a/libraries/rush-daemon/src/start.ts b/libraries/rush-daemon/src/start.ts new file mode 100644 index 00000000000..7f1b6378c24 --- /dev/null +++ b/libraries/rush-daemon/src/start.ts @@ -0,0 +1,11 @@ +#!/usr/bin/env node + +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { launchRushDaemonAsync } from './RushDaemonCommandLine'; + +launchRushDaemonAsync().catch((error: Error) => { + process.stderr.write(`${error.stack ?? error.message}\n`); + process.exitCode = 1; +}); diff --git a/libraries/rush-daemon/src/test/RushDaemonCommandLine.test.ts b/libraries/rush-daemon/src/test/RushDaemonCommandLine.test.ts new file mode 100644 index 00000000000..e8c83068390 --- /dev/null +++ b/libraries/rush-daemon/src/test/RushDaemonCommandLine.test.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'node:path'; +import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; + +import { + resolveRushDaemonWorkspace, + type IRushDaemonWorkspace +} from '../RushDaemonCommandLine'; + +describe(resolveRushDaemonWorkspace.name, () => { + let tempFolder: string; + + beforeEach(async () => { + tempFolder = await mkdtemp(path.join(tmpdir(), 'rushd-cli-')); + }); + + afterEach(async () => { + await rm(tempFolder, { force: true, recursive: true }); + }); + + it('finds and reads the nearest rush.json from a nested folder', async () => { + const nestedFolder: string = path.join(tempFolder, 'apps', 'example'); + await mkdir(nestedFolder, { recursive: true }); + await writeFile( + path.join(tempFolder, 'rush.json'), + '{\n // The selected Rush version\n "rushVersion": "5.178.0"\n}\n' + ); + + const workspace: IRushDaemonWorkspace = resolveRushDaemonWorkspace(nestedFolder); + + expect(workspace).toEqual({ + repoRoot: tempFolder, + rushVersion: '5.178.0' + }); + }); + + it('rejects a rush.json without a string rushVersion', async () => { + await writeFile(path.join(tempFolder, 'rush.json'), '{ "rushVersion": 5 }\n'); + + expect(() => resolveRushDaemonWorkspace(tempFolder)).toThrow( + /The "rushVersion" field .* must be a string/ + ); + }); + + it('reports when no rush.json exists', () => { + expect(() => resolveRushDaemonWorkspace(tempFolder)).toThrow(/Unable to find rush\.json/); + }); +}); diff --git a/libraries/rush-lib/package.json b/libraries/rush-lib/package.json index 82238adf8e4..c1e072cd268 100644 --- a/libraries/rush-lib/package.json +++ b/libraries/rush-lib/package.json @@ -50,7 +50,6 @@ "@rushstack/package-deps-hash": "workspace:*", "@rushstack/package-extractor": "workspace:*", "@rushstack/rig-package": "workspace:*", - "@rushstack/rush-daemon": "workspace:*", "@rushstack/rush-pnpm-kit-v10": "workspace:*", "@rushstack/rush-pnpm-kit-v8": "workspace:*", "@rushstack/rush-pnpm-kit-v9": "workspace:*", diff --git a/libraries/rush-lib/src/api/Rush.ts b/libraries/rush-lib/src/api/Rush.ts index 5d1d4f548c3..64e06354047 100644 --- a/libraries/rush-lib/src/api/Rush.ts +++ b/libraries/rush-lib/src/api/Rush.ts @@ -15,7 +15,6 @@ import { CommandLineMigrationAdvisor } from '../cli/CommandLineMigrationAdvisor' import { EnvironmentVariableNames } from './EnvironmentConfiguration'; import type { IBuiltInPluginConfiguration } from '../pluginFramework/PluginLoader/BuiltInPluginLoader'; import { RushPnpmCommandLine } from '../cli/RushPnpmCommandLine'; -import { RushDaemonCommandLine } from '../cli/RushDaemonCommandLine'; import { measureAsyncFn } from '../utilities/performance'; /** @@ -122,19 +121,6 @@ export class Rush { RushPnpmCommandLine.launch(launcherVersion, { ...options }); } - /** - * This API is used by the `@microsoft/rush` front end to launch the opt-in "rushd" command. - * - * @internal - */ - public static launchRushDaemon(): void { - _assignRushInvokedFolder(); - RushDaemonCommandLine.launchAsync().catch((error: Error) => { - process.stderr.write(`${error.stack ?? error.message}\n`); - process.exitCode = 1; - }); - } - /** * The currently executing version of the "rush-lib" library. * This is the same as the Rush tool version for that release. diff --git a/libraries/rush-lib/src/cli/RushDaemonCommandLine.ts b/libraries/rush-lib/src/cli/RushDaemonCommandLine.ts deleted file mode 100644 index f968a24989b..00000000000 --- a/libraries/rush-lib/src/cli/RushDaemonCommandLine.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { serveRushDaemonAsync } from '@rushstack/rush-daemon'; -import type { RushDaemonHost } from '@rushstack/rush-daemon'; - -import { RushConfiguration } from '../api/RushConfiguration'; -import { Rush } from '../api/Rush'; - -/** - * Internal entry point for the version-selected `rushd` command. - * - * @internal - */ -export class RushDaemonCommandLine { - public static async launchAsync(): Promise { - const rushConfiguration: RushConfiguration = RushConfiguration.loadFromDefaultLocation(); - await serveRushDaemonAsync({ - daemonVersion: Rush.version, - repoRoot: rushConfiguration.rushJsonFolder, - rushVersion: Rush.version, - onError: (error: Error) => process.stderr.write(`${error.stack ?? error.message}\n`), - onReady: (host: RushDaemonHost) => { - process.stdout.write(`rushd ready at ${host.paths.socketPath}\n`); - } - }); - } -}