Repository: smithy-lang/smithy
Pull request #1896 (2023-08-01 18:44:25)

Cache template directory in init command
*Description of changes:*
Caches a copy of the smithy init template directory in the default system temp directory. This greatly speeds up subsequent executions of both the list and create template subcommands of the `smithy init`. 
Previously, creating a template could take up to 6 seconds. With this change that time is down to ~0.3sec after the initial caching of the template repo.

---

*Example Executions*
Create a Template
![cli](https://github.com/smithy-lang/smithy/assets/124718352/aacf4a1a-1de9-4d1c-bf4c-37d6dbf98ce4)

List Templates 
![list](https://github.com/smithy-lang/smithy/assets/124718352/e29bee59-e6df-42cd-a4ac-e92e50fd1a5c)

Note: the gif recorder used here adds additional lag so these do not accurately represent the time these commands take to execute.
___

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.


mtdowling: rather than cache to the temp directory, I suggest caching to the build directory, allowing `smithy clean` to be used to clear any caches.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d70c06398..4ae8f88b8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -28,6 +28,10 @@ jobs:
       - name: Clean, build and javadoc
         run: ./gradlew clean build javadoc -Plog-tests --stacktrace
 
+      - name: Allow long file names in git for windows
+        if: matrix.os == 'windows-latest'
+        run: git config --system core.longpaths true
+
       - name: CLI integration tests
         if: matrix.java >= 17
         run: ./gradlew :smithy-cli:integ -Plog-tests --stacktrace
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1981809c1..dd3fd3f60 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,33 @@
 # Smithy Changelog
 
+## 1.36.0 (2023-08-03)
+
+### Features
+* Allowed disabling format on integers when converting to OpenAPI ([#1904](https://github.com/smithy-lang/smithy/pull/1904))
+* Added intEnum support when converting to OpenAPI ([#1898](https://github.com/smithy-lang/smithy/pull/1898))
+* Added support for overriding validation severity ([#1890](https://github.com/smithy-lang/smithy/pull/1890))
+* Added `disableDefaultValues` option when converting to OpenAPI ([#1887](https://github.com/smithy-lang/smithy/pull/1887))
+* Updated brew workflow to use new smithy tap ([#1897](https://github.com/smithy-lang/smithy/pull/1897))
+* Added progress tracker and message for CLI while cloning a template ([#1888](https://github.com/smithy-lang/smithy/pull/1888))
+* Updated init command to honor quiet setting ([#1889](https://github.com/smithy-lang/smithy/pull/1889))
+* Updated appearance of smithy init list output ([#1901](https://github.com/smithy-lang/smithy/pull/1901))
+* Added exceptions for invalid paths in template definition ([#1907](https://github.com/smithy-lang/smithy/pull/1907))
+* Check for existing directory when creating template with init ([#1885](https://github.com/smithy-lang/smithy/pull/1885))
+
+### Documentation
+* Clarified constraint trait enforcement ([#1902](https://github.com/smithy-lang/smithy/pull/1902))
+* Better document trait merging ([#1895](https://github.com/smithy-lang/smithy/pull/1895))
+* Updated docs to use new smithy-lang tap ([#1893](https://github.com/smithy-lang/smithy/pull/1893))
+* Added naming recommendations ([#1892](https://github.com/smithy-lang/smithy/pull/1892))
+
+### Bug Fixes
+* Fixed NPE when docId is null when ServiceTrait.equals is called ([#1903](https://github.com/smithy-lang/smithy/pull/1903))
+* Fixed off-by-one issues in TokenTree and TreeCursor ([#1891](https://github.com/smithy-lang/smithy/pull/1891))
+* Fixed snapshot dependency resolution ([#1884](https://github.com/smithy-lang/smithy/pull/1884))
+
+### Other
+* Use standard output for regular messages ([#1894](https://github.com/smithy-lang/smithy/pull/1894))
+
 ## 1.35.0 (2023-07-27)
 
 ### Features
diff --git a/VERSION b/VERSION
index 2aeaa11ee..39fc130ef 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.35.0
+1.36.0
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java
index 22015c527..636a2609d 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/IntegUtils.java
@@ -39,7 +39,9 @@ import software.amazon.smithy.utils.IoUtils;
 import software.amazon.smithy.utils.MapUtils;
 
 public final class IntegUtils {
-
+    public static final Path SMITHY_ROOT_CACHE_PATH = Paths.get(System.getProperty("java.io.tmpdir"))
+            .resolve("smithy-cache");
+    public static final Path SMITHY_TEMPLATE_CACHE_PATH = SMITHY_ROOT_CACHE_PATH.resolve("templates");
     private static final Logger LOGGER = Logger.getLogger(IntegUtils.class.getName());
 
     private IntegUtils() {}
@@ -146,4 +148,8 @@ public final class IntegUtils {
             }
         });
     }
+
+    public static void clearCacheDirIfExists() {
+        IoUtils.rmdir(SMITHY_ROOT_CACHE_PATH);
+    }
 }
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java
index 8873302bd..d861e9459 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CleanCommand.java
@@ -20,8 +20,10 @@ import java.nio.file.Paths;
 import java.util.logging.Logger;
 import software.amazon.smithy.build.SmithyBuild;
 import software.amazon.smithy.build.model.SmithyBuildConfig;
+import software.amazon.smithy.cli.ArgumentReceiver;
 import software.amazon.smithy.cli.Arguments;
 import software.amazon.smithy.cli.Command;
+import software.amazon.smithy.cli.HelpPrinter;
 import software.amazon.smithy.utils.IoUtils;
 
 final class CleanCommand implements Command {
@@ -40,20 +42,32 @@ final class CleanCommand implements Command {
 
     @Override
     public String getSummary() {
-        return "Removes Smithy build artifacts.";
+        return "Removes Smithy build artifacts and caches.";
     }
 
     @Override
     public int execute(Arguments arguments, Env env) {
         arguments.addReceiver(new ConfigOptions());
+        arguments.addReceiver(new Options());
 
         CommandAction action = HelpActionWrapper.fromCommand(this, parentCommandName, this::run);
         return action.apply(arguments, env);
     }
 
     private int run(Arguments arguments, Env env) {
-        ConfigOptions options = arguments.getReceiver(ConfigOptions.class);
-        SmithyBuildConfig config = options.createSmithyBuildConfig();
+        ConfigOptions configOptions = arguments.getReceiver(ConfigOptions.class);
+        Options options = arguments.getReceiver(Options.class);
+
+        if (options.cleanTemplateCache) {
+            LOGGER.fine(() -> "Clearing template cache.");
+            if (CliCache.getTemplateCache().clear()) {
+                LOGGER.fine(() -> "No template cache found.");
+            }
+            return 0;
+        }
+
+
+        SmithyBuildConfig config = configOptions.createSmithyBuildConfig();
         Path dir = config.getOutputDirectory()
                 .map(Paths::get)
                 .orElseGet(SmithyBuild::getDefaultOutputDirectory);
@@ -62,6 +76,35 @@ final class CleanCommand implements Command {
             LOGGER.fine(() -> "Directory does not exist: " + dir);
         }
         LOGGER.fine(() -> "Deleted directory " + dir);
+
+        LOGGER.fine(() -> "Clearing all caches.");
+        if (!CliCache.clearAll()) {
+            LOGGER.fine(() -> "No caches found.");
+        }
+
         return 0;
     }
+
+
+    private static final class Options implements ArgumentReceiver {
+        private Boolean cleanTemplateCache = false;
+
+        @Override
+        public boolean testOption(String name) {
+            switch (name) {
+                case "--templates":
+                case "-t":
+                    cleanTemplateCache = true;
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        @Override
+        public void registerHelp(HelpPrinter printer) {
+            printer.param("--templates", "-t", null,
+                    "Clean only the templates cache.");
+        }
+    }
 }
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CliCache.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CliCache.java
new file mode 100644
index 000000000..7d588d1d2
--- /dev/null
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/CliCache.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.cli.commands;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import software.amazon.smithy.cli.CliError;
+import software.amazon.smithy.utils.IoUtils;
+
+interface CliCache {
+    Path DEFAULT_TEMP_DIR = Paths.get(System.getProperty("java.io.tmpdir"));
+    Path ROOT_CACHE_DIR = DEFAULT_TEMP_DIR.resolve("smithy-cache");
+
+    static CliCache getTemplateCache() {
+        return () -> ROOT_CACHE_DIR.resolve("templates");
+    }
+
+    Path getPath();
+
+    default boolean clear() {
+        return IoUtils.rmdir(getPath());
+    }
+
+    default Path get() {
+        Path cachePath = getPath();
+        if (Files.exists(cachePath)) {
+            return cachePath;
+        }
+        // If cache dir does not exist, create it and all required parent directories
+        try {
+            return Files.createDirectories(cachePath);
+        } catch (IOException e) {
+            throw new CliError("Could not create cache at path: " + cachePath);
+        }
+    }
+
+    static boolean clearAll() {
+        return IoUtils.rmdir(ROOT_CACHE_DIR);
+    }
+}
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java
index 79f596189..3200734f4 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/InitCommand.java
@@ -16,8 +16,8 @@
 package software.amazon.smithy.cli.commands;
 
 import java.io.IOException;
-import java.net.URISyntaxException;
 import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -43,6 +43,7 @@ import software.amazon.smithy.utils.IoUtils;
 import software.amazon.smithy.utils.ListUtils;
 import software.amazon.smithy.utils.StringUtils;
 
+
 final class InitCommand implements Command {
     private static final int LINE_LENGTH = 100;
     private static final String COLUMN_SEPARATOR = "  ";
@@ -89,32 +90,99 @@ final class InitCommand implements Command {
         Options options = arguments.getReceiver(Options.class);
         StandardOptions standardOptions = arguments.getReceiver(StandardOptions.class);
 
-        try {
-            Path root = Paths.get(".");
-            Path temp = Files.createTempDirectory("temp");
+        boolean isLocalRepo = isLocalRepo(options.repositoryUrl);
+        Path templateRepoDirPath = getTemplateRepoDirPath(options.repositoryUrl, isLocalRepo);
+
+        // If the cache directory does not exist, create it
+        if (!Files.exists(templateRepoDirPath)) {
+            try (ProgressTracker t = new ProgressTracker(env,
+                    ProgressStyle.dots("cloning template repo", "template repo cloned"),
+                    standardOptions.quiet()
+            )) {
+                Path templateCachePath = CliCache.getTemplateCache().get();
+                String relativeTemplateDir = templateCachePath.relativize(templateRepoDirPath).toString();
+                // Only clone the latest commit from HEAD. Do not include history
+                exec(ListUtils.of("git", "clone", "--depth", "1", "--single-branch",
+                        options.repositoryUrl, relativeTemplateDir), templateCachePath);
+            }
+        }
 
-            loadSmithyTemplateJsonFile(options.repositoryUrl, root, temp);
+        validateTemplateDir(templateRepoDirPath, options.repositoryUrl);
+
+        // Check for updates and update repo to latest if applicable
+        if (!isLocalRepo) {
+            // update remote
+            exec(ListUtils.of("git", "fetch", "--depth", "1"), templateRepoDirPath);
+            String response = exec(ListUtils.of("git", "rev-list", "origin..HEAD"), templateRepoDirPath);
+            // If a change was detected, force the template repo to update
+            if (!StringUtils.isEmpty(response)) {
+                try (ProgressTracker t = new ProgressTracker(env,
+                        ProgressStyle.dots("updating template cache", "template repo updated"),
+                        standardOptions.quiet()
+                )) {
+                    exec(ListUtils.of("git", "reset", "--hard", "origin/main"), templateRepoDirPath);
+                    exec(ListUtils.of("git", "clean", "-dfx"), templateRepoDirPath);
+                }
+            }
+        }
 
-            ObjectNode smithyTemplatesNode = getSmithyTemplatesNode(temp);
 
-            if (options.listTemplates) {
-                this.listTemplates(smithyTemplatesNode, env);
-            } else {
-                this.cloneTemplate(temp, smithyTemplatesNode, options, standardOptions, env);
-            }
-        } catch (IOException | InterruptedException | URISyntaxException e) {
-            throw new RuntimeException(e);
+        ObjectNode smithyTemplatesNode = readJsonFileAsNode(templateRepoDirPath.resolve(SMITHY_TEMPLATE_JSON))
+                .expectObjectNode();
+        if (options.listTemplates) {
+            this.listTemplates(smithyTemplatesNode, env);
+        } else {
+            this.cloneTemplate(templateRepoDirPath, smithyTemplatesNode, options, standardOptions, env);
         }
 
         return 0;
     }
 
-    private void listTemplates(ObjectNode smithyTemplatesNode, Env env) throws IOException {
+    private void validateTemplateDir(Path templateRepoDirPath, String templateUrl) {
+        if (!Files.isDirectory(templateRepoDirPath)) {
+            throw new CliError("Template repository " + templateRepoDirPath + " is not a directory");
+        }
+        Path templateJsonPath = templateRepoDirPath.resolve(SMITHY_TEMPLATE_JSON);
+        if (!Files.exists(templateJsonPath) && Files.isRegularFile(templateJsonPath)) {
+            throw new CliError("Template repository " + templateUrl
+                    + " does not contain a valid `smithy-templates.json`.");
+        }
+    }
+
+    private Path getTemplateRepoDirPath(String repoPath, boolean isLocalRepo) {
+        if (isLocalRepo) {
+            // Just use the local path if the git repo is local
+            return Paths.get(repoPath);
+        } else {
+            return CliCache.getTemplateCache().get().resolve(getCacheDirFromURL(repoPath));
+        }
+    }
+
+    // Remove any trailing .git
+    // Remove "/" and ".." and ":" characters so a directory can be created with no nesting
+    private String getCacheDirFromURL(final String repositoryUrl) {
+        return repositoryUrl.replace(".git", "")
+                .replace(":", "_")
+                .replace("/", "_")
+                .replace(".", "_");
+    }
+
+    private void listTemplates(ObjectNode smithyTemplatesNode, Env env) {
         try (ColorBuffer buffer = ColorBuffer.of(env.colors(), env.stdout())) {
             buffer.println(getTemplateList(smithyTemplatesNode, env));
         }
     }
 
+    private boolean isLocalRepo(String repoPath) {
+        try  {
+            Path localPath = Paths.get(repoPath);
+            return Files.exists(localPath);
+        } catch (InvalidPathException exc) {
+            return false;
+        }
+
+    }
+
     private String getTemplateList(ObjectNode smithyTemplatesNode, Env env) {
         int maxTemplateLength = 0;
         Map<String, String> templates = new TreeMap<>();
@@ -158,6 +226,7 @@ final class InitCommand implements Command {
         return builder.toString();
     }
 
+
     private static void writeTemplateBorder(ColorBuffer writer, int maxNameLength, int maxDocLength) {
         writer.print(pad("", maxNameLength).replace(" ", "─"), ColorTheme.TEMPLATE_LIST_BORDER)
                 .print(COLUMN_SEPARATOR)
@@ -169,9 +238,8 @@ final class InitCommand implements Command {
         return StringUtils.wrap(doc, maxLength, System.lineSeparator() + pad("", offset), false);
     }
 
-    private void cloneTemplate(Path temp, ObjectNode smithyTemplatesNode, Options options,
-                               StandardOptions standardOptions, Env env)
-            throws IOException, InterruptedException, URISyntaxException {
+    private void cloneTemplate(Path templateRepoDirPath, ObjectNode smithyTemplatesNode, Options options,
+                               StandardOptions standardOptions, Env env) {
 
         String template = options.template;
         String directory = options.directory;
@@ -184,7 +252,7 @@ final class InitCommand implements Command {
         if (directory == null) {
             directory = template;
         }
-        final Path dest = Paths.get(directory);
+        Path dest = Paths.get(directory);
         if (Files.exists(dest)) {
             throw new CliError("Output directory `" + directory + "` already exists.");
         }
@@ -195,32 +263,27 @@ final class InitCommand implements Command {
                     "Invalid template `%s`. `%s` provides the following templates:%n%n%s",
                     template, getTemplatesName(smithyTemplatesNode), getTemplateList(smithyTemplatesNode, env)));
         }
-
         ObjectNode templateNode = templatesNode.expectObjectMember(template).expectObjectNode();
 
-        final String templatePath = getTemplatePath(templateNode, template);
+        String templatePath = getTemplatePath(templateNode, template);
         List<String> includedFiles = getIncludedFiles(templateNode);
 
-        try (ProgressTracker ignored = new ProgressTracker(env,
-                ProgressStyle.dots("cloning template", "template cloned"),
-                standardOptions.quiet()
-        )) {
-            // Specify the subdirectory to download
-            exec(ListUtils.of("git", "sparse-checkout", "set", "--no-cone", templatePath), temp);
-            // add any additional files that should be included
-            for (String includedFile : includedFiles) {
-                exec(ListUtils.of("git", "sparse-checkout", "add", "--no-cone", includedFile), temp);
-            }
-            exec(ListUtils.of("git", "checkout"), temp);
+        Path stagingPath = createStagingRepo(templateRepoDirPath);
+
+        // Specify the subdirectory to check out
+        exec(ListUtils.of("git", "sparse-checkout", "set", "--no-cone", templatePath), stagingPath);
+        // add any additional files that should be included
+        for (String includedFile : includedFiles) {
+            exec(ListUtils.of("git", "sparse-checkout", "add", "--no-cone", includedFile), stagingPath);
         }
+        exec(ListUtils.of("git", "checkout"), stagingPath);
 
-        if (!Files.exists(temp.resolve(templatePath))) {
+        if (!Files.exists(stagingPath.resolve(templatePath))) {
             throw new CliError(String.format("Template path `%s` for template \"%s\" is invalid.",
                     templatePath, template));
         }
-
-        IoUtils.copyDir(Paths.get(temp.toString(), templatePath), dest);
-        copyIncludedFiles(temp.toString(), dest.toString(), includedFiles, template);
+        IoUtils.copyDir(Paths.get(stagingPath.toString(), templatePath), dest);
+        copyIncludedFiles(stagingPath.toString(), dest.toString(), includedFiles, template, env);
 
         if (!standardOptions.quiet()) {
             try (ColorBuffer buffer = ColorBuffer.of(env.colors(), env.stdout())) {
@@ -229,13 +292,17 @@ final class InitCommand implements Command {
         }
     }
 
-    private static void loadSmithyTemplateJsonFile(String repositoryUrl, Path root, Path temp) {
-        exec(ListUtils.of("git", "clone", "--filter=blob:none", "--no-checkout", "--depth", "1", "--sparse",
-                repositoryUrl, temp.toString()), root);
-
-        exec(ListUtils.of("git", "sparse-checkout", "set", "--no-cone", SMITHY_TEMPLATE_JSON), temp);
+    private static Path createStagingRepo(Path repoPath) {
+        Path temp;
+        try {
+            temp = Files.createTempDirectory("temp");
+        } catch (IOException exc) {
+            throw new CliError("Unable to create staging directory for template.");
+        }
+        exec(ListUtils.of("git", "clone", "--no-checkout", "--depth", "1", "--sparse",
+                "file://" + repoPath.toString(), temp.toString()), Paths.get("."));
 
-        exec(ListUtils.of("git", "checkout"), temp);
+        return temp;
     }
 
     private static ObjectNode getSmithyTemplatesNode(Path jsonFilePath) {
@@ -271,7 +338,7 @@ final class InitCommand implements Command {
     }
 
     private static void copyIncludedFiles(String temp, String dest, List<String> includedFiles,
-                                          String templateName) throws IOException {
+                                          String templateName, Env env) {
         for (String included : includedFiles) {
             Path includedPath = Paths.get(temp, included);
             if (!Files.exists(includedPath)) {
@@ -284,7 +351,12 @@ final class InitCommand implements Command {
             if (Files.isDirectory(includedPath)) {
                 IoUtils.copyDir(includedPath, target);
             } else if (Files.isRegularFile(includedPath)) {
-                Files.copy(includedPath, target);
+                try {
+                    Files.copy(includedPath, target);
+                } catch (IOException e) {
+                    throw new CliError("Unable to copy included file: " + includedPath
+                            + "to destination directory: " + target);
+                }
             }
         }
     }
diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java
index 0439e23f0..1a5107cf4 100644
--- a/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java
+++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/IoUtils.java
@@ -334,6 +334,9 @@ public final class IoUtils {
             Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
                 @Override
                 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                    // Workaround for Windows systems that set some git packfiles to readonly
+                    file.toFile().setWritable(true);
+
                     Files.delete(file);
                     return FileVisitResult.CONTINUE;
                 }


Test patch:
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java
index 24c7525b6..29fb1da6e 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/CleanCommandTest.java
@@ -4,18 +4,25 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasLength;
 import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
+
 import org.junit.jupiter.api.Test;
+import software.amazon.smithy.utils.IoUtils;
 import software.amazon.smithy.utils.ListUtils;
 
+
 public class CleanCommandTest {
+    private static final String PROJECT_NAME = "simple-config-sources";
     @Test
     public void exitNormallyIfBuildDirMissing() {
-        IntegUtils.run("simple-config-sources", ListUtils.of("clean"), result -> {
+        IntegUtils.run(PROJECT_NAME, ListUtils.of("clean"), result -> {
             assertThat(result.getExitCode(), equalTo(0));
             assertThat(result.getOutput(), hasLength(0));
         });
@@ -23,7 +30,7 @@ public class CleanCommandTest {
 
     @Test
     public void deletesContentsOfBuildDir() {
-        IntegUtils.withProject("simple-config-sources", root -> {
+        IntegUtils.withProject(PROJECT_NAME, root -> {
             try {
                 Path created = Files.createDirectories(root.resolve("build").resolve("smithy").resolve("foo"));
                 assertThat(Files.exists(created), is(true));
@@ -35,4 +42,52 @@ public class CleanCommandTest {
             }
         });
     }
+
+    @Test
+    public void cleanRemovesAllCacheDirectories() throws IOException {
+        IntegUtils.clearCacheDirIfExists();
+        try {
+            Files.createDirectories(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH);
+            assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)
+                    && Files.isDirectory(IntegUtils.SMITHY_ROOT_CACHE_PATH));
+            IntegUtils.run(PROJECT_NAME, ListUtils.of("clean"), result -> {
+                assertThat(result.getExitCode(), equalTo(0));
+                assertThat(result.getOutput(), hasLength(0));
+                assertFalse(Files.exists(IntegUtils.SMITHY_ROOT_CACHE_PATH));
+            });
+        } finally {
+            IntegUtils.clearCacheDirIfExists();
+        }
+    }
+
+    @Test
+    public void cleanWithTemplateOptionRemovesOnlyTemplateDir() throws IOException {
+        IntegUtils.clearCacheDirIfExists();
+        try {
+            Files.createDirectories(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH);
+            assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)
+                    && Files.isDirectory(IntegUtils.SMITHY_ROOT_CACHE_PATH));
+
+            IntegUtils.withProject(PROJECT_NAME, root -> {
+                Path created = null;
+                try {
+                    created = Files.createDirectories(root.resolve("build").resolve("smithy").resolve("foo"));
+                    assertThat(Files.exists(created), is(true));
+                    RunResult result = IntegUtils.run(root, ListUtils.of("clean", "--templates"));
+                    assertThat(Files.exists(created), is(true));
+                    assertThat(result.getExitCode(), is(0));
+                    assertTrue(Files.exists(IntegUtils.SMITHY_ROOT_CACHE_PATH));
+                    assertFalse(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH));
+                } catch (IOException e) {
+                    throw new UncheckedIOException(e);
+                } finally {
+                    if (created != null) {
+                        IoUtils.rmdir(created);
+                    }
+                }
+            });
+        } finally {
+            IntegUtils.clearCacheDirIfExists();
+        }
+    }
 }
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java
index a8f77ba9d..102a97993 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/InitCommandTest.java
@@ -3,7 +3,12 @@ package software.amazon.smithy.cli;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.emptyString;
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 import org.junit.jupiter.api.Test;
 import software.amazon.smithy.utils.IoUtils;
 import software.amazon.smithy.utils.ListUtils;
@@ -295,6 +300,54 @@ public class InitCommandTest {
         });
     }
 
+    @Test
+    public void cacheCreatedOnFirstCreationOfTemplate() {
+        IntegUtils.clearCacheDirIfExists();
+        IntegUtils.withProject(PROJECT_NAME, root -> {
+            try {
+                RunResult resultFirst = IntegUtils.run(root, ListUtils.of("init", "-o", "hello-world"));
+                assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)
+                        && Files.isDirectory(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH));
+                assertThat(resultFirst.getExitCode(), equalTo(0));
+                assertThat(resultFirst.getOutput(),
+                        containsString("template repo cloned"));
+                assertThat(resultFirst.getOutput(),
+                        containsString("Smithy project created in directory: hello-world"));
+                IoUtils.rmdir(root.resolve("hello-world"));
+
+                RunResult resultSecond = IntegUtils.run(root, ListUtils.of("init", "-o", "hello-world"));
+                assertTrue(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH)
+                        && Files.isDirectory(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH));
+                assertThat(resultSecond.getExitCode(), equalTo(0));
+                assertThat(resultSecond.getOutput(), not(containsString("template repo cloned")));
+                assertThat(resultSecond.getOutput(),
+                        containsString("Smithy project created in directory: hello-world"));
+            } finally {
+                IntegUtils.clearCacheDirIfExists();
+            }
+        });
+    }
+
+    @Test
+    public void noCacheCreatedWhenLocalRepo() {
+        IntegUtils.clearCacheDirIfExists();
+        IntegUtils.withProject(PROJECT_NAME, templatesDir -> {
+            setupTemplatesDirectory(templatesDir);
+
+            IntegUtils.withTempDir("exitZero", dir -> {
+                RunResult result = IntegUtils.run(
+                        dir, ListUtils.of("init", "-t", "quickstart-cli", "-u", templatesDir.toString()));
+                assertThat(result.getOutput(),
+                        containsString("Smithy project created in directory: quickstart-cli"));
+                assertThat(result.getExitCode(), is(0));
+                assertThat(Files.exists(Paths.get(dir.toString(), "quickstart-cli")), is(true));
+
+                assertFalse(Files.exists(IntegUtils.SMITHY_TEMPLATE_CACHE_PATH));
+                assertFalse(Files.exists(IntegUtils.SMITHY_ROOT_CACHE_PATH));
+            });
+        });
+    }
+
     private static void run(List<String> args, Path root) {
         StringBuilder output = new StringBuilder();
         int result = IoUtils.runCommand(args, root, output, Collections.emptyMap());