nx 23.1.1 → 23.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/release/changelog-renderer/index.js +4 -5
  2. package/dist/release/index.d.ts +1 -1
  3. package/dist/src/command-line/add/add.js +5 -0
  4. package/dist/src/command-line/affected/affected.js +1 -0
  5. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +2 -2
  6. package/dist/src/command-line/import/import.js +1 -2
  7. package/dist/src/command-line/init/init-v2.js +4 -5
  8. package/dist/src/command-line/migrate/migrate.d.ts +4 -0
  9. package/dist/src/command-line/migrate/migrate.js +89 -26
  10. package/dist/src/command-line/release/changelog.js +5 -0
  11. package/dist/src/command-line/release/index.d.ts +1 -1
  12. package/dist/src/command-line/release/utils/git.js +1 -1
  13. package/dist/src/command-line/release/utils/release-graph.d.ts +17 -0
  14. package/dist/src/command-line/release/utils/release-graph.js +71 -19
  15. package/dist/src/command-line/release/utils/remote-release-clients/github.js +6 -0
  16. package/dist/src/command-line/release/version/release-group-processor.d.ts +24 -0
  17. package/dist/src/command-line/release/version/release-group-processor.js +80 -6
  18. package/dist/src/command-line/release/version/version-actions.d.ts +27 -2
  19. package/dist/src/command-line/release/version/version-actions.js +9 -1
  20. package/dist/src/command-line/release/version.js +9 -5
  21. package/dist/src/command-line/reset/reset.js +6 -1
  22. package/dist/src/command-line/run/run-one.js +1 -0
  23. package/dist/src/command-line/run-many/run-many.js +1 -0
  24. package/dist/src/command-line/watch/watch.js +13 -0
  25. package/dist/src/core/graph/main.js +1 -1
  26. package/dist/src/core/graph/styles.js +1 -1
  27. package/dist/src/daemon/client/client.d.ts +52 -1
  28. package/dist/src/daemon/client/client.js +160 -36
  29. package/dist/src/daemon/client/daemon-environment.d.ts +17 -0
  30. package/dist/src/daemon/client/daemon-environment.js +110 -2
  31. package/dist/src/daemon/client/daemon-socket-messenger.js +4 -0
  32. package/dist/src/daemon/message-types/daemon-message.d.ts +33 -0
  33. package/dist/src/daemon/message-types/daemon-message.js +48 -0
  34. package/dist/src/daemon/server/file-watching/file-watcher-sockets.d.ts +5 -0
  35. package/dist/src/daemon/server/file-watching/file-watcher-sockets.js +16 -0
  36. package/dist/src/daemon/server/latest-nx.js +5 -2
  37. package/dist/src/daemon/server/server.js +24 -0
  38. package/dist/src/daemon/server/shutdown-utils.d.ts +5 -0
  39. package/dist/src/daemon/server/shutdown-utils.js +11 -4
  40. package/dist/src/daemon/socket-utils.d.ts +1 -0
  41. package/dist/src/daemon/socket-utils.js +24 -3
  42. package/dist/src/daemon/tmp-dir.d.ts +43 -3
  43. package/dist/src/daemon/tmp-dir.js +437 -15
  44. package/dist/src/devkit-internals.d.ts +94 -4
  45. package/dist/src/devkit-internals.js +194 -5
  46. package/dist/src/generators/internal-utils/format-changed-files-with-prettier-if-available.d.ts +5 -0
  47. package/dist/src/generators/internal-utils/format-changed-files-with-prettier-if-available.js +9 -2
  48. package/dist/src/generators/tree.d.ts +4 -2
  49. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.d.ts +12 -0
  50. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.js +250 -0
  51. package/dist/src/migrations/update-23-2-0/set-cache-on-executor-target-defaults.md +50 -0
  52. package/dist/src/native/index.d.ts +10 -0
  53. package/dist/src/native/index.js +71 -28
  54. package/dist/src/native/native-bindings.js +1 -0
  55. package/dist/src/native/native-file-cache-location.d.ts +17 -1
  56. package/dist/src/native/native-file-cache-location.js +86 -12
  57. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  58. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  59. package/dist/src/nx-cloud/utilities/get-cloud-options.js +5 -2
  60. package/dist/src/nx-cloud/utilities/onboarding.js +5 -2
  61. package/dist/src/plugins/js/lock-file/bun-parser.js +43 -10
  62. package/dist/src/plugins/js/lock-file/pnpm-parser.js +60 -0
  63. package/dist/src/plugins/js/lock-file/project-graph-pruning.js +14 -8
  64. package/dist/src/plugins/js/project-graph/affected/npm-packages.js +51 -20
  65. package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.js +2 -0
  66. package/dist/src/project-graph/affected/affected-project-graph-models.d.ts +1 -1
  67. package/dist/src/project-graph/affected/affected-project-graph.d.ts +1 -1
  68. package/dist/src/project-graph/affected/affected-project-graph.js +2 -2
  69. package/dist/src/project-graph/affected/locators/project-glob-changes.js +5 -2
  70. package/dist/src/project-graph/error-types.d.ts +12 -1
  71. package/dist/src/project-graph/error-types.js +27 -2
  72. package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +1 -0
  73. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +18 -1
  74. package/dist/src/project-graph/plugins/isolation/messaging.d.ts +16 -3
  75. package/dist/src/project-graph/plugins/isolation/messaging.js +3 -0
  76. package/dist/src/project-graph/plugins/isolation/plugin-worker.js +58 -8
  77. package/dist/src/project-graph/project-graph.js +20 -0
  78. package/dist/src/project-graph/utils/project-configuration/project-nodes-manager.js +5 -1
  79. package/dist/src/project-graph/utils/project-configuration/target-normalization.js +133 -3
  80. package/dist/src/tasks-runner/task-env.js +5 -2
  81. package/dist/src/tasks-runner/utils.js +2 -6
  82. package/dist/src/utils/assert-workspace-validity.js +7 -2
  83. package/dist/src/utils/fileutils.d.ts +5 -0
  84. package/dist/src/utils/fileutils.js +7 -2
  85. package/dist/src/utils/git-worktrees.d.ts +30 -0
  86. package/dist/src/utils/git-worktrees.js +212 -0
  87. package/dist/src/utils/ignore.d.ts +128 -1
  88. package/dist/src/utils/ignore.js +182 -12
  89. package/dist/src/utils/long-running-target.d.ts +10 -0
  90. package/dist/src/utils/long-running-target.js +19 -0
  91. package/dist/src/utils/min-release-age/behavior/bun.js +7 -11
  92. package/dist/src/utils/min-release-age/behavior/npm.js +5 -2
  93. package/dist/src/utils/min-release-age/packument.js +1 -1
  94. package/dist/src/utils/nx-tmp-dir.d.ts +18 -0
  95. package/dist/src/utils/nx-tmp-dir.js +40 -0
  96. package/dist/src/utils/owned-private-dir.d.ts +132 -0
  97. package/dist/src/utils/owned-private-dir.js +323 -0
  98. package/dist/src/utils/package-manager-config/bunfig.d.ts +14 -0
  99. package/dist/src/utils/package-manager-config/bunfig.js +49 -0
  100. package/dist/src/utils/package-manager-config/npmrc.d.ts +23 -0
  101. package/dist/src/utils/package-manager-config/npmrc.js +124 -0
  102. package/dist/src/utils/package-manager-config/pnpm-config.d.ts +17 -0
  103. package/dist/src/utils/package-manager-config/pnpm-config.js +60 -0
  104. package/dist/src/utils/package-manager.d.ts +21 -2
  105. package/dist/src/utils/package-manager.js +244 -47
  106. package/dist/src/utils/provenance.js +6 -14
  107. package/dist/src/utils/registry-config/bun.d.ts +2 -0
  108. package/dist/src/utils/registry-config/bun.js +257 -0
  109. package/dist/src/utils/registry-config/index.d.ts +13 -0
  110. package/dist/src/utils/registry-config/index.js +102 -0
  111. package/dist/src/utils/registry-config/pnpm.d.ts +2 -0
  112. package/dist/src/utils/registry-config/pnpm.js +1551 -0
  113. package/dist/src/utils/registry-config/utils.d.ts +195 -0
  114. package/dist/src/utils/registry-config/utils.js +496 -0
  115. package/dist/src/utils/registry-config/yarn-berry.d.ts +2 -0
  116. package/dist/src/utils/registry-config/yarn-berry.js +612 -0
  117. package/dist/src/utils/registry-config/yarn-classic.d.ts +2 -0
  118. package/dist/src/utils/registry-config/yarn-classic.js +1001 -0
  119. package/dist/src/utils/safe-spawn.d.ts +24 -0
  120. package/dist/src/utils/safe-spawn.js +104 -0
  121. package/dist/src/utils/shell-quoting.d.ts +15 -0
  122. package/dist/src/utils/shell-quoting.js +35 -0
  123. package/dist/src/utils/wait-for-socket-connection.d.ts +11 -0
  124. package/dist/src/utils/wait-for-socket-connection.js +14 -8
  125. package/migrations.json +6 -0
  126. package/package.json +12 -12
  127. package/dist/src/utils/min-release-age/npmrc.d.ts +0 -15
  128. package/dist/src/utils/min-release-age/npmrc.js +0 -45
