Repository: ProjectEvergreen/greenwood (js)

Issue:
It would be nice to have a greenwood init/template scaffold command
## Type of Change
- [x] New Feature Request
- [ ] Documentation / Website
- [ ] Improvement / Suggestion
- [ ] Bug
- [ ] Other (please clarify below)

## Summary
As per discussion #770 it would be nice to have a command to scaffold a minimal template as well as scaffold other user-gen templates.

## Details

It's pretty much what we did with create-evergreen-app, we copy a basic template package.json with a simple src/pages/index.md as well as a gitignore

Pull request:
Feature/777 greenwood init command
## Related Issue
Resolves #777

## Summary of Changes

* adds new `@greenwood/init` package that will copy a default minimal template to the current working directory and optionally install dependencies via `--install` also optionally through yarn with `--yarn`

Patch:
diff --git a/.ls-lint.yml b/.ls-lint.yml
index 727c43b3..6db0a5ef 100644
--- a/.ls-lint.yml
+++ b/.ls-lint.yml
@@ -13,4 +13,5 @@ ls:
 
 ignore: 
   - .git 
-  - node_modules
\ No newline at end of file
+  - node_modules
+  - packages/init/node_modules
\ No newline at end of file
diff --git a/.mocharc.js b/.mocharc.js
index 61504b77..5eb4c48b 100644
--- a/.mocharc.js
+++ b/.mocharc.js
@@ -2,5 +2,5 @@ const path = require('path');
 
 module.exports = {
   spec: path.join(__dirname, 'packages/**/test/**/**/**/*.spec.js'),
-  timeout: 30000
+  timeout: 60000
 };
