hardhat 3.5.1 → 3.7.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 (61) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/src/internal/builtin-plugins/flatten/task-action.d.ts.map +1 -1
  3. package/dist/src/internal/builtin-plugins/flatten/task-action.js +23 -7
  4. package/dist/src/internal/builtin-plugins/flatten/task-action.js.map +1 -1
  5. package/dist/src/internal/builtin-plugins/solidity/build-system/build-system.d.ts +1 -1
  6. package/dist/src/internal/builtin-plugins/solidity/build-system/build-system.d.ts.map +1 -1
  7. package/dist/src/internal/builtin-plugins/solidity/build-system/build-system.js +78 -1
  8. package/dist/src/internal/builtin-plugins/solidity/build-system/build-system.js.map +1 -1
  9. package/dist/src/internal/builtin-plugins/solidity/tasks/build.d.ts.map +1 -1
  10. package/dist/src/internal/builtin-plugins/solidity/tasks/build.js +1 -5
  11. package/dist/src/internal/builtin-plugins/solidity/tasks/build.js.map +1 -1
  12. package/dist/src/internal/builtin-plugins/solidity/type-extensions.d.ts +53 -1
  13. package/dist/src/internal/builtin-plugins/solidity/type-extensions.d.ts.map +1 -1
  14. package/dist/src/internal/cli/help/utils.d.ts.map +1 -1
  15. package/dist/src/internal/cli/help/utils.js +3 -2
  16. package/dist/src/internal/cli/help/utils.js.map +1 -1
  17. package/dist/src/internal/cli/init/init.d.ts.map +1 -1
  18. package/dist/src/internal/cli/init/init.js +110 -1
  19. package/dist/src/internal/cli/init/init.js.map +1 -1
  20. package/dist/src/internal/cli/init/prompt.js +1 -1
  21. package/dist/src/internal/cli/init/prompt.js.map +1 -1
  22. package/dist/src/internal/cli/main.d.ts.map +1 -1
  23. package/dist/src/internal/cli/main.js +3 -1
  24. package/dist/src/internal/cli/main.js.map +1 -1
  25. package/dist/src/internal/core/tasks/resolved-task.d.ts.map +1 -1
  26. package/dist/src/internal/core/tasks/resolved-task.js +3 -2
  27. package/dist/src/internal/core/tasks/resolved-task.js.map +1 -1
  28. package/dist/src/internal/core/tasks/utils.d.ts +3 -0
  29. package/dist/src/internal/core/tasks/utils.d.ts.map +1 -1
  30. package/dist/src/internal/core/tasks/utils.js +6 -0
  31. package/dist/src/internal/core/tasks/utils.js.map +1 -1
  32. package/dist/src/internal/core/tasks/validations.js +3 -3
  33. package/dist/src/internal/core/tasks/validations.js.map +1 -1
  34. package/dist/src/types/solidity/build-system.d.ts +22 -2
  35. package/dist/src/types/solidity/build-system.d.ts.map +1 -1
  36. package/dist/src/types/solidity/build-system.js.map +1 -1
  37. package/package.json +4 -3
  38. package/skills/hardhat/SKILL.md +152 -0
  39. package/skills/hardhat-toolbox-mocha-ethers/SKILL.md +119 -0
  40. package/skills/hardhat-toolbox-viem/SKILL.md +126 -0
  41. package/src/internal/builtin-plugins/flatten/task-action.ts +37 -7
  42. package/src/internal/builtin-plugins/solidity/build-system/build-system.ts +124 -6
  43. package/src/internal/builtin-plugins/solidity/hook-handlers/hre.ts +1 -1
  44. package/src/internal/builtin-plugins/solidity/tasks/build.ts +1 -6
  45. package/src/internal/builtin-plugins/solidity/type-extensions.ts +69 -0
  46. package/src/internal/cli/help/utils.ts +9 -2
  47. package/src/internal/cli/init/init.ts +136 -0
  48. package/src/internal/cli/init/prompt.ts +1 -1
  49. package/src/internal/cli/main.ts +4 -2
  50. package/src/internal/core/tasks/resolved-task.ts +5 -2
  51. package/src/internal/core/tasks/utils.ts +13 -0
  52. package/src/internal/core/tasks/validations.ts +3 -3
  53. package/src/types/solidity/build-system.ts +24 -5
  54. package/templates/hardhat-3/01-node-test-runner-viem/AGENTS.md +20 -0
  55. package/templates/hardhat-3/01-node-test-runner-viem/README.md +3 -3
  56. package/templates/hardhat-3/01-node-test-runner-viem/package.json +3 -3
  57. package/templates/hardhat-3/02-mocha-ethers/AGENTS.md +20 -0
  58. package/templates/hardhat-3/02-mocha-ethers/README.md +3 -3
  59. package/templates/hardhat-3/02-mocha-ethers/package.json +3 -3
  60. package/templates/hardhat-3/03-minimal/README.md +2 -2
  61. package/templates/hardhat-3/03-minimal/package.json +1 -1
