movehat 0.0.9-alpha.0 → 0.0.10-alpha.0

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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +16 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/test-move.d.ts.map +1 -1
  5. package/dist/commands/test-move.js +10 -40
  6. package/dist/commands/test-move.js.map +1 -1
  7. package/dist/commands/test.d.ts.map +1 -1
  8. package/dist/commands/test.js +36 -51
  9. package/dist/commands/test.js.map +1 -1
  10. package/dist/commands/update.d.ts +5 -0
  11. package/dist/commands/update.d.ts.map +1 -0
  12. package/dist/commands/update.js +121 -0
  13. package/dist/commands/update.js.map +1 -0
  14. package/dist/helpers/move-tests.d.ts +13 -0
  15. package/dist/helpers/move-tests.d.ts.map +1 -0
  16. package/dist/helpers/move-tests.js +54 -0
  17. package/dist/helpers/move-tests.js.map +1 -0
  18. package/dist/helpers/npm-registry.d.ts +19 -0
  19. package/dist/helpers/npm-registry.d.ts.map +1 -0
  20. package/dist/helpers/npm-registry.js +47 -0
  21. package/dist/helpers/npm-registry.js.map +1 -0
  22. package/dist/helpers/semver-utils.d.ts +7 -0
  23. package/dist/helpers/semver-utils.d.ts.map +1 -0
  24. package/dist/helpers/semver-utils.js +47 -0
  25. package/dist/helpers/semver-utils.js.map +1 -0
  26. package/dist/helpers/version-check.d.ts +6 -0
  27. package/dist/helpers/version-check.d.ts.map +1 -0
  28. package/dist/helpers/version-check.js +85 -0
  29. package/dist/helpers/version-check.js.map +1 -0
  30. package/dist/templates/README.md +1 -1
  31. package/dist/templates/move/Move.toml +3 -2
  32. package/package.json +1 -1
  33. package/src/cli.ts +19 -2
  34. package/src/commands/test-move.ts +10 -45
  35. package/src/commands/test.ts +37 -56
  36. package/src/commands/update.ts +148 -0
  37. package/src/helpers/move-tests.ts +68 -0
  38. package/src/helpers/npm-registry.ts +71 -0
  39. package/src/helpers/semver-utils.ts +53 -0
  40. package/src/helpers/version-check.ts +97 -0
  41. package/src/templates/README.md +1 -1
  42. package/src/templates/move/Move.toml +3 -2
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Compare two semver versions
3
+ * Returns true if newVersion > currentVersion
4
+ * Handles variable-length versions (1.2, 1.2.3, 1.2.3.4, etc.)
5
+ */
6
+ export function isNewerVersion(currentVersion, newVersion) {
7
+ // Remove any pre-release tags (e.g., -alpha.0, -beta.1)
8
+ const cleanCurrent = currentVersion.split("-")[0];
9
+ const cleanNew = newVersion.split("-")[0];
10
+ // Split and validate numeric parts
11
+ const currentParts = cleanCurrent.split(".").map((part) => {
12
+ const num = Number(part);
13
+ if (isNaN(num) || !part.trim()) {
14
+ throw new Error(`Invalid version format: ${currentVersion}`);
15
+ }
16
+ return num;
17
+ });
18
+ const newerParts = cleanNew.split(".").map((part) => {
19
+ const num = Number(part);
20
+ if (isNaN(num) || !part.trim()) {
21
+ throw new Error(`Invalid version format: ${newVersion}`);
22
+ }
23
+ return num;
24
+ });
25
+ // Compare up to the maximum length, treating missing parts as 0
26
+ const maxLength = Math.max(currentParts.length, newerParts.length);
27
+ for (let i = 0; i < maxLength; i++) {
28
+ const currentPart = currentParts[i] || 0;
29
+ const newerPart = newerParts[i] || 0;
30
+ if (newerPart > currentPart)
31
+ return true;
32
+ if (newerPart < currentPart)
33
+ return false;
34
+ }
35
+ // If base versions are equal, check pre-release tags
36
+ // A version with no pre-release tag is considered newer than one with a tag
37
+ const currentHasPrerelease = currentVersion.includes("-");
38
+ const newHasPrerelease = newVersion.includes("-");
39
+ if (!currentHasPrerelease && newHasPrerelease) {
40
+ return false; // Current stable is newer than new pre-release
41
+ }
42
+ if (currentHasPrerelease && !newHasPrerelease) {
43
+ return true; // New stable is newer than current pre-release
44
+ }
45
+ return false;
46
+ }
47
+ //# sourceMappingURL=semver-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semver-utils.js","sourceRoot":"","sources":["../../src/helpers/semver-utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,cAAsB,EAAE,UAAkB;IACvE,wDAAwD;IACxD,MAAM,YAAY,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAE1C,mCAAmC;IACnC,MAAM,YAAY,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACxD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,2BAA2B,cAAc,EAAE,CAAC,CAAC;QAC/D,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CAAC;IAEH,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QAClD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QACzB,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,2BAA2B,UAAU,EAAE,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,CAAC,CAAC;IAEH,gEAAgE;IAChE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IAEnE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,MAAM,WAAW,GAAG,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACzC,MAAM,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAErC,IAAI,SAAS,GAAG,WAAW;YAAE,OAAO,IAAI,CAAC;QACzC,IAAI,SAAS,GAAG,WAAW;YAAE,OAAO,KAAK,CAAC;IAC5C,CAAC;IAED,qDAAqD;IACrD,4EAA4E;IAC5E,MAAM,oBAAoB,GAAG,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC1D,MAAM,gBAAgB,GAAG,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAElD,IAAI,CAAC,oBAAoB,IAAI,gBAAgB,EAAE,CAAC;QAC9C,OAAO,KAAK,CAAC,CAAC,+CAA+C;IAC/D,CAAC;IAED,IAAI,oBAAoB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC9C,OAAO,IAAI,CAAC,CAAC,+CAA+C;IAC9D,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Check if a newer version is available and notify the user
3
+ * This runs synchronously using cache, and updates cache in background
4
+ */
5
+ export declare function checkForUpdates(currentVersion: string, packageName: string): void;
6
+ //# sourceMappingURL=version-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-check.d.ts","sourceRoot":"","sources":["../../src/helpers/version-check.ts"],"names":[],"mappings":"AAmDA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAyCjF"}
@@ -0,0 +1,85 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { isNewerVersion } from "./semver-utils.js";
5
+ import { fetchLatestVersion } from "./npm-registry.js";
6
+ const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
7
+ const CACHE_DIR = join(homedir(), ".movehat");
8
+ const CACHE_FILE = join(CACHE_DIR, "version-cache.json");
9
+ /**
10
+ * Read version from cache
11
+ */
12
+ function readCache() {
13
+ try {
14
+ if (!existsSync(CACHE_FILE)) {
15
+ return null;
16
+ }
17
+ const cacheContent = readFileSync(CACHE_FILE, "utf-8");
18
+ return JSON.parse(cacheContent);
19
+ }
20
+ catch (error) {
21
+ return null;
22
+ }
23
+ }
24
+ /**
25
+ * Write version to cache
26
+ */
27
+ function writeCache(latestVersion) {
28
+ try {
29
+ if (!existsSync(CACHE_DIR)) {
30
+ mkdirSync(CACHE_DIR, { recursive: true });
31
+ }
32
+ const cache = {
33
+ lastChecked: Date.now(),
34
+ latestVersion,
35
+ };
36
+ writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
37
+ }
38
+ catch (error) {
39
+ // Silently fail - don't interrupt user's workflow
40
+ }
41
+ }
42
+ /**
43
+ * Check if a newer version is available and notify the user
44
+ * This runs synchronously using cache, and updates cache in background
45
+ */
46
+ export function checkForUpdates(currentVersion, packageName) {
47
+ try {
48
+ const cache = readCache();
49
+ let shouldNotify = false;
50
+ // Check cache synchronously for immediate notification
51
+ if (cache && isNewerVersion(currentVersion, cache.latestVersion)) {
52
+ shouldNotify = true;
53
+ }
54
+ // Display notification immediately if cache says there's an update
55
+ if (shouldNotify && cache) {
56
+ const updateMessage = "\n" +
57
+ "┌" + "─".repeat(60) + "┐\n" +
58
+ `│ Update available: ${currentVersion} → ${cache.latestVersion}`.padEnd(61) + "│\n" +
59
+ `│ Run: movehat update`.padEnd(61) + "│\n" +
60
+ "└" + "─".repeat(60) + "┘\n";
61
+ console.error(updateMessage);
62
+ }
63
+ // Update cache in background if needed (doesn't block)
64
+ if (!cache || Date.now() - cache.lastChecked > CACHE_DURATION) {
65
+ setImmediate(async () => {
66
+ try {
67
+ const latestVersion = await fetchLatestVersion(packageName, {
68
+ timeout: 2000,
69
+ throwOnError: false,
70
+ });
71
+ if (latestVersion) {
72
+ writeCache(latestVersion);
73
+ }
74
+ }
75
+ catch (error) {
76
+ // Silently fail
77
+ }
78
+ });
79
+ }
80
+ }
81
+ catch (error) {
82
+ // Silently fail - never interrupt user's workflow
83
+ }
84
+ }
85
+ //# sourceMappingURL=version-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-check.js","sourceRoot":"","sources":["../../src/helpers/version-check.ts"],"names":[],"mappings":"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"}
@@ -12,7 +12,7 @@ A Move smart contract project built with Movehat.
12
12
 