\ No newline at end of file
diff --git a/packages/init/README.md b/packages/init/README.md
new file mode 100644
index 00000000..6e44d6bf
--- /dev/null
+++ b/packages/init/README.md
@@ -0,0 +1,43 @@
+# @greenwood/init
+
+## Overview
+Init package for scaffolding out a new Greenwood project.  For more information and complete docs, please visit the [Greenwood website](https://www.greenwoodjs.io/docs).
+
+## Usage
+
+Create a directory and then run the `init` command to scaffold a minimal Greenwood project.
+
+```bash
+mkdir my-app && cd my-app
+npx @greenwood/init
+```
+
+This will then output the following
+```bash
+├── greenwood.config.js
+├── .gitignore
+├── package.json
+└── src/
+     └─ pages/
+         └─ index.md
+```
+
+## API
+
+### NPM Install
+
+To automatically run `npm install` after scaffolding, pass the `--install` flag.
+
+```bash
+# example
+npx @greenwood/init --install
+```
+
+### Yarn Install
+
+To automatically run `yarn install` after scaffolding, pass the `--yarn` flag.
+
+```bash
+# example
+npx @greenwood/init --yarn
+```
\ No newline at end of file
diff --git a/packages/init/package.json b/packages/init/package.json
new file mode 100644
index 00000000..95458f81
--- /dev/null
+++ b/packages/init/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "@greenwood/init",
+  "version": "0.18.0",
+  "description": "A package for scaffolding a new Greenwood project.",
+  "repository": "https://github.com/ProjectEvergreen/greenwood/tree/master/packages/init",
+  "author": "Grant Hutchinson <grant@hutchdev.ca>",
+  "license": "MIT",
+  "main": "src/index.js",
+  "keywords": [
+    "Greenwood",
+    "Static Site Generator",
+    "NodeJS",
+    "Scaffold"
+  ],
+  "files": [
+    "src/"
+  ],
+  "publishConfig": {
+    "access": "public"
+  },
+  "dependencies": {
+    "chalk": "^4.1.2",
+    "commander": "^8.2.0"
+  }
+}
diff --git a/packages/init/src/copy-folder.js b/packages/init/src/copy-folder.js
new file mode 100644
index 00000000..b213dda8
--- /dev/null
+++ b/packages/init/src/copy-folder.js
@@ -0,0 +1,43 @@
+const fs = require('fs');
+const path = require('path');
+
+// https://stackoverflow.com/a/26038979
+function copyFileSync(source, target) {
+  let targetFile = target;
+
+  // if target is a directory a new file with the same name will be created
+  if (fs.existsSync(target)) {
+    if (fs.lstatSync(target).isDirectory()) {
+      targetFile = path.join(target, path.basename(source));
+    }
+  }
+
+  fs.writeFileSync(targetFile, fs.readFileSync(source));
+}
+
+function copyFolderRecursiveSync(source, target) {
+  let files = [];
+
+  // check if folder needs to be created or integrated
+  const targetFolder = path.join(target, path.basename(source));
+  
+  if (!fs.existsSync(targetFolder)) {
+    fs.mkdirSync(targetFolder);
+  }
+
+  // copy
+  if (fs.lstatSync(source).isDirectory()) {
+    files = fs.readdirSync(source);
+    files.forEach((file) => {
+      var curSource = path.join(source, file);
+
+      if (fs.lstatSync(curSource).isDirectory()) {
+        copyFolderRecursiveSync(curSource, targetFolder);
+      } else {
+        copyFileSync(curSource, targetFolder);
+      }
+    });
+  }
+}
+
+module.exports = copyFolder = copyFolderRecursiveSync;
\ No newline at end of file
diff --git a/packages/init/src/index.js b/packages/init/src/index.js
new file mode 100644
index 00000000..cf4f4556
--- /dev/null
+++ b/packages/init/src/index.js
@@ -0,0 +1,137 @@
+/* eslint no-console: 0 */
+const copyFolder = require('./copy-folder');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { spawn } = require('child_process');
+const chalk = require('chalk');
+const commander = require('commander');
+const scriptPkg = require(path.join(__dirname, '..', '/package.json'));
+const templateDir = path.join(__dirname, 'template');
+
+const TARGET_DIR = process.cwd();
+
+console.log(`${chalk.rgb(175, 207, 71)('-------------------------------------------------------')}`);
+console.log(`${chalk.rgb(175, 207, 71)('Initialize Greenwood Template ♻️')}`);
+console.log(`${chalk.rgb(175, 207, 71)('-------------------------------------------------------')}`);
+
+const program = new commander.Command(scriptPkg.name)
+  .version(scriptPkg.version)
+  .usage(`${chalk.green('<application-directory>')} [options]`)
+  .option('--yarn', 'Use yarn package manager instead of npm default')
+  .option('--install', 'Install dependencies upon init')
+  .parse(process.argv)
+  .opts();
+
+if (program.yarn) {
+  console.log('Yarn Enabled');
+}
+
+// Create new package.json
+const npmInit = async () => {
+  let appPkg = require(path.join(templateDir, 'package.json'));
+  
+  // use installation path's folder name for packages
+  appPkg.name = path.basename(process.cwd());
+  
+  await fs.writeFileSync(
+    path.join(TARGET_DIR, 'package.json'),
+    JSON.stringify(appPkg, null, 2) + os.EOL
+  );
+};
+
+// Copy root and src files to target directory
+const srcInit = async () => {
+
+  templateFolder = path.join(__dirname, 'template/');
+
+  await createGitIgnore();
+
+  templateFiles = [];
+  fs.readdirSync(templateFolder).forEach(file => {
+    templateFiles.push(file);
+  });
+
+  await Promise.all(
+    templateFiles.map(async file => {
+      const resolvedPath = path.join(__dirname, 'template', file);
+      if (fs.lstatSync(resolvedPath).isDirectory()) {
+        return await copyFolder(resolvedPath, TARGET_DIR);
+      } else if (await fs.existsSync(resolvedPath)) {
+        return await fs.copyFileSync(
+          resolvedPath,
+          path.join(TARGET_DIR, file)
+        );
+      }
+    })
+  );
+};
+
+// Create the missing gitignore because npm won't publish it https://docs.npmjs.com/files/package.json#files
+const createGitIgnore = () => {
+  return new Promise((resolve, reject) => {
+
+    const resolvedPath = path.join(TARGET_DIR, '.gitignore');
+    const stream = fs.createWriteStream(resolvedPath);
+    const patterns = ['*DS_Store', '*.log', 'node_modules/', 'public/', '.greenwood/'];
+
+    stream.once('open', () => {
+      patterns.forEach(pattern => {
+        stream.write(`${pattern}\n`);
+      });
+      stream.end();
+    });
+    stream.once('close', () => {
+      resolve();
+    });
+    stream.once('error', (err) => {
+      reject(err);
+    });
+  });
+};
+
+// Install npm dependencies
+const install = async () => {
+  const pkgMng = program.yarn ? 'yarn' : 'npm'; // default to npm
+  const pkgCommand = os.platform() === 'win32' ? `${pkgMng}.cmd` : pkgMng;
+  const args = ['install', '--loglevel', 'error'];
+
+  return new Promise((resolve, reject) => {
+
+    const process = spawn(pkgCommand, args, { stdio: 'ignore' });
+
+    process.on('close', code => {
+      if (code !== 0) {
+        reject({
+          command: `${pkgCommand} ${args.join(' ')}`
+        });
+        return;
+      }
+      resolve();
+    });
+  });
+};
+
+const run = async () => {
+  try {
+    // map all the template files and copy them to the current working directory
+    console.log('Initialzing project with files...');
+    await srcInit();
+    
+    console.log('Creating manifest (package.json)...');
+    await npmInit();
+
+    if (program.install || program.yarn) {
+      console.log('Installing project dependencies...');
+      await install();
+    }
+
+    console.log(`${chalk.rgb(175, 207, 71)('Initializing new project complete!')}`);
+  } catch (err) {
+    console.error(err);
+  }
+
+  process.exit(); // eslint-disable-line no-process-exit
+};
+
+run();
\ No newline at end of file
diff --git a/packages/init/src/template/greenwood.config.js b/packages/init/src/template/greenwood.config.js
new file mode 100644
index 00000000..3ded1357
--- /dev/null
+++ b/packages/init/src/template/greenwood.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+  title: 'My Project'
+};
\ No newline at end of file
diff --git a/packages/init/src/template/package.json b/packages/init/src/template/package.json
new file mode 100644
index 00000000..9b2bdca3
--- /dev/null
+++ b/packages/init/src/template/package.json
@@ -0,0 +1,14 @@
+{
+  "name": "my-project",
+  "version": "1.0.0",
+  "description": "",
+  "scripts": {
+    "start": "greenwood develop",
+    "build": "greenwood build",
+    "serve": "greenwood serve"
+  },
+  "devDependencies": {
+    "@greenwood/cli": "^0.18.0"
+  },
+  "license": "ISC"
+}
diff --git a/packages/init/src/template/src/pages/index.md b/packages/init/src/template/src/pages/index.md
new file mode 100644
index 00000000..6b343b58
--- /dev/null
+++ b/packages/init/src/template/src/pages/index.md
@@ -0,0 +1,1 @@
+## My Project
\ No newline at end of file
diff --git a/www/pages/getting-started/quick-start.md b/www/pages/getting-started/quick-start.md
index 98685f5c..9be6be91 100644
--- a/www/pages/getting-started/quick-start.md
+++ b/www/pages/getting-started/quick-start.md
@@ -10,24 +10,21 @@ linkheadings: 3
 
 If you want to get right into some code, we have a few options to get you started with building with Greenwood right away:
 