@@ -29,6 +29,7 @@ import type {
29
29
  CompilerOutputError,
30
30
  DependencyGraph,
31
31
  SolidityBuildInfo,
32
+ ResolvedBuildOptions,
32
33
  } from "../../../../types/solidity.js";
33
34
 
34
35
  import os from "node:os";
@@ -277,13 +278,14 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem {
277
278
  rootFilePaths: string[],
278
279
  options?: BuildOptions,
279
280
  ): Promise<CompilationJobCreationError | Map<string, FileBuildResult>> {
280
- const resolvedOptions: Required<BuildOptions> = {
281
+ const resolvedOptions: ResolvedBuildOptions = {
281
282
  buildProfile: DEFAULT_BUILD_PROFILE,
282
283
  concurrency: Math.max(os.cpus().length - 1, 1),
283
284
  force: false,
284
285
  isolated: false,
285
286
  quiet: false,
286
287
  scope: "contracts",
288
+ cleanupArtifacts: false,
287
289
  ...options,
288
290
  };
289
291
 
@@ -368,6 +370,8 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem {
368
370
  ReadonlyMap<string, string[]>
369
371
  > = new Map();
370
372
 
373
+ let contractArtifactsAfterBuild: string[] | undefined;
374
+
371
375
  if (isSuccessfulBuild) {
372
376
  log("Emitting artifacts of successful build");
373
377
  await Promise.all(
@@ -397,6 +401,43 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem {
397
401
  );
398
402
 
399
403
  await saveCache(this.#options.cachePath, this.#compileCache);
404
+
405
+ if (resolvedOptions.cleanupArtifacts) {
406
+ const paths = await this.cleanupArtifacts(rootFilePaths, {
407
+ scope: resolvedOptions.scope,
408
+ });
409
+
410
+ if (resolvedOptions.scope !== "tests") {
411
+ contractArtifactsAfterBuild = paths;
412
+ }
413
+ }
414
+
415
+ if (resolvedOptions.scope !== "tests") {
416
+ if (
417
+ await this.#hooks.hasHandlers(
418
+ "solidity",
419
+ "processArtifactsAfterSuccessfulBuild",
420
+ )
421
+ ) {
422
+ contractArtifactsAfterBuild ??=
423
+ await this.#getCurrentContractArtifactPaths();
424
+
425
+ if (!this.#options.solidityConfig.splitTestsCompilation) {
426
+ // In unified mode the contracts artifacts directory also contains
427
+ // test artifacts, so we must filter them out before invoking the
428
+ // processArtifactsAfterSuccessfulBuild hook.
429
+ contractArtifactsAfterBuild = await this.#filterOutTestArtifacts(
430
+ contractArtifactsAfterBuild,
431
+ );
432
+ }
433
+
434
+ await this.#hooks.runSequentialHandlers(
435
+ "solidity",
436
+ "processArtifactsAfterSuccessfulBuild",
437
+ [contractArtifactsAfterBuild, rootFilePaths, resolvedOptions],
438
+ );
439
+ }
440
+ }
400
441
  }
401
442
 
402
443
  spinner.stop();
@@ -415,10 +456,16 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem {
415
456
  "We emitted contract artifacts for all the jobs if the build was successful",
416
457
  );
417
458
 
418
- const errors = await Promise.all(
419
- (result.compilerOutput.errors ?? []).map((error) =>
420
- this.remapCompilerError(result.compilationJob, error, true),
421
- ),
459
+ const errors = await this.#hooks.runHandlerChain(
460
+ "solidity",
461
+ "getCompilationJobErrors",
462
+ [result.compilationJob, result.compilerOutput],
463
+ async (_context, nextCompilationJob, nextCompilerOutput) =>
464
+ await Promise.all(
465
+ (nextCompilerOutput.errors ?? []).map((error) =>
466
+ this.remapCompilerError(nextCompilationJob, error, true),
467
+ ),
468
+ ),
422
469
  );
423
470
 
424
471
  this.#printSolcErrorsAndWarnings(errors);
@@ -1064,7 +1111,7 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem {
1064
1111
  public async cleanupArtifacts(
1065
1112
  rootFilePaths: string[],
1066
1113
  options: { scope?: BuildScope } = {},
1067
- ): Promise<void> {
1114
+ ): Promise<string[]> {
1068
1115
  const scope = options.scope ?? "contracts";
1069
1116
 
1070
1117
  this.#ensureSplitCompilationModeIfTestsScope(scope);
@@ -1191,6 +1238,77 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem {
1191
1238
  async () => {},
1192
1239
  );
1193
1240
  }
1241
+
1242
+ return artifactPaths;
1243
+ }
1244
+
1245
+ /**
1246
+ * Returns every `.json` artifact under the contracts artifacts directory,
1247
+ * excluding the project-wide top-level files (e.g. `artifacts.d.ts`) and
1248
+ * the `build-info/` subtree.
1249
+ *
1250
+ * In unified mode, the returned list also contains test artifacts. Callers
1251
+ * that need a contracts-only view should pass the result through
1252
+ * `#filterOutTestArtifacts`.
1253
+ */
1254
+ async #getCurrentContractArtifactPaths(): Promise<string[]> {
1255
+ const artifactsDirectory = await this.getArtifactsDirectory("contracts");
1256
+ const buildInfosDir = path.join(artifactsDirectory, `build-info`);
1257
+
1258
+ const allArtifactFiles = await getAllFilesMatching(
1259
+ artifactsDirectory,
1260
+ (p) => {
1261
+ // Ignore top level files (e.g. the project-wide artifacts.d.ts)
1262
+ if (
1263
+ p.indexOf(path.sep, artifactsDirectory.length + path.sep.length) ===
1264
+ -1
1265
+ ) {
1266
+ return false;
1267
+ }
1268
+ return p.endsWith(".json");
1269
+ },
1270
+ (dir) => dir !== buildInfosDir,
1271
+ );
1272
+
1273
+ return allArtifactFiles;
1274
+ }
1275
+
1276
+ /**
1277
+ * Returns `artifactPaths` with the test artifacts removed.
1278
+ *
1279
+ * This is only meaningful in unified mode, where contracts and tests share
1280
+ * the same artifacts directory. The caller is responsible for ensuring that
1281
+ * precondition.
1282
+ *
1283
+ * Each artifact's user source name is recovered from its path and classified
1284
+ * via `getScope`. Results are cached per source name to avoid repeating the
1285
+ * FS stat work for sibling artifacts. npm-style source names don't resolve
1286
+ * to a real path inside the project, so `getScope` falls back to
1287
+ * `"contracts"`, which is the intended outcome.
1288
+ */
1289
+ async #filterOutTestArtifacts(artifactPaths: string[]): Promise<string[]> {
1290
+ const artifactsDirectory = await this.getArtifactsDirectory("contracts");
1291
+ const scopeBySource = new Map<string, BuildScope>();
1292
+ const contractArtifactPaths: string[] = [];
1293
+
1294
+ for (const artifactPath of artifactPaths) {
1295
+ const userSourceName = toForwardSlash(
1296
+ path.relative(artifactsDirectory, path.dirname(artifactPath)),
1297
+ );
1298
+
1299
+ let scope = scopeBySource.get(userSourceName);
1300
+ if (scope === undefined) {
1301
+ const fsPath = path.resolve(this.#options.projectRoot, userSourceName);
1302
+ scope = await this.getScope(fsPath);
1303
+ scopeBySource.set(userSourceName, scope);
1304
+ }
1305
+
1306
+ if (scope === "contracts") {
1307
+ contractArtifactPaths.push(artifactPath);
1308
+ }
1309
+ }
1310
+
1311
+ return contractArtifactPaths;
1194
1312
  }
1195
1313
 
1196
1314
  public async compileBuildInfo(
@@ -114,7 +114,7 @@ class LazySolidityBuildSystem implements SolidityBuildSystem {
114
114
  public async cleanupArtifacts(
115
115
  rootFilePaths: string[],
116
116
  options: { scope?: BuildScope } = {},
117
- ): Promise<void> {
117
+ ): Promise<string[]> {
118
118
  const buildSystem = await this.#getBuildSystem();
119
119
  return await buildSystem.cleanupArtifacts(rootFilePaths, options);
120
120
  }
@@ -186,6 +186,7 @@ async function runSolidityBuild({
186
186
  buildProfile,
187
187
  quiet,
188
188
  scope,
189
+ cleanupArtifacts: isFullBuild,
189
190
  },
190
191
  );
191
192
 
@@ -194,12 +195,6 @@ async function runSolidityBuild({
194
195
  // We use the result keys in case a hook added or removed root files
195
196
  const builtRootPaths = [...results.keys()];
196
197
 
197
- if (isFullBuild) {
198
- await solidity.cleanupArtifacts(builtRootPaths, {
199
- scope,
200
- });
201
- }
202
-
203
198
  const preBuildRoots = new Set([...contractRootPaths, ...testRootPaths]);
204
199
  if (
205
200
  builtRootPaths.length === preBuildRoots.size &&
@@ -5,10 +5,13 @@ import type {
5
5
  FileBuildResult,
6
6
  SolidityBuildSystem,
7
7
  } from "../../../types/solidity/build-system.js";
8
+ import type { CompilationJob } from "../../../types/solidity/compilation-job.js";
9
+ import type { CompilerOutputError } from "../../../types/solidity/compiler-io.js";
8
10
  import type {
9
11
  Compiler,
10
12
  CompilerInput,
11
13
  CompilerOutput,
14
+ ResolvedBuildOptions,
12
15
  } from "../../../types/solidity.js";
13
16
 
14
17
  declare module "../../../types/config.js" {
@@ -359,6 +362,7 @@ declare module "../../../types/hooks.js" {
359
362
  * @param compilerConfig The compiler configuration to get a compiler for.
360
363
  * @param next A function to call the next handler for this hook.
361
364
  * @returns A Compiler instance.
365
+ * @deprecated This hook will soon be removed.
362
366
  */
363
367
  getCompiler: (
364
368
  context: HookContext,
@@ -377,6 +381,8 @@ declare module "../../../types/hooks.js" {
377
381
  * @param artifactPaths The file paths of artifacts that remain after cleanup.
378
382
  * @param next A function to call the next handler for this hook, or the
379
383
  * default implementation if no more handlers exist.
384
+ * @deprecated This hook will soon be removed. Use
385
+ * `processArtifactsAfterSuccessfulBuild` instead.
380
386
  */
381
387
  onCleanUpArtifacts: (
382
388
  context: HookContext,
@@ -402,6 +408,7 @@ declare module "../../../types/hooks.js" {
402
408
  * default implementation if no more handlers exist.
403
409
  *
404
410
  * @returns The modified file content.
411
+ * @deprecated This hook will soon be removed.
405
412
  */
406
413
  preprocessProjectFileBeforeBuilding(
407
414
  context: HookContext,
@@ -427,6 +434,7 @@ declare module "../../../types/hooks.js" {
427
434
  * default implementation if no more handlers exist.
428
435
  *
429
436
  * @returns The modified solc input.
437
+ * @deprecated This hook will soon be removed.
430
438
  */
431
439
  preprocessSolcInputBeforeBuilding(
432
440
  context: HookContext,
@@ -437,6 +445,9 @@ declare module "../../../types/hooks.js" {
437
445
  ) => Promise<CompilerInput>,
438
446
  ): Promise<CompilerInput>;
439
447
 
448
+ /**
449
+ * @deprecated This hook will soon be removed.
450
+ */
440
451
  readSourceFile: (
441
452
  context: HookContext,
442
453
  absolutePath: string,
@@ -482,6 +493,7 @@ declare module "../../../types/hooks.js" {
482
493
  * @param solcConfig The compiler configuration (version, type, etc.).
483
494
  * @param next A function to call the next handler for this hook, or the
484
495
  * default implementation if no more handlers exist.
496
+ * @deprecated This hook will soon be removed. Use `getCompilationJobErrors` instead.
485
497
  */
486
498
  invokeSolc(
487
499
  context: HookContext,
@@ -510,6 +522,7 @@ declare module "../../../types/hooks.js" {
510
522
  * @param next A function to get remappings from other sources (including default behavior).
511
523
  * @returns An array of remapping sources, each containing an array of remapping strings
512
524
  * and the source path they came from.
525
+ * @deprecated This hook will soon be removed.
513
526
  */
514
527
  readNpmPackageRemappings: (
515
528
  context: HookContext,
@@ -523,5 +536,61 @@ declare module "../../../types/hooks.js" {
523
536
  nextPackagePath: string,
524
537
  ) => Promise<Array<{ remappings: string[]; source: string }>>,
525
538
  ) => Promise<Array<{ remappings: string[]; source: string }>>;
539
+
540
+ /**
541
+ * Sequential hook run when a solidity build finished successfully,
542
+ * and the artifacts are ready to be processed by a plugin.
543
+ *
544
+ * This hook is only run for the "contracts" scope, and never includes
545
+ * test artifacts in its parameters.
546
+ *
547
+ * @param context The hook context.
548
+ * @param artifactPaths All the contract artifact paths, including
549
+ * pre-existing ones.
550
+ * @param buildRootFilePaths The root file paths provided to the build,
551
+ * as absolute paths or `npm:<package-name>/<file-path>` identifiers. In
552
+ * unified mode this may include test files.
553
+ * @param buildOptions The resolved options used during the build, with
554
+ * the build system's defaults filled in for any field the caller didn't
555
+ * provide.
556
+ */
557
+ processArtifactsAfterSuccessfulBuild(
558
+ context: HookContext,
559
+ artifactPaths: readonly string[],
560
+ buildRootFilePaths: readonly string[],
561
+ buildOptions: Readonly<ResolvedBuildOptions>,
562
+ ): Promise<void>;
563
+
564
+ /**
565
+ * A hook run to get and potentially process the errors of the compiler
566
+ * output after it was run by the build system.
567
+ *
568
+ * This hook allows plugin authors to process the compiler output errors
569
+ * list before it's used by the build system to report them to the users,
570
+ * but it doesn't let plugins alter the logic that determines if a
571
+ * compilation job succeeded or failed.
572
+ *
573
+ * This hook must not mutate the parameters passed to `next`. Doing so can
574
+ * have unexpected behavior, and will eventually crash Hardhat.
575
+ *
576
+ * The recommended way to use this hook is to call `next` first, and then
577
+ * work with the returned list.
578
+ *
579
+ * @param context The hook context.
580
+ * @param compilationJob The compilation job run by the build system.
581
+ * @param compilerOutput The output returned by the compiler.
582
+ * @param next A function to call the next handler for this hook.
583
+ * @returns The processed compiler output error list.
584
+ */
585
+ getCompilationJobErrors(
586
+ context: HookContext,
587
+ compilationJob: Readonly<CompilationJob>,
588
+ compilerOutput: Readonly<CompilerOutput>,
589
+ next: (
590
+ nextContext: HookContext,
591
+ nextCompilationJob: Readonly<CompilationJob>,
592
+ nextCompilerOutput: Readonly<CompilerOutput>,
593
+ ) => Promise<CompilerOutputError[]>,
594
+ ): Promise<CompilerOutputError[]>;
526
595
  }
527
596
  }
@@ -7,6 +7,8 @@ import type { Task } from "../../../types/tasks.js";
7
7
 
8
8
  import { camelToKebabCase } from "@nomicfoundation/hardhat-utils/string";
9
9
 
10
+ import { isArgumentRequired } from "../../core/tasks/utils.js";
11
+
10
12
  export const GLOBAL_NAME_PADDING = 6;
11
13
 
12
14
  interface ArgumentDescriptor {
@@ -84,11 +86,16 @@ export function parseOptions(task: Task): {
84
86
  });
85
87
  }
86
88
 
87
- for (const { name, description, defaultValue } of task.positionalArguments) {
89
+ for (const {
90
+ name,
91
+ description,
92
+ defaultValue,
93
+ type,
94
+ } of task.positionalArguments) {
88
95
  positionalArguments.push({
89
96
  name,
90
97
  description: trimFullStop(description),
91
- isRequired: defaultValue === undefined,
98
+ isRequired: isArgumentRequired(type, defaultValue),
92
99
  ...(defaultValue !== undefined && {
93
100
  defaultValue: Array.isArray(defaultValue)
94
101
  ? defaultValue.join(", ")
@@ -14,11 +14,16 @@ import {
14
14
  copy,
15
15
  ensureDir,
16
16
  exists,
17
+ getAllFilesMatching,
17
18
  isDirectory,
18
19
  mkdir,
19
20
  readJsonFile,
21
+ remove,
22
+ symlink,
20
23
  writeJsonFile,
24
+ writeUtf8File,
21
25
  } from "@nomicfoundation/hardhat-utils/fs";
26
+ import { findClosestPackageRoot } from "@nomicfoundation/hardhat-utils/package";
22
27
  import { resolveFromRoot } from "@nomicfoundation/hardhat-utils/path";
23
28
  import * as semver from "semver";
24
29
 
@@ -591,6 +596,10 @@ export async function copyProjectFiles(
591
596
  await copy(absoluteTemplatePath, absoluteWorkspacePath);
592
597
  }
593
598
 
599
+ await installSkills(workspace, template, force);
600
+ await createClaudeMd(workspace, force);
601
+ await createDotClaude(workspace);
602
+
594
603
  console.log(`✨ ${styleText("cyan", `Template files copied`)} ✨`);
595
604
  }
596
605
 
@@ -660,6 +669,133 @@ export async function copyProjectFilesNonInteractive(
660
669
  absoluteWorkspacePath,
661
670
  );
662
671
  }
672
+
673
+ await installSkills(workspace, template);
674
+ await createClaudeMd(workspace);
675
+ await createDotClaude(workspace);
676
+ }
677
+
678
+ /**
679
+ * Skills published from `packages/hardhat/skills/` and the npm package whose presence
680
+ * in a template's dependencies opts that template into installing the skill.
681
+ * Each skill is tied to exactly one package so they can be versioned and
682
+ * upgraded independently.
683
+ */
684
+ const SKILL_PACKAGES: ReadonlyArray<{
685
+ packageName: string;
686
+ skillName: string;
687
+ }> = [
688
+ { packageName: "hardhat", skillName: "hardhat" },
689
+ {
690
+ packageName: "@nomicfoundation/hardhat-toolbox-viem",
691
+ skillName: "hardhat-toolbox-viem",
692
+ },
693
+ {
694
+ packageName: "@nomicfoundation/hardhat-toolbox-mocha-ethers",
695
+ skillName: "hardhat-toolbox-mocha-ethers",
696
+ },
697
+ ];
698
+
699
+ /**
700
+ * For agent-aware templates (those that ship an `AGENTS.md`), copies any
701
+ * skills from `packages/hardhat/skills/` whose corresponding package is a template
702
+ * dependency into `<workspace>/.agents/skills/<skill-name>/`.
703
+ */
704
+ async function installSkills(
705
+ workspace: string,
706
+ template: Template,
707
+ force?: boolean,
708
+ ): Promise<void> {
709
+ if (!template.files.includes("AGENTS.md")) {
710
+ return;
711
+ }
712
+
713
+ const deps = {
714
+ ...template.packageJson.dependencies,
715
+ ...template.packageJson.devDependencies,
716
+ };
717
+ const relevantSkills = SKILL_PACKAGES.filter(
718
+ ({ packageName }) => deps[packageName] !== undefined,
719
+ );
720
+ if (relevantSkills.length === 0) {
721
+ return;
722
+ }
723
+
724
+ const hardhatPackageRoot = await findClosestPackageRoot(import.meta.url);
725
+ const skillsRoot = path.join(hardhatPackageRoot, "skills");
726
+
727
+ for (const { skillName } of relevantSkills) {
728
+ const skillSrcDir = path.join(skillsRoot, skillName);
729
+ const skillDestDir = path.join(workspace, ".agents", "skills", skillName);
730
+ const skillFiles = await getAllFilesMatching(skillSrcDir);
731
+ for (const file of skillFiles) {
732
+ const dest = path.join(skillDestDir, path.relative(skillSrcDir, file));
733
+ if (force !== true && (await exists(dest))) {
734
+ continue;
735
+ }
736
+ await ensureDir(path.dirname(dest));
737
+ await copy(file, dest);
738
+ }
739
+ }
740
+ }
741
+
742
+ /**
743
+ * Creates a `CLAUDE.md` file if an `AGENTS.md` file exists. Uses a symlink
744
+ * except on Windows, where a file is created that references `AGENTS.md`.
745
+ * Overwrites an existing `CLAUDE.md` only if `force` is true.
746
+ */
747
+ async function createClaudeMd(
748
+ workspace: string,
749
+ force?: boolean,
750
+ ): Promise<void> {
751
+ const agentsMdPath = path.join(workspace, "AGENTS.md");
752
+ if (!(await exists(agentsMdPath))) {
753
+ return;
754
+ }
755
+
756
+ const claudeMdPath = path.join(workspace, "CLAUDE.md");
757
+ if (await exists(claudeMdPath, { followSymlinks: false })) {
758
+ if (force !== true) {
759
+ return;
760
+ }
761
+ await remove(claudeMdPath);
762
+ }
763
+
764
+ if (process.platform === "win32") {
765
+ await writeUtf8File(claudeMdPath, "@AGENTS.md\n");
766
+ } else {
767
+ await symlink("AGENTS.md", claudeMdPath);
768
+ }
769
+ }
770
+
771
+ /**
772
+ * Creates `.claude` if `.agents` exists. Uses a symlink except on Windows,
773
+ * where the whole directory is copied. Does nothing if `.claude` already
774
+ * exists; the `force` flag is intentionally not used here because
775
+ * overwriting depends on whether the existing `.claude` is a file, directory,
776
+ * or symlink, and on the OS, which is more complexity than is worthwhile.
777
+ */
778
+ async function createDotClaude(workspace: string): Promise<void> {
779
+ const agentsDirPath = path.join(workspace, ".agents");
780
+ if (!(await exists(agentsDirPath))) {
781
+ return;
782
+ }
783
+
784
+ const claudeDirPath = path.join(workspace, ".claude");
785
+ if (await exists(claudeDirPath, { followSymlinks: false })) {
786
+ return;
787
+ }
788
+
789
+ if (process.platform === "win32") {
790
+ const agentsFiles = await getAllFilesMatching(agentsDirPath);
791
+ for (const file of agentsFiles) {
792
+ const dest = path.join(claudeDirPath, path.relative(agentsDirPath, file));
793
+ await ensureDir(path.dirname(dest));
794
+ await copy(file, dest);
795
+ }
796
+ } else {
797
+ await symlink(".agents", claudeDirPath);
798
+ }
663
799
  }
664
800
 
665
801
  /**
@@ -22,7 +22,7 @@ export async function promptForHardhatVersion(): Promise<
22
22
  choices: [
23
23
  {
24
24
  name: "hardhat-3",
25
- message: "Hardhat 3 Beta (recommended for new projects)",
25
+ message: "Hardhat 3 (recommended for new projects)",
26
26
  value: "hardhat-3",
27
27
  },
28
28
  {
@@ -40,6 +40,7 @@ import { parseArgumentValue } from "../core/arguments.js";
40
40
  import { buildGlobalOptionDefinitions } from "../core/global-options.js";
41
41
  import { resolveProjectRoot } from "../core/hre.js";
42
42
  import { resolvePluginList } from "../core/plugins/resolve-plugin-list.js";
43
+ import { isArgumentRequired } from "../core/tasks/utils.js";
43
44
  import { setGlobalHardhatRuntimeEnvironment } from "../global-hre-instance.js";
44
45
  import { createHardhatRuntimeEnvironment } from "../hre-initialization.js";
45
46
 
@@ -767,8 +768,9 @@ function validateRequiredArguments(
767
768
  taskArguments: TaskArguments,
768
769
  ) {
769
770
  const missingRequiredArgument = argumentDefinitions.find(
770
- ({ defaultValue, name }) =>
771
- defaultValue === undefined && taskArguments[name] === undefined,
771
+ ({ defaultValue, name, type }) =>
772
+ isArgumentRequired(type, defaultValue) &&
773
+ taskArguments[name] === undefined,
772
774
  );
773
775
 
774
776
  if (missingRequiredArgument === undefined) {
@@ -22,7 +22,7 @@ import { ensureError } from "@nomicfoundation/hardhat-utils/error";
22
22
 
23
23
  import { detectPluginNpmDependencyProblems } from "../plugins/detect-plugin-npm-dependency-problems.js";
24
24
 
25
- import { formatTaskId } from "./utils.js";
25
+ import { formatTaskId, isArgumentRequired } from "./utils.js";
26
26
  import { validateTaskArgumentValue } from "./validations.js";
27
27
 
28
28
  export class ResolvedTask implements Task {
@@ -203,7 +203,10 @@ export class ResolvedTask implements Task {
203
203
  argument: PositionalArgumentDefinition,
204
204
  value: ArgumentValue | ArgumentValue[],
205
205
  ) {
206
- if (argument.defaultValue === undefined && value === undefined) {
206
+ if (
207
+ isArgumentRequired(argument.type, argument.defaultValue) &&
208
+ value === undefined
209
+ ) {
207
210
  throw new HardhatError(
208
211
  HardhatError.ERRORS.CORE.TASK_DEFINITIONS.MISSING_VALUE_FOR_TASK_ARGUMENT,
209
212
  {
@@ -1,3 +1,5 @@
1
+ import type { ArgumentType, ArgumentValue } from "../../../types/arguments.js";
2
+
1
3
  export function formatTaskId(taskId: string | string[]): string {
2
4
  if (typeof taskId === "string") {
3
5
  return taskId;
@@ -9,3 +11,14 @@ export function formatTaskId(taskId: string | string[]): string {
9
11
  export function getActorFragment(pluginId: string | undefined): string {
10
12
  return pluginId !== undefined ? `Plugin ${pluginId} is` : "You are";
11
13
  }
14
+
15
+ export function isOptionalArgumentType(type: ArgumentType): boolean {
16
+ return type.endsWith("_WITHOUT_DEFAULT");
17
+ }
18
+
19
+ export function isArgumentRequired(
20
+ type: ArgumentType,
21
+ defaultValue: ArgumentValue | ArgumentValue[] | undefined,
22
+ ): boolean {
23
+ return defaultValue === undefined && !isOptionalArgumentType(type);
24
+ }
@@ -19,7 +19,7 @@ import {
19
19
  validateArgumentValue,
20
20
  } from "../arguments.js";
21
21
 
22
- import { formatTaskId } from "./utils.js";
22
+ import { isArgumentRequired, formatTaskId } from "./utils.js";
23
23
 
24
24
  export function validateId(id: string | string[]): void {
25
25
  if (id.length === 0) {
@@ -133,8 +133,8 @@ export function validatePositionalArgument(
133
133
 
134
134
  if (
135
135
  lastArg !== undefined &&
136
- lastArg.defaultValue !== undefined &&
137
- defaultValue === undefined
136
+ !isArgumentRequired(lastArg.type, lastArg.defaultValue) &&
137
+ isArgumentRequired(type, defaultValue)
138
138
  ) {
139
139
  throw new HardhatError(
140
140
  HardhatError.ERRORS.CORE.TASK_DEFINITIONS.REQUIRED_ARG_AFTER_OPTIONAL,
@@ -52,18 +52,36 @@ export interface BuildOptions {
52
52
  * and produce separate compilation passes.
53
53
  */
54
54
  scope?: BuildScope;
55
+
56
+ /**
57
+ * If `true`, a clean up process is run after a successful build.
58
+ *
59
+ * The clean up deletes all the orphan artifacts that aren't reachable from
60
+ * the provided `rootFilePaths`.
61
+ *
62
+ * When building with `"contracts"` scope, it also updates the top-level
63
+ * `artifacts.d.ts`.
64
+ *
65
+ * Only use this option when the provided `rootFilePaths` represent the
66
+ * entire set of contracts for the scope you are using (i.e. not during
67
+ * partial builds). Otherwise, you'll delete artifacts generated in previous
68
+ * builds, despite their sources still being available.
69
+ */
70
+ cleanupArtifacts?: boolean;
55
71
  }
56
72
 
73
+ /**
74
+ * The resolved BuildOptions used to run a build.
75
+ */
76
+ export type ResolvedBuildOptions = Required<BuildOptions>;
77
+
57
78
  /**
58
79
  * The options of the `getCompilationJobs` method.
59
80
  *
60
81
  * Note that this option object includes a `quiet` property, as this process
61
82
  * may require downloading compilers, and potentially printing some output.
62
83
  */
63
- export type GetCompilationJobsOptions = Omit<
64
- BuildOptions,
65
- "removeUnusedArtifacts"
66
- >;
84
+ export type GetCompilationJobsOptions = Omit<BuildOptions, "cleanupArtifacts">;
67
85
 
68
86
  /**
69
87
  * The options of the `runCompilationJob` method.
@@ -434,11 +452,12 @@ export interface SolidityBuildSystem {
434
452
  * used.
435
453
  *
436
454
  * @param rootFilePaths All the root files of the project.
455
+ * @returns The list of artifact paths that remain after the cleanup.
437
456
  */
438
457
  cleanupArtifacts(
439
458
  rootFilePaths: string[],
440
459
  options?: { scope?: BuildScope },
441
- ): Promise<void>;
460
+ ): Promise<string[]>;
442
461
 
443
462
  /**
444
463
  * Compiles a build info, returning the output of the compilation, verbatim,