nx 23.2.0-beta.4 → 23.2.0-beta.6

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 (120) hide show
  1. package/dist/bin/nx.js +1 -0
  2. package/dist/src/command-line/affected/affected.js +1 -0
  3. package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +2 -2
  4. package/dist/src/command-line/init/init-v2.js +2 -2
  5. package/dist/src/command-line/migrate/agentic/definitions.js +2 -2
  6. package/dist/src/command-line/migrate/agentic/handoff-gitignore.d.ts +15 -19
  7. package/dist/src/command-line/migrate/agentic/handoff-gitignore.js +19 -31
  8. package/dist/src/command-line/migrate/agentic/handoff.d.ts +4 -6
  9. package/dist/src/command-line/migrate/agentic/handoff.js +9 -8
  10. package/dist/src/command-line/migrate/agentic/print-dropped-agent-context.d.ts +7 -12
  11. package/dist/src/command-line/migrate/agentic/print-dropped-agent-context.js +7 -0
  12. package/dist/src/command-line/migrate/agentic/run-step.d.ts +10 -0
  13. package/dist/src/command-line/migrate/agentic/select.d.ts +10 -2
  14. package/dist/src/command-line/migrate/agentic/select.js +10 -2
  15. package/dist/src/command-line/migrate/agentic/types.d.ts +17 -3
  16. package/dist/src/command-line/migrate/agentic/types.js +26 -0
  17. package/dist/src/command-line/migrate/command-object.d.ts +3 -0
  18. package/dist/src/command-line/migrate/command-object.js +6 -1
  19. package/dist/src/command-line/migrate/execute-migration.d.ts +14 -9
  20. package/dist/src/command-line/migrate/execute-migration.js +82 -18
  21. package/dist/src/command-line/migrate/migrate-analytics.d.ts +7 -0
  22. package/dist/src/command-line/migrate/migrate-analytics.js +14 -0
  23. package/dist/src/command-line/migrate/migrate-commits.d.ts +32 -8
  24. package/dist/src/command-line/migrate/migrate-commits.js +77 -9
  25. package/dist/src/command-line/migrate/migrate-config.d.ts +11 -12
  26. package/dist/src/command-line/migrate/migrate-config.js +14 -14
  27. package/dist/src/command-line/migrate/migrate-output.d.ts +45 -7
  28. package/dist/src/command-line/migrate/migrate-output.js +52 -8
  29. package/dist/src/command-line/migrate/migrate-ui-api.d.ts +2 -1
  30. package/dist/src/command-line/migrate/migrate-ui-api.js +16 -7
  31. package/dist/src/command-line/migrate/migrate.d.ts +17 -68
  32. package/dist/src/command-line/migrate/migrate.js +411 -229
  33. package/dist/src/command-line/migrate/migration-shape.d.ts +8 -0
  34. package/dist/src/command-line/migrate/run/index.d.ts +2 -0
  35. package/dist/src/command-line/migrate/run/index.js +5 -0
  36. package/dist/src/command-line/migrate/run/util.d.ts +1 -0
  37. package/dist/src/command-line/migrate/run/util.js +16 -0
  38. package/dist/src/command-line/migrate/run/worker.d.ts +15 -0
  39. package/dist/src/command-line/migrate/run/worker.js +419 -0
  40. package/dist/src/command-line/migrate/run-migration-process.js +6 -5
  41. package/dist/src/command-line/migrate/sort-migrations.d.ts +16 -0
  42. package/dist/src/command-line/migrate/sort-migrations.js +32 -0
  43. package/dist/src/command-line/migrate/version-skew-guard.d.ts +58 -0
  44. package/dist/src/command-line/migrate/version-skew-guard.js +155 -0
  45. package/dist/src/command-line/nx-cloud/start-nx-agents/command-object.d.ts +2 -0
  46. package/dist/src/command-line/nx-cloud/start-nx-agents/command-object.js +16 -0
  47. package/dist/src/command-line/nx-cloud/start-nx-agents/start-nx-agents.d.ts +4 -0
  48. package/dist/src/command-line/nx-cloud/start-nx-agents/start-nx-agents.js +13 -0
  49. package/dist/src/command-line/nx-commands.js +26 -24
  50. package/dist/src/command-line/release/changelog.js +5 -0
  51. package/dist/src/command-line/release/utils/git.js +1 -1
  52. package/dist/src/command-line/release/utils/release-graph.d.ts +1 -0
  53. package/dist/src/command-line/release/utils/release-graph.js +15 -3
  54. package/dist/src/command-line/release/version/version-actions.d.ts +8 -0
  55. package/dist/src/command-line/release/version/version-actions.js +8 -0
  56. package/dist/src/command-line/release/version.js +9 -5
  57. package/dist/src/command-line/reset/reset.js +6 -1
  58. package/dist/src/command-line/run/run-one.js +1 -0
  59. package/dist/src/command-line/run-many/run-many.js +1 -0
  60. package/dist/src/config/misc-interfaces.d.ts +19 -7
  61. package/dist/src/core/graph/main.js +1 -1
  62. package/dist/src/core/graph/styles.js +1 -1
  63. package/dist/src/daemon/client/client.d.ts +45 -1
  64. package/dist/src/daemon/client/client.js +128 -32
  65. package/dist/src/daemon/client/daemon-socket-messenger.js +4 -0
  66. package/dist/src/daemon/message-types/daemon-message.d.ts +32 -0
  67. package/dist/src/daemon/message-types/daemon-message.js +25 -0
  68. package/dist/src/daemon/server/latest-nx.js +5 -2
  69. package/dist/src/daemon/server/server.js +22 -0
  70. package/dist/src/daemon/server/shutdown-utils.d.ts +5 -0
  71. package/dist/src/daemon/server/shutdown-utils.js +11 -4
  72. package/dist/src/daemon/socket-utils.d.ts +1 -0
  73. package/dist/src/daemon/socket-utils.js +24 -3
  74. package/dist/src/daemon/tmp-dir.d.ts +43 -3
  75. package/dist/src/daemon/tmp-dir.js +437 -15
  76. package/dist/src/devkit-internals.d.ts +92 -3
  77. package/dist/src/devkit-internals.js +189 -5
  78. package/dist/src/generators/internal-utils/format-changed-files-with-prettier-if-available.d.ts +5 -0
  79. package/dist/src/generators/internal-utils/format-changed-files-with-prettier-if-available.js +9 -2
  80. package/dist/src/generators/tree.d.ts +4 -2
  81. package/dist/src/native/index.d.ts +10 -0
  82. package/dist/src/native/index.js +71 -28
  83. package/dist/src/native/native-bindings.js +1 -0
  84. package/dist/src/native/native-file-cache-location.d.ts +17 -1
  85. package/dist/src/native/native-file-cache-location.js +86 -12
  86. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  87. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  88. package/dist/src/nx-cloud/utilities/get-cloud-options.js +5 -2
  89. package/dist/src/nx-cloud/utilities/onboarding.js +5 -2
  90. package/dist/src/nx-cloud/utilities/url-shorten.d.ts +1 -4
  91. package/dist/src/nx-cloud/utilities/url-shorten.js +9 -8
  92. package/dist/src/plugins/js/lock-file/project-graph-pruning.js +14 -8
  93. package/dist/src/plugins/js/project-graph/build-dependencies/target-project-locator.js +2 -0
  94. package/dist/src/project-graph/affected/affected-project-graph-models.d.ts +1 -1
  95. package/dist/src/project-graph/affected/affected-project-graph.d.ts +1 -1
  96. package/dist/src/project-graph/affected/affected-project-graph.js +2 -2
  97. package/dist/src/project-graph/affected/locators/project-glob-changes.js +5 -2
  98. package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +1 -0
  99. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +18 -1
  100. package/dist/src/project-graph/plugins/isolation/messaging.d.ts +16 -3
  101. package/dist/src/project-graph/plugins/isolation/messaging.js +3 -0
  102. package/dist/src/project-graph/plugins/isolation/plugin-worker.js +58 -8
  103. package/dist/src/project-graph/project-graph.js +20 -0
  104. package/dist/src/tasks-runner/life-cycles/performance-report.d.ts +12 -0
  105. package/dist/src/tasks-runner/life-cycles/performance-report.js +62 -3
  106. package/dist/src/tasks-runner/run-command.js +4 -0
  107. package/dist/src/utils/assert-workspace-validity.js +7 -2
  108. package/dist/src/utils/child-process.d.ts +31 -1
  109. package/dist/src/utils/child-process.js +103 -0
  110. package/dist/src/utils/ignore.d.ts +128 -1
  111. package/dist/src/utils/ignore.js +182 -12
  112. package/dist/src/utils/nx-tmp-dir.d.ts +18 -0
  113. package/dist/src/utils/nx-tmp-dir.js +40 -0
  114. package/dist/src/utils/owned-private-dir.d.ts +132 -0
  115. package/dist/src/utils/owned-private-dir.js +323 -0
  116. package/dist/src/utils/shell-quoting.d.ts +15 -0
  117. package/dist/src/utils/shell-quoting.js +35 -0
  118. package/dist/src/utils/wait-for-socket-connection.d.ts +11 -0
  119. package/dist/src/utils/wait-for-socket-connection.js +14 -8
  120. package/package.json +12 -12
