movehat 0.2.1 → 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 (176) hide show
  1. package/dist/__tests__/deployContract.test.js +56 -47
  2. package/dist/__tests__/deployContract.test.js.map +1 -1
  3. package/dist/__tests__/exports.test.d.ts +2 -0
  4. package/dist/__tests__/exports.test.d.ts.map +1 -0
  5. package/dist/__tests__/exports.test.js +30 -0
  6. package/dist/__tests__/exports.test.js.map +1 -0
  7. package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts +4 -3
  8. package/dist/__tests__/fixtures/sigint-deploy-harness.d.ts.map +1 -1
  9. package/dist/__tests__/fixtures/sigint-deploy-harness.js +8 -7
  10. package/dist/__tests__/fixtures/sigint-deploy-harness.js.map +1 -1
  11. package/dist/__tests__/fork/api.test.js +5 -0
  12. package/dist/__tests__/fork/api.test.js.map +1 -1
  13. package/dist/__tests__/fork/api.timeout.test.d.ts +2 -0
  14. package/dist/__tests__/fork/api.timeout.test.d.ts.map +1 -0
  15. package/dist/__tests__/fork/api.timeout.test.js +98 -0
  16. package/dist/__tests__/fork/api.timeout.test.js.map +1 -0
  17. package/dist/cli.js +4 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/commands/__tests__/compile.toml-mutation.test.d.ts +2 -0
  20. package/dist/commands/__tests__/compile.toml-mutation.test.d.ts.map +1 -0
  21. package/dist/commands/__tests__/compile.toml-mutation.test.js +69 -0
  22. package/dist/commands/__tests__/compile.toml-mutation.test.js.map +1 -0
  23. package/dist/commands/__tests__/init.test.js +73 -11
  24. package/dist/commands/__tests__/init.test.js.map +1 -1
  25. package/dist/commands/compile.d.ts.map +1 -1
  26. package/dist/commands/compile.js +19 -10
  27. package/dist/commands/compile.js.map +1 -1
  28. package/dist/commands/init.d.ts +22 -0
  29. package/dist/commands/init.d.ts.map +1 -1
  30. package/dist/commands/init.js +55 -6
  31. package/dist/commands/init.js.map +1 -1
  32. package/dist/commands/test.js +12 -19
  33. package/dist/commands/test.js.map +1 -1
  34. package/dist/core/AccountManager.d.ts.map +1 -1
  35. package/dist/core/AccountManager.js +14 -2
  36. package/dist/core/AccountManager.js.map +1 -1
  37. package/dist/core/Publisher.d.ts.map +1 -1
  38. package/dist/core/Publisher.js +72 -82
  39. package/dist/core/Publisher.js.map +1 -1
  40. package/dist/core/__tests__/AccountManager.global-state.test.d.ts +2 -0
  41. package/dist/core/__tests__/AccountManager.global-state.test.d.ts.map +1 -0
  42. package/dist/core/__tests__/AccountManager.global-state.test.js +69 -0
  43. package/dist/core/__tests__/AccountManager.global-state.test.js.map +1 -0
  44. package/dist/core/__tests__/movementProfile.test.d.ts +2 -0
  45. package/dist/core/__tests__/movementProfile.test.d.ts.map +1 -0
  46. package/dist/core/__tests__/movementProfile.test.js +112 -0
  47. package/dist/core/__tests__/movementProfile.test.js.map +1 -0
  48. package/dist/core/config.d.ts.map +1 -1
  49. package/dist/core/config.js +14 -10
  50. package/dist/core/config.js.map +1 -1
  51. package/dist/core/deployments.d.ts.map +1 -1
  52. package/dist/core/deployments.js +4 -2
  53. package/dist/core/deployments.js.map +1 -1
  54. package/dist/core/movementProfile.d.ts +55 -22
  55. package/dist/core/movementProfile.d.ts.map +1 -1
  56. package/dist/core/movementProfile.js +77 -99
  57. package/dist/core/movementProfile.js.map +1 -1
  58. package/dist/fork/__tests__/server.cors.test.d.ts +2 -0
  59. package/dist/fork/__tests__/server.cors.test.d.ts.map +1 -0
  60. package/dist/fork/__tests__/server.cors.test.js +79 -0
  61. package/dist/fork/__tests__/server.cors.test.js.map +1 -0
  62. package/dist/fork/api.d.ts +9 -1
  63. package/dist/fork/api.d.ts.map +1 -1
  64. package/dist/fork/api.js +37 -7
  65. package/dist/fork/api.js.map +1 -1
  66. package/dist/fork/manager.js +10 -10
  67. package/dist/fork/manager.js.map +1 -1
  68. package/dist/fork/server.d.ts +20 -1
  69. package/dist/fork/server.d.ts.map +1 -1
  70. package/dist/fork/server.js +40 -24
  71. package/dist/fork/server.js.map +1 -1
  72. package/dist/fork/test.d.ts.map +1 -1
  73. package/dist/fork/test.js +3 -2
  74. package/dist/fork/test.js.map +1 -1
  75. package/dist/harness/Harness.d.ts +6 -2
  76. package/dist/harness/Harness.d.ts.map +1 -1
  77. package/dist/harness/Harness.js +8 -2
  78. package/dist/harness/Harness.js.map +1 -1
  79. package/dist/harness/codeObject.d.ts.map +1 -1
  80. package/dist/harness/codeObject.js +41 -41
  81. package/dist/harness/codeObject.js.map +1 -1
  82. package/dist/harness/script.d.ts +3 -3
  83. package/dist/harness/script.d.ts.map +1 -1
  84. package/dist/harness/script.js +42 -35
  85. package/dist/harness/script.js.map +1 -1
  86. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts +2 -0
  87. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.d.ts.map +1 -0
  88. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js +172 -0
  89. package/dist/helpers/__tests__/setupLocalTesting.fork-network.test.js.map +1 -0
  90. package/dist/helpers/setupLocalTesting.d.ts.map +1 -1
  91. package/dist/helpers/setupLocalTesting.js +31 -5
  92. package/dist/helpers/setupLocalTesting.js.map +1 -1
  93. package/dist/index.d.ts +1 -0
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/node/LocalNodeManager.d.ts +8 -0
  96. package/dist/node/LocalNodeManager.d.ts.map +1 -1
  97. package/dist/node/LocalNodeManager.js +70 -23
  98. package/dist/node/LocalNodeManager.js.map +1 -1
  99. package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts +2 -0
  100. package/dist/node/__tests__/LocalNodeManager.api-port.test.d.ts.map +1 -0
  101. package/dist/node/__tests__/LocalNodeManager.api-port.test.js +55 -0
  102. package/dist/node/__tests__/LocalNodeManager.api-port.test.js.map +1 -0
  103. package/dist/node/__tests__/LocalNodeManager.test.js +114 -14
  104. package/dist/node/__tests__/LocalNodeManager.test.js.map +1 -1
  105. package/dist/templates/move/Move.toml +1 -1
  106. package/dist/templates/move/sources/Counter.move +31 -4
  107. package/dist/templates/scripts/deploy-counter.ts +10 -0
  108. package/dist/types/config.d.ts +8 -1
  109. package/dist/types/config.d.ts.map +1 -1
  110. package/dist/ui/__tests__/logger.test.d.ts +2 -0
  111. package/dist/ui/__tests__/logger.test.d.ts.map +1 -0
  112. package/dist/ui/__tests__/logger.test.js +75 -0
  113. package/dist/ui/__tests__/logger.test.js.map +1 -0
  114. package/dist/ui/formatters.d.ts +0 -16
  115. package/dist/ui/formatters.d.ts.map +1 -1
  116. package/dist/ui/formatters.js +1 -1
  117. package/dist/ui/formatters.js.map +1 -1
  118. package/dist/ui/logger.d.ts +41 -0
  119. package/dist/ui/logger.d.ts.map +1 -1
  120. package/dist/ui/logger.js +49 -0
  121. package/dist/ui/logger.js.map +1 -1
  122. package/dist/ui/spinner.d.ts +25 -0
  123. package/dist/ui/spinner.d.ts.map +1 -1
  124. package/dist/ui/spinner.js +44 -0
  125. package/dist/ui/spinner.js.map +1 -1
  126. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts +2 -0
  127. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.d.ts.map +1 -0
  128. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js +43 -0
  129. package/dist/utils/__tests__/childProcessAdapter.maxBuffer.test.js.map +1 -0
  130. package/dist/utils/childProcessAdapter.d.ts +7 -0
  131. package/dist/utils/childProcessAdapter.d.ts.map +1 -1
  132. package/dist/utils/childProcessAdapter.js +20 -2
  133. package/dist/utils/childProcessAdapter.js.map +1 -1
  134. package/package.json +1 -1
  135. package/src/__tests__/deployContract.test.ts +59 -50
  136. package/src/__tests__/exports.test.ts +32 -0
  137. package/src/__tests__/fixtures/sigint-deploy-harness.ts +8 -7
  138. package/src/__tests__/fork/api.test.ts +5 -0
  139. package/src/__tests__/fork/api.timeout.test.ts +150 -0
  140. package/src/cli.ts +4 -0
  141. package/src/commands/__tests__/compile.toml-mutation.test.ts +77 -0
  142. package/src/commands/__tests__/init.test.ts +96 -11
  143. package/src/commands/compile.ts +24 -15
  144. package/src/commands/init.ts +77 -6
  145. package/src/commands/test.ts +12 -19
  146. package/src/core/AccountManager.ts +18 -1
  147. package/src/core/Publisher.ts +103 -107
  148. package/src/core/__tests__/AccountManager.global-state.test.ts +83 -0
  149. package/src/core/__tests__/movementProfile.test.ts +131 -0
  150. package/src/core/config.ts +18 -11
  151. package/src/core/deployments.ts +5 -4
  152. package/src/core/movementProfile.ts +75 -127
  153. package/src/fork/__tests__/server.cors.test.ts +101 -0
  154. package/src/fork/api.ts +69 -10
  155. package/src/fork/manager.ts +10 -10
  156. package/src/fork/server.ts +59 -24
  157. package/src/fork/test.ts +3 -2
  158. package/src/harness/Harness.ts +11 -2
  159. package/src/harness/codeObject.ts +45 -48
  160. package/src/harness/script.ts +47 -43
  161. package/src/helpers/__tests__/setupLocalTesting.fork-network.test.ts +212 -0
  162. package/src/helpers/setupLocalTesting.ts +39 -5
  163. package/src/index.ts +9 -1
  164. package/src/node/LocalNodeManager.ts +87 -26
  165. package/src/node/__tests__/LocalNodeManager.api-port.test.ts +62 -0
  166. package/src/node/__tests__/LocalNodeManager.test.ts +144 -17
  167. package/src/templates/move/Move.toml +1 -1
  168. package/src/templates/move/sources/Counter.move +31 -4
  169. package/src/templates/scripts/deploy-counter.ts +10 -0
  170. package/src/types/config.ts +8 -1
  171. package/src/ui/__tests__/logger.test.ts +89 -0
  172. package/src/ui/formatters.ts +1 -1
  173. package/src/ui/logger.ts +62 -0
  174. package/src/ui/spinner.ts +47 -0
  175. package/src/utils/__tests__/childProcessAdapter.maxBuffer.test.ts +51 -0
  176. package/src/utils/childProcessAdapter.ts +32 -2
