movehat 0.2.5 → 0.2.7

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.
@@ -1,4 +1,21 @@
1
1
  import type { RunResult } from "../utils/childProcessAdapter.js";
2
+ /**
3
+ * Resolve the tsx CLI entrypoint (`<tsx-pkg>/dist/cli.mjs`) used by
4
+ * `movehat run` to execute user scripts. Prefers movehat's bundled tsx
5
+ * by default; falls back to the cwd's tsx only if bundled is missing
6
+ * (defensive — should not happen for normal installs). Set
7
+ * `MOVEHAT_TSX_FROM_CWD=1` or pass `preferCwd: true` to flip the
8
+ * order — power-users who pinned a different tsx in their project.
9
+ *
10
+ * Security: the default order closes #52 (untrusted cwd could ship a
11
+ * malicious `node_modules/tsx/dist/cli.mjs`).
12
+ *
13
+ * Exported so the resolution logic can be unit-tested without
14
+ * spawning a real process.
15
+ */
16
+ export declare function resolveTsxCliPath(opts?: {
17
+ preferCwd?: boolean;
18
+ }): string | null;
2
19
  /**
3
20
  * Apply the exit policy for a child whose output was inherited by the
4
21
  * parent. When the child dies via signal, re-raise it on the parent so
@@ -4,6 +4,45 @@ import { fileURLToPath } from "url";
4
4
  import { createRequire } from "module";
5
5
  import { runCli } from "../utils/runCli.js";
6
6
  import { logger } from "../ui/index.js";
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const requireFromHere = createRequire(import.meta.url);
9
+ /**
10
+ * Resolve the tsx CLI entrypoint (`<tsx-pkg>/dist/cli.mjs`) used by
11
+ * `movehat run` to execute user scripts. Prefers movehat's bundled tsx
12
+ * by default; falls back to the cwd's tsx only if bundled is missing
13
+ * (defensive — should not happen for normal installs). Set
14
+ * `MOVEHAT_TSX_FROM_CWD=1` or pass `preferCwd: true` to flip the
15
+ * order — power-users who pinned a different tsx in their project.
16
+ *
17
+ * Security: the default order closes #52 (untrusted cwd could ship a
18
+ * malicious `node_modules/tsx/dist/cli.mjs`).
19
+ *
20
+ * Exported so the resolution logic can be unit-tested without
21
+ * spawning a real process.
22
+ */
23
+ export function resolveTsxCliPath(opts) {
24
+ const preferCwd = opts?.preferCwd ?? process.env.MOVEHAT_TSX_FROM_CWD === "1";
25
+ const bundledRoot = __dirname;
26
+ const cwdRoot = process.cwd();
27
+ const lookupOrder = preferCwd
28
+ ? [cwdRoot, bundledRoot]
29
+ : [bundledRoot, cwdRoot];
30
+ for (const root of lookupOrder) {
31
+ try {
32
+ const tsxEntry = requireFromHere.resolve("tsx", { paths: [root] });
33
+ // require.resolve("tsx") returns .../tsx/dist/loader.mjs; walk up
34
+ // to the package root and into dist/cli.mjs.
35
+ const packageRoot = dirname(dirname(tsxEntry));
36
+ const cliPath = join(packageRoot, "dist", "cli.mjs");
37
+ if (existsSync(cliPath))
38
+ return cliPath;
39
+ }
40
+ catch {
41
+ // Lookup failed at this root; try the next one.
42
+ }
43
+ }
44
+ return null;
45
+ }
7
46
  /**
8
47
  * Apply the exit policy for a child whose output was inherited by the
9
48
  * parent. When the child dies via signal, re-raise it on the parent so
@@ -50,39 +89,14 @@ export default async function runCommand(scriptPath) {
50
89
  logger.plain(` Network: ${network}`);
51
90
  }
52
91
  logger.newline();
53
- // Find tsx binary - try multiple locations for compatibility
54
- // Uses require.resolve for cross-platform compatibility (works on Windows, macOS, Linux)
55
- const __filename = fileURLToPath(import.meta.url);
56
- const __dirname = dirname(__filename);
57
- // Create require function for ESM (needed to use require.resolve in ESM modules)
58
- const require = createRequire(import.meta.url);
59
- let tsxPath;
60
- try {
61
- // Try to resolve tsx package from user's project first
62
- const tsxPackagePath = require.resolve("tsx", { paths: [process.cwd()] });
63
- // require.resolve("tsx") returns .../tsx/dist/loader.mjs
64
- // We need to go up to the tsx package root, then into dist/cli.mjs
65
- const tsxPackageRoot = dirname(dirname(tsxPackagePath));
66
- tsxPath = join(tsxPackageRoot, "dist", "cli.mjs");
67
- // Verify the file exists
68
- if (!existsSync(tsxPath)) {
69
- throw new Error("cli.mjs not found");
70
- }
71
- }
72
- catch {
73
- try {
74
- // Fallback to movehat's own tsx
75
- const tsxPackagePath = require.resolve("tsx", { paths: [__dirname] });
76
- const tsxPackageRoot = dirname(dirname(tsxPackagePath));
77
- tsxPath = join(tsxPackageRoot, "dist", "cli.mjs");
78
- if (!existsSync(tsxPath)) {
79
- throw new Error("cli.mjs not found");
80
- }
81
- }
82
- catch {
83
- tsxPath = "";
84
- }
92
+ // Find tsx binary — bundled-first per #52 (supply-chain hardening).
93
+ // Cwd resolution stays available behind an opt-in env var for users
94
+ // who pinned a different tsx in their project.
95
+ if (process.env.MOVEHAT_TSX_FROM_CWD === "1") {
96
+ logger.warning("MOVEHAT_TSX_FROM_CWD=1: resolving tsx from cwd first. " +
97
+ "Only use this in trusted project directories.");
85
98
  }
99
+ const tsxPath = resolveTsxCliPath();
86
100
  if (!tsxPath) {
87
101
  logger.error("tsx binary not found");
88
102
  logger.plain(" Make sure 'tsx' is installed in your project:");
@@ -14,183 +14,165 @@ export interface SaveAccountPoolOptions {
14
14
  */