@@ -1 +1 @@
1
- "use strict";(self.webpackChunk_nx_graph_client=self.webpackChunk_nx_graph_client||[]).push([[869],{8027(){}},e=>{var n;n=8027,e(e.s=n)}]);
1
+ "use strict";(self.webpackChunk_nx_graph_client=self.webpackChunk_nx_graph_client||[]).push([[869],{946(){}},e=>{var n;n=946,e(e.s=n)}]);
@@ -8,11 +8,23 @@ import { ConfigurationSourceMaps } from '../../project-graph/utils/project-confi
8
8
  import type { FlushSyncGeneratorChangesResult, SyncGeneratorRunResult } from '../../utils/sync-generators';
9
9
  import { type ConfigureAiAgentsStatusResponse } from '../message-types/configure-ai-agents';
10
10
  import { type NxConsoleStatusResponse, type SetNxConsolePreferenceAndInstallResponse } from '../message-types/nx-console';
11
+ /** A refused connect: the errno, and the path it was made against. */
12
+ type ConnectRefusal = {
13
+ error: NodeJS.ErrnoException;
14
+ socketPath: string;
15
+ };
11
16
  export type UnregisterCallback = () => void;
12
17
  export type ChangedFile = {
13
18
  path: string;
14
19
  type: 'create' | 'update' | 'delete';
15
20
  };