@@ -13,7 +13,8 @@ vi.mock("../../helpers/banner.js", () => ({
13
13
  printMovehatBanner: () => undefined,
14
14
  }));
15
15
 
16
- const { default: initCommand } = await import("../init.js");
16
+ const initModule = await import("../init.js");
17
+ const { default: initCommand, resolveProjectNames, InvalidProjectNameError } = initModule;
17
18
 
18
19
  /**
19
20
  * Strategy: real tmpdir + real fs ops (matches §6.1's example-as-canonical
@@ -28,10 +29,38 @@ const { default: initCommand } = await import("../init.js");
28
29
  * to `src/templates/` cleanly. No bundling step required.
29
30
  */
30
31
 
32
+ describe("resolveProjectNames", () => {
33
+ const cases: Array<[
34
+ string,
35
+ { dirName: string; npmName: string; moveName: string; sanitized: boolean }
36
+ ]> = [
37
+ ["my_project", { dirName: "my_project", npmName: "my_project", moveName: "my_project", sanitized: false }],
38
+ ["my-project", { dirName: "my-project", npmName: "my-project", moveName: "my_project", sanitized: true }],
39
+ ["/tmp/my-project", { dirName: "/tmp/my-project", npmName: "my-project", moveName: "my_project", sanitized: true }],
40
+ ["123abc", { dirName: "123abc", npmName: "123abc", moveName: "pkg_123abc", sanitized: true }],
41
+ ["_underscore", { dirName: "_underscore", npmName: "_underscore", moveName: "_underscore", sanitized: false }],
42
+ ["UPPER", { dirName: "UPPER", npmName: "UPPER", moveName: "UPPER", sanitized: false }],
43
+ [" spaced ", { dirName: "spaced", npmName: "spaced", moveName: "spaced", sanitized: false }],
44
+ ["with spaces", { dirName: "with spaces", npmName: "with spaces", moveName: "with_spaces", sanitized: true }],
45
+ ];
46
+
47
+ it.each(cases)("derives names for %j", (input, expected) => {
48
+ expect(resolveProjectNames(input)).toEqual(expected);
49
+ });
50
+
51
+ it.each(["", " ", ".", "..", "/", "////"])(
52
+ "rejects %j as an invalid project name",
53
+ (input) => {
54
+ expect(() => resolveProjectNames(input)).toThrow(InvalidProjectNameError);
55
+ }
56
+ );
57
+ });
58
+
31
59
  describe("initCommand", () => {
32
60
  let tmpParent: string;
33
61
  let origCwd: string;
34
62
  let exitSpy: ReturnType<typeof vi.spyOn>;
63
+ let warnSpy: ReturnType<typeof vi.spyOn>;
35
64
 
36
65
  beforeEach(() => {
37
66
  promptsMock.mockReset();
@@ -48,6 +77,7 @@ describe("initCommand", () => {
48
77
  }) as never);
49
78
  vi.spyOn(console, "log").mockImplementation(() => undefined);
50
79
  vi.spyOn(console, "error").mockImplementation(() => undefined);
80
+ warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined);
51
81
  });
