Repository: FHIR/sushi
Pull request #620 (2020-10-01 18:18:17)

New ig publisher integration
It's time... !!!

We want to preserve the commits we did on the branch, so please use the REBASE option when you merge!


Patch:
diff --git a/README.md b/README.md
index 2eccd4a..775a5fc 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/fhir/sushi/Lint%20and%20Test)
 ![npm](https://img.shields.io/npm/v/fsh-sushi)
 
-# SUSHI (WORK IN PROGRESS)
+# SUSHI
 
 SUSHI (aka "SUSHI Unshortens Short Hand Inputs") is a reference implementation command-line interpreter/compiler for FHIR Shorthand (FSH).
 
@@ -36,32 +36,21 @@ Options:
 Additional information:
   [path-to-fsh-defs]
     Default: "."
-    If fsh/ subdirectory present, it is included in [path-to-fsh-defs]
+    If input/fsh/ subdirectory present, it is included in [path-to-fsh-defs]
   -o, --out <out>
-    Default: "build"
-    If fsh/ subdirectory present, default output is one directory above fsh/
+    Default: "fsh-generated"
+    If legacy publisher mode (fsh subdirectory present), default output is parent of "fsh"
+    If legacy flat mode (no input/fsh or fsh subdirectories present), default output is "build"
+
 ```
 
+See the [SUSHI documentation](https://fshschool.org/docs/sushi/) for detailed information on using SUSHI.
+
 # IG Generation
 
 SUSHI supports publishing implementation guides via the new template-based IG Publisher.  The template-based publisher is still being developed by the FHIR community.  See the [Guidance for HL7 IG Creation](https://build.fhir.org/ig/FHIR/ig-guidance/) for more details.
 
-If the input folder (i.e., "FSH Tank") contains a sub-folder named "ig-data", then SUSHI will generate a basic Implementation Guide project that can be built using the template-based IG Publisher.  SUSHI currently supports limited customization of the IG via the following files:
-
-* `ig-data/ig.ini`: If present, the user-provided igi.ini values will be merged with SUSHI-generated ig.ini.
-* `ig-data/package-list.json`: If present, it will be used instead of a generated package-list.json.
-* `ig-data/input/ignoreWarnings.txt`: If present, will be copied into the corresponding location in the IG input folder
-* `ig-data/input/images/*`: If present, image files will be copied into the IG input and can be referenced by user-provided pages.
-* `ig-data/input/pagecontent/index.[md|xml]`: If present, it will provide the content for the IG's main page.
-* `ig-data/input/pagecontent/*.[md|xml]`: If present, these files will be generated as individual pages in the IG and will be present in the table of contents.
-* `ig-data/input/pagecontent/{name-of-resource-file}-[intro|notes].[md|xml]`: If present, these files will place content directly on the relevant resource page. Intro files will place content before the resource definition; notes files will place content after.
-* `ig-data/input/pagecontent/*`: If present, all other files of any type that do not match the above patterns will be copied into the IG input, but will not appear in the table of contents.
-* `ig-data/input/{supported-resource-input-folder}`: JSON files found in supported subfolders will be processed as additional resources in the IG, added to the IG JSON file, and copied to the corresponding locations in the IG input. This allows for a "Bring Your Own JSON" capability when you already have resources that were generated via another method. If a provided JSON file has the same URL as a FSH-defined resource, the provided JSON file will overwrite the FSH-defined one. Supported folders match those documented in the [Guidance for FHIR IG Creation](https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root.input): capabilities, examples, extensions, models, operations, profiles, resources, vocabulary.
-
-After running SUSHI, change to the output folder and run the `_updatePublisher` script and enter `y|Y` to download latest publisher when prompted. Then run the `_genonce` or `_gencontinuous` script.
-`_genonce` will generate the IG one time. `_gencontinuous` will continuously run the `genonce` script, which will regenerate the IG with any changes that were made to the input of the IG build.
-
-If the input folder does not contain a sub-folder named "ig-data", then only the resources (e.g., profiles, extensions, etc.) will be generated.
+Based on the inputs in FSH files, **sushi-config.yaml**, and the IG project directory, SUSHI populates the output directory. See the [documentation on IG Project with SUSHI](https://fshschool.org/docs/sushi/project/#ig-projects) for more information on using SUSHI to generate IGs.
 
 # Installation for Developers
 
diff --git a/package.json b/package.json
index 6f34970..be516e9 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
   "version": "0.16.0",
   "description": "Sushi Unshortens Short Hand Inputs (FSH Compiler)",
   "scripts": {
-    "build": "rm -rf dist && tsc && cp -r src/ig/files dist/ig/files && cp -r src/utils/init-project dist/utils/init-project",
+    "build": "rm -rf dist && tsc && cp -r src/utils/init-project dist/utils/init-project",
     "build:watch": "tsc -w",
     "build:grammar": "bash antlr/gradlew -p antlr generateGrammarSource",
     "test": "jest --maxWorkers=4 --coverage",
diff --git a/src/app.ts b/src/app.ts
index a5e6fe1..1a9a8dc 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -23,7 +23,7 @@ import {
   init
 } from './utils/Processing';
 
-const FSH_VERSION = '0.13.x';
+const FSH_VERSION = '1.0.0';
 
 app().catch(e => {
   logger.error(`SUSHI encountered the following unexpected error: ${e.message}`);
@@ -46,10 +46,15 @@ async function app() {
       console.log('Additional information:');
       console.log('  [path-to-fsh-defs]');
       console.log('    Default: "."');
-      console.log('    If fsh/ subdirectory present, it is included in [path-to-fsh-defs]');
+      console.log('    If input/fsh/ subdirectory present, it is included in [path-to-fsh-defs]');
       console.log('  -o, --out <out>');
-      console.log('    Default: "build"');
-      console.log('    If fsh/ subdirectory present, default output is one directory above fsh/');
+      console.log('    Default: "fsh-generated"');
+      console.log(
+        '    If legacy publisher mode (fsh subdirectory present), default output is parent of "fsh"'
+      );
+      console.log(
+        '    If legacy flat mode (no input/fsh or fsh subdirectories present), default output is "build"'
+      );
     })
     .arguments('[path-to-fsh-defs]')
     .action(function (pathToFshDefs) {
@@ -58,22 +63,46 @@ async function app() {
     .parse(process.argv);
 
   if (program.init) {
-    init();
+    await init();
     process.exit(0);
   }
   if (program.debug) logger.level = 'debug';
 
   logger.info(`Running ${getVersion()}`);
 
+  logger.info('Arguments:');
+  if (program.debug) {
+    logger.info('  --debug');
+  }
+  if (program.snapshot) {
+    logger.info('  --snapshot');
+  }
+  if (program.out) {
+    logger.info(`  --out ${path.resolve(program.out)}`);
+  }
+  logger.info(`  ${path.resolve(input)}`);
+
+  // IG Publisher HACK: the IG Publisher invokes SUSHI with `/fsh` appended (even if it doesn't
+  // exist).  If we detect a direct fsh path, we need to fix it by backing up a folder, else it
+  // won't correctly detect the IG Publisher mode.
+  if (path.basename(input) === 'fsh') {
+    input = path.dirname(input);
+  }
+
+  const originalInput = input;
   input = findInputDir(input);
 
-  // If a fsh subdirectory is used, we are in an IG Publisher context
-  const isIgPubContext = path.parse(input).base === 'fsh';
-  const outDir = ensureOutputDir(input, program.out, isIgPubContext);
+  // If an input/fsh subdirectory is used, we are in an IG Publisher context
+  const fshFolder = path.basename(input) === 'fsh';
+  const inputFshFolder = fshFolder && path.basename(path.dirname(input)) === 'input';
+  const isIgPubContext = inputFshFolder;
+  // TODO: Legacy support for top level fsh/ subdirectory. Remove when no longer supported.
+  const isLegacyIgPubContext = fshFolder && !inputFshFolder;
+  const outDir = ensureOutputDir(input, program.out, isIgPubContext, isLegacyIgPubContext);
 
   let config: Configuration;
   try {
-    config = readConfig(input);
+    config = readConfig(isIgPubContext ? originalInput : input);
   } catch {
     process.exit(1);
   }
@@ -82,8 +111,14 @@ async function app() {
   const defs = new FHIRDefinitions();
   const dependencyDefs = loadExternalDependencies(defs, config);
 
-  // Load custom resources specified in ig-data folder
-  loadCustomResources(input, defs);
+  // Load custom resources
+  if (!isIgPubContext) {
+    // In legacy configuration (both IG publisher context and any other tank), resources are in ig-data/input/
+    loadCustomResources(path.join(input, 'ig-data', 'input'), defs);
+  } else {
+    // In current tank configuration (input/fsh), resources will be in input/
+    loadCustomResources(path.join(input, '..'), defs);
+  }
 
   let tank: FSHTank;
   try {
@@ -108,17 +143,28 @@ async function app() {
 
   logger.info('Converting FSH to FHIR resources...');
   const outPackage = exportFHIR(tank, defs);
-  writeFHIRResources(outDir, outPackage, program.snapshot);
+  writeFHIRResources(outDir, outPackage, program.snapshot, isIgPubContext);
 
   // If FSHOnly is true in the config, do not generate IG content, otherwise, generate IG content
   if (config.FSHOnly) {
     logger.info('Exporting FSH definitions only. No IG related content will be exported.');
   } else {
-    const igDataPath = path.resolve(input, 'ig-data');
+    const igDataPath = isIgPubContext
+      ? path.resolve(input, '..', '..')
+      : path.resolve(input, 'ig-data');
     logger.info('Assembling Implementation Guide sources...');
     const igExporter = new IGExporter(outPackage, defs, igDataPath, isIgPubContext);
     igExporter.export(outDir);
     logger.info('Assembled Implementation Guide sources; ready for IG Publisher.');
+    if (
+      !fs
+        .readdirSync(outDir)
+        .some(file => file.startsWith('_genonce') || file.startsWith('_updatePublisher'))
+    ) {
+      logger.info(
+        'The sample-ig located at https://github.com/FHIR/sample-ig contains scripts useful for downloading and running the IG Publisher.'
+      );
+    }
   }
 
   console.log();
@@ -166,7 +212,6 @@ function printResults(pkg: Package, isIG: boolean) {
     clr('║') + ` │ ${prNum} │ ${extnNum} │ ${vstNum} │ ${cdsysNum} │ ${insNum} │ ` + clr('║'),
     clr('║') + ' ╰──────────┴────────────┴───────────┴─────────────┴───────────╯ ' + clr('║'),
     clr('║' + '                                                                 ' + '' + '║'),
-    clr('║') + ' See SUSHI-GENERATED-FILES.md for details on generated IG files. ' + clr('║'),
     clr('╠' + '═════════════════════════════════════════════════════════════════' + '' + '╣'),
     clr('║') + ` ${aWittyMessageInvolvingABadFishPun} ${errorNumMsg} ${wrNumMsg} ` + clr('║'),
     clr('╚' + '═════════════════════════════════════════════════════════════════' + '' + '╝')
diff --git a/src/fhirdefs/load.ts b/src/fhirdefs/load.ts
index a07c8b7..ef148a0 100644
--- a/src/fhirdefs/load.ts
+++ b/src/fhirdefs/load.ts
@@ -171,10 +171,10 @@ export function cleanCachedPackage(packageDirectory: string): void {
 
 /**
  * Loads custom resources defined in ig-data into FHIRDefs
- * @param {string} input - The input path to the cli
+ * @param {string} resourceDir - The path to the directory containing the resource subdirs
  * @param {FHIRDefinitions} defs - The FHIRDefinitions object to load definitions into
  */
-export function loadCustomResources(input: string, defs: FHIRDefinitions): void {
+export function loadCustomResources(resourceDir: string, defs: FHIRDefinitions): void {
   // Similar code for loading custom resources exists in IGExporter.ts addPredefinedResources()
   const pathEnds = [
     'capabilities',
@@ -189,7 +189,7 @@ export function loadCustomResources(input: string, defs: FHIRDefinitions): void
   const converter = new FHIRConverter();
   for (const pathEnd of pathEnds) {
     let invalidFile = false;
-    const dirPath = path.join(input, 'ig-data', 'input', pathEnd);
+    const dirPath = path.join(resourceDir, pathEnd);
     if (fs.existsSync(dirPath)) {
       const files = fs.readdirSync(dirPath);
       for (const file of files) {
diff --git a/src/fshtypes/Configuration.ts b/src/fshtypes/Configuration.ts
index 91a4848..98454d7 100644
--- a/src/fshtypes/Configuration.ts
+++ b/src/fshtypes/Configuration.ts
@@ -97,10 +97,11 @@ export type Configuration = {
   parameters?: ImplementationGuideDefinitionParameter[];
 
   // The templates property corresponds 1:1 with IG.definition.template. Note that plural templates
-  // refers to the IG.definiton.template definitions; singular template refers to the specific
-  // template to use for this IG.
+  // refers to the IG.definiton.template definitions; not the publisher template in ig.ini.
   templates?: ImplementationGuideDefinitionTemplate[];
 
+  // DEPRECATED: Use of the template property is no longer recommended.  Authors should manage the
+  // ig.ini file themselves. Support for the template property will be removed in a future release.
   // The template property will be used to generate an ig.ini file.
   // To provide a custom ig.ini file, do not include this property and include an `ig.ini` file in ig-data.
   template?: string;
@@ -109,9 +110,6 @@ export type Configuration = {
   // a simple structure where the YAML key is the menu item name and the value is the URL. The IG
   // publisher currently only supports one level deep on sub-menus. To provide a custom menu.xml
   // file, do not include this property and include a `menu.xml` file in input/includes.
-  // TO CONSIDER: If no menu data is provided, can we generate the menu based on the pages order
-  // or should we just generate a very standard menu (since there may be too many pages to fit in
-  // a menu)?
   menu?: ConfigurationMenuItem[];
 
   // The history property corresponds to package-list.json. SUSHI will use the existing top-level
diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts
index 0780a7e..716d8e4 100644
--- a/src/ig/IGExporter.ts
+++ b/src/ig/IGExporter.ts
@@ -9,7 +9,6 @@ import {
   copySync,
   outputJSONSync,
   outputFileSync,
-  chmodSync,
   existsSync,
   readdirSync,
   readFileSync
@@ -45,15 +44,21 @@ export class IGExporter {
   private readonly configPath: string;
   private readonly outputLog: Map<string, outputLogDetails>;
   private readonly config: Configuration;
+  private readonly configName: string;
+  private shouldCopyFiles: boolean;
   constructor(
     private readonly pkg: Package,
     private readonly fhirDefs: FHIRDefinitions,
     private readonly igDataPath: string,
-    private readonly isIgPubContext: boolean = false
+    private readonly isIgPubContext = false
   ) {
-    this.configPath = path.resolve(this.igDataPath, '..', 'config.yaml');
     this.outputLog = new Map();
     this.config = pkg.config;
+    this.configPath = path.resolve(this.igDataPath, '..', path.basename(this.config.filePath));
+    this.configName = path.basename(this.configPath);
+    // In legacy IG publisher mode and legacy flat tank, always copy files and prefer config files.
+    // Since current supported tank configuration requires input/fsh folder, both legacy cases occur when not in IG Pub context
+    this.shouldCopyFiles = !this.isIgPubContext;
   }
 
   getOutputLogDetails(file: string) {
@@ -73,7 +78,6 @@ export class IGExporter {
     this.addPredefinedResources(outPath);
     this.addConfiguredResources();
     this.addConfiguredGroups();
-    this.addStaticFiles(outPath);
     this.addIndex(outPath);
     if (!this.config.pages?.length) {
       this.addOtherPageContent(outPath);
@@ -197,7 +201,7 @@ export class IGExporter {
       // No need for the detailed log message since we already logged one in the package loader.
       logger.error(
         `Failed to add ${dependency.packageId} to ImplementationGuide instance because no ` +
-          'version was specified in your config.yaml.'
+          `version was specified in your ${this.configName}.`
       );
       return;
     }
@@ -217,7 +221,7 @@ export class IGExporter {
         logger.error(
           `Failed to add ${dependsOn.packageId}:${dependsOn.version} to ` +
             'ImplementationGuide instance because SUSHI could not find the IG URL in the ' +
-            'dependency IG. To specify the IG URL in your config.yaml, use the dependency ' +
+            `dependency IG. To specify the IG URL in your ${this.configName}, use the dependency ` +
             'details format:\n\n' +
             'dependencies:\n' +
             `  ${dependsOn.packageId}:\n` +
@@ -241,48 +245,8 @@ export class IGExporter {
   }
 
   /**
-   * Add the static files that (currently) do not change from IG to IG.
-   *
-   * @param igPath {string} - the path where the IG is exported to
-   */
-  private addStaticFiles(igPath: string): void {
-    const inputPath = path.join(__dirname, 'files');
-    this.copyAsIs(inputPath, igPath, src => {
-      // If in an IG Publisher context, do not include any of the publisher scripts
-      if (this.isIgPubContext) {
-        if (path.parse(src).base.startsWith('_genonce.')) return false;
-        if (path.parse(src).base.startsWith('_gencontinuous.')) return false;
-        if (path.parse(src).base.startsWith('_updatePublisher.')) return false;
-      }
-      // Filter out menu because handled separately
-      if (path.parse(src).base.startsWith('menu.xml')) return false;
-      return true;
-    });
-
-    // On Windows, the file permissions are not always preserved. This doesn't
-    // cause a problem for the Windows user, but it may cause problems for
-    // Mac and Linux users who use an NPM package published by a Windows user.
-    // To work around this, we set the necessary permissions on executable
-    // scripts after copying them to the IG path.
-    try {
-      if (!this.isIgPubContext) {
-        chmodSync(path.join(igPath, '_genonce.sh'), 0o755);
-        chmodSync(path.join(igPath, '_gencontinuous.sh'), 0o755);
-        chmodSync(path.join(igPath, '_updatePublisher.sh'), 0o755);
-      }
-    } catch (e) {
-      // We don't want to fail the whole export for this, but we should log it
-      logger.warn(
-        'Failed to set executable permissions on IG publisher scripts (_genonce.sh, ' +
-          '_gencontinuous.sh, _updatePublisher.sh). You may need to set these permissions ' +
-          'manually before they can be executed (e.g., chmod 755 _genonce.sh).'
-      );
-    }
-  }
-
-  /**
-   * Add the index.md file. Creates an index.md based on the "indexPageContent" in config.yaml.
-   * If the user specified an index file in the ig-data folder, and no "indexPageContent" in config.yaml is specified,
+   * Add the index.md file. Creates an index.md based on the "indexPageContent" in sushi-config.yaml.
+   * If the user specified an index file in the ig-data folder, and no "indexPageContent" in sushi-config.yaml is specified,
    * the file from ig-data is used instead. The provided file may be in one of two locations:
    * ig-data/input/pagecontent or ig-data/input/pages
    *
@@ -319,53 +283,91 @@ export class IGExporter {
 
     if (this.config.indexPageContent) {
       ensureDirSync(pageContentExportPath);
-      const warning = warningBlock('<!-- index.md {% comment %}', '{% endcomment %} -->', [
-        'To change the contents of this file, edit the "indexPageContent" attribute in the tank config.yaml file',
-        `or provide your own index file in the ig-data${path.sep}input${path.sep}pagecontent or ig-data${path.sep}input${path.sep}pages folder.`,
-        'See: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root.input'
-      ]);
-      const outputPath = path.join(pageContentExportPath, 'index.md');
-      outputFileSync(outputPath, `${warning}${this.config.indexPageContent}`);
-      this.updateOutputLog(outputPath, [this.configPath], 'generated');
-      logger.info('Generated index.md based on "indexPageContent" in config.yaml.');
 
       if (filePath) {
+        const filePathString = path.join(
+          path.basename(this.igDataPath),
+          path.relative(this.igDataPath, filePath)
+        );
+
+        let preferredFileMessage =
+          `Since a ${filePathString} file was found, the "indexPageContent" property in the ${this.configName} ` +
+          'will be ignored and an index.md file will not be generated. Remove the "indexPageContent" ' +
+          `property in ${this.configName} to resolve this warning.`;
+        if (this.shouldCopyFiles) {
+          preferredFileMessage =
+            `Since the "indexPageContent" property is present in the ${this.configName}, ` +
+            `an index.md file will be generated and the ${filePathString} file will be ignored.` +
+            `Remove the "indexPageContent" property in ${this.configName} to use the ${filePathString} file instead.`;
+        }
         logger.warn(
-          'Found both an "indexPageContent" property in config.yaml and an index file at ' +
-            `ig-data${path.sep}${path.relative(this.igDataPath, filePath)}. ` +
-            'Since the "indexPageContent" property is present in the config.yaml, an index.md file will be generated and ' +
-            `the ig-data${path.sep}${path.relative(this.igDataPath, filePath)} file will be ` +
-            'ignored. Remove the "indexPageContent" property in config.yaml to use the ' +
-            `ig-data${path.sep}${path.relative(this.igDataPath, filePath)} file instead.`,
+          `Found both an "indexPageContent" property in ${this.configName} and an index file at ` +
+            `${filePathString}. ${preferredFileMessage}`,
           {
             file: filePath
           }
         );
+
+        if (!this.shouldCopyFiles) {
+          if (existsSync(inputIndexXMLPagesPath) || existsSync(inputIndexXMLPageContentPath)) {
+            generation = 'html';
+          }
+          // Add user-provided index file to IG definition
+          this.ig.definition.page.page.push({
+            nameUrl: 'index.html',
+            title: 'Home',
+            generation
+          });
+          return;
+        }
       }
+
+      const warning = warningBlock('<!-- index.md {% comment %}', '{% endcomment %} -->', [
+        `To change the contents of this file, edit the "indexPageContent" attribute in the tank ${this.configName} file`,
+        `or provide your own index file in the ${path.basename(this.igDataPath)}${path.sep}input${
+          path.sep
+        }pagecontent or ${path.basename(this.igDataPath)}${path.sep}input${path.sep}pages folder.`,
+        'See: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root.input'
+      ]);
+      const outputPath = path.join(pageContentExportPath, 'index.md');
+      outputFileSync(outputPath, `${warning}${this.config.indexPageContent}`);
+      this.updateOutputLog(outputPath, [this.configPath], 'generated');
+      logger.info(`Generated index.md based on "indexPageContent" in ${this.configName}.`);
     } else if (existsSync(inputIndexMarkdownPageContentPath)) {
-      ensureDirSync(pageContentExportPath);
-      this.copyWithWarningText(
-        inputIndexMarkdownPageContentPath,
-        path.join(pageContentExportPath, 'index.md')
-      );
-      logger.info(`Copied ${path.join(pageContentExportPath, 'index.md')}`);
+      if (this.shouldCopyFiles) {
+        ensureDirSync(pageContentExportPath);
+        this.copyWithWarningText(
+          inputIndexMarkdownPageContentPath,
+          path.join(pageContentExportPath, 'index.md')
+        );
+        logger.info(`Copied ${path.join(pageContentExportPath, 'index.md')}`);
+      }
     } else if (existsSync(inputIndexXMLPageContentPath)) {
-      ensureDirSync(pageContentExportPath);
-      this.copyWithWarningText(
-        inputIndexXMLPageContentPath,
-        path.join(pageContentExportPath, 'index.xml')
-      );
       generation = 'html';
-      logger.info(`Copied ${path.join(pageContentExportPath, 'index.xml')}`);
+      if (this.shouldCopyFiles) {
+        ensureDirSync(pageContentExportPath);
+        this.copyWithWarningText(
+          inputIndexXMLPageContentPath,
+          path.join(pageContentExportPath, 'index.xml')
+        );
+        logger.info(`Copied ${path.join(pageContentExportPath, 'index.xml')}`);
+      }
     } else if (existsSync(inputIndexMarkdownPagesPath)) {
-      ensureDirSync(pagesExportPath);
-      this.copyWithWarningText(inputIndexMarkdownPagesPath, path.join(pagesExportPath, 'index.md'));
-      logger.info(`Copied ${path.join(pagesExportPath, 'index.md')}`);
+      if (this.shouldCopyFiles) {
+        ensureDirSync(pagesExportPath);
+        this.copyWithWarningText(
+          inputIndexMarkdownPagesPath,
+          path.join(pagesExportPath, 'index.md')
+        );
+        logger.info(`Copied ${path.join(pagesExportPath, 'index.md')}`);
+      }
     } else if (existsSync(inputIndexXMLPagesPath)) {
-      ensureDirSync(pagesExportPath);
-      this.copyWithWarningText(inputIndexXMLPagesPath, path.join(pagesExportPath, 'index.xml'));
       generation = 'html';
-      logger.info(`Copied ${path.join(pagesExportPath, 'index.xml')}`);
+      if (this.shouldCopyFiles) {
+        ensureDirSync(pagesExportPath);
+        this.copyWithWarningText(inputIndexXMLPagesPath, path.join(pagesExportPath, 'index.xml'));
+        logger.info(`Copied ${path.join(pagesExportPath, 'index.xml')}`);
+      }
     } else {
       // do nothing -- no indexPageContent in config, no index file in ig-data
     }
@@ -403,10 +405,12 @@ export class IGExporter {
           // All user defined pages are included in input/${contentFolder}
           const pagePath = path.join(this.igDataPath, 'input', contentFolder, page.originalName);
 
-          this.copyWithWarningText(
-            pagePath,
-            path.join(igPath, 'input', contentFolder, `${page.name}.${page.fileType}`)
-          );
+          if (this.shouldCopyFiles) {
+            this.copyWithWarningText(
+              pagePath,
+              path.join(igPath, 'input', contentFolder, `${page.name}.${page.fileType}`)
+            );
+          }
           const isSupportedFileType = page.fileType === 'md' || page.fileType === 'xml';
           const isIntroOrNotesFile = page.name.endsWith('-intro') || page.name.endsWith('-notes');
           if (isSupportedFileType) {
@@ -550,18 +554,19 @@ export class IGExporter {
     const pageData = pages.map(page => {
       const nameParts = page.match(/^(\d+)_(.*)/);
       let prefix: number = null;
-      let name: string;
+      let nameWithoutPrefix: string;
+      const nameWithPrefix = page.slice(0, page.lastIndexOf('.'));
       if (nameParts == null) {
-        name = page.slice(0, page.lastIndexOf('.'));
+        nameWithoutPrefix = page.slice(0, page.lastIndexOf('.'));
       } else {
         prefix = parseInt(nameParts[1]);
-        name = nameParts[2].slice(0, nameParts[2].lastIndexOf('.'));
+        nameWithoutPrefix = nameParts[2].slice(0, nameParts[2].lastIndexOf('.'));
       }
       return {
         originalName: page,
         prefix: prefix,
-        name: name,
-        title: titleCase(words(name).join(' ')),
+        name: this.isIgPubContext ? nameWithPrefix : nameWithoutPrefix,
+        title: titleCase(words(nameWithoutPrefix).join(' ')),
         fileType: page.slice(page.lastIndexOf('.') + 1)
       };
     });
@@ -647,32 +652,53 @@ export class IGExporter {
 
   /**
    * Adds menu.xml
-   * A user can define a menu in config.yaml or provide one in ig-data/input/includes.
+   * A user can define a menu in sushi-config.yaml or provide one in ig-data/input/includes.
    * If neither is provided, the static one SUSHI provides will be used.
    *
    * @param {string} igPath - the path where the IG is exported to
    */
   addMenuXML(igPath: string): void {
     const menuXMLDefaultPath = path.join(this.igDataPath, 'input', 'includes', 'menu.xml');
-    const menuXMLOutputPath = path.join(igPath, 'input', 'includes', 'menu.xml');
+    const menuXMLOutputPath = this.isIgPubContext
+      ? path.join(igPath, 'fsh-generated', 'includes', 'menu.xml')
+      : path.join(igPath, 'input', 'includes', 'menu.xml');
 
     // If user provided menu file in input/includes and no config, copy over the file.
-    if (existsSync(menuXMLDefaultPath) && !this.config.menu) {
+    if (existsSync(menuXMLDefaultPath) && !this.config.menu && this.shouldCopyFiles) {
       this.copyWithWarningText(menuXMLDefaultPath, menuXMLOutputPath);
       return;
     }
 
     // If user provided file and config, log a warning but prefer the config.
     if (existsSync(menuXMLDefaultPath) && this.config.menu) {
+      const filePathString = path.join(
+        path.basename(this.igDataPath),
+        'input',
+        'includes',
+        'menu.xml'
+      );
+
+      let preferredFileMessage =
+        `Since a ${filePathString} file was found, the "menu" property in the ${this.configName} ` +
+        `will be ignored and a menu.xml file will not be generated. Remove the ${filePathString} ` +
+        `file to use the "menu" property in ${this.configName} to generate a menu.xml file instead.`;
+      if (this.shouldCopyFiles) {
+        preferredFileMessage =
+          `Since the "menu" property is present in the ${this.configName}, a menu.xml file will be generated and ` +
+          `the ${filePathString} file will be ignored. Remove the "menu" property in ${this.configName} ` +
+          `to use the ${filePathString} file instead.`;
+      }
       logger.warn(
-        `Found both a "menu" property in config.yaml and a menu.xml file at ig-data${path.sep}input${path.sep}includes${path.sep}menu.xml. ` +
-          'Since the "menu" property is present in the config.yaml, a menu.xml file will be generated and ' +
-          `the ig-data${path.sep}input${path.sep}includes${path.sep}menu.xml file will be ignored. Remove the "menu" property in config.yaml ` +
-          `to use the ig-data${path.sep}input${path.sep}includes${path.sep}menu.xml file instead.`,
+        `Found both a "menu" property in ${this.configName} and a menu.xml file at ${filePathString}. ` +
+          `${preferredFileMessage}`,
         {
           file: menuXMLDefaultPath
         }
       );
+
+      if (!this.shouldCopyFiles) {
+        return;
+      }
     }
 
     // Always use config menu if defined
@@ -683,12 +709,13 @@ export class IGExporter {
       });
       menu += '</ul>';
 
+      const filePathString = path.join(path.basename(this.igDataPath), 'input', 'includes');
       const warning = warningBlock(
         `<!-- ${path.parse(menuXMLOutputPath).base} {% comment %}`,
         '{% endcomment %} -->',
         [
-          'To change the contents of this file, edit the "menu" attribute in the tank config.yaml file',
-          'or provide your own menu.xml in the ig-data/input/includes folder'
+          `To change the contents of this file, edit the "menu" attribute in the tank ${this.configName} file`,
+          `or provide your own menu.xml in the ${filePathString} folder`
         ]
       );
       outputFileSync(menuXMLOutputPath, `${warning}${menu}`, 'utf8');
@@ -760,11 +787,28 @@ export class IGExporter {
    *
    * @param {string} igPath - the path where the IG is exported to
    */
-  private addIgnoreWarningsFile(igPath: string): void {
-    const ignorePath = path.join(this.igDataPath, 'input', 'ignoreWarnings.txt');
-    if (existsSync(ignorePath)) {
+  public addIgnoreWarningsFile(igPath: string): void {
+    // Only copy ignoreWarnings.txt for legacy configurations
+    if (!this.isIgPubContext) {
       const outputPath = path.join(igPath, 'input', 'ignoreWarnings.txt');
-      this.copyAsIs(ignorePath, outputPath);
+      const ignorePath = path.join(this.igDataPath, 'input', 'ignoreWarnings.txt');
+      if (existsSync(ignorePath)) {
+        this.copyAsIs(ignorePath, outputPath);
+      } else {
+        const ignoreWarningsContents = `== Suppressed Messages ==
+
+        # ********************************************************************************************************************************
+        # *                               WARNING: DO NOT EDIT THIS FILE                                                                 *
+        # *                                                                                                                              *
+        # * This file is generated by SUSHI. Any edits you make to this file will be overwritten.                                        *
+        # *                                                                                                                              *
+        # * Authors should create their own ignoreWarnings.txt file at ig-data/input/ignoreWarnings.txt.                                 *
+        # * If that file exists, SUSHI will use it instead of generating this one.                                                       *
+        # * See: https://confluence.hl7.org/pages/viewpage.action?pageId=66938614#ImplementationGuideParameters-ManagingWarningsandHints *
+        # ********************************************************************************************************************************`;
+        outputFileSync(outputPath, ignoreWarningsContents);
+        this.updateOutputLog(outputPath, [], 'generated');
+      }
     }
   }
 
@@ -974,14 +1018,15 @@ export class IGExporter {
               } else {
                 this.ig.definition.resource.push(newResource);
               }
-              const inputPath = path.join(dirPath, file);
-              const outputPath = path.join(
-                igPath,
-                'input',
-                pathEnd,
-                `${resourceJSON.resourceType}-${resourceJSON.id}${path.extname(file)}`
-              );
-              this.copyAsIs(inputPath, outputPath);
+
+              if (this.shouldCopyFiles) {
+                const inputPath = path.join(dirPath, file);
+                const outputFileName = `${resourceJSON.resourceType}-${
+                  resourceJSON.id
+                }${path.extname(file)}`;
+                const outputPath = path.join(igPath, 'input', pathEnd, outputFileName);
+                this.copyAsIs(inputPath, outputPath);
+              }
             }
           }
         }
@@ -1073,7 +1118,8 @@ export class IGExporter {
    * @param igPath {string} - the path where the IG is exported to
    */
   addImplementationGuide(igPath: string): void {
-    const igJsonPath = path.join(igPath, 'input', `ImplementationGuide-${this.ig.id}.json`);
+    const igJSONFolder = this.isIgPubContext ? path.join('fsh-generated', 'resources') : 'input';
+    const igJsonPath = path.join(igPath, igJSONFolder, `ImplementationGuide-${this.ig.id}.json`);
     outputJSONSync(igJsonPath, this.ig, { spaces: 2 });
     this.updateOutputLog(
       igJsonPath,
@@ -1084,8 +1130,8 @@ export class IGExporter {
   }
 
   /**
-   * Creates an ig.ini file based on the "template" in config.yaml and exports it to the IG folder.
-   * If the user specified an igi.ini file in the ig-data folder, and no "template" in config.yaml is specified,
+   * Creates an ig.ini file based on the "template" in sushi-config.yaml and exports it to the IG folder.
+   * If the user specified an igi.ini file in the ig-data folder, and no "template" in sushi-config.yaml is specified,
    * the file from ig-data is used instead.
    *
    * @param igPath {string} - the path where the IG is exported to
@@ -1093,18 +1139,46 @@ export class IGExporter {
   addIgIni(igPath: string): void {
     const inputIniPath = path.join(this.igDataPath, 'ig.ini');
     if (this.config.template != null) {
-      this.generateIgIni(igPath);
       if (existsSync(inputIniPath)) {
-        logger.warn(
-          `Found both a "template" property in config.yaml and an ig.ini file at ig-data${path.sep}ig.ini. ` +
-            'Since the "template" property is present in the config.yaml, an ig.ini file will be generated and ' +
-            `the ig-data${path.sep}ig.ini file will be ignored. Remove the "template" property in config.yaml ` +
-            `to use the ig-data${path.sep}ig.ini file instead.`,
-          {
-            file: inputIniPath
-          }
-        );
+        const filePathString = path.join(path.basename(this.igDataPath), 'ig.ini');
+        let message = `Found both a "template" property in ${this.configName} and an ig.ini file at ${filePathString}. `;
+        if (this.shouldCopyFiles) {
+          message +=
+            `Since the "template" property is present in the ${this.configName}, an ig.ini file will be generated and ` +
+            `the ${filePathString} file will be ignored. NOTE: Support for the "template" property in ${this.configName} will ` +
+            `be removed in a future version of SUSHI. Please remove the "template" property in ${this.configName} and ` +
+            'manage the ig.ini file directly.';
+          logger.warn(message, { file: inputIniPath });
+        } else {
+          message +=
+            `The "template" property in ${this.configName} has been deprecated and will be ignored. The existing ` +
+            `${filePathString} file will be used instead.  To resolve this error, remove the "template" property in ` +
+            `${this.configName} and manage the ig.ini file directly.`;
+          logger.error(message, { file: inputIniPath });
+        }
+      } else {
+        let message =
+          `The "template" property in ${this.configName} has been deprecated. Please remove the "template" ` +
+          `property in ${this.configName} and manage the ig.ini file directly.`;
+        if (this.shouldCopyFiles) {
+          message +=
+            ` Support for the "template" property in ${this.configName} will be removed in a future ` +
+            'version of SUSHI.';
+          logger.warn(message);
+        } else {
+          message += ` To resolve this error, create an ig.ini file in your project folder with the following contents:\n\n${this.generateIgIniString(
+            false
+          )}`;
+          logger.error(message);
+        }
       }
+
+      // Don't ever generate ig.ini in new IG Publisher project structure
+      if (!this.shouldCopyFiles) {
+        return;
+      }
+
+      this.generateIgIni(igPath);
     } else if (existsSync(inputIniPath)) {
       this.processIgIni(igPath, inputIniPath);
     } else {
@@ -1113,50 +1187,57 @@ export class IGExporter {
   }
 
   /**
-   * Generates an ig.ini file using the information in the configuration.
+   * Generates the contents of an ig.ini file using the information in the configuration.
    *
-   * @param igPath {string} - the path where the IG is exported to
+   * @returns {string} contents of ig.ini file
    */
-  generateIgIni(igPath: string): void {
+  generateIgIniString(includeWarning = true): string {
     // Create an ig.ini object from the configuration
     const iniObj: any = {};
-    iniObj.ig = `input/ImplementationGuide-${this.config.id}.json`;
+    const igFolder = this.isIgPubContext ? 'fsh-generated/resources' : 'input';
+    iniObj.ig = `${igFolder}/ImplementationGuide-${this.config.id}.json`;
     iniObj.template = this.config.template;
-    const comment = [
-      'This ig.ini was generated using the template property in config.yaml. To provide your own',
-      'ig.ini, create an ig.ini file in the ig-data folder with required properties: ig, template.',
-      'See: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root'
-    ];
-    const iniWarning = [...warningTextArray('; ', comment), ''].join(EOL);
-    let outputIniContents = ini
-      .encode(iniObj, { section: 'IG', whitespace: true })
-      .replace('\n', `\n${iniWarning}`);
+    let outputIniContents = ini.encode(iniObj, { section: 'IG', whitespace: true });
+    if (includeWarning) {
+      const comment = [
+        `This ig.ini was generated using the template property in ${this.configName}. To provide your own`,
+        `ig.ini, create an ig.ini file in the ${path.basename(
+          this.igDataPath
+        )} folder with required properties: ig, template.`,
+        'See: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root'
+      ];
+      const iniWarning = [...warningTextArray('; ', comment), ''].join(EOL);
+      outputIniContents = outputIniContents.replace('\n', `\n${iniWarning}`);
+    }
 
     // The encoder escapes '#' but FHIR doesn't like that, so if `#` is escaped in the template, then unescape it.
     outputIniContents = outputIniContents.replace(/^template\s*=\s*.*?(\\#.+)?$/m, ($0, $1) =>
       $1 ? $0.replace($1, $1.slice(1)) : $0
     );
 
-    // Write the ig.ini to disk
+    return outputIniContents;
+  }
+
+  /**
+   * Generates an ig.ini file using the information in the configuration.
+   *
+   * @param igPath {string} - the path where the IG is exported to
+   */
+  generateIgIni(igPath: string): void {
     const outputPath = path.join(igPath, 'ig.ini');
-    outputFileSync(outputPath, outputIniContents);
+    outputFileSync(outputPath, this.generateIgIniString());
     this.updateOutputLog(outputPath, [this.configPath], 'generated');
     logger.info('Generated ig.ini.');
   }
 
   /**
-   * Process an existing ig.ini. This may involve merging in default values when one of the required properties
-   * is missing.  If required properties are present, the file will be copied as-is.
+   * Process an existing ig.ini. The file will always be copied as-is.
+   * When one of the required properties is missing, an error is logged but the file is copied as-is.
    *
    * @param igPath {string} - the path where the IG is exported to
    * @param inputIniPath {string} - the path to the input ig.ini file
    */
   processIgIni(igPath: string, inputIniPath: string): void {
-    // Since we used to merge ig.ini, we did not allow the ig property -- so it may be missing for existing users.
-    // Similarly, we merged in the template property if it was missing, so that may be missing as well. In these
-    // cases, we should add them to the copied file (to be friendly) but also issue a warning that the user should
-    // update their ig.ini to include them.
-    let modified = false;
     let inputIniContents = readFileSync(inputIniPath, 'utf8');
     // FHIR allows templates to have versions identified using #.  E.g.,
     //   template = hl7.fhir.template#0.1.0
@@ -1170,34 +1251,31 @@ export class IGExporter {
         file: inputIniPath
       });
     }
+    const filePathString = path.join(path.basename(this.igDataPath), 'ig.ini');
     if (inputIni.IG) {
       if (inputIni.IG.ig == null) {
         const igValue = `input/ImplementationGuide-${this.config.id}.json`;
         inputIni.IG.ig = igValue;
-        logger.warn(
-          'The ig.ini file must have an "ig" property pointing to the IG file. SUSHI has added the following line ' +
-            'to the copied version of the ig.ini:\n' +
-            `ig = ${igValue}\n` +
-            `Please update ig-data${path.sep}ig.ini to include this value.`,
+        logger.error(
+          'The ig.ini file must have an "ig" property pointing to the IG file. Please add the following line ' +
+            `to ${filePathString}:\n'` +
+            `ig = ${igValue}`,
           {
             file: inputIniPath
           }
         );
-        modified = true;
       }
       if (inputIni.IG.template == null) {
         const templateValue = 'fhir.base.template';
         inputIni.IG.template = templateValue;
-        logger.warn(
-          'The ig.ini file must have a "template" property. SUSHI has added the following line to the copied ' +
-            'version of the ig.ini:\n' +
-            `template = ${templateValue}\n` +
-            `Please update ig-data${path.sep}ig.ini to include your desired template value.`,
+        logger.error(
+          `The ig.ini file must have a "template" property. Please update ${filePathString} to include ` +
+            'your desired template value. Consider adding the following line:\n' +
+            `template = ${templateValue}`,
           {
             file: inputIniPath
           }
         );
-        modified = true;
       }
       const deprecatedProps = [
         'copyrightyear',
@@ -1213,7 +1291,7 @@ export class IGExporter {
       if (deprecatedProps.length > 0) {
         const propList = deprecatedProps.join(', ');
         logger.warn(
-          `Your ig-data${path.sep}ig.ini file contains the following deprecated properties: ${propList}. ` +
+          `Your ${filePathString} file contains the following deprecated properties: ${propList}. ` +
             'These are no longer supported in ig.ini and should be removed.  See the following link for details: ' +
             'https://github.com/HL7/ig-template-base/releases/tag/0.0.2',
           {
@@ -1223,40 +1301,22 @@ export class IGExporter {
       }
     }
 
-    let outputIniContents;
-    if (modified) {
-      // Generate a merged version of the ig.ini
-      outputIniContents = ini.encode(inputIni.IG, { section: 'IG', whitespace: true });
-      // Now we need to do the reverse of what we did before.  If `#` is escaped, then unescape it.
-      outputIniContents = outputIniContents.replace(/^template\s*=\s*.*?(\\#.+)?$/m, ($0, $1) =>
-        $1 ? $0.replace($1, $1.slice(1)) : $0
-      );
-      const comment = [
-        'This ig.ini was generated by merging in required properties: "ig" and/or "template". Review your ',
-        `original ig-data${path.sep}ig.ini file and add the missing required properties.`
-      ];
-      const iniWarning = [...warningTextArray('; ', comment), ''].join(EOL);
-      outputIniContents = outputIniContents.replace('\n', `\n${iniWarning}`);
+    // Copy the ig.ini as-is with an embedded comment
+    let outputIniContents = inputIniContents;
+    // Now we need to do the reverse of what we did before.  If `#` is escaped, then unescape it.
+    outputIniContents = outputIniContents.replace(/^template\s*=\s*.*?(\\#.+)?$/m, ($0, $1) =>
+      $1 ? $0.replace($1, $1.slice(1)) : $0
+    );
+    const comment = [
+      'To change the contents of this file, edit the original source file at:',
+      `${filePathString}`
+    ];
+    const iniWarning = [...warningTextArray('; ', comment), ''].join(EOL);
+    outputIniContents = outputIniContents.replace('\n', `\n${iniWarning}`);
 
-      // Finally, write it to disk
-      const outputPath = path.join(igPath, 'ig.ini');
-      outputFileSync(outputPath, outputIniContents);
-      this.updateOutputLog(outputPath, [this.configPath, inputIniPath], 'generated');
-      logger.info(`Merged ig-data${path.sep}ig.ini w/ default values for missing properties.`);
-    } else {
-      // Copy the ig.ini as-is with an embedded comment
-      outputIniContents = inputIniContents;
-      // Now we need to do the reverse of what we did before.  If `#` is escaped, then unescape it.
-      outputIniContents = outputIniContents.replace(/^template\s*=\s*.*?(\\#.+)?$/m, ($0, $1) =>
-        $1 ? $0.replace($1, $1.slice(1)) : $0
-      );
-      const comment = [
-        'To change the contents of this file, edit the original source file at:',
-        `ig-data${path.sep}ig.ini`
-      ];
-      const iniWarning = [...warningTextArray('; ', comment), ''].join(EOL);
-      outputIniContents = outputIniContents.replace('\n', `\n${iniWarning}`);
-      const outputPath = path.join(igPath, 'ig.ini');
+    // Finally, write it to disk
+    const outputPath = path.join(igPath, 'ig.ini');
+    if (this.shouldCopyFiles) {
       outputFileSync(outputPath, outputIniContents);
       this.updateOutputLogForCopiedPath(outputPath, inputIniPath);
       logger.info('Copied ig.ini.');
@@ -1272,26 +1332,39 @@ export class IGExporter {
   addPackageList(igPath: string): void {
     const packageListPath = path.join(this.igDataPath, 'package-list.json');
     const isIgDataPackageList = existsSync(packageListPath);
+    const filePathString = path.join(path.basename(this.igDataPath), 'package-list.json');
 
     if (this.config.history) {
+      if (this.isIgPubContext) {
+        logger.error(
+          'Detected "history" property in configuration. The use of "history" is deprecated. Please remove the property and provide a package-list.json directly. ' +
+            'The package-list.json corresponding to the "history" property in the configuration is:\n\n' +
+            JSON.stringify(this.config.history, null, 2) +
+            '\n'
+        );
+        return;
+      }
+      logger.warn(
+        'Use of "history" property in configuration is deprecated and will be removed in a future version. Please remove the property and provide a package-list.json directly.'
+      );
       const outputPath = path.join(igPath, 'package-list.json');
       outputJSONSync(outputPath, this.config.history, { spaces: 2 });
       logger.info('Generated package-list.json');
       this.updateOutputLog(outputPath, [this.configPath], 'generated');
       if (isIgDataPackageList) {
         logger.warn(
-          `Found both a "history" property in config.yaml and a package-list.json file at ig-data${path.sep}package-list.json. ` +
-            'Since the "history" property is present in the config.yaml, a package-list.json file will be generated and ' +
-            `the ig-data${path.sep}package-list.json file will be ignored. Remove the "history" property in config.yaml ` +
-            `to use the ig-data${path.sep}package-list.json file instead.`,
+          `Found both a "history" property in ${this.configName} and a package-list.json file at ${filePathString}. ` +
+            `Since the "history" property is present in the ${this.configName}, a package-list.json file will be generated and ` +
+            `the ${filePathString} file will be ignored. Remove the "history" property in ${this.configName} ` +
+            `to use the ${filePathString} file instead.`,
           {
             file: packageListPath
           }
         );
       }
-    } else if (isIgDataPackageList) {
+    } else if (isIgDataPackageList && this.shouldCopyFiles) {
       this.copyAsIs(packageListPath, path.join(igPath, 'package-list.json'));
-      logger.info('Copied ig-data/package-list.json.');
+      logger.info(`Copied ${filePathString}.`);
     }
   }
 
@@ -1302,6 +1375,10 @@ export class IGExporter {
    * @param igPath {string} - the path where the IG is exported to
    */
   private addOutputLog(igPath: string): void {
+    // If we are in the new context, all output goes into fsh-generated, so no point having SUSHI-GENERATED-FILES
+    if (this.isIgPubContext) {
+      return;
+    }
     const intro = [
       '# SUSHI-GENERATED FILES #',
       '',
@@ -1312,7 +1389,9 @@ export class IGExporter {
       'files.',
       '',
       'NOTE: This file does not currently list the FHIR resources and examples generated from .fsh files. It only',
-      'lists those files generated from project configs or the contents in the ig-data folder.',
+      `lists those files generated from project configs or the contents in the ${path.basename(
+        this.igDataPath
+      )} folder.`,
       '',
       ''
     ].join(EOL);
@@ -1350,6 +1429,11 @@ export class IGExporter {
       return;
     }
 
+    // Don't copy to the same directory it came from
+    const fullInputPath = path.resolve(inputPath);
+    const fullOutputPath = path.resolve(outputPath);
+    if (fullInputPath === fullOutputPath) return;
+
     copySync(inputPath, outputPath, { filter });
     this.updateOutputLogForCopiedPath(outputPath, inputPath, filter);
   }
@@ -1393,7 +1477,9 @@ export class IGExporter {
     // Otherwise, it's .md or .xml
     const extra = [
       'To change the contents of this file, edit the original source file at:',
-      inputPath.slice(inputPath.indexOf(`${path.sep}ig-data${path.sep}`) + 1)
+      inputPath.slice(
+        inputPath.indexOf(`${path.sep}${path.basename(this.igDataPath)}${path.sep}`) + 1
+      )
     ];
     // .xml files can't have bare jekyll comments at the start of the file, as they fail XML parsing,
     // so we must surround the warning w/ XML comments.  To avoid the final HTML having just an empty
diff --git a/src/ig/files/_gencontinuous.bat b/src/ig/files/_gencontinuous.bat
deleted file mode 100755
index d943435..0000000
--- a/src/ig/files/_gencontinuous.bat
+++ /dev/null
@@ -1,8 +0,0 @@
-@ECHO OFF
-REM ***********************************************************************************************
-REM *                              WARNING: DO NOT EDIT THIS FILE                                 *
-REM *                                                                                             *
-REM * This file is generated by SUSHI. Any edits you make to this file will be overwritten.       *
-REM ***********************************************************************************************
-
-CALL ./_genonce.bat -watch
\ No newline at end of file
diff --git a/src/ig/files/_gencontinuous.sh b/src/ig/files/_gencontinuous.sh
deleted file mode 100755
index 1413db6..0000000
--- a/src/ig/files/_gencontinuous.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-# *************************************************************************************************
-# *                               WARNING: DO NOT EDIT THIS FILE                                  *
-# *                                                                                               *
-# * This file is generated by SUSHI. Any edits you make to this file will be overwritten.         *
-# *************************************************************************************************
-
-./_genonce.sh -watch
\ No newline at end of file
diff --git a/src/ig/files/_genonce.bat b/src/ig/files/_genonce.bat
deleted file mode 100644
index 4df343c..0000000
--- a/src/ig/files/_genonce.bat
+++ /dev/null
@@ -1,33 +0,0 @@
-@ECHO OFF
-REM ***********************************************************************************************
-REM *                              WARNING: DO NOT EDIT THIS FILE                                 *
-REM *                                                                                             *
-REM * This file is generated by SUSHI. Any edits you make to this file will be overwritten.       *
-REM ***********************************************************************************************
-
-SET publisher_jar=publisher.jar
-SET input_cache_path=%CD%\input-cache
-
-ECHO Checking internet connection...
-PING tx.fhir.org -4 -n 1 -w 1000 | FINDSTR TTL && GOTO isonline
-ECHO We're offline...
-SET txoption=-tx n/a
-GOTO igpublish
-
-:isonline
-ECHO We're online
-SET txoption=
-
-:igpublish
-
-SET JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8
-
-IF EXIST "%input_cache_path%\%publisher_jar%" (
-	JAVA -jar "%input_cache_path%\%publisher_jar%" -ig ig.ini %txoption% %*
-) ELSE If exist "..\%publisher_jar%" (
-	JAVA -jar "..\%publisher_jar%" -ig ig.ini %txoption% %*
-) ELSE (
-	ECHO IG Publisher NOT FOUND in input-cache or parent folder.  Please run _updatePublisher.  Aborting...
-)
-
-PAUSE
\ No newline at end of file
diff --git a/src/ig/files/_genonce.sh b/src/ig/files/_genonce.sh
deleted file mode 100755
index 4dafa8e..0000000
--- a/src/ig/files/_genonce.sh
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/bin/bash
-
-# *************************************************************************************************
-# *                               WARNING: DO NOT EDIT THIS FILE                                  *
-# *                                                                                               *
-# * This file is generated by SUSHI. Any edits you make to this file will be overwritten.         *
-# *************************************************************************************************
-
-publisher_jar=publisher.jar
-input_cache_path=./input-cache/
-echo Checking internet connection...
-curl -sSf tx.fhir.org > /dev/null
-
-if [ $? -eq 0 ]; then
-	echo "Online"
-	txoption=""
-else
-	echo "Offline"
-	txoption="-tx n/a"
-fi
-
-echo "$txoption"
-
-publisher=$input_cache_path/$publisher_jar
-if test -f "$publisher"; then
-	java -jar $publisher -ig ig.ini $txoption $*
-
-else
-	publisher=../$publisher_jar
-	if test -f "$publisher"; then
-		java -jar $publisher -ig ig.ini $txoption $*
-	else
-		echo IG Publisher NOT FOUND in input-cache or parent folder.  Please run _updatePublisher.  Aborting...
-	fi
-fi
\ No newline at end of file
diff --git a/src/ig/files/_updatePublisher.bat b/src/ig/files/_updatePublisher.bat
deleted file mode 100644
index 0a0eddc..0000000
--- a/src/ig/files/_updatePublisher.bat
+++ /dev/null
@@ -1,223 +0,0 @@
-@ECHO OFF
-REM ***********************************************************************************************
-REM *                              WARNING: DO NOT EDIT THIS FILE                                 *
-REM *                                                                                             *
-REM * This file is generated by SUSHI. Any edits you make to this file will be overwritten.       *
-REM ***********************************************************************************************
-
-SETLOCAL
-
-SET dlurl=https://github.com/HL7/fhir-ig-publisher/releases/latest/download/publisher.jar
-SET publisher_jar=publisher.jar
-SET input_cache_path=%CD%\input-cache\
-SET skipPrompts=false
-
-set update_bat_url=https://raw.githubusercontent.com/FHIR/sample-ig/master/_updatePublisher.bat
-set gen_bat_url=https://raw.githubusercontent.com/FHIR/sample-ig/master/_genonce.bat
-set gencont_bat_url=https://raw.githubusercontent.com/FHIR/sample-ig/master/_gencontinuous.bat
-set gencont_sh_url=https://raw.githubusercontent.com/FHIR/sample-ig/master/_gencontinuous.sh
-set gen_sh_url=https://raw.githubusercontent.com/FHIR/sample-ig/master/_genonce.sh
-set update_sh_url=https://raw.githubusercontent.com/FHIR/sample-ig/master/_updatePublisher.sh
-
-IF "%~1"=="/f" SET skipPrompts=true
-
-
-ECHO.
-ECHO Checking internet connection...
-PING tx.fhir.org -4 -n 1 -w 1000 | FINDSTR TTL && GOTO isonline
-ECHO We're offline, nothing to do...
-GOTO end
-
-:isonline
-ECHO We're online
-
-
-:processflags
-SET ARG=%1
-IF DEFINED ARG (
-	IF "%ARG%"=="-f" SET FORCE=true
-	IF "%ARG%"=="--force" SET FORCE=true
-	SHIFT
-	GOTO processflags
-)
-
-FOR %%x IN ("%CD%") DO SET upper_path=%%~dpx
-
-ECHO.
-IF NOT EXIST "%input_cache_path%%publisher_jar%" (
-	IF NOT EXIST "%upper_path%%publisher_jar%" (
-		SET jarlocation="%input_cache_path%%publisher_jar%"
-		SET jarlocationname=Input Cache
-		ECHO IG Publisher is not yet in input-cache or parent folder.
-		REM we don't use jarlocation below because it will be empty because we're in a bracketed if statement
-		GOTO create
-	) ELSE (
-		ECHO IG Publisher FOUND in parent folder
-		SET jarlocation="%upper_path%%publisher_jar%"
-		SET jarlocationname=Parent folder
-		GOTO upgrade
-	)
-) ELSE (
-	ECHO IG Publisher FOUND in input-cache
-	SET jarlocation="%input_cache_path%%publisher_jar%"
-	SET jarlocationname=Input Cache
-	GOTO upgrade
-)
-
-:create
-IF DEFINED FORCE (
-	MKDIR "%input_cache_path%" 2> NUL
-	GOTO download
-)
-
-IF "%skipPrompts%"=="true" (
-	SET create="Y"
-) ELSE (
-	SET /p create="Ok? (Y/N) "
-)
-IF /I "%create%"=="Y" (
-    ECHO Will place publisher jar here: %input_cache_path%%publisher_jar%
-	MKDIR "%input_cache_path%" 2> NUL
-	GOTO download
-)
-GOTO done
-
-:upgrade
-IF "%skipPrompts%"=="true" (
-	SET overwrite="Y"
-) ELSE (
-	SET /p overwrite="Overwrite %jarlocation%? (Y/N) "
-)
-
-IF /I "%overwrite%"=="Y" (
-	GOTO download
-)
-GOTO done
-
-:download
-ECHO Downloading most recent publisher to %jarlocationname% - it's ~100 MB, so this may take a bit
-
-FOR /f "tokens=4-5 delims=. " %%i IN ('ver') DO SET VERSION=%%i.%%j
-IF "%version%" == "10.0" GOTO win10
-IF "%version%" == "6.3" GOTO win8.1
-IF "%version%" == "6.2" GOTO win8
-IF "%version%" == "6.1" GOTO win7
-IF "%version%" == "6.0" GOTO vista
-
-ECHO Unrecognized version: %version%
-GOTO done
-
-:win10
-CALL POWERSHELL -command if ('System.Net.WebClient' -as [type]) {(new-object System.Net.WebClient).DownloadFile(\"%dlurl%\",\"%jarlocation%\") } else { Invoke-WebRequest -Uri "%dlurl%" -Outfile "%jarlocation%" }
-
-GOTO done
-
-:win7
-rem this may be triggering the antivirus - bitsadmin.exe is a known threat
-rem CALL bitsadmin /transfer GetPublisher /download /priority normal "%dlurl%" "%jarlocation%"
-
-rem this didn't work in win 10
-rem CALL Start-BitsTransfer /priority normal "%dlurl%" "%jarlocation%"
-
-rem this should work - untested
-call (New-Object Net.WebClient).DownloadFile('%dlurl%', '%jarlocation%')
-GOTO done
-
-:win8.1
-:win8
-:vista
-GOTO done
-
-
-
-:done
-
-
-
-
-ECHO.
-ECHO Updating scripts
-IF "%skipPrompts%"=="true" (
-	SET updateScripts="Y"
-) ELSE (
-	SET /p updateScripts="Update scripts? (Y/N) "
-)
-IF /I "%updateScripts%"=="Y" (
-	GOTO scripts
-)
-GOTO end
-
-
-:scripts
-
-REM Download all batch files (and this one with a new name)
-
-SETLOCAL DisableDelayedExpansion
-
-
-
-:dl_script_1
-ECHO Updating _updatePublisher.sh
-call POWERSHELL -command if ('System.Net.WebClient' -as [type]) {(new-object System.Net.WebClient).DownloadFile(\"%update_sh_url%\",\"_updatePublisher.new.sh\") } else { Invoke-WebRequest -Uri "%update_sh_url%" -Outfile "_updatePublisher.new.sh" }
-if %ERRORLEVEL% == 0 goto upd_script_1
-echo "Errors encountered during download: %errorlevel%"
-goto dl_script_2
-:upd_script_1
-start copy /y "_updatePublisher.new.sh" "_updatePublisher.sh" ^&^& del "_updatePublisher.new.sh" ^&^& exit
-
-
-:dl_script_2
-ECHO Updating _genonce.bat
-call POWERSHELL -command if ('System.Net.WebClient' -as [type]) {(new-object System.Net.WebClient).DownloadFile(\"%gen_bat_url%\",\"_genonce.new.bat\") } else { Invoke-WebRequest -Uri "%gen_bat_url%" -Outfile "_genonce.bat" }
-if %ERRORLEVEL% == 0 goto upd_script_2
-echo "Errors encountered during download: %errorlevel%"
-goto dl_script_3
-:upd_script_2
-start copy /y "_genonce.new.bat" "_genonce.bat" ^&^& del "_genonce.new.bat" ^&^& exit
-
-:dl_script_3
-ECHO Updating _gencontinuous.bat
-call POWERSHELL -command if ('System.Net.WebClient' -as [type]) {(new-object System.Net.WebClient).DownloadFile(\"%gencont_bat_url%\",\"_gencontinuous.new.bat\") } else { Invoke-WebRequest -Uri "%gencont_bat_url%" -Outfile "_gencontinuous.bat" }
-if %ERRORLEVEL% == 0 goto upd_script_3
-echo "Errors encountered during download: %errorlevel%"
-goto dl_script_4
-:upd_script_3
-start copy /y "_gencontinuous.new.bat" "_gencontinuous.bat" ^&^& del "_gencontinuous.new.bat" ^&^& exit
-
-
-:dl_script_4
-ECHO Updating _genonce.sh
-call POWERSHELL -command if ('System.Net.WebClient' -as [type]) {(new-object System.Net.WebClient).DownloadFile(\"%gen_sh_url%\",\"_genonce.new.sh\") } else { Invoke-WebRequest -Uri "%gen_sh_url%" -Outfile "_genonce.sh" }
-if %ERRORLEVEL% == 0 goto upd_script_4
-echo "Errors encountered during download: %errorlevel%"
-goto dl_script_5
-:upd_script_4
-start copy /y "_genonce.new.sh" "_genonce.sh" ^&^& del "_genonce.new.sh" ^&^& exit
-
-:dl_script_5
-ECHO Updating _gencontinuous.sh
-call POWERSHELL -command if ('System.Net.WebClient' -as [type]) {(new-object System.Net.WebClient).DownloadFile(\"%gencont_sh_url%\",\"_gencontinuous.new.sh\") } else { Invoke-WebRequest -Uri "%gencont_sh_url%" -Outfile "_gencontinuous.sh" }
-if %ERRORLEVEL% == 0 goto upd_script_5
-echo "Errors encountered during download: %errorlevel%"
-goto dl_script_6
-:upd_script_5
-start copy /y "_gencontinuous.new.sh" "_gencontinuous.sh" ^&^& del "_gencontinuous.new.sh" ^&^& exit
-
-
-
-:dl_script_6
-ECHO Updating _updatePublisher.bat
-call POWERSHELL -command if ('System.Net.WebClient' -as [type]) {(new-object System.Net.WebClient).DownloadFile(\"%update_bat_url%\",\"_updatePublisher.new.bat\") } else { Invoke-WebRequest -Uri "%update_bat_url%" -Outfile "_updatePublisher.new.bat" }
-if %ERRORLEVEL% == 0 goto upd_script_6
-echo "Errors encountered during download: %errorlevel%"
-goto end
-:upd_script_6
-start copy /y "_updatePublisher.new.bat" "_updatePublisher.bat" ^&^& del "_updatePublisher.new.bat" ^&^& exit
-
-
-:end
-
-
-IF "%skipPrompts%"=="true" (
-  PAUSE
-}
\ No newline at end of file
diff --git a/src/ig/files/_updatePublisher.sh b/src/ig/files/_updatePublisher.sh
deleted file mode 100755
index 58a2abf..0000000
--- a/src/ig/files/_updatePublisher.sh
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/bin/bash
-
-# *************************************************************************************************
-# *                               WARNING: DO NOT EDIT THIS FILE                                  *
-# *                                                                                               *
-# * This file is generated by SUSHI. Any edits you make to this file will be overwritten.         *
-# *************************************************************************************************
-
-dlurl=https://github.com/HL7/fhir-ig-publisher/releases/latest/download/publisher.jar
-publisher_jar=publisher.jar
-input_cache_path=./input-cache/
-
-set -e
-if ! type "curl" > /dev/null; then
-	echo "ERROR: Script needs curl to download latest IG Publisher. Please install curl."
-	exit 1
-fi
-
-FORCE=false
-
-while :; do
-    case $1 in
-        -f|--force) FORCE=true ;;
-        --)
-            shift
-            break
-            ;;
-        *) break
-    esac
-    shift
-done
-
-publisher="$input_cache_path$publisher_jar"
-if test -f "$publisher"; then
-	echo "IG Publisher FOUND in input-cache"
-	jarlocation="$publisher"
-	jarlocationname="Input Cache"
-	upgrade=true
-else
-	publisher="../$publisher_jar"
-	upgrade=true
-	if test -f "$publisher"; then
-		echo "IG Publisher FOUND in parent folder"
-		jarlocation="$publisher"
-		jarlocationname="Parent Folder"
-		upgrade=true
-	else
-		echo IG Publisher NOT FOUND in input-cache or parent folder...
-		jarlocation=$input_cache_path$publisher_jar
-		jarlocationname="Input Cache"
-		upgrade=false
-	fi
-fi
-
-if [[ "$FORCE" != true ]]; then
-  if "$upgrade"; then
-    message="Overwrite $jarlocation? (Y/N) "
-  else
-    echo Will place publisher jar here: "$jarlocation"
-    message="Ok (enter 'y' or 'Y' to continue, any other key to cancel)?"
-  fi
-  read -r -p "$message" response
-fi
-
-if [[ "$FORCE" == true ]] || [[ "$response" =~ ^([yY])$ ]]; then
-	echo "Downloading most recent publisher to $jarlocationname - it's ~100 MB, so this may take a bit"
-#	wget "https://fhir.github.io/latest-ig-publisher/org.hl7.fhir.publisher.jar" -O "$jarlocation"
-	curl -L $dlurl -o "$jarlocation" --create-dirs
-else
-	echo cancel...
-fi
diff --git a/src/ig/files/input/ignoreWarnings.txt b/src/ig/files/input/ignoreWarnings.txt
deleted file mode 100644
index b5a3dca..0000000
--- a/src/ig/files/input/ignoreWarnings.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-== Suppressed Messages ==
-
-# ********************************************************************************************************************************
-# *                               WARNING: DO NOT EDIT THIS FILE                                                                 *
-# *                                                                                                                              *
-# * This file is generated by SUSHI. Any edits you make to this file will be overwritten.                                        *
-# *                                                                                                                              *
-# * Authors should create their own ignoreWarnings.txt file at ig-data/input/ignoreWarnings.txt.                                 *
-# * If that file exists, SUSHI will use it instead of generating this one.                                                       *
-# * See: https://confluence.hl7.org/pages/viewpage.action?pageId=66938614#ImplementationGuideParameters-ManagingWarningsandHints *
-# ********************************************************************************************************************************
\ No newline at end of file
diff --git a/src/import/ensureConfigurationFile.ts b/src/import/ensureConfigurationFile.ts
index 66045dd..27e3889 100644
--- a/src/import/ensureConfigurationFile.ts
+++ b/src/import/ensureConfigurationFile.ts
@@ -50,19 +50,29 @@ class CommentPair extends YAMLPair {
 }
 
 /**
- * Checks for a config.yaml or config.yml file. If it finds one, it returns its path, otherwise it creates one
+ * Checks for a sushi-config.yaml or sushi-config.yml file. If it finds one, it returns its path, otherwise it creates one
  * and then returns the new file's path.
  * @param root - the root path of the FSH Tank
  * @param allowFromScratch - create a config file from scratch even if package.json isn't found
  * @returns {string|undefined} path to the config file or undefined if it couldn't find or create one
  */
-export function ensureConfigurationFile(root: string, allowFromScratch = false): string {
-  const configPath = [path.join(root, 'config.yaml'), path.join(root, 'config.yml')].find(
-    fs.existsSync
-  );
+export function ensureConfiguration(root: string, allowFromScratch = false): string {
+  const configPath = [
+    path.join(root, 'sushi-config.yaml'),
+    path.join(root, 'sushi-config.yml'),
+    path.join(root, 'config.yaml'),
+    path.join(root, 'config.yml')
+  ].find(fs.existsSync);
   if (configPath) {
     // The config already exists, so return it
     logger.info(`Using configuration file: ${path.resolve(configPath)}`);
+    if (!path.basename(configPath).match(/sushi/)) {
+      logger.warn(
+        `Use of ${path.basename(
+          configPath
+        )} is deprecated and will be removed in a future release. Please rename configuration file to "sushi-config.yaml".`
+      );
+    }
     return configPath;
   }
 
@@ -71,7 +81,7 @@ export function ensureConfigurationFile(root: string, allowFromScratch = false):
 }
 
 /**
- * Creates a new config.yaml file using available other configs (package.json, ig.ini, package-list.json, menu.xml)
+ * Creates a new sushi-config.yaml file using available other configs (package.json, ig.ini, package-list.json, menu.xml)
  * @param root - the root path of the FSH Tank
  * @param allowFromScratch - create a config file from scratch even if package.json isn't found
  * @returns {string|undefined} path to the config file or undefined if it couldn't find or create one
@@ -90,7 +100,7 @@ function generateConfiguration(root: string, allowFromScratch: boolean): string
   }
 
   // Create the new YAML document
-  const configPath = path.join(root, 'config.yaml');
+  const configPath = path.join(root, 'sushi-config.yaml');
   const doc = new Document();
   const contents = new YAMLMap();
   // @ts-ignore See: https://github.com/eemeli/yaml/issues/156
@@ -371,7 +381,7 @@ function generateConfiguration(root: string, allowFromScratch: boolean): string
   }
 
   // FSHOnly - in SUSHI 0.12.x, presence or absence of ig-data indicated if you wanted an IG or not.
-  // But don't ever set FSHOnly when creating config.yaml "from scratch".
+  // But don't ever set FSHOnly when creating sushi-config.yaml "from scratch".
   if (!fs.existsSync(path.join(root, 'ig-data')) && !isFromScratch) {
     setPairs.push(new YAMLPair('FSHOnly', true));
   }
diff --git a/src/utils/Processing.ts b/src/utils/Processing.ts
index ed2682a..9f1b4e1 100644
--- a/src/utils/Processing.ts
+++ b/src/utils/Processing.ts
@@ -1,16 +1,11 @@
+import axios from 'axios';
 import path from 'path';
 import fs from 'fs-extra';
 import readlineSync from 'readline-sync';
 import { logger } from './FSHLogger';
 import { loadDependency } from '../fhirdefs/load';
 import { FHIRDefinitions } from '../fhirdefs';
-import {
-  FSHTank,
-  RawFSH,
-  importText,
-  ensureConfigurationFile,
-  importConfiguration
-} from '../import';
+import { FSHTank, RawFSH, importText, ensureConfiguration, importConfiguration } from '../import';
 import { cloneDeep, padEnd } from 'lodash';
 import YAML from 'yaml';
 import { Package } from '../export';
@@ -34,39 +29,95 @@ export function findInputDir(input: string): string {
     logger.info('path-to-fsh-defs defaulted to current working directory');
   }
 
+  const originalInput = input;
+
+  // TODO: Legacy support. Remove when no longer supported.
+  // Use input/fsh/ subdirectory if not already specified and present
+  const inputFshSubdirectoryPath = path.join(originalInput, 'input', 'fsh');
+  if (fs.existsSync(inputFshSubdirectoryPath)) {
+    input = path.join(originalInput, 'input', 'fsh');
+  }
+
   // Use fsh/ subdirectory if not already specified and present
-  const fshSubdirectoryPath = path.join(input, 'fsh');
-  if (fs.existsSync(fshSubdirectoryPath)) {
-    input = path.join(input, 'fsh');
+  const fshSubdirectoryPath = path.join(originalInput, 'fsh');
+  if (!fs.existsSync(inputFshSubdirectoryPath)) {
+    let msg =
+      '\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n';
+    if (fs.existsSync(fshSubdirectoryPath)) {
+      input = path.join(input, 'fsh');
+      msg +=
+        '\nSUSHI detected a "fsh" directory that will be used in the input path.\n' +
+        'Use of this folder is DEPRECATED and will be REMOVED in a future release.\n' +
+        'To migrate to the new folder structure, make the following changes:\n' +
+        `  - move fsh${path.sep}config.yaml to .${path.sep}sushi-config.yaml\n` +
+        `  - move fsh${path.sep}*.fsh files to .${path.sep}input${path.sep}fsh${path.sep}*.fsh\n`;
+      if (fs.existsSync(path.join(input, 'ig-data'))) {
+        msg += `  - move fsh${path.sep}ig-data${path.sep}* files and folders to .${path.sep}*\n`;
+      }
+    } else {
+      msg +=
+        '\nSUSHI has adopted a new folder structure for FSH tanks (a.k.a. SUSHI projects).\n' +
+        'Support for other folder structures is DEPRECATED and will be REMOVED in a future release.\n' +
+        'To migrate to the new folder structure, make the following changes:\n' +
+        `  - rename .${path.sep}config.yaml to .${path.sep}sushi-config.yaml\n` +
+        `  - move .${path.sep}*.fsh files to .${path.sep}input${path.sep}fsh${path.sep}*.fsh\n`;
+      if (fs.existsSync(path.join(input, 'ig-data'))) {
+        msg += `  - move .${path.sep}ig-data${path.sep}* files and folders to .${path.sep}*\n`;
+      }
+    }
+    if (!fs.existsSync(path.join(input, 'ig-data', 'ig.ini'))) {
+      msg += `  - if you used the "template" property in your config, remove it and manage .${path.sep}ig.ini directly\n`;
+    }
+    if (!fs.existsSync(path.join(input, 'ig-data', 'package-list.json'))) {
+      msg += `  - if you used the "history" property in your config, remove it and manage .${path.sep}package-list.json directly\n`;
+    }
+    msg +=
+      '  - ensure your .gitignore file is not configured to ignore the sources in their new locations\n' +
+      '  - add /fsh-generated to your .gitignore file to prevent SUSHI output from being checked into source control\n\n' +
+      `NOTE: After you make these changes, the default ouput folder for SUSHI will change to .${path.sep}fsh-generated.\n\n`;
+    logger.warn(msg);
   }
   return input;
 }
 
-export function ensureOutputDir(input: string, output: string, isIgPubContext: boolean): string {
-  if (isIgPubContext) {
-    logger.info(
-      'SUSHI detected a "fsh" directory in the input path. As a result, SUSHI will operate in "IG Publisher integration" mode. This means:\n' +
-        '  - the "fsh" directory will be used as the input path\n' +
-        '  - the parent of the "fsh" directory (e.g., "../fsh") will be used as the output path unless otherwise specified with --out option\n' +
-        '  - generation of publisher-related scripts will be suppressed (i.e., assumed to be managed by you)'
-    );
-  }
+export function ensureOutputDir(
+  input: string,
+  output: string,
+  isIgPubContext: boolean,
+  isLegacyIgPubContext: boolean
+): string {
   let outDir = output;
-  if (isIgPubContext && !output) {
-    // When running in an IG Publisher context, default output is the parent folder of the tank
+  if (isLegacyIgPubContext && !output) {
+    // TODO: Legacy support for top level "fsh" directory. Remove when no longer supported.
+    // When running in a legacy IG Publisher context, default output is the parent folder of the tank
     outDir = path.join(input, '..');
     logger.info(`No output path specified. Output to ${outDir}`);
+  } else if (isIgPubContext && !output) {
+    // When running in an IG Publisher context, default output is the parent folder of the input/fsh folder
+    outDir = path.join(input, '..', '..');
+    logger.info(`No output path specified. Output to ${outDir}`);
   } else if (!output) {
     // Any other time, default output is just to 'build'
     outDir = path.join('.', 'build');
     logger.info(`No output path specified. Output to ${outDir}`);
   }
   fs.ensureDirSync(outDir);
+  // If the outDir contains a fsh-generated folder, we ensure that folder is empty
+  const fshGeneratedFolder = path.join(outDir, 'fsh-generated');
+  if (fs.existsSync(fshGeneratedFolder)) {
+    try {
+      fs.emptyDirSync(fshGeneratedFolder);
+    } catch (e) {
+      logger.error(
+        `Unable to empty existing fsh-generated folder because of the following error: ${e.message}`
+      );
+    }
+  }
   return outDir;
 }
 
 export function readConfig(input: string): Configuration {
-  const configPath = ensureConfigurationFile(input);
+  const configPath = ensureConfiguration(input);
   let config: Configuration;
   if (configPath == null || !fs.existsSync(configPath)) {
     config = loadConfigurationFromIgResource(input);
@@ -76,15 +127,15 @@ export function readConfig(input: string): Configuration {
   }
   if (!config) {
     logger.error(
-      'No config.yaml in FSH definition folder, and no configuration could' +
+      `No sushi-config.yaml in ${input} folder, and no configuration could` +
         ' be extracted from an ImplementationGuide resource.'
     );
     throw Error;
   }
   if (!config.fhirVersion.includes('4.0.1')) {
     logger.error(
-      'The config.yaml must specify FHIR R4 as a fhirVersion. Be sure to' +
-        ' add "fhirVersion: 4.0.1" to the config.yaml file.'
+      `The ${path.basename(config.filePath)} must specify FHIR R4 as a fhirVersion. Be sure to` +
+        ` add "fhirVersion: 4.0.1" to the ${path.basename(config.filePath)} file.`
     );
     throw Error;
   }
@@ -105,7 +156,7 @@ export function loadExternalDependencies(
     if (dep.version == null) {
       logger.error(
         `Failed to load ${dep.packageId}: No version specified. To specify the version in your ` +
-          'config.yaml, either use the simple dependency format:\n\n' +
+          `${path.basename(config.filePath)}, either use the simple dependency format:\n\n` +
           'dependencies:\n' +
           `  ${dep.packageId}: current\n\n` +
           'or use the detailed dependency format to specify other properties as well:\n\n' +
@@ -153,14 +204,21 @@ export function fillTank(rawFSHes: RawFSH[], config: Configuration): FSHTank {
   return new FSHTank(docs, config);
 }
 
-export function writeFHIRResources(outDir: string, outPackage: Package, snapshot: boolean) {
+export function writeFHIRResources(
+  outDir: string,
+  outPackage: Package,
+  snapshot: boolean,
+  isIgPubContext: boolean
+) {
   logger.info('Exporting FHIR resources as JSON...');
   let count = 0;
   const writeResources = (
     folder: string,
     resources: { getFileName: () => string; toJSON: (snapshot: boolean) => any }[]
   ) => {
-    const exportDir = path.join(outDir, 'input', folder);
+    const exportDir = isIgPubContext
+      ? path.join(outDir, 'fsh-generated', 'resources')
+      : path.join(outDir, 'input', folder);
     resources.forEach(resource => {
       fs.outputJSONSync(path.join(exportDir, resource.getFileName()), resource.toJSON(snapshot), {
         spaces: 2
@@ -190,7 +248,7 @@ export function writeFHIRResources(outDir: string, outPackage: Package, snapshot
 /**
  * Initializes an empty sample FSH within a user specified subdirectory of the current working directory
  */
-export function init(): void {
+export async function init(): Promise<void> {
   console.log(
     '\n╭──────────────────────────────────────────────────────────╮\n' +
       '│ This interactive tool will use your answers to create a  │\n' +
@@ -200,7 +258,7 @@ export function init(): void {
   );
 
   const configDoc = YAML.parseDocument(
-    fs.readFileSync(path.join(__dirname, 'init-project', 'config.yaml'), 'utf-8')
+    fs.readFileSync(path.join(__dirname, 'init-project', 'sushi-config.yaml'), 'utf-8')
   );
   // Accept user input for certain fields
   ['name', 'id', 'canonical', 'status', 'version'].forEach(field => {
@@ -215,7 +273,7 @@ export function init(): void {
   configDoc.set('copyrightYear', `${new Date().getFullYear()}+`);
   const projectName = configDoc.get('name');
 
-  // Write init directory out, including user made config.yaml, files in utils/init-project, and build scripts from ig/files
+  // Write init directory out, including user made sushi-config.yaml, files in utils/init-project, and build scripts from ig/files
   const outputDir = path.resolve('.', projectName);
   const initProjectDir = path.join(__dirname, 'init-project');
   if (!readlineSync.keyInYN(`Initialize SUSHI project in ${outputDir}?`)) {
@@ -227,31 +285,48 @@ export function init(): void {
   const indexPageContent = fs
     .readFileSync(path.join(initProjectDir, 'index.md'), 'utf-8')
     .replace('ExampleIG', projectName);
-  fs.ensureDirSync(path.join(outputDir, 'fsh', 'ig-data', 'input', 'pagecontent'));
-  fs.writeFileSync(
-    path.join(outputDir, 'fsh', 'ig-data', 'input', 'pagecontent', 'index.md'),
-    indexPageContent
-  );
+  fs.ensureDirSync(path.join(outputDir, 'input', 'pagecontent'));
+  fs.writeFileSync(path.join(outputDir, 'input', 'pagecontent', 'index.md'), indexPageContent);
+  // Add ig.ini, updating to reflect the user given id
+  const iniContent = fs
+    .readFileSync(path.join(initProjectDir, 'ig.ini'), 'utf-8')
+    .replace('fhir.example', configDoc.get('id'));
+  fs.writeFileSync(path.join(outputDir, 'ig.ini'), iniContent);
   // Add the config
-  fs.writeFileSync(path.join(outputDir, 'fsh', 'config.yaml'), configDoc.toString());
+  fs.writeFileSync(path.join(outputDir, 'sushi-config.yaml'), configDoc.toString());
   // Copy over remaining static files
+  fs.ensureDirSync(path.join(outputDir, 'input', 'fsh'));
   fs.copyFileSync(
     path.join(initProjectDir, 'patient.fsh'),
-    path.join(outputDir, 'fsh', 'patient.fsh')
+    path.join(outputDir, 'input', 'fsh', 'patient.fsh')
   );
   fs.copyFileSync(
     path.join(initProjectDir, 'init-gitignore.txt'),
     path.join(outputDir, '.gitignore')
   );
+  fs.copyFileSync(
+    path.join(initProjectDir, 'ignoreWarnings.txt'),
+    path.join(outputDir, 'input', 'ignoreWarnings.txt')
+  );
   // Add the _updatePublisher, _genonce, and _gencontinuous scripts
-  const scriptsDir = path.join(__dirname, '..', 'ig', 'files');
-  const scriptsDirContents = fs.readdirSync(scriptsDir);
-  scriptsDirContents
-    .filter(file => file.startsWith('_'))
-    .forEach(file => {
-      fs.copyFileSync(path.join(scriptsDir, file), path.join(outputDir, file));
-    });
-
+  console.log('Downloading publisher scripts from https://github.com/FHIR/sample-ig');
+  for (const script of [
+    '_genonce.bat',
+    '_genonce.sh',
+    '_updatePublisher.bat',
+    '_updatePublisher.sh'
+  ]) {
+    const url = `http://raw.githubusercontent.com/FHIR/sample-ig/master/${script}`;
+    try {
+      const res = await axios.get(url);
+      fs.writeFileSync(path.join(outputDir, script), res.data);
+      if (script.endsWith('.sh')) {
+        fs.chmodSync(path.join(outputDir, script), 0o755);
+      }
+    } catch (e) {
+      logger.error(`Unable to download ${script} from ${url}: ${e.message}`);
+    }
+  }
   const maxLength = 31;
   const printName =
     projectName.length > maxLength ? projectName.slice(0, maxLength - 3) + '...' : projectName;
diff --git a/src/utils/init-project/ig.ini b/src/utils/init-project/ig.ini
new file mode 100644
index 0000000..8381cc6
--- /dev/null
+++ b/src/utils/init-project/ig.ini
@@ -0,0 +1,3 @@
+[IG]
+ig = fsh-generated/resources/ImplementationGuide-fhir.example.json
+template = fhir.base.template#current
\ No newline at end of file
diff --git a/src/utils/init-project/ignoreWarnings.txt b/src/utils/init-project/ignoreWarnings.txt
new file mode 100644
index 0000000..d03142b
--- /dev/null
+++ b/src/utils/init-project/ignoreWarnings.txt
@@ -0,0 +1,5 @@
+== Suppressed Messages ==
+
+# Add warning and/or information messages here after you've confirmed that they aren't really a problem
+# (And include comments like this justifying why)
+# See https://github.com/FHIR/sample-ig/blob/master/input/ignoreWarnings.txt for examples
\ No newline at end of file
diff --git a/src/utils/init-project/init-gitignore.txt b/src/utils/init-project/init-gitignore.txt
index 7489d2f..5a93fa4 100644
--- a/src/utils/init-project/init-gitignore.txt
+++ b/src/utils/init-project/init-gitignore.txt
@@ -1,9 +1,7 @@
 .DS_Store
 Thumbs.db
-/SUSHI-GENERATED-FILES.md
-/ig.ini
+/fsh-generated
 /input-cache
-/input
 /output
 /temp
 /template
\ No newline at end of file
diff --git a/src/utils/init-project/config.yaml b/src/utils/init-project/sushi-config.yaml
similarity index 68%
rename from src/utils/init-project/config.yaml
rename to src/utils/init-project/sushi-config.yaml
index e6fb0aa..3e4164c 100644
--- a/src/utils/init-project/config.yaml
+++ b/src/utils/init-project/sushi-config.yaml
@@ -11,11 +11,6 @@ fhirVersion: 4.0.1
 copyrightYear: 2020+
 releaseLabel: ci-build
 
-# ╭─────────────────────────────────────────────ig.ini─────────────────────────────────────────────╮
-# │  To use a provided ig-data/ig.ini file, delete the "template" property below.                  │
-# ╰────────────────────────────────────────────────────────────────────────────────────────────────╯
-template: fhir.base.template#current
-
 # ╭────────────────────────────────────────────menu.xml────────────────────────────────────────────╮
 # │  To use a provided ig-data/input/includes/menu.xml file, delete the "menu" property below.     │
 # ╰────────────────────────────────────────────────────────────────────────────────────────────────╯


Test patch:
diff --git a/test/export/FHIRExporter.test.ts b/test/export/FHIRExporter.test.ts
index 64aeb19..d101af4 100644
--- a/test/export/FHIRExporter.test.ts
+++ b/test/export/FHIRExporter.test.ts
@@ -13,7 +13,7 @@ describe('FHIRExporter', () => {
     const result = exportFHIR(input, new FHIRDefinitions());
     expect(result).toEqual(
       new Package({
-        filePath: 'config.yaml',
+        filePath: 'sushi-config.yaml',
         id: 'fhir.us.minimal',
         version: '1.0.0',
         canonical: 'http://hl7.org/fhir/us/minimal',
diff --git a/test/fhirdefs/load.test.ts b/test/fhirdefs/load.test.ts
index e34fe4e..312606a 100644
--- a/test/fhirdefs/load.test.ts
+++ b/test/fhirdefs/load.test.ts
@@ -317,7 +317,13 @@ describe('#loadCustomResources', () => {
   let defs: FHIRDefinitions;
   beforeAll(() => {
     defs = new FHIRDefinitions();
-    const fixtures = path.join(__dirname, 'fixtures', 'customized-ig-with-resources');
+    const fixtures = path.join(
+      __dirname,
+      'fixtures',
+      'customized-ig-with-resources',
+      'ig-data',
+      'input'
+    );
     loadCustomResources(fixtures, defs);
   });
 
diff --git a/test/ig/IGExporter.IG.test.ts b/test/ig/IGExporter.IG.test.ts
index d030f3e..db150e5 100644
--- a/test/ig/IGExporter.IG.test.ts
+++ b/test/ig/IGExporter.IG.test.ts
@@ -88,9 +88,10 @@ describe('IGExporter', () => {
     });
 
     beforeEach(() => {
+      loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
       config = {
-        filePath: path.join(fixtures, 'config.yml'),
+        filePath: path.join(fixtures, 'sushi-config.yml'),
         id: 'sushi-test',
         canonical: 'http://hl7.org/fhir/sushi-test',
         url: 'http://hl7.org/fhir/sushi-test/ImplementationGuide/FSHTestIG',
@@ -147,29 +148,6 @@ describe('IGExporter', () => {
       temp.cleanupSync();
     });
 
-    it('should copy over the static files', () => {
-      exporter.export(tempOut);
-      expect(fs.existsSync(path.join(tempOut, '_genonce.bat'))).toBeTruthy();
-      expect(fs.existsSync(path.join(tempOut, '_genonce.sh'))).toBeTruthy();
-      expect(fs.existsSync(path.join(tempOut, '_gencontinuous.bat'))).toBeTruthy();
-      expect(fs.existsSync(path.join(tempOut, '_gencontinuous.sh'))).toBeTruthy();
-      expect(fs.existsSync(path.join(tempOut, '_updatePublisher.bat'))).toBeTruthy();
-      expect(fs.existsSync(path.join(tempOut, '_updatePublisher.sh'))).toBeTruthy();
-      expect(fs.existsSync(path.join(tempOut, 'input', 'ignoreWarnings.txt'))).toBeTruthy();
-    });
-
-    it('should not copy over scripts in publisher context', () => {
-      exporter = new IGExporter(pkg, defs, path.resolve(fixtures, 'ig-data'), true);
-      exporter.export(tempOut);
-      expect(fs.existsSync(path.join(tempOut, '_genonce.bat'))).toBeFalsy();
-      expect(fs.existsSync(path.join(tempOut, '_genonce.sh'))).toBeFalsy();
-      expect(fs.existsSync(path.join(tempOut, '_gencontinuous.bat'))).toBeFalsy();
-      expect(fs.existsSync(path.join(tempOut, '_gencontinuous.sh'))).toBeFalsy();
-      expect(fs.existsSync(path.join(tempOut, '_updatePublisher.bat'))).toBeFalsy();
-      expect(fs.existsSync(path.join(tempOut, '_updatePublisher.sh'))).toBeFalsy();
-      expect(fs.existsSync(path.join(tempOut, 'input', 'ignoreWarnings.txt'))).toBeTruthy();
-    });
-
     it('should generate an implementation guide for simple-ig', () => {
       exporter.export(tempOut);
       const igPath = path.join(tempOut, 'input', 'ImplementationGuide-sushi-test.json');
@@ -561,7 +539,7 @@ describe('IGExporter', () => {
         }
       ];
       exporter.export(tempOut);
-      expect(loggerSpy.getLastMessage('warn')).toMatch(
+      expect(loggerSpy.getFirstMessage('warn')).toMatch(
         /StructureDefinition\/sample-patient is listed as a member of group JustOneGroup.*does not need a groupingId/s
       );
     });
@@ -586,6 +564,7 @@ describe('IGExporter', () => {
     });
 
     beforeEach(() => {
+      loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
       config = cloneDeep(minimalConfig);
       pkg = new Package(config);
@@ -809,7 +788,7 @@ describe('IGExporter', () => {
     });
   });
 
-  describe('#customized-ig-with-resources', () => {
+  describe('#customized-ig-with-resources in legacy IG Publisher mode', () => {
     let pkg: Package;
     let exporter: IGExporter;
     let tempOut: string;
@@ -825,10 +804,11 @@ describe('IGExporter', () => {
         defs
       );
       fixtures = path.join(__dirname, 'fixtures', 'customized-ig-with-resources');
-      loadCustomResources(fixtures, defs);
+      loadCustomResources(path.join(fixtures, 'ig-data', 'input'), defs);
     });
 
     beforeEach(() => {
+      loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
       config = cloneDeep(minimalConfig);
       pkg = new Package(config);
@@ -848,14 +828,14 @@ describe('IGExporter', () => {
       patientInstance._instanceMeta.usage = 'Example';
       pkg.instances.push(patientInstance);
 
-      exporter = new IGExporter(pkg, defs, path.resolve(fixtures, 'ig-data'));
+      exporter = new IGExporter(pkg, defs, path.resolve(fixtures, 'ig-data'), false);
     });
 
     afterAll(() => {
       temp.cleanupSync();
     });
 
-    it('should copy over resource files', () => {
+    it('should copy over resource files and fix names where possible', () => {
       exporter.export(tempOut);
       const directoryContents = new Map<string, string[]>();
       const dirNames = [
@@ -889,10 +869,81 @@ describe('IGExporter', () => {
         'Patient-FooPatient.json' // Renamed from "PoorlyNamedPatient.json"
       ]);
     });
+  });
+
+  describe('#customized-ig-with-resources', () => {
+    let pkg: Package;
+    let exporter: IGExporter;
+    let tempOut: string;
+    let fixtures: string;
+    let config: Configuration;
+    let defs: FHIRDefinitions;
+
+    beforeAll(() => {
+      defs = new FHIRDefinitions();
+      loadFromPath(
+        path.join(__dirname, '..', 'testhelpers', 'testdefs', 'package'),
+        'testPackage',
+        defs
+      );
+      fixtures = path.join(__dirname, 'fixtures', 'customized-ig-with-resources');
+      loadCustomResources(path.join(fixtures, 'ig-data', 'input'), defs);
+    });
+
+    beforeEach(() => {
+      loggerSpy.reset();
+      tempOut = temp.mkdirSync('sushi-test');
+      config = cloneDeep(minimalConfig);
+      pkg = new Package(config);
+      // Add a patient to the package that will be overwritten
+      const fisher = new TestFisher(null, defs, pkg);
+      const patient = fisher.fishForStructureDefinition('Patient');
+      patient.id = 'MyPatient';
+      patient.name = 'MyPatient';
+      patient.description = 'This should go away';
+      pkg.profiles.push(patient);
+
+      const patientInstance = new InstanceDefinition();
+      patientInstance.resourceType = 'Patient';
+      patientInstance.id = 'FooPatient';
+      patientInstance._instanceMeta.description = 'This should stay';
+      patientInstance._instanceMeta.name = 'StayName';
+      patientInstance._instanceMeta.usage = 'Example';
+      pkg.instances.push(patientInstance);
+
+      exporter = new IGExporter(pkg, defs, path.resolve(fixtures, 'ig-data'), true);
+    });
+
+    afterAll(() => {
+      temp.cleanupSync();
+    });
+
+    it('should not copy over resource files', () => {
+      exporter.export(tempOut);
+      const dirNames = [
+        'capabilities',
+        'extensions',
+        'models',
+        'operations',
+        'profiles',
+        'resources',
+        'vocabulary',
+        'examples'
+      ];
+      for (const dirName of dirNames) {
+        // No provided resources are copied to output
+        expect(fs.existsSync(path.join(tempOut, 'input', dirName))).toBeFalsy();
+      }
+    });
 
     it('should add basic resource references to the ImplementationGuide resource', () => {
       exporter.export(tempOut);
-      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-fhir.us.minimal.json');
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
       expect(fs.existsSync(igPath)).toBeTruthy();
       const igContent: ImplementationGuide = fs.readJSONSync(igPath);
       expect(igContent.definition.resource).toContainEqual({
@@ -942,7 +993,12 @@ describe('IGExporter', () => {
 
     it('should overwrite existing resource references in the ImplementationGuide resource', () => {
       exporter.export(tempOut);
-      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-fhir.us.minimal.json');
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
       expect(fs.existsSync(igPath)).toBeTruthy();
       const igContent: ImplementationGuide = fs.readJSONSync(igPath);
       // Should only have one copy of MyPatient
@@ -963,7 +1019,12 @@ describe('IGExporter', () => {
 
     it('should add resource references with a description to the ImplementationGuide resource', () => {
       exporter.export(tempOut);
-      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-fhir.us.minimal.json');
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
       expect(fs.existsSync(igPath)).toBeTruthy();
       const igContent: ImplementationGuide = fs.readJSONSync(igPath);
       expect(igContent.definition.resource).toContainEqual({
@@ -978,7 +1039,12 @@ describe('IGExporter', () => {
 
     it('should add example references to the ImplementationGuide resource', () => {
       exporter.export(tempOut);
-      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-fhir.us.minimal.json');
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
       expect(fs.existsSync(igPath)).toBeTruthy();
       const igContent: ImplementationGuide = fs.readJSONSync(igPath);
       expect(igContent.definition.resource).toContainEqual({
@@ -1001,7 +1067,12 @@ describe('IGExporter', () => {
 
     it('should add resource references with a title to the ImplementationGuide resource', () => {
       exporter.export(tempOut);
-      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-fhir.us.minimal.json');
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
       expect(fs.existsSync(igPath)).toBeTruthy();
       const igContent: ImplementationGuide = fs.readJSONSync(igPath);
       expect(igContent.definition.resource).toContainEqual({
@@ -1023,7 +1094,12 @@ describe('IGExporter', () => {
         }
       ];
       exporter.export(tempOut);
-      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-fhir.us.minimal.json');
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
       expect(fs.existsSync(igPath)).toBeTruthy();
       const igContent: ImplementationGuide = fs.readJSONSync(igPath);
       const myPatient: ImplementationGuideDefinitionResource = igContent.definition.resource.find(
@@ -1035,7 +1111,7 @@ describe('IGExporter', () => {
 
     it('should log an error for input files missing resourceType or id', () => {
       exporter.export(tempOut);
-      expect(loggerSpy.getMessageAtIndex(-1, 'error')).toMatch(
+      expect(loggerSpy.getFirstMessage('error')).toMatch(
         /.*InvalidPatient.json must define resourceType and id/
       );
     });
@@ -1060,6 +1136,7 @@ describe('IGExporter', () => {
     });
 
     beforeEach(() => {
+      loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
       config = cloneDeep(minimalConfig);
       pkg = new Package(config);
@@ -1157,6 +1234,7 @@ describe('IGExporter', () => {
     });
 
     beforeEach(() => {
+      loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
       config = minimalConfig;
       pkg = new Package(config);
@@ -1182,7 +1260,7 @@ describe('IGExporter', () => {
       expect(fs.existsSync(path.join(tempOut, 'input', 'pagecontent', 'index.md'))).toBeTruthy();
       expect(fs.existsSync(path.join(tempOut, 'input', 'pagecontent', 'invalid.txt'))).toBeTruthy();
       // Check for log messages indicating invalid input
-      expect(loggerSpy.getLastMessage('warn')).toMatch(
+      expect(loggerSpy.getFirstMessage('warn')).toMatch(
         /Files not in the supported file types \(\.md and \.xml\) were detected\. These files will be copied over without any processing\..*File: .*[\/\\]invalid-pages-folder-ig[\/\\]ig-data[\/\\]input[\/\\]pagecontent/s
       );
     });
@@ -1190,98 +1268,194 @@ describe('IGExporter', () => {
 
   describe('#sorted-pages-ig', () => {
     let tempOut: string;
+    let fixtures: string;
+    let defs: FHIRDefinitions;
+    let pkg: Package;
 
     beforeAll(() => {
       tempOut = temp.mkdirSync('sushi-test');
-      const fixtures = path.join(__dirname, 'fixtures', 'sorted-pages-ig');
-      const defs = new FHIRDefinitions();
+      fixtures = path.join(__dirname, 'fixtures', 'sorted-pages-ig');
+      defs = new FHIRDefinitions();
       loadFromPath(
         path.join(__dirname, '..', 'testhelpers', 'testdefs', 'package'),
         'testPackage',
         defs
       );
-      const pkg = new Package(minimalConfig);
-      const exporter = new IGExporter(pkg, defs, path.resolve(fixtures, 'ig-data'), false);
-      // No need to regenerate the IG on every test -- generate it once and inspect what you
-      // need to in the tests
-      exporter.export(tempOut);
+      pkg = new Package(minimalConfig);
+    });
+
+    beforeEach(() => {
+      loggerSpy.reset();
     });
 
     afterAll(() => {
       temp.cleanupSync();
     });
 
-    it('should add user-provided pages in the user-specified order', () => {
-      const pageContentPath = path.join(tempOut, 'input', 'pagecontent');
-      expect(fs.existsSync(pageContentPath)).toBeTruthy();
+    describe('legacy IG Publisher mode', () => {
+      beforeAll(() => {
+        const exporter = new IGExporter(pkg, defs, path.resolve(fixtures, 'ig-data'), false);
+        // No need to regenerate the IG on every test -- generate it once and inspect what you
+        // need to in the tests
+        exporter.export(tempOut);
+      });
 
-      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-fhir.us.minimal.json');
-      expect(fs.existsSync(igPath)).toBeTruthy();
-      const igContent = fs.readJSONSync(igPath);
-      expect(igContent.definition.page.page).toHaveLength(9);
-      expect(igContent.definition.page.page).toEqual([
-        {
-          nameUrl: 'index.html',
-          title: 'Home',
-          generation: 'html'
-        },
-        {
-          nameUrl: 'oranges.html',
-          title: 'Oranges',
-          generation: 'markdown'
-        },
-        {
-          nameUrl: 'apples.html',
-          title: 'Apples',
-          generation: 'markdown'
-        },
-        {
-          nameUrl: 'bananas.html',
-          title: 'Bananas',
-          generation: 'markdown'
-        },
-        {
-          nameUrl: 'pears.html',
-          title: 'Pears',
-          generation: 'markdown'
-        },
-        {
-          nameUrl: 'left.html',
-          title: 'Left',
-          generation: 'markdown'
-        },
-        {
-          nameUrl: 'right.html',
-          title: 'Right',
-          generation: 'markdown'
-        },
-        {
-          nameUrl: 'big.html',
-          title: 'Big',
-          generation: 'markdown'
-        },
-        {
-          nameUrl: 'pasta.html',
-          title: 'Pasta',
-          generation: 'markdown'
-        }
-      ]);
+      afterAll(() => {
+        temp.cleanupSync();
+      });
+
+      it('should add user-provided pages in the user-specified order', () => {
+        const pageContentPath = path.join(tempOut, 'input', 'pagecontent');
+        expect(fs.existsSync(pageContentPath)).toBeTruthy();
+
+        const igPath = path.join(tempOut, 'input', 'ImplementationGuide-fhir.us.minimal.json');
+        expect(fs.existsSync(igPath)).toBeTruthy();
+        const igContent = fs.readJSONSync(igPath);
+        expect(igContent.definition.page.page).toHaveLength(9);
+        expect(igContent.definition.page.page).toEqual([
+          {
+            nameUrl: 'index.html',
+            title: 'Home',
+            generation: 'html'
+          },
+          {
+            nameUrl: 'oranges.html',
+            title: 'Oranges',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: 'apples.html',
+            title: 'Apples',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: 'bananas.html',
+            title: 'Bananas',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: 'pears.html',
+            title: 'Pears',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: 'left.html',
+            title: 'Left',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: 'right.html',
+            title: 'Right',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: 'big.html',
+            title: 'Big',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: 'pasta.html',
+            title: 'Pasta',
+            generation: 'markdown'
+          }
+        ]);
+      });
+
+      it('should remove numeric prefixes from copied files', () => {
+        const pageContentPath = path.join(tempOut, 'input', 'pagecontent');
+        expect(fs.existsSync(pageContentPath)).toBeTruthy();
+        const pageContentFiles = fs.readdirSync(pageContentPath);
+        expect(pageContentFiles).toHaveLength(9);
+        expect(pageContentFiles).toContain('index.xml');
+        expect(pageContentFiles).toContain('oranges.md');
+        expect(pageContentFiles).toContain('apples.md');
+        expect(pageContentFiles).toContain('bananas.md');
+        expect(pageContentFiles).toContain('pears.md');
+        expect(pageContentFiles).toContain('left.md');
+        expect(pageContentFiles).toContain('right.md');
+        expect(pageContentFiles).toContain('big.md');
+        expect(pageContentFiles).toContain('pasta.md');
+      });
     });
 
-    it('should remove numeric prefixes from copied files', () => {
-      const pageContentPath = path.join(tempOut, 'input', 'pagecontent');
-      expect(fs.existsSync(pageContentPath)).toBeTruthy();
-      const pageContentFiles = fs.readdirSync(pageContentPath);
-      expect(pageContentFiles).toHaveLength(9);
-      expect(pageContentFiles).toContain('index.xml');
-      expect(pageContentFiles).toContain('oranges.md');
-      expect(pageContentFiles).toContain('apples.md');
-      expect(pageContentFiles).toContain('bananas.md');
-      expect(pageContentFiles).toContain('pears.md');
-      expect(pageContentFiles).toContain('left.md');
-      expect(pageContentFiles).toContain('right.md');
-      expect(pageContentFiles).toContain('big.md');
-      expect(pageContentFiles).toContain('pasta.md');
+    describe('IG Publisher mode', () => {
+      beforeAll(() => {
+        const exporter = new IGExporter(pkg, defs, path.resolve(fixtures, 'ig-data'), true); // New publisher mode w/ input/fsh
+        // No need to regenerate the IG on every test -- generate it once and inspect what you
+        // need to in the tests
+        exporter.export(tempOut);
+      });
+
+      afterAll(() => {
+        temp.cleanupSync();
+      });
+
+      it('should configure user-provided pages in numeric then alphabetic order', () => {
+        const igPath = path.join(
+          tempOut,
+          'fsh-generated',
+          'resources',
+          'ImplementationGuide-fhir.us.minimal.json'
+        );
+        expect(fs.existsSync(igPath)).toBeTruthy();
+        const igContent = fs.readJSONSync(igPath);
+        expect(igContent.definition.page.page).toHaveLength(9);
+        expect(igContent.definition.page.page).toEqual([
+          {
+            nameUrl: 'index.html',
+            title: 'Home',
+            generation: 'html'
+          },
+          {
+            nameUrl: '1_oranges.html',
+            title: 'Oranges',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: '2_apples.html',
+            title: 'Apples',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: '3_bananas.html',
+            title: 'Bananas',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: '4_pears.html',
+            title: 'Pears',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: '7_left.html',
+            title: 'Left',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: '7_right.html',
+            title: 'Right',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: '100_big.html',
+            title: 'Big',
+            generation: 'markdown'
+          },
+          {
+            nameUrl: 'pasta.html',
+            title: 'Pasta',
+            generation: 'markdown'
+          }
+        ]);
+      });
+
+      it('should not copy files', () => {
+        let pageContentPath = path.join(tempOut, 'input', 'pagecontent');
+        expect(fs.existsSync(pageContentPath)).toBeFalsy();
+
+        pageContentPath = path.join(tempOut, 'fsh-generated', 'input', 'pagecontent');
+        expect(fs.existsSync(pageContentPath)).toBeFalsy();
+      });
     });
   });
 
@@ -1289,6 +1463,7 @@ describe('IGExporter', () => {
     let tempOut: string;
 
     beforeAll(() => {
+      loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
       const fixtures = path.join(__dirname, 'fixtures', 'name-collision-ig');
       const defs = new FHIRDefinitions();
@@ -1370,8 +1545,10 @@ describe('IGExporter', () => {
     });
 
     beforeEach(() => {
+      loggerSpy.reset();
       tempOut = temp.mkdirSync('sushi-test');
       config = cloneDeep(minimalConfig);
+      delete config.template;
       pkg = new Package(config);
       exporter = new IGExporter(
         pkg,
@@ -1379,7 +1556,6 @@ describe('IGExporter', () => {
         path.resolve(fixtures, 'ig-data'),
         false
       );
-      loggerSpy.reset();
     });
 
     it('should avoid copying over extra system files', () => {
diff --git a/test/ig/IGExporter.SUSHI-GENERATED-FILES.test.ts b/test/ig/IGExporter.SUSHI-GENERATED-FILES.test.ts
index caa2da1..bfa1ab5 100644
--- a/test/ig/IGExporter.SUSHI-GENERATED-FILES.test.ts
+++ b/test/ig/IGExporter.SUSHI-GENERATED-FILES.test.ts
@@ -41,14 +41,8 @@ describe('IGExporter', () => {
       expect(fs.existsSync(reportPath)).toBeTruthy();
       const content = fs.readFileSync(reportPath, 'utf8');
       expect(content).toMatch('# SUSHI-GENERATED FILES #');
-      expect(content).toMatch(/\| _gencontinuous.bat .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _gencontinuous.sh .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _genonce.bat .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _genonce.sh .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _updatePublisher.bat .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _updatePublisher.sh .*\| generated \| \s* \|/);
       expect(content).toMatch(
-        /\| input[\/\\]ImplementationGuide-fhir\.us\.minimal\.json \s*\| generated \| .*[\/\\]customized-ig[\/\\]config\.yaml, \{all input resources and pages\} \s*\|/
+        /\| input[\/\\]ImplementationGuide-fhir\.us\.minimal\.json \s*\| generated \| .*[\/\\]customized-ig[\/\\]sushi-config\.yaml, \{all input resources and pages\} \s*\|/
       );
       expect(content).toMatch(/\| ig\.ini \s*\| copied \s*\| ..*[\/\\]ig-data[\/\\]ig\.ini \s*\|/);
       expect(content).toMatch(
@@ -118,27 +112,20 @@ describe('IGExporter', () => {
       expect(fs.existsSync(reportPath)).toBeTruthy();
       const content = fs.readFileSync(reportPath, 'utf8');
       expect(content).toMatch('# SUSHI-GENERATED FILES #');
-      expect(content).toMatch(/\| _gencontinuous.bat .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _gencontinuous.sh .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _genonce.bat .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _genonce.sh .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _updatePublisher.bat .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _updatePublisher.sh .*\| generated \| \s* \|/);
       expect(content).toMatch(
-        /\| ig\.ini \s*\| generated \s*\| .*[\/\\]simple-ig[\/\\]config\.yaml \s*\|/
+        /\| ig\.ini \s*\| generated \s*\| .*[\/\\]simple-ig[\/\\]sushi-config\.yaml \s*\|/
       );
       expect(content).toMatch(
-        /\| input[\/\\]ImplementationGuide-fhir\.us\.minimal\.json \s*\| generated \| .*[\/\\]simple-ig[\/\\]config\.yaml, \{all input resources and pages\} \s*\|/
+        /\| input[\/\\]ImplementationGuide-fhir\.us\.minimal\.json \s*\| generated \| .*[\/\\]simple-ig[\/\\]sushi-config\.yaml, \{all input resources and pages\} \s*\|/
       );
-      expect(content).toMatch(/\| input[\/\\]ignoreWarnings\.txt \s*\| generated \s*\|\s*\|/);
       expect(content).toMatch(
-        /\| input[\/\\]includes[\/\\]menu\.xml \s*\| generated \s*\| .*[\/\\]simple-ig[\/\\]config\.yaml \s*\|/
+        /\| input[\/\\]includes[\/\\]menu\.xml \s*\| generated \s*\| .*[\/\\]simple-ig[\/\\]sushi-config\.yaml \s*\|/
       );
       expect(content).toMatch(
-        /\| input[\/\\]pagecontent[\/\\]index\.md \s*\| generated \s*\| .*[\/\\]simple-ig[\/\\]config\.yaml \s*\|/
+        /\| input[\/\\]pagecontent[\/\\]index\.md \s*\| generated \s*\| .*[\/\\]simple-ig[\/\\]sushi-config\.yaml \s*\|/
       );
       expect(content).toMatch(
-        /\| package-list\.json \s*\| generated \s*\| .*[\/\\]simple-ig[\/\\]config\.yaml \s*\|/
+        /\| package-list\.json \s*\| generated \s*\| .*[\/\\]simple-ig[\/\\]sushi-config\.yaml \s*\|/
       );
     });
 
@@ -155,23 +142,16 @@ describe('IGExporter', () => {
       expect(fs.existsSync(reportPath)).toBeTruthy();
       const content = fs.readFileSync(reportPath, 'utf8');
       expect(content).toMatch('# SUSHI-GENERATED FILES #');
-      expect(content).toMatch(/\| _gencontinuous.bat .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _gencontinuous.sh .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _genonce.bat .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _genonce.sh .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _updatePublisher.bat .*\| generated \| \s* \|/);
-      expect(content).toMatch(/\| _updatePublisher.sh .*\| generated \| \s* \|/);
       expect(content).toMatch(
-        /\| input[\/\\]ImplementationGuide-fhir\.us\.minimal\.json \s*\| generated \| .*[\/\\]simple-ig[\/\\]config\.yaml, \{all input resources and pages\} \s*\|/
+        /\| input[\/\\]ImplementationGuide-fhir\.us\.minimal\.json \s*\| generated \| .*[\/\\]simple-ig[\/\\]sushi-config\.yaml, \{all input resources and pages\} \s*\|/
       );
-      expect(content).toMatch(/\| input[\/\\]ignoreWarnings\.txt \s*\| generated \s*\|\s*\|/);
       expect(content).not.toMatch('ig.ini');
       expect(content).not.toMatch('menu.xml');
       expect(content).not.toMatch('index.md');
       expect(content).not.toMatch('package-list.json');
     });
 
-    it('should generate a SUSHI-GENERATED-FILES.md with the correct listings when in publisher context', () => {
+    it('should not generate a SUSHI-GENERATED-FILES.md when in publisher context', () => {
       const config = cloneDeep(minimalConfig);
       delete config.template;
       const pkg = new Package(config);
@@ -181,20 +161,7 @@ describe('IGExporter', () => {
       exporter.export(tempOut);
 
       const reportPath = path.join(tempOut, 'SUSHI-GENERATED-FILES.md');
-      expect(fs.existsSync(reportPath)).toBeTruthy();
-      const content = fs.readFileSync(reportPath, 'utf8');
-      expect(content).toMatch('# SUSHI-GENERATED FILES #');
-      expect(content).toMatch(
-        /\| input[\/\\]ImplementationGuide-fhir\.us\.minimal\.json \s*\| generated \| .*[\/\\]simple-ig[\/\\]config\.yaml, \{all input resources and pages\} \s*\|/
-      );
-      expect(content).toMatch(/\| input[\/\\]ignoreWarnings\.txt \s*\| generated \s*\|\s*\|/);
-      expect(content).not.toMatch('_gencontinuous');
-      expect(content).not.toMatch('_genonce');
-      expect(content).not.toMatch('_updatePublisher');
-      expect(content).not.toMatch('ig.ini');
-      expect(content).not.toMatch('menu.xml');
-      expect(content).not.toMatch('index.md');
-      expect(content).not.toMatch('package-list.json');
+      expect(fs.existsSync(reportPath)).toBeFalsy();
     });
   });
 });
diff --git a/test/ig/IGExporter.ig-ini.test.ts b/test/ig/IGExporter.ig-ini.test.ts
index 8ddcd56..6cf1f0d 100644
--- a/test/ig/IGExporter.ig-ini.test.ts
+++ b/test/ig/IGExporter.ig-ini.test.ts
@@ -17,20 +17,17 @@ describe('IGExporter', () => {
     let templatelessConfig: Configuration;
     let tempOut: string;
 
-    beforeAll(() => {
-      tempOut = temp.mkdirSync('sushi-test');
-    });
-
-    afterAll(() => {
-      temp.cleanupSync();
-    });
-
     beforeEach(() => {
+      tempOut = temp.mkdirSync('sushi-test');
       templatelessConfig = cloneDeep(minimalConfig);
       delete templatelessConfig.template;
       loggerSpy.reset();
     });
 
+    afterEach(() => {
+      temp.cleanupSync();
+    });
+
     it('should do nothing when template is undefined and ig.ini is not provided', () => {
       const pkg = new Package(templatelessConfig);
       const igDataPath = path.resolve(__dirname, 'fixtures', 'simple-ig', 'ig-data');
@@ -41,7 +38,7 @@ describe('IGExporter', () => {
       expect(loggerSpy.getAllMessages()).toHaveLength(0);
     });
 
-    it('should generate an ig.ini when template is defined in the config', () => {
+    it('should generate an ig.ini when template is defined in the config (legacy)', () => {
       const pkg = new Package(minimalConfig);
       const igDataPath = path.resolve(__dirname, 'fixtures', 'simple-ig', 'ig-data');
       const exporter = new IGExporter(pkg, null, igDataPath);
@@ -52,25 +49,28 @@ describe('IGExporter', () => {
       expect(content).toEqual(
         [
           '[IG]',
-          '; ***********************************************************************************************',
-          '; *                               WARNING: DO NOT EDIT THIS FILE                                *',
-          '; *                                                                                             *',
-          '; * This file is generated by SUSHI. Any edits you make to this file will be overwritten.       *',
-          '; *                                                                                             *',
-          '; * This ig.ini was generated using the template property in config.yaml. To provide your own   *',
-          '; * ig.ini, create an ig.ini file in the ig-data folder with required properties: ig, template. *',
-          '; * See: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root                   *',
-          '; ***********************************************************************************************',
+          '; ***************************************************************************************************',
+          '; *                                 WARNING: DO NOT EDIT THIS FILE                                  *',
+          '; *                                                                                                 *',
+          '; * This file is generated by SUSHI. Any edits you make to this file will be overwritten.           *',
+          '; *                                                                                                 *',
+          '; * This ig.ini was generated using the template property in sushi-config.yaml. To provide your own *',
+          '; * ig.ini, create an ig.ini file in the ig-data folder with required properties: ig, template.     *',
+          '; * See: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root                       *',
+          '; ***************************************************************************************************',
           'ig = input/ImplementationGuide-fhir.us.minimal.json',
           'template = hl7.fhir.template#0.0.5',
           ''
         ].join(EOL) // Windows: /r/n; Mac: /n
       );
-      expect(loggerSpy.getAllMessages()).toHaveLength(1);
+      expect(loggerSpy.getAllMessages()).toHaveLength(2);
       expect(loggerSpy.getLastMessage('info')).toMatch('Generated ig.ini.');
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        'The "template" property in sushi-config.yaml has been deprecated.'
+      );
     });
 
-    it('should generate an ig.ini when local template is defined in the config', () => {
+    it('should generate an ig.ini when local template is defined in the config (legacy)', () => {
       const localTemplateConfig = cloneDeep(minimalConfig);
       localTemplateConfig.template = '#local-template';
       const pkg = new Package(localTemplateConfig);
@@ -83,25 +83,28 @@ describe('IGExporter', () => {
       expect(content).toEqual(
         [
           '[IG]',
-          '; ***********************************************************************************************',
-          '; *                               WARNING: DO NOT EDIT THIS FILE                                *',
-          '; *                                                                                             *',
-          '; * This file is generated by SUSHI. Any edits you make to this file will be overwritten.       *',
-          '; *                                                                                             *',
-          '; * This ig.ini was generated using the template property in config.yaml. To provide your own   *',
-          '; * ig.ini, create an ig.ini file in the ig-data folder with required properties: ig, template. *',
-          '; * See: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root                   *',
-          '; ***********************************************************************************************',
+          '; ***************************************************************************************************',
+          '; *                                 WARNING: DO NOT EDIT THIS FILE                                  *',
+          '; *                                                                                                 *',
+          '; * This file is generated by SUSHI. Any edits you make to this file will be overwritten.           *',
+          '; *                                                                                                 *',
+          '; * This ig.ini was generated using the template property in sushi-config.yaml. To provide your own *',
+          '; * ig.ini, create an ig.ini file in the ig-data folder with required properties: ig, template.     *',
+          '; * See: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root                       *',
+          '; ***************************************************************************************************',
           'ig = input/ImplementationGuide-fhir.us.minimal.json',
           'template = #local-template',
           ''
         ].join(EOL) // Windows: /r/n; Mac: /n
       );
-      expect(loggerSpy.getAllMessages()).toHaveLength(1);
+      expect(loggerSpy.getAllMessages()).toHaveLength(2);
       expect(loggerSpy.getLastMessage('info')).toMatch('Generated ig.ini.');
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        'The "template" property in sushi-config.yaml has been deprecated.'
+      );
     });
 
-    it('should generate an ig.ini when template is defined in the config and warn if there is also an ig-data/ig.ini file', () => {
+    it('should generate an ig.ini when template is defined in the config and warn if there is also an ig-data/ig.ini (legacy)', () => {
       const pkg = new Package(minimalConfig);
       const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
       const exporter = new IGExporter(pkg, null, igDataPath);
@@ -112,15 +115,15 @@ describe('IGExporter', () => {
       expect(content).toEqual(
         [
           '[IG]',
-          '; ***********************************************************************************************',
-          '; *                               WARNING: DO NOT EDIT THIS FILE                                *',
-          '; *                                                                                             *',
-          '; * This file is generated by SUSHI. Any edits you make to this file will be overwritten.       *',
-          '; *                                                                                             *',
-          '; * This ig.ini was generated using the template property in config.yaml. To provide your own   *',
-          '; * ig.ini, create an ig.ini file in the ig-data folder with required properties: ig, template. *',
-          '; * See: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root                   *',
-          '; ***********************************************************************************************',
+          '; ***************************************************************************************************',
+          '; *                                 WARNING: DO NOT EDIT THIS FILE                                  *',
+          '; *                                                                                                 *',
+          '; * This file is generated by SUSHI. Any edits you make to this file will be overwritten.           *',
+          '; *                                                                                                 *',
+          '; * This ig.ini was generated using the template property in sushi-config.yaml. To provide your own *',
+          '; * ig.ini, create an ig.ini file in the ig-data folder with required properties: ig, template.     *',
+          '; * See: https://build.fhir.org/ig/FHIR/ig-guidance/using-templates.html#root                       *',
+          '; ***************************************************************************************************',
           'ig = input/ImplementationGuide-fhir.us.minimal.json',
           'template = hl7.fhir.template#0.0.5',
           ''
@@ -128,11 +131,48 @@ describe('IGExporter', () => {
       );
       expect(loggerSpy.getAllMessages()).toHaveLength(2);
       expect(loggerSpy.getLastMessage('warn')).toMatch(
-        `Found both a "template" property in config.yaml and an ig.ini file at ig-data${path.sep}ig.ini.`
+        `Found both a "template" property in sushi-config.yaml and an ig.ini file at ig-data${path.sep}ig.ini.`
+      );
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        `the ig-data${path.sep}ig.ini file will be ignored.`
       );
       expect(loggerSpy.getLastMessage('info')).toMatch('Generated ig.ini.');
     });
 
+    it('should error if there is an ig-data/ig.ini file and template is defined in the config and not generate or copy an ig.ini', () => {
+      const pkg = new Package(minimalConfig);
+      const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
+      const exporter = new IGExporter(pkg, null, igDataPath, true); // New tank configuration input/fsh/
+      exporter.addIgIni(tempOut);
+      const igIniPath = path.join(tempOut, 'ig.ini');
+      expect(fs.existsSync(igIniPath)).toBeFalsy(); // Does not copy ig.ini to output
+      expect(loggerSpy.getAllMessages()).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        `Found both a "template" property in sushi-config.yaml and an ig.ini file at ig-data${path.sep}ig.ini.`
+      );
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        'The "template" property in sushi-config.yaml has been deprecated and will be ignored'
+      );
+    });
+
+    it('should error if there is no ig.ini file and template is defined in the config (and not generate an ig.ini)', () => {
+      const pkg = new Package(minimalConfig);
+      const igDataPath = path.resolve(
+        __dirname,
+        'fixtures',
+        'customized-ig-with-resources', // NOTE: This fixture does not have an ig.ini
+        'ig-data'
+      );
+      const exporter = new IGExporter(pkg, null, igDataPath, true); // New tank configuration input/fsh/
+      exporter.addIgIni(tempOut);
+      const igIniPath = path.join(tempOut, 'ig.ini');
+      expect(fs.existsSync(igIniPath)).toBeFalsy(); // Does not copy ig.ini to output
+      expect(loggerSpy.getAllMessages()).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        'create an ig.ini file in your project folder'
+      );
+    });
+
     it('should use user-provided ig.ini when template is not defined', () => {
       const pkg = new Package(templatelessConfig);
       const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
@@ -194,7 +234,7 @@ describe('IGExporter', () => {
       expect(loggerSpy.getLastMessage('info')).toMatch('Copied ig.ini.');
     });
 
-    it('should merge default values into user-provided ig.ini when template is not defined', () => {
+    it('should not merge default values into user-provided ig.ini and log an error when required properties missing and template is not defined in legacy configuration', () => {
       const pkg = new Package(templatelessConfig);
       const igDataPath = path.resolve(
         __dirname,
@@ -210,33 +250,50 @@ describe('IGExporter', () => {
       expect(content).toEqual(
         [
           '[IG]',
-          '; *****************************************************************************************************',
-          '; *                                  WARNING: DO NOT EDIT THIS FILE                                   *',
-          '; *                                                                                                   *',
-          '; * This file is generated by SUSHI. Any edits you make to this file will be overwritten.             *',
-          '; *                                                                                                   *',
-          '; * This ig.ini was generated by merging in required properties: "ig" and/or "template". Review your  *',
-          `; * original ig-data${path.sep}ig.ini file and add the missing required properties.                             *`,
-          '; *****************************************************************************************************',
+          '; *****************************************************************************************',
+          '; *                            WARNING: DO NOT EDIT THIS FILE                             *',
+          '; *                                                                                       *',
+          '; * This file is generated by SUSHI. Any edits you make to this file will be overwritten. *',
+          '; *                                                                                       *',
+          '; * To change the contents of this file, edit the original source file at:                *',
+          `; * ig-data${path.sep}ig.ini                                                                        *`,
+          '; *****************************************************************************************',
           'usage-stats-opt-out = true',
-          'ig = input/ImplementationGuide-fhir.us.minimal.json',
-          'template = fhir.base.template',
           ''
         ].join(EOL) // Windows: /r/n; Mac: /n
       );
       expect(loggerSpy.getAllMessages()).toHaveLength(3);
-      expect(loggerSpy.getMessageAtIndex(-2, 'warn')).toMatch(
+      expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(
         'The ig.ini file must have an "ig" property'
       );
-      expect(loggerSpy.getLastMessage('warn')).toMatch(
+      expect(loggerSpy.getLastMessage('error')).toMatch(
         'The ig.ini file must have a "template" property'
       );
-      expect(loggerSpy.getLastMessage('info')).toMatch(
-        `Merged ig-data${path.sep}ig.ini w/ default values for missing properties.`
+      expect(loggerSpy.getLastMessage('info')).toMatch('Copied ig.ini.');
+    });
+
+    it('should log an error when missing required properties and not copy provided ig.ini when template is not defined', () => {
+      const pkg = new Package(templatelessConfig);
+      const igDataPath = path.resolve(
+        __dirname,
+        'fixtures',
+        'ig-ini-missing-properties',
+        'ig-data'
+      );
+      const exporter = new IGExporter(pkg, null, igDataPath, true); // New tank configuration input/fsh/
+      exporter.addIgIni(tempOut);
+      const igIniPath = path.join(tempOut, 'ig.ini');
+      expect(fs.existsSync(igIniPath)).toBeFalsy(); // Does not copy ig.ini
+      expect(loggerSpy.getAllMessages()).toHaveLength(2);
+      expect(loggerSpy.getMessageAtIndex(-2, 'error')).toMatch(
+        'The ig.ini file must have an "ig" property'
+      );
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        'The ig.ini file must have a "template" property'
       );
     });
 
-    it('should report deprecated properties in user-provided ig.ini when template is not defined', () => {
+    it('should report deprecated properties in user-provided ig.ini when template is not defined in legacy configuration', () => {
       const pkg = new Package(templatelessConfig);
       const igDataPath = path.resolve(
         __dirname,
@@ -282,5 +339,24 @@ describe('IGExporter', () => {
       );
       expect(loggerSpy.getLastMessage('info')).toMatch('Copied ig.ini.');
     });
+
+    it('should report deprecated properties in user-provided ig.ini and not copy file when template is not defined', () => {
+      const pkg = new Package(templatelessConfig);
+      const igDataPath = path.resolve(
+        __dirname,
+        'fixtures',
+        'ig-ini-with-deprecated-properties',
+        'ig-data'
+      );
+      const exporter = new IGExporter(pkg, null, igDataPath, true); // New tank configuration input/fsh/
+      exporter.addIgIni(tempOut);
+      const igIniPath = path.join(tempOut, 'ig.ini');
+      expect(fs.existsSync(igIniPath)).toBeFalsy(); // Does not copy ig.ini
+      expect(loggerSpy.getAllMessages()).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        `Your ig-data${path.sep}ig.ini file contains the following deprecated properties: ` +
+          'copyrightyear, license, version, ballotstatus, fhirspec, excludexml, excludejson, excludettl, excludeMaps.'
+      );
+    });
   });
 });
diff --git a/test/ig/IGExporter.ignoreWarnings.test.ts b/test/ig/IGExporter.ignoreWarnings.test.ts
new file mode 100644
index 0000000..597c1d7
--- /dev/null
+++ b/test/ig/IGExporter.ignoreWarnings.test.ts
@@ -0,0 +1,86 @@
+import fs from 'fs-extra';
+import path from 'path';
+import temp from 'temp';
+import { IGExporter } from '../../src/ig';
+import { Package } from '../../src/export';
+import { loggerSpy } from '../testhelpers/loggerSpy';
+import { minimalConfig } from '../utils/minimalConfig';
+import { Configuration } from '../../src/fshtypes';
+import { cloneDeep } from 'lodash';
+
+describe('IGExporter', () => {
+  // Track temp files/folders for cleanup
+  temp.track();
+
+  describe('#ignoreWarnings', () => {
+    let tempOut: string;
+    let config: Configuration;
+
+    beforeEach(() => {
+      tempOut = temp.mkdirSync('sushi-test');
+      config = cloneDeep(minimalConfig);
+      loggerSpy.reset();
+    });
+
+    afterEach(() => {
+      temp.cleanupSync();
+    });
+
+    it('should not copy ignoreWarnings.txt file in new IG structure', () => {
+      const pkg = new Package(config);
+      const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
+      const exporter = new IGExporter(pkg, null, igDataPath, true); // New tank configuration input/fsh/
+      exporter.addIgnoreWarningsFile(tempOut);
+      const ignoreWarningsPath = path.join(tempOut, 'input', 'ignoreWarnings.txt');
+      expect(fs.existsSync(ignoreWarningsPath)).toBeFalsy(); // Do not copy user provided file
+      expect(loggerSpy.getAllMessages('info')).toHaveLength(0);
+    });
+
+    it('should not generate ignoreWarnings.txt file in new IG structure', () => {
+      const pkg = new Package(config);
+      const igDataPath = path.resolve(
+        __dirname,
+        'fixtures',
+        'customized-ig-with-resources',
+        'ig-data'
+      );
+      const exporter = new IGExporter(pkg, null, igDataPath, true); // New tank configuration input/fsh/
+      exporter.addIgnoreWarningsFile(tempOut);
+      const ignoreWarningsPath = path.join(tempOut, 'input', 'ignoreWarnings.txt');
+      expect(fs.existsSync(ignoreWarningsPath)).toBeFalsy(); // Do not generate a new file
+      expect(loggerSpy.getAllMessages('info')).toHaveLength(0);
+    });
+
+    it('should copy ignoreWarnings.txt file in legacy mode', () => {
+      const pkg = new Package(config);
+      const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
+      const exporter = new IGExporter(pkg, null, igDataPath); // Legacy tank configuration
+      exporter.addIgnoreWarningsFile(tempOut);
+      const ignoreWarningsPath = path.join(tempOut, 'input', 'ignoreWarnings.txt');
+      expect(fs.existsSync(ignoreWarningsPath)).toBeTruthy(); // Copy user provided file
+      const outputLogDetails = exporter.getOutputLogDetails(ignoreWarningsPath);
+      expect(outputLogDetails.action).toBe('copied');
+      expect(outputLogDetails.inputs).toHaveLength(1);
+      expect(outputLogDetails.inputs[0].endsWith('ignoreWarnings.txt')).toBeTruthy();
+      expect(loggerSpy.getAllMessages('info')).toHaveLength(0);
+    });
+
+    it('should copy ignoreWarnings.txt file in legacy mode', () => {
+      const pkg = new Package(config);
+      const igDataPath = path.resolve(
+        __dirname,
+        'fixtures',
+        'customized-ig-with-resources',
+        'ig-data'
+      );
+      const exporter = new IGExporter(pkg, null, igDataPath); // Legacy tank configuration
+      exporter.addIgnoreWarningsFile(tempOut);
+      const ignoreWarningsPath = path.join(tempOut, 'input', 'ignoreWarnings.txt');
+      expect(fs.existsSync(ignoreWarningsPath)).toBeTruthy(); // Generate a new file
+      const outputLogDetails = exporter.getOutputLogDetails(ignoreWarningsPath);
+      expect(outputLogDetails.action).toBe('generated');
+      expect(outputLogDetails.inputs).toHaveLength(0);
+      expect(loggerSpy.getAllMessages('info')).toHaveLength(0);
+    });
+  });
+});
diff --git a/test/ig/IGExporter.index.test.ts b/test/ig/IGExporter.index.test.ts
index 1f5b400..f52871d 100644
--- a/test/ig/IGExporter.index.test.ts
+++ b/test/ig/IGExporter.index.test.ts
@@ -18,23 +18,20 @@ describe('IGExporter', () => {
     let indexXMLPagesPath: string;
     let igPath: string;
 
-    beforeAll(() => {
+    beforeEach(() => {
       tempOut = temp.mkdirSync('sushi-test');
       indexMarkdownPageContentPath = path.join(tempOut, 'input', 'pagecontent', 'index.md');
       indexXMLPageContentPath = path.join(tempOut, 'input', 'pagecontent', 'index.xml');
       indexMarkdownPagesPath = path.join(tempOut, 'input', 'pages', 'index.md');
       indexXMLPagesPath = path.join(tempOut, 'input', 'pages', 'index.xml');
       igPath = path.join(tempOut, 'input', 'ImplementationGuide-fhir.us.minimal.json');
+      loggerSpy.reset();
     });
 
-    afterAll(() => {
+    afterEach(() => {
       temp.cleanupSync();
     });
 
-    beforeEach(() => {
-      loggerSpy.reset();
-    });
-
     it('should not export index file when config.indexPageContent is not defined and none provided', () => {
       const pkg = new Package(minimalConfig);
       const exporter = new IGExporter(pkg, null, '');
@@ -205,7 +202,8 @@ describe('IGExporter', () => {
     it('should use config.indexPageContent to generate an index.md file', () => {
       const config = { ...minimalConfig, indexPageContent: 'An index file defined in config' };
       const pkg = new Package(config);
-      const exporter = new IGExporter(pkg, null, '');
+      const igDataPath = path.resolve(__dirname, 'fixtures', 'simple-ig', 'ig-data');
+      const exporter = new IGExporter(pkg, null, igDataPath);
       exporter.initIG();
       exporter.addIndex(tempOut);
       exporter.addImplementationGuide(tempOut);
@@ -215,7 +213,7 @@ describe('IGExporter', () => {
       expect(content).toMatch(/^<!-- index.md {% comment %}$/m);
       expect(content).toMatch(/^\*\s+WARNING: DO NOT EDIT THIS FILE\s+\*$/m);
       expect(content).toMatch(
-        /^\*\s+To change the contents of this file, edit the "indexPageContent" attribute in the tank config.yaml file\s+\*/m
+        /^\*\s+To change the contents of this file, edit the "indexPageContent" attribute in the tank sushi-config.yaml file\s+\*/m
       );
       expect(content).toMatch(
         /^\*\s+or provide your own index file in the ig-data[\/\\]input[\/\\]pagecontent or ig-data[\/\\]input[\/\\]pages folder.\s+\*/m
@@ -237,7 +235,7 @@ describe('IGExporter', () => {
       expect(loggerSpy.getAllMessages()).toHaveLength(loggerSpy.getAllMessages('info').length);
     });
 
-    it('should log a warning if config.indexPageContent defined and user provided index file', () => {
+    it('should log a warning if config.indexPageContent defined and user provided index file and generate index.md in legacy configuration', () => {
       const config = { ...minimalConfig, indexPageContent: 'An index file defined in config' };
       const pkg = new Package(config);
       const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
@@ -265,7 +263,49 @@ describe('IGExporter', () => {
       expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
       expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
       expect(loggerSpy.getLastMessage('warn')).toMatch(
-        /Found both an "indexPageContent" property in config\.yaml and an index file.*File: .*index.md/s
+        /Found both an "indexPageContent" property in sushi-config\.yaml and an index file.*File: .*index.md/s
+      );
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        `ig-data${path.sep}input${path.sep}pagecontent${path.sep}index.md file will be ignored`
+      );
+    });
+
+    it('should log a warning if config.indexPageContent defined and use user provided file but not copy file', () => {
+      const config = { ...minimalConfig, indexPageContent: 'An index file defined in config' };
+      const pkg = new Package(config);
+      const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
+      const exporter = new IGExporter(pkg, null, igDataPath, true); // New tank configuration input/fsh/;
+      exporter.initIG();
+      exporter.addIndex(tempOut);
+      exporter.addImplementationGuide(tempOut);
+      const indexPath = path.join(tempOut, 'input', 'pagecontent', 'index.md');
+      expect(fs.existsSync(indexPath)).toBeFalsy(); // Do not copy user provided file or generate a new file
+
+      // Checks that the index.md file is added to IG definition
+      igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
+      expect(fs.existsSync(igPath)).toBeTruthy();
+      const igContent = fs.readJSONSync(igPath);
+      expect(igContent.definition.page.page).toEqual([
+        {
+          nameUrl: 'index.html',
+          title: 'Home',
+          generation: 'markdown'
+        }
+      ]);
+
+      // No errors logged, warning logged that both indexPageContent and index file provided
+      expect(loggerSpy.getAllMessages('error')).toHaveLength(0);
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /Found both an "indexPageContent" property in sushi-config\.yaml and an index file.*File: .*index.md/s
+      );
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /the "indexPageContent" property in the sushi-config\.yaml will be ignored/s
       );
     });
   });
diff --git a/test/ig/IGExporter.menu-xml.test.ts b/test/ig/IGExporter.menu-xml.test.ts
index 9f9c40d..2798fa8 100644
--- a/test/ig/IGExporter.menu-xml.test.ts
+++ b/test/ig/IGExporter.menu-xml.test.ts
@@ -21,18 +21,15 @@ describe('IGExporter', () => {
   describe('#menu-xml', () => {
     let tempOut: string;
 
-    beforeAll(() => {
+    beforeEach(() => {
       tempOut = temp.mkdirSync('sushi-test');
+      loggerSpy.reset();
     });
 
-    afterAll(() => {
+    afterEach(() => {
       temp.cleanupSync();
     });
 
-    beforeEach(() => {
-      loggerSpy.reset();
-    });
-
     it('should do nothing when config.menu is undefined and none provided', () => {
       const pkg = new Package(minimalConfig);
       const igDataPath = path.resolve(__dirname, 'fixtures', 'simple-ig', 'ig-data');
@@ -62,7 +59,7 @@ describe('IGExporter', () => {
       expect(loggerSpy.getAllMessages()).toHaveLength(0);
     });
 
-    it('should use config.menu when defined even with user-provided menu.xml present and log a warning', () => {
+    it('should use config.menu when defined even with user-provided menu.xml present and log a warning in legacy configuration', () => {
       const config = { ...minimalConfig };
       config.menu = [{ name: 'Animals', url: 'animals.html' }];
       const pkg = new Package(config);
@@ -75,7 +72,7 @@ describe('IGExporter', () => {
       expect(content).toMatch(/^<!-- menu.xml {% comment %}$/m);
       expect(content).toMatch(/^\*\s+WARNING: DO NOT EDIT THIS FILE\s+\*$/m);
       expect(content).toMatch(
-        /^\*\s+To change the contents of this file, edit the "menu" attribute in the tank config.yaml file\s+\*$/m
+        /^\*\s+To change the contents of this file, edit the "menu" attribute in the tank sushi-config.yaml file\s+\*$/m
       );
       expect(content).toMatch(
         /^\*\s+or provide your own menu\.xml in the ig-data[\/\\]input[\/\\]includes folder\s+\*$/m
@@ -84,11 +81,33 @@ describe('IGExporter', () => {
 
       expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
       expect(loggerSpy.getLastMessage()).toMatch(
-        /Found both a "menu" property in config.yaml and a menu.xml file.*File: .*menu.xml/s
+        /Found both a "menu" property in sushi-config.yaml and a menu.xml file.*File: .*menu.xml/s
+      );
+      expect(loggerSpy.getLastMessage()).toMatch(
+        `ig-data${path.sep}input${path.sep}includes${path.sep}menu.xml file will be ignored`
       );
     });
 
-    it('should build simple menu.xml when provided in config.menu', () => {
+    it('should use user-provided menu.xml even when config.menu when defined and log a warning', () => {
+      const config = { ...minimalConfig };
+      config.menu = [{ name: 'Animals', url: 'animals.html' }];
+      const pkg = new Package(config);
+      const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
+      const exporter = new IGExporter(pkg, null, igDataPath, true); // New tank configuration input/fsh/;
+      exporter.addMenuXML(tempOut);
+      const menuPath = path.join(tempOut, 'fsh-generated', 'includes', 'menu.xml');
+      expect(fs.existsSync(menuPath)).toBeFalsy(); // Should not copy file or generate new one
+
+      expect(loggerSpy.getAllMessages('warn')).toHaveLength(1);
+      expect(loggerSpy.getLastMessage()).toMatch(
+        /Found both a "menu" property in sushi-config.yaml and a menu.xml file.*File: .*menu.xml/s
+      );
+      expect(loggerSpy.getLastMessage()).toMatch(
+        /the "menu" property in the sushi-config\.yaml will be ignored/s
+      );
+    });
+
+    it('should build simple menu.xml when provided in config.menu in legacy configuration', () => {
       const config = { ...minimalConfig };
       config.menu = [
         { name: 'Animals', url: 'animals.html' },
@@ -96,7 +115,8 @@ describe('IGExporter', () => {
         { name: 'Other' }
       ];
       const pkg = new Package(config);
-      const exporter = new IGExporter(pkg, null, '');
+      const igDataPath = path.resolve(__dirname, 'fixtures', 'simple-ig', 'ig-data');
+      const exporter = new IGExporter(pkg, null, igDataPath);
       exporter.addMenuXML(tempOut);
       const menuPath = path.join(tempOut, 'input', 'includes', 'menu.xml');
       expect(fs.existsSync(menuPath)).toBeTruthy();
@@ -104,7 +124,33 @@ describe('IGExporter', () => {
       expect(content).toMatch(/^<!-- menu.xml {% comment %}$/m);
       expect(content).toMatch(/^\*\s+WARNING: DO NOT EDIT THIS FILE\s+\*$/m);
       expect(content).toMatch(
-        /^\*\s+To change the contents of this file, edit the "menu" attribute in the tank config.yaml file\s+\*$/m
+        /^\*\s+To change the contents of this file, edit the "menu" attribute in the tank sushi-config.yaml file\s+\*$/m
+      );
+      expect(content).toMatch(
+        /^\*\s+or provide your own menu\.xml in the ig-data[\/\\]input[\/\\]includes folder\s+\*$/m
+      );
+      expect(content).toContain(simpleMenuXMLContent.replace(/\n/g, EOL));
+      expect(loggerSpy.getAllMessages()).toHaveLength(0);
+    });
+
+    it('should build simple menu.xml when provided in config.menu', () => {
+      const config = { ...minimalConfig };
+      config.menu = [
+        { name: 'Animals', url: 'animals.html' },
+        { name: 'Plants', url: 'plants.html' },
+        { name: 'Other' }
+      ];
+      const pkg = new Package(config);
+      const igDataPath = path.resolve(__dirname, 'fixtures', 'simple-ig', 'ig-data');
+      const exporter = new IGExporter(pkg, null, igDataPath, true);
+      exporter.addMenuXML(tempOut);
+      const menuPath = path.join(tempOut, 'fsh-generated', 'includes', 'menu.xml');
+      expect(fs.existsSync(menuPath)).toBeTruthy();
+      const content = fs.readFileSync(menuPath, 'utf8');
+      expect(content).toMatch(/^<!-- menu.xml {% comment %}$/m);
+      expect(content).toMatch(/^\*\s+WARNING: DO NOT EDIT THIS FILE\s+\*$/m);
+      expect(content).toMatch(
+        /^\*\s+To change the contents of this file, edit the "menu" attribute in the tank sushi-config.yaml file\s+\*$/m
       );
       expect(content).toMatch(
         /^\*\s+or provide your own menu\.xml in the ig-data[\/\\]input[\/\\]includes folder\s+\*$/m
@@ -121,9 +167,9 @@ describe('IGExporter', () => {
         { name: 'Other' }
       ];
       const pkg = new Package(config);
-      const exporter = new IGExporter(pkg, null, '');
+      const exporter = new IGExporter(pkg, null, '', true);
       exporter.addMenuXML(tempOut);
-      const menuPath = path.join(tempOut, 'input', 'includes', 'menu.xml');
+      const menuPath = path.join(tempOut, 'fsh-generated', 'includes', 'menu.xml');
       expect(fs.existsSync(menuPath)).toBeTruthy();
       const content = fs.readFileSync(menuPath, 'utf8');
       expect(content).toContain(simpleMenuXMLWithOpenInNewTabContent.replace(/\n/g, EOL));
@@ -138,9 +184,9 @@ describe('IGExporter', () => {
         { name: 'Other' }
       ];
       const pkg = new Package(config);
-      const exporter = new IGExporter(pkg, null, '');
+      const exporter = new IGExporter(pkg, null, '', true);
       exporter.addMenuXML(tempOut);
-      const menuPath = path.join(tempOut, 'input', 'includes', 'menu.xml');
+      const menuPath = path.join(tempOut, 'fsh-generated', 'includes', 'menu.xml');
       expect(fs.existsSync(menuPath)).toBeTruthy();
       const content = fs.readFileSync(menuPath, 'utf8');
       expect(content).toContain(simpleMenuXMLWithExternalContent.replace(/\n/g, EOL));
@@ -161,9 +207,9 @@ describe('IGExporter', () => {
         }
       ];
       const pkg = new Package(config);
-      const exporter = new IGExporter(pkg, null, '');
+      const exporter = new IGExporter(pkg, null, '', true);
       exporter.addMenuXML(tempOut);
-      const menuPath = path.join(tempOut, 'input', 'includes', 'menu.xml');
+      const menuPath = path.join(tempOut, 'fsh-generated', 'includes', 'menu.xml');
       expect(fs.existsSync(menuPath)).toBeTruthy();
       const content = fs.readFileSync(menuPath, 'utf8');
       expect(content).toContain(subMenuXMLContent.replace(/\n/g, EOL));
@@ -186,9 +232,9 @@ describe('IGExporter', () => {
         }
       ];
       const pkg = new Package(config);
-      const exporter = new IGExporter(pkg, null, '');
+      const exporter = new IGExporter(pkg, null, '', true);
       exporter.addMenuXML(tempOut);
-      const menuPath = path.join(tempOut, 'input', 'includes', 'menu.xml');
+      const menuPath = path.join(tempOut, 'fsh-generated', 'includes', 'menu.xml');
       expect(fs.existsSync(menuPath)).toBeTruthy();
       const content = fs.readFileSync(menuPath, 'utf8');
       expect(content).toContain(subMenuWithWarningXMLContent.replace(/\n/g, EOL));
diff --git a/test/ig/IGExporter.package-list.test.ts b/test/ig/IGExporter.package-list.test.ts
index 302e148..f42077c 100644
--- a/test/ig/IGExporter.package-list.test.ts
+++ b/test/ig/IGExporter.package-list.test.ts
@@ -16,15 +16,13 @@ describe('IGExporter', () => {
     let tempOut: string;
     let config: Configuration;
 
-    beforeAll(() => {
-      tempOut = temp.mkdirSync('sushi-test');
-    });
-
     beforeEach(() => {
+      tempOut = temp.mkdirSync('sushi-test');
       config = cloneDeep(minimalConfig);
+      loggerSpy.reset();
     });
 
-    afterAll(() => {
+    afterEach(() => {
       temp.cleanupSync();
     });
 
@@ -73,8 +71,11 @@ describe('IGExporter', () => {
       const outputLogDetails = exporter.getOutputLogDetails(pkgListPath);
       expect(outputLogDetails.action).toBe('generated');
       expect(outputLogDetails.inputs).toHaveLength(1);
-      expect(outputLogDetails.inputs[0].endsWith('config.yaml')).toBeTruthy();
+      expect(outputLogDetails.inputs[0].endsWith('sushi-config.yaml')).toBeTruthy();
       expect(loggerSpy.getLastMessage('info')).toBe('Generated package-list.json');
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /Use of "history" property in configuration is deprecated/
+      );
     });
 
     it('should export package-list.json and emit a warning when both config.history and ig-data/package-list.json are defined', () => {
@@ -113,14 +114,14 @@ describe('IGExporter', () => {
       const outputLogDetails = exporter.getOutputLogDetails(pkgListPath);
       expect(outputLogDetails.action).toBe('generated');
       expect(outputLogDetails.inputs).toHaveLength(1);
-      expect(outputLogDetails.inputs[0].endsWith('config.yaml')).toBeTruthy();
+      expect(outputLogDetails.inputs[0].endsWith('sushi-config.yaml')).toBeTruthy();
       expect(loggerSpy.getLastMessage('info')).toBe('Generated package-list.json');
       expect(loggerSpy.getLastMessage('warn')).toMatch(
-        /Found both a "history" property in config\.yaml and a package-list\.json file.*File: .*package-list.json/s
+        /Found both a "history" property in sushi-config\.yaml and a package-list\.json file.*File: .*package-list.json/s
       );
     });
 
-    it('should copy package-list.json when ig-data/package-list.json is defined', () => {
+    it('should copy package-list.json when ig-data/package-list.json is defined in legacy configuration', () => {
       const pkg = new Package(config);
       const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
       const exporter = new IGExporter(pkg, null, igDataPath);
@@ -134,7 +135,56 @@ describe('IGExporter', () => {
       expect(outputLogDetails.action).toBe('copied');
       expect(outputLogDetails.inputs).toHaveLength(1);
       expect(outputLogDetails.inputs[0].endsWith('package-list.json')).toBeTruthy();
-      expect(loggerSpy.getLastMessage('info')).toBe('Copied ig-data/package-list.json.');
+      expect(loggerSpy.getLastMessage('info')).toBe(`Copied ig-data${path.sep}package-list.json.`);
+    });
+
+    it('should not copy package-list.json even if ig-data/package-list.json is defined', () => {
+      const pkg = new Package(config);
+      const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
+      const exporter = new IGExporter(pkg, null, igDataPath, true); // New tank configuration input/fsh/
+      exporter.addPackageList(tempOut);
+      const pkgListPath = path.join(tempOut, 'package-list.json');
+      expect(fs.existsSync(pkgListPath)).toBeFalsy(); // Do not copy user provided file or generate a new file
+      expect(exporter.getOutputLogDetails(pkgListPath)).toBeUndefined();
+      expect(loggerSpy.getAllMessages('info')).toHaveLength(0);
+    });
+
+    it('should log an error when a "history" is specified in the non-legacy IgPubContext', () => {
+      config.history = {
+        'package-id': 'fhir.us.example',
+        canonical: 'http://hl7.org/fhir/us/example',
+        title: 'HL7 FHIR Implementation Guide: Example IG Release 1 - US Realm | STU1',
+        introduction: 'Example IG exercises many of the fields in a SUSHI configuration.',
+        list: [
+          {
+            version: 'current',
+            desc: 'Continuous Integration Build (latest in version control)',
+            path: 'http://build.fhir.org/ig/HL7/example-ig/',
+            status: 'ci-build',
+            current: true
+          },
+          {
+            version: '0.9.1',
+            fhirversion: '4.0.0',
+            date: '2019-06-10',
+            desc: 'Initial STU ballot (Sep 2019 Ballot)',
+            path: 'https://hl7.org/fhir/us/example/2019Sep/',
+            status: 'ballot',
+            sequence: 'STU 1'
+          }
+        ]
+      };
+      const pkg = new Package(config);
+      const igDataPath = path.resolve(__dirname, 'fixtures', 'customized-ig', 'ig-data');
+      const exporter = new IGExporter(pkg, null, igDataPath, true); // New tank configuration input/fsh/
+      exporter.addPackageList(tempOut);
+      const pkgListPath = path.join(tempOut, 'package-list.json');
+      expect(fs.existsSync(pkgListPath)).toBeFalsy(); // Do not copy user provided file or generate a new file
+      expect(exporter.getOutputLogDetails(pkgListPath)).toBeUndefined();
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Detected "history" property in configuration/
+      );
+      expect(loggerSpy.getLastMessage('error')).toMatch(JSON.stringify(config.history, null, 2));
     });
   });
 });
diff --git a/test/ig/IGExporter.test.ts b/test/ig/IGExporter.test.ts
index dd9957f..915a40b 100644
--- a/test/ig/IGExporter.test.ts
+++ b/test/ig/IGExporter.test.ts
@@ -18,7 +18,7 @@ describe('IGExporter', () => {
       const configYaml = fs.readFileSync(configPath, 'utf8');
       const config = importConfiguration(configYaml, configPath);
       const pkg = new Package(config);
-      const exporter = new IGExporter(pkg, null, path.join(__dirname, 'ig-data'));
+      const exporter = new IGExporter(pkg, null, path.join(__dirname, 'ig-data'), true);
       exporter.export(tempOut);
     });
 
@@ -34,21 +34,28 @@ describe('IGExporter', () => {
       // no menu.xml
       const menuPath = path.join(tempOut, 'input', 'includes', 'menu.xml');
       expect(fs.existsSync(menuPath)).toBeFalsy();
-      // generated ig.ini
+      // no ig.ini
       const igIniPath = path.join(tempOut, 'ig.ini');
-      expect(fs.existsSync(igIniPath)).toBeTruthy();
+      expect(fs.existsSync(igIniPath)).toBeFalsy();
       // generated implementation guide
-      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-fhir.us.minimal.json');
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
       expect(fs.existsSync(igPath)).toBeTruthy();
     });
 
-    it('should put default content into ig.ini and ImplementationGuide files', () => {
-      const igIniPath = path.join(tempOut, 'ig.ini');
-      const igPath = path.join(tempOut, 'input', 'ImplementationGuide-fhir.us.minimal.json');
-      const igIniContent = fs.readFileSync(igIniPath, 'utf8');
+    it('should put default content into ImplementationGuide file', () => {
+      const igPath = path.join(
+        tempOut,
+        'fsh-generated',
+        'resources',
+        'ImplementationGuide-fhir.us.minimal.json'
+      );
       const igContent = fs.readJSONSync(igPath);
 
-      expect(igIniContent).toMatch('template = hl7.fhir.template#0.0.5');
       expect(igContent.id).toBe('fhir.us.minimal');
       expect(igContent.definition.resource).toHaveLength(0);
     });
diff --git a/test/import/ensureConfigurationFile.test.ts b/test/import/ensureConfigurationFile.test.ts
index 8ae9a60..c3ccf2a 100644
--- a/test/import/ensureConfigurationFile.test.ts
+++ b/test/import/ensureConfigurationFile.test.ts
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
 import temp from 'temp';
 import YAML from 'yaml';
 import { loggerSpy } from '../testhelpers/loggerSpy';
-import { ensureConfigurationFile } from '../../src/import';
+import { ensureConfiguration } from '../../src/import';
 
 describe('ensureConfigurationFile', () => {
   // Track temp files/folders for cleanup
@@ -17,25 +17,41 @@ describe('ensureConfigurationFile', () => {
     loggerSpy.reset();
   });
 
-  it('should return the path to a pre-existing config.yaml', () => {
-    const tank = path.join(__dirname, 'fixtures', 'existing-config-yaml');
-    const configPath = ensureConfigurationFile(tank);
-    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+  it('should return the path to a pre-existing sushi-config.yaml', () => {
+    const tank = path.join(__dirname, 'fixtures', 'existing-sushi-config-yaml');
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yaml'));
     // Generating a config causes a warning, so there should be NO warnings in this case
     expect(loggerSpy.getAllLogs('warn')).toHaveLength(0);
   });
 
-  it('should return the path to a pre-existing config.yml', () => {
-    const tank = path.join(__dirname, 'fixtures', 'existing-config-yml');
-    const configPath = ensureConfigurationFile(tank);
-    expect(configPath).toBe(path.join(tank, 'config.yml'));
+  it('should return the path to a pre-existing sushi-config.yml', () => {
+    const tank = path.join(__dirname, 'fixtures', 'existing-sushi-config-yml');
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yml'));
     // Generating a config causes a warning, so there should be NO warnings in this case
     expect(loggerSpy.getAllLogs('warn')).toHaveLength(0);
   });
 
+  it('should return the path to a pre-existing config.yaml and log a warning', () => {
+    const tank = path.join(__dirname, 'fixtures', 'existing-config-yaml');
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    expect(loggerSpy.getAllLogs('warn')).toHaveLength(1);
+    expect(loggerSpy.getLastMessage('warn')).toMatch(/Use of config\.yaml is deprecated/);
+  });
+
+  it('should return the path to a pre-existing config.yml and log a warning', () => {
+    const tank = path.join(__dirname, 'fixtures', 'existing-config-yml');
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'config.yml'));
+    expect(loggerSpy.getAllLogs('warn')).toHaveLength(1);
+    expect(loggerSpy.getLastMessage('warn')).toMatch(/Use of config\.yml is deprecated/);
+  });
+
   it('should return undefined on an empty folder when fromScratch is false', () => {
     const tank = temp.mkdirSync('sushi-test');
-    const configPath = ensureConfigurationFile(tank, false);
+    const configPath = ensureConfiguration(tank, false);
     expect(configPath).toBeUndefined();
     // Generating a config causes a warning, so there should be NO warnings in this case
     expect(loggerSpy.getAllLogs('warn')).toHaveLength(0);
@@ -43,8 +59,8 @@ describe('ensureConfigurationFile', () => {
 
   it('should generate a default config on an empty folder when fromScratch is true', () => {
     const tank = temp.mkdirSync('sushi-test');
-    const configPath = ensureConfigurationFile(tank, true);
-    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    const configPath = ensureConfiguration(tank, true);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
     );
@@ -115,8 +131,8 @@ describe('ensureConfigurationFile', () => {
     // Copy the fixture to a temp folder since we actually create files in the tank
     const tank = temp.mkdirSync('sushi-test');
     fs.copySync(path.join(__dirname, 'fixtures', 'package-json-only'), tank);
-    const configPath = ensureConfigurationFile(tank);
-    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
     );
@@ -197,9 +213,9 @@ describe('ensureConfigurationFile', () => {
     delete packageJSON.license;
     fs.writeJsonSync(path.join(tank, 'package.json'), packageJSON);
 
-    // ensureConfigurationFile
-    const configPath = ensureConfigurationFile(tank);
-    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    // ensureConfiguration
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
     );
@@ -264,9 +280,9 @@ describe('ensureConfigurationFile', () => {
     ];
     fs.writeJsonSync(path.join(tank, 'package.json'), packageJSON);
 
-    // ensureConfigurationFile
-    const configPath = ensureConfigurationFile(tank);
-    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    // ensureConfiguration
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
     );
@@ -346,8 +362,8 @@ describe('ensureConfigurationFile', () => {
     // Copy the fixture to a temp folder since we actually create files in the tank
     const tank = temp.mkdirSync('sushi-test');
     fs.copySync(path.join(__dirname, 'fixtures', 'package-json-and-ig-ini'), tank);
-    const configPath = ensureConfigurationFile(tank);
-    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
     );
@@ -422,9 +438,9 @@ describe('ensureConfigurationFile', () => {
     const newIgIni = igIni.split('\n').slice(0, 4).join('\n');
     fs.writeFileSync(path.join(tank, 'ig-data', 'ig.ini'), newIgIni, 'utf8');
 
-    // ensureConfigurationFile
-    const configPath = ensureConfigurationFile(tank);
-    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    // ensureConfiguration
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
     );
@@ -480,8 +496,8 @@ describe('ensureConfigurationFile', () => {
     // Copy the fixture to a temp folder since we actually create files in the tank
     const tank = temp.mkdirSync('sushi-test');
     fs.copySync(path.join(__dirname, 'fixtures', 'package-json-and-package-list'), tank);
-    const configPath = ensureConfigurationFile(tank);
-    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
     );
@@ -581,9 +597,9 @@ describe('ensureConfigurationFile', () => {
     packageList.list[0].desc = 'Continuous Integration Build (latest in version control)';
     fs.writeJsonSync(path.join(tank, 'ig-data', 'package-list.json'), packageList);
 
-    // ensureConfigurationFile
-    const configPath = ensureConfigurationFile(tank);
-    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    // ensureConfiguration
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
     );
@@ -657,8 +673,8 @@ describe('ensureConfigurationFile', () => {
     // Copy the fixture to a temp folder since we actually create files in the tank
     const tank = temp.mkdirSync('sushi-test');
     fs.copySync(path.join(__dirname, 'fixtures', 'package-json-and-menu-xml'), tank);
-    const configPath = ensureConfigurationFile(tank);
-    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
     );
@@ -745,8 +761,8 @@ describe('ensureConfigurationFile', () => {
     // Copy the fixture to a temp folder since we actually create files in the tank
     const tank = temp.mkdirSync('sushi-test');
     fs.copySync(path.join(__dirname, 'fixtures', 'package-json-and-index-md'), tank);
-    const configPath = ensureConfigurationFile(tank);
-    expect(configPath).toBe(path.join(tank, 'config.yaml'));
+    const configPath = ensureConfiguration(tank);
+    expect(configPath).toBe(path.join(tank, 'sushi-config.yaml'));
     expect(loggerSpy.getLastMessage('warn')).toBe(
       `Generated new configuration file: ${configPath}. Please review to ensure configuration is correct.`
     );
@@ -795,6 +811,6 @@ describe('ensureConfigurationFile', () => {
       // indexPageContent should not exist since the provided index.md is used
     });
 
-    // Nothing is added as a commented pair to the config.yaml because there is not configuration equivalent
+    // Nothing is added as a commented pair to the sushi-config.yaml because there is not configuration equivalent
   });
 });
diff --git a/test/import/fixtures/existing-sushi-config-yaml/sushi-config.yaml b/test/import/fixtures/existing-sushi-config-yaml/sushi-config.yaml
new file mode 100644
index 0000000..b260cdd
--- /dev/null
+++ b/test/import/fixtures/existing-sushi-config-yaml/sushi-config.yaml
@@ -0,0 +1,9 @@
+id: fhir.us.minimal
+canonical: http://hl7.org/fhir/us/minimal
+name: MinimalIG
+status: draft
+version: 1.0.0
+fhirVersion: 4.0.1
+copyrightYear: 2020+
+releaseLabel: Build CI
+template: hl7.fhir.template#0.0.5
\ No newline at end of file
diff --git a/test/import/fixtures/existing-sushi-config-yml/sushi-config.yml b/test/import/fixtures/existing-sushi-config-yml/sushi-config.yml
new file mode 100644
index 0000000..b260cdd
--- /dev/null
+++ b/test/import/fixtures/existing-sushi-config-yml/sushi-config.yml
@@ -0,0 +1,9 @@
+id: fhir.us.minimal
+canonical: http://hl7.org/fhir/us/minimal
+name: MinimalIG
+status: draft
+version: 1.0.0
+fhirVersion: 4.0.1
+copyrightYear: 2020+
+releaseLabel: Build CI
+template: hl7.fhir.template#0.0.5
\ No newline at end of file
diff --git a/test/utils/Processing.test.ts b/test/utils/Processing.test.ts
index 3a51da3..98c0f22 100644
--- a/test/utils/Processing.test.ts
+++ b/test/utils/Processing.test.ts
@@ -1,3 +1,4 @@
+import axios from 'axios';
 import fs from 'fs-extra';
 import path from 'path';
 import temp from 'temp';
@@ -27,7 +28,10 @@ describe('Processing', () => {
 
     beforeAll(() => {
       tempRoot = temp.mkdirSync('sushi-test');
-      fs.mkdirpSync(path.join(tempRoot, 'has-fsh', 'fsh'));
+      fs.mkdirpSync(path.join(tempRoot, 'has-fsh', 'fsh')); // TODO: Tests legacy support. Remove when no longer supported.
+      fs.mkdirpSync(path.join(tempRoot, 'has-input-fsh', 'input', 'fsh'));
+      fs.mkdirpSync(path.join(tempRoot, 'has-fsh-and-input-fsh', 'fsh')); // TODO: Tests legacy support. Remove when no longer supported.
+      fs.mkdirpSync(path.join(tempRoot, 'has-fsh-and-input-fsh', 'input', 'fsh'));
       fs.mkdirSync(path.join(tempRoot, 'no-fsh'));
     });
 
@@ -40,10 +44,27 @@ describe('Processing', () => {
       expect(foundInput).toBe('.');
     });
 
+    // TODO: Tests legacy support. Remove when no longer supported.
     it('should find a path to the fsh subdirectory if present', () => {
       const input = path.join(tempRoot, 'has-fsh');
       const foundInput = findInputDir(input);
       expect(foundInput).toBe(path.join(tempRoot, 'has-fsh', 'fsh'));
+      expect(loggerSpy.getLastMessage('warn')).toMatch(
+        /Use of this folder is DEPRECATED and will be REMOVED in a future release/s
+      );
+    });
+
+    it('should find a path to the input/fsh subdirectory if present', () => {
+      const input = path.join(tempRoot, 'has-input-fsh');
+      const foundInput = findInputDir(input);
+      expect(foundInput).toBe(path.join(tempRoot, 'has-input-fsh', 'input', 'fsh'));
+    });
+
+    // TODO: Tests legacy support. Remove when no longer supported.
+    it('should prefer path to input/fsh over fsh/ if both present', () => {
+      const input = path.join(tempRoot, 'has-fsh-and-input-fsh');
+      const foundInput = findInputDir(input);
+      expect(foundInput).toBe(path.join(tempRoot, 'has-fsh-and-input-fsh', 'input', 'fsh'));
     });
 
     it('should find a path to the provided directory if the fsh subdirectory is not present', () => {
@@ -55,12 +76,18 @@ describe('Processing', () => {
 
   describe('#ensureOutputDir()', () => {
     let tempRoot: string;
+    let emptyDirSpy: jest.SpyInstance;
 
     beforeAll(() => {
       tempRoot = temp.mkdirSync('sushi-test');
       fs.mkdirSync(path.join(tempRoot, 'my-input'));
     });
 
+    beforeEach(() => {
+      emptyDirSpy = jest.spyOn(fs, 'emptyDirSync').mockImplementation(() => '');
+      emptyDirSpy.mockReset();
+    });
+
     afterAll(() => {
       temp.cleanupSync();
     });
@@ -68,25 +95,64 @@ describe('Processing', () => {
     it('should use and create the output directory when it is provided', () => {
       const input = path.join(tempRoot, 'my-input');
       const output = path.join(tempRoot, 'my-output');
-      const igContextOutputDir = ensureOutputDir(input, output, true);
-      const nonIgContextOutputDir = ensureOutputDir(input, output, false);
+      const legacyIgContextOutputDir = ensureOutputDir(input, output, false, true);
+      const igContextOutputDir = ensureOutputDir(input, output, true, false);
+      const nonIgContextOutputDir = ensureOutputDir(input, output, false, false);
       expect(igContextOutputDir).toBe(output);
       expect(nonIgContextOutputDir).toBe(output);
+      expect(legacyIgContextOutputDir).toBe(output);
       expect(fs.existsSync(output)).toBeTruthy();
     });
 
     it('should default the output directory to "build" when not running in IG Publisher context', () => {
       const input = path.join(tempRoot, 'my-input');
-      const outputDir = ensureOutputDir(input, undefined, false);
+      const outputDir = ensureOutputDir(input, undefined, false, false);
       expect(outputDir).toBe('build');
       expect(fs.existsSync(outputDir)).toBeTruthy();
     });
 
-    it('should default the output directory to the parent of the input when running in IG Publisher context', () => {
+    it('should default the output directory to the parent of the input when running in legacy IG Publisher context', () => {
       const input = path.join(tempRoot, 'my-input');
-      const outputDir = ensureOutputDir(input, undefined, true);
+      const outputDir = ensureOutputDir(input, undefined, false, true);
+      expect(outputDir).toBe(tempRoot);
+      expect(fs.existsSync(outputDir)).toBeTruthy();
+    });
+
+    it('should default the output directory to the grandparent of the input when running in IG Publisher context', () => {
+      const input = path.join(tempRoot, 'my-input', 'my-fsh');
+      const outputDir = ensureOutputDir(input, undefined, true, false);
+      expect(outputDir).toBe(tempRoot);
+      expect(fs.existsSync(outputDir)).toBeTruthy();
+    });
+
+    it('should empty the fsh-generated folder if the output directory contains one', () => {
+      jest
+        .spyOn(fs, 'existsSync')
+        .mockImplementationOnce(dir => dir === path.join(tempRoot, 'fsh-generated'));
+      const input = path.join(tempRoot, 'my-input', 'my-fsh');
+      const outputDir = ensureOutputDir(input, undefined, true, false);
       expect(outputDir).toBe(tempRoot);
       expect(fs.existsSync(outputDir)).toBeTruthy();
+      expect(emptyDirSpy.mock.calls).toHaveLength(1);
+      expect(emptyDirSpy.mock.calls[0][0]).toBe(path.join(tempRoot, 'fsh-generated'));
+    });
+
+    it('should log an error when emptying the directory fails', () => {
+      emptyDirSpy = emptyDirSpy.mockImplementation(() => {
+        throw Error('foo');
+      });
+      jest
+        .spyOn(fs, 'existsSync')
+        .mockImplementationOnce(dir => dir === path.join(tempRoot, 'fsh-generated'));
+      const input = path.join(tempRoot, 'my-input', 'my-fsh');
+      const outputDir = ensureOutputDir(input, undefined, true, false);
+      expect(outputDir).toBe(tempRoot);
+      expect(fs.existsSync(outputDir)).toBeTruthy();
+      expect(emptyDirSpy.mock.calls).toHaveLength(1);
+      expect(emptyDirSpy.mock.calls[0][0]).toBe(path.join(tempRoot, 'fsh-generated'));
+      expect(loggerSpy.getLastMessage('error')).toMatch(
+        /Unable to empty existing fsh-generated folder.*: foo/
+      );
     });
   });
 
@@ -95,11 +161,11 @@ describe('Processing', () => {
       loggerSpy.reset();
     });
 
-    it('should return the contents of config.yaml from the input directory', () => {
+    it('should return the contents of sushi-config.yaml from the input directory', () => {
       const input = path.join(__dirname, 'fixtures', 'valid-yaml');
       const config = readConfig(input);
       expect(config).toEqual({
-        filePath: path.join(__dirname, 'fixtures', 'valid-yaml', 'config.yaml'),
+        filePath: path.join(__dirname, 'fixtures', 'valid-yaml', 'sushi-config.yaml'),
         id: 'sushi-test',
         packageId: 'sushi-test',
         canonical: 'http://hl7.org/fhir/sushi-test',
@@ -148,15 +214,15 @@ describe('Processing', () => {
       });
     });
 
-    it('should log and throw an error when config.yaml is not found in the input directory', () => {
+    it('should log and throw an error when sushi-config.yaml is not found in the input directory', () => {
       const input = path.join(__dirname, 'fixtures', 'no-package');
       expect(() => {
         readConfig(input);
       }).toThrow();
-      expect(loggerSpy.getLastMessage('error')).toMatch(/No config\.yaml/s);
+      expect(loggerSpy.getLastMessage('error')).toMatch(/No sushi-config\.yaml/s);
     });
 
-    it('should log and throw an error when the contents of config.yaml are not valid yaml', () => {
+    it('should log and throw an error when the contents of sushi-config.yaml are not valid yaml', () => {
       const input = path.join(__dirname, 'fixtures', 'invalid-yaml');
       expect(() => {
         readConfig(input);
@@ -260,10 +326,12 @@ describe('Processing', () => {
 
   describe('#writeFHIRResources()', () => {
     let tempRoot: string;
+    let tempIGPubRoot: string;
     let outPackage: Package;
 
     beforeAll(() => {
       tempRoot = temp.mkdirSync('output-dir');
+      tempIGPubRoot = temp.mkdirSync('output-ig-dir');
       const input = path.join(__dirname, 'fixtures', 'valid-yaml');
       const config = readConfig(input);
       outPackage = new Package(config);
@@ -326,83 +394,121 @@ describe('Processing', () => {
         myProfileInstance,
         myOtherInstance
       );
-      writeFHIRResources(tempRoot, outPackage, false);
     });
 
     afterAll(() => {
       temp.cleanupSync();
     });
 
-    it('should write profiles and profile instances to the "profiles" directory', () => {
-      const profilesPath = path.join(tempRoot, 'input', 'profiles');
-      expect(fs.existsSync(profilesPath)).toBeTruthy();
-      const profilesFiles = fs.readdirSync(profilesPath);
-      expect(profilesFiles.length).toBe(2);
-      expect(profilesFiles).toContain('StructureDefinition-my-profile.json');
-      expect(profilesFiles).toContain('StructureDefinition-my-profile-instance.json');
-    });
+    describe('IG Publisher mode', () => {
+      beforeAll(() => {
+        writeFHIRResources(tempIGPubRoot, outPackage, false, true);
+      });
 
-    it('should write extensions and extension instances to the "extensions" directory', () => {
-      const extensionsPath = path.join(tempRoot, 'input', 'extensions');
-      expect(fs.existsSync(extensionsPath)).toBeTruthy();
-      const extensionsFiles = fs.readdirSync(extensionsPath);
-      expect(extensionsFiles.length).toBe(2);
-      expect(extensionsFiles).toContain('StructureDefinition-my-extension.json');
-      expect(extensionsFiles).toContain('StructureDefinition-my-extension-instance.json');
-    });
+      afterAll(() => {
+        temp.cleanupSync();
+      });
 
-    it('should write value sets, code systems, and vocabulary instances to the "vocabulary" directory', () => {
-      const vocabularyPath = path.join(tempRoot, 'input', 'vocabulary');
-      expect(fs.existsSync(vocabularyPath)).toBeTruthy();
-      const vocabularyFiles = fs.readdirSync(vocabularyPath);
-      expect(vocabularyFiles.length).toBe(3);
-      expect(vocabularyFiles).toContain('ValueSet-my-value-set.json');
-      expect(vocabularyFiles).toContain('CodeSystem-my-code-system.json');
-      expect(vocabularyFiles).toContain('ConceptMap-my-concept-map.json');
+      it('should write all resources to the "fsh-generated/resources" directory', () => {
+        const generatedPath = path.join(tempIGPubRoot, 'fsh-generated', 'resources');
+        expect(fs.existsSync(generatedPath)).toBeTruthy();
+        const allGeneratedFiles = fs.readdirSync(generatedPath);
+        expect(allGeneratedFiles.length).toBe(12);
+        expect(allGeneratedFiles).toContain('StructureDefinition-my-profile.json');
+        expect(allGeneratedFiles).toContain('StructureDefinition-my-profile-instance.json');
+        expect(allGeneratedFiles).toContain('StructureDefinition-my-extension.json');
+        expect(allGeneratedFiles).toContain('StructureDefinition-my-extension-instance.json');
+        expect(allGeneratedFiles).toContain('ValueSet-my-value-set.json');
+        expect(allGeneratedFiles).toContain('CodeSystem-my-code-system.json');
+        expect(allGeneratedFiles).toContain('ConceptMap-my-concept-map.json');
+        expect(allGeneratedFiles).toContain('Observation-my-example.json');
+        expect(allGeneratedFiles).toContain('CapabilityStatement-my-capabilities.json');
+        expect(allGeneratedFiles).toContain('StructureDefinition-my-model.json');
+        expect(allGeneratedFiles).toContain('OperationDefinition-my-operation.json');
+        expect(allGeneratedFiles).toContain('Observation-my-other-instance.json');
+      });
     });
 
-    it('should write example instances to the "examples" directory', () => {
-      const examplesPath = path.join(tempRoot, 'input', 'examples');
-      expect(fs.existsSync(examplesPath)).toBeTruthy();
-      const examplesFiles = fs.readdirSync(examplesPath);
-      expect(examplesFiles.length).toBe(1);
-      expect(examplesFiles).toContain('Observation-my-example.json');
-    });
+    describe('legacy IG Publisher mode and legacy flat tank', () => {
+      beforeAll(() => {
+        writeFHIRResources(tempRoot, outPackage, false, false);
+      });
 
-    it('should write capability instances to the "capabilities" directory', () => {
-      const capabilitiesPath = path.join(tempRoot, 'input', 'capabilities');
-      expect(fs.existsSync(capabilitiesPath)).toBeTruthy();
-      const capabilitiesFiles = fs.readdirSync(capabilitiesPath);
-      expect(capabilitiesFiles.length).toBe(1);
-      expect(capabilitiesFiles).toContain('CapabilityStatement-my-capabilities.json');
-    });
+      afterAll(() => {
+        temp.cleanupSync();
+      });
 
-    it('should write model instances to the "models" directory', () => {
-      const modelsPath = path.join(tempRoot, 'input', 'models');
-      expect(fs.existsSync(modelsPath)).toBeTruthy();
-      const modelsFiles = fs.readdirSync(modelsPath);
-      expect(modelsFiles.length).toBe(1);
-      expect(modelsFiles).toContain('StructureDefinition-my-model.json');
-    });
+      it('should write profiles and profile instances to the "profiles" directory', () => {
+        const profilesPath = path.join(tempRoot, 'input', 'profiles');
+        expect(fs.existsSync(profilesPath)).toBeTruthy();
+        const profilesFiles = fs.readdirSync(profilesPath);
+        expect(profilesFiles.length).toBe(2);
+        expect(profilesFiles).toContain('StructureDefinition-my-profile.json');
+        expect(profilesFiles).toContain('StructureDefinition-my-profile-instance.json');
+      });
 
-    it('should write operation instances to the "operations" directory', () => {
-      const operationsPath = path.join(tempRoot, 'input', 'operations');
-      expect(fs.existsSync(operationsPath)).toBeTruthy();
-      const operationsFiles = fs.readdirSync(operationsPath);
-      expect(operationsFiles.length).toBe(1);
-      expect(operationsFiles).toContain('OperationDefinition-my-operation.json');
-    });
+      it('should write extensions and extension instances to the "extensions" directory', () => {
+        const extensionsPath = path.join(tempRoot, 'input', 'extensions');
+        expect(fs.existsSync(extensionsPath)).toBeTruthy();
+        const extensionsFiles = fs.readdirSync(extensionsPath);
+        expect(extensionsFiles.length).toBe(2);
+        expect(extensionsFiles).toContain('StructureDefinition-my-extension.json');
+        expect(extensionsFiles).toContain('StructureDefinition-my-extension-instance.json');
+      });
 
-    it('should write all other non-inline instances to the "resources" directory', () => {
-      const resourcesPath = path.join(tempRoot, 'input', 'resources');
-      expect(fs.existsSync(resourcesPath)).toBeTruthy();
-      const resourcesFiles = fs.readdirSync(resourcesPath);
-      expect(resourcesFiles.length).toBe(1);
-      expect(resourcesFiles).toContain('Observation-my-other-instance.json');
-    });
+      it('should write value sets, code systems, and vocabulary instances to the "vocabulary" directory', () => {
+        const vocabularyPath = path.join(tempRoot, 'input', 'vocabulary');
+        expect(fs.existsSync(vocabularyPath)).toBeTruthy();
+        const vocabularyFiles = fs.readdirSync(vocabularyPath);
+        expect(vocabularyFiles.length).toBe(3);
+        expect(vocabularyFiles).toContain('ValueSet-my-value-set.json');
+        expect(vocabularyFiles).toContain('CodeSystem-my-code-system.json');
+        expect(vocabularyFiles).toContain('ConceptMap-my-concept-map.json');
+      });
+
+      it('should write example instances to the "examples" directory', () => {
+        const examplesPath = path.join(tempRoot, 'input', 'examples');
+        expect(fs.existsSync(examplesPath)).toBeTruthy();
+        const examplesFiles = fs.readdirSync(examplesPath);
+        expect(examplesFiles.length).toBe(1);
+        expect(examplesFiles).toContain('Observation-my-example.json');
+      });
+
+      it('should write capability instances to the "capabilities" directory', () => {
+        const capabilitiesPath = path.join(tempRoot, 'input', 'capabilities');
+        expect(fs.existsSync(capabilitiesPath)).toBeTruthy();
+        const capabilitiesFiles = fs.readdirSync(capabilitiesPath);
+        expect(capabilitiesFiles.length).toBe(1);
+        expect(capabilitiesFiles).toContain('CapabilityStatement-my-capabilities.json');
+      });
 
-    it('should write an info message with the number of instances exported', () => {
-      expect(loggerSpy.getLastMessage('info')).toMatch(/Exported 12 FHIR resources/s);
+      it('should write model instances to the "models" directory', () => {
+        const modelsPath = path.join(tempRoot, 'input', 'models');
+        expect(fs.existsSync(modelsPath)).toBeTruthy();
+        const modelsFiles = fs.readdirSync(modelsPath);
+        expect(modelsFiles.length).toBe(1);
+        expect(modelsFiles).toContain('StructureDefinition-my-model.json');
+      });
+
+      it('should write operation instances to the "operations" directory', () => {
+        const operationsPath = path.join(tempRoot, 'input', 'operations');
+        expect(fs.existsSync(operationsPath)).toBeTruthy();
+        const operationsFiles = fs.readdirSync(operationsPath);
+        expect(operationsFiles.length).toBe(1);
+        expect(operationsFiles).toContain('OperationDefinition-my-operation.json');
+      });
+
+      it('should write all other non-inline instances to the "resources" directory', () => {
+        const resourcesPath = path.join(tempRoot, 'input', 'resources');
+        expect(fs.existsSync(resourcesPath)).toBeTruthy();
+        const resourcesFiles = fs.readdirSync(resourcesPath);
+        expect(resourcesFiles.length).toBe(1);
+        expect(resourcesFiles).toContain('Observation-my-other-instance.json');
+      });
+
+      it('should write an info message with the number of instances exported', () => {
+        expect(loggerSpy.getLastMessage('info')).toMatch(/Exported 12 FHIR resources/s);
+      });
     });
   });
 
@@ -413,6 +519,7 @@ describe('Processing', () => {
     let copyFileSpy: jest.SpyInstance;
     let ensureDirSpy: jest.SpyInstance;
     let consoleSpy: jest.SpyInstance;
+    let getSpy: jest.SpyInstance;
 
     beforeEach(() => {
       readlineSpy = jest.spyOn(readlineSync, 'question').mockImplementation(() => '');
@@ -421,16 +528,20 @@ describe('Processing', () => {
       copyFileSpy = jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
       ensureDirSpy = jest.spyOn(fs, 'ensureDirSync').mockImplementation(() => undefined);
       consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
+      getSpy = jest.spyOn(axios, 'get').mockImplementation(url => {
+        return Promise.resolve({ data: url.slice(url.lastIndexOf('/') + 1) });
+      });
       readlineSpy.mockClear();
       yesNoSpy.mockClear();
       writeSpy.mockClear();
       copyFileSpy.mockClear();
       ensureDirSpy.mockClear();
       consoleSpy.mockClear();
+      getSpy.mockClear();
     });
 
-    it('should initialize a default project when no user input is given', () => {
-      init();
+    it('should initialize a default project when no user input is given', async () => {
+      await init();
       expect(readlineSpy.mock.calls).toEqual([
         ['Name (Default: ExampleIG): '],
         ['Id (Default: fhir.example): '],
@@ -441,16 +552,17 @@ describe('Processing', () => {
       expect(yesNoSpy.mock.calls).toHaveLength(1);
       expect(yesNoSpy.mock.calls[0][0]).toMatch(/Initialize SUSHI project in .*ExampleIG/);
 
-      expect(ensureDirSpy.mock.calls).toHaveLength(1);
-      expect(ensureDirSpy.mock.calls[0][0]).toMatch(
-        /.*ExampleIG.*fsh.*ig-data.*input.*pagecontent/
-      );
+      expect(ensureDirSpy.mock.calls).toHaveLength(2);
+      expect(ensureDirSpy.mock.calls[0][0]).toMatch(/.*ExampleIG.*input.*pagecontent/);
+      expect(ensureDirSpy.mock.calls[1][0]).toMatch(/.*ExampleIG.*input.*fsh/);
 
-      expect(writeSpy.mock.calls).toHaveLength(2);
+      expect(writeSpy.mock.calls).toHaveLength(7);
       expect(writeSpy.mock.calls[0][0]).toMatch(/.*index\.md/);
       expect(writeSpy.mock.calls[0][1]).toMatch(/# ExampleIG/);
-      expect(writeSpy.mock.calls[1][0]).toMatch(/.*config\.yaml/);
-      expect(writeSpy.mock.calls[1][1].replace(/[\n\r]/g, '')).toBe(
+      expect(writeSpy.mock.calls[1][0]).toMatch(/.*ig\.ini/);
+      expect(writeSpy.mock.calls[1][1]).toMatch(/fhir.example/);
+      expect(writeSpy.mock.calls[2][0]).toMatch(/.*sushi-config\.yaml/);
+      expect(writeSpy.mock.calls[2][1].replace(/[\n\r]/g, '')).toBe(
         fs
           .readFileSync(
             path.join(__dirname, 'fixtures', 'init-config', 'default-config.yaml'),
@@ -459,18 +571,29 @@ describe('Processing', () => {
           .replace(/[\n\r]/g, '')
       );
 
-      expect(copyFileSpy.mock.calls).toHaveLength(8);
+      expect(copyFileSpy.mock.calls).toHaveLength(3);
       expect(copyFileSpy.mock.calls[0][1]).toMatch(/.*ExampleIG.*fsh.*patient.fsh/);
       expect(copyFileSpy.mock.calls[1][1]).toMatch(/.*ExampleIG.*\.gitignore/);
-      expect(copyFileSpy.mock.calls[2][1]).toMatch(/.*ExampleIG.*_gencontinuous\.bat/);
-      expect(copyFileSpy.mock.calls[3][1]).toMatch(/.*ExampleIG.*_gencontinuous\.sh/);
-      expect(copyFileSpy.mock.calls[4][1]).toMatch(/.*ExampleIG.*_genonce\.bat/);
-      expect(copyFileSpy.mock.calls[5][1]).toMatch(/.*ExampleIG.*_genonce\.sh/);
-      expect(copyFileSpy.mock.calls[6][1]).toMatch(/.*ExampleIG.*_updatePublisher\.bat/);
-      expect(copyFileSpy.mock.calls[7][1]).toMatch(/.*ExampleIG.*_updatePublisher\.sh/);
-    });
-
-    it('should initialize a project with user input', () => {
+      expect(copyFileSpy.mock.calls[2][1]).toMatch(/.*ExampleIG.*input.*ignoreWarnings\.txt/);
+
+      expect(getSpy.mock.calls).toHaveLength(4);
+      const base = 'http://raw.githubusercontent.com/FHIR/sample-ig/master/';
+      expect(getSpy.mock.calls[0][0]).toBe(base + '_genonce.bat');
+      expect(getSpy.mock.calls[1][0]).toBe(base + '_genonce.sh');
+      expect(getSpy.mock.calls[2][0]).toBe(base + '_updatePublisher.bat');
+      expect(getSpy.mock.calls[3][0]).toBe(base + '_updatePublisher.sh');
+
+      expect(writeSpy.mock.calls[3][0]).toMatch(/.*_genonce\.bat/);
+      expect(writeSpy.mock.calls[3][1]).toMatch(/_genonce\.bat/);
+      expect(writeSpy.mock.calls[4][0]).toMatch(/.*_genonce\.sh/);
+      expect(writeSpy.mock.calls[4][1]).toMatch(/_genonce\.sh/);
+      expect(writeSpy.mock.calls[5][0]).toMatch(/.*_updatePublisher\.bat/);
+      expect(writeSpy.mock.calls[5][1]).toMatch(/_updatePublisher\.bat/);
+      expect(writeSpy.mock.calls[6][0]).toMatch(/.*_updatePublisher\.sh/);
+      expect(writeSpy.mock.calls[6][1]).toMatch(/_updatePublisher\.sh/);
+    });
+
+    it('should initialize a project with user input', async () => {
       readlineSpy.mockImplementation((question: string) => {
         if (question.startsWith('Name')) {
           return 'MyNonDefaultName';
@@ -484,7 +607,7 @@ describe('Processing', () => {
           return '2.0.0';
         }
       });
-      init();
+      await init();
       expect(readlineSpy.mock.calls).toEqual([
         ['Name (Default: ExampleIG): '],
         ['Id (Default: fhir.example): '],
@@ -495,16 +618,17 @@ describe('Processing', () => {
       expect(yesNoSpy.mock.calls).toHaveLength(1);
       expect(yesNoSpy.mock.calls[0][0]).toMatch(/Initialize SUSHI project in .*MyNonDefaultName/);
 
-      expect(ensureDirSpy.mock.calls).toHaveLength(1);
-      expect(ensureDirSpy.mock.calls[0][0]).toMatch(
-        /.*MyNonDefaultName.*fsh.*ig-data.*input.*pagecontent/
-      );
+      expect(ensureDirSpy.mock.calls).toHaveLength(2);
+      expect(ensureDirSpy.mock.calls[0][0]).toMatch(/.*MyNonDefaultName.*input.*pagecontent/);
+      expect(ensureDirSpy.mock.calls[1][0]).toMatch(/.*MyNonDefaultName.*input.*fsh/);
 
-      expect(writeSpy.mock.calls).toHaveLength(2);
+      expect(writeSpy.mock.calls).toHaveLength(7);
       expect(writeSpy.mock.calls[0][0]).toMatch(/.*index\.md/);
       expect(writeSpy.mock.calls[0][1]).toMatch(/# MyNonDefaultName/);
-      expect(writeSpy.mock.calls[1][0]).toMatch(/.*config\.yaml/);
-      expect(writeSpy.mock.calls[1][1].replace(/[\n\r]/g, '')).toBe(
+      expect(writeSpy.mock.calls[1][0]).toMatch(/.*ig\.ini/);
+      expect(writeSpy.mock.calls[1][1]).toMatch(/foo.bar/);
+      expect(writeSpy.mock.calls[2][0]).toMatch(/.*sushi-config\.yaml/);
+      expect(writeSpy.mock.calls[2][1].replace(/[\n\r]/g, '')).toBe(
         fs
           .readFileSync(
             path.join(__dirname, 'fixtures', 'init-config', 'user-input-config.yaml'),
@@ -512,21 +636,34 @@ describe('Processing', () => {
           )
           .replace(/[\n\r]/g, '')
       );
-      expect(copyFileSpy.mock.calls).toHaveLength(8);
+      expect(copyFileSpy.mock.calls).toHaveLength(3);
       expect(copyFileSpy.mock.calls[0][1]).toMatch(/.*MyNonDefaultName.*fsh.*patient.fsh/);
       expect(copyFileSpy.mock.calls[1][1]).toMatch(/.*MyNonDefaultName.*\.gitignore/);
-      expect(copyFileSpy.mock.calls[2][1]).toMatch(/.*MyNonDefaultName.*_gencontinuous\.bat/);
-      expect(copyFileSpy.mock.calls[3][1]).toMatch(/.*MyNonDefaultName.*_gencontinuous\.sh/);
-      expect(copyFileSpy.mock.calls[4][1]).toMatch(/.*MyNonDefaultName.*_genonce\.bat/);
-      expect(copyFileSpy.mock.calls[5][1]).toMatch(/.*MyNonDefaultName.*_genonce\.sh/);
-      expect(copyFileSpy.mock.calls[6][1]).toMatch(/.*MyNonDefaultName.*_updatePublisher\.bat/);
-      expect(copyFileSpy.mock.calls[7][1]).toMatch(/.*MyNonDefaultName.*_updatePublisher\.sh/);
+      expect(copyFileSpy.mock.calls[2][1]).toMatch(
+        /.*MyNonDefaultName.*input.*ignoreWarnings\.txt/
+      );
+
+      expect(getSpy.mock.calls).toHaveLength(4);
+      const base = 'http://raw.githubusercontent.com/FHIR/sample-ig/master/';
+      expect(getSpy.mock.calls[0][0]).toBe(base + '_genonce.bat');
+      expect(getSpy.mock.calls[1][0]).toBe(base + '_genonce.sh');
+      expect(getSpy.mock.calls[2][0]).toBe(base + '_updatePublisher.bat');
+      expect(getSpy.mock.calls[3][0]).toBe(base + '_updatePublisher.sh');
+
+      expect(writeSpy.mock.calls[3][0]).toMatch(/.*_genonce\.bat/);
+      expect(writeSpy.mock.calls[3][1]).toMatch(/_genonce\.bat/);
+      expect(writeSpy.mock.calls[4][0]).toMatch(/.*_genonce\.sh/);
+      expect(writeSpy.mock.calls[4][1]).toMatch(/_genonce\.sh/);
+      expect(writeSpy.mock.calls[5][0]).toMatch(/.*_updatePublisher\.bat/);
+      expect(writeSpy.mock.calls[5][1]).toMatch(/_updatePublisher\.bat/);
+      expect(writeSpy.mock.calls[6][0]).toMatch(/.*_updatePublisher\.sh/);
+      expect(writeSpy.mock.calls[6][1]).toMatch(/_updatePublisher\.sh/);
     });
 
-    it('should abort initalizing a project when the user does not confirm', () => {
+    it('should abort initalizing a project when the user does not confirm', async () => {
       yesNoSpy.mockImplementation(() => false);
 
-      init();
+      await init();
       expect(readlineSpy.mock.calls).toEqual([
         ['Name (Default: ExampleIG): '],
         ['Id (Default: fhir.example): '],
diff --git a/test/utils/fixtures/fhir-dstu2/config.yaml b/test/utils/fixtures/fhir-dstu2/sushi-config.yaml
similarity index 100%
rename from test/utils/fixtures/fhir-dstu2/config.yaml
rename to test/utils/fixtures/fhir-dstu2/sushi-config.yaml
diff --git a/test/utils/fixtures/init-config/default-config.yaml b/test/utils/fixtures/init-config/default-config.yaml
index 2665022..3e4164c 100644
--- a/test/utils/fixtures/init-config/default-config.yaml
+++ b/test/utils/fixtures/init-config/default-config.yaml
@@ -11,14 +11,9 @@ fhirVersion: 4.0.1
 copyrightYear: 2020+
 releaseLabel: ci-build
 
-# ╭─────────────────────────────────────────────ig.ini─────────────────────────────────────────────╮
-# │  To use a provided ig-data/ig.ini file, delete the "template" property below.                  │
-# ╰────────────────────────────────────────────────────────────────────────────────────────────────╯
-template: fhir.base.template#current
-
 # ╭────────────────────────────────────────────menu.xml────────────────────────────────────────────╮
 # │  To use a provided ig-data/input/includes/menu.xml file, delete the "menu" property below.     │
 # ╰────────────────────────────────────────────────────────────────────────────────────────────────╯
 menu:
   Home: index.html
-  Artifacts: artifacts.html
+  Artifacts: artifacts.html
\ No newline at end of file
diff --git a/test/utils/fixtures/init-config/user-input-config.yaml b/test/utils/fixtures/init-config/user-input-config.yaml
index 211ddac..fb9a60b 100644
--- a/test/utils/fixtures/init-config/user-input-config.yaml
+++ b/test/utils/fixtures/init-config/user-input-config.yaml
@@ -11,10 +11,6 @@ fhirVersion: 4.0.1
 copyrightYear: 2020+
 releaseLabel: ci-build
 
-# ╭─────────────────────────────────────────────ig.ini─────────────────────────────────────────────╮
-# │  To use a provided ig-data/ig.ini file, delete the "template" property below.                  │
-# ╰────────────────────────────────────────────────────────────────────────────────────────────────╯
-template: fhir.base.template#current
 
 # ╭────────────────────────────────────────────menu.xml────────────────────────────────────────────╮
 # │  To use a provided ig-data/input/includes/menu.xml file, delete the "menu" property below.     │
diff --git a/test/utils/fixtures/invalid-yaml/config.yaml b/test/utils/fixtures/invalid-yaml/sushi-config.yaml
similarity index 100%
rename from test/utils/fixtures/invalid-yaml/config.yaml
rename to test/utils/fixtures/invalid-yaml/sushi-config.yaml
diff --git a/test/utils/fixtures/valid-yaml/config.yaml b/test/utils/fixtures/valid-yaml/sushi-config.yaml
similarity index 100%
rename from test/utils/fixtures/valid-yaml/config.yaml
rename to test/utils/fixtures/valid-yaml/sushi-config.yaml
diff --git a/test/utils/minimalConfig.ts b/test/utils/minimalConfig.ts
index e34da27..7e2f2b2 100644
--- a/test/utils/minimalConfig.ts
+++ b/test/utils/minimalConfig.ts
@@ -1,7 +1,7 @@
 import { Configuration } from '../../src/fshtypes';
 
 export const minimalConfig: Configuration = {
-  filePath: 'config.yaml',
+  filePath: 'sushi-config.yaml',
   id: 'fhir.us.minimal',
   canonical: 'http://hl7.org/fhir/us/minimal',
   name: 'MinimalIG',