movehat 0.2.2 → 0.2.4
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.
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +19 -10
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/test.js +12 -19
- package/dist/commands/test.js.map +1 -1
- package/dist/core/Publisher.d.ts.map +1 -1
- package/dist/core/Publisher.js +20 -14
- package/dist/core/Publisher.js.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +8 -5
- package/dist/core/config.js.map +1 -1
- package/dist/core/deployments.d.ts.map +1 -1
- package/dist/core/deployments.js +4 -2
- package/dist/core/deployments.js.map +1 -1
- package/dist/fork/manager.d.ts +1 -1
- package/dist/fork/manager.js +11 -11
- package/dist/fork/manager.js.map +1 -1
- package/dist/fork/server.d.ts.map +1 -1
- package/dist/fork/server.js +21 -15
- package/dist/fork/server.js.map +1 -1
- package/dist/fork/test.d.ts.map +1 -1
- package/dist/fork/test.js +3 -2
- package/dist/fork/test.js.map +1 -1
- package/dist/harness/codeObject.js +11 -8
- package/dist/harness/codeObject.js.map +1 -1
- package/dist/harness/script.d.ts.map +1 -1
- package/dist/harness/script.js +9 -6
- package/dist/harness/script.js.map +1 -1
- package/dist/helpers/setupLocalTesting.js +5 -5
- package/dist/helpers/setupLocalTesting.js.map +1 -1
- package/dist/node/LocalNodeManager.d.ts +1 -1
- package/dist/node/LocalNodeManager.d.ts.map +1 -1
- package/dist/node/LocalNodeManager.js +61 -23
- package/dist/node/LocalNodeManager.js.map +1 -1
- package/dist/node/__tests__/LocalNodeManager.test.js +110 -11
- package/dist/node/__tests__/LocalNodeManager.test.js.map +1 -1
- package/dist/ui/__tests__/logger.test.d.ts +2 -0
- package/dist/ui/__tests__/logger.test.d.ts.map +1 -0
- package/dist/ui/__tests__/logger.test.js +75 -0
- package/dist/ui/__tests__/logger.test.js.map +1 -0
- package/dist/ui/formatters.d.ts +0 -16
- package/dist/ui/formatters.d.ts.map +1 -1
- package/dist/ui/formatters.js +1 -1
- package/dist/ui/formatters.js.map +1 -1
- package/dist/ui/logger.d.ts +41 -0
- package/dist/ui/logger.d.ts.map +1 -1
- package/dist/ui/logger.js +49 -0
- package/dist/ui/logger.js.map +1 -1
- package/dist/ui/spinner.d.ts +25 -0
- package/dist/ui/spinner.d.ts.map +1 -1
- package/dist/ui/spinner.js +44 -0
- package/dist/ui/spinner.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +4 -0
- package/src/commands/compile.ts +24 -15
- package/src/commands/test.ts +12 -19
- package/src/core/Publisher.ts +49 -34
- package/src/core/config.ts +9 -6
- package/src/core/deployments.ts +5 -4
- package/src/fork/manager.ts +11 -11
- package/src/fork/server.ts +21 -15
- package/src/fork/test.ts +3 -2
- package/src/harness/codeObject.ts +8 -5
- package/src/harness/script.ts +7 -4
- package/src/helpers/setupLocalTesting.ts +5 -5
- package/src/node/LocalNodeManager.ts +64 -25
- package/src/node/__tests__/LocalNodeManager.test.ts +140 -14
- package/src/types/config.ts +1 -1
- package/src/ui/__tests__/logger.test.ts +89 -0
- package/src/ui/formatters.ts +1 -1
- package/src/ui/logger.ts +62 -0
- package/src/ui/spinner.ts +47 -0
package/src/core/deployments.ts
CHANGED
|
@@ -86,9 +86,9 @@ export function saveDeployment(deployment: DeploymentInfo): void {
|
|
|
86
86
|
`Deployment saved: deployments/${deployment.network}/${deployment.moduleName}.json`
|
|
87
87
|
);
|
|
88
88
|
} catch (error) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
90
|
+
logger.error(
|
|
91
|
+
`Failed to save deployment for ${deployment.moduleName} on ${deployment.network} at ${filePath}: ${msg}`
|
|
92
92
|
);
|
|
93
93
|
throw error;
|
|
94
94
|
}
|
|
@@ -110,7 +110,8 @@ export function loadDeployment(network: string, moduleName: string): DeploymentI
|
|
|
110
110
|
const content = readFileSync(filePath, "utf-8");
|
|
111
111
|
return JSON.parse(content) as DeploymentInfo;
|
|
112
112
|
} catch (error) {
|
|
113
|
-
|
|
113
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
114
|
+
logger.error(`Failed to load deployment for ${moduleName} on ${network}: ${msg}`);
|
|
114
115
|
return null;
|
|
115
116
|
}
|
|
116
117
|
}
|
package/src/fork/manager.ts
CHANGED
|
@@ -89,7 +89,7 @@ export class ForkManager {
|
|
|
89
89
|
|
|
90
90
|
this.storage.saveMetadata(this.metadata);
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
logger.success(`Fork initialized at ledger version ${ledgerInfo.ledger_version}`);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
/**
|
|
@@ -123,7 +123,7 @@ export class ForkManager {
|
|
|
123
123
|
throw new Error('Fork not initialized. Call initialize() or load() first.');
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
logger.info(`Fetching account ${normalizedAddress} from network...`, 2);
|
|
127
127
|
const accountData = await this.apiClient.getAccount(normalizedAddress);
|
|
128
128
|
|
|
129
129
|
accountState = {
|
|
@@ -132,7 +132,7 @@ export class ForkManager {
|
|
|
132
132
|
};
|
|
133
133
|
|
|
134
134
|
this.storage.saveAccount(normalizedAddress, accountState);
|
|
135
|
-
|
|
135
|
+
logger.success(`Cached account ${normalizedAddress}`, 2);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
return accountState;
|
|
@@ -148,14 +148,14 @@ export class ForkManager {
|
|
|
148
148
|
throw new Error('Fork not initialized. Call initialize() or load() first.');
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
logger.info(`Fetching resource ${resourceType} for ${normalizedAddress}...`, 2);
|
|
152
152
|
|
|
153
153
|
try {
|
|
154
154
|
const resourceData = await this.apiClient.getAccountResource(normalizedAddress, resourceType);
|
|
155
155
|
resource = resourceData.data;
|
|
156
156
|
|
|
157
157
|
this.storage.saveResource(normalizedAddress, resourceType, resource);
|
|
158
|
-
|
|
158
|
+
logger.success(`Cached resource ${resourceType}`, 2);
|
|
159
159
|
} catch (error) {
|
|
160
160
|
const msg = error instanceof Error ? error.message : String(error);
|
|
161
161
|
if (msg.includes('404')) {
|
|
@@ -178,7 +178,7 @@ export class ForkManager {
|
|
|
178
178
|
throw new Error('Fork not initialized. Call initialize() or load() first.');
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
logger.info(`Fetching all resources for ${normalizedAddress}...`, 2);
|
|
182
182
|
const resourcesList = await this.apiClient.getAccountResources(normalizedAddress);
|
|
183
183
|
|
|
184
184
|
resources = {};
|
|
@@ -187,7 +187,7 @@ export class ForkManager {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
this.storage.saveAllResources(normalizedAddress, resources);
|
|
190
|
-
|
|
190
|
+
logger.success(`Cached ${Object.keys(resources).length} resources`, 2);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
return resources;
|
|
@@ -196,7 +196,7 @@ export class ForkManager {
|
|
|
196
196
|
async setResource(address: string, resourceType: string, data: unknown): Promise<void> {
|
|
197
197
|
const normalizedAddress = normalizeAddress(address);
|
|
198
198
|
this.storage.saveResource(normalizedAddress, resourceType, data);
|
|
199
|
-
|
|
199
|
+
logger.success(`Updated resource ${resourceType} for ${normalizedAddress}`, 2);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/** Adds to the existing balance rather than replacing it. */
|
|
@@ -258,7 +258,7 @@ export class ForkManager {
|
|
|
258
258
|
this.storage.saveAccount(normalizedAddress, account);
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
|
|
261
|
+
logger.success(`Funded ${normalizedAddress} with ${amount} coins`, 2);
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
listAccounts(): string[] {
|
|
@@ -275,7 +275,7 @@ export class ForkManager {
|
|
|
275
275
|
* @example
|
|
276
276
|
* await forkManager.fundMultipleAccounts(
|
|
277
277
|
* ["0x123...", "0x456..."],
|
|
278
|
-
* 100_000_000 //
|
|
278
|
+
* 100_000_000 // 1 MOVE
|
|
279
279
|
* );
|
|
280
280
|
*/
|
|
281
281
|
async fundMultipleAccounts(
|
|
@@ -335,7 +335,7 @@ export class ForkManager {
|
|
|
335
335
|
};
|
|
336
336
|
|
|
337
337
|
this.storage.saveAccount(normalizedAddress, newAccount);
|
|
338
|
-
|
|
338
|
+
logger.success(`Created new account ${normalizedAddress}`, 2);
|
|
339
339
|
|
|
340
340
|
return newAccount;
|
|
341
341
|
}
|
package/src/fork/server.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import { URL } from 'url';
|
|
3
3
|
import { ForkManager } from './manager.js';
|
|
4
|
+
import { logger } from '../ui/index.js';
|
|
4
5
|
|
|
5
6
|
export interface ForkServerOptions {
|
|
6
7
|
/**
|
|
@@ -66,16 +67,17 @@ export class ForkServer {
|
|
|
66
67
|
this.forkManager.load();
|
|
67
68
|
const metadata = this.forkManager.getMetadata();
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
logger.newline();
|
|
71
|
+
logger.phase("Fork Server");
|
|
72
|
+
logger.kv("Network", metadata.network, 2);
|
|
73
|
+
logger.kv("Chain ID", String(metadata.chainId), 2);
|
|
74
|
+
logger.kv("Ledger Version", String(metadata.ledgerVersion), 2);
|
|
75
|
+
logger.kv("Forked at", metadata.createdAt, 2);
|
|
74
76
|
|
|
75
77
|
this.server = http.createServer((req, res) => {
|
|
76
78
|
this.handleRequest(req, res).catch((error) => {
|
|
77
79
|
// Log full error server-side for diagnostics
|
|
78
|
-
|
|
80
|
+
logger.error(`Error handling request: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
81
|
|
|
80
82
|
// Only send response if headers haven't been sent yet
|
|
81
83
|
if (!res.headersSent) {
|
|
@@ -124,13 +126,15 @@ export class ForkServer {
|
|
|
124
126
|
: isIpv6
|
|
125
127
|
? `[${this.host}]`
|
|
126
128
|
: this.host;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
logger.newline();
|
|
130
|
+
logger.success(`Fork Server listening on http://${displayHost}:${this.port}`);
|
|
131
|
+
logger.kv("Bound interface", this.host, 2);
|
|
132
|
+
logger.kv("Ledger Info", `http://${displayHost}:${this.port}/v1/`, 2);
|
|
130
133
|
if (this.host === '0.0.0.0') {
|
|
131
|
-
|
|
134
|
+
logger.warning("Server is bound to 0.0.0.0 — fork state is reachable from the LAN.", 2);
|
|
132
135
|
}
|
|
133
|
-
|
|
136
|
+
logger.newline();
|
|
137
|
+
logger.info("Press Ctrl+C to stop");
|
|
134
138
|
resolve();
|
|
135
139
|
});
|
|
136
140
|
});
|
|
@@ -143,7 +147,8 @@ export class ForkServer {
|
|
|
143
147
|
return new Promise((resolve) => {
|
|
144
148
|
if (this.server) {
|
|
145
149
|
this.server.close(() => {
|
|
146
|
-
|
|
150
|
+
logger.newline();
|
|
151
|
+
logger.success("Fork Server stopped");
|
|
147
152
|
resolve();
|
|
148
153
|
});
|
|
149
154
|
} else {
|
|
@@ -172,8 +177,9 @@ export class ForkServer {
|
|
|
172
177
|
const url = new URL(req.url || '/', `http://localhost:${this.port}`);
|
|
173
178
|
const pathname = url.pathname;
|
|
174
179
|
|
|
175
|
-
// Log request
|
|
176
|
-
|
|
180
|
+
// Log request — plain so the fork-server access log retains its
|
|
181
|
+
// grep-friendly line shape (timestamp + method + path, no symbol).
|
|
182
|
+
logger.plain(`[${new Date().toISOString()}] ${req.method} ${pathname}`);
|
|
177
183
|
|
|
178
184
|
this.applyCors(req, res);
|
|
179
185
|
|
|
@@ -216,7 +222,7 @@ export class ForkServer {
|
|
|
216
222
|
}
|
|
217
223
|
} catch (error) {
|
|
218
224
|
// Log full error server-side for diagnostics
|
|
219
|
-
|
|
225
|
+
logger.error(`Error handling request: ${error instanceof Error ? error.message : String(error)}`);
|
|
220
226
|
|
|
221
227
|
// Send generic error to client (don't expose internal details)
|
|
222
228
|
this.sendError(res, 500, 'Internal server error');
|
package/src/fork/test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from 'path';
|
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import { runCli } from '../utils/runCli.js';
|
|
4
4
|
import type { ChildProcessAdapter } from '../utils/childProcessAdapter.js';
|
|
5
|
+
import { logger } from '../ui/index.js';
|
|
5
6
|
|
|
6
7
|
export interface SnapshotOptions {
|
|
7
8
|
path?: string;
|
|
@@ -40,7 +41,7 @@ export async function snapshot(options: SnapshotOptions = {}): Promise<string> {
|
|
|
40
41
|
const name = options.name || `snapshot-${Date.now()}`;
|
|
41
42
|
const snapshotPath = options.path || join(process.cwd(), '.movehat', 'snapshots', name);
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
logger.info(`Creating snapshot: ${name}...`);
|
|
44
45
|
|
|
45
46
|
try {
|
|
46
47
|
// Initialize fork/snapshot using aptos CLI.
|
|
@@ -68,7 +69,7 @@ export async function snapshot(options: SnapshotOptions = {}): Promise<string> {
|
|
|
68
69
|
throw new Error('Snapshot directory was not created');
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
logger.success(`Snapshot created at ${snapshotPath}`, 2);
|
|
72
73
|
return snapshotPath;
|
|
73
74
|
} catch (error) {
|
|
74
75
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "../errors.js";
|
|
21
21
|
import { runCli } from "../utils/runCli.js";
|
|
22
22
|
import { parseTxHash } from "../utils/parseCliOutput.js";
|
|
23
|
-
import { logger } from "../ui/index.js";
|
|
23
|
+
import { logger, isVerbose } from "../ui/index.js";
|
|
24
24
|
import {
|
|
25
25
|
writeTempKeyFile,
|
|
26
26
|
removeKeyFile,
|
|
@@ -206,7 +206,7 @@ async function executeMovementMoveObject(
|
|
|
206
206
|
},
|
|
207
207
|
{ adapter: opts.adapter }
|
|
208
208
|
);
|
|
209
|
-
if (buildResult.stdout)
|
|
209
|
+
if (isVerbose() && buildResult.stdout) logger.info(buildResult.stdout.trim(), 2);
|
|
210
210
|
|
|
211
211
|
// Format the private key into AIP-80 shape so the Movement CLI
|
|
212
212
|
// doesn't emit its raw-hex deprecation warning. `formatPrivateKey`
|
|
@@ -265,8 +265,11 @@ async function executeMovementMoveObject(
|
|
|
265
265
|
{ adapter: opts.adapter }
|
|
266
266
|
);
|
|
267
267
|
deployOut = result.stdout;
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
// Both streams gated behind isVerbose(); see §9 — stream channel
|
|
269
|
+
// is not by itself a failure signal. Real failures throw via
|
|
270
|
+
// CliExecutionError and are surfaced from the catch below.
|
|
271
|
+
if (isVerbose() && result.stdout) logger.info(result.stdout.trim(), 2);
|
|
272
|
+
if (isVerbose() && result.stderr) logger.info(result.stderr.trim(), 2);
|
|
270
273
|
} finally {
|
|
271
274
|
// Unlink via the observable helper — emit a warning if the file
|
|
272
275
|
// could not be removed AND still exists on disk (private key
|
|
@@ -336,7 +339,7 @@ async function executeMovementMoveObject(
|
|
|
336
339
|
throw error;
|
|
337
340
|
}
|
|
338
341
|
if (error instanceof CliExecutionError) {
|
|
339
|
-
if (error.stdoutPreview)
|
|
342
|
+
if (error.stdoutPreview) logger.info(error.stdoutPreview, 2);
|
|
340
343
|
logger.error(
|
|
341
344
|
`Failed to ${subcommand === "deploy-object" ? "deploy" : "upgrade"} module: ${error.message}\n${error.stderr}`
|
|
342
345
|
);
|
package/src/harness/script.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { validatePathSafety } from "../core/shell.js";
|
|
|
10
10
|
import { CliExecutionError } from "../errors.js";
|
|
11
11
|
import { runCli } from "../utils/runCli.js";
|
|
12
12
|
import { parseTxHash } from "../utils/parseCliOutput.js";
|
|
13
|
-
import { logger } from "../ui/index.js";
|
|
13
|
+
import { logger, isVerbose } from "../ui/index.js";
|
|
14
14
|
import {
|
|
15
15
|
writeTempKeyFile,
|
|
16
16
|
removeKeyFile,
|
|
@@ -132,8 +132,11 @@ export async function runMoveScript(
|
|
|
132
132
|
{ adapter: options.adapter }
|
|
133
133
|
);
|
|
134
134
|
scriptOut = result.stdout;
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// Both streams gated behind isVerbose(); Movement CLI uses
|
|
136
|
+
// stderr for progress messages too. Real failures throw via
|
|
137
|
+
// CliExecutionError and are surfaced from the catch below.
|
|
138
|
+
if (isVerbose() && result.stdout) logger.info(result.stdout.trim(), 2);
|
|
139
|
+
if (isVerbose() && result.stderr) logger.info(result.stderr.trim(), 2);
|
|
137
140
|
} finally {
|
|
138
141
|
// Observable cleanup — emit a warning if the unlink failed and
|
|
139
142
|
// the file is still on disk (private key would persist silently
|
|
@@ -167,7 +170,7 @@ export async function runMoveScript(
|
|
|
167
170
|
return out;
|
|
168
171
|
} catch (error) {
|
|
169
172
|
if (error instanceof CliExecutionError) {
|
|
170
|
-
if (error.stdoutPreview)
|
|
173
|
+
if (error.stdoutPreview) logger.info(error.stdoutPreview, 2);
|
|
171
174
|
logger.error(`Failed to run Move script: ${error.message}\n${error.stderr}`);
|
|
172
175
|
} else {
|
|
173
176
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -95,9 +95,9 @@ export async function setupLocalTesting(
|
|
|
95
95
|
const accountLabels = options.accountLabels || ["deployer", "alice", "bob"];
|
|
96
96
|
|
|
97
97
|
logger.newline();
|
|
98
|
-
logger.
|
|
99
|
-
logger.
|
|
100
|
-
logger.
|
|
98
|
+
logger.phase("Setting up local testing environment");
|
|
99
|
+
logger.kv("Mode", mode, 2);
|
|
100
|
+
logger.kv("Accounts", accountLabels.join(", "), 2);
|
|
101
101
|
logger.newline();
|
|
102
102
|
|
|
103
103
|
if (mode === 'local-node') {
|
|
@@ -239,7 +239,7 @@ async function setupWithLocalNode(
|
|
|
239
239
|
logger.plain(` RPC: ${nodeInfo.rpcUrl}/v1`);
|
|
240
240
|
logger.plain(` Faucet: ${nodeInfo.faucetUrl}`);
|
|
241
241
|
logger.plain(` Accounts: ${Array.from(accountLabels).join(", ")}`);
|
|
242
|
-
logger.plain(` Balance per account: ${defaultBalance / 100_000_000}
|
|
242
|
+
logger.plain(` Balance per account: ${defaultBalance / 100_000_000} MOVE`);
|
|
243
243
|
logger.newline();
|
|
244
244
|
|
|
245
245
|
return { runtime, localNode };
|
|
@@ -376,7 +376,7 @@ async function setupWithFork(
|
|
|
376
376
|
logger.plain(` Mode: fork (read-only)`);
|
|
377
377
|
logger.plain(` RPC: http://localhost:${forkPort}/v1`);
|
|
378
378
|
logger.plain(` Accounts: ${Array.from(accountLabels).join(", ")}`);
|
|
379
|
-
logger.plain(` Balance per account: ${defaultBalance / 100_000_000}
|
|
379
|
+
logger.plain(` Balance per account: ${defaultBalance / 100_000_000} MOVE`);
|
|
380
380
|
logger.newline();
|
|
381
381
|
|
|
382
382
|
return { runtime, forkServer, forkManager };
|
|
@@ -6,7 +6,16 @@ import {
|
|
|
6
6
|
type ChildProcessAdapter,
|
|
7
7
|
type SpawnedProcess,
|
|
8
8
|
} from "../utils/childProcessAdapter.js";
|
|
9
|
-
import { logger } from "../ui/index.js";
|
|
9
|
+
import { logger, isVerbose, colors, symbols } from "../ui/index.js";
|
|
10
|
+
import { withTimedSpinner, withSpinner } from "../ui/spinner.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Substrings that always surface from the movement subprocess regardless
|
|
14
|
+
* of verbosity. These are signals the user must see to debug a stuck
|
|
15
|
+
* startup (panic, fatal, address-in-use). Tested in
|
|
16
|
+
* __tests__/LocalNodeManager.test.ts to guard against silent regressions.
|
|
17
|
+
*/
|
|
18
|
+
const CRITICAL_NODE_OUTPUT = /panic|fatal|address already in use|EADDRINUSE/i;
|
|
10
19
|
|
|
11
20
|
export interface LocalNodeOptions {
|
|
12
21
|
testDir?: string; // Directory for node data (default: .movehat/local-node)
|
|
@@ -86,7 +95,7 @@ export class LocalNodeManager {
|
|
|
86
95
|
*/
|
|
87
96
|
async start(): Promise<LocalNodeInfo> {
|
|
88
97
|
if (this.spawned) {
|
|
89
|
-
|
|
98
|
+
logger.info("Local node already running");
|
|
90
99
|
return this.getNodeInfo();
|
|
91
100
|
}
|
|
92
101
|
|
|
@@ -98,11 +107,11 @@ export class LocalNodeManager {
|
|
|
98
107
|
|
|
99
108
|
try {
|
|
100
109
|
logger.newline();
|
|
101
|
-
logger.step("Starting local Movement node
|
|
102
|
-
logger.
|
|
103
|
-
logger.
|
|
104
|
-
logger.
|
|
105
|
-
logger.
|
|
110
|
+
logger.step("Starting local Movement node");
|
|
111
|
+
logger.kv("Test directory", this.options.testDir, 2);
|
|
112
|
+
logger.kv("RPC port", String(this.options.apiPort), 2);
|
|
113
|
+
logger.kv("Faucet port", String(this.options.faucetPort), 2);
|
|
114
|
+
logger.kv("Ready port", String(this.options.readyPort), 2);
|
|
106
115
|
logger.newline();
|
|
107
116
|
|
|
108
117
|
// Clean state if force restart
|
|
@@ -133,19 +142,43 @@ export class LocalNodeManager {
|
|
|
133
142
|
stdio: this.options.silent ? "ignore" : "pipe",
|
|
134
143
|
});
|
|
135
144
|
|
|
136
|
-
//
|
|
145
|
+
// Subprocess output handling (see §9 Console UX in CLAUDE.md):
|
|
146
|
+
// - stdout chatter is hidden by default; gated by isVerbose()
|
|
147
|
+
// - lines matching CRITICAL_NODE_OUTPUT always surface as warnings
|
|
148
|
+
// so the user is never silenced through a real failure
|
|
149
|
+
// - stderr is always surfaced (real signal), modulo benign WARN
|
|
150
|
+
// lines that the movement binary emits during normal startup
|
|
137
151
|
if (!this.options.silent && this.spawned.stdout && this.spawned.stderr) {
|
|
138
152
|
this.spawned.stdout.on("data", (data: Buffer) => {
|
|
139
153
|
const output = data.toString().trim();
|
|
140
|
-
if (output)
|
|
141
|
-
|
|
154
|
+
if (!output) return;
|
|
155
|
+
if (CRITICAL_NODE_OUTPUT.test(output)) {
|
|
156
|
+
logger.warning(output);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (isVerbose()) {
|
|
160
|
+
for (const line of output.split("\n")) {
|
|
161
|
+
if (line) console.log(` ${colors.muted(symbols.pointer + " " + line)}`);
|
|
162
|
+
}
|
|
142
163
|
}
|
|
143
164
|
});
|
|
144
165
|
|
|
145
166
|
this.spawned.stderr.on("data", (data: Buffer) => {
|
|
146
167
|
const output = data.toString().trim();
|
|
147
|
-
if (
|
|
148
|
-
|
|
168
|
+
if (!output) return;
|
|
169
|
+
// Movement CLI uses stderr for both progress messages
|
|
170
|
+
// ("Applying post startup steps...", "Compiling...") and
|
|
171
|
+
// real errors. Stream channel alone isn't a reliable
|
|
172
|
+
// signal — gate routine lines behind verbosity, escalate
|
|
173
|
+
// anything matching CRITICAL_NODE_OUTPUT regardless.
|
|
174
|
+
if (CRITICAL_NODE_OUTPUT.test(output)) {
|
|
175
|
+
logger.error(output);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (isVerbose()) {
|
|
179
|
+
for (const line of output.split("\n")) {
|
|
180
|
+
if (line) console.error(` ${colors.muted(symbols.pointer + " " + line)}`);
|
|
181
|
+
}
|
|
149
182
|
}
|
|
150
183
|
});
|
|
151
184
|
}
|
|
@@ -161,11 +194,13 @@ export class LocalNodeManager {
|
|
|
161
194
|
this.spawned = null;
|
|
162
195
|
});
|
|
163
196
|
|
|
164
|
-
// Wait for node to be ready
|
|
165
|
-
|
|
166
|
-
|
|
197
|
+
// Wait for node to be ready — wrapped in withTimedSpinner so the
|
|
198
|
+
// user sees live elapsed-time feedback while subprocess chatter is
|
|
199
|
+
// hidden in non-verbose mode.
|
|
200
|
+
await withTimedSpinner("Waiting for node to be ready", () =>
|
|
201
|
+
this.waitForReady(60000)
|
|
202
|
+
);
|
|
167
203
|
|
|
168
|
-
logger.success("Local Movement node is ready!");
|
|
169
204
|
logger.newline();
|
|
170
205
|
|
|
171
206
|
this.starting = false;
|
|
@@ -277,7 +312,7 @@ export class LocalNodeManager {
|
|
|
277
312
|
* Fund an account using the local faucet
|
|
278
313
|
*
|
|
279
314
|
* @param account Account instance or address to fund
|
|
280
|
-
* @param amount Amount in octas (default: 100_000_000 =
|
|
315
|
+
* @param amount Amount in octas (default: 100_000_000 = 1 MOVE)
|
|
281
316
|
*/
|
|
282
317
|
async fundAccount(account: Account | string, amount: number = 100_000_000): Promise<void> {
|
|
283
318
|
if (!this.isRunning()) {
|
|
@@ -303,7 +338,9 @@ export class LocalNodeManager {
|
|
|
303
338
|
}
|
|
304
339
|
|
|
305
340
|
const result = await response.json();
|
|
306
|
-
|
|
341
|
+
if (isVerbose()) {
|
|
342
|
+
logger.success(`Funded ${address} with ${amount} octas`, 2);
|
|
343
|
+
}
|
|
307
344
|
|
|
308
345
|
return result;
|
|
309
346
|
} catch (error) {
|
|
@@ -317,13 +354,15 @@ export class LocalNodeManager {
|
|
|
317
354
|
*/
|
|
318
355
|
async fundAccounts(accounts: (Account | string)[], amount: number = 100_000_000): Promise<void> {
|
|
319
356
|
logger.newline();
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
357
|
+
await withSpinner(
|
|
358
|
+
`Funding ${accounts.length} accounts from local faucet`,
|
|
359
|
+
async () => {
|
|
360
|
+
for (const account of accounts) {
|
|
361
|
+
await this.fundAccount(account, amount);
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
`Funded ${accounts.length} accounts (${(amount / 1e8).toFixed(0)} MOVE each)`,
|
|
365
|
+
);
|
|
327
366
|
logger.newline();
|
|
328
367
|
}
|
|
329
368
|
|
|
@@ -181,7 +181,7 @@ describe("LocalNodeManager — start / stop / lifecycle", () => {
|
|
|
181
181
|
expect(proc.stderr!.listenerCount("data")).toBe(0);
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
it("start in non-silent mode wires stdout/stderr listeners that
|
|
184
|
+
it("start in non-silent mode wires stdout/stderr listeners that respect §9 filtering", async () => {
|
|
185
185
|
const { adapter, spawned } = buildFakeAdapter();
|
|
186
186
|
stubFetchAlwaysOk();
|
|
187
187
|
const mgr = new LocalNodeManager({ adapter, testDir: tmpDir, silent: false });
|
|
@@ -191,19 +191,9 @@ describe("LocalNodeManager — start / stop / lifecycle", () => {
|
|
|
191
191
|
const proc = spawned[0]!;
|
|
192
192
|
expect(proc.stdout!.listenerCount("data")).toBeGreaterThan(0);
|
|
193
193
|
expect(proc.stderr!.listenerCount("data")).toBeGreaterThan(0);
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("local node ready"));
|
|
198
|
-
|
|
199
|
-
// stderr non-WARN line goes through console.error.
|
|
200
|
-
proc.stderr!.emit("data", Buffer.from("real error\n"));
|
|
201
|
-
expect(errSpy).toHaveBeenCalledWith(expect.stringContaining("real error"));
|
|
202
|
-
|
|
203
|
-
// stderr WARN line should be filtered (no error log).
|
|
204
|
-
errSpy.mockClear();
|
|
205
|
-
proc.stderr!.emit("data", Buffer.from("WARN something\n"));
|
|
206
|
-
expect(errSpy).not.toHaveBeenCalledWith(expect.stringContaining("WARN"));
|
|
194
|
+
// Detailed filter behavior — what passes through vs what's gated
|
|
195
|
+
// behind isVerbose() — lives in the "§9 console UX" describe block
|
|
196
|
+
// below. This test only locks the listener-wiring contract.
|
|
207
197
|
});
|
|
208
198
|
|
|
209
199
|
it("force-restart cleans the test directory before spawn", async () => {
|
|
@@ -451,3 +441,139 @@ describe("LocalNodeManager — fundAccount", () => {
|
|
|
451
441
|
expect(fetchFn.mock.calls.length).toBeGreaterThanOrEqual(4);
|
|
452
442
|
});
|
|
453
443
|
});
|
|
444
|
+
|
|
445
|
+
describe("LocalNodeManager — subprocess output filtering (§9 console UX)", () => {
|
|
446
|
+
let tmpDir: string;
|
|
447
|
+
let logSpy: ReturnType<typeof vi.spyOn>;
|
|
448
|
+
let errSpy: ReturnType<typeof vi.spyOn>;
|
|
449
|
+
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
450
|
+
|
|
451
|
+
beforeEach(() => {
|
|
452
|
+
tmpDir = mkdtempSync(join(tmpdir(), "movehat-localnode-filter-"));
|
|
453
|
+
logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
|
|
454
|
+
errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
455
|
+
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
|
456
|
+
// Ensure quiet mode is the default for each test; the verbose tests
|
|
457
|
+
// set MOVEHAT_VERBOSE explicitly inside their own scope.
|
|
458
|
+
delete process.env.MOVEHAT_VERBOSE;
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
afterEach(() => {
|
|
462
|
+
vi.restoreAllMocks();
|
|
463
|
+
vi.unstubAllGlobals();
|
|
464
|
+
delete process.env.MOVEHAT_VERBOSE;
|
|
465
|
+
if (existsSync(tmpDir)) {
|
|
466
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("hides routine stdout chatter from the movement node in quiet mode", async () => {
|
|
471
|
+
const { adapter, spawned } = buildFakeAdapter();
|
|
472
|
+
stubFetchAlwaysOk();
|
|
473
|
+
const mgr = new LocalNodeManager({ adapter, testDir: tmpDir });
|
|
474
|
+
await mgr.start();
|
|
475
|
+
|
|
476
|
+
const proc = spawned[0]!;
|
|
477
|
+
logSpy.mockClear();
|
|
478
|
+
|
|
479
|
+
// Push routine chatter — exactly the noise we used to spam to stdout.
|
|
480
|
+
proc.stdout!.emit("data", Buffer.from("Loading aptos framework module"));
|
|
481
|
+
proc.stdout!.emit("data", Buffer.from("Compiling Move bytecode"));
|
|
482
|
+
proc.stdout!.emit("data", Buffer.from("aptos_account: account created"));
|
|
483
|
+
|
|
484
|
+
// None of these should have reached stdout (no `[Node]` prefix, no
|
|
485
|
+
// muted gray `›` prefix). The only console.log calls that may arise
|
|
486
|
+
// are from the spinner mock falling back to plain text — but since
|
|
487
|
+
// ora auto-disables in non-TTY (vitest is non-TTY), even those are
|
|
488
|
+
// suppressed.
|
|
489
|
+
const stdoutCalls = logSpy.mock.calls.flat().join(" ");
|
|
490
|
+
expect(stdoutCalls).not.toContain("Loading aptos framework module");
|
|
491
|
+
expect(stdoutCalls).not.toContain("Compiling Move bytecode");
|
|
492
|
+
expect(stdoutCalls).not.toContain("aptos_account: account created");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("always surfaces panic / fatal lines as warnings, even in quiet mode", async () => {
|
|
496
|
+
const { adapter, spawned } = buildFakeAdapter();
|
|
497
|
+
stubFetchAlwaysOk();
|
|
498
|
+
const mgr = new LocalNodeManager({ adapter, testDir: tmpDir });
|
|
499
|
+
await mgr.start();
|
|
500
|
+
|
|
501
|
+
const proc = spawned[0]!;
|
|
502
|
+
warnSpy.mockClear();
|
|
503
|
+
|
|
504
|
+
proc.stdout!.emit("data", Buffer.from("thread 'main' panicked at 'state corrupted'"));
|
|
505
|
+
|
|
506
|
+
const warnCalls = warnSpy.mock.calls.flat().join(" ");
|
|
507
|
+
expect(warnCalls).toContain("panicked");
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("surfaces 'address already in use' (port conflict) regardless of verbosity", async () => {
|
|
511
|
+
const { adapter, spawned } = buildFakeAdapter();
|
|
512
|
+
stubFetchAlwaysOk();
|
|
513
|
+
const mgr = new LocalNodeManager({ adapter, testDir: tmpDir });
|
|
514
|
+
await mgr.start();
|
|
515
|
+
|
|
516
|
+
const proc = spawned[0]!;
|
|
517
|
+
warnSpy.mockClear();
|
|
518
|
+
|
|
519
|
+
proc.stdout!.emit(
|
|
520
|
+
"data",
|
|
521
|
+
Buffer.from("error: address already in use: 127.0.0.1:8080"),
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const warnCalls = warnSpy.mock.calls.flat().join(" ");
|
|
525
|
+
expect(warnCalls).toContain("address already in use");
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("surfaces stdout chatter with gray prefix in verbose mode", async () => {
|
|
529
|
+
process.env.MOVEHAT_VERBOSE = "1";
|
|
530
|
+
const { adapter, spawned } = buildFakeAdapter();
|
|
531
|
+
stubFetchAlwaysOk();
|
|
532
|
+
const mgr = new LocalNodeManager({ adapter, testDir: tmpDir });
|
|
533
|
+
await mgr.start();
|
|
534
|
+
|
|
535
|
+
const proc = spawned[0]!;
|
|
536
|
+
logSpy.mockClear();
|
|
537
|
+
|
|
538
|
+
proc.stdout!.emit("data", Buffer.from("Loading aptos framework module"));
|
|
539
|
+
|
|
540
|
+
const stdoutCalls = logSpy.mock.calls.flat().join(" ");
|
|
541
|
+
expect(stdoutCalls).toContain("Loading aptos framework module");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("always surfaces critical stderr (panic/EADDRINUSE) via logger.error", async () => {
|
|
545
|
+
const { adapter, spawned } = buildFakeAdapter();
|
|
546
|
+
stubFetchAlwaysOk();
|
|
547
|
+
const mgr = new LocalNodeManager({ adapter, testDir: tmpDir });
|
|
548
|
+
await mgr.start();
|
|
549
|
+
|
|
550
|
+
const proc = spawned[0]!;
|
|
551
|
+
errSpy.mockClear();
|
|
552
|
+
|
|
553
|
+
proc.stderr!.emit("data", Buffer.from("thread panicked: failed to bind socket"));
|
|
554
|
+
|
|
555
|
+
const stderrCalls = errSpy.mock.calls.flat().join(" ");
|
|
556
|
+
expect(stderrCalls).toContain("panicked");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("hides routine progress stderr in quiet mode (Movement CLI emits progress to stderr too)", async () => {
|
|
560
|
+
const { adapter, spawned } = buildFakeAdapter();
|
|
561
|
+
stubFetchAlwaysOk();
|
|
562
|
+
const mgr = new LocalNodeManager({ adapter, testDir: tmpDir });
|
|
563
|
+
await mgr.start();
|
|
564
|
+
|
|
565
|
+
const proc = spawned[0]!;
|
|
566
|
+
errSpy.mockClear();
|
|
567
|
+
|
|
568
|
+
// The movement subprocess emits progress messages to stderr that
|
|
569
|
+
// are NOT errors: "Applying post startup steps...", "Compiling,
|
|
570
|
+
// may take a little while...". Hiding stream channel from the
|
|
571
|
+
// user keeps the console clean.
|
|
572
|
+
proc.stderr!.emit("data", Buffer.from("Applying post startup steps..."));
|
|
573
|
+
proc.stderr!.emit("data", Buffer.from("[WARN] deprecated config field"));
|
|
574
|
+
|
|
575
|
+
const stderrCalls = errSpy.mock.calls.flat().join(" ");
|
|
576
|
+
expect(stderrCalls).not.toContain("Applying post startup steps");
|
|
577
|
+
expect(stderrCalls).not.toContain("deprecated config field");
|
|
578
|
+
});
|
|
579
|
+
});
|