13
13
  Verify: `movement --version`
14
14
 
15
- **⚠️ Important:** Without Movement CLI, compilation will fail!
15
+ **IMPORTANT:** Without Movement CLI, compilation will fail!
16
16
 
17
17
  ## Getting Started
18
18
 
@@ -7,8 +7,9 @@ authors = []
7
7
  counter = "_"
8
8
 
9
9
  [dev-addresses]
10
- # Dev addresses are auto-detected by movehat during compilation
11
- # You can also add them manually here if needed
10
+ # Dev addresses for testing (used with movement move test --dev)
11
+ # These addresses are temporary and used for local development/testing only
12
+ counter = "0xcafe"
12
13
 
13
14
  [dependencies.AptosFramework]
14
15
  git = "https://github.com/movementlabsxyz/aptos-core.git"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "movehat",
3
- "version": "0.0.9-alpha.0",
3
+ "version": "0.0.10-alpha.0",
4
4
  "type": "module",
5
5
  "description": "Hardhat-like development framework for Movement L1 and Aptos Move smart contracts",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -8,16 +8,19 @@ import testMoveCommand from './commands/test-move.js';
8
8
  import compileCommand from './commands/compile.js';
9
9
  import initCommand from './commands/init.js';
10
10
  import runCommand from './commands/run.js';