15
15
  includeImported?: boolean | undefined;
16
16
  }
17
+ export interface AccountManagerOptions {
18
+ /**
19
+ * Filesystem location where `saveAccountPool` writes `test-pool.json`
20
+ * and `loadAccountPool` reads from. When omitted, the path is computed
21
+ * LAZILY on each call from `join(process.cwd(), ".movehat", "accounts")`
22
+ * — so `process.chdir()` between construction and pool I/O is respected.
23
+ *
24
+ * Pass an explicit value when per-instance isolation matters (parallel
25
+ * test files, harness pools that must not collide with another suite's).
26
+ *
27
+ * The process-wide static facade exposed on the `AccountManager` class
28
+ * itself uses a separate singleton whose `poolPath` is EAGERLY captured
29
+ * at module-import time. That preserves the legacy F8(b) behavior for
30
+ * any code still on the static API during the 0.2.7 deprecation window;
31
+ * the static facade is removed in 0.3.0.
32
+ */
33
+ poolPath?: string | undefined;
34
+ }
17
35
  /**
18
36
  * Centralized Account Manager for movehat
19
37
  *
20
38
  * Manages all account creation, loading, and lifecycle operations.
21
- * Provides a pool of reusable test accounts with labels for better test readability.
39
+ * Provides a pool of reusable test accounts with labels for better test
40
+ * readability.
41
+ *
42
+ * Two API surfaces are exposed:
43
+ *
44
+ * **Instance API (recommended; M9.1+)** — `new AccountManager(options?)`
45
+ * gives a fully isolated pool. Two instances in the same process have
46
+ * independent labelMaps, private-key maps, and account pools. Each
47
+ * `Harness` constructed in 0.3.0+ will own one of these; user code
48
+ * accesses labeled accounts via `harness.accounts.<label>` (Harness
49
+ * snapshots them at construction time).
50
+ *
51
+ * **Static facade (deprecated; will be removed in 0.3.0)** — every
52
+ * former static method (`AccountManager.createAccount(...)`, etc.) still
53
+ * works and forwards to a single process-wide singleton. This preserves
54
+ * the existing label-sharing behavior across calls during the
55
+ * deprecation window. Migration target is the instance API; see M9 meta
56
+ * (#270) and migration guide at `/docs/upgrading/0.3.0` (published in
57
+ * M9.4).
22
58
  */