@@ -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,6 +8,11 @@ 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;
@@ -93,6 +98,12 @@ export declare class DaemonClient {
93
98
  resetConfigureAiAgentsStatus(): Promise<{
94
99
  success: boolean;
95
100
  }>;
101
+ /**
102
+ * The pre-start probe, returning why it failed rather than leaving it on the
103
+ * instance: `_daemonStatus` does not serialize `isServerAvailable`'s five
104
+ * public callers against `startDaemonIfNecessary`.
105
+ */
106
+ private probeServer;
96
107
  isServerAvailable(): Promise<boolean>;
97
108
  private startDaemonIfNecessary;
98
109
  private sendToDaemonViaQueue;
@@ -108,8 +119,41 @@ export declare class DaemonClient {
108
119
  private sendMessageToDaemon;
109
120
  private registerDaemonProcessWithMetricsService;
110
121
  private handleMessage;
111
- startInBackground(): Promise<ChildProcess['pid']>;
122
+ /**
123
+ * @param probeRefusal what the caller's pre-start probe saw. The only evidence
124
+ * when a daemon refuses us and then exits — the poll cannot reproduce it
125
+ * once the process json is gone. `nx daemon --start` passes nothing.
126
+ */
127
+ startInBackground(probeRefusal?: ConnectRefusal): Promise<ChildProcess['pid']>;
112
128
  stop(): Promise<void>;
113
129
  }
114
130
  export declare const daemonClient: DaemonClient;
115
131
  export declare function isDaemonEnabled(): boolean;
132
+ /**
133
+ * EACCES and EPERM are the two errnos that mean the OS refused us rather than
134
+ * that nothing was listening. They need opposite remedies — a socket owned by
135
+ * someone else versus a sandbox refusing the connect syscall — but they share
136
+ * the property that retrying cannot change the answer.
137
+ */
138
+ export declare function isPermissionErrno(error: NodeJS.ErrnoException): boolean;
139
+ /**
140
+ * The operating system refused the connection. Most often the socket belongs to
141
+ * another user, which is the guarantee the owner-only socket directory buys —
142
+ * but a sandbox that denies unix-socket connects produces the same errno, so the
143
+ * message does not assert which. Either way it is an environment condition
144
+ * rather than a defect in Nx, and it deliberately does not
145
+ * carry `internalDaemonError`: that tag tells the user to file an issue and
146
+ * disables the daemon until `nx reset`, which would outlast the stale socket
147
+ * that caused it.
148
+ *
149
+ * It also skips the daemon log that `daemonProcessException` appends. The log
150
+ * belongs to *our* daemon; the process holding this socket is someone else's, so
151
+ * quoting it would describe an unrelated run.
152
+ */
153
+ export declare function daemonPermissionException(socketPath: string, cause: string): Error;
154
+ /**
155
+ * Exported for testing: the `internalDaemonError` tag decides whether a daemon
156
+ * failure degrades to a daemonless graph build or aborts the command.
157
+ */
158
+ export declare function daemonProcessException(message: string): Error;
159
+ export {};
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.daemonClient = exports.DaemonClient = 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");
@@ -276,9 +279,9 @@ class DaemonClient {
276
279
  // Wait for daemon server to be available before trying to reconnect
277
280
  let serverAvailable;
278
281
  try {
279
- serverAvailable = await this.waitForServerToBeAvailable({
282
+ ({ available: serverAvailable } = await this.waitForServerToBeAvailable({
280
283
  ignoreVersionMismatch: false,
281
- });
284
+ }));
282
285
  }
283
286
  catch (err) {
284
287
  // Version mismatch - pass error to callbacks so they can handle it
@@ -428,9 +431,9 @@ class DaemonClient {
428
431
  // Wait for daemon server to be available before trying to reconnect
429
432
  let serverAvailable;
430
433
  try {
431
- serverAvailable = await this.waitForServerToBeAvailable({
434
+ ({ available: serverAvailable } = await this.waitForServerToBeAvailable({
432
435
  ignoreVersionMismatch: false,
433
- });
436
+ }));
434
437
  }
435
438
  catch (err) {
436
439
  // Version mismatch - pass error to callbacks so they can handle it
@@ -665,30 +668,40 @@ class DaemonClient {
665
668
  };
666
669
  return this.sendToDaemonViaQueue(message);
667
670
  }
668
- async isServerAvailable() {
671
+ /**
672
+ * The pre-start probe, returning why it failed rather than leaving it on the
673
+ * instance: `_daemonStatus` does not serialize `isServerAvailable`'s five
674
+ * public callers against `startDaemonIfNecessary`.
675
+ */
676
+ async probeServer() {
669
677
  return new Promise((resolve, reject) => {
670
678
  try {
671
679
  const socketPath = this.getSocketPath();
672
680
  if (!socketPath) {
673
- resolve(false);
681
+ resolve({ available: false });
674
682
  return;
675
683
  }
676
684
  const socket = (0, net_1.connect)(socketPath, () => {
677
685
  socket.destroy();
678
- resolve(true);
686
+ resolve({ available: true });
679
687
  });
680
- socket.once('error', () => {
681
- resolve(false);
688
+ socket.once('error', (err) => {
689
+ // The only place the errno for "the socket is there but refuses us"
690
+ // is produced, and the only thing separating it from "no daemon yet".
691
+ resolve({ available: false, refusal: { error: err, socketPath } });
682
692
  });
683
693
  }
684
694
  catch (err) {
685
695
  if (err instanceof daemon_socket_messenger_1.VersionMismatchError) {
686
696
  reject(err); // Let version mismatch bubble up
687
697
  }
688
- resolve(false);
698
+ resolve({ available: false });
689
699
  }
690
700
  });
691
701
  }
702
+ async isServerAvailable() {
703
+ return (await this.probeServer()).available;
704
+ }
692
705
  async startDaemonIfNecessary() {
693
706
  if (this._daemonStatus == DaemonStatus.CONNECTED) {
694
707
  return;
@@ -697,21 +710,22 @@ class DaemonClient {
697
710
  if (this._daemonStatus == DaemonStatus.DISCONNECTED) {
698
711
  this._daemonStatus = DaemonStatus.CONNECTING;
699
712
  let daemonPid = null;
700
- let serverAvailable;
713
+ let probe;
701
714
  try {
702
- serverAvailable = await this.isServerAvailable();
715
+ probe = await this.probeServer();
703
716
  }
704
717
  catch (err) {
705
718
  // Version mismatch - treat as server not available, start new one
706
719
  if (err instanceof daemon_socket_messenger_1.VersionMismatchError) {
707
- serverAvailable = false;
720
+ probe = { available: false };
708
721
  }
709
722
  else {
710
723
  throw err;
711
724
  }
712
725
  }
713
- if (!serverAvailable) {
714
- daemonPid = await this.startInBackground();
726
+ if (!probe.available) {
727
+ // Carried as a value so no other caller's probe can substitute for it.
728
+ daemonPid = await this.startInBackground(probe.refusal);
715
729
  }
716
730
  this.setUpConnection();
717
731
  this._daemonStatus = DaemonStatus.CONNECTED;
@@ -762,6 +776,11 @@ class DaemonClient {
762
776
  if (err.message.startsWith('connect ENOENT')) {
763
777
  error = daemonProcessException('The Daemon Server is not running');
764
778
  }
779
+ else if (isPermissionErrno(err)) {
780
+ // The 0700 dir and 0600 socket mean the OS refuses this rather than the
781
+ // connect silently succeeding.
782
+ error = daemonPermissionException(socketPath, err.message);
783
+ }
765
784
  else if (err.message.startsWith('connect ECONNREFUSED')) {
766
785
  error = daemonProcessException(`A server instance had not been fully shut down. Please try running the command again.`);
767
786
  }
@@ -782,9 +801,9 @@ class DaemonClient {
782
801
  this._daemonStatus = DaemonStatus.CONNECTING;
783
802
  let serverAvailable;
784
803
  try {
785
- serverAvailable = await this.waitForServerToBeAvailable({
804
+ ({ available: serverAvailable } = await this.waitForServerToBeAvailable({
786
805
  ignoreVersionMismatch: false,
787
- });
806
+ }));
788
807
  }
789
808
  catch (err) {
790
809
  if (err instanceof daemon_socket_messenger_1.VersionMismatchError) {
@@ -830,6 +849,11 @@ class DaemonClient {
830
849
  */
831
850
  async waitForServerToBeAvailable(options) {
832
851
  logger_1.clientLogger.log(`[Client] Waiting for server (max: ${WAIT_FOR_SERVER_CONFIG.maxAttempts} attempts, ${WAIT_FOR_SERVER_CONFIG.delayMs}ms interval)`);
852
+ // Poll-scoped, not instance state: reconnect paths have their own flags, so
853
+ // several attempts can be in flight and a shared slot would let one report
854
+ // another's socket.
855
+ let stoppedOnRefusal = false;
856
+ let refusal;
833
857
  const socket = await (0, wait_for_socket_connection_1.waitForSocketConnection)(() => {
834
858
  try {
835
859
  return this.getSocketPath();
@@ -846,14 +870,29 @@ class DaemonClient {
846
870
  }, {
847
871
  maxAttempts: WAIT_FOR_SERVER_CONFIG.maxAttempts,
848
872
  delayMs: WAIT_FOR_SERVER_CONFIG.delayMs,
873
+ onConnectError: (error, socketPath) => {
874
+ refusal = { error, socketPath };
875
+ // A refusal is not expected to become an acceptance, so polling the
876
+ // full 60s budget only delays the same answer. Not absolute:
877
+ // `server.ts` binds before it chmods to 0600, so under a umask that
878
+ // strips owner write a same-user connect can lose a microsecond race
879
+ // and see EACCES. That costs a specific error rather than a retry — a
880
+ // better trade than a 60s hang.
881
+ return (stoppedOnRefusal = isPermissionErrno(error));
882
+ },
849
883
  });
850
884
  if (socket) {
851
885
  socket.destroy();
852
886
  logger_1.clientLogger.log(`[Client] Server available`);
853
- return true;
887
+ return { available: true };
854
888
  }
855
- logger_1.clientLogger.log(`[Client] Server not available after ${WAIT_FOR_SERVER_CONFIG.maxAttempts} attempts`);
856
- return false;
889
+ // Keyed on the early exit taken, not on whether an errno was recorded:
890
+ // every failed connect records one, including an ordinary cold start's
891
+ // ENOENT.
892
+ logger_1.clientLogger.log(stoppedOnRefusal
893
+ ? `[Client] Server refused the connection (${refusal?.error.code}), stopped polling`
894
+ : `[Client] Server not available after ${WAIT_FOR_SERVER_CONFIG.maxAttempts} attempts`);
895
+ return { available: false, refusal };
857
896
  }
858
897
  async sendMessageToDaemon(message, force) {
859
898
  // the first message sent to the daemon includes an env prop
@@ -935,7 +974,12 @@ class DaemonClient {
935
974
  ].join('\n')));
936
975
  }
937
976
  }
938
- async startInBackground() {
977
+ /**
978
+ * @param probeRefusal what the caller's pre-start probe saw. The only evidence
979
+ * when a daemon refuses us and then exits — the poll cannot reproduce it
980
+ * once the process json is gone. `nx daemon --start` passes nothing.
981
+ */
982
+ async startInBackground(probeRefusal) {
939
983
  if (global.NX_PLUGIN_WORKER) {
940
984
  throw new Error('Fatal Error: Something unexpected has occurred. Plugin Workers should not start a new daemon process. Please report this issue.');
941
985
  }
@@ -973,14 +1017,25 @@ class DaemonClient {
973
1017
  /**
974
1018
  * Ensure the server is actually available to connect to via IPC before resolving
975
1019
  */
976
- const serverAvailable = await this.waitForServerToBeAvailable({
977
- ignoreVersionMismatch: true,
978
- });
979
- if (serverAvailable) {
1020
+ const { available, refusal: polled } = await this.waitForServerToBeAvailable({ ignoreVersionMismatch: true });
1021
+ if (available) {
980
1022
  logger_1.clientLogger.log(`[Client] Daemon server started, pid=${backgroundProcess.pid}`);
981
1023
  return backgroundProcess.pid;
982
1024
  }
983
1025
  else {
1026
+ // A permission refusal from either source wins, then the poll's errno,
1027
+ // then the probe's. Recency alone would report a daemon's ENOENT over the
1028
+ // EACCES the probe saw a moment earlier, losing the diagnosis.
1029
+ const refusal = [polled, probeRefusal].find((r) => r && isPermissionErrno(r.error)) ??
1030
+ polled ??
1031
+ probeRefusal;
1032
+ if (refusal && isPermissionErrno(refusal.error)) {
1033
+ // Reported here rather than as a generic startup failure, so it degrades
1034
+ // without disabling the daemon until `nx reset`. Both operands come from
1035
+ // the refusal: resolving a path here instead would throw once a daemon
1036
+ // that failed to bind has unlinked its process json.
1037
+ throw daemonPermissionException(refusal.socketPath, refusal.error.message);
1038
+ }
984
1039
  throw daemonProcessException('Failed to start or connect to the Nx Daemon process.');
985
1040
  }
986
1041
  }
@@ -1023,24 +1078,65 @@ function isDocker() {
1023
1078
  function nxJsonIsNotPresent() {
1024
1079
  return !(0, nx_json_1.hasNxJson)(workspace_root_1.workspaceRoot);
1025
1080
  }
1081
+ /**
1082
+ * EACCES and EPERM are the two errnos that mean the OS refused us rather than
1083
+ * that nothing was listening. They need opposite remedies — a socket owned by
1084
+ * someone else versus a sandbox refusing the connect syscall — but they share
1085
+ * the property that retrying cannot change the answer.
1086
+ */
1087
+ function isPermissionErrno(error) {
1088
+ return error?.code === 'EACCES' || error?.code === 'EPERM';
1089
+ }
1090
+ /**
1091
+ * The operating system refused the connection. Most often the socket belongs to
1092
+ * another user, which is the guarantee the owner-only socket directory buys —
1093
+ * but a sandbox that denies unix-socket connects produces the same errno, so the
1094
+ * message does not assert which. Either way it is an environment condition
1095
+ * rather than a defect in Nx, and it deliberately does not
1096
+ * carry `internalDaemonError`: that tag tells the user to file an issue and
1097
+ * disables the daemon until `nx reset`, which would outlast the stale socket
1098
+ * that caused it.
1099
+ *
1100
+ * It also skips the daemon log that `daemonProcessException` appends. The log
1101
+ * belongs to *our* daemon; the process holding this socket is someone else's, so
1102
+ * quoting it would describe an unrelated run.
1103
+ */
1104
+ function daemonPermissionException(socketPath, cause) {
1105
+ const error = new Error([
1106
+ `The operating system refused the connection to the Nx Daemon socket (${cause}).`,
1107
+ '',
1108
+ `Socket: ${socketPath}`,
1109
+ '',
1110
+ '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.',
1111
+ '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',
1112
+ ].join('\n'));
1113
+ error.daemonPermissionError = true;
1114
+ return error;
1115
+ }
1116
+ /**
1117
+ * Exported for testing: the `internalDaemonError` tag decides whether a daemon
1118
+ * failure degrades to a daemonless graph build or aborts the command.
1119
+ */
1026
1120
  function daemonProcessException(message) {
1121
+ // The log is an enrichment, not the classifier: it is absent on a first run,
1122
+ // which is exactly when the daemon is most likely to fail to start.
1123
+ let body = message;
1027
1124
  try {
1028
1125
  let log = (0, node_fs_1.readFileSync)(tmp_dir_1.DAEMON_OUTPUT_LOG_FILE).toString().split('\n');
1029
1126
  if (log.length > 20) {
1030
1127
  log = log.slice(log.length - 20);
1031
1128
  }
1032
- const error = new Error([
1129
+ body = [
1033
1130
  message,
1034
1131
  '',
1035
1132
  'Messages from the log:',
1036
1133
  ...log,
1037
1134
  '\n',
1038
1135
  `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);
1136
+ ].join('\n');
1045
1137
  }
1138
+ catch { }
1139
+ const error = new Error(body);
1140
+ error.internalDaemonError = true;
1141
+ return error;
1046
1142
  }
@@ -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);
@@ -1,6 +1,38 @@
1
1
  export type DaemonMessage = {
2
2
  type: string;
3
3
  env?: Record<string, string>;
4
+ /**
5
+ * Stamped centrally by `DaemonSocketMessenger.sendMessage`, not by the ~30
6
+ * sites that construct messages — hence optional. It can also be absent on a
7
+ * message that predates the field, which is why `isForeignWorkspaceMessage`
8
+ * treats `undefined` as "not foreign".
9
+ */
10
+ workspaceRoot?: string;
4
11
  data?: any;
5
12
  };
6
13
  export declare function isDaemonMessage(msg: unknown): msg is DaemonMessage;
14
+ /**
15
+ * Any message carrying the sender's workspace root. Structural so the checks
16
+ * below serve the daemon and the plugin worker without either importing the
17
+ * other's message type. `type` is required only so the type is not all-optional,
18
+ * which would accept `{}`.
19
+ */
20
+ type WorkspaceScopedMessage = {
21
+ type: string;
22
+ workspaceRoot?: string;
23
+ };
24
+ /**
25
+ * A message from a different workspace — two sharing an NX_SOCKET_DIR — must not
26
+ * be processed. Compared directly; both come from the same resolution.
27
+ */
28
+ export declare function isForeignWorkspaceMessage(msg: WorkspaceScopedMessage, receiverWorkspaceRoot: string): boolean;
29
+ /**
30
+ * Throws on a message this receiver must not act on. The daemon catches it and
31
+ * responds with the mismatch; the plugin worker catches it and drops the
32
+ * message. `receiverDescription` names whichever raised it.
33
+ *
34
+ * An unstamped message is deliberately accepted: this is accident detection,
35
+ * not a control.
36
+ */
37
+ export declare function assertNotForeignWorkspaceMessage(msg: WorkspaceScopedMessage, receiverWorkspaceRoot: string, receiverDescription?: string): void;
38
+ export {};
@@ -1,6 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isDaemonMessage = isDaemonMessage;
4
+ exports.isForeignWorkspaceMessage = isForeignWorkspaceMessage;
5
+ exports.assertNotForeignWorkspaceMessage = assertNotForeignWorkspaceMessage;
4
6
  function isDaemonMessage(msg) {
5
7
  return typeof msg === 'object' && msg && 'type' in msg;
6
8
  }
9
+ /**
10
+ * A message from a different workspace — two sharing an NX_SOCKET_DIR — must not
11
+ * be processed. Compared directly; both come from the same resolution.
12
+ */
13
+ function isForeignWorkspaceMessage(msg, receiverWorkspaceRoot) {
14
+ if (msg.workspaceRoot === undefined) {
15
+ return false;
16
+ }
17
+ return msg.workspaceRoot !== receiverWorkspaceRoot;
18
+ }
19
+ /**
20
+ * Throws on a message this receiver must not act on. The daemon catches it and
21
+ * responds with the mismatch; the plugin worker catches it and drops the
22
+ * message. `receiverDescription` names whichever raised it.
23
+ *
24
+ * An unstamped message is deliberately accepted: this is accident detection,
25
+ * not a control.
26
+ */
27
+ function assertNotForeignWorkspaceMessage(msg, receiverWorkspaceRoot, receiverDescription = `The Nx Daemon for '${receiverWorkspaceRoot}'`) {
28
+ if (isForeignWorkspaceMessage(msg, receiverWorkspaceRoot)) {
29
+ throw new Error(`${receiverDescription} received a message from a different workspace ('${msg.workspaceRoot}') and refused to process it. This usually means multiple workspaces are sharing a socket directory; ensure NX_SOCKET_DIR (or NX_DAEMON_SOCKET_DIR) is not set to a shared location.`);
30
+ }
31
+ }
@@ -2,7 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getLatestNxTmpPath = getLatestNxTmpPath;
4
4
  exports.cleanupLatestNx = cleanupLatestNx;
5
- const devkit_internals_1 = require("../../devkit-internals");
5
+ // Import from the defining module, not the devkit-internals barrel: the barrel
6
+ // eagerly pulls in the task-execution subsystem and instantiates a daemon
7
+ // client, all of which would stay resident for the daemon server's lifetime.
8
+ const package_json_1 = require("../../utils/package-json");
6
9
  const package_manager_1 = require("../../utils/package-manager");
7
10
  const provenance_1 = require("../../utils/provenance");
8
11
  const workspace_root_1 = require("../../utils/workspace-root");
@@ -28,7 +31,7 @@ async function getLatestNxTmpPath() {
28
31
  try {
29
32
  logger_1.serverLogger.log('[LATEST-NX]: Pulling latest Nx...');
30
33
  await (0, provenance_1.ensurePackageHasProvenance)('nx', 'latest');
31
- const result = await (0, devkit_internals_1.installPackageToTmpAsync)('nx', 'latest', (0, package_manager_1.detectPackageManager)(workspace_root_1.workspaceRoot));
34
+ const result = await (0, package_json_1.installPackageToTmpAsync)('nx', 'latest', (0, package_manager_1.detectPackageManager)(workspace_root_1.workspaceRoot));
32
35
  latestNxTmpPath = result.tempDir;
33
36
  cleanupFn = result.cleanup;
34
37
  logger_1.serverLogger.log('[LATEST-NX]: Successfully pulled latest Nx to', latestNxTmpPath);
@@ -119,6 +119,17 @@ async function handleMessage(socket, data) {
119
119
  await (0, shutdown_utils_1.respondWithErrorAndExit)(socket, `Invalid payload from the client`, new Error(`Unsupported payload sent to daemon server: ${unparsedPayload}`));
120
120
  }
121
121
  logger_1.serverLogger.log(`Received ${mode} message of type ${payload.type}`);
122
+ // A mismatch means the client reached the wrong daemon (e.g. a shared
123
+ // NX_SOCKET_DIR). Respond, but stay alive for our own workspace.
124
+ if ((0, daemon_message_1.isDaemonMessage)(payload)) {
125
+ try {
126
+ (0, daemon_message_1.assertNotForeignWorkspaceMessage)(payload, workspace_root_1.workspaceRoot);
127
+ }
128
+ catch (e) {
129
+ await (0, shutdown_utils_1.respondWithError)(socket, `Workspace root mismatch`, e);
130
+ return;
131
+ }
132
+ }
122
133
  if ((0, daemon_message_1.isDaemonMessage)(payload) && payload.env) {
123
134
  const changedEnvKeys = (0, daemon_environment_1.applyDaemonEnvFromClient)(payload.env);
124
135
  if (changedEnvKeys.length > 0) {
@@ -423,6 +434,17 @@ async function startServer() {
423
434
  server.listen(socketPath, async () => {
424
435
  try {
425
436
  logger_1.serverLogger.log(`Started listening on: ${socketPath}`);
437
+ // Linux gates connect() on write permission to the socket file; macOS/BSD gate
438
+ // on the directory, which is already 0700. Done after listen because
439
+ // net.Server.listen takes no mode and umask is process-global.
440
+ if (!socket_utils_1.isWindows) {
441
+ try {
442
+ (0, fs_1.chmodSync)(socketPath, 0o600);
443
+ }
444
+ catch {
445
+ // Best effort; the 0700 socket directory is the primary control.
446
+ }
447
+ }
426
448
  // this triggers the storage of the lock file hash
427
449
  daemonIsOutdated();
428
450
  if (!(0, shutdown_utils_1.getOutputWatcherInstance)()) {
@@ -14,5 +14,10 @@ export declare function handleServerProcessTermination({ server, reason, sockets
14
14
  export declare function handleServerProcessTerminationWithRestart({ server, reason, sockets, }: HandleServerProcessTerminationParams): Promise<void>;
15
15
  export declare function resetInactivityTimeout(cb: () => void): void;
16
16
  export declare function respondToClient(socket: Socket, response: string, description: string): Promise<unknown>;
17
+ /**
18
+ * Send an error to the client without terminating the daemon. Use this when the
19
+ * daemon should keep running for other clients after refusing a request.
20
+ */
21
+ export declare function respondWithError(socket: Socket, description: string, error: Error): Promise<void>;
17
22
  export declare function respondWithErrorAndExit(socket: Socket, description: string, error: Error): Promise<void>;
18
23
  export {};
@@ -9,6 +9,7 @@ exports.handleServerProcessTermination = handleServerProcessTermination;
9
9
  exports.handleServerProcessTerminationWithRestart = handleServerProcessTerminationWithRestart;
10
10
  exports.resetInactivityTimeout = resetInactivityTimeout;
11
11
  exports.respondToClient = respondToClient;
12
+ exports.respondWithError = respondWithError;
12
13
  exports.respondWithErrorAndExit = respondWithErrorAndExit;
13
14
  const workspace_root_1 = require("../../utils/workspace-root");
14
15
  const logger_1 = require("../logger");
@@ -135,9 +136,12 @@ function respondToClient(socket, response, description) {
135
136
  });
136
137
  });
137
138
  }
138
- async function respondWithErrorAndExit(socket, description, error) {
139
- const isProjectGraphError = error instanceof error_types_1.DaemonProjectGraphError;
140
- const normalizedError = isProjectGraphError
139
+ /**
140
+ * Send an error to the client without terminating the daemon. Use this when the
141
+ * daemon should keep running for other clients after refusing a request.
142
+ */
143
+ async function respondWithError(socket, description, error) {
144
+ const normalizedError = error instanceof error_types_1.DaemonProjectGraphError
141
145
  ? error_types_1.ProjectGraphError.fromDaemonProjectGraphError(error)
142
146
  : error;
143
147
  // print some extra stuff in the error message
@@ -145,8 +149,11 @@ async function respondWithErrorAndExit(socket, description, error) {
145
149
  console.error(normalizedError.stack);
146
150
  // Respond with the original error
147
151
  await respondToClient(socket, (0, socket_utils_1.serializeResult)(error, null, null), null);
152
+ }
153
+ async function respondWithErrorAndExit(socket, description, error) {
154
+ await respondWithError(socket, description, error);
148
155
  // Project Graph errors are okay. Restarting the daemon won't help with this.
149
- if (!isProjectGraphError) {
156
+ if (!(error instanceof error_types_1.DaemonProjectGraphError)) {
150
157
  process.exit(1);
151
158
  }
152
159
  }
@@ -8,6 +8,7 @@ export declare const isWindows: boolean;
8
8
  export declare const getFullOsSocketPath: () => string;
9
9
  export declare const getForkedProcessOsSocketPath: (id: string) => string;
10
10
  export declare const getPluginOsSocketPath: (id: string) => string;
11
+ export declare function getPluginSocketFileName(id: string): string;
11
12
  export declare function killSocketOrPath(): void;
12
13
  export declare function serializeResult(error: Error | null, serializedProjectGraph: string | null, serializedSourceMaps: string | null): string | null;
13
14
  /**
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getPluginOsSocketPath = exports.getForkedProcessOsSocketPath = exports.getFullOsSocketPath = exports.isWindows = void 0;
4
+ exports.getPluginSocketFileName = getPluginSocketFileName;
4
5
  exports.killSocketOrPath = killSocketOrPath;
5
6
  exports.serializeResult = serializeResult;
6
7
  exports.serialize = serialize;
@@ -31,18 +32,38 @@ const getForkedProcessOsSocketPath = (id) => {
31
32
  };
32
33
  exports.getForkedProcessOsSocketPath = getForkedProcessOsSocketPath;
33
34
  const getPluginOsSocketPath = (id) => {
34
- let path = (0, path_1.resolve)((0, path_1.join)((0, tmp_dir_1.getSocketDir)(true), 'plugin' + id + '.sock'));
35
+ let path = (0, path_1.resolve)((0, path_1.join)((0, tmp_dir_1.getPluginSocketDir)(), getPluginSocketFileName(id)));
35
36
  assertValidSocketPath(path);
36
37
  return exports.isWindows ? '\\\\.\\pipe\\nx\\' + path : path;
37
38
  };
38
39
  exports.getPluginOsSocketPath = getPluginOsSocketPath;
40
+ function getPluginSocketFileName(id) {
41
+ return `p${id}.sock`;
42
+ }
39
43
  function assertValidSocketPath(path) {
40
44
  if (path.length > 95) {
45
+ const fallbackCause = (0, tmp_dir_1.getSocketDirFallbackCause)();
46
+ const refusedConfiguredSocketDir = (0, tmp_dir_1.getRefusedConfiguredSocketDir)();
41
47
  throw new Error([
42
48
  'Attempted to open socket that exceeds the maximum socket length.',
49
+ ...(fallbackCause === undefined
50
+ ? []
51
+ : [
52
+ `Nx fell back to ${(0, path_1.dirname)(path)} because the default socket directory could not be used.`,
53
+ 'Run the command with --verbose to see why the default directory was rejected.',
54
+ ]),
43
55
  '',
44
- `Set NX_SOCKET_DIR to a shorter path (e.g. ${exports.isWindows ? '%TMP%/nx-tmp' : '/tmp/nx-tmp'}) to avoid this issue.`,
45
- ].join('\n'));
56
+ ...(refusedConfiguredSocketDir === undefined
57
+ ? [
58
+ `Set NX_SOCKET_DIR to a shorter path (e.g. ${exports.isWindows ? '%TMP%/nx-tmp' : '/tmp/nx-tmp'}) to avoid this issue.`,
59
+ ]
60
+ : [
61
+ // Saying "set a shorter path" here would be advice they already
62
+ // followed: they set one, and it was refused for another reason.
63
+ `The directory set in NX_SOCKET_DIR (${refusedConfiguredSocketDir}) could not be used — see the warning above — so Nx fell back to a longer path.`,
64
+ 'Point NX_SOCKET_DIR at a short directory your user owns.',
65
+ ]),
66
+ ].join('\n'), fallbackCause === undefined ? undefined : { cause: fallbackCause });
46
67
  }
47
68
  }
48
69
  function killSocketOrPath() {