movehat 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/cli.js +4 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/compile.d.ts.map +1 -1
  4. package/dist/commands/compile.js +19 -10
  5. package/dist/commands/compile.js.map +1 -1
  6. package/dist/commands/test.js +12 -19
  7. package/dist/commands/test.js.map +1 -1
  8. package/dist/core/Publisher.d.ts.map +1 -1
  9. package/dist/core/Publisher.js +20 -14
  10. package/dist/core/Publisher.js.map +1 -1
  11. package/dist/core/config.d.ts.map +1 -1
  12. package/dist/core/config.js +8 -5
  13. package/dist/core/config.js.map +1 -1
  14. package/dist/core/deployments.d.ts.map +1 -1
  15. package/dist/core/deployments.js +4 -2
  16. package/dist/core/deployments.js.map +1 -1
  17. package/dist/fork/manager.js +10 -10
  18. package/dist/fork/manager.js.map +1 -1
  19. package/dist/fork/server.d.ts.map +1 -1
  20. package/dist/fork/server.js +21 -15
  21. package/dist/fork/server.js.map +1 -1
  22. package/dist/fork/test.d.ts.map +1 -1
  23. package/dist/fork/test.js +3 -2
  24. package/dist/fork/test.js.map +1 -1
  25. package/dist/harness/codeObject.js +11 -8
  26. package/dist/harness/codeObject.js.map +1 -1
  27. package/dist/harness/script.d.ts.map +1 -1
  28. package/dist/harness/script.js +9 -6
  29. package/dist/harness/script.js.map +1 -1
  30. package/dist/helpers/setupLocalTesting.js +3 -3
  31. package/dist/helpers/setupLocalTesting.js.map +1 -1
  32. package/dist/node/LocalNodeManager.d.ts.map +1 -1
  33. package/dist/node/LocalNodeManager.js +60 -22
  34. package/dist/node/LocalNodeManager.js.map +1 -1
  35. package/dist/node/__tests__/LocalNodeManager.test.js +110 -11
  36. package/dist/node/__tests__/LocalNodeManager.test.js.map +1 -1
  37. package/dist/ui/__tests__/logger.test.d.ts +2 -0
  38. package/dist/ui/__tests__/logger.test.d.ts.map +1 -0
  39. package/dist/ui/__tests__/logger.test.js +75 -0
  40. package/dist/ui/__tests__/logger.test.js.map +1 -0
  41. package/dist/ui/formatters.d.ts +0 -16
  42. package/dist/ui/formatters.d.ts.map +1 -1
  43. package/dist/ui/formatters.js +1 -1
  44. package/dist/ui/formatters.js.map +1 -1
  45. package/dist/ui/logger.d.ts +41 -0
  46. package/dist/ui/logger.d.ts.map +1 -1
  47. package/dist/ui/logger.js +49 -0
  48. package/dist/ui/logger.js.map +1 -1
  49. package/dist/ui/spinner.d.ts +25 -0
  50. package/dist/ui/spinner.d.ts.map +1 -1
  51. package/dist/ui/spinner.js +44 -0
  52. package/dist/ui/spinner.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/cli.ts +4 -0
  55. package/src/commands/compile.ts +24 -15
  56. package/src/commands/test.ts +12 -19
  57. package/src/core/Publisher.ts +49 -34
  58. package/src/core/config.ts +9 -6
  59. package/src/core/deployments.ts +5 -4
  60. package/src/fork/manager.ts +10 -10
  61. package/src/fork/server.ts +21 -15
  62. package/src/fork/test.ts +3 -2
  63. package/src/harness/codeObject.ts +8 -5
  64. package/src/harness/script.ts +7 -4
  65. package/src/helpers/setupLocalTesting.ts +3 -3
  66. package/src/node/LocalNodeManager.ts +63 -24
  67. package/src/node/__tests__/LocalNodeManager.test.ts +140 -14
  68. package/src/ui/__tests__/logger.test.ts +89 -0
  69. package/src/ui/formatters.ts +1 -1
  70. package/src/ui/logger.ts +62 -0
  71. package/src/ui/spinner.ts +47 -0