23
59
  export declare class AccountManager {
24
- private static pool;
25
- private static privateKeys;
26
- private static labelMap;
27
- private static generatedAccountAddresses;
28
- private static poolLoaded;
29
- private static defaultPoolPath;
30
- /**
31
- * Get a test account from the pool. If label is provided and exists, returns that account.
32
- * Otherwise creates a new account with the optional label.
33
- *
34
- * @param label Optional label for the account (e.g., "alice", "bob", "deployer")
60
+ private readonly pool;
61
+ private readonly privateKeys;
62
+ private readonly labelMap;
63
+ private readonly generatedAccountAddresses;
64
+ private poolLoaded;
65
+ private readonly poolPathOverride;
66
+ constructor(options?: AccountManagerOptions);
67
+ /**
68
+ * Resolved pool directory. When the caller supplied an explicit
69
+ * `poolPath` to the constructor, that value is returned verbatim.
70
+ * Otherwise the path is computed LAZILY from the current
71
+ * `process.cwd()` on each access — so `process.chdir()` performed
72
+ * between construction and pool I/O takes effect, unlike the legacy
73
+ * static-only API which captured cwd at module import time
74
+ * (audit finding F8(b)).
75
+ */
76
+ private get poolPath();
77
+ /**
78
+ * Get a test account from the pool. If label is provided and exists,
79
+ * returns that account. Otherwise creates a new account with the
80
+ * optional label.
81
+ *
82
+ * @param label Optional label for the account (e.g., "alice", "bob")
35
83
  * @returns An Account instance
36
- *
37
- * @example
38
- * const alice = AccountManager.getTestAccount("alice");
39
- * const randomAccount = AccountManager.getTestAccount();
40
84
  */
41
- static getTestAccount(label?: string): Account;
85
+ getTestAccount(label?: string): Account;
42
86
  /**
43
- * Create a new account and optionally add it to the pool with a label
44
- *
45
- * @param label Optional label for the account
46
- * @param fund Whether to fund the account (handled externally)
47
- * @returns A new Account instance
48
- *
49
- * @example
50
- * const deployer = AccountManager.createAccount("deployer");
51
- * const bob = AccountManager.createAccount("bob", true);
87
+ * Create a new account and optionally add it to the pool with a
88
+ * label. The `fund` parameter is accepted for API symmetry; actual
89
+ * funding is the caller's responsibility.
52
90
  */
53
- static createAccount(label?: string, fund?: boolean): Account;
91
+ createAccount(label?: string, _fund?: boolean): Account;
54
92
  /**
55
- * Get an account by its label
56
- *
57
- * @param label The label to look up
58
- * @returns The Account if found, undefined otherwise
59
- *
60
- * @example
61
- * const alice = AccountManager.getAccountByLabel("alice");
62
- * if (alice) {
63
- * console.log(`Alice's address: ${alice.accountAddress.toString()}`);
64
- * }
93
+ * Get an account by its label, or undefined if not present.
65
94
  */
66
- static getAccountByLabel(label: string): Account | undefined;
95
+ getAccountByLabel(label: string): Account | undefined;
67
96
  /**
68
- * Load account from environment variable
69
- *
70
- * @param envVar The environment variable name (defaults to "PRIVATE_KEY")
71
- * @returns Account instance
72
- * @throws Error if environment variable is not set
97
+ * Load account from environment variable.
73
98
  *
74
- * @example
75
- * const account = AccountManager.loadAccountFromEnv("MH_PRIVATE_KEY");
99
+ * @throws Error if the environment variable is not set
76
100
  */
77
- static loadAccountFromEnv(envVar?: string): Account;
101
+ loadAccountFromEnv(envVar?: string): Account;
78
102
  /**
79
- * Load account from a private key hex string
80
- *
81
- * @param privateKeyHex The private key as a hex string (with or without 0x prefix)
82
- * @returns Account instance
83
- *
84
- * @example
85
- * const account = AccountManager.loadAccountFromPrivateKey("0xabc123...");
103
+ * Load account from a private key hex string (with or without 0x prefix).
86
104
  */
87
- static loadAccountFromPrivateKey(privateKeyHex: string): Account;
105
+ loadAccountFromPrivateKey(privateKeyHex: string): Account;
88
106
  /**
89
- * Load all accounts from movehat config
90
- *
91
- * @param config The resolved MovehatConfig
92
- * @returns Array of Account instances
93
- *
94
- * @example
95
- * const config = await resolveNetworkConfig(userConfig);
96
- * const accounts = AccountManager.loadAccountsFromConfig(config);
107
+ * Load all accounts from movehat config.
97
108
  */
98
- static loadAccountsFromConfig(config: MovehatConfig): Account[];
109
+ loadAccountsFromConfig(config: MovehatConfig): Account[];
99
110
  /**
100
- * Get all labeled accounts in the pool
101
- *
102
- * @returns Record of label to Account mappings
103
- *
104
- * @example
105
- * const labeled = AccountManager.getLabeledAccounts();
106
- * console.log(`Deployer: ${labeled.deployer?.accountAddress.toString()}`);
111
+ * Get all labeled accounts in the pool.
107
112
  */
108
- static getLabeledAccounts(): Record<string, Account>;
113
+ getLabeledAccounts(): Record<string, Account>;
109
114
  /**
110
- * Save the current account pool to disk for persistence
111
- *
112
- * @param poolPath Optional custom path (defaults to .movehat/accounts)
113
- *
114
- * @example
115
- * AccountManager.saveAccountPool();
115
+ * Save the current account pool to disk for persistence.
116
116
  */
117
- static saveAccountPool(): void;
118
- static saveAccountPool(poolPath: string): void;
119
- static saveAccountPool(options: SaveAccountPoolOptions): void;
120
- static saveAccountPool(poolPath: string, options: SaveAccountPoolOptions): void;
117
+ saveAccountPool(): void;
118
+ saveAccountPool(poolPath: string): void;
119
+ saveAccountPool(options: SaveAccountPoolOptions): void;
120
+ saveAccountPool(poolPath: string, options: SaveAccountPoolOptions): void;
121
121
  /**
122
- * Load account pool from disk
123
- *
124
- * @param poolPath Optional custom path (defaults to .movehat/accounts)
125
- * @returns true if pool was loaded, false if file doesn't exist
126
- *
127
- * @example
128
- * if (AccountManager.loadAccountPool()) {
129
- * console.log("Pool loaded successfully");
130
- * }
122
+ * Load account pool from disk. Returns true if a pool file was found
123
+ * and parsed, false if the file does not exist or parsing failed.
131
124
  */
132
- static loadAccountPool(poolPath?: string): boolean;
125
+ loadAccountPool(poolPath?: string): boolean;
133
126
  /**
134
- * Clear the entire account pool
135
- * Useful for test isolation
136
- *
137
- * @example
138
- * after(() => {
139
- * AccountManager.clearPool();
140
- * });
127
+ * Clear the entire account pool. Useful for test isolation.
141
128
  */
142
- static clearPool(): void;
129
+ clearPool(): void;
143
130
  /**
144
- * Get the current size of the account pool
145
- *
146
- * @returns Number of accounts in the pool
131
+ * Number of accounts in the pool.
147
132
  */
148
- static getPoolSize(): number;
133
+ getPoolSize(): number;
149
134
  /**
150
- * Get all accounts in the pool
151
- *
152
- * @returns Array of all Account instances
135
+ * All accounts in the pool.
153
136
  */
154
- static getAllAccounts(): Account[];
137
+ getAllAccounts(): Account[];
155
138
  /**
156
- * Check if a label is already in use
157
- *
158
- * @param label The label to check
159
- * @returns true if label exists, false otherwise
139
+ * Whether a label is currently bound to an account in this pool.
160
140
  */
161
- static hasLabel(label: string): boolean;
141
+ hasLabel(label: string): boolean;
162
142
  /**
163
- * Get or create an account with a specific label
164
- * If the label exists, returns the existing account
165
- * Otherwise creates a new account with that label
166
- *
167
- * @param label The label for the account
168
- * @returns Account instance
169
- *
170
- * @example
171
- * const alice = AccountManager.getOrCreateLabeled("alice");
172
- * const aliceAgain = AccountManager.getOrCreateLabeled("alice"); // Same account
143
+ * Get or create an account with a specific label.
173
144
  */
174
- static getOrCreateLabeled(label: string): Account;
145
+ getOrCreateLabeled(label: string): Account;
175
146
  /**
176
- * Batch create multiple labeled accounts
177
- *
178
- * @param labels Array of labels to create
179
- * @returns Record of label to Account mappings
180
- *
181
- * @example
182
- * const accounts = AccountManager.createBatch(["alice", "bob", "charlie"]);
183
- * console.log(accounts.alice.accountAddress.toString());
147
+ * Batch create multiple labeled accounts.
184
148
  */
185
- static createBatch(labels: readonly string[]): Record<string, Account>;
149
+ createBatch(labels: readonly string[]): Record<string, Account>;
186
150
  /**
187
- * Export account private keys for backup/sharing
188
- * WARNING: Only use this for test accounts, never production keys
189
- *
190
- * @param labels Optional array of labels to export (exports all if not provided)
191
- * @returns Record of label/address to private key hex string
151
+ * Export account private keys for backup/sharing.
152
+ * WARNING: Only use this for test accounts, never production keys.
192
153
  */
154
+ exportPrivateKeys(labels?: string[]): Record<string, string>;
155
+ private accountFromPrivateKey;
156
+ private trackAccount;
157
+ static getTestAccount(label?: string): Account;
158
+ static createAccount(label?: string, fund?: boolean): Account;
159
+ static getAccountByLabel(label: string): Account | undefined;
160
+ static loadAccountFromEnv(envVar?: string): Account;
161
+ static loadAccountFromPrivateKey(privateKeyHex: string): Account;
162
+ static loadAccountsFromConfig(config: MovehatConfig): Account[];
163
+ static getLabeledAccounts(): Record<string, Account>;
164
+ static saveAccountPool(): void;
165
+ static saveAccountPool(poolPath: string): void;
166
+ static saveAccountPool(options: SaveAccountPoolOptions): void;
167
+ static saveAccountPool(poolPath: string, options: SaveAccountPoolOptions): void;
168
+ static loadAccountPool(poolPath?: string): boolean;
169
+ static clearPool(): void;
170
+ static getPoolSize(): number;
171
+ static getAllAccounts(): Account[];
172
+ static hasLabel(label: string): boolean;
173
+ static getOrCreateLabeled(label: string): Account;
174
+ static createBatch(labels: readonly string[]): Record<string, Account>;
193
175
  static exportPrivateKeys(labels?: string[]): Record<string, string>;
194
- private static accountFromPrivateKey;
195
- private static trackAccount;
196
176
  }
177
+ /** @internal — test-only. Resets the once-per-method dedup Set. */
178
+ export declare function _resetDeprecationWarnings(): void;