21
+ /**
22
+ * The daemon's workspace watcher died. Nothing it serves will see file changes
23
+ * again, so a watching client has to restart rather than keep waiting.
24
+ */
25
+ export declare class WatcherFailedError extends Error {
26
+ constructor(message: string);
27
+ }
16
28
  export declare class DaemonClient {
17
29
  private readonly nxJson;
18
30
  constructor();
@@ -93,6 +105,12 @@ export declare class DaemonClient {
93
105
  resetConfigureAiAgentsStatus(): Promise<{
94
106
  success: boolean;
95
107
  }>;
108
+ /**
109
+ * The pre-start probe, returning why it failed rather than leaving it on the
110
+ * instance: `_daemonStatus` does not serialize `isServerAvailable`'s five
111
+ * public callers against `startDaemonIfNecessary`.
112
+ */
113
+ private probeServer;
96
114
  isServerAvailable(): Promise<boolean>;
97
115
  private startDaemonIfNecessary;
98
116
  private sendToDaemonViaQueue;
@@ -108,8 +126,41 @@ export declare class DaemonClient {
108
126
  private sendMessageToDaemon;
109
127
  private registerDaemonProcessWithMetricsService;
110
128
  private handleMessage;
111
- startInBackground(): Promise<ChildProcess['pid']>;
129
+ /**
130
+ * @param probeRefusal what the caller's pre-start probe saw. The only evidence
131
+ * when a daemon refuses us and then exits — the poll cannot reproduce it
132
+ * once the process json is gone. `nx daemon --start` passes nothing.
133
+ */
134
+ startInBackground(probeRefusal?: ConnectRefusal): Promise<ChildProcess['pid']>;
112
135
  stop(): Promise<void>;
113
136
  }
114
137
  export declare const daemonClient: DaemonClient;
115
138
  export declare function isDaemonEnabled(): boolean;
139
+ /**
140
+ * EACCES and EPERM are the two errnos that mean the OS refused us rather than
141
+ * that nothing was listening. They need opposite remedies — a socket owned by
142
+ * someone else versus a sandbox refusing the connect syscall — but they share
143
+ * the property that retrying cannot change the answer.
144
+ */
145
+ export declare function isPermissionErrno(error: NodeJS.ErrnoException): boolean;
146
+ /**
147
+ * The operating system refused the connection. Most often the socket belongs to
148
+ * another user, which is the guarantee the owner-only socket directory buys —
149
+ * but a sandbox that denies unix-socket connects produces the same errno, so the
150
+ * message does not assert which. Either way it is an environment condition
151
+ * rather than a defect in Nx, and it deliberately does not
152
+ * carry `internalDaemonError`: that tag tells the user to file an issue and
153
+ * disables the daemon until `nx reset`, which would outlast the stale socket
154
+ * that caused it.
155
+ *
156
+ * It also skips the daemon log that `daemonProcessException` appends. The log
157
+ * belongs to *our* daemon; the process holding this socket is someone else's, so
158
+ * quoting it would describe an unrelated run.
159
+ */
160
+ export declare function daemonPermissionException(socketPath: string, cause: string): Error;
161
+ /**
162
+ * Exported for testing: the `internalDaemonError` tag decides whether a daemon
163
+ * failure degrades to a daemonless graph build or aborts the command.
164
+ */
165
+ export declare function daemonProcessException(message: string): Error;
166
+ export {};
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.daemonClient = exports.DaemonClient = void 0;
3
+ exports.daemonClient = exports.DaemonClient = exports.WatcherFailedError = void 0;
4
4
  exports.isDaemonEnabled = isDaemonEnabled;
5
+ exports.isPermissionErrno = isPermissionErrno;
6
+ exports.daemonPermissionException = daemonPermissionException;
7
+ exports.daemonProcessException = daemonProcessException;
5
8
  const child_process_1 = require("child_process");
6
9
  const net_1 = require("net");
7
10
  const node_fs_1 = require("node:fs");
@@ -52,6 +55,18 @@ const WAIT_FOR_SERVER_CONFIG = {
52
55
  delayMs: 10,
53
56
  maxAttempts: 6000, // 6000 * 10ms = 60 seconds
54
57
  };
58
+ /**
59
+ * The daemon's workspace watcher died. Nothing it serves will see file changes
60
+ * again, so a watching client has to restart rather than keep waiting.
61
+ */
62
+ class WatcherFailedError extends Error {
63
+ constructor(message) {
64
+ super(message);
65
+ this.name = 'WatcherFailedError';
66
+ Object.setPrototypeOf(this, WatcherFailedError.prototype);
67
+ }
68
+ }
69
+ exports.WatcherFailedError = WatcherFailedError;
55
70
  class DaemonClient {
56
71
  constructor() {
57
72
  // Tracks the spinner owned by the in-flight request so streamed
@@ -177,12 +192,21 @@ class DaemonClient {
177
192
  return await this.sendToDaemonViaQueue({ type: 'REQUEST_FILE_DATA' });
178
193
  }
179
194
  hashTasks(runnerOptions, tasks, taskGraph, perTaskEnvs, cwd, collectInputs) {
195
+ // Task results get written back onto these task objects as the run
196
+ // progresses — hash/hashDetails/timestamps by hashing and the
197
+ // orchestrator, terminalOutput by the Nx Cloud life cycle (untyped) —
198
+ // so a later message would otherwise re-ship every earlier result.
199
+ const trimmedTasks = {};
200
+ for (const [id, t] of Object.entries(taskGraph.tasks)) {
201
+ const { hash, hashDetails, startTime, endTime, terminalOutput, ...strippedTask } = t;
202
+ trimmedTasks[id] = strippedTask;
203
+ }
180
204
  return this.sendToDaemonViaQueue({
181
205
  type: 'HASH_TASKS',
182
206
  runnerOptions,
183
207
  perTaskEnvs,
184
- tasks,
185
- taskGraph,
208
+ tasks: tasks.map((t) => trimmedTasks[t.id]),
209
+ taskGraph: { ...taskGraph, tasks: trimmedTasks },
186
210
  cwd,
187
211
  collectInputs,
188
212
  });
@@ -218,6 +242,13 @@ class DaemonClient {
218
242
  this.fileWatcherMessenger = new daemon_socket_messenger_1.DaemonSocketMessenger((0, net_1.connect)(socketPath)).listen((message) => {
219
243
  try {
220
244
  const parsedMessage = (0, consume_messages_from_socket_1.parseMessage)(message);
245
+ if (parsedMessage?.watcherError) {
246
+ const error = new WatcherFailedError(parsedMessage.watcherError);
247
+ for (const cb of this.fileWatcherCallbacks.values()) {
248
+ cb(error, null);
249
+ }
250
+ return;
251
+ }
221
252
  // Notify all callbacks
222
253
  for (const cb of this.fileWatcherCallbacks.values()) {
223
254
  cb(null, parsedMessage);
@@ -276,9 +307,9 @@ class DaemonClient {
276
307
  // Wait for daemon server to be available before trying to reconnect
277
308
  let serverAvailable;
278
309
  try {
279
- serverAvailable = await this.waitForServerToBeAvailable({
310
+ ({ available: serverAvailable } = await this.waitForServerToBeAvailable({
280
311
  ignoreVersionMismatch: false,
281
- });
312
+ }));
282
313
  }
283
314
  catch (err) {
284
315
  // Version mismatch - pass error to callbacks so they can handle it
@@ -428,9 +459,9 @@ class DaemonClient {
428
459
  // Wait for daemon server to be available before trying to reconnect
429
460
  let serverAvailable;
430
461
  try {
431
- serverAvailable = await this.waitForServerToBeAvailable({
462
+ ({ available: serverAvailable } = await this.waitForServerToBeAvailable({
432
463
  ignoreVersionMismatch: false,
433
- });
464
+ }));
434
465
  }
435
466
  catch (err) {
436
467
  // Version mismatch - pass error to callbacks so they can handle it
@@ -665,30 +696,40 @@ class DaemonClient {
665
696
  };
666
697
  return this.sendToDaemonViaQueue(message);
667
698
  }
668
- async isServerAvailable() {
699
+ /**
700
+ * The pre-start probe, returning why it failed rather than leaving it on the
701
+ * instance: `_daemonStatus` does not serialize `isServerAvailable`'s five
702
+ * public callers against `startDaemonIfNecessary`.
703
+ */
704
+ async probeServer() {
669
705
  return new Promise((resolve, reject) => {
670
706
  try {
671
707
  const socketPath = this.getSocketPath();
672
708
  if (!socketPath) {
673
- resolve(false);
709
+ resolve({ available: false });
674
710
  return;
675
711
  }
676
712
  const socket = (0, net_1.connect)(socketPath, () => {
677
713
  socket.destroy();
678
- resolve(true);
714
+ resolve({ available: true });
679
715
  });
680
- socket.once('error', () => {
681
- resolve(false);
716
+ socket.once('error', (err) => {
717
+ // The only place the errno for "the socket is there but refuses us"
718
+ // is produced, and the only thing separating it from "no daemon yet".
719
+ resolve({ available: false, refusal: { error: err, socketPath } });
682
720
  });
683
721
  }
684
722
  catch (err) {
685
723
  if (err instanceof daemon_socket_messenger_1.VersionMismatchError) {
686
724
  reject(err); // Let version mismatch bubble up
687
725
  }
688
- resolve(false);
726
+ resolve({ available: false });
689
727
  }
690
728
  });
691
729
  }
730
+ async isServerAvailable() {
731
+ return (await this.probeServer()).available;
732
+ }
692
733
  async startDaemonIfNecessary() {
693
734
  if (this._daemonStatus == DaemonStatus.CONNECTED) {
694
735
  return;
@@ -697,21 +738,22 @@ class DaemonClient {
697
738
  if (this._daemonStatus == DaemonStatus.DISCONNECTED) {
698
739
  this._daemonStatus = DaemonStatus.CONNECTING;
699
740
  let daemonPid = null;
700
- let serverAvailable;
741
+ let probe;
701
742
  try {
702
- serverAvailable = await this.isServerAvailable();
743
+ probe = await this.probeServer();
703
744
  }
704
745
  catch (err) {
705
746
  // Version mismatch - treat as server not available, start new one
706
747
  if (err instanceof daemon_socket_messenger_1.VersionMismatchError) {
707
- serverAvailable = false;
748
+ probe = { available: false };
708
749
  }
709
750
  else {
710
751
  throw err;
711
752
  }
712
753
  }
713
- if (!serverAvailable) {
714
- daemonPid = await this.startInBackground();
754
+ if (!probe.available) {
755
+ // Carried as a value so no other caller's probe can substitute for it.
756
+ daemonPid = await this.startInBackground(probe.refusal);
715
757
  }
716
758
  this.setUpConnection();
717
759
  this._daemonStatus = DaemonStatus.CONNECTED;
@@ -762,6 +804,11 @@ class DaemonClient {
762
804
  if (err.message.startsWith('connect ENOENT')) {
763
805
  error = daemonProcessException('The Daemon Server is not running');
764
806
  }
807
+ else if (isPermissionErrno(err)) {
808
+ // The 0700 dir and 0600 socket mean the OS refuses this rather than the
809
+ // connect silently succeeding.
810
+ error = daemonPermissionException(socketPath, err.message);
811
+ }
765
812
  else if (err.message.startsWith('connect ECONNREFUSED')) {
766
813
  error = daemonProcessException(`A server instance had not been fully shut down. Please try running the command again.`);
767
814
  }
@@ -782,9 +829,9 @@ class DaemonClient {
782
829
  this._daemonStatus = DaemonStatus.CONNECTING;
783
830
  let serverAvailable;
784
831
  try {
785
- serverAvailable = await this.waitForServerToBeAvailable({
832
+ ({ available: serverAvailable } = await this.waitForServerToBeAvailable({
786
833
  ignoreVersionMismatch: false,
787
- });
834
+ }));
788
835
  }
789
836
  catch (err) {
790
837
  if (err instanceof daemon_socket_messenger_1.VersionMismatchError) {
@@ -830,6 +877,11 @@ class DaemonClient {
830
877
  */
831
878
  async waitForServerToBeAvailable(options) {
832
879
  logger_1.clientLogger.log(`[Client] Waiting for server (max: ${WAIT_FOR_SERVER_CONFIG.maxAttempts} attempts, ${WAIT_FOR_SERVER_CONFIG.delayMs}ms interval)`);
880
+ // Poll-scoped, not instance state: reconnect paths have their own flags, so
881
+ // several attempts can be in flight and a shared slot would let one report
882
+ // another's socket.
883
+ let stoppedOnRefusal = false;
884
+ let refusal;
833
885
  const socket = await (0, wait_for_socket_connection_1.waitForSocketConnection)(() => {
834
886
  try {
835
887
  return this.getSocketPath();
@@ -846,14 +898,29 @@ class DaemonClient {
846
898
  }, {
847
899
  maxAttempts: WAIT_FOR_SERVER_CONFIG.maxAttempts,
848
900
  delayMs: WAIT_FOR_SERVER_CONFIG.delayMs,
901
+ onConnectError: (error, socketPath) => {
902
+ refusal = { error, socketPath };
903
+ // A refusal is not expected to become an acceptance, so polling the
904
+ // full 60s budget only delays the same answer. Not absolute:
905
+ // `server.ts` binds before it chmods to 0600, so under a umask that
906
+ // strips owner write a same-user connect can lose a microsecond race
907
+ // and see EACCES. That costs a specific error rather than a retry — a
908
+ // better trade than a 60s hang.
909
+ return (stoppedOnRefusal = isPermissionErrno(error));
910
+ },
849
911
  });
850
912
  if (socket) {
851
913
  socket.destroy();
852
914
  logger_1.clientLogger.log(`[Client] Server available`);
853
- return true;
915
+ return { available: true };
854
916
  }
855
- logger_1.clientLogger.log(`[Client] Server not available after ${WAIT_FOR_SERVER_CONFIG.maxAttempts} attempts`);
856
- return false;
917
+ // Keyed on the early exit taken, not on whether an errno was recorded:
918
+ // every failed connect records one, including an ordinary cold start's
919
+ // ENOENT.
920
+ logger_1.clientLogger.log(stoppedOnRefusal
921
+ ? `[Client] Server refused the connection (${refusal?.error.code}), stopped polling`
922
+ : `[Client] Server not available after ${WAIT_FOR_SERVER_CONFIG.maxAttempts} attempts`);
923
+ return { available: false, refusal };
857
924
  }
858
925
  async sendMessageToDaemon(message, force) {
859
926
  // the first message sent to the daemon includes an env prop
@@ -935,7 +1002,12 @@ class DaemonClient {
935
1002
  ].join('\n')));
936
1003
  }
937
1004
  }
938
- async startInBackground() {
1005
+ /**
1006
+ * @param probeRefusal what the caller's pre-start probe saw. The only evidence
1007
+ * when a daemon refuses us and then exits — the poll cannot reproduce it
1008
+ * once the process json is gone. `nx daemon --start` passes nothing.
1009
+ */
1010
+ async startInBackground(probeRefusal) {
939
1011
  if (global.NX_PLUGIN_WORKER) {
940
1012
  throw new Error('Fatal Error: Something unexpected has occurred. Plugin Workers should not start a new daemon process. Please report this issue.');
941
1013
  }
@@ -961,7 +1033,7 @@ class DaemonClient {
961
1033
  detached: true,
962
1034
  windowsHide: true,
963
1035
  shell: false,
964
- env: (0, daemon_environment_1.getDaemonEnv)(),
1036
+ env: (0, daemon_environment_1.getDaemonSpawnEnv)(),
965
1037
  });
966
1038
  // The child now owns dup'd copies of the descriptors, so release ours.
967
1039
  (0, node_fs_1.closeSync)(outFd);
@@ -973,14 +1045,25 @@ class DaemonClient {
973
1045
  /**
974
1046
  * Ensure the server is actually available to connect to via IPC before resolving
975
1047
  */
976
- const serverAvailable = await this.waitForServerToBeAvailable({
977
- ignoreVersionMismatch: true,
978
- });
979
- if (serverAvailable) {
1048
+ const { available, refusal: polled } = await this.waitForServerToBeAvailable({ ignoreVersionMismatch: true });
1049
+ if (available) {
980
1050
  logger_1.clientLogger.log(`[Client] Daemon server started, pid=${backgroundProcess.pid}`);
981
1051
  return backgroundProcess.pid;
982
1052
  }
983
1053
  else {
1054
+ // A permission refusal from either source wins, then the poll's errno,
1055
+ // then the probe's. Recency alone would report a daemon's ENOENT over the
1056
+ // EACCES the probe saw a moment earlier, losing the diagnosis.
1057
+ const refusal = [polled, probeRefusal].find((r) => r && isPermissionErrno(r.error)) ??
1058
+ polled ??
1059
+ probeRefusal;
1060
+ if (refusal && isPermissionErrno(refusal.error)) {
1061
+ // Reported here rather than as a generic startup failure, so it degrades
1062
+ // without disabling the daemon until `nx reset`. Both operands come from
1063
+ // the refusal: resolving a path here instead would throw once a daemon
1064
+ // that failed to bind has unlinked its process json.
1065
+ throw daemonPermissionException(refusal.socketPath, refusal.error.message);
1066
+ }
984
1067
  throw daemonProcessException('Failed to start or connect to the Nx Daemon process.');
985
1068
  }
986
1069
  }
@@ -1023,24 +1106,65 @@ function isDocker() {
1023
1106
  function nxJsonIsNotPresent() {
1024
1107
  return !(0, nx_json_1.hasNxJson)(workspace_root_1.workspaceRoot);
1025
1108
  }
1109
+ /**
1110
+ * EACCES and EPERM are the two errnos that mean the OS refused us rather than
1111
+ * that nothing was listening. They need opposite remedies — a socket owned by
1112
+ * someone else versus a sandbox refusing the connect syscall — but they share
1113
+ * the property that retrying cannot change the answer.
1114
+ */
1115
+ function isPermissionErrno(error) {
1116
+ return error?.code === 'EACCES' || error?.code === 'EPERM';
1117
+ }
1118
+ /**
1119
+ * The operating system refused the connection. Most often the socket belongs to
1120
+ * another user, which is the guarantee the owner-only socket directory buys —
1121
+ * but a sandbox that denies unix-socket connects produces the same errno, so the
1122
+ * message does not assert which. Either way it is an environment condition
1123
+ * rather than a defect in Nx, and it deliberately does not
1124
+ * carry `internalDaemonError`: that tag tells the user to file an issue and
1125
+ * disables the daemon until `nx reset`, which would outlast the stale socket
1126
+ * that caused it.
1127
+ *
1128
+ * It also skips the daemon log that `daemonProcessException` appends. The log
1129
+ * belongs to *our* daemon; the process holding this socket is someone else's, so
1130
+ * quoting it would describe an unrelated run.
1131
+ */
1132
+ function daemonPermissionException(socketPath, cause) {
1133
+ const error = new Error([
1134
+ `The operating system refused the connection to the Nx Daemon socket (${cause}).`,
1135
+ '',
1136
+ `Socket: ${socketPath}`,
1137
+ '',
1138
+ 'Most often the socket belongs to a different user: a daemon left behind by running Nx under `sudo`, a different uid inside a container, or a working copy shared between accounts. If the socket is your own, a sandbox is refusing the connection instead.',
1139
+ 'If it belongs to another user, delete the socket above or set NX_SOCKET_DIR to a directory only your user can reach. If you are in a sandbox, allow unix sockets under the Nx socket root — in Claude Code a scoped `allowUnixSockets` only permits connecting, so starting a daemon there needs `allowAllUnixSockets: true`. See https://nx.dev/docs/kb/nx-sandbox-unix-sockets',
1140
+ ].join('\n'));
1141
+ error.daemonPermissionError = true;
1142
+ return error;
1143
+ }
1144
+ /**
1145
+ * Exported for testing: the `internalDaemonError` tag decides whether a daemon
1146
+ * failure degrades to a daemonless graph build or aborts the command.
1147
+ */
1026
1148
  function daemonProcessException(message) {
1149
+ // The log is an enrichment, not the classifier: it is absent on a first run,
1150
+ // which is exactly when the daemon is most likely to fail to start.
1151
+ let body = message;
1027
1152
  try {
1028
1153
  let log = (0, node_fs_1.readFileSync)(tmp_dir_1.DAEMON_OUTPUT_LOG_FILE).toString().split('\n');
1029
1154
  if (log.length > 20) {
1030
1155
  log = log.slice(log.length - 20);
1031
1156
  }
1032
- const error = new Error([
1157
+ body = [
1033
1158
  message,
1034
1159
  '',
1035
1160
  'Messages from the log:',
1036
1161
  ...log,
1037
1162
  '\n',
1038
1163
  `More information: ${tmp_dir_1.DAEMON_OUTPUT_LOG_FILE}`,
1039
- ].join('\n'));
1040
- error.internalDaemonError = true;
1041
- return error;
1042
- }
1043
- catch (e) {
1044
- return new Error(message);
1164
+ ].join('\n');
1045
1165
  }
1166
+ catch { }
1167
+ const error = new Error(body);
1168
+ error.internalDaemonError = true;
1169
+ return error;
1046
1170
  }
@@ -2,6 +2,23 @@ export declare function getDaemonEnv(): NodeJS.ProcessEnv & {
2
2
  NX_PROJECT_GLOB_CACHE: string;
3
3
  NX_CACHE_PROJECTS_CONFIG: string;
4
4
  };
5
+ /**
6
+ * Env for spawning the daemon process. On top of the reflected env, it must
7
+ * keep excluded vars the daemon needs to start correctly:
8
+ * - ELECTRON_RUN_AS_NODE (matched by the ELECTRON_ prefix exclusion): when
9
+ * the spawning client runs inside an Electron host, process.execPath is
10
+ * the Electron binary and only this var makes it run the daemon's Node
11
+ * entry point.
12
+ * - NX_WORKSPACE_ROOT_PATH: the daemon resolves its workspace root at
13
+ * startup by walking up from cwd looking for workspace markers; without
14
+ * the pin, a root without markers under an ancestor that has them
15
+ * resolves to the ancestor and the daemon publishes its socket under the
16
+ * wrong workspace.
17
+ */
18
+ export declare function getDaemonSpawnEnv(): NodeJS.ProcessEnv & {
19
+ NX_PROJECT_GLOB_CACHE: string;
20
+ NX_CACHE_PROJECTS_CONFIG: string;
21
+ };
5
22
  /**
6
23
  * Without the deletion step, a var set by one client (e.g.
7
24
  * `NX_PREFER_NODE_STRIP_TYPES=true` or `JAVA_TOOL_OPTIONS=...` for a single
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getDaemonEnv = getDaemonEnv;
4
+ exports.getDaemonSpawnEnv = getDaemonSpawnEnv;
4
5
  exports.applyDaemonEnvFromClient = applyDaemonEnvFromClient;
5
6
  const DAEMON_ENV_REQUIRED_SETTINGS = {
6
7
  NX_PROJECT_GLOB_CACHE: 'false',
@@ -37,6 +38,12 @@ const DAEMON_ENV_VARS_EXCLUSIONS = new Set([
37
38
  'NX_BATCH_MODE',
38
39
  'NX_CI_EXECUTION_ID',
39
40
  'NX_DAEMON_PROCESS',
41
+ 'NX_CLI_SET',
42
+ // Set by Nx Console on the extension host; always equals the daemon's own
43
+ // workspace root (foreign-workspace messages are rejected), and the daemon
44
+ // resolves its root at startup before any client env is applied. The spawn
45
+ // env keeps it so that startup resolution honors the pinned root.
46
+ 'NX_WORKSPACE_ROOT_PATH',
40
47
  // Nx UI/logging vars (don't affect graph structure)
41
48
  'NX_TUI',
42
49
  'NX_TUI_AUTO_EXIT',
@@ -46,6 +53,7 @@ const DAEMON_ENV_VARS_EXCLUSIONS = new Set([
46
53
  'NX_NATIVE_LOGGING',
47
54
  'NX_PROFILE',
48
55
  'NX_DAEMON_VERBOSE_LOGGING',
56
+ 'NX_ORIGINAL_FORCE_COLOR',
49
57
  // AI agent detection vars (the daemon itself is not an AI agent)
50
58
  'CLAUDECODE',
51
59
  'CLAUDE_CODE',
@@ -54,9 +62,12 @@ const DAEMON_ENV_VARS_EXCLUSIONS = new Set([
54
62
  'COMPOSER_NO_INTERACTION',
55
63
  'OPENCODE',
56
64
  'GEMINI_CLI',
65
+ 'CODEX_THREAD_ID',
66
+ 'AI_AGENT',
57
67
  // Shell mechanics
58
68
  '_',
59
69
  'SHLVL',
70
+ 'PWD',
60
71
  'OLDPWD',
61
72
  'SHELL_SESSION_ID',
62
73
  'TERM_SESSION_ID',
@@ -66,6 +77,56 @@ const DAEMON_ENV_VARS_EXCLUSIONS = new Set([
66
77
  'COLUMNS',
67
78
  'LINES',
68
79
  'TMPDIR',
80
+ 'PROMPT',
81
+ // Package-manager per-invocation vars not covered by the npm_/pnpm_ prefixes
82
+ 'INIT_CWD',
83
+ 'COLOR',
84
+ // Per-shell-session state; these tools' other vars are stable or can be
85
+ // meaningful (e.g. fnm's FNM_DIR), so no prefix exclusion
86
+ 'FNM_MULTISHELL_PATH',
87
+ 'POSH_SESSION_ID',
88
+ 'MISE_SHELL',
89
+ // Editor preference; graph computation is non-interactive
90
+ 'EDITOR',
91
+ 'VISUAL',
92
+ // Output presentation
93
+ 'PAGER',
94
+ 'FORCE_COLOR',
95
+ 'NO_COLOR',
96
+ 'NODE_DISABLE_COLORS',
97
+ 'FORCE_HYPERLINK',
98
+ // Terminal identification/presentation; differs per terminal app, so it
99
+ // churns whenever clients from different terminals share the daemon
100
+ 'TERM',
101
+ 'TERM_PROGRAM',
102
+ 'TERM_PROGRAM_VERSION',
103
+ 'COLORTERM',
104
+ 'COLORFGBG',
105
+ 'VTE_VERSION',
106
+ 'GNOME_TERMINAL_SCREEN',
107
+ 'GNOME_TERMINAL_SERVICE',
108
+ 'WT_SESSION',
109
+ 'WT_PROFILE_ID',
110
+ 'TERMINAL_EMULATOR',
111
+ 'TERMINUS_SUBLIME',
112
+ 'ConEmuTask',
113
+ 'ZELLIJ_PANE_ID',
114
+ 'LC_TERMINAL',
115
+ 'LC_TERMINAL_VERSION',
116
+ // Injected by VS Code and its extensions into integrated terminals. Single
117
+ // entries rather than GIT_/CHROME_/COPILOT_ prefixes: GIT_DIR, CHROME_BIN,
118
+ // COPILOT_HOME and COPILOT_GITHUB_TOKEN are functional inputs.
119
+ 'GIT_ASKPASS',
120
+ 'GIT_EDITOR',
121
+ 'GIT_PAGER',
122
+ 'GIT_MERGE_AUTOEDIT',
123
+ 'APPLICATION_INSIGHTS_NO_STATSBEAT',
124
+ // Override for the @microsoft/mxc-sdk sandbox binaries; the SDK falls back
125
+ // to its bundled binaries when unset
126
+ 'MXC_BIN_DIR',
127
+ 'CHROME_CRASHPAD_PIPE_NAME',
128
+ 'COPILOT_OTEL_FILE_EXPORTER_PATH',
129
+ 'COPILOT_DEBUG_NONCE',
69
130
  // Session / auth
70
131
  'SSH_AUTH_SOCK',
71
132
  'SSH_AGENT_PID',
@@ -103,16 +164,37 @@ const DAEMON_ENV_PREFIX_EXCLUSIONS = [
103
164
  // Editors / IDEs
104
165
  'VSCODE_',
105
166
  'JETBRAINS_',
167
+ 'ELECTRON_',
168
+ // AI agent harnesses (per-session ids; the bare CLAUDECODE/CLAUDE_CODE
169
+ // detection vars are excluded above)
170
+ 'CLAUDE_CODE_',
171
+ // Shell prompts / integrations (per-command and per-session state)
172
+ 'ATUIN_',
173
+ 'STARSHIP_',
174
+ 'MCFLY_',
175
+ 'DIRENV_',
176
+ '__MISE_',
106
177
  // Terminal emulators
107
178
  'ITERM_',
108
179
  'KITTY_',
109
180
  'WEZTERM_',
110
181
  'ALACRITTY_',
111
182
  'KONSOLE_',
183
+ 'GHOSTTY_',
112
184
  'TMUX',
113
185
  // Benchmarking / profiling tools
114
186
  'HYPERFINE_', // hyperfine sets HYPERFINE_RANDOMIZED_ENVIRONMENT_OFFSET for each iteration
115
187
  ];
188
+ /**
189
+ * Env var name patterns that should never be sent to the daemon. For vars
190
+ * whose NAME embeds a process id, so neither an exact entry nor a safe
191
+ * prefix can match them.
192
+ */
193
+ const DAEMON_ENV_PATTERN_EXCLUSIONS = [
194
+ // Set by Windows 11 explorer.exe as EFC_<pid> (observed as EFC_<pid>_<hash>
195
+ // in Electron-spawned shells); regenerated on every login
196
+ /^EFC_\d+(_\d+)?$/,
197
+ ];
116
198
  /**
117
199
  * Vars that match an excluded prefix but should still reach the daemon. The
118
200
  * Nx Cloud auth/access tokens are excluded by the `NX_CLOUD_` prefix, but
@@ -125,11 +207,14 @@ const DAEMON_ENV_PREFIX_EXCLUSION_OVERRIDES = new Set([
125
207
  'NX_CLOUD_AUTH_TOKEN',
126
208
  ]);
127
209
  function isExcludedEnvVar(key) {
210
+ if (DAEMON_ENV_VARS_EXCLUSIONS.has(key) ||
211
+ DAEMON_ENV_PATTERN_EXCLUSIONS.some((pattern) => pattern.test(key))) {
212
+ return true;
213
+ }
128
214
  if (DAEMON_ENV_PREFIX_EXCLUSION_OVERRIDES.has(key)) {
129
215
  return false;
130
216
  }
131
- return (DAEMON_ENV_VARS_EXCLUSIONS.has(key) ||
132
- DAEMON_ENV_PREFIX_EXCLUSIONS.some((prefix) => key.startsWith(prefix)));
217
+ return DAEMON_ENV_PREFIX_EXCLUSIONS.some((prefix) => key.startsWith(prefix));
133
218
  }
134
219
  function getDaemonEnv() {
135
220
  const env = { ...DAEMON_ENV_OVERRIDABLE_SETTINGS };
@@ -140,6 +225,29 @@ function getDaemonEnv() {
140
225
  }
141
226
  return Object.assign(env, DAEMON_ENV_REQUIRED_SETTINGS);
142
227
  }
228
+ /**
229
+ * Env for spawning the daemon process. On top of the reflected env, it must
230
+ * keep excluded vars the daemon needs to start correctly:
231
+ * - ELECTRON_RUN_AS_NODE (matched by the ELECTRON_ prefix exclusion): when
232
+ * the spawning client runs inside an Electron host, process.execPath is
233
+ * the Electron binary and only this var makes it run the daemon's Node
234
+ * entry point.
235
+ * - NX_WORKSPACE_ROOT_PATH: the daemon resolves its workspace root at
236
+ * startup by walking up from cwd looking for workspace markers; without
237
+ * the pin, a root without markers under an ancestor that has them
238
+ * resolves to the ancestor and the daemon publishes its socket under the
239
+ * wrong workspace.
240
+ */
241
+ function getDaemonSpawnEnv() {
242
+ const env = getDaemonEnv();
243
+ if (process.env.ELECTRON_RUN_AS_NODE !== undefined) {
244
+ env.ELECTRON_RUN_AS_NODE = process.env.ELECTRON_RUN_AS_NODE;
245
+ }
246
+ if (process.env.NX_WORKSPACE_ROOT_PATH !== undefined) {
247
+ env.NX_WORKSPACE_ROOT_PATH = process.env.NX_WORKSPACE_ROOT_PATH;
248
+ }
249
+ return env;
250
+ }
143
251
  /**
144
252
  * Without the deletion step, a var set by one client (e.g.
145
253
  * `NX_PREFER_NODE_STRIP_TYPES=true` or `JAVA_TOOL_OPTIONS=...` for a single
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DaemonSocketMessenger = exports.VersionMismatchError = void 0;
4
4
  const perf_hooks_1 = require("perf_hooks");
5
5
  const consume_messages_from_socket_1 = require("../../utils/consume-messages-from-socket");
6
+ const workspace_root_1 = require("../../utils/workspace-root");
6
7
  const logger_1 = require("../logger");
7
8
  const socket_utils_1 = require("../socket-utils");
8
9
  class VersionMismatchError extends Error {
@@ -21,6 +22,9 @@ class DaemonSocketMessenger {
21
22
  if (!this.socket) {
22
23
  throw new Error('Socket not initialized.');
23
24
  }
25
+ // Stamp every message with the sending workspace's root so the daemon can
26
+ // reject messages from a different workspace (e.g. a shared NX_SOCKET_DIR).
27
+ messageToDaemon.workspaceRoot = workspace_root_1.workspaceRoot;
24
28
  logger_1.clientLogger.log('[Messenger] Sending message type:', messageToDaemon.type);
25
29
  perf_hooks_1.performance.mark('daemon-message-serialization-start-' + messageToDaemon.type);
26
30
  const serialized = (0, socket_utils_1.serialize)(messageToDaemon, force);