@@ -65,6 +65,31 @@ export declare const spinner: (options: SpinnerOptions) => Ora;
65
65
  * );
66
66
  */
67
67
  export declare const withSpinner: <T>(startText: string, task: () => Promise<T>, successText?: string, errorText?: string, indent?: number) => Promise<T>;
68
+ /**
69
+ * Execute async task with a spinner that updates its label with
70
+ * elapsed seconds while the task runs. Use for long-running phases
71
+ * (local node startup, publish + tx wait) where the user wants
72
+ * visible progress feedback in lieu of subprocess chatter.
73
+ *
74
+ * Pairs with the `§9` console-UX convention: any phase that
75
+ * empirically takes ≥3s in normal use should wrap its body in
76
+ * `withTimedSpinner` so the terminal never goes silent while work
77
+ * happens.
78
+ *
79
+ * @param label - Stable label shown next to the spinner (e.g. "Starting node")
80
+ * @param task - Async function to execute
81
+ * @param indent - Number of spaces to indent (default: 0)
82
+ * @returns Promise resolving to task result
83
+ *
84
+ * @example
85
+ * await withTimedSpinner('Starting local node', async () => {
86
+ * await this.waitForReady(60_000);
87
+ * });
88
+ * // Renders: ⠋ Starting local node — 0.0s ... ⠼ Starting local node — 14.2s
89
+ * // On success: ✔ Starting local node (14.2s)
90
+ * // On error: ✖ <error.message>
91
+ */
92
+ export declare const withTimedSpinner: <T>(label: string, task: () => Promise<T>, indent?: number) => Promise<T>;
68
93
  /**
69
94
  * Spinner chain for sequential operations
70
95
  * Manages multiple spinners in sequence
@@ -1 +1 @@
1
- {"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../../src/ui/spinner.ts"],"names":[],"mappings":"AAAA,OAAY,EAAE,KAAK,GAAG,EAAE,KAAK,OAAO,IAAI,UAAU,EAAE,MAAM,KAAK,CAAC;AAGhE;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEvG;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IAChC,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,OAAO,GAAI,SAAS,cAAc,KAAG,GAcjD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,WAAW,GAAU,CAAC,EACjC,WAAW,MAAM,EACjB,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,EACtB,cAAc,MAAM,EACpB,YAAY,MAAM,EAClB,SAAQ,MAAU,KACjB,OAAO,CAAC,CAAC,CAYX,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAE1E;;OAEG;IACH,QAAQ,IAAI,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,QAAO,YA0BrC,CAAC"}
1
+ {"version":3,"file":"spinner.d.ts","sourceRoot":"","sources":["../../src/ui/spinner.ts"],"names":[],"mappings":"AAAA,OAAY,EAAE,KAAK,GAAG,EAAE,KAAK,OAAO,IAAI,UAAU,EAAE,MAAM,KAAK,CAAC;AAGhE;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAEvG;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IAChC,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,OAAO,GAAI,SAAS,cAAc,KAAG,GAcjD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,WAAW,GAAU,CAAC,EACjC,WAAW,MAAM,EACjB,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,EACtB,cAAc,MAAM,EACpB,YAAY,MAAM,EAClB,SAAQ,MAAU,KACjB,OAAO,CAAC,CAAC,CAYX,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,gBAAgB,GAAU,CAAC,EACtC,OAAO,MAAM,EACb,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,EACtB,SAAQ,MAAU,KACjB,OAAO,CAAC,CAAC,CAiBX,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAE1E;;OAEG;IACH,QAAQ,IAAI,IAAI,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,QAAO,YA0BrC,CAAC"}
@@ -72,6 +72,50 @@ export const withSpinner = async (startText, task, successText, errorText, inden
72
72
  throw error;
73
73
  }
74
74
  };
75
+ /**
76
+ * Execute async task with a spinner that updates its label with
77
+ * elapsed seconds while the task runs. Use for long-running phases
78
+ * (local node startup, publish + tx wait) where the user wants
79
+ * visible progress feedback in lieu of subprocess chatter.
80
+ *
81
+ * Pairs with the `§9` console-UX convention: any phase that
82
+ * empirically takes ≥3s in normal use should wrap its body in
83
+ * `withTimedSpinner` so the terminal never goes silent while work
84
+ * happens.
85
+ *
86
+ * @param label - Stable label shown next to the spinner (e.g. "Starting node")
87
+ * @param task - Async function to execute
88
+ * @param indent - Number of spaces to indent (default: 0)
89
+ * @returns Promise resolving to task result
90
+ *
91
+ * @example
92
+ * await withTimedSpinner('Starting local node', async () => {
93
+ * await this.waitForReady(60_000);
94
+ * });
95
+ * // Renders: ⠋ Starting local node — 0.0s ... ⠼ Starting local node — 14.2s
96
+ * // On success: ✔ Starting local node (14.2s)
97
+ * // On error: ✖ <error.message>
98
+ */
99
+ export const withTimedSpinner = async (label, task, indent = 0) => {
100
+ const start = Date.now();
101
+ const spin = spinner({ text: `${label} — 0.0s`, indent });
102
+ const timer = setInterval(() => {
103
+ spin.text = `${label} — ${((Date.now() - start) / 1000).toFixed(1)}s`;
104
+ }, 500);
105
+ try {
106
+ const result = await task();
107
+ spin.succeed(`${label} (${((Date.now() - start) / 1000).toFixed(1)}s)`);
108
+ return result;
109
+ }
110
+ catch (error) {
111
+ const errMsg = error instanceof Error ? error.message : String(error);
112
+ spin.fail(errMsg);
113
+ throw error;
114
+ }
115
+ finally {
116
+ clearInterval(timer);
117
+ }
118
+ };
75
119
  /**
76
120
  * Create a sequential spinner chain
77
121
  * Useful for multi-step processes like initialization
@@ -1 +1 @@
1
- {"version":3,"file":"spinner.js","sourceRoot":"","sources":["../../src/ui/spinner.ts"],"names":[],"mappings":"AAAA,OAAO,GAA6C,MAAM,KAAK,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAqB7C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,OAAuB,EAAO,EAAE;IACtD,MAAM,EAAE,IAAI,EAAE,KAAK,GAAG,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,MAAM,GAAG,CAAC,EAAE,GAAG,OAAO,CAAC;IAEzE,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAExC,MAAM,UAAU,GAAe;QAC7B,IAAI,EAAE,YAAY,GAAG,IAAI;QACzB,KAAK;QACL,OAAO;QACP,6DAA6D;QAC7D,SAAS,EAAE,cAAc,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;KAC7D,CAAC;IAEF,OAAO,GAAG,CAAC,UAAU,CAAC,CAAC,KAAK,EAAE,CAAC;AACjC,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAC9B,SAAiB,EACjB,IAAsB,EACtB,WAAoB,EACpB,SAAkB,EAClB,SAAiB,CAAC,EACN,EAAE;IACd,MAAM,IAAI,GAAG,OAAO,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;IAElD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO,CAAC,WAAW,IAAI,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC;QAC/D,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,WAAW,MAAM,EAAE,CAAC,CAAC;QAC5C,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAkBF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAiB,EAAE;IACnD,IAAI,cAAc,GAAe,IAAI,CAAC;IAEtC,OAAO;QACL,KAAK,CAAC,GAAG,CACP,IAAY,EACZ,IAAsB,EACtB,SAAiB,CAAC;YAElB,cAAc,GAAG,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YAC3C,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;gBAC5B,cAAc,CAAC,OAAO,EAAE,CAAC;gBACzB,OAAO,MAAM,CAAC;YAChB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,cAAc,CAAC,IAAI,EAAE,CAAC;gBACtB,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;QAED,QAAQ;YACN,IAAI,cAAc,EAAE,CAAC;gBACnB,cAAc,CAAC,IAAI,EAAE,CAAC;YACxB,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
1
+ {"version":3,"file":"spinner.js","sourceRoot":"","sources":["../../src/ui/spinner.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "movehat",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "Hardhat-like development framework for Movement L1 smart contracts",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -50,6 +50,7 @@ program
50
50
  .version(version)
51
51
  .option('--network <name>', 'Network to use (testnet, mainnet, local, etc.)')
52
52
  .option('--redeploy', 'Force redeploy even if already deployed')
53
+ .option('-v, --verbose', 'Show subprocess output (movement node, aptos move) for debugging')
53
54
  .hook('preAction', (thisCommand) => {
54
55
  // Store network option in environment for commands to access
55
56
  const options = thisCommand.opts();
@@ -59,6 +60,9 @@ program
59
60
  if (options.redeploy) {
60
61
  process.env.MH_CLI_REDEPLOY = 'true';
61
62
  }
63
+ if (options.verbose) {
64
+ process.env.MOVEHAT_VERBOSE = '1';
65
+ }
62
66
  });
63
67
 
64
68
  program
@@ -2,7 +2,8 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { loadUserConfig } from "../core/config.js";
4
4
  import { validatePathSafety } from "../core/shell.js";
5
- import { logger } from "../ui/index.js";
5
+ import { logger, isVerbose } from "../ui/index.js";
6
+ import { withSpinner } from "../ui/spinner.js";
6
7
  import { runCli } from "../utils/runCli.js";
7
8
 
8
9
  /**
@@ -195,25 +196,33 @@ async function runMovementBuild(
195
196
  args: readonly string[],
196
197
  cwd: string
197
198
  ): Promise<void> {
198
- // Use throwOnNonZeroExit:false so we can log stdout/stderr in both
199
- // success and failure paths, matching the behavior of the previous
200
- // exec-based helper.
201
- const result = await runCli(
202
- {
203
- command: "movement",
204
- args,
205
- cwd,
206
- timeoutMs: 120000, // 2 minutes for git dependency downloads
207
- },
208
- { throwOnNonZeroExit: false }
199
+ // Use throwOnNonZeroExit:false so we can route stdout/stderr through
200
+ // the §9 verbosity gate ourselves. On failure we surface everything;
201
+ // on success the chatter is hidden unless isVerbose().
202
+ const result = await withSpinner("Compiling Move package", () =>
203
+ runCli(
204
+ {
205
+ command: "movement",
206
+ args,
207
+ cwd,
208
+ timeoutMs: 120000, // 2 minutes for git dependency downloads
209
+ },
210
+ { throwOnNonZeroExit: false }
211
+ ),
209
212
  );
210
213
 
211
- if (result.stdout) console.log(result.stdout.trim());
212
- if (result.stderr) console.error(result.stderr.trim());
213
-
214
214
  if (result.exitCode !== 0) {
215
+ // Build failed — show the user everything we have so they can debug.
216
+ if (result.stdout) logger.plain(result.stdout.trim());
217
+ if (result.stderr) logger.plain(result.stderr.trim());
215
218
  throw new Error(`movement move build exited with code ${result.exitCode}`);
216
219
  }
220
+
221
+ // Success path: route output through the verbosity gate.
222
+ if (isVerbose()) {
223
+ if (result.stdout) logger.info(result.stdout.trim(), 2);
224
+ if (result.stderr) logger.info(result.stderr.trim(), 2);
225
+ }
217
226
  }
218
227
 
219
228
  /**
@@ -97,8 +97,7 @@ async function showTestMenu(): Promise<TestType | undefined> {
97
97
  */
