@metamask-previews/wallet-cli 0.0.0-preview-153490ebd → 0.0.0-preview-ed0047684

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 (64) hide show
  1. package/CHANGELOG.md +5 -2
  2. package/dist/daemon/daemon-client.cjs +140 -0
  3. package/dist/daemon/daemon-client.cjs.map +1 -0
  4. package/dist/daemon/daemon-client.d.cts +79 -0
  5. package/dist/daemon/daemon-client.d.cts.map +1 -0
  6. package/dist/daemon/daemon-client.d.mts +79 -0
  7. package/dist/daemon/daemon-client.d.mts.map +1 -0
  8. package/dist/daemon/daemon-client.mjs +135 -0
  9. package/dist/daemon/daemon-client.mjs.map +1 -0
  10. package/dist/daemon/daemon-spawn.cjs +106 -0
  11. package/dist/daemon/daemon-spawn.cjs.map +1 -0
  12. package/dist/daemon/daemon-spawn.d.cts +28 -0
  13. package/dist/daemon/daemon-spawn.d.cts.map +1 -0
  14. package/dist/daemon/daemon-spawn.d.mts +28 -0
  15. package/dist/daemon/daemon-spawn.d.mts.map +1 -0
  16. package/dist/daemon/daemon-spawn.mjs +102 -0
  17. package/dist/daemon/daemon-spawn.mjs.map +1 -0
  18. package/dist/daemon/prompts.cjs +21 -0
  19. package/dist/daemon/prompts.cjs.map +1 -0
  20. package/dist/daemon/prompts.d.cts +11 -0
  21. package/dist/daemon/prompts.d.cts.map +1 -0
  22. package/dist/daemon/prompts.d.mts +11 -0
  23. package/dist/daemon/prompts.d.mts.map +1 -0
  24. package/dist/daemon/prompts.mjs +17 -0
  25. package/dist/daemon/prompts.mjs.map +1 -0
  26. package/dist/daemon/rpc-socket-server.cjs +271 -0
  27. package/dist/daemon/rpc-socket-server.cjs.map +1 -0
  28. package/dist/daemon/rpc-socket-server.d.cts +44 -0
  29. package/dist/daemon/rpc-socket-server.d.cts.map +1 -0
  30. package/dist/daemon/rpc-socket-server.d.mts +44 -0
  31. package/dist/daemon/rpc-socket-server.d.mts.map +1 -0
  32. package/dist/daemon/rpc-socket-server.mjs +267 -0
  33. package/dist/daemon/rpc-socket-server.mjs.map +1 -0
  34. package/dist/daemon/socket-line.cjs +89 -0
  35. package/dist/daemon/socket-line.cjs.map +1 -0
  36. package/dist/daemon/socket-line.d.cts +19 -0
  37. package/dist/daemon/socket-line.d.cts.map +1 -0
  38. package/dist/daemon/socket-line.d.mts +19 -0
  39. package/dist/daemon/socket-line.d.mts.map +1 -0
  40. package/dist/daemon/socket-line.mjs +84 -0
  41. package/dist/daemon/socket-line.mjs.map +1 -0
  42. package/dist/daemon/stop-daemon.cjs +103 -0
  43. package/dist/daemon/stop-daemon.cjs.map +1 -0
  44. package/dist/daemon/stop-daemon.d.cts +18 -0
  45. package/dist/daemon/stop-daemon.d.cts.map +1 -0
  46. package/dist/daemon/stop-daemon.d.mts +18 -0
  47. package/dist/daemon/stop-daemon.d.mts.map +1 -0
  48. package/dist/daemon/stop-daemon.mjs +99 -0
  49. package/dist/daemon/stop-daemon.mjs.map +1 -0
  50. package/dist/daemon/types.cjs.map +1 -1
  51. package/dist/daemon/types.d.cts +28 -0
  52. package/dist/daemon/types.d.cts.map +1 -1
  53. package/dist/daemon/types.d.mts +28 -0
  54. package/dist/daemon/types.d.mts.map +1 -1
  55. package/dist/daemon/types.mjs.map +1 -1
  56. package/dist/daemon/utils.cjs +109 -0
  57. package/dist/daemon/utils.cjs.map +1 -0
  58. package/dist/daemon/utils.d.cts +50 -0
  59. package/dist/daemon/utils.d.cts.map +1 -0
  60. package/dist/daemon/utils.d.mts +50 -0
  61. package/dist/daemon/utils.d.mts.map +1 -0
  62. package/dist/daemon/utils.mjs +101 -0
  63. package/dist/daemon/utils.mjs.map +1 -0
  64. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -9,9 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Added
11
11
 