52
82
 
53
83
  afterEach(() => {
@@ -59,9 +89,9 @@ describe("initCommand", () => {
59
89
  });
60
90
 
61
91
  it("happy path: scaffolds a project from the canonical template", async () => {
62
- await initCommand("my-test-project");
92
+ await initCommand("my_test_project");
63
93
 
64
- const target = join(tmpParent, "my-test-project");
94
+ const target = join(tmpParent, "my_test_project");
65
95
  expect(existsSync(target)).toBe(true);
66
96
  // Canonical files at the project root.
67
97
  expect(existsSync(join(target, "package.json"))).toBe(true);
@@ -78,32 +108,37 @@ describe("initCommand", () => {
78
108
  // Excluded template-development directory.
79
109
  expect(existsSync(join(target, "types"))).toBe(false);
80
110
  expect(existsSync(join(target, ".vscode"))).toBe(false);
111
+ // Move.toml gets a valid Move identifier.
112
+ const moveToml = readFileSync(join(target, "move", "Move.toml"), "utf-8");
113
+ expect(moveToml).toContain('name = "my_test_project"');
114
+ expect(moveToml).not.toContain("{{movePackageName}}");
115
+ expect(moveToml).not.toContain("{{projectName}}");
81
116
  });
82
117
 
83
118
  it("substitutes {{projectName}} placeholders inside template files", async () => {
84
- await initCommand("substituted-project");
119
+ await initCommand("substituted_project");
85
120
 
86
121
  const pkg = readFileSync(
87
- join(tmpParent, "substituted-project", "package.json"),
122
+ join(tmpParent, "substituted_project", "package.json"),
88
123
  "utf-8"
89
124
  );
90
- expect(pkg).toContain("substituted-project");
125
+ expect(pkg).toContain("substituted_project");
91
126
  expect(pkg).not.toContain("{{projectName}}");
92
127
 
93
128
  const readme = readFileSync(
94
- join(tmpParent, "substituted-project", "README.md"),
129
+ join(tmpParent, "substituted_project", "README.md"),
95
130
  "utf-8"
96
131
  );
97
- expect(readme).toContain("substituted-project");
132
+ expect(readme).toContain("substituted_project");
98
133
  });
99
134
 
100
135
  it("prompts for project name when none is provided", async () => {
101
- promptsMock.mockResolvedValueOnce({ projectName: "from-prompt" });
136
+ promptsMock.mockResolvedValueOnce({ projectName: "from_prompt" });
102
137
 
103
138
  await initCommand();
104
139
 
105
140
  expect(promptsMock).toHaveBeenCalledTimes(1);
106
- expect(existsSync(join(tmpParent, "from-prompt"))).toBe(true);
141
+ expect(existsSync(join(tmpParent, "from_prompt"))).toBe(true);
107
142
  });
108
143
 
109
144
  it("exits 0 when the user Ctrl+Cs the prompt (no project created)", async () => {
@@ -116,10 +151,60 @@ describe("initCommand", () => {
116
151
  it("exits 1 when the template copy step hits an unwriteable target", async () => {
117
152
  // Plant a directory where the command will try to write package.json
118
153
  // as a file — fs.writeFile rejects with EISDIR.
119
- const targetName = "blocked-project";
154
+ const targetName = "blocked_project";
120
155
  mkdirSync(join(tmpParent, targetName, "package.json"), { recursive: true });
121
156
 
122
157
  await expect(initCommand(targetName)).rejects.toThrow("__test_exit_1__");
123
158
  expect(exitSpy).toHaveBeenCalledWith(1);
124
159
  });
160
+
161
+ it("sanitizes Move.toml for hyphenated names but keeps package.json original", async () => {
162
+ await initCommand("my-project");
163
+
164
+ const target = join(tmpParent, "my-project");
165
+ expect(existsSync(target)).toBe(true);
166
+ // package.json keeps the original (npm allows hyphens).
167
+ const pkg = readFileSync(join(target, "package.json"), "utf-8");
168
+ expect(pkg).toContain('"my-project"');
169
+ // Move.toml gets the sanitized identifier.
170
+ const moveToml = readFileSync(join(target, "move", "Move.toml"), "utf-8");
171
+ expect(moveToml).toContain('name = "my_project"');
172
+ expect(moveToml).not.toContain('"my-project"');
173
+ // Warning was emitted.
174
+ expect(warnSpy).toHaveBeenCalled();
175
+ const warningText = warnSpy.mock.calls.flat().join(" ");
176
+ expect(warningText).toContain("my-project");
177
+ expect(warningText).toContain("my_project");
178
+ });
179
+
180
+ it("with a path argument, creates the dir at the full path and sanitizes Move.toml", async () => {
181
+ const nestedPath = join(tmpParent, "nested", "sub-project");
182
+
183
+ await initCommand(nestedPath);
184
+
185
+ expect(existsSync(nestedPath)).toBe(true);
186
+ const moveToml = readFileSync(join(nestedPath, "move", "Move.toml"), "utf-8");
187
+ expect(moveToml).toContain('name = "sub_project"');
188
+ const pkg = readFileSync(join(nestedPath, "package.json"), "utf-8");
189
+ expect(pkg).toContain('"sub-project"');
190
+ });
191
+
192
+ it("prefixes Move.toml package name with pkg_ for names that start with a digit", async () => {
193
+ await initCommand("123abc");
194
+
195
+ const moveToml = readFileSync(
196
+ join(tmpParent, "123abc", "move", "Move.toml"),
197
+ "utf-8"
198
+ );
199
+ expect(moveToml).toContain('name = "pkg_123abc"');
200
+ expect(warnSpy).toHaveBeenCalled();
201
+ });
202
+
203
+ it.each([".", "..", "/", " "])(
204
+ "rejects %j as an invalid project name and exits 1",
205
+ async (input) => {
206
+ await expect(initCommand(input)).rejects.toThrow("__test_exit_1__");
207
+ expect(exitSpy).toHaveBeenCalledWith(1);
208
+ }
209
+ );
125
210
  });
@@ -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
  /**
@@ -8,6 +8,60 @@ import { logger, createSpinnerChain, formatCommand } from "../ui/index.js";
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
10
10
 
11
+ export class InvalidProjectNameError extends Error {
12
+ constructor(message: string) {
13
+ super(message);
14
+ this.name = "InvalidProjectNameError";
15
+ }
16
+ }
17
+
18
+ export interface ProjectNames {
19
+ /** User input as-is (after trim) — used for the filesystem target. */
20
+ dirName: string;
21
+ /** Basename of the resolved path — used for package.json + README.md. */
22
+ npmName: string;
23
+ /** Valid Move identifier — used for Move.toml. */
24
+ moveName: string;
25
+ /** True when moveName had to diverge from npmName to satisfy Move identifier rules. */
26
+ sanitized: boolean;
27
+ }
28
+
29
+ /**
30
+ * Derive filesystem, npm, and Move identifier names from a single user input.
31
+ *
32
+ * Move identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. npm package names are
33
+ * looser (hyphens allowed). The filesystem accepts almost anything. Rather
34
+ * than reject inputs that would compile fine for npm but not for Move, we
35
+ * sanitize the Move identifier and leave the npm name + dir unchanged.
36
+ */
37
+ export function resolveProjectNames(input: string): ProjectNames {
38
+ const trimmed = input.trim();
39
+ if (
40
+ trimmed === "" ||
41
+ trimmed === "." ||
42
+ trimmed === ".." ||
43
+ /^\/+$/.test(trimmed)
44
+ ) {
45
+ throw new InvalidProjectNameError(
46
+ `Project name '${input}' must include at least one character besides path separators.`
47
+ );
48
+ }
49
+
50
+ const dirName = trimmed;
51
+ const npmName = path.basename(path.resolve(trimmed));
52
+ let moveName = npmName.replace(/[^a-zA-Z0-9_]/g, "_");
53
+ if (!/^[a-zA-Z_]/.test(moveName)) {
54
+ moveName = `pkg_${moveName}`;
55
+ }
56
+
57
+ return {
58
+ dirName,
59
+ npmName,
60
+ moveName,
61
+ sanitized: moveName !== npmName,
62
+ };
63
+ }
64
+
11
65
  /**
12
66
  * Initialize a new Movehat project with template files
13
67
  *
@@ -49,8 +103,25 @@ export default async function initCommand(projectName?: string) {
49
103
  projectName = response.projectName;
50
104
  }
51
105
 
52
- const targetDir = projectName!;
53
- const projectPath = path.resolve(process.cwd(), targetDir);
106
+ let names: ProjectNames;
107
+ try {
108
+ names = resolveProjectNames(projectName!);
109
+ } catch (error) {
110
+ if (error instanceof InvalidProjectNameError) {
111
+ logger.error(error.message);
112
+ process.exit(1);
113
+ }
114
+ throw error;
115
+ }
116
+
117
+ const { dirName, npmName, moveName, sanitized } = names;
118
+ const projectPath = path.resolve(process.cwd(), dirName);
119
+
120
+ if (sanitized) {
121
+ logger.warning(
122
+ `'${npmName}' is not a valid Move identifier; using '${moveName}' for move/Move.toml. (package.json keeps '${npmName}'.)`
123
+ );
124
+ }
54
125
 
55
126
  logger.newline();
56
127
  logger.info(`Initializing new Movehat project in ${projectPath}...`);
@@ -67,7 +138,7 @@ export default async function initCommand(projectName?: string) {
67
138
  await copyFile(
68
139
  path.join(templatesDir, "package.json"),
69
140
  path.join(projectPath, "package.json"),
70
- { projectName: projectName! }
141
+ { projectName: npmName }
71
142
  );
72
143
 
73
144
  await copyFile(
@@ -98,7 +169,7 @@ export default async function initCommand(projectName?: string) {
98
169
  await copyFile(
99
170
  path.join(templatesDir, "README.md"),
100
171
  path.join(projectPath, "README.md"),
101
- { projectName: projectName! }
172
+ { projectName: npmName }
102
173
  );
103
174
  });
104
175
 
@@ -107,7 +178,7 @@ export default async function initCommand(projectName?: string) {
107
178
  await copyDir(
108
179
  path.join(templatesDir, "move"),
109
180
  path.join(projectPath, "move"),
110
- { projectName: projectName! }
181
+ { projectName: npmName, movePackageName: moveName }
111
182
  );
112
183
  });
113
184
 
@@ -136,7 +207,7 @@ export default async function initCommand(projectName?: string) {
136
207
 
137
208
  // Next steps
138
209
  logger.section('Next steps');
139
- logger.item(formatCommand(`cd ${projectName}`), 2);
210
+ logger.item(formatCommand(`cd ${dirName}`), 2);
140
211
  logger.item(formatCommand('cp .env.example .env'), 2);
141
212
  logger.plain(' # Edit .env with your credentials');
142
213
  logger.item(formatCommand('npm install'), 2);
@@ -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
  }
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  Account,
3
3
  Ed25519PrivateKey,
4
+ PrivateKey,
5
+ PrivateKeyVariants,
4
6
  } from "@aptos-labs/ts-sdk";
5
7
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
6
8
  import { join } from "path";
@@ -25,10 +27,18 @@ interface AccountPool {
25
27
  * Provides a pool of reusable test accounts with labels for better test readability.
26
28
  */
27
29
  export class AccountManager {
30
+ // Class-static maps: shared across every consumer in the same Node
31
+ // process (e.g. two Harness instances). Re-using a label across
32
+ // "sessions" overwrites the labelMap entry. Documented + fixed by
33
+ // contract in `AccountManager.global-state.test.ts` (audit F8).
28
34
  private static pool: Map<string, Account> = new Map(); // address → Account
29
35
  private static privateKeys: Map<string, string> = new Map(); // address → privateKey hex
30
36
  private static labelMap: Map<string, string> = new Map(); // label → address
31
37
  private static poolLoaded = false;
38
+ // `defaultPoolPath` is captured ONCE at module-import time. A later
39
+ // `process.chdir(...)` does NOT redirect the save destination — pass
40
+ // an explicit `poolPath` to `saveAccountPool`/`loadAccountPool` when
41
+ // per-test isolation matters. See audit finding F8.
32
42
  private static defaultPoolPath = join(process.cwd(), ".movehat", "accounts");
33
43
 
34
44
  /**
@@ -131,7 +141,14 @@ export class AccountManager {
131
141
  * const account = AccountManager.loadAccountFromPrivateKey("0xabc123...");
132
142
  */
133
143
  static loadAccountFromPrivateKey(privateKeyHex: string): Account {
134
- const privateKey = new Ed25519PrivateKey(privateKeyHex);
144
+ // Format into AIP-80 shape (`ed25519-priv-0x…`) before constructing
145
+ // the SDK type. Without this, raw-hex inputs trigger a noisy
146
+ // deprecation warning from `@aptos-labs/ts-sdk` on every call.
147
+ const formatted = PrivateKey.formatPrivateKey(
148
+ privateKeyHex,
149
+ PrivateKeyVariants.Ed25519,
150
+ );
151
+ const privateKey = new Ed25519PrivateKey(formatted);
135
152
  const account = Account.fromPrivateKey({ privateKey });
136
153
  const address = account.accountAddress.toString();
137
154