-- GitHub repo
-- Command line (w/ `npx`)
+- Init Command
+- Command Line (w/ `npx`)
+- Clone GitHub Repo
 - Stackblitz ⚡
 
-### GitHub Repo
-If you want to _clone and go_ then we welcome you to check out the [companion repo](https://github.com/ProjectEvergreen/greenwood-getting-started) we made to accompany this guide.  To get the code from this walkthrough, you can simply clone the repo, install dependencies, and off you go!
+### Init Package
 
-```bash
-$ git clone https://github.com/ProjectEvergreen/greenwood-getting-started
-$ cd greenwood-getting-started
+You can use Greenwood's [`init` package](https://github.com/ProjectEvergreen/greenwood/blob/master/packages/init/README.md) to scaffold out a new Greenwood project for you that will be all set and ready for you start using.
 
-$ npm install
+```bash
+mkdir my-app && cd my-app
 
-$ npm start
+npx @greenwood/init
 ```
 
-Done!
-
 ### Command Line
 
 Additionally, you can start your own project right now as easy as 1.. 2.. 3, right from your terminal!
@@ -43,6 +40,19 @@ $ echo "## hello world" > src/pages/index.md
 $ npx @greenwood/cli develop
 ```
 
+### Clone GitHub Repo
+If you just want to _clone and go_ then we welcome you to check out the [companion repo](https://github.com/ProjectEvergreen/greenwood-getting-started) we made to accompany this guide.  To get the code from this walkthrough, you can simply clone the repo, install dependencies, and off you go!
+
+```bash
+$ git clone https://github.com/ProjectEvergreen/greenwood-getting-started
+$ cd greenwood-getting-started
+
+$ npm install
+
+$ npm start
+```
+
+Done!
 
 ### Stackblitz
 
diff --git a/yarn.lock b/yarn.lock
index 42c3a3f1..06f09363 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3895,6 +3895,14 @@ chalk@^4.0.0, chalk@^4.1.0:
     ansi-styles "^4.1.0"
     supports-color "^7.1.0"
 
+chalk@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
 character-entities-html4@^1.0.0:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125"
@@ -4135,6 +4143,11 @@ commander@^2.20.0, commander@^2.20.3:
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
+commander@^8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-8.2.0.tgz#37fe2bde301d87d47a53adeff8b5915db1381ca8"
+  integrity sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA==
+
 commondir@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"


Test patch:
diff --git a/packages/init/test/cases/build.default/build.default.spec.js b/packages/init/test/cases/build.default/build.default.spec.js
new file mode 100644
index 00000000..c06715df
--- /dev/null
+++ b/packages/init/test/cases/build.default/build.default.spec.js
@@ -0,0 +1,64 @@
+/*
+ * Use Case
+ * Scaffold from minimal template and run Greenwood build command.
+ *
+ * User Result
+ * Should scaffold from template and run the buildå.
+ *
+ * User Command
+ * @greenwood/init --install && greenwood buildå
+ *
+ * User Workspace
+ * N / A
+ */
+const expect = require('chai').expect;
+const fs = require('fs');
+const path = require('path');
+const Runner = require('gallinago').Runner;
+const runSmokeTest = require('../../../../../test/smoke-test');
+
+describe('Scaffold Greenwood and Run Build command: ', function() {
+  const LABEL = 'Default Greenwood Configuration and Workspace';
+  const initPath = path.join(process.cwd(), 'packages/init/src/index.js');
+  const outputPath = path.join(__dirname, 'my-app');
+  let runner;
+
+  before(function() {
+    this.context = {
+      publicDir: path.join(outputPath, 'public')
+    };
+    runner = new Runner();
+  });
+
+  describe('default minimal template', function () {
+
+    before(async function() {
+      await runner.setup(outputPath);
+      await runner.runCommand(initPath, '--install');
+    });
+
+    describe(`should build ${LABEL}`, function () {
+      const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
+
+      before(async function() {
+        await runner.setup(outputPath);
+        await runner.runCommand(cliPath, 'build');
+      });
+      
+      runSmokeTest(['public', 'index'], LABEL);
+    
+      it('should generate a package-lock.json file', function() {
+        expect(fs.existsSync(path.join(outputPath, 'package-lock.json'))).to.be.true;
+      });
+
+      it('should not generate a yarn.lock file', function() {
+        expect(fs.existsSync(path.join(outputPath, 'yarn.lock'))).to.be.false;
+      });
+    });
+  });
+
+  after(function() {
+    runner.teardown([outputPath]);
+  });
+
+});
\ No newline at end of file
diff --git a/packages/init/test/cases/develop.default/develop.default.spec.js b/packages/init/test/cases/develop.default/develop.default.spec.js
new file mode 100644
index 00000000..c9939003
--- /dev/null
+++ b/packages/init/test/cases/develop.default/develop.default.spec.js
@@ -0,0 +1,146 @@
+/*
+ * Use Case
+ * Scaffold from minimal template and run Greenwood develop command with no config.
+ *
+ * User Result
+ * Should scaffold from template and start the development server and render the template.
+ *
+ * User Command
+ * @greenwood/init --install && greenwood develop
+ *
+ * User Workspace
+ * N / A
+ */
+const expect = require('chai').expect;
+const fs = require('fs');
+const { JSDOM } = require('jsdom');
+const path = require('path');
+const { getSetupFiles } = require('../../../../../test/utils');
+const request = require('request');
+const Runner = require('gallinago').Runner;
+const runSmokeTest = require('../../../../../test/smoke-test');
+
+describe('Scaffold Greenwood and Run Develop command: ', function() {
+  const initPath = path.join(process.cwd(), 'packages/init/src/index.js');
+  const outputPath = path.join(__dirname, 'my-app');
+  let runner;
+
+  before(function() {
+    this.context = {
+      publicDir: path.join(outputPath, 'public')
+    };
+    runner = new Runner();
+  });
+
+  describe('default minimal template', function () {
+
+    before(async function() {
+      await runner.setup(outputPath);
+      await runner.runCommand(initPath, '--install');
+    });
+
+    describe('Develop Greenwood With: ', function() {
+      const LABEL = 'Default Greenwood Configuration and Workspace';
+      const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
+      const hostname = 'http://localhost';
+      const port = 1984;
+      let runner;
+
+      before(function() {
+        this.context = {
+          hostname: `${hostname}:${port}`
+        };
+        runner = new Runner();
+      });
+
+      describe(LABEL, function() {
+
+        before(async function() {
+
+          await runner.setup(outputPath, [
+            ...getSetupFiles(outputPath)
+          ]);
+
+          return new Promise(async (resolve) => {
+            setTimeout(() => {
+              resolve();
+            }, 5000);
+
+            await runner.runCommand(cliPath, 'develop');
+          });
+        });
+
+        runSmokeTest(['serve'], LABEL);
+
+        it('should generate a package-lock.json file', function() {
+          expect(fs.existsSync(path.join(outputPath, 'package-lock.json'))).to.be.true;
+        });
+  
+        it('should not generate a yarn.lock file', function() {
+          expect(fs.existsSync(path.join(outputPath, 'yarn.lock'))).to.be.false;
+        });
+
+        it('should not generate a public directory', function() {
+          expect(fs.existsSync(path.join(outputPath, 'public'))).to.be.false;
+        });
+
+        describe('Develop command specific HTML behaviors', function() {
+          let response = {};
+          let dom;
+
+          before(async function() {
+            return new Promise((resolve, reject) => {
+              request.get({
+                url: `http://127.0.0.1:${port}`,
+                headers: {
+                  accept: 'text/html'
+                }
+              }, (err, res, body) => {
+                if (err) {
+                  reject();
+                }
+
+                response = res;
+                
+                dom = new JSDOM(body);
+                resolve();
+              });
+            });
+          });
+
+          it('should return the correct content type', function(done) {
+            expect(response.headers['content-type']).to.contain('text/html');
+            done();
+          });
+
+          it('should return a 200', function(done) {
+            expect(response.statusCode).to.equal(200);
+
+            done();
+          });
+
+          it('should display My Project heading', function(done) {
+            const heading = dom.window.document.querySelector('body > h2');
+            
+            expect(heading.textContent).to.equal('My Project');
+
+            done();
+          });
+
+          it('should display My Project title', function(done) {
+            const title = dom.window.document.querySelector('head > title');
+            
+            expect(title.textContent).to.equal('My Project');
+
+            done();
+          });
+        });
+
+        after(function() {
+          runner.stopCommand();
+          runner.teardown([outputPath]);
+        });
+      });
+    });
+  });
+});
\ No newline at end of file
diff --git a/packages/init/test/cases/init.default/init.default.spec.js b/packages/init/test/cases/init.default/init.default.spec.js
new file mode 100644
index 00000000..d560c00b
--- /dev/null
+++ b/packages/init/test/cases/init.default/init.default.spec.js
@@ -0,0 +1,106 @@
+/*
+ * Use Case
+ * Scaffold from minimal template with no flags.
+ *
+ * User Result
+ * Should scaffold from template build.
+ *
+ * User Command
+ * @greenwood/init
+ *
+ * User Workspace
+ * N / A
+ */
+const expect = require('chai').expect;
+const fs = require('fs');
+const path = require('path');
+const Runner = require('gallinago').Runner;
+
+describe('Scaffold Greenwood With Default Template: ', function() {
+  const initPath = path.join(process.cwd(), 'packages/init/src/index.js');
+  const outputPath = path.join(__dirname, 'my-app');
+  let runner;
+
+  before(function() {
+    this.context = {
+      publicDir: path.join(outputPath, 'my-app')
+    };
+    runner = new Runner();
+  });
+
+  describe('default minimal output', function () {
+
+    before(async function() {
+      await fs.promises.mkdir(outputPath);
+      await runner.setup(outputPath);
+      await runner.runCommand(initPath);
+    });
+
+    describe('should scaffold project files and folders', () => {
+      
+      it('should create a src/pages directory', function() {
+        expect(fs.existsSync(path.join(outputPath, 'src', 'pages'))).to.be.true;
+      });
+
+      it('should generate a greenwood.config.js file', function() {
+        expect(fs.existsSync(path.join(outputPath, 'greenwood.config.js'))).to.be.true;
+      });
+
+      it('should generate a .gitignore file', function() {
+        expect(fs.existsSync(path.join(outputPath, '.gitignore'))).to.be.true;
+      });
+  
+      it('should generate a package.json file', function() {
+        expect(fs.existsSync(path.join(outputPath, 'package.json'))).to.be.true;
+      });
+
+      it('should not generate a package-lock.json file', function() {
+        expect(fs.existsSync(path.join(outputPath, 'package-lock.json'))).to.be.false;
+      });
+
+      it('should not generate a yarn.lock file', function() {
+        expect(fs.existsSync(path.join(outputPath, 'yarn.lock'))).to.be.false;
+      });
+
+      it('should not generate a public directory', function() {
+        expect(fs.existsSync(path.join(outputPath, 'public'))).to.be.false;
+      });
+    });
+
+    describe('initial package.json contents', function() {
+      it('the should have the correct Greenwood scripts', function() {
+        const scripts = require(path.join(outputPath, 'package.json')).scripts;
+
+        expect(scripts.start).to.equal('greenwood develop');
+        expect(scripts.build).to.equal('greenwood build');
+        expect(scripts.serve).to.equal('greenwood serve');
+      });
+
+      it('the should have the correct Greenwood devDependency', function() {
+        const devDependencies = require(path.join(outputPath, 'package.json')).devDependencies;
+
+        expect(devDependencies['@greenwood/cli']).to.equal('^0.18.0');
+      });
+    });
+
+    describe('initial greenwood.config.js contents', function() {
+      it('should have the correct title configuration', function() {
+        const config = require(path.join(outputPath, 'greenwood.config.js'));
+
+        expect(config.title).to.equal('My Project');
+      });
+    });
+
+    describe('initial page contents', function() {
+      it('should have the correct contents for src/pages.index.md', function() {
+        const pageContents = fs.readFileSync(path.join(outputPath, 'src/pages/index.md'), 'utf-8');
+
+        expect(pageContents).to.equal('## My Project');
+      });
+    });
+  });
+
+  after(function() {
+    runner.teardown([outputPath]);
+  });
+});
\ No newline at end of file
diff --git a/packages/init/test/cases/init.yarn/init.yarn.spec.js b/packages/init/test/cases/init.yarn/init.yarn.spec.js
new file mode 100644
index 00000000..5ad87575
--- /dev/null
+++ b/packages/init/test/cases/init.yarn/init.yarn.spec.js
@@ -0,0 +1,67 @@
+/*
+ * Use Case
+ * Scaffold from minimal template using the --yarn flag.
+ *
+ * User Result
+ * Should scaffold from template and with lockfile.
+ *
+ * User Command
+ * @greenwood/init --yarn
+ *
+ * User Workspace
+ * N / A
+ */
+const expect = require('chai').expect;
+const fs = require('fs');
+const path = require('path');
+const Runner = require('gallinago').Runner;
+const runSmokeTest = require('../../../../../test/smoke-test');
+
+describe('Scaffold Greenwood With Yarn: ', function() {
+  const LABEL = 'Default Greenwood Configuration and Workspace';
+  const initPath = path.join(process.cwd(), 'packages/init/src/index.js');
+  const outputPath = path.join(__dirname, 'my-app');
+  let runner;
+
+  before(function() {
+    this.context = {
+      publicDir: path.join(outputPath, 'public')
+    };
+    runner = new Runner();
+  });
+
+  describe('default minimal template', function () {
+
+    before(async function() {
+      await runner.setup(outputPath);
+      await runner.runCommand(initPath, '--yarn');
+    });
+
+    describe('should install with Yarn', function () {
+      const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
+
+      before(async function() {
+        await runner.runCommand(cliPath, 'build');
+      });
+      
+      runSmokeTest(['public', 'index'], LABEL);
+    
+      it('should generate a yarn.lock file', function() {
+        expect(fs.existsSync(path.join(outputPath, 'yarn.lock'))).to.be.true;
+      });
+
+      it('should not generate a package-lock.json file', function() {
+        expect(fs.existsSync(path.join(outputPath, 'package-lock.json'))).to.be.false;
+      });
+
+      it('should generate a public directory', function() {
+        expect(fs.existsSync(path.join(outputPath, 'public'))).to.be.true;
+      });
+    });
+  });
+
+  after(function() {
+    runner.teardown([outputPath]);
+  });
+
+});
\ No newline at end of file