12
+ - Add a daemon transport layer: a JSON-RPC client and server over a Unix socket, plus daemon spawn/stop lifecycle helpers ([#9108](https://github.com/MetaMask/core/pull/9108))
12
13
  - Add SQLite-backed persistence for wallet controller state ([#9067](https://github.com/MetaMask/core/pull/9067))
13
- - A `KeyValueStore` backed by `better-sqlite3` for synchronous reads and writes.
14
- - `loadState` to rehydrate persist-flagged controller state from the store and `subscribeToChanges` to write persist-flagged controller state through to disk on every `stateChanged` event.
15
14
  - Initial package scaffold for `@metamask/wallet-cli`, an [oclif](https://oclif.io)-based `mm` CLI for `@metamask/wallet` ([#9065](https://github.com/MetaMask/core/pull/9065)).
16
15
 
16
+ ### Changed
17
+
18
+ - Bump `@metamask/wallet` from `^3.0.0` to `^4.0.0` ([#9218](https://github.com/MetaMask/core/pull/9218))
19
+
17
20
  [Unreleased]: https://github.com/MetaMask/core/
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pingDaemon = exports.sendCommand = void 0;
4
+ const utils_1 = require("@metamask/utils");
5
+ const node_crypto_1 = require("node:crypto");
6
+ const node_net_1 = require("node:net");
7
+ const socket_line_1 = require("./socket-line.cjs");
8
+ const utils_2 = require("./utils.cjs");
9
+ const DEFAULT_TIMEOUT_MS = 30000;
10
+ async function connectSocket(socketPath) {
11
+ return new Promise((resolve, reject) => {
12
+ const socket = (0, node_net_1.createConnection)(socketPath, () => {
13
+ socket.removeAllListeners('error');
14
+ resolve(socket);
15
+ });
16
+ socket.on('error', (error) => {
17
+ // A failed connect never reaches the caller, so destroy the socket here
18
+ // to avoid leaking its file descriptor.
19
+ socket.destroy();
20
+ reject(error);
21
+ });
22
+ });
23
+ }
24
+ /**
25
+ * Send a JSON-RPC request to the daemon over a Unix socket and return the
26
+ * response.
27
+ *
28
+ * Opens a connection, writes one JSON-RPC request line, reads one JSON-RPC
29
+ * response line, then closes the connection. Retries once after a short delay
30
+ * on transient connection errors (ECONNREFUSED, ECONNRESET). Verifies that the
31
+ * response `id` matches the outgoing request `id`.
32
+ *
33
+ * @param options - Command options.
34
+ * @param options.socketPath - The Unix socket path.
35
+ * @param options.method - The RPC method name.
36
+ * @param options.params - Optional method parameters.
37
+ * @param options.timeoutMs - Read timeout in milliseconds.
38
+ * @returns The parsed JSON-RPC response.
39
+ */
40
+ async function sendCommand({ socketPath, method, params, timeoutMs, }) {
41
+ const id = (0, node_crypto_1.randomUUID)();
42
+ const request = {
43
+ jsonrpc: '2.0',
44
+ id,
45
+ method,
46
+ ...(params === undefined ? {} : { params }),
47
+ };
48
+ const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
49
+ const attempt = async () => {
50
+ const socket = await connectSocket(socketPath);
51
+ try {
52
+ await (0, socket_line_1.writeLine)(socket, JSON.stringify(request));
53
+ const responseLine = await (0, socket_line_1.readLine)(socket, effectiveTimeout);
54
+ const parsed = JSON.parse(responseLine);
55
+ (0, utils_1.assertIsJsonRpcResponse)(parsed);
56
+ if (parsed.id !== id) {
57
+ throw new Error(`JSON-RPC response id ${JSON.stringify(parsed.id)} does not match request id ${JSON.stringify(id)}`);
58
+ }
59
+ return parsed;
60
+ }
61
+ finally {
62
+ socket.destroy();
63
+ }
64
+ };
65
+ try {
66
+ return await attempt();
67
+ }
68
+ catch (error) {
69
+ if (!(0, utils_2.isErrorWithCode)(error, 'ECONNREFUSED') &&
70
+ !(0, utils_2.isErrorWithCode)(error, 'ECONNRESET')) {
71
+ throw error;
72
+ }
73
+ await new Promise((resolve) => setTimeout(resolve, 100));
74
+ return attempt();
75
+ }
76
+ }
77
+ exports.sendCommand = sendCommand;
78
+ /**
79
+ * Normalize an unknown throw into a real Error instance so that downstream
80
+ * consumers (which read `.message`) cannot crash on string/object throws.
81
+ *
82
+ * @param error - The caught value.
83
+ * @returns An Error mirroring the caught value.
84
+ */
85
+ function toError(error) {
86
+ return error instanceof Error ? error : new Error(String(error));
87
+ }
88
+ /**
89
+ * Categorise an unreachable error by Node errno / message shape so callers can
90
+ * make decisions per failure mode rather than parsing message strings.
91
+ *
92
+ * @param error - The caught value.
93
+ * @returns A {@link PingUnreachableReason} label.
94
+ */
95
+ function classifyUnreachable(error) {
96
+ if ((0, utils_2.isErrorWithCode)(error, 'ECONNREFUSED') ||
97
+ (0, utils_2.isErrorWithCode)(error, 'ECONNRESET')) {
98
+ return 'refused';
99
+ }
100
+ if ((0, utils_2.isErrorWithCode)(error, 'EACCES') || (0, utils_2.isErrorWithCode)(error, 'EPERM')) {
101
+ return 'permission';
102
+ }
103
+ if (error instanceof Error && error.message === 'Socket read timed out') {
104
+ return 'timeout';
105
+ }
106
+ if (error instanceof Error &&
107
+ (error.message.includes('JSON-RPC response id') ||
108
+ /Expected .* JSON-RPC/u.test(error.message) ||
109
+ error.name === 'SyntaxError')) {
110
+ return 'protocol';
111
+ }
112
+ return 'other';
113
+ }
114
+ /**
115
+ * Check whether the daemon is running by sending a lightweight `getStatus`
116
+ * RPC call. Distinguishes "no daemon present" (socket file missing) from
117
+ * "daemon present but unreachable" (socket file exists but the daemon is
118
+ * wedged, mid-shutdown, or owned by a different user).
119
+ *
120
+ * @param socketPath - The Unix socket path.
121
+ * @returns A {@link PingResult} describing the daemon's reachability.
122
+ */
123
+ async function pingDaemon(socketPath) {
124
+ try {
125
+ await sendCommand({ socketPath, method: 'getStatus', timeoutMs: 3000 });
126
+ return { status: 'responsive' };
127
+ }
128
+ catch (error) {
129
+ if ((0, utils_2.isErrorWithCode)(error, 'ENOENT')) {
130
+ return { status: 'absent' };
131
+ }
132
+ return {
133
+ status: 'unreachable',
134
+ reason: classifyUnreachable(error),
135
+ error: toError(error),
136
+ };
137
+ }
138
+ }
139
+ exports.pingDaemon = pingDaemon;
140
+ //# sourceMappingURL=daemon-client.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-client.cjs","sourceRoot":"","sources":["../../src/daemon/daemon-client.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonRpcParams, JsonRpcResponse } from '@metamask/utils';\nimport { assertIsJsonRpcResponse } from '@metamask/utils';\nimport { randomUUID } from 'node:crypto';\nimport { createConnection } from 'node:net';\nimport type { Socket } from 'node:net';\n\nimport { readLine, writeLine } from './socket-line';\nimport { isErrorWithCode } from './utils';\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Options for {@link sendCommand}.\n */\ntype SendCommandOptions = {\n /** The Unix socket path. */\n socketPath: string;\n /** The RPC method name. */\n method: string;\n /** Optional method parameters (object or positional array). */\n params?: JsonRpcParams | undefined;\n /** Response read timeout in milliseconds (default: 30 000). */\n timeoutMs?: number | undefined;\n};\n\nasync function connectSocket(socketPath: string): Promise<Socket> {\n return new Promise((resolve, reject) => {\n const socket = createConnection(socketPath, () => {\n socket.removeAllListeners('error');\n resolve(socket);\n });\n socket.on('error', (error) => {\n // A failed connect never reaches the caller, so destroy the socket here\n // to avoid leaking its file descriptor.\n socket.destroy();\n reject(error);\n });\n });\n}\n\n/**\n * Send a JSON-RPC request to the daemon over a Unix socket and return the\n * response.\n *\n * Opens a connection, writes one JSON-RPC request line, reads one JSON-RPC\n * response line, then closes the connection. Retries once after a short delay\n * on transient connection errors (ECONNREFUSED, ECONNRESET). Verifies that the\n * response `id` matches the outgoing request `id`.\n *\n * @param options - Command options.\n * @param options.socketPath - The Unix socket path.\n * @param options.method - The RPC method name.\n * @param options.params - Optional method parameters.\n * @param options.timeoutMs - Read timeout in milliseconds.\n * @returns The parsed JSON-RPC response.\n */\nexport async function sendCommand({\n socketPath,\n method,\n params,\n timeoutMs,\n}: SendCommandOptions): Promise<JsonRpcResponse> {\n const id = randomUUID();\n const request = {\n jsonrpc: '2.0',\n id,\n method,\n ...(params === undefined ? {} : { params }),\n };\n\n const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const attempt = async (): Promise<JsonRpcResponse> => {\n const socket = await connectSocket(socketPath);\n try {\n await writeLine(socket, JSON.stringify(request));\n const responseLine = await readLine(socket, effectiveTimeout);\n const parsed: unknown = JSON.parse(responseLine);\n assertIsJsonRpcResponse(parsed);\n if (parsed.id !== id) {\n throw new Error(\n `JSON-RPC response id ${JSON.stringify(parsed.id)} does not match request id ${JSON.stringify(id)}`,\n );\n }\n return parsed;\n } finally {\n socket.destroy();\n }\n };\n\n try {\n return await attempt();\n } catch (error: unknown) {\n if (\n !isErrorWithCode(error, 'ECONNREFUSED') &&\n !isErrorWithCode(error, 'ECONNRESET')\n ) {\n throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, 100));\n return attempt();\n }\n}\n\n/**\n * Why an unreachable daemon cannot be queried.\n *\n * - `'refused'`: connection refused after retry (`ECONNREFUSED` / `ECONNRESET`).\n * Typical of a daemon that has crashed or is mid-restart.\n * - `'timeout'`: the daemon accepted the connection but did not respond within\n * the read timeout — most likely wedged on a long-running operation.\n * - `'permission'`: the socket exists but cannot be opened (`EACCES` / `EPERM`).\n * The daemon almost certainly belongs to another user.\n * - `'protocol'`: the daemon responded but the response did not parse as a\n * valid JSON-RPC response, or the response id did not match.\n * - `'other'`: anything else.\n */\nexport type PingUnreachableReason =\n | 'refused'\n | 'timeout'\n | 'permission'\n | 'protocol'\n | 'other';\n\n/**\n * Outcome of a daemon health check.\n *\n * - `'responsive'`: the daemon answered a `getStatus` RPC.\n * - `'absent'`: the socket connect attempt failed with `ENOENT`, i.e. no\n * socket file exists at the path. No daemon present.\n * - `'unreachable'`: any other non-success outcome. The daemon may still be\n * alive but is not responding. Callers should not silently take over the\n * slot or assume the daemon is dead. The `reason` field categorises the\n * failure so callers can distinguish a wedged sibling daemon from a\n * foreign-user daemon from a transient crash. User-initiated stop / purge\n * flows may still escalate to signals against the recorded PID.\n */\nexport type PingResult =\n | { status: 'responsive' }\n | { status: 'absent' }\n | { status: 'unreachable'; reason: PingUnreachableReason; error: Error };\n\n/**\n * Normalize an unknown throw into a real Error instance so that downstream\n * consumers (which read `.message`) cannot crash on string/object throws.\n *\n * @param error - The caught value.\n * @returns An Error mirroring the caught value.\n */\nfunction toError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\n/**\n * Categorise an unreachable error by Node errno / message shape so callers can\n * make decisions per failure mode rather than parsing message strings.\n *\n * @param error - The caught value.\n * @returns A {@link PingUnreachableReason} label.\n */\nfunction classifyUnreachable(error: unknown): PingUnreachableReason {\n if (\n isErrorWithCode(error, 'ECONNREFUSED') ||\n isErrorWithCode(error, 'ECONNRESET')\n ) {\n return 'refused';\n }\n if (isErrorWithCode(error, 'EACCES') || isErrorWithCode(error, 'EPERM')) {\n return 'permission';\n }\n if (error instanceof Error && error.message === 'Socket read timed out') {\n return 'timeout';\n }\n if (\n error instanceof Error &&\n (error.message.includes('JSON-RPC response id') ||\n /Expected .* JSON-RPC/u.test(error.message) ||\n error.name === 'SyntaxError')\n ) {\n return 'protocol';\n }\n return 'other';\n}\n\n/**\n * Check whether the daemon is running by sending a lightweight `getStatus`\n * RPC call. Distinguishes \"no daemon present\" (socket file missing) from\n * \"daemon present but unreachable\" (socket file exists but the daemon is\n * wedged, mid-shutdown, or owned by a different user).\n *\n * @param socketPath - The Unix socket path.\n * @returns A {@link PingResult} describing the daemon's reachability.\n */\nexport async function pingDaemon(socketPath: string): Promise<PingResult> {\n try {\n await sendCommand({ socketPath, method: 'getStatus', timeoutMs: 3_000 });\n return { status: 'responsive' };\n } catch (error: unknown) {\n if (isErrorWithCode(error, 'ENOENT')) {\n return { status: 'absent' };\n }\n return {\n status: 'unreachable',\n reason: classifyUnreachable(error),\n error: toError(error),\n };\n }\n}\n"]}
@@ -0,0 +1,79 @@
1
+ import type { JsonRpcParams, JsonRpcResponse } from "@metamask/utils";
2
+ /**
3
+ * Options for {@link sendCommand}.
4
+ */
5
+ type SendCommandOptions = {
6
+ /** The Unix socket path. */
7
+ socketPath: string;
8
+ /** The RPC method name. */
9
+ method: string;
10
+ /** Optional method parameters (object or positional array). */
11
+ params?: JsonRpcParams | undefined;
12
+ /** Response read timeout in milliseconds (default: 30 000). */
13
+ timeoutMs?: number | undefined;
14
+ };
15
+ /**
16
+ * Send a JSON-RPC request to the daemon over a Unix socket and return the
17
+ * response.
18
+ *
19
+ * Opens a connection, writes one JSON-RPC request line, reads one JSON-RPC
20
+ * response line, then closes the connection. Retries once after a short delay
21
+ * on transient connection errors (ECONNREFUSED, ECONNRESET). Verifies that the
22
+ * response `id` matches the outgoing request `id`.
23
+ *
24
+ * @param options - Command options.
25
+ * @param options.socketPath - The Unix socket path.
26
+ * @param options.method - The RPC method name.
27
+ * @param options.params - Optional method parameters.
28
+ * @param options.timeoutMs - Read timeout in milliseconds.
29
+ * @returns The parsed JSON-RPC response.
30
+ */
31
+ export declare function sendCommand({ socketPath, method, params, timeoutMs, }: SendCommandOptions): Promise<JsonRpcResponse>;
32
+ /**
33
+ * Why an unreachable daemon cannot be queried.
34
+ *
35
+ * - `'refused'`: connection refused after retry (`ECONNREFUSED` / `ECONNRESET`).
36
+ * Typical of a daemon that has crashed or is mid-restart.
37
+ * - `'timeout'`: the daemon accepted the connection but did not respond within
38
+ * the read timeout — most likely wedged on a long-running operation.
39
+ * - `'permission'`: the socket exists but cannot be opened (`EACCES` / `EPERM`).
40
+ * The daemon almost certainly belongs to another user.
41
+ * - `'protocol'`: the daemon responded but the response did not parse as a
42
+ * valid JSON-RPC response, or the response id did not match.
43
+ * - `'other'`: anything else.
44
+ */
45
+ export type PingUnreachableReason = 'refused' | 'timeout' | 'permission' | 'protocol' | 'other';
46
+ /**
47
+ * Outcome of a daemon health check.
48
+ *
49
+ * - `'responsive'`: the daemon answered a `getStatus` RPC.
50
+ * - `'absent'`: the socket connect attempt failed with `ENOENT`, i.e. no
51
+ * socket file exists at the path. No daemon present.
52
+ * - `'unreachable'`: any other non-success outcome. The daemon may still be
53
+ * alive but is not responding. Callers should not silently take over the
54
+ * slot or assume the daemon is dead. The `reason` field categorises the
55
+ * failure so callers can distinguish a wedged sibling daemon from a
56
+ * foreign-user daemon from a transient crash. User-initiated stop / purge
57
+ * flows may still escalate to signals against the recorded PID.
58
+ */
59
+ export type PingResult = {
60
+ status: 'responsive';
61
+ } | {
62
+ status: 'absent';
63
+ } | {
64
+ status: 'unreachable';
65
+ reason: PingUnreachableReason;
66
+ error: Error;
67
+ };
68
+ /**
69
+ * Check whether the daemon is running by sending a lightweight `getStatus`
70
+ * RPC call. Distinguishes "no daemon present" (socket file missing) from
71
+ * "daemon present but unreachable" (socket file exists but the daemon is
72
+ * wedged, mid-shutdown, or owned by a different user).
73
+ *
74
+ * @param socketPath - The Unix socket path.
75
+ * @returns A {@link PingResult} describing the daemon's reachability.
76
+ */
77
+ export declare function pingDaemon(socketPath: string): Promise<PingResult>;
78
+ export {};
79
+ //# sourceMappingURL=daemon-client.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-client.d.cts","sourceRoot":"","sources":["../../src/daemon/daemon-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,wBAAwB;AAWtE;;GAEG;AACH,KAAK,kBAAkB,GAAG;IACxB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,MAAM,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACnC,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC,CAAC;AAiBF;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,WAAW,CAAC,EAChC,UAAU,EACV,MAAM,EACN,MAAM,EACN,SAAS,GACV,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CAyC/C;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,qBAAqB,GAC7B,SAAS,GACT,SAAS,GACT,YAAY,GACZ,UAAU,GACV,OAAO,CAAC;AAEZ;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,GACxB;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpB;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,qBAAqB,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AA4C3E;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAcxE"}
@@ -0,0 +1,79 @@
1
+ import type { JsonRpcParams, JsonRpcResponse } from "@metamask/utils";
2
+ /**
3
+ * Options for {@link sendCommand}.
4
+ */
5
+ type SendCommandOptions = {
6
+ /** The Unix socket path. */
7
+ socketPath: string;
8
+ /** The RPC method name. */
9
+ method: string;
10
+ /** Optional method parameters (object or positional array). */
11
+ params?: JsonRpcParams | undefined;
12
+ /** Response read timeout in milliseconds (default: 30 000). */
13
+ timeoutMs?: number | undefined;
14
+ };
15
+ /**
16
+ * Send a JSON-RPC request to the daemon over a Unix socket and return the
17
+ * response.
18
+ *
19
+ * Opens a connection, writes one JSON-RPC request line, reads one JSON-RPC
20
+ * response line, then closes the connection. Retries once after a short delay
21
+ * on transient connection errors (ECONNREFUSED, ECONNRESET). Verifies that the
22
+ * response `id` matches the outgoing request `id`.
23
+ *
24
+ * @param options - Command options.
25
+ * @param options.socketPath - The Unix socket path.
26
+ * @param options.method - The RPC method name.
27
+ * @param options.params - Optional method parameters.
28
+ * @param options.timeoutMs - Read timeout in milliseconds.
29
+ * @returns The parsed JSON-RPC response.
30
+ */
31
+ export declare function sendCommand({ socketPath, method, params, timeoutMs, }: SendCommandOptions): Promise<JsonRpcResponse>;
32
+ /**
33
+ * Why an unreachable daemon cannot be queried.
34
+ *
35
+ * - `'refused'`: connection refused after retry (`ECONNREFUSED` / `ECONNRESET`).
36
+ * Typical of a daemon that has crashed or is mid-restart.
37
+ * - `'timeout'`: the daemon accepted the connection but did not respond within
38
+ * the read timeout — most likely wedged on a long-running operation.
39
+ * - `'permission'`: the socket exists but cannot be opened (`EACCES` / `EPERM`).
40
+ * The daemon almost certainly belongs to another user.
41
+ * - `'protocol'`: the daemon responded but the response did not parse as a
42
+ * valid JSON-RPC response, or the response id did not match.
43
+ * - `'other'`: anything else.
44
+ */
45
+ export type PingUnreachableReason = 'refused' | 'timeout' | 'permission' | 'protocol' | 'other';
46
+ /**
47
+ * Outcome of a daemon health check.
48
+ *
49
+ * - `'responsive'`: the daemon answered a `getStatus` RPC.
50
+ * - `'absent'`: the socket connect attempt failed with `ENOENT`, i.e. no
51
+ * socket file exists at the path. No daemon present.
52
+ * - `'unreachable'`: any other non-success outcome. The daemon may still be
53
+ * alive but is not responding. Callers should not silently take over the
54
+ * slot or assume the daemon is dead. The `reason` field categorises the
55
+ * failure so callers can distinguish a wedged sibling daemon from a
56
+ * foreign-user daemon from a transient crash. User-initiated stop / purge
57
+ * flows may still escalate to signals against the recorded PID.
58
+ */
59
+ export type PingResult = {
60
+ status: 'responsive';
61
+ } | {
62
+ status: 'absent';
63
+ } | {
64
+ status: 'unreachable';
65
+ reason: PingUnreachableReason;
66
+ error: Error;
67
+ };
68
+ /**
69
+ * Check whether the daemon is running by sending a lightweight `getStatus`
70
+ * RPC call. Distinguishes "no daemon present" (socket file missing) from
71
+ * "daemon present but unreachable" (socket file exists but the daemon is
72
+ * wedged, mid-shutdown, or owned by a different user).
73
+ *
74
+ * @param socketPath - The Unix socket path.
75
+ * @returns A {@link PingResult} describing the daemon's reachability.
76
+ */
77
+ export declare function pingDaemon(socketPath: string): Promise<PingResult>;
78
+ export {};
79
+ //# sourceMappingURL=daemon-client.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-client.d.mts","sourceRoot":"","sources":["../../src/daemon/daemon-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,wBAAwB;AAWtE;;GAEG;AACH,KAAK,kBAAkB,GAAG;IACxB,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,2BAA2B;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,MAAM,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACnC,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC,CAAC;AAiBF;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,WAAW,CAAC,EAChC,UAAU,EACV,MAAM,EACN,MAAM,EACN,SAAS,GACV,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CAyC/C;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,qBAAqB,GAC7B,SAAS,GACT,SAAS,GACT,YAAY,GACZ,UAAU,GACV,OAAO,CAAC;AAEZ;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,MAAM,EAAE,YAAY,CAAA;CAAE,GACxB;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpB;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,qBAAqB,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AA4C3E;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAcxE"}
@@ -0,0 +1,135 @@
1
+ import { assertIsJsonRpcResponse } from "@metamask/utils";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createConnection } from "node:net";
4
+ import { readLine, writeLine } from "./socket-line.mjs";
5
+ import { isErrorWithCode } from "./utils.mjs";
6
+ const DEFAULT_TIMEOUT_MS = 30000;
7
+ async function connectSocket(socketPath) {
8
+ return new Promise((resolve, reject) => {
9
+ const socket = createConnection(socketPath, () => {
10
+ socket.removeAllListeners('error');
11
+ resolve(socket);
12
+ });
13
+ socket.on('error', (error) => {
14
+ // A failed connect never reaches the caller, so destroy the socket here
15
+ // to avoid leaking its file descriptor.
16
+ socket.destroy();
17
+ reject(error);
18
+ });
19
+ });
20
+ }
21
+ /**
22
+ * Send a JSON-RPC request to the daemon over a Unix socket and return the
23
+ * response.
24
+ *
25
+ * Opens a connection, writes one JSON-RPC request line, reads one JSON-RPC
26
+ * response line, then closes the connection. Retries once after a short delay
27
+ * on transient connection errors (ECONNREFUSED, ECONNRESET). Verifies that the
28
+ * response `id` matches the outgoing request `id`.
29
+ *
30
+ * @param options - Command options.
31
+ * @param options.socketPath - The Unix socket path.
32
+ * @param options.method - The RPC method name.
33
+ * @param options.params - Optional method parameters.
34
+ * @param options.timeoutMs - Read timeout in milliseconds.
35
+ * @returns The parsed JSON-RPC response.
36
+ */
37
+ export async function sendCommand({ socketPath, method, params, timeoutMs, }) {
38
+ const id = randomUUID();
39
+ const request = {
40
+ jsonrpc: '2.0',
41
+ id,
42
+ method,
43
+ ...(params === undefined ? {} : { params }),
44
+ };
45
+ const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
46
+ const attempt = async () => {
47
+ const socket = await connectSocket(socketPath);
48
+ try {
49
+ await writeLine(socket, JSON.stringify(request));
50
+ const responseLine = await readLine(socket, effectiveTimeout);
51
+ const parsed = JSON.parse(responseLine);
52
+ assertIsJsonRpcResponse(parsed);
53
+ if (parsed.id !== id) {
54
+ throw new Error(`JSON-RPC response id ${JSON.stringify(parsed.id)} does not match request id ${JSON.stringify(id)}`);
55
+ }
56
+ return parsed;
57
+ }
58
+ finally {
59
+ socket.destroy();
60
+ }
61
+ };
62
+ try {
63
+ return await attempt();
64
+ }
65
+ catch (error) {
66
+ if (!isErrorWithCode(error, 'ECONNREFUSED') &&
67
+ !isErrorWithCode(error, 'ECONNRESET')) {
68
+ throw error;
69
+ }
70
+ await new Promise((resolve) => setTimeout(resolve, 100));
71
+ return attempt();
72
+ }
73
+ }
74
+ /**
75
+ * Normalize an unknown throw into a real Error instance so that downstream
76
+ * consumers (which read `.message`) cannot crash on string/object throws.
77
+ *
78
+ * @param error - The caught value.
79
+ * @returns An Error mirroring the caught value.
80
+ */
81
+ function toError(error) {
82
+ return error instanceof Error ? error : new Error(String(error));
83
+ }
84
+ /**
85
+ * Categorise an unreachable error by Node errno / message shape so callers can
86
+ * make decisions per failure mode rather than parsing message strings.
87
+ *
88
+ * @param error - The caught value.
89
+ * @returns A {@link PingUnreachableReason} label.
90
+ */
91
+ function classifyUnreachable(error) {
92
+ if (isErrorWithCode(error, 'ECONNREFUSED') ||
93
+ isErrorWithCode(error, 'ECONNRESET')) {
94
+ return 'refused';
95
+ }
96
+ if (isErrorWithCode(error, 'EACCES') || isErrorWithCode(error, 'EPERM')) {
97
+ return 'permission';
98
+ }
99
+ if (error instanceof Error && error.message === 'Socket read timed out') {
100
+ return 'timeout';
101
+ }
102
+ if (error instanceof Error &&
103
+ (error.message.includes('JSON-RPC response id') ||
104
+ /Expected .* JSON-RPC/u.test(error.message) ||
105
+ error.name === 'SyntaxError')) {
106
+ return 'protocol';
107
+ }
108
+ return 'other';
109
+ }
110
+ /**
111
+ * Check whether the daemon is running by sending a lightweight `getStatus`
112
+ * RPC call. Distinguishes "no daemon present" (socket file missing) from
113
+ * "daemon present but unreachable" (socket file exists but the daemon is
114
+ * wedged, mid-shutdown, or owned by a different user).
115
+ *
116
+ * @param socketPath - The Unix socket path.
117
+ * @returns A {@link PingResult} describing the daemon's reachability.
118
+ */
119
+ export async function pingDaemon(socketPath) {
120
+ try {
121
+ await sendCommand({ socketPath, method: 'getStatus', timeoutMs: 3000 });
122
+ return { status: 'responsive' };
123
+ }
124
+ catch (error) {
125
+ if (isErrorWithCode(error, 'ENOENT')) {
126
+ return { status: 'absent' };
127
+ }
128
+ return {
129
+ status: 'unreachable',
130
+ reason: classifyUnreachable(error),
131
+ error: toError(error),
132
+ };
133
+ }
134
+ }
135
+ //# sourceMappingURL=daemon-client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-client.mjs","sourceRoot":"","sources":["../../src/daemon/daemon-client.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonRpcParams, JsonRpcResponse } from '@metamask/utils';\nimport { assertIsJsonRpcResponse } from '@metamask/utils';\nimport { randomUUID } from 'node:crypto';\nimport { createConnection } from 'node:net';\nimport type { Socket } from 'node:net';\n\nimport { readLine, writeLine } from './socket-line';\nimport { isErrorWithCode } from './utils';\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Options for {@link sendCommand}.\n */\ntype SendCommandOptions = {\n /** The Unix socket path. */\n socketPath: string;\n /** The RPC method name. */\n method: string;\n /** Optional method parameters (object or positional array). */\n params?: JsonRpcParams | undefined;\n /** Response read timeout in milliseconds (default: 30 000). */\n timeoutMs?: number | undefined;\n};\n\nasync function connectSocket(socketPath: string): Promise<Socket> {\n return new Promise((resolve, reject) => {\n const socket = createConnection(socketPath, () => {\n socket.removeAllListeners('error');\n resolve(socket);\n });\n socket.on('error', (error) => {\n // A failed connect never reaches the caller, so destroy the socket here\n // to avoid leaking its file descriptor.\n socket.destroy();\n reject(error);\n });\n });\n}\n\n/**\n * Send a JSON-RPC request to the daemon over a Unix socket and return the\n * response.\n *\n * Opens a connection, writes one JSON-RPC request line, reads one JSON-RPC\n * response line, then closes the connection. Retries once after a short delay\n * on transient connection errors (ECONNREFUSED, ECONNRESET). Verifies that the\n * response `id` matches the outgoing request `id`.\n *\n * @param options - Command options.\n * @param options.socketPath - The Unix socket path.\n * @param options.method - The RPC method name.\n * @param options.params - Optional method parameters.\n * @param options.timeoutMs - Read timeout in milliseconds.\n * @returns The parsed JSON-RPC response.\n */\nexport async function sendCommand({\n socketPath,\n method,\n params,\n timeoutMs,\n}: SendCommandOptions): Promise<JsonRpcResponse> {\n const id = randomUUID();\n const request = {\n jsonrpc: '2.0',\n id,\n method,\n ...(params === undefined ? {} : { params }),\n };\n\n const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const attempt = async (): Promise<JsonRpcResponse> => {\n const socket = await connectSocket(socketPath);\n try {\n await writeLine(socket, JSON.stringify(request));\n const responseLine = await readLine(socket, effectiveTimeout);\n const parsed: unknown = JSON.parse(responseLine);\n assertIsJsonRpcResponse(parsed);\n if (parsed.id !== id) {\n throw new Error(\n `JSON-RPC response id ${JSON.stringify(parsed.id)} does not match request id ${JSON.stringify(id)}`,\n );\n }\n return parsed;\n } finally {\n socket.destroy();\n }\n };\n\n try {\n return await attempt();\n } catch (error: unknown) {\n if (\n !isErrorWithCode(error, 'ECONNREFUSED') &&\n !isErrorWithCode(error, 'ECONNRESET')\n ) {\n throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, 100));\n return attempt();\n }\n}\n\n/**\n * Why an unreachable daemon cannot be queried.\n *\n * - `'refused'`: connection refused after retry (`ECONNREFUSED` / `ECONNRESET`).\n * Typical of a daemon that has crashed or is mid-restart.\n * - `'timeout'`: the daemon accepted the connection but did not respond within\n * the read timeout — most likely wedged on a long-running operation.\n * - `'permission'`: the socket exists but cannot be opened (`EACCES` / `EPERM`).\n * The daemon almost certainly belongs to another user.\n * - `'protocol'`: the daemon responded but the response did not parse as a\n * valid JSON-RPC response, or the response id did not match.\n * - `'other'`: anything else.\n */\nexport type PingUnreachableReason =\n | 'refused'\n | 'timeout'\n | 'permission'\n | 'protocol'\n | 'other';\n\n/**\n * Outcome of a daemon health check.\n *\n * - `'responsive'`: the daemon answered a `getStatus` RPC.\n * - `'absent'`: the socket connect attempt failed with `ENOENT`, i.e. no\n * socket file exists at the path. No daemon present.\n * - `'unreachable'`: any other non-success outcome. The daemon may still be\n * alive but is not responding. Callers should not silently take over the\n * slot or assume the daemon is dead. The `reason` field categorises the\n * failure so callers can distinguish a wedged sibling daemon from a\n * foreign-user daemon from a transient crash. User-initiated stop / purge\n * flows may still escalate to signals against the recorded PID.\n */\nexport type PingResult =\n | { status: 'responsive' }\n | { status: 'absent' }\n | { status: 'unreachable'; reason: PingUnreachableReason; error: Error };\n\n/**\n * Normalize an unknown throw into a real Error instance so that downstream\n * consumers (which read `.message`) cannot crash on string/object throws.\n *\n * @param error - The caught value.\n * @returns An Error mirroring the caught value.\n */\nfunction toError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error));\n}\n\n/**\n * Categorise an unreachable error by Node errno / message shape so callers can\n * make decisions per failure mode rather than parsing message strings.\n *\n * @param error - The caught value.\n * @returns A {@link PingUnreachableReason} label.\n */\nfunction classifyUnreachable(error: unknown): PingUnreachableReason {\n if (\n isErrorWithCode(error, 'ECONNREFUSED') ||\n isErrorWithCode(error, 'ECONNRESET')\n ) {\n return 'refused';\n }\n if (isErrorWithCode(error, 'EACCES') || isErrorWithCode(error, 'EPERM')) {\n return 'permission';\n }\n if (error instanceof Error && error.message === 'Socket read timed out') {\n return 'timeout';\n }\n if (\n error instanceof Error &&\n (error.message.includes('JSON-RPC response id') ||\n /Expected .* JSON-RPC/u.test(error.message) ||\n error.name === 'SyntaxError')\n ) {\n return 'protocol';\n }\n return 'other';\n}\n\n/**\n * Check whether the daemon is running by sending a lightweight `getStatus`\n * RPC call. Distinguishes \"no daemon present\" (socket file missing) from\n * \"daemon present but unreachable\" (socket file exists but the daemon is\n * wedged, mid-shutdown, or owned by a different user).\n *\n * @param socketPath - The Unix socket path.\n * @returns A {@link PingResult} describing the daemon's reachability.\n */\nexport async function pingDaemon(socketPath: string): Promise<PingResult> {\n try {\n await sendCommand({ socketPath, method: 'getStatus', timeoutMs: 3_000 });\n return { status: 'responsive' };\n } catch (error: unknown) {\n if (isErrorWithCode(error, 'ENOENT')) {\n return { status: 'absent' };\n }\n return {\n status: 'unreachable',\n reason: classifyUnreachable(error),\n error: toError(error),\n };\n }\n}\n"]}
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ensureDaemon = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ const daemon_client_1 = require("./daemon-client.cjs");
8
+ const paths_1 = require("./paths.cjs");
9
+ const POLL_INTERVAL_MS = 100;
10
+ const MAX_POLLS = 300; // 30 seconds
11
+ /**
12
+ * Ensure the daemon is running. If a responsive daemon already exists, return
13
+ * `'already-running'` (caller decides how to surface that). Otherwise spawn
14
+ * one as a detached process and wait until the socket becomes responsive.
15
+ *
16
+ * Refuses to spawn when pinging the existing socket fails with anything other
17
+ * than `ENOENT` (wedged or foreign daemon) — taking over could orphan the
18
+ * existing process and corrupt its PID file.
19
+ *
20
+ * @param config - Spawn configuration.
21
+ * @returns The state of the daemon and the socket path it's listening on.
22
+ */
23
+ async function ensureDaemon(config) {
24
+ const { socketPath } = (0, paths_1.getDaemonPaths)(config.dataDir);
25
+ const initialPing = await (0, daemon_client_1.pingDaemon)(socketPath);
26
+ if (initialPing.status === 'responsive') {
27
+ return { state: 'already-running', socketPath };
28
+ }
29
+ if (initialPing.status === 'unreachable') {
30
+ if (initialPing.reason === 'permission') {
31
+ throw new Error(`Refusing to start: the socket at ${socketPath} is owned by another user. ` +
32
+ `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +
33
+ `(${initialPing.error.message})`);
34
+ }
35
+ throw new Error(`Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +
36
+ `Run \`mm daemon stop\` (or \`mm daemon purge\`) before starting a new daemon. ` +
37
+ `(${initialPing.error.message})`);
38
+ }
39
+ process.stderr.write('Starting daemon...\n');
40
+ const { entryPath, args } = resolveEntryPoint(config.packageRoot);
41
+ const child = (0, node_child_process_1.spawn)(process.execPath, [...args, entryPath], {
42
+ detached: true,
43
+ stdio: 'ignore',
44
+ env: {
45
+ ...process.env,
46
+ MM_DAEMON_DATA_DIR: config.dataDir,
47
+ MM_DAEMON_SOCKET_PATH: socketPath,
48
+ INFURA_PROJECT_ID: config.infuraProjectId,
49
+ MM_WALLET_PASSWORD: config.password,
50
+ MM_WALLET_SRP: config.srp,
51
+ },
52
+ });
53
+ const exitInfo = { value: null };
54
+ // A failed spawn (bad interpreter, EACCES, ENOENT) emits 'error' and may
55
+ // never emit 'exit'. Capture it so the readiness loop can surface the real
56
+ // cause immediately instead of hanging for the full timeout.
57
+ const spawnError = { value: null };
58
+ child.on('error', (error) => {
59
+ process.stderr.write(`Failed to spawn daemon process: ${String(error)}\n`);
60
+ spawnError.value = error;
61
+ });
62
+ child.on('exit', (code, signal) => {
63
+ exitInfo.value = { code, signal };
64
+ });
65
+ child.unref();
66
+ for (let i = 0; i < MAX_POLLS; i++) {
67
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
68
+ if (spawnError.value !== null) {
69
+ throw new Error(`Failed to spawn daemon process: ${spawnError.value.message}. ` +
70
+ `Check the daemon log at ${(0, paths_1.getDaemonPaths)(config.dataDir).logPath}.`);
71
+ }
72
+ if (exitInfo.value !== null) {
73
+ const { code, signal } = exitInfo.value;
74
+ throw new Error(`Daemon process exited during startup (code=${String(code)}, signal=${String(signal)}). ` +
75
+ `Check the daemon log at ${(0, paths_1.getDaemonPaths)(config.dataDir).logPath}.`);
76
+ }
77
+ const ping = await (0, daemon_client_1.pingDaemon)(socketPath);
78
+ if (ping.status === 'responsive') {
79
+ process.stderr.write('Daemon ready.\n');
80
+ return { state: 'started', socketPath };
81
+ }
82
+ }
83
+ throw new Error(`Daemon did not start within ${(MAX_POLLS * POLL_INTERVAL_MS) / 1000}s`);
84
+ }
85
+ exports.ensureDaemon = ensureDaemon;
86
+ /**
87
+ * Resolve the daemon entry point path and any extra Node.js args needed.
88
+ *
89
+ * In production, uses the compiled dist output. In development, uses tsx
90
+ * to run TypeScript source directly.
91
+ *
92
+ * @param packageRoot - The root directory of the wallet-cli package.
93
+ * @returns The entry path and any extra node args.
94
+ */
95
+ function resolveEntryPoint(packageRoot) {
96
+ const distEntry = (0, node_path_1.join)(packageRoot, 'dist', 'daemon', 'daemon-entry.mjs');
97
+ if ((0, node_fs_1.existsSync)(distEntry)) {
98
+ return { entryPath: distEntry, args: [] };
99
+ }
100
+ const srcEntry = (0, node_path_1.join)(packageRoot, 'src', 'daemon', 'daemon-entry.ts');
101
+ return {
102
+ entryPath: srcEntry,
103
+ args: ['--import', 'tsx'],
104
+ };
105
+ }
106
+ //# sourceMappingURL=daemon-spawn.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon-spawn.cjs","sourceRoot":"","sources":["../../src/daemon/daemon-spawn.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { pingDaemon } from './daemon-client';\nimport { getDaemonPaths } from './paths';\nimport type { DaemonSpawnConfig } from './types';\n\nconst POLL_INTERVAL_MS = 100;\nconst MAX_POLLS = 300; // 30 seconds\n\n/**\n * Outcome of {@link ensureDaemon}.\n *\n * - `'already-running'`: a responsive daemon was found at the configured\n * socket path. The supplied flags (`infuraProjectId`, `password`, `srp`)\n * were NOT applied to that daemon; the caller should surface this so a\n * user who is trying to change them isn't silently ignored.\n * - `'started'`: a new daemon was spawned and is now responsive.\n */\nexport type EnsureDaemonResult = {\n state: 'already-running' | 'started';\n socketPath: string;\n};\n\n/**\n * Ensure the daemon is running. If a responsive daemon already exists, return\n * `'already-running'` (caller decides how to surface that). Otherwise spawn\n * one as a detached process and wait until the socket becomes responsive.\n *\n * Refuses to spawn when pinging the existing socket fails with anything other\n * than `ENOENT` (wedged or foreign daemon) — taking over could orphan the\n * existing process and corrupt its PID file.\n *\n * @param config - Spawn configuration.\n * @returns The state of the daemon and the socket path it's listening on.\n */\nexport async function ensureDaemon(\n config: DaemonSpawnConfig,\n): Promise<EnsureDaemonResult> {\n const { socketPath } = getDaemonPaths(config.dataDir);\n\n const initialPing = await pingDaemon(socketPath);\n if (initialPing.status === 'responsive') {\n return { state: 'already-running', socketPath };\n }\n if (initialPing.status === 'unreachable') {\n if (initialPing.reason === 'permission') {\n throw new Error(\n `Refusing to start: the socket at ${socketPath} is owned by another user. ` +\n `Choose a different data directory (MM_DAEMON_DATA_DIR) or remove the socket manually. ` +\n `(${initialPing.error.message})`,\n );\n }\n throw new Error(\n `Refusing to start: a daemon socket already exists at ${socketPath} but is unresponsive. ` +\n `Run \\`mm daemon stop\\` (or \\`mm daemon purge\\`) before starting a new daemon. ` +\n `(${initialPing.error.message})`,\n );\n }\n\n process.stderr.write('Starting daemon...\\n');\n\n const { entryPath, args } = resolveEntryPoint(config.packageRoot);\n\n const child = spawn(process.execPath, [...args, entryPath], {\n detached: true,\n stdio: 'ignore',\n env: {\n ...process.env,\n MM_DAEMON_DATA_DIR: config.dataDir,\n MM_DAEMON_SOCKET_PATH: socketPath,\n INFURA_PROJECT_ID: config.infuraProjectId,\n MM_WALLET_PASSWORD: config.password,\n MM_WALLET_SRP: config.srp,\n },\n });\n\n type ExitInfo = { code: number | null; signal: NodeJS.Signals | null };\n const exitInfo: { value: ExitInfo | null } = { value: null };\n // A failed spawn (bad interpreter, EACCES, ENOENT) emits 'error' and may\n // never emit 'exit'. Capture it so the readiness loop can surface the real\n // cause immediately instead of hanging for the full timeout.\n const spawnError: { value: Error | null } = { value: null };\n\n child.on('error', (error: Error) => {\n process.stderr.write(`Failed to spawn daemon process: ${String(error)}\\n`);\n spawnError.value = error;\n });\n child.on('exit', (code, signal) => {\n exitInfo.value = { code, signal };\n });\n child.unref();\n\n for (let i = 0; i < MAX_POLLS; i++) {\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n if (spawnError.value !== null) {\n throw new Error(\n `Failed to spawn daemon process: ${spawnError.value.message}. ` +\n `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`,\n );\n }\n if (exitInfo.value !== null) {\n const { code, signal } = exitInfo.value;\n throw new Error(\n `Daemon process exited during startup (code=${String(code)}, signal=${String(signal)}). ` +\n `Check the daemon log at ${getDaemonPaths(config.dataDir).logPath}.`,\n );\n }\n const ping = await pingDaemon(socketPath);\n if (ping.status === 'responsive') {\n process.stderr.write('Daemon ready.\\n');\n return { state: 'started', socketPath };\n }\n }\n\n throw new Error(\n `Daemon did not start within ${(MAX_POLLS * POLL_INTERVAL_MS) / 1000}s`,\n );\n}\n\n/**\n * Resolve the daemon entry point path and any extra Node.js args needed.\n *\n * In production, uses the compiled dist output. In development, uses tsx\n * to run TypeScript source directly.\n *\n * @param packageRoot - The root directory of the wallet-cli package.\n * @returns The entry path and any extra node args.\n */\nfunction resolveEntryPoint(packageRoot: string): {\n entryPath: string;\n args: string[];\n} {\n const distEntry = join(packageRoot, 'dist', 'daemon', 'daemon-entry.mjs');\n if (existsSync(distEntry)) {\n return { entryPath: distEntry, args: [] };\n }\n\n const srcEntry = join(packageRoot, 'src', 'daemon', 'daemon-entry.ts');\n return {\n entryPath: srcEntry,\n args: ['--import', 'tsx'],\n };\n}\n"]}