11
+ import updateCommand from './commands/update.js';
11
12
  import forkCreateCommand from './commands/fork/create.js';
12
13
  import forkViewResourceCommand from './commands/fork/view-resource.js';
13
14
  import forkFundCommand from './commands/fork/fund.js';
14
15
  import forkListCommand from './commands/fork/list.js';
15
16
  import forkServeCommand from './commands/fork/serve.js';
17
+ import { checkForUpdates } from './helpers/version-check.js';
16
18
 
17
19
  const __filename = fileURLToPath(import.meta.url);
18
20
  const __dirname = dirname(__filename);
19
21
  const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
20
22
  const version = packageJson.version;
23
+ const packageName = packageJson.name;
21
24
 
22
25
  /**
23
26
  * Parse and validate port number
@@ -30,6 +33,15 @@ function parsePort(value: string): number {
30
33
  return port;
31
34
  }
32
35
 
36
+ // Check for updates at startup (skip if running update command or help)
37
+ const args = process.argv.slice(2);
38
+ const isUpdateCommand = args.includes('update');
39
+ const isHelpOnly = args.length === 0 || args.includes('-h') || args.includes('--help');
40
+
41
+ if (!isUpdateCommand && !isHelpOnly) {
42
+ checkForUpdates(version, packageName);
43
+ }
44
+
33
45
  const program = new Command();
34
46
 
35
47
  program
@@ -71,8 +83,8 @@ program
71
83
  .description('Run all tests (Move + TypeScript)')
72
84
  .option('--move-only', 'Run only Move unit tests')
73
85
  .option('--ts-only', 'Run only TypeScript integration tests')
74
- .option('--watch', 'Run TypeScript tests in watch mode')
75
- .option('--filter <pattern>', 'Filter Move tests by name pattern')
86
+ .option('--watch', 'Run TypeScript tests in watch mode (implies --ts-only)')
87
+ .option('--filter <pattern>', 'Filter Move tests by name pattern (Move tests only)')
76
88
  .action((options) => testCommand(options));
77
89
 
78
90
  program
@@ -88,6 +100,11 @@ program
88
100
  .option('--watch', 'Run tests in watch mode')
89
101
  .action((options) => testCommand({ tsOnly: true, watch: options.watch }));
90
102
 
103
+ program
104
+ .command('update')
105
+ .description('Check for updates and upgrade to the latest version')
106
+ .action(() => updateCommand());
107
+
91
108
  // Fork commands
92
109
  const fork = program
93
110
  .command('fork')
@@ -1,7 +1,4 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { spawn } from "child_process";
4
- import { loadUserConfig } from "../core/config.js";
1
+ import { runMoveTests } from "../helpers/move-tests.js";
5
2
 
6
3
  interface TestMoveOptions {
7
4
  filter?: string;
@@ -10,52 +7,20 @@ interface TestMoveOptions {
10
7
 
11
8
  export default async function testMoveCommand(options: TestMoveOptions = {}) {
12
9
  try {
13
- const userConfig = await loadUserConfig();
14
- const moveDir = path.resolve(process.cwd(), userConfig.moveDir || "./move");
15
-
16
- if (!fs.existsSync(moveDir)) {
17
- console.error(`Move directory not found: ${moveDir}`);
18
- console.error(` Update movehat.config.ts -> moveDir`);
19
- process.exit(1);
20
- }
21
-
22
10
  console.log("Running Move unit tests...\n");
23
11
 
24
- const args = ["move", "test", "--package-dir", moveDir];
25
-
26
- // Add filter if provided
27
- if (options.filter) {
28
- args.push("--filter", options.filter);
29
- }
30
-
31
- // Add ignore-warnings flag if provided
32
- if (options.ignoreWarnings) {
33
- args.push("--ignore-compile-warnings");
34
- }
35
-
36
- // Spawn movement CLI
37
- const child = spawn("movement", args, {
38
- stdio: "inherit",
39
- cwd: process.cwd(),
40
- });
41
-
42
- child.on("exit", (code) => {
43
- if (code === 0) {
44
- console.log("\n✓ Move tests passed");
45
- } else {
46
- console.error("\n✗ Move tests failed");
47
- }
48
- process.exit(code || 0);
12
+ await runMoveTests({
13
+ filter: options.filter,
14
+ ignoreWarnings: options.ignoreWarnings,
15
+ skipIfMissing: false, // Fail if no Move directory (standalone command mode)
49
16
  });
50
17
 
51
- child.on("error", (error) => {
52
- console.error(`Failed to run Move tests: ${error.message}`);
53
- console.error(" Make sure Movement CLI is installed");
54
- console.error(" Run: movement --version");
55
- process.exit(1);
56
- });
18
+ process.exit(0);
57
19
  } catch (err: any) {
58
- console.error("Move tests failed:", err.message ?? err);
20
+ console.error("\n✗ Move tests failed");
21
+ if (err.message) {
22
+ console.error(` ${err.message}`);
23
+ }
59
24
  process.exit(1);
60
25
  }
61
26
  }
@@ -1,8 +1,7 @@
1
1
  import { spawn } from "child_process";
2
- import { join } from "path";
2
+ import { join, resolve } from "path";
3
3
  import { existsSync } from "fs";
4
- import { loadUserConfig } from "../core/config.js";
5
- import path from "path";
4
+ import { runMoveTests } from "../helpers/move-tests.js";
6
5
 
7
6
  interface TestOptions {
8
7
  moveOnly?: boolean;
@@ -14,15 +13,20 @@ interface TestOptions {
14
13
  export default async function testCommand(options: TestOptions = {}) {
15
14
  // Handle move-only flag
16
15
  if (options.moveOnly) {
16
+ if (options.watch) {
17
+ console.error("ERROR: --watch flag is not supported with --move-only");
18
+ console.error(" Watch mode only works with TypeScript tests");
19
+ process.exit(1);
20
+ }
17
21
  return runMoveTestsSync(options.filter);
18
22
  }
19
23
 
20
- // Handle ts-only flag (current behavior)
21
- if (options.tsOnly) {
24
+ // Handle ts-only flag or watch flag (watch implies ts-only)
25
+ if (options.tsOnly || options.watch) {
22
26
  return runTypeScriptTests(options.watch);
23
27
  }
24
28
 
25
- // Default: Run both Move and TypeScript tests
29
+ // Default: Run both Move and TypeScript tests (no watch mode)
26
30
  console.log("Running all tests...\n");
27
31
  console.log("=" + "=".repeat(60) + "\n");
28
32
 
@@ -36,54 +40,26 @@ export default async function testCommand(options: TestOptions = {}) {
36
40
  process.exit(1);
37
41
  }
38
42
 
39
- // Then run TypeScript tests
40
- return runTypeScriptTests(false);
43
+ // Then run TypeScript tests (never in watch mode for "all tests")
44
+ try {
45
+ await runTypeScriptTests(false);
46
+ console.log("\n" + "=" + "=".repeat(60));
47
+ console.log("\n✓ All tests passed!\n");
48
+ } catch (error) {
49
+ console.error("\n" + "=" + "=".repeat(60));
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ console.error(`\n${message}\n`);
52
+ process.exit(1);
53
+ }
41
54
  }
42
55
 
43
- function runMoveTestsSync(filter?: string): Promise<void> {
44
- return new Promise(async (resolve, reject) => {
45
- console.log("1. Move Unit Tests");
46
- console.log("-" + "-".repeat(60) + "\n");
47
-
48
- try {
49
- const userConfig = await loadUserConfig();
50
- const moveDir = path.resolve(process.cwd(), userConfig.moveDir || "./move");
51
-
52
- if (!existsSync(moveDir)) {
53
- console.log("⊘ No Move directory found (./move not found)");
54
- console.log(" Skipping Move tests...\n");
55
- resolve();
56
- return;
57
- }
58
-
59
- const args = ["move", "test", "--package-dir", moveDir];
56
+ async function runMoveTestsSync(filter?: string): Promise<void> {
57
+ console.log("1. Move Unit Tests");
58
+ console.log("-" + "-".repeat(60) + "\n");
60
59
 
61
- if (filter) {
62
- args.push("--filter", filter);
63
- }
64
-
65
- const child = spawn("movement", args, {
66
- stdio: "inherit",
67
- cwd: process.cwd(),
68
- });
69
-
70
- child.on("exit", (code) => {
71
- if (code === 0) {
72
- console.log("\n✓ Move tests passed");
73
- resolve();
74
- } else {
75
- reject(new Error("Move tests failed"));
76
- }
77
- });
78
-
79
- child.on("error", (error) => {
80
- console.error(`Failed to run Move tests: ${error.message}`);
81
- console.error(" Make sure Movement CLI is installed");
82
- reject(error);
83
- });
84
- } catch (error) {
85
- reject(error);
86
- }
60
+ return runMoveTests({
61
+ filter,
62
+ skipIfMissing: true, // Gracefully skip if no Move directory (orchestrated test mode)
87
63
  });
88
64
  }
89
65
 
@@ -119,16 +95,21 @@ function runTypeScriptTests(watch: boolean = false): Promise<void> {
119
95
  },
120
96
  });
121
97
 
98
+ // In watch mode, Mocha never exits, so resolve immediately
99
+ if (watch) {
100
+ console.log("Watch mode active. Press Ctrl+C to exit.\n");
101
+ resolve();
102
+ return;
103
+ }
104
+
105
+ // Non-watch mode: wait for exit
122
106
  child.on("exit", (code) => {
123
107
  if (code === 0) {
124
108
  console.log("\n✓ TypeScript tests passed");
125
- console.log("\n" + "=" + "=".repeat(60));
126
- console.log("\n✓ All tests passed!\n");
127
109
  resolve();
128
110
  } else {
129
- console.error("\n✗ TypeScript tests failed");
130
- console.log("\n" + "=" + "=".repeat(60));
131
- process.exit(code || 1);
111
+ const exitCode = typeof code === "number" ? code : 1;
112
+ reject(new Error(`TypeScript tests failed with exit code ${exitCode}`));
132
113
  }
133
114
  });
134
115
 
@@ -0,0 +1,148 @@
1
+ import { spawn } from "child_process";
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { join, dirname, resolve } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { homedir } from "os";
6
+ import { isNewerVersion } from "../helpers/semver-utils.js";
7
+ import { fetchLatestVersion } from "../helpers/npm-registry.js";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ interface PackageJson {
13
+ name: string;
14
+ version: string;
15
+ }
16
+
17
+ /**
18
+ * Detect which package manager to use for update
19
+ * Searches for lockfiles upward from cwd, checks user agent, or falls back to defaults
20
+ */
21
+ function detectPackageManager(): "yarn" | "npm" | "pnpm" {
22
+ // First, try to detect from lockfiles by searching upward
23
+ let currentDir = process.cwd();
24
+ const root = resolve("/");
25
+
26
+ while (currentDir !== root) {
27
+ if (existsSync(join(currentDir, "pnpm-lock.yaml"))) {
28
+ return "pnpm";
29
+ }
30
+ if (existsSync(join(currentDir, "yarn.lock"))) {
31
+ return "yarn";
32
+ }
33
+ if (
34
+ existsSync(join(currentDir, "package-lock.json")) ||
35
+ existsSync(join(currentDir, "npm-shrinkwrap.json"))
36
+ ) {
37
+ return "npm";
38
+ }
39
+
40
+ const parentDir = dirname(currentDir);
41
+ if (parentDir === currentDir) break; // Reached root
42
+ currentDir = parentDir;
43
+ }
44
+
45
+ // No lockfile found, check user agent environment variables
46
+ const userAgent =
47
+ process.env.npm_config_user_agent || process.env.npm_execpath || "";
48
+
49
+ if (userAgent.includes("pnpm")) {
50
+ return "pnpm";
51
+ }
52
+ if (userAgent.includes("yarn")) {
53
+ return "yarn";
54
+ }
55
+ if (userAgent.includes("npm")) {
56
+ return "npm";
57
+ }
58
+
59
+ // Default fallback to npm for global installs
60
+ return "npm";
61
+ }
62
+
63
+ /**
64
+ * Update command - checks for updates and installs the latest version
65
+ */
66
+ export default async function updateCommand() {
67
+ try {
68
+ console.log("Checking for updates...\n");
69
+
70
+ // Read current version from package.json
71
+ const packageJsonPath = join(__dirname, "../../package.json");
72
+ const packageJson: PackageJson = JSON.parse(
73
+ readFileSync(packageJsonPath, "utf-8")
74
+ );
75
+
76
+ const currentVersion = packageJson.version;
77
+ const packageName = packageJson.name;
78
+
79
+ console.log(`Current version: ${currentVersion}`);
80
+
81
+ // Fetch latest version from npm
82
+ const latestVersion = await fetchLatestVersion(packageName, {
83
+ throwOnError: true,
84
+ });
85
+
86
+ if (!latestVersion) {
87
+ console.error("Failed to fetch latest version from npm registry");
88
+ process.exit(1);
89
+ }
90
+
91
+ console.log(`Latest version: ${latestVersion}\n`);
92
+
93
+ // Compare versions
94
+ if (!isNewerVersion(currentVersion, latestVersion)) {
95
+ console.log("✓ You are already using the latest version!");
96
+ return;
97
+ }
98
+
99
+ console.log(`New version available: ${currentVersion} -> ${latestVersion}`);
100
+ console.log("\nUpdating movehat...\n");
101
+
102
+ // Detect package manager
103
+ const packageManager = detectPackageManager();
104
+
105
+ // Build update command based on package manager
106
+ let updateArgs: string[];
107
+ switch (packageManager) {
108
+ case "yarn":
109
+ updateArgs = ["global", "upgrade", packageName];
110
+ break;
111
+ case "pnpm":
112
+ updateArgs = ["add", "-g", `${packageName}@latest`];
113
+ break;
114
+ case "npm":
115
+ default:
116
+ updateArgs = ["update", "-g", packageName];
117
+ break;
118
+ }
119
+
120
+ // Execute update
121
+ // Use home directory as cwd to avoid packageManager conflicts from local package.json
122
+ const child = spawn(packageManager, updateArgs, {
123
+ stdio: "inherit",
124
+ cwd: homedir() || process.cwd(),
125
+ });
126
+
127
+ child.on("exit", (code) => {
128
+ if (code === 0) {
129
+ console.log(`\n✓ Successfully updated to version ${latestVersion}!`);
130
+ process.exit(0);
131
+ } else {
132
+ console.error("\n✗ Update failed");
133
+ console.error(` Try manually: ${packageManager} ${updateArgs.join(" ")}`);
134
+ process.exit(1);
135
+ }
136
+ });
137
+
138
+ child.on("error", (error) => {
139
+ console.error(`Failed to update: ${error.message}`);
140
+ console.error(` Try manually: ${packageManager} ${updateArgs.join(" ")}`);
141
+ process.exit(1);
142
+ });
143
+ } catch (error) {
144
+ const message = error instanceof Error ? error.message : String(error);
145
+ console.error(`Error: ${message}`);
146
+ process.exit(1);
147
+ }
148
+ }
@@ -0,0 +1,68 @@
1
+ import { spawn } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { resolve } from "path";
4
+ import { loadUserConfig } from "../core/config.js";
5
+
6
+ interface RunMoveTestsOptions {
7
+ filter?: string;
8
+ ignoreWarnings?: boolean;
9
+ skipIfMissing?: boolean; // If true, skip gracefully when Move dir missing (for orchestrated tests)
10
+ }
11
+
12
+ /**
13
+ * Run Move unit tests using Movement CLI
14
+ * @param options Test options including filter, warnings, and skip behavior
15
+ * @returns Promise that resolves when tests complete successfully
16
+ */
17
+ export async function runMoveTests(options: RunMoveTestsOptions = {}): Promise<void> {
18
+ const userConfig = await loadUserConfig();
19
+ const moveDir = resolve(process.cwd(), userConfig.moveDir || "./move");
20
+
21
+ if (!existsSync(moveDir)) {
22
+ if (options.skipIfMissing) {
23
+ console.log("⊘ No Move directory found (./move not found)");
24
+ console.log(" Skipping Move tests...\n");
25
+ return;
26
+ } else {
27
+ throw new Error(
28
+ `Move directory not found: ${moveDir}\n` +
29
+ ` Update movehat.config.ts -> moveDir`
30
+ );
31
+ }
32
+ }
33
+
34
+ const args = ["move", "test", "--package-dir", moveDir];
35
+
36
+ // Add dev flag for auto-detected addresses
37
+ args.push("--dev");
38
+
39
+ if (options.filter) {
40
+ args.push("--filter", options.filter);
41
+ }
42
+
43
+ if (options.ignoreWarnings) {
44
+ args.push("--ignore-compile-warnings");
45
+ }
46
+
47
+ return new Promise<void>((resolve, reject) => {
48
+ const child = spawn("movement", args, {
49
+ stdio: "inherit",
50
+ cwd: process.cwd(),
51
+ });
52
+
53
+ child.on("exit", (code) => {
54
+ if (code === 0) {
55
+ console.log("\n✓ Move tests passed");
56
+ resolve();
57
+ } else {
58
+ reject(new Error("Move tests failed"));
59
+ }
60
+ });
61
+
62
+ child.on("error", (error) => {
63
+ console.error(`Failed to run Move tests: ${error.message}`);
64
+ console.error(" Make sure Movement CLI is installed");
65
+ reject(error);
66
+ });
67
+ });
68
+ }