98
98
  async function runMoveTestsOnly(filter?: string): Promise<void> {
99
99
  logger.newline();
100
- console.log(colors.bold("Move Unit Tests"));
101
- console.log(colors.muted("─".repeat(50)));
100
+ logger.phase("Move Unit Tests");
102
101
  logger.newline();
103
102
 
104
103
  try {
@@ -118,8 +117,7 @@ async function runMoveTestsOnly(filter?: string): Promise<void> {
118
117
  */
119
118
  async function runTypeScriptTestsOnly(watch: boolean = false): Promise<void> {
120
119
  logger.newline();
121
- console.log(colors.bold("TypeScript Integration Tests"));
122
- console.log(colors.muted("─".repeat(50)));
120
+ logger.phase("TypeScript Integration Tests");
123
121
 
124
122
  if (!watch) {
125
123
  logger.newline();
@@ -146,13 +144,11 @@ async function runTypeScriptTestsOnly(watch: boolean = false): Promise<void> {
146
144
  */
147
145
  async function runAllTests(filter?: string): Promise<void> {
148
146
  logger.newline();
149
- console.log(colors.bold("Running All Tests"));
150
- console.log(colors.muted("═".repeat(50)));
147
+ logger.phase("Running All Tests");
151
148
 
152
149
  // Section 1: Move Tests
153
150
  logger.newline();
154
- console.log(`${colors.brandBright("1.")} ${colors.bold("Move Unit Tests")}`);
155
- console.log(colors.muted("─".repeat(50)));
151
+ logger.phase("1. Move Unit Tests");
156
152
  logger.newline();
157
153
 
158
154
  try {
@@ -163,28 +159,25 @@ async function runAllTests(filter?: string): Promise<void> {
163
159
  } catch (error) {
164
160
  logger.newline();
165
161
  logger.error("Move tests failed");
166
- console.log(colors.muted("═".repeat(50)));
162
+ logger.divider();
167
163
  process.exit(1);
168
164
  }
169
165
 
170
166
  // Section 2: TypeScript Tests
171
167
  logger.newline();
172
- console.log(colors.muted("═".repeat(50)));
173
- logger.newline();
174
- console.log(`${colors.brandBright("2.")} ${colors.bold("TypeScript Integration Tests")}`);
175
- console.log(colors.muted("─".repeat(50)));
168
+ logger.phase("2. TypeScript Integration Tests");
176
169
  logger.newline();
177
170
 
178
171
  try {
179
172
  await runTypeScriptTests(false);
180
173
  logger.newline();
181
- console.log(colors.muted("═".repeat(50)));
174
+ logger.divider();
182
175
  logger.newline();
183
176
  logger.success("All tests passed!");
184
177
  logger.newline();
185
178
  } catch (error) {
186
179
  logger.newline();
187
- console.log(colors.muted("═".repeat(50)));
180
+ logger.divider();
188
181
  const message = error instanceof Error ? error.message : String(error);
189
182
  logger.error(message);
190
183
  process.exit(1);
@@ -198,8 +191,8 @@ async function runTypeScriptTests(watch: boolean = false): Promise<void> {
198
191
  const testDir = join(process.cwd(), "tests");
199
192
 
200
193
  if (!existsSync(testDir)) {
201
- console.log(`${colors.muted(symbols.info)} No TypeScript tests found ${colors.muted("(tests/ directory not found)")}`);
202
- console.log(` ${colors.muted("Skipping TypeScript tests...")}`);
194
+ logger.plain(`${colors.muted(symbols.info)} No TypeScript tests found ${colors.muted("(tests/ directory not found)")}`);
195
+ logger.plain(` ${colors.muted("Skipping TypeScript tests...")}`);
203
196
  logger.newline();
204
197
  return;
205
198
  }
@@ -208,7 +201,7 @@ async function runTypeScriptTests(watch: boolean = false): Promise<void> {
208
201
 
209
202
  if (!existsSync(mochaPath)) {
210
203
  logger.error("Mocha not found in project dependencies");
211
- console.log(` ${colors.muted("Install it with:")} ${colors.info("npm install --save-dev mocha")}`);
204
+ logger.plain(` ${colors.muted("Install it with:")} ${colors.info("npm install --save-dev mocha")}`);
212
205
  throw new Error("Mocha not found");
213
206
  }
214
207
 
@@ -232,7 +225,7 @@ async function runTypeScriptTests(watch: boolean = false): Promise<void> {
232
225
  process.exit(1);
233
226
  });
234
227
 
235
- console.log(`${colors.info(symbols.info)} Watch mode active. Press Ctrl+C to exit.`);
228
+ logger.plain(`${colors.info(symbols.info)} Watch mode active. Press Ctrl+C to exit.`);
236
229
  logger.newline();
237
230
  return;
238
231
  }
@@ -10,7 +10,8 @@ import {
10
10
  import { validatePathSafety } from "./shell.js";
11
11
  import { CliExecutionError, ModuleAlreadyDeployedError, PostPublishError } from "../errors.js";
12
12
  import { runCli } from "../utils/runCli.js";
13
- import { logger } from "../ui/index.js";
13
+ import { logger, isVerbose } from "../ui/index.js";
14
+ import { withSpinner } from "../ui/spinner.js";
14
15
  import type { ChildProcessAdapter } from "../utils/childProcessAdapter.js";
15
16
  import {
16
17
  writeTempKeyFile,
@@ -122,19 +123,23 @@ export class Publisher {
122
123
  : [];
123
124
 
124
125
  // Build first with named addresses
125
- logger.step("Building package...");
126
- const buildResult = await runCli(
127
- {
128
- command: "movement",
129
- args: ["move", "build", "--package-dir", safeDir, ...namedAddrArgs],
130
- timeoutMs: 120000, // 2 minutes for git dependency downloads
131
- },
132
- { adapter: this.deps.adapter }
126
+ const buildResult = await withSpinner(
127
+ "Building package",
128
+ () =>
129
+ runCli(
130
+ {
131
+ command: "movement",
132
+ args: ["move", "build", "--package-dir", safeDir, ...namedAddrArgs],
133
+ timeoutMs: 120000, // 2 minutes for git dependency downloads
134
+ },
135
+ { adapter: this.deps.adapter }
136
+ ),
133
137
  );
134
- if (buildResult.stdout) console.log(buildResult.stdout.trim());
138
+ if (isVerbose() && buildResult.stdout) {
139
+ logger.info(buildResult.stdout.trim(), 2);
140
+ }
135
141
 
136
142
  // Publish using direct parameters (avoid config file issues)
137
- logger.step("Publishing to blockchain...");
138
143
 
139
144
  // Format the private key into AIP-80 shape so the Movement CLI
140
145
  // doesn't emit its raw-hex deprecation warning. `formatPrivateKey`
@@ -179,31 +184,41 @@ export class Publisher {
179
184
  // stdout/stderr redaction still applies as defense in depth
180
185
  // for any `ed25519-priv-…` substring that surfaces in CLI
181
186
  // output (Movement CLI sometimes echoes the key on error).
182
- const publishResult = await runCli(
183
- {
184
- command: "movement",
185
- args: [
186
- "move",
187
- "publish",
188
- "--package-dir",
189
- safeDir,
190
- "--url",
191
- config.rpc,
192
- "--private-key-file",
193
- keyFilePath,
194
- "--sender-account",
195
- deployerAddress,
196
- "--assume-yes",
197
- ...namedAddrArgs,
198
- ],
199
- timeoutMs: 120000, // 2 minutes for blockchain transactions
200
- },
201
- { adapter: this.deps.adapter }
187
+ const publishResult = await withSpinner(
188
+ "Publishing to blockchain",
189
+ () =>
190
+ runCli(
191
+ {
192
+ command: "movement",
193
+ args: [
194
+ "move",
195
+ "publish",
196
+ "--package-dir",
197
+ safeDir,
198
+ "--url",
199
+ config.rpc,
200
+ "--private-key-file",
201
+ keyFilePath,
202
+ "--sender-account",
203
+ deployerAddress,
204
+ "--assume-yes",
205
+ ...namedAddrArgs,
206
+ ],
207
+ timeoutMs: 120000, // 2 minutes for blockchain transactions
208
+ },
209
+ { adapter: this.deps.adapter }
210
+ ),
202
211
  );
203
212
  publishOut = publishResult.stdout;
204
213
  publishErr = publishResult.stderr;
205
- if (publishOut) console.log(publishOut.trim());
206
- if (publishErr) console.error(publishErr.trim());
214
+ // Both stdout and stderr from the publish subprocess are gated
215
+ // behind isVerbose() — Movement CLI emits progress to both
216
+ // streams ("Compiling, may take a little while..."), so a
217
+ // visible stderr line is not by itself a failure signal. The
218
+ // surrounding withSpinner converts the runCli throw on real
219
+ // failure into the visible spinner.fail() output instead.
220
+ if (isVerbose() && publishOut) logger.info(publishOut.trim(), 2);
221
+ if (isVerbose() && publishErr) logger.info(publishErr.trim(), 2);
207
222
  } finally {
208
223
  // Unlink the temp key file via the observable cleanup helper.
209
224
  // ENOENT and other already-gone outcomes are benign (null).
@@ -279,7 +294,7 @@ export class Publisher {
279
294
  if (error instanceof CliExecutionError) {
280
295
  // stdout/stderr are already redacted by runCli before reaching here,
281
296
  // so this branch is safe to log verbatim.
282
- if (error.stdoutPreview) console.log(error.stdoutPreview);
297
+ if (error.stdoutPreview) logger.info(error.stdoutPreview, 2);
283
298
  logger.error(`Failed to publish module: ${error.message}\n${error.stderr}`);
284
299
  } else {
285
300
  // Preserve existing behaviour for non-CLI errors (filesystem write
@@ -3,6 +3,7 @@ import { join } from "path";
3
3
  import { existsSync, statSync } from "fs";
4
4
  import { Account, Ed25519PrivateKey, PrivateKey, PrivateKeyVariants } from "@aptos-labs/ts-sdk";
5
5
  import { MovehatConfig, MovehatUserConfig } from "../types/config.js";
6
+ import { logger } from "../ui/index.js";
6
7
 
7
8
  interface ConfigCacheEntry {
8
9
  mtimeMs: number;
@@ -132,7 +133,7 @@ export async function resolveNetworkConfig(
132
133
  url: "https://testnet.movementnetwork.xyz/v1",
133
134
  chainId: "testnet",
134
135
  };
135
- console.log(`testnet not found in config - using default Movement testnet configuration`);
136
+ logger.info("testnet not found in config - using default Movement testnet configuration");
136
137
  }
137
138
 
138
139
  // Special case: Auto-generate config for local fork server
@@ -141,7 +142,7 @@ export async function resolveNetworkConfig(
141
142
  url: "http://localhost:8080/v1",
142
143
  chainId: "local",
143
144
  };
144
- console.log(`Local network not found in config - using default fork server configuration`);
145
+ logger.info("Local network not found in config - using default fork server configuration");
145
146
  }
146
147
 
147
148
  if (!networkConfig) {
@@ -187,8 +188,10 @@ export async function resolveNetworkConfig(
187
188
  // 3. Deterministic = consistent test results
188
189
  const testPrivateKey = "0x0000000000000000000000000000000000000000000000000000000000000001";
189
190
  accounts = [testPrivateKey];
190
- console.log(`\n[TESTNET] Using auto-generated test account (safe for testing only)`);
191
- console.log(`[TESTNET] For mainnet, set PRIVATE_KEY in .env\n`);
191
+ logger.newline();
192
+ logger.warning("[TESTNET] Using auto-generated test account (safe for testing only)");
193
+ logger.warning("[TESTNET] For mainnet, set PRIVATE_KEY in .env");
194
+ logger.newline();
192
195
  } else {
193
196
  // For any other network (especially mainnet), REQUIRE explicit configuration
194
197
  // This prevents accidentally using the test key on production networks
@@ -273,8 +276,8 @@ function deriveAccountAddress(privateKeyHex: string | undefined): string {
273
276
  // The private key may have come from several sources (network.accounts,
274
277
  // global accounts, PRIVATE_KEY env, auto-generated testnet key). Keep
275
278
  // the hint generic so it never points at the wrong source.
276
- console.warn(
277
- `[movehat] Could not derive account address from the resolved private key: ${
279
+ logger.warning(
280
+ `Could not derive account address from the resolved private key: ${
278
281
  (err as Error).message
279
282
  }. Verify the key configured for this network is a valid Ed25519 private key (with or without the "ed25519-priv-" prefix).`
280
283
  );
@@ -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
- console.error(
90
- `Failed to save deployment for ${deployment.moduleName} on ${deployment.network} at ${filePath}:`,
91
- error
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
- console.error(`Failed to load deployment for ${moduleName} on ${network}:`, error);
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
  }
@@ -89,7 +89,7 @@ export class ForkManager {
89
89
 
90
90
  this.storage.saveMetadata(this.metadata);
91
91
 
92
- console.log(`✓ Fork initialized at ledger version ${ledgerInfo.ledger_version}`);
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
- console.log(` Fetching account ${normalizedAddress} from network...`);
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
- console.log(`Cached account ${normalizedAddress}`);
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
- console.log(` Fetching resource ${resourceType} for ${normalizedAddress}...`);
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
- console.log(`Cached resource ${resourceType}`);
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
- console.log(` Fetching all resources for ${normalizedAddress}...`);
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
- console.log(`Cached ${Object.keys(resources).length} resources`);
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
- console.log(`Updated resource ${resourceType} for ${normalizedAddress}`);
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
- console.log(`Funded ${normalizedAddress} with ${amount} coins`);
261
+ logger.success(`Funded ${normalizedAddress} with ${amount} coins`, 2);
262
262
  }
263
263
 
264
264
  listAccounts(): string[] {
@@ -335,7 +335,7 @@ export class ForkManager {
335
335
  };
336
336
 
337
337
  this.storage.saveAccount(normalizedAddress, newAccount);
338
- console.log(`Created new account ${normalizedAddress}`);
338
+ logger.success(`Created new account ${normalizedAddress}`, 2);
339
339
 
340
340
  return newAccount;
341
341
  }
@@ -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
- console.log(`\nStarting Fork Server...`);
70
- console.log(` Network: ${metadata.network}`);
71
- console.log(` Chain ID: ${metadata.chainId}`);
72
- console.log(` Ledger Version: ${metadata.ledgerVersion}`);
73
- console.log(` Forked at: ${metadata.createdAt}`);
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
- console.error(`Error handling request:`, error);
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
- console.log(`\nFork Server listening on http://${displayHost}:${this.port}`);
128
- console.log(` Bound interface: ${this.host}`);
129
- console.log(` Ledger Info: http://${displayHost}:${this.port}/v1/`);
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
- console.warn(` Server is bound to 0.0.0.0 — fork state is reachable from the LAN.`);
134
+ logger.warning("Server is bound to 0.0.0.0 — fork state is reachable from the LAN.", 2);
132
135
  }
133
- console.log(`\nPress Ctrl+C to stop`);
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
- console.log('\nFork Server stopped');
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
- console.log(`[${new Date().toISOString()}] ${req.method} ${pathname}`);
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
- console.error('Error handling request:', error);
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');