[
  {
    "path": ".babelrc",
    "content": "{\n  \"env\": {\n    \"test\": {\n      \"plugins\": [ \"istanbul\" ],\n      \"sourceMaps\": \"inline\"\n    }\n  }\n}\n"
  },
  {
    "path": ".github/workflows/code-scanning-2022-06-29.yml",
    "content": "# This workflow is inherited from our internal .github repo at https://github.com/lifeomic/.github/blob/master/workflow-templates/code-scanning-2022-06-29.yml\n# Setting up this workflow on the repository will perform a static scan for security issues using GitHub Code Scanning. \n# Any findings for a repository can be found under the `Security` tab -> `Code Scanning Alerts`\nname: \"CodeQL\"\n\non:\n  push:\n    branches:\n      - main\n      - master\n    paths-ignore:\n      - test\n      - tests\n      - '**/test'\n      - '**/tests'\n      - '**/*.test.js'\n      - '**/*.test.ts'\n  pull_request:\n    branches:\n      - main\n      - master\n    paths-ignore:\n      - test\n      - tests\n      - '**/test'\n      - '**/tests'\n      - '**/*.test.js'\n      - '**/*.test.ts'\n\njobs:\n  analyze:\n    if: ${{ !contains(github.head_ref, 'dependabot') }}\n    name: Analyze\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: false\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v3\n      with:\n        # We must fetch at least the immediate parents so that if this is\n        # a pull request then we can checkout the head.\n        fetch-depth: 2\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v2\n      with:\n        config-file: lifeomic/.github/config-files/codeql-config.yml@master  # uses our config file from the lifeomic/.github repo\n        queries: +security-extended  # This will run all queries at https://github.com/github/codeql/:language/ql/src/codeql-suites/:language-security-extended.qls\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).\n    # If this step fails, it should be removed and replaced with custom build steps.\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v2\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v2\n\n"
  },
  {
    "path": ".gitignore",
    "content": "/.git/\nnode_modules/\ntemp/\nwork/\ndist/\n*.log\n*.bak\n*.bak.*\n.npmrc\n.DS_STORE\ndump.rdb\n.nyc_output/\n.vscode/\n.nyc_output/\n/package-lock.json\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - \"8\"\n  - \"10\"\nscript:\n  - yarn build\nafter_success: yarn coverage\n\nnotifications:\n  email:\n    on_success: never\n    on_failure: always\n\nbefore_deploy: cd work/dist\ndeploy:\n  skip_cleanup: true\n  provider: npm\n  email: $NPM_EMAIL\n  api_key: $NPM_KEY\n  on:\n    tags: true\n    repo: lifeomic/terraform-plan-parser\n    node: \"8\"\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright 2018 LifeOmic\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Terraform Plan Parser\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/lifeomic/terraform-plan-parser.svg)](https://greenkeeper.io/) [![Build Status](https://travis-ci.org/lifeomic/terraform-plan-parser.svg?branch=master)](https://travis-ci.org/lifeomic/terraform-plan-parser) [![Coverage Status](https://coveralls.io/repos/github/lifeomic/terraform-plan-parser/badge.svg?branch=master)](https://coveralls.io/github/lifeomic/terraform-plan-parser?branch=master) [![npm version](https://badge.fury.io/js/terraform-plan-parser.svg)](https://badge.fury.io/js/terraform-plan-parser)\n\nThis project provides a CLI and JavaScript API for parsing terraform\nplan output.\n\n**IMPORTANT:** This tool does not parse the file produced by the `-out=path`\nargument to `terraform plan` which is a binary file. There is not a stable\nspecification for this binary file format so, at this time, it is safer\nto parse the somewhat structured textual output that gets written to `stdout`.\n\n## Why should I use this?\n\nThis parser allows the textual log output from `terraform plan` to be converted\nto JSON which is more machine readable.\n\nHere are some suggested use cases:\n\n- Send notification when certain types of changes are detected.\n  For example, email security team if an IAM policy is modified.\n- Validate that certain changes are allowed for a given _change management_\n  request before invoking `terraform apply`.\n- Kick-off a special workflow for certain types of changes to the\n  infrastructure (possibly, before calling `terraform apply`).\n\nIf you wish to perform linting or enforcement of best practices then your\nbetter option might be to analyze the source terraform code instead of\nonly looking at the changes that are described by the `terraform plan`\noutput.\n\n## Usage\n\n### JavaScript API\n\n**NPM:**\n\n```bash\nnpm install terraform-plan-parser\n```\n\n**Yarn Package Manager:**\n\n```bash\nyarn add terraform-plan-parser\n```\n\n**IMPORTANT:**\n\nThis project requires [Node v8.9.0 (LTS)](https://nodejs.org/en/blog/release/v8.9.0/)\nor newer because the source code utilizes language features such as\n`async` / `await`. If you are using an unsupported version of Node then you\nwill see `SyntaxError: Unexpected token function`. It's possible to use\n`babel` to transpile the code for older versions of the Node runtime.\nThe [babel-preset-env](https://github.com/babel/babel/tree/master/packages/babel-preset-env)\nis a good package for supporting this.\n\n### Parse string that contains stdout logs from terraform plan\n\n```javascript\nconst fs = require('fs');\nconst parser = require('terraform-plan-parser');\n\nconst stdout = fs.readFileSync('terraform-plan.stdout', {encoding: 'utf8'});\nconst result = parser.parseStdout(stdout);\n```\n\n### Command Line\n\n**NPM:**\n\n```bash\nnpm install -g terraform-plan-parser\n```\n\n**Yarn Package Manager:**\n\n```bash\nyarn add global terraform-plan-parser\n```\n\n**Command help:**\n\n```bash\n# Get help on using command\nparse-terraform-plan --help\n```\n\n```\nOptions:\n  --help        Show help                                              [boolean]\n  --version     Show version number                                    [boolean]\n  -i, --input   Input file (stdin is used if not provided)              [string]\n  -o, --output  Output file (stdout is used if not provided)            [string]\n  --pretty      Output JSON in pretty format          [boolean] [default: false]\n```\n\n**Read from stdin and write to stdout**:\n\n```bash\n# Pipe output from \"terraform plan\" to parser which will convert it to JSON\nterraform plan | parse-terraform-plan --pretty\n```\n\n**Read from file and write to file**:\n\n```bash\n# Store \"terraform plan\" output in file\nterraform plan > terraform-plan.stdout\n\n# Read from \"terraform plan\" output file and write to JSON file\nparse-terraform-plan --pretty -i terraform-plan.stdout -o terraform-plan.json\n```\n\n## Output Schema\n\nThe output is an object with these top-level properties:\n\n- **`errors`:** An array of parsing errors\n- **`changedResources`:** An array of changed resources\n- **`changedDataSources`:** An array of changed data sources\n\nEach _changed resource_ has the following properties:\n\n- **`action`:** One of `\"create\"`, `\"destroy\"`, `\"replace\"`, `\"update\"`\n- **`type`:** Type of resource (e.g. `\"aws_ecs_service\"`)\n- **`name`:** Resource name (e.g. `\"my_service\"`)\n- **`path`:** Full path to resource as printed in plan output (e.g. `\"module.module1.module.module2.aws_ecs_service.my_service\"`)\n- **`module`:** Fully qualified module name (e.g. `\"module1.module2\"`) or `undefined` if resource not within module.\n- **`changedAttributes`:** An object whose keys are an attribute name and value is an object\n- **`newResourceRequired`:** A flag to indicate if a new resource is required (only present if `true`)\n- **`tainted`:** A flag to indicate if resource is tainted (only present if `true`)\n\nA _changed attribute_ object has the following properties:\n\n- **`old`:** An object with `type` property and `value` property which\n  describes the old state of the attribute.\n  The `type` will be `\"computed\"` or `\"string\"`. The `value` will be a string.\n- **`new`:** An object with `type` property and `value` property which\n  describes the new state of the attribute.\n  The `type` will be `\"computed\"` or `\"string\"`. The `value` will be a string.\n\nEach _data source_ has the following properties:\n\n- **`action`:** The action will always be `\"read\"`\n- **`type`:** Type of resource (e.g. `\"external\"`)\n- **`name`:** Data source name (e.g. `\"ecr_image_digests\"`)\n- **`path`:** Full path to data source as printed in plan output (e.g. `\"module.module1.module.module2.data.external.ecr_image_digests\"`)\n- **`module`:** Fully qualified module name (e.g. `\"module1.module2\"`) or `undefined` if data source not within module.\n- **`changedAttributes`:** An object whose keys are an attribute name and value is an object\n\n## Example Output\n\n```json\n{\n  \"errors\": [],\n  \"changedResources\": [\n    {\n      \"action\": \"update\",\n      \"type\": \"aws_ecs_service\",\n      \"name\": \"sample_app\",\n      \"path\": \"aws_ecs_service.sample_app\",\n      \"changedAttributes\": {\n        \"task_definition\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\"\n          },\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"${ aws_ecs_task_definition.sample_app.arn }\"\n          }\n        }\n      }\n    }\n  ],\n  \"changedDataSources\": [\n    {\n      \"action\": \"read\",\n      \"type\": \"external\",\n      \"name\": \"ecr_image_digests\",\n      \"path\": \"data.external.ecr_image_digests\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"program.#\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"1\"\n          }\n        },\n        \"program.0\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"extract-image-digests\"\n          }\n        },\n        \"result.%\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        }\n      }\n    }\n  ]\n}\n```\n"
  },
  {
    "path": "index.d.ts",
    "content": "export * from './src/';\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"terraform-plan-parser\",\n  \"version\": \"1.6.0\",\n  \"description\": \"This module provides functionality for parsing stdout from \\\"terraform plan\\\" and converting it to JSON that can be more easily analyzed.\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/lifeomic/terraform-plan-parser.git\"\n  },\n  \"author\": \"Phil Gates-Idem <phil.gates-idem@lifeomic.com>\",\n  \"license\": \"MIT\",\n  \"main\": \"./work/dist/src/\",\n  \"module\": \"./work/dist/es6/src/\",\n  \"jsnext:main\": \"./work/dist/esnext/src/\",\n  \"types\": \"./work/dist/src/index.d.ts\",\n  \"bin\": {\n    \"parse-terraform-plan\": \"./work/dist/src/cli.js\"\n  },\n  \"scripts\": {\n    \"compile-src-cjs\": \"tsc --declaration --declarationDir ./work/dist -p tsconfig-src-cjs.json\",\n    \"compile-src-es6\": \"tsc -p tsconfig-src-es6.json\",\n    \"compile-src-esnext\": \"tsc -p tsconfig-src-esnext.json\",\n    \"precompile-src\": \"rm -rf ./work/dist\",\n    \"compile-src\": \"yarn compile-src-cjs && yarn compile-src-es6 && yarn compile-src-esnext\",\n    \"transpile-test-js\": \"BABEL_ENV=test babel work/dist-test --out-dir work/dist-test --source-maps\",\n    \"precompile-test\": \"rm -rf ./work/dist-test\",\n    \"compile-test\": \"tsc -p tsconfig-test.json && yarn transpile-test-js\",\n    \"postcompile-test\": \"cp -r test/unit/data ./work/dist-test/test/unit/\",\n    \"lint\": \"tslint --format codeFrame --project tsconfig.json 'src/**/*.ts' 'test/**/*.ts'\",\n    \"pretest\": \"yarn lint && yarn compile-test\",\n    \"test\": \"BABEL_ENV=test nyc ava 'work/dist-test/test/**/*.test.js'\",\n    \"coverage\": \"nyc report --reporter=text-lcov | coveralls\",\n    \"prebuild\": \"yarn test\",\n    \"build\": \"yarn compile-src && ./tools/bin/postbuild\",\n    \"publish-patch\": \"yarn build && ./tools/bin/publish patch\",\n    \"publish-minor\": \"yarn build && ./tools/bin/publish minor\",\n    \"publish-major\": \"yarn build && ./tools/bin/publish major\"\n  },\n  \"dependencies\": {\n    \"strip-ansi\": \"^5.0.0\",\n    \"yargs\": \"^12.0.1\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^10.0.7\",\n    \"ava\": \"^0.25.0\",\n    \"babel-cli\": \"^6.26.0\",\n    \"babel-plugin-istanbul\": \"^4.1.5\",\n    \"chalk\": \"^2.3.1\",\n    \"coveralls\": \"^3.0.0\",\n    \"inquirer\": \"^6.0.0\",\n    \"nyc\": \"^12.0.1\",\n    \"proxyquire\": \"^2.0.0\",\n    \"tslint\": \"^5.9.1\",\n    \"tslint-config-semistandard\": \"^7.0.0\",\n    \"typescript\": \"^2.7.2\"\n  },\n  \"ava\": {\n    \"require\": [\n      \"source-map-support/register\"\n    ]\n  },\n  \"nyc\": {\n    \"sourceMap\": false,\n    \"instrument\": false\n  }\n}\n"
  },
  {
    "path": "src/cli.ts",
    "content": "#!/usr/bin/env node\n\nimport * as fs from 'fs';\nimport { promisify } from 'util';\nimport * as parser from '.';\n\nconst readFileAsync = promisify(fs.readFile);\nconst writeFileAsync = promisify(fs.writeFile);\n\nconst yargs = require('yargs');\n\nasync function readFromStdin (): Promise<string> {\n  const chunks: Array<Buffer> = [];\n  await new Promise((resolve, reject) => {\n    const stdin = process.stdin;\n\n    stdin.on('data', (chunk) => {\n      chunks.push(chunk);\n    });\n\n    stdin.on('error', reject);\n    stdin.on('end', resolve);\n  });\n  return Buffer.concat(chunks).toString('utf8');\n}\n\nasync function readFromFile (file: string): Promise<string> {\n  return readFileAsync(file, { encoding: 'utf8' });\n}\n\nasync function run () {\n  const input = yargs\n    .option('i', {\n        alias: 'input',\n        describe: 'Input file (stdin is used if not provided)',\n        type: 'string'\n    })\n    .option('o', {\n        alias: 'output',\n        describe: 'Output file (stdout is used if not provided)',\n        type: 'string'\n    })\n    .option('pretty', {\n        describe: 'Output JSON in pretty format',\n        type: 'boolean',\n        default: false\n    })\n    .argv;\n  if (input.help) {\n    return yargs.showHelp();\n  }\n  const inputData = (input.input)\n    ? await readFromFile(input.input)\n    : await readFromStdin();\n\n  const json = input.pretty\n    ? JSON.stringify(parser.parseStdout(inputData), null, '  ')\n    : JSON.stringify(parser.parseStdout(inputData));\n\n  if (input.output) {\n    await writeFileAsync(input.output, json, { encoding: 'utf8' });\n  } else {\n    process.stdout.write(json);\n  }\n}\n\nrun().catch(function (err) {\n  console.error('Error parsing terraform plan. ' + (err.stack || err.toString()));\n  process.exitCode = -1;\n});\n"
  },
  {
    "path": "src/index.ts",
    "content": "const stripAnsi = require('strip-ansi');\nimport endsWith from './util/endsWith';\n\nexport enum Action {\n  CREATE = 'create',\n  DESTROY = 'destroy',\n  REPLACE = 'replace',\n  UPDATE = 'update',\n  READ = 'read'\n}\n\nexport interface ChangedAttributesMap {\n  [key: string]: ChangedAttribute;\n}\n\nexport interface Changed {\n  module: string;\n  action: Action;\n  type: string;\n  name: string;\n  path: string;\n  changedAttributes: ChangedAttributesMap;\n  newResourceRequired: boolean;\n  tainted: boolean;\n}\n\nexport enum AttributeValueType {\n  UNKNOWN = 'unknown',\n  STRING = 'string',\n  COMPUTED = 'computed'\n}\n\nexport interface AttributeValue {\n  type: AttributeValueType;\n  value: string;\n}\n\nexport interface ChangedAttribute {\n  new: AttributeValue;\n  old: AttributeValue;\n  forcesNewResource: boolean;\n}\n\nexport class ParseError {\n  code: String;\n  message: String;\n}\n\nexport interface ParseResult {\n  errors: Array<ParseError>;\n  changedResources: Array<Changed>;\n  changedDataSources: Array<Changed>;\n}\n\nconst NO_CHANGES_STRING = '\\nNo changes. Infrastructure is up-to-date.\\n';\nconst CONTENT_START_STRING = '\\nTerraform will perform the following actions:\\n';\nconst CONTENT_END_STRING = '\\nPlan:';\nconst OLD_NEW_SEPARATOR = ' => ';\nconst ATTRIBUTE_FORCES_NEW_RESOURCE_SUFFIX = ' (forces new resource)';\n\n/**\n * Find the starting position within terraform stdout that we should try\n * to parse.\n *\n * @param logOutput terraform log output\n * @return the position at which parsing should begin or -1 if not found\n */\nfunction findParseableContentStartPos (logOutput: string): number {\n  const pos = logOutput.indexOf(CONTENT_START_STRING);\n  return (pos === -1) ? pos : pos + CONTENT_START_STRING.length;\n}\n\n/**\n * Find the ending position within terraform stdout that we should try\n * to parse.\n *\n * @param logOutput terraform log output\n * @return the position at which parsing should end or -1 if not found\n */\nfunction findParseableContentEndPos (logOutput: string, startPos: number): number {\n  return logOutput.indexOf(CONTENT_END_STRING, startPos);\n}\n\ninterface ActionMapping {\n  [key: string]: Action;\n}\n\nconst ACTION_MAPPING: ActionMapping = {};\n\nACTION_MAPPING['+'] = Action.CREATE;\nACTION_MAPPING['-'] = Action.DESTROY;\nACTION_MAPPING['-/+'] = Action.REPLACE;\nACTION_MAPPING['~'] = Action.UPDATE;\nACTION_MAPPING['<='] = Action.READ;\n\nconst ACTION_LINE_REGEX = /^(?:((?:.*\\.)?module\\.[^.]*)\\.)?(?:(data)\\.)?([^.]+)\\.([^ ]+)( \\(tainted\\))?( \\(new resource required\\))?$/;\nconst ATTRIBUTE_LINE_REGEX = /^ {6}[^ ]/;\n\n// Convert something like \"module.test1.module.test2\" to \"test1.test2\"\nfunction parseModulePath (rawModuleStr: string) {\n  return rawModuleStr.split(/\\.?module./).slice(1).join('.');\n}\n\n/**\n * Parse a line that looks similar to a resource or data source.\n *\n * Example line for resource:\n * ```\n * -/+ aws_ecs_task_definition.sample_app (new resource required)\n * ```\n *\n * Example line for data source:\n * ```\n * <= data.external.ecr_image_digests\n * ```\n * @param line current line within stdout text\n * @param action the pre-determined action which was found by looking at start of line\n * @param result an object that collects changed data sources, changed resources, and errors\n * @return an object that identifies a changed resource or data sources\n */\nfunction parseActionLine (offset: number, line: string, action: Action, result: ParseResult): Changed | null {\n  // start position is after the action symbol\n  // For example, we move past \"-/+ \" (4 characters)\n  const match = ACTION_LINE_REGEX.exec(line.substring(offset));\n\n  if (!match) {\n    result.errors.push({\n      code: 'UNABLE_TO_PARSE_CHANGE_LINE',\n      message: `Unable to parse \"${line}\" (ignoring)`\n    });\n    return null;\n  }\n\n  const [, module, dataSourceStr, type, name, taintedStr, newResourceRequiredStr] = match;\n  const fullyQualifiedPath = [module, dataSourceStr, type, name].filter(str =>\n    str && str.length > 0).join('.');\n\n  let change;\n  change = {\n    action: action,\n    type: type,\n    name: name,\n    path: fullyQualifiedPath,\n    changedAttributes: {}\n  } as Changed;\n\n  if (module) {\n    change.module = parseModulePath(module);\n  }\n\n  if (taintedStr === ' (tainted)') {\n    change.tainted = true;\n  }\n\n  if (dataSourceStr) {\n    result.changedDataSources.push(change);\n  } else {\n    if (newResourceRequiredStr) {\n      change.newResourceRequired = true;\n    }\n\n    result.changedResources.push(change);\n  }\n  return change;\n}\n\n/**\n * Find the position of next non-space character or -1 if we didn't\n * find a non-space character\n * @param str a string\n * @param fromIndex the starting position\n * @return the next position within string that is non-space character or -1 if not found\n */\nfunction findPosOfNextNonSpaceChar (str: string, fromIndex: number): number {\n  let pos = fromIndex;\n  const end = str.length;\n  while (pos < end) {\n    if (str.charAt(pos) !== ' ') {\n      return pos;\n    }\n    pos++;\n  }\n  return -1;\n}\n\n/**\n * Read ahead in a string until we ecounter the given terminator character or end of string\n * @param str a string\n * @param fromIndex the starting position\n * @param terminatorChar a terminator string of length 1\n * @return the substring from `fromIndex` up to (but not including) the terminator character\n */\nfunction readUpToChar (str: string, fromIndex: number, terminatorChar: string): string | null {\n  let pos = fromIndex;\n  const end = str.length;\n  while (pos < end) {\n    if (str.charAt(pos) === terminatorChar) {\n      return str.substring(fromIndex, pos);\n    }\n    pos++;\n  }\n\n  return null;\n}\n\n/**\n * [findStringEndDelimiterPos description]\n * @param str the string that contains a quoted string that needs to be parsed\n * @param fromIndex the position of the first character after the `\"` character\n * @return the position of the ending `\"` or -1 if the string is unterminated\n */\nfunction findStringEndDelimiterPos (str: string, fromIndex: number): number {\n  let pos = fromIndex;\n  let escaped = false;\n  const end = str.length;\n  while (pos < end) {\n    if (escaped) {\n      escaped = false;\n    } else {\n      if (str.charAt(pos) === '\"') {\n        return pos;\n      } else if (str.charAt(pos) === '\\\\') {\n        escaped = true;\n      }\n    }\n    pos++;\n  }\n\n  return -1;\n}\n\n/**\n * This function is used to parse values such as:\n * `<computed>`\n * `\"arn:aws:iam::123123123123:role/SampleApp\"`\n *\n * @param line the line read from terraform stdout content\n * @param fromIndex the starting position of a _value_\n * @param errors an array that collects errors\n * @return an array with two items (first item is result value object and second item is the end position of the value)\n */\nfunction parseValue (line: string, fromIndex: number, errors: Array<ParseError>): [AttributeValue, number] {\n  const foundDelimiter = line.charAt(fromIndex);\n  let endPos: number;\n  let type: AttributeValueType | undefined;\n  let value;\n\n  if (foundDelimiter === '\"') {\n    endPos = findStringEndDelimiterPos(line, fromIndex + 1);\n    if (endPos === -1) {\n      endPos = line.length;\n      value = line.substring(fromIndex, endPos);\n      errors.push({\n        code: 'UNTERMINATED_STRING',\n        message: `Unterminated string on line \"${line}\"`\n      });\n    } else {\n      type = AttributeValueType.STRING;\n      value = JSON.parse(line.substring(fromIndex, endPos + 1));\n    }\n  } else if (foundDelimiter === '<') {\n    const contents = readUpToChar(line, fromIndex + 1, '>');\n    if (contents === null) {\n      // we did not find the terminator character\n      value = line.substring(fromIndex);\n      endPos = line.length;\n    } else {\n      if (contents === 'computed') {\n        type = AttributeValueType.COMPUTED;\n      } else {\n        value = line.substring(fromIndex, fromIndex + contents.length + 1);\n      }\n      endPos = fromIndex + contents.length + 1;\n    }\n  } else {\n    value = line.substring(fromIndex);\n    endPos = fromIndex + value.length;\n  }\n\n  const result = {} as AttributeValue;\n  result.type = type || AttributeValueType.UNKNOWN;\n\n  if (value !== undefined) {\n    result.value = value;\n  }\n\n  return [result, endPos];\n}\n\n/**\n * Parses a line that we think looks like an attribute because it starts\n * with six spaces and then a non-space character.\n *\n * @param line the line that looks like an attribute change\n * @param lastChange the change object for resource or data source that will hold attribute\n * @param errors an array that will collect errors\n */\nfunction parseAttributeLine (line: string, lastChange: Changed, errors: Array<ParseError>) {\n  let startPos = 6;\n  const nameEndPos = line.indexOf(':', startPos + 1);\n  if (nameEndPos === -1) {\n    errors.push({\n      code: 'UNABLE_TO_PARSE_ATTRIBUTE_NAME',\n      message: `Attribute name not found on line \"${line}\" (ignored)`\n    });\n    return;\n  }\n\n  let oldObj: AttributeValue | undefined;\n  let newObj: AttributeValue | undefined;\n  let forcesNewResource;\n\n  const name = line.substring(startPos, nameEndPos);\n  startPos = findPosOfNextNonSpaceChar(line, nameEndPos + 1);\n  if (startPos !== -1) {\n    const [firstObj, firstValueEndPos] = parseValue(line, startPos, errors);\n    startPos = firstValueEndPos + 1;\n\n    if (line.substring(startPos, startPos + OLD_NEW_SEPARATOR.length) === OLD_NEW_SEPARATOR) {\n      // there is a \" => \" so we have an old and new value\n      [newObj] = parseValue(line, startPos + OLD_NEW_SEPARATOR.length, errors);\n      oldObj = firstObj;\n    } else {\n      // there is no \" => \" so we only have a new value\n      newObj = firstObj;\n    }\n\n    if (endsWith(line, ATTRIBUTE_FORCES_NEW_RESOURCE_SUFFIX)) {\n      forcesNewResource = true;\n    }\n  }\n\n  const result = {} as ChangedAttribute;\n\n  if (oldObj) {\n    result.old = oldObj;\n  }\n\n  if (newObj) {\n    result.new = newObj;\n  }\n\n  if (result.old && result.old.value === result.new.value) {\n    return;\n  }\n\n  if (forcesNewResource) {\n    result.forcesNewResource = true;\n  }\n\n  lastChange.changedAttributes[name] = result;\n}\n\nexport function parseStdout (logOutput: string): ParseResult {\n  logOutput = stripAnsi(logOutput).replace(/\\r\\n/g, '\\n');\n\n  const result = {} as ParseResult;\n  result.errors = [];\n  result.changedResources = [];\n  result.changedDataSources = [];\n\n  let lastChange = null;\n\n  if (logOutput.includes(NO_CHANGES_STRING)) {\n    // no changes to parse...\n    return result;\n  }\n\n  const startPos = findParseableContentStartPos(logOutput);\n  if (startPos === -1) {\n    result.errors.push({\n      code: 'UNABLE_TO_FIND_STARTING_POSITION_WITHIN_STDOUT',\n      message: `Did not find magic starting string: ${CONTENT_START_STRING}`\n    });\n    return result;\n  }\n\n  const endPos = findParseableContentEndPos(logOutput, startPos);\n\n  if (endPos === -1) {\n    result.errors.push({\n      code: 'UNABLE_TO_FIND_ENDING_POSITION_WITHIN_STDOUT',\n      message: `Did not find magic ending string: ${CONTENT_END_STRING}`\n    });\n    return result;\n  }\n\n  const changesText = logOutput.substring(startPos, endPos);\n  const lines = changesText.split('\\n');\n\n  for (const line of lines) {\n    if (line.length === 0) {\n      // blank lines separate each resource / data source.\n      lastChange = null;\n      continue;\n    }\n\n    let offset;\n    let possibleActionSymbol = line.substring(0, 3).trim();\n    const spacePos = possibleActionSymbol.lastIndexOf(' ');\n    if (spacePos === -1) {\n      // action line is something like:\n      // \"-/+ aws_ecs_task_definition.sample_app (new resource required)\"\n      offset = 4;\n    } else {\n      // action line is something like:\n      // \"+ aws_iam_role.terraform_demo\"\n      offset = spacePos + 1;\n      possibleActionSymbol = possibleActionSymbol.substring(0, spacePos);\n    }\n\n    const action = ACTION_MAPPING[possibleActionSymbol];\n\n    if (action) {\n      // line starts with an action symbol so it will be followed by\n      // something like \"data.external.ecr_image_digests\"\n      // or \"aws_ecs_task_definition.sample_app (new resource required)\"\n      lastChange = parseActionLine(offset, line, action, result);\n    } else if (ATTRIBUTE_LINE_REGEX.test(line)) {\n      if (lastChange) {\n        parseAttributeLine(line, lastChange, result.errors);\n      } else {\n        // This line looks like an attribute but there is no resource\n        // or data source that will hold it.\n        result.errors.push({\n          code: 'ORPHAN_ATTRIBUTE_LINE',\n          message: `Attribute line \"${line}\" is not associated with a data source or resource (ignoring)`\n        });\n      }\n    } else {\n      // We don't recognize what this line is....\n      result.errors.push({\n        code: 'UNABLE_TO_PARSE_LINE',\n        message: `Unable to parse \"${line}\" (ignoring)`\n      });\n    }\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "src/util/endsWith.ts",
    "content": "export default function (str: string, search: string): boolean {\n  const strLen = str.length;\n  return str.substring(strLen - search.length, strLen) === search;\n}\n"
  },
  {
    "path": "test/unit/data/00-terraform-plan.expected.json",
    "content": "{\n  \"errors\": [],\n  \"changedResources\": [\n    {\n      \"action\": \"update\",\n      \"type\": \"aws_ecs_service\",\n      \"name\": \"sample_app\",\n      \"path\": \"aws_ecs_service.sample_app\",\n      \"changedAttributes\": {\n        \"task_definition\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\"\n          },\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"${ aws_ecs_task_definition.sample_app.arn }\"\n          }\n        }\n      }\n    },\n    {\n      \"action\": \"replace\",\n      \"type\": \"aws_ecs_task_definition\",\n      \"name\": \"sample_app\",\n      \"path\": \"aws_ecs_task_definition.sample_app\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"sample-app\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          },\n          \"forcesNewResource\": true\n        },\n        \"arn\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"container_definitions\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"[{\\\"cpu\\\":1,\\\"environment\\\":[],\\\"essential\\\":true,\\\"image\\\":\\\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\\\",\\\"logConfiguration\\\":{\\\"logDriver\\\":\\\"awslogs\\\",\\\"options\\\":{\\\"awslogs-group\\\":\\\"sample-app\\\",\\\"awslogs-region\\\":\\\"us-east-1\\\",\\\"awslogs-stream-prefix\\\":\\\"sample-app\\\"}},\\\"memory\\\":256,\\\"mountPoints\\\":[],\\\"name\\\":\\\"sample-app\\\",\\\"portMappings\\\":[{\\\"containerPort\\\":8443,\\\"hostPort\\\":0,\\\"protocol\\\":\\\"tcp\\\"}],\\\"volumesFrom\\\":[]}]\"\n          },\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"[\\n  {\\n    \\\"name\\\": \\\"sample-app\\\",\\n    \\\"image\\\": \\\"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\\\"sample-app\\\"] }\\\",\\n    \\\"cpu\\\": 1,\\n    \\\"memory\\\": 256,\\n    \\\"essential\\\": true,\\n    \\\"logConfiguration\\\": {\\n      \\\"logDriver\\\": \\\"awslogs\\\",\\n      \\\"options\\\": {\\n        \\\"awslogs-group\\\": \\\"${ aws_cloudwatch_log_group.sample_app.name }\\\",\\n        \\\"awslogs-region\\\": \\\"${ var.target_aws_region }\\\",\\n        \\\"awslogs-stream-prefix\\\": \\\"sample-app\\\"\\n      }\\n    },\\n    \\\"portMappings\\\": [\\n      {\\n        \\\"containerPort\\\": 8443,\\n        \\\"hostPort\\\": 0\\n      }\\n    ]\\n  }\\n]\\n\"\n          },\n          \"forcesNewResource\": true\n        },\n        \"network_mode\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"revision\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"186\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        }\n      },\n      \"newResourceRequired\": true\n    },\n    {\n      \"action\": \"replace\",\n      \"type\": \"null_resource\",\n      \"name\": \"promote_images\",\n      \"path\": \"null_resource.promote_images\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"1236159896537553123\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          },\n          \"forcesNewResource\": true\n        },\n        \"triggers.deploy_job_hash\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"6c37ac7175bdf35e24a2f2755addd238\"\n          },\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"1a0bd86fc5831ee66858f2e159efa547\"\n          },\n          \"forcesNewResource\": true\n        }\n      },\n      \"newResourceRequired\": true\n    }\n  ],\n  \"changedDataSources\": [\n    {\n      \"action\": \"read\",\n      \"type\": \"external\",\n      \"name\": \"ecr_image_digests\",\n      \"path\": \"data.external.ecr_image_digests\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"program.#\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"1\"\n          }\n        },\n        \"program.0\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"extract-image-digests\"\n          }\n        },\n        \"result.%\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "test/unit/data/00-terraform-plan.stdout.txt",
    "content": "\u001b[0m\u001b[1mRefreshing Terraform state in-memory prior to plan...\u001b[0m\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\u001b[0m\n\u001b[0m\u001b[1maws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)\u001b[0m\n\u001b[0m\u001b[1maws_iam_role.service_role: Refreshing state... (ID: SampleApp)\u001b[0m\n\u001b[0m\u001b[1maws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)\u001b[0m\n\u001b[0m\u001b[1maws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)\u001b[0m\n\u001b[0m\u001b[1maws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)\u001b[0m\n\u001b[0m\u001b[1mnull_resource.promote_images: Refreshing state... (ID: 1236159896537553123)\u001b[0m\n\u001b[0m\u001b[1maws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)\u001b[0m\n\u001b[0m\u001b[1maws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)\u001b[0m\n\u001b[0m\u001b[1maws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)\u001b[0m\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  \u001b[33m~\u001b[0m update in-place\n\u001b[31m-\u001b[0m/\u001b[32m+\u001b[0m destroy and then create replacement\n \u001b[36m<=\u001b[0m read (data resources)\n\u001b[0m\nTerraform will perform the following actions:\n\n\u001b[36m \u001b[36m<=\u001b[0m \u001b[36mdata.external.ecr_image_digests\n\u001b[0m      id:                       <computed>\n      program.#:                \"1\"\n      program.0:                \"extract-image-digests\"\n      result.%:                 <computed>\n\u001b[0m\n\u001b[0m\u001b[33m  \u001b[33m~\u001b[0m \u001b[33maws_ecs_service.sample_app\n\u001b[0m      task_definition:          \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\" => \"${ aws_ecs_task_definition.sample_app.arn }\"\n\u001b[0m\n\u001b[0m\u001b[33m\u001b[31m-\u001b[0m/\u001b[32m+\u001b[0m \u001b[33maws_ecs_task_definition.sample_app \u001b[31m\u001b[1m(new resource required)\u001b[0m\n\u001b[0m      id:                       \"sample-app\" => <computed> \u001b[31m(forces new resource)\u001b[0m\n      arn:                      \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\" => <computed>\n      container_definitions:    \"[{\\\"cpu\\\":1,\\\"environment\\\":[],\\\"essential\\\":true,\\\"image\\\":\\\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\\\",\\\"logConfiguration\\\":{\\\"logDriver\\\":\\\"awslogs\\\",\\\"options\\\":{\\\"awslogs-group\\\":\\\"sample-app\\\",\\\"awslogs-region\\\":\\\"us-east-1\\\",\\\"awslogs-stream-prefix\\\":\\\"sample-app\\\"}},\\\"memory\\\":256,\\\"mountPoints\\\":[],\\\"name\\\":\\\"sample-app\\\",\\\"portMappings\\\":[{\\\"containerPort\\\":8443,\\\"hostPort\\\":0,\\\"protocol\\\":\\\"tcp\\\"}],\\\"volumesFrom\\\":[]}]\" => \"[\\n  {\\n    \\\"name\\\": \\\"sample-app\\\",\\n    \\\"image\\\": \\\"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\\\"sample-app\\\"] }\\\",\\n    \\\"cpu\\\": 1,\\n    \\\"memory\\\": 256,\\n    \\\"essential\\\": true,\\n    \\\"logConfiguration\\\": {\\n      \\\"logDriver\\\": \\\"awslogs\\\",\\n      \\\"options\\\": {\\n        \\\"awslogs-group\\\": \\\"${ aws_cloudwatch_log_group.sample_app.name }\\\",\\n        \\\"awslogs-region\\\": \\\"${ var.target_aws_region }\\\",\\n        \\\"awslogs-stream-prefix\\\": \\\"sample-app\\\"\\n      }\\n    },\\n    \\\"portMappings\\\": [\\n      {\\n        \\\"containerPort\\\": 8443,\\n        \\\"hostPort\\\": 0\\n      }\\n    ]\\n  }\\n]\\n\" \u001b[31m(forces new resource)\u001b[0m\n      family:                   \"sample-app\" => \"sample-app\"\n      network_mode:             \"\" => <computed>\n      revision:                 \"186\" => <computed>\n      task_role_arn:            \"arn:aws:iam::123123123123:role/SampleApp\" => \"arn:aws:iam::123123123123:role/SampleApp\"\n\u001b[0m\n\u001b[0m\u001b[33m\u001b[31m-\u001b[0m/\u001b[32m+\u001b[0m \u001b[33mnull_resource.promote_images \u001b[31m\u001b[1m(new resource required)\u001b[0m\n\u001b[0m      id:                       \"1236159896537553123\" => <computed> \u001b[31m(forces new resource)\u001b[0m\n      triggers.%:               \"1\" => \"1\"\n      triggers.deploy_job_hash: \"6c37ac7175bdf35e24a2f2755addd238\" => \"1a0bd86fc5831ee66858f2e159efa547\" \u001b[31m(forces new resource)\u001b[0m\n\u001b[0m\n\u001b[0m\n\u001b[0m\u001b[1mPlan:\u001b[0m 2 to add, 1 to change, 2 to destroy.\u001b[0m\n\n------------------------------------------------------------------------\n\nThis plan was saved to: terraform.plan\n\nTo perform exactly these actions, run the following command to apply:\n    terraform apply \"terraform.plan\"\n"
  },
  {
    "path": "test/unit/data/01-terraform-plan.expected.json",
    "content": "{\n  \"errors\": [],\n  \"changedResources\": [\n    {\n      \"action\": \"update\",\n      \"type\": \"aws_ecs_service\",\n      \"name\": \"sample_app\",\n      \"path\": \"aws_ecs_service.sample_app\",\n      \"changedAttributes\": {\n        \"task_definition\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\"\n          },\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"${ aws_ecs_task_definition.sample_app.arn }\"\n          }\n        }\n      }\n    },\n    {\n      \"action\": \"replace\",\n      \"type\": \"aws_ecs_task_definition\",\n      \"name\": \"sample_app\",\n      \"path\": \"aws_ecs_task_definition.sample_app\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"sample-app\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          },\n          \"forcesNewResource\": true\n        },\n        \"arn\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"container_definitions\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"[{\\\"cpu\\\":1,\\\"environment\\\":[],\\\"essential\\\":true,\\\"image\\\":\\\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\\\",\\\"logConfiguration\\\":{\\\"logDriver\\\":\\\"awslogs\\\",\\\"options\\\":{\\\"awslogs-group\\\":\\\"sample-app\\\",\\\"awslogs-region\\\":\\\"us-east-1\\\",\\\"awslogs-stream-prefix\\\":\\\"sample-app\\\"}},\\\"memory\\\":256,\\\"mountPoints\\\":[],\\\"name\\\":\\\"sample-app\\\",\\\"portMappings\\\":[{\\\"containerPort\\\":8443,\\\"hostPort\\\":0,\\\"protocol\\\":\\\"tcp\\\"}],\\\"volumesFrom\\\":[]}]\"\n          },\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"[\\n  {\\n    \\\"name\\\": \\\"sample-app\\\",\\n    \\\"image\\\": \\\"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\\\"sample-app\\\"] }\\\",\\n    \\\"cpu\\\": 1,\\n    \\\"memory\\\": 256,\\n    \\\"essential\\\": true,\\n    \\\"logConfiguration\\\": {\\n      \\\"logDriver\\\": \\\"awslogs\\\",\\n      \\\"options\\\": {\\n        \\\"awslogs-group\\\": \\\"${ aws_cloudwatch_log_group.sample_app.name }\\\",\\n        \\\"awslogs-region\\\": \\\"${ var.target_aws_region }\\\",\\n        \\\"awslogs-stream-prefix\\\": \\\"sample-app\\\"\\n      }\\n    },\\n    \\\"portMappings\\\": [\\n      {\\n        \\\"containerPort\\\": 8443,\\n        \\\"hostPort\\\": 0\\n      }\\n    ]\\n  }\\n]\\n\"\n          },\n          \"forcesNewResource\": true\n        },\n        \"network_mode\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"revision\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"186\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        }\n      },\n      \"newResourceRequired\": true\n    },\n    {\n      \"action\": \"replace\",\n      \"type\": \"null_resource\",\n      \"name\": \"promote_images\",\n      \"path\": \"null_resource.promote_images\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"1236159896537553123\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          },\n          \"forcesNewResource\": true\n        },\n        \"triggers.deploy_job_hash\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"6c37ac7175bdf35e24a2f2755addd238\"\n          },\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"1a0bd86fc5831ee66858f2e159efa547\"\n          },\n          \"forcesNewResource\": true\n        }\n      },\n      \"newResourceRequired\": true\n    }\n  ],\n  \"changedDataSources\": [\n    {\n      \"action\": \"read\",\n      \"type\": \"external\",\n      \"name\": \"ecr_image_digests\",\n      \"path\": \"data.external.ecr_image_digests\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"program.#\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"1\"\n          }\n        },\n        \"program.0\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"extract-image-digests\"\n          }\n        },\n        \"result.%\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "test/unit/data/01-terraform-plan.stdout.txt",
    "content": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\naws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)\naws_iam_role.service_role: Refreshing state... (ID: SampleApp)\naws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)\naws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)\naws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)\nnull_resource.promote_images: Refreshing state... (ID: 1236159896537553123)\naws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)\naws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)\naws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  ~ update in-place\n-/+ destroy and then create replacement\n <= read (data resources)\n\nTerraform will perform the following actions:\n\n <= data.external.ecr_image_digests\n      id:                       <computed>\n      program.#:                \"1\"\n      program.0:                \"extract-image-digests\"\n      result.%:                 <computed>\n\n  ~ aws_ecs_service.sample_app\n      task_definition:          \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\" => \"${ aws_ecs_task_definition.sample_app.arn }\"\n\n-/+ aws_ecs_task_definition.sample_app (new resource required)\n      id:                       \"sample-app\" => <computed> (forces new resource)\n      arn:                      \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\" => <computed>\n      container_definitions:    \"[{\\\"cpu\\\":1,\\\"environment\\\":[],\\\"essential\\\":true,\\\"image\\\":\\\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\\\",\\\"logConfiguration\\\":{\\\"logDriver\\\":\\\"awslogs\\\",\\\"options\\\":{\\\"awslogs-group\\\":\\\"sample-app\\\",\\\"awslogs-region\\\":\\\"us-east-1\\\",\\\"awslogs-stream-prefix\\\":\\\"sample-app\\\"}},\\\"memory\\\":256,\\\"mountPoints\\\":[],\\\"name\\\":\\\"sample-app\\\",\\\"portMappings\\\":[{\\\"containerPort\\\":8443,\\\"hostPort\\\":0,\\\"protocol\\\":\\\"tcp\\\"}],\\\"volumesFrom\\\":[]}]\" => \"[\\n  {\\n    \\\"name\\\": \\\"sample-app\\\",\\n    \\\"image\\\": \\\"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\\\"sample-app\\\"] }\\\",\\n    \\\"cpu\\\": 1,\\n    \\\"memory\\\": 256,\\n    \\\"essential\\\": true,\\n    \\\"logConfiguration\\\": {\\n      \\\"logDriver\\\": \\\"awslogs\\\",\\n      \\\"options\\\": {\\n        \\\"awslogs-group\\\": \\\"${ aws_cloudwatch_log_group.sample_app.name }\\\",\\n        \\\"awslogs-region\\\": \\\"${ var.target_aws_region }\\\",\\n        \\\"awslogs-stream-prefix\\\": \\\"sample-app\\\"\\n      }\\n    },\\n    \\\"portMappings\\\": [\\n      {\\n        \\\"containerPort\\\": 8443,\\n        \\\"hostPort\\\": 0\\n      }\\n    ]\\n  }\\n]\\n\" (forces new resource)\n      family:                   \"sample-app\" => \"sample-app\"\n      network_mode:             \"\" => <computed>\n      revision:                 \"186\" => <computed>\n      task_role_arn:            \"arn:aws:iam::123123123123:role/SampleApp\" => \"arn:aws:iam::123123123123:role/SampleApp\"\n\n-/+ null_resource.promote_images (new resource required)\n      id:                       \"1236159896537553123\" => <computed> (forces new resource)\n      triggers.%:               \"1\" => \"1\"\n      triggers.deploy_job_hash: \"6c37ac7175bdf35e24a2f2755addd238\" => \"1a0bd86fc5831ee66858f2e159efa547\" (forces new resource)\n\n\nPlan: 2 to add, 1 to change, 2 to destroy.\n\n------------------------------------------------------------------------\n\nThis plan was saved to: terraform.plan\n\nTo perform exactly these actions, run the following command to apply:\n    terraform apply \"terraform.plan\"\n"
  },
  {
    "path": "test/unit/data/02-terraform-plan.expected.json",
    "content": "{\n  \"errors\": [\n    {\n      \"code\": \"UNABLE_TO_PARSE_LINE\",\n      \"message\": \"Unable to parse \\\"XYZ strange line will be ignored\\\" (ignoring)\"\n    },\n    {\n      \"code\": \"ORPHAN_ATTRIBUTE_LINE\",\n      \"message\": \"Attribute line \\\"      floating_attribute_at_start:                       \\\"myservice\\\" (forces new resource)\\\" is not associated with a data source or resource (ignoring)\"\n    },\n    {\n      \"code\": \"UNTERMINATED_STRING\",\n      \"message\": \"Unterminated string on line \\\"      unterminated_string:          \\\"arn:aws:ecs:us-east-1:123123123123\\\"\"\n    },\n    {\n      \"code\": \"ORPHAN_ATTRIBUTE_LINE\",\n      \"message\": \"Attribute line \\\"      floating_attribute_at_end:                       \\\"myservice\\\" (forces new resource)\\\" is not associated with a data source or resource (ignoring)\"\n    }\n  ],\n  \"changedResources\": [\n    {\n      \"action\": \"update\",\n      \"type\": \"aws_ecs_service\",\n      \"name\": \"sample_app\",\n      \"path\": \"aws_ecs_service.sample_app\",\n      \"changedAttributes\": {\n        \"unterminated_string\": {\n          \"new\": {\n            \"type\": \"unknown\",\n            \"value\": \"\\\"arn:aws:ecs:us-east-1:123123123123\"\n          }\n        },\n        \"unterminated_computed\": {\n          \"new\": {\n            \"type\": \"unknown\",\n            \"value\": \"<computed\"\n          }\n        },\n        \"unrecognized_attribute_value\": {\n          \"new\": {\n            \"type\": \"unknown\",\n            \"value\": \"<blah\"\n          }\n        },\n        \"some_value_that_is_not_a_string\": {\n          \"new\": {\n            \"type\": \"unknown\",\n            \"value\": \"1234\"\n          }\n        }\n      }\n    },\n    {\n      \"action\": \"replace\",\n      \"type\": \"aws_ecs_task_definition\",\n      \"name\": \"sample_app\",\n      \"path\": \"aws_ecs_task_definition.sample_app\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"myservice\"\n          },\n          \"forcesNewResource\": true\n        }\n      },\n      \"newResourceRequired\": true\n    }\n  ],\n  \"changedDataSources\": [\n    {\n      \"action\": \"read\",\n      \"type\": \"external\",\n      \"name\": \"ecr_image_digests\",\n      \"path\": \"data.external.ecr_image_digests\",\n      \"changedAttributes\": {\n        \"attribute-with-no-value\": {}\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "test/unit/data/02-terraform-plan.stdout.txt",
    "content": "The line that starts with \"Terraform will perform the following actions:\" is\nthe magic delimiter when parsing content.\n\nChanging anything above the magic \"start string\" should not cause a problem\n\nTerraform will perform the following actions:\n\nXYZ strange line will be ignored\n\n      floating_attribute_at_start:                       \"myservice\" (forces new resource)\n\n <= data.external.ecr_image_digests\n      attribute-with-no-value:\n\n  ~ aws_ecs_service.sample_app\n      unterminated_string:          \"arn:aws:ecs:us-east-1:123123123123\n      unterminated_computed:        <computed\n      unrecognized_attribute_value: <blah>\n      some_value_that_is_not_a_string: 1234\n\n-/+ aws_ecs_task_definition.sample_app (new resource required)\n      id:                       \"myservice\" (forces new resource)\n\n      floating_attribute_at_end:                       \"myservice\" (forces new resource)\n\nPlan: 2 to add, 1 to change, 2 to destroy. <-- The line that starts with \"Plan: \" is the magic end string\n\nChanging anything below the magic \"end string\" should not cause a problem\n"
  },
  {
    "path": "test/unit/data/03-terraform-plan.expected.json",
    "content": "{\n  \"errors\": [\n    {\n      \"code\": \"UNABLE_TO_PARSE_LINE\",\n      \"message\": \"Unable to parse \\\"--- aws_ecs_service.this_is_unrecognized\\\" (ignoring)\"\n    }\n  ],\n  \"changedResources\": [\n    {\n      \"action\": \"create\",\n      \"type\": \"aws_ecs_service\",\n      \"name\": \"this_is_created\",\n      \"path\": \"aws_ecs_service.this_is_created\",\n      \"changedAttributes\": {}\n    },\n    {\n      \"action\": \"destroy\",\n      \"type\": \"aws_ecs_service\",\n      \"name\": \"this_is_destroyed\",\n      \"path\": \"aws_ecs_service.this_is_destroyed\",\n      \"changedAttributes\": {}\n    },\n    {\n      \"action\": \"replace\",\n      \"type\": \"aws_ecs_service\",\n      \"name\": \"this_is_replaced\",\n      \"path\": \"aws_ecs_service.this_is_replaced\",\n      \"changedAttributes\": {}\n    },\n    {\n      \"action\": \"update\",\n      \"type\": \"aws_ecs_service\",\n      \"name\": \"this_is_updated\",\n      \"path\": \"aws_ecs_service.this_is_updated\",\n      \"changedAttributes\": {}\n    }\n  ],\n  \"changedDataSources\": [\n    {\n      \"action\": \"read\",\n      \"type\": \"external\",\n      \"name\": \"this_is_a_read\",\n      \"path\": \"data.external.this_is_a_read\",\n      \"changedAttributes\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "test/unit/data/03-terraform-plan.stdout.txt",
    "content": "\nTerraform will perform the following actions:\n\n <= data.external.this_is_a_read\n\n  + aws_ecs_service.this_is_created\n\n  - aws_ecs_service.this_is_destroyed\n\n-/+ aws_ecs_service.this_is_replaced\n\n  ~ aws_ecs_service.this_is_updated\n\n--- aws_ecs_service.this_is_unrecognized\n\nPlan: 2 to add, 1 to change, 2 to destroy.\n"
  },
  {
    "path": "test/unit/data/04-no-magic-start.expected.json",
    "content": "{\n  \"changedDataSources\": [],\n  \"changedResources\": [],\n  \"errors\": [\n    {\n      \"code\": \"UNABLE_TO_FIND_STARTING_POSITION_WITHIN_STDOUT\",\n      \"message\": \"Did not find magic starting string: \\nTerraform will perform the following actions:\\n\"\n    }\n  ]\n}\n"
  },
  {
    "path": "test/unit/data/04-no-magic-start.stdout.txt",
    "content": "The line that starts with \"Terraform will perform the following actions:\" is\nthe magic delimiter when parsing content.\n\nChanging anything above the magic \"start string\" should not cause a problem\n\nBLAH BLAH BLAH Terraform will perform the following actions:\n\nXYZ strange line will be ignored\n\n      floating_attribute_at_start:                       \"myservice\" (forces new resource)\n\n <= data.external.ecr_image_digests\n      attribute-with-no-value:\n\n  ~ aws_ecs_service.sample_app\n      unterminated_string:          \"arn:aws:ecs:us-east-1:123123123123\n      unterminated_computed:        <computed\n      unrecognized_attribute_value: <blah>\n      some_value_that_is_not_a_string: 1234\n\n-/+ aws_ecs_task_definition.sample_app (new resource required)\n      id:                       \"myservice\" (forces new resource)\n\n      floating_attribute_at_end:                       \"myservice\" (forces new resource)\n\nPlan: 2 to add, 1 to change, 2 to destroy. <-- The line that starts with \"Plan: \" is the magic end string\n\nChanging anything below the magic \"end string\" should not cause a problem\n"
  },
  {
    "path": "test/unit/data/05-no-magic-end.expected.json",
    "content": "{\n  \"changedDataSources\": [],\n  \"changedResources\": [],\n  \"errors\": [\n    {\n      \"code\": \"UNABLE_TO_FIND_ENDING_POSITION_WITHIN_STDOUT\",\n      \"message\": \"Did not find magic ending string: \\nPlan:\"\n    }\n  ]\n}\n"
  },
  {
    "path": "test/unit/data/05-no-magic-end.stdout.txt",
    "content": "The line that starts with \"Terraform will perform the following actions:\" is\nthe magic delimiter when parsing content.\n\nChanging anything above the magic \"start string\" should not cause a problem\n\nTerraform will perform the following actions:\n\nXYZ strange line will be ignored\n\n      floating_attribute_at_start:                       \"myservice\" (forces new resource)\n\n <= data.external.ecr_image_digests\n      attribute-with-no-value:\n\n  ~ aws_ecs_service.sample_app\n      unterminated_string:          \"arn:aws:ecs:us-east-1:123123123123\n      unterminated_computed:        <computed\n      unrecognized_attribute_value: <blah>\n      some_value_that_is_not_a_string: 1234\n\n-/+ aws_ecs_task_definition.sample_app (new resource required)\n      id:                       \"myservice\" (forces new resource)\n\n      floating_attribute_at_end:                       \"myservice\" (forces new resource)\n\nBLAH BLAH BLAH Plan: 2 to add, 1 to change, 2 to destroy. <-- The line that starts with \"Plan: \" is the magic end string\n\nChanging anything below the magic \"end string\" should not cause a problem\n"
  },
  {
    "path": "test/unit/data/06-attribute-value-unexpected-delimiter.expected.json",
    "content": "{\n  \"errors\": [],\n  \"changedResources\": [\n    {\n      \"action\": \"update\",\n      \"type\": \"aws_ecs_service\",\n      \"name\": \"sample_app\",\n      \"path\": \"aws_ecs_service.sample_app\",\n      \"changedAttributes\": {\n        \"task_definition\": {\n          \"new\": {\n            \"type\": \"unknown\",\n            \"value\": \"this is a test\"\n          }\n        }\n      }\n    }\n  ],\n  \"changedDataSources\": []\n}\n"
  },
  {
    "path": "test/unit/data/06-attribute-value-unexpected-delimiter.stdout.txt",
    "content": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\naws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)\naws_iam_role.service_role: Refreshing state... (ID: SampleApp)\naws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)\naws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)\naws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)\nnull_resource.promote_images: Refreshing state... (ID: 1236159896537553123)\naws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)\naws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)\naws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  ~ update in-place\n-/+ destroy and then create replacement\n <= read (data resources)\n\nTerraform will perform the following actions:\n\n  ~ aws_ecs_service.sample_app\n      task_definition:          this is a test\n\n\nPlan: 2 to add, 1 to change, 2 to destroy.\n\n------------------------------------------------------------------------\n\nThis plan was saved to: terraform.plan\n\nTo perform exactly these actions, run the following command to apply:\n    terraform apply \"terraform.plan\"\n"
  },
  {
    "path": "test/unit/data/07-invalid-action-line.expected.json",
    "content": "{\n  \"errors\": [\n    {\n      \"code\": \"UNABLE_TO_PARSE_CHANGE_LINE\",\n      \"message\": \"Unable to parse \\\"  ~ aws_ecs_service\\\" (ignoring)\"\n    },\n    {\n      \"code\": \"ORPHAN_ATTRIBUTE_LINE\",\n      \"message\": \"Attribute line \\\"      task_definition:          \\\"this is a test\\\"\\\" is not associated with a data source or resource (ignoring)\"\n    }\n  ],\n  \"changedResources\": [],\n  \"changedDataSources\": []\n}"
  },
  {
    "path": "test/unit/data/07-invalid-action-line.stdout.txt",
    "content": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\naws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)\naws_iam_role.service_role: Refreshing state... (ID: SampleApp)\naws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)\naws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)\naws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)\nnull_resource.promote_images: Refreshing state... (ID: 1236159896537553123)\naws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)\naws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)\naws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  ~ update in-place\n-/+ destroy and then create replacement\n <= read (data resources)\n\nTerraform will perform the following actions:\n\n  ~ aws_ecs_service\n      task_definition:          \"this is a test\"\n\n\nPlan: 2 to add, 1 to change, 2 to destroy.\n\n------------------------------------------------------------------------\n\nThis plan was saved to: terraform.plan\n\nTo perform exactly these actions, run the following command to apply:\n    terraform apply \"terraform.plan\"\n"
  },
  {
    "path": "test/unit/data/08-no-attribute-name.expected.json",
    "content": "{\n  \"errors\": [\n    {\n      \"code\": \"UNABLE_TO_PARSE_ATTRIBUTE_NAME\",\n      \"message\": \"Attribute name not found on line \\\"      task_definition\\\" (ignored)\"\n    }\n  ],\n  \"changedResources\": [\n    {\n      \"action\": \"update\",\n      \"type\": \"aws_ecs_service\",\n      \"name\": \"blah\",\n      \"path\": \"aws_ecs_service.blah\",\n      \"changedAttributes\": {}\n    }\n  ],\n  \"changedDataSources\": []\n}\n"
  },
  {
    "path": "test/unit/data/08-no-attribute-name.stdout.txt",
    "content": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\naws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)\naws_iam_role.service_role: Refreshing state... (ID: SampleApp)\naws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)\naws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)\naws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)\nnull_resource.promote_images: Refreshing state... (ID: 1236159896537553123)\naws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)\naws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)\naws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  ~ update in-place\n-/+ destroy and then create replacement\n <= read (data resources)\n\nTerraform will perform the following actions:\n\n  ~ aws_ecs_service.blah\n      task_definition\n\n\nPlan: 2 to add, 1 to change, 2 to destroy.\n\n------------------------------------------------------------------------\n\nThis plan was saved to: terraform.plan\n\nTo perform exactly these actions, run the following command to apply:\n    terraform apply \"terraform.plan\"\n"
  },
  {
    "path": "test/unit/data/09-terraform-plan-windows-line-end.expected.json",
    "content": "{\n  \"errors\": [],\n  \"changedResources\": [\n    {\n      \"action\": \"update\",\n      \"type\": \"aws_ecs_service\",\n      \"name\": \"sample_app\",\n      \"path\": \"aws_ecs_service.sample_app\",\n      \"changedAttributes\": {\n        \"task_definition\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\"\n          },\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"${ aws_ecs_task_definition.sample_app.arn }\"\n          }\n        }\n      }\n    },\n    {\n      \"action\": \"replace\",\n      \"type\": \"aws_ecs_task_definition\",\n      \"name\": \"sample_app\",\n      \"path\": \"aws_ecs_task_definition.sample_app\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"sample-app\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          },\n          \"forcesNewResource\": true\n        },\n        \"arn\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"container_definitions\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"[{\\\"cpu\\\":1,\\\"environment\\\":[],\\\"essential\\\":true,\\\"image\\\":\\\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\\\",\\\"logConfiguration\\\":{\\\"logDriver\\\":\\\"awslogs\\\",\\\"options\\\":{\\\"awslogs-group\\\":\\\"sample-app\\\",\\\"awslogs-region\\\":\\\"us-east-1\\\",\\\"awslogs-stream-prefix\\\":\\\"sample-app\\\"}},\\\"memory\\\":256,\\\"mountPoints\\\":[],\\\"name\\\":\\\"sample-app\\\",\\\"portMappings\\\":[{\\\"containerPort\\\":8443,\\\"hostPort\\\":0,\\\"protocol\\\":\\\"tcp\\\"}],\\\"volumesFrom\\\":[]}]\"\n          },\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"[\\n  {\\n    \\\"name\\\": \\\"sample-app\\\",\\n    \\\"image\\\": \\\"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\\\"sample-app\\\"] }\\\",\\n    \\\"cpu\\\": 1,\\n    \\\"memory\\\": 256,\\n    \\\"essential\\\": true,\\n    \\\"logConfiguration\\\": {\\n      \\\"logDriver\\\": \\\"awslogs\\\",\\n      \\\"options\\\": {\\n        \\\"awslogs-group\\\": \\\"${ aws_cloudwatch_log_group.sample_app.name }\\\",\\n        \\\"awslogs-region\\\": \\\"${ var.target_aws_region }\\\",\\n        \\\"awslogs-stream-prefix\\\": \\\"sample-app\\\"\\n      }\\n    },\\n    \\\"portMappings\\\": [\\n      {\\n        \\\"containerPort\\\": 8443,\\n        \\\"hostPort\\\": 0\\n      }\\n    ]\\n  }\\n]\\n\"\n          },\n          \"forcesNewResource\": true\n        },\n        \"network_mode\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"revision\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"186\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        }\n      },\n      \"newResourceRequired\": true\n    },\n    {\n      \"action\": \"replace\",\n      \"type\": \"null_resource\",\n      \"name\": \"promote_images\",\n      \"path\": \"null_resource.promote_images\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"1236159896537553123\"\n          },\n          \"new\": {\n            \"type\": \"computed\"\n          },\n          \"forcesNewResource\": true\n        },\n        \"triggers.deploy_job_hash\": {\n          \"old\": {\n            \"type\": \"string\",\n            \"value\": \"6c37ac7175bdf35e24a2f2755addd238\"\n          },\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"1a0bd86fc5831ee66858f2e159efa547\"\n          },\n          \"forcesNewResource\": true\n        }\n      },\n      \"newResourceRequired\": true\n    }\n  ],\n  \"changedDataSources\": [\n    {\n      \"action\": \"read\",\n      \"type\": \"external\",\n      \"name\": \"ecr_image_digests\",\n      \"path\": \"data.external.ecr_image_digests\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"program.#\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"1\"\n          }\n        },\n        \"program.0\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"extract-image-digests\"\n          }\n        },\n        \"result.%\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "test/unit/data/09-terraform-plan-windows-line-end.stdout.txt",
    "content": "\u001b[0m\u001b[1mRefreshing Terraform state in-memory prior to plan...\u001b[0m\r\nThe refreshed state will be used to calculate this plan, but will not be\r\npersisted to local or remote state storage.\r\n\u001b[0m\r\n\u001b[0m\u001b[1maws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)\u001b[0m\r\n\u001b[0m\u001b[1maws_iam_role.service_role: Refreshing state... (ID: SampleApp)\u001b[0m\r\n\u001b[0m\u001b[1maws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)\u001b[0m\r\n\u001b[0m\u001b[1maws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)\u001b[0m\r\n\u001b[0m\u001b[1maws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)\u001b[0m\r\n\u001b[0m\u001b[1mnull_resource.promote_images: Refreshing state... (ID: 1236159896537553123)\u001b[0m\r\n\u001b[0m\u001b[1maws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)\u001b[0m\r\n\u001b[0m\u001b[1maws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)\u001b[0m\r\n\u001b[0m\u001b[1maws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)\u001b[0m\r\n\r\n------------------------------------------------------------------------\r\n\r\nAn execution plan has been generated and is shown below.\r\nResource actions are indicated with the following symbols:\r\n  \u001b[33m~\u001b[0m update in-place\r\n\u001b[31m-\u001b[0m/\u001b[32m+\u001b[0m destroy and then create replacement\r\n \u001b[36m<=\u001b[0m read (data resources)\r\n\u001b[0m\r\nTerraform will perform the following actions:\r\n\r\n\u001b[36m \u001b[36m<=\u001b[0m \u001b[36mdata.external.ecr_image_digests\r\n\u001b[0m      id:                       <computed>\r\n      program.#:                \"1\"\r\n      program.0:                \"extract-image-digests\"\r\n      result.%:                 <computed>\r\n\u001b[0m\r\n\u001b[0m\u001b[33m  \u001b[33m~\u001b[0m \u001b[33maws_ecs_service.sample_app\r\n\u001b[0m      task_definition:          \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\" => \"${ aws_ecs_task_definition.sample_app.arn }\"\r\n\u001b[0m\r\n\u001b[0m\u001b[33m\u001b[31m-\u001b[0m/\u001b[32m+\u001b[0m \u001b[33maws_ecs_task_definition.sample_app \u001b[31m\u001b[1m(new resource required)\u001b[0m\r\n\u001b[0m      id:                       \"sample-app\" => <computed> \u001b[31m(forces new resource)\u001b[0m\r\n      arn:                      \"arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186\" => <computed>\r\n      container_definitions:    \"[{\\\"cpu\\\":1,\\\"environment\\\":[],\\\"essential\\\":true,\\\"image\\\":\\\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\\\",\\\"logConfiguration\\\":{\\\"logDriver\\\":\\\"awslogs\\\",\\\"options\\\":{\\\"awslogs-group\\\":\\\"sample-app\\\",\\\"awslogs-region\\\":\\\"us-east-1\\\",\\\"awslogs-stream-prefix\\\":\\\"sample-app\\\"}},\\\"memory\\\":256,\\\"mountPoints\\\":[],\\\"name\\\":\\\"sample-app\\\",\\\"portMappings\\\":[{\\\"containerPort\\\":8443,\\\"hostPort\\\":0,\\\"protocol\\\":\\\"tcp\\\"}],\\\"volumesFrom\\\":[]}]\" => \"[\\n  {\\n    \\\"name\\\": \\\"sample-app\\\",\\n    \\\"image\\\": \\\"${ aws_ecr_repository.sample_app.repository_url }@${ data.external.ecr_image_digests.result[\\\"sample-app\\\"] }\\\",\\n    \\\"cpu\\\": 1,\\n    \\\"memory\\\": 256,\\n    \\\"essential\\\": true,\\n    \\\"logConfiguration\\\": {\\n      \\\"logDriver\\\": \\\"awslogs\\\",\\n      \\\"options\\\": {\\n        \\\"awslogs-group\\\": \\\"${ aws_cloudwatch_log_group.sample_app.name }\\\",\\n        \\\"awslogs-region\\\": \\\"${ var.target_aws_region }\\\",\\n        \\\"awslogs-stream-prefix\\\": \\\"sample-app\\\"\\n      }\\n    },\\n    \\\"portMappings\\\": [\\n      {\\n        \\\"containerPort\\\": 8443,\\n        \\\"hostPort\\\": 0\\n      }\\n    ]\\n  }\\n]\\n\" \u001b[31m(forces new resource)\u001b[0m\r\n      family:                   \"sample-app\" => \"sample-app\"\r\n      network_mode:             \"\" => <computed>\r\n      revision:                 \"186\" => <computed>\r\n      task_role_arn:            \"arn:aws:iam::123123123123:role/SampleApp\" => \"arn:aws:iam::123123123123:role/SampleApp\"\r\n\u001b[0m\r\n\u001b[0m\u001b[33m\u001b[31m-\u001b[0m/\u001b[32m+\u001b[0m \u001b[33mnull_resource.promote_images \u001b[31m\u001b[1m(new resource required)\u001b[0m\r\n\u001b[0m      id:                       \"1236159896537553123\" => <computed> \u001b[31m(forces new resource)\u001b[0m\r\n      triggers.%:               \"1\" => \"1\"\r\n      triggers.deploy_job_hash: \"6c37ac7175bdf35e24a2f2755addd238\" => \"1a0bd86fc5831ee66858f2e159efa547\" \u001b[31m(forces new resource)\u001b[0m\r\n\u001b[0m\r\n\u001b[0m\r\n\u001b[0m\u001b[1mPlan:\u001b[0m 2 to add, 1 to change, 2 to destroy.\u001b[0m\r\n\r\n------------------------------------------------------------------------\r\n\r\nThis plan was saved to: terraform.plan\r\n\r\nTo perform exactly these actions, run the following command to apply:\r\n    terraform apply \"terraform.plan\"\r\n"
  },
  {
    "path": "test/unit/data/10-issue-4.expected.json",
    "content": "{\n  \"errors\": [],\n  \"changedResources\": [\n    {\n      \"action\": \"create\",\n      \"type\": \"aws_iam_role\",\n      \"name\": \"terraform_demo\",\n      \"path\": \"aws_iam_role.terraform_demo\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"arn\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"assume_role_policy\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"{\\n \\\"Version\\\": \\\"2012-10-17\\\",\\n \\\"Statement\\\": [\\n   {\\n     \\\"Action\\\": \\\"sts:AssumeRole\\\",\\n     \\\"Principal\\\": {\\n       \\\"Service\\\": \\\"ec2.amazonaws.com\\\"\\n     },\\n     \\\"Effect\\\": \\\"Allow\\\",\\n     \\\"Sid\\\": \\\"\\\"\\n   }\\n ]\\n}\\n\"\n          }\n        },\n        \"create_date\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"description\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"test IAM role\"\n          }\n        },\n        \"force_detach_policies\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"false\"\n          }\n        },\n        \"name\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"terraform_demo\"\n          }\n        },\n        \"path\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"/\"\n          }\n        },\n        \"unique_id\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        }\n      }\n    }\n  ],\n  \"changedDataSources\": []\n}\n"
  },
  {
    "path": "test/unit/data/10-issue-4.stdout.txt",
    "content": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  + create\n\nTerraform will perform the following actions:\n\n+ aws_iam_role.terraform_demo\n      id:                    <computed>\n      arn:                   <computed>\n      assume_role_policy:    \"{\\n \\\"Version\\\": \\\"2012-10-17\\\",\\n \\\"Statement\\\": [\\n   {\\n     \\\"Action\\\": \\\"sts:AssumeRole\\\",\\n     \\\"Principal\\\": {\\n       \\\"Service\\\": \\\"ec2.amazonaws.com\\\"\\n     },\\n     \\\"Effect\\\": \\\"Allow\\\",\\n     \\\"Sid\\\": \\\"\\\"\\n   }\\n ]\\n}\\n\"\n      create_date:           <computed>\n      description:           \"test IAM role\"\n      force_detach_policies: \"false\"\n      name:                  \"terraform_demo\"\n      path:                  \"/\"\n      unique_id:             <computed>\nPlan: 1 to add, 0 to change, 0 to destroy.\n\n------------------------------------------------------------------------\n\nThis plan was saved to: xxx/iam/terraform.tfstate.d/default/terraform.tfplan\n\nTo perform exactly these actions, run the following command to apply:\n    terraform apply \"xxx/iam/terraform.tfstate.d/default/terraform.tfplan\"\n"
  },
  {
    "path": "test/unit/data/11-tainted-resource.expected.json",
    "content": "{\n  \"errors\": [],\n  \"changedResources\": [\n    {\n      \"action\": \"replace\",\n      \"type\": \"aws_ecs_task_definition\",\n      \"name\": \"sample_app\",\n      \"path\": \"aws_ecs_task_definition.sample_app\",\n      \"tainted\": true,\n      \"changedAttributes\": {\n        \"id\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"myservice\"\n          },\n          \"forcesNewResource\": true\n        }\n      },\n      \"newResourceRequired\": true\n    }\n  ],\n  \"changedDataSources\": []\n}\n"
  },
  {
    "path": "test/unit/data/11-tainted-resource.stdout.txt",
    "content": "\nTerraform will perform the following actions:\n\n-/+ aws_ecs_task_definition.sample_app (tainted) (new resource required)\n      id:                       \"myservice\" (forces new resource)\n\nPlan: 1 to add, 0 to change, 1 to destroy.\n"
  },
  {
    "path": "test/unit/data/12-modules.expected.json",
    "content": "{\n  \"errors\": [],\n  \"changedResources\": [\n    {\n      \"action\": \"create\",\n      \"type\": \"null_resource\",\n      \"name\": \"test0\",\n      \"path\": \"null_resource.test0\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"triggers.%\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"1\"\n          }\n        },\n        \"triggers.trigger\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"test0\"\n          }\n        }\n      }\n    },\n    {\n      \"action\": \"create\",\n      \"module\": \"test1\",\n      \"type\": \"null_resource\",\n      \"name\": \"test1\",\n      \"path\": \"module.test1.null_resource.test1\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"triggers.%\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"1\"\n          }\n        },\n        \"triggers.trigger\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"test1\"\n          }\n        }\n      }\n    },\n    {\n      \"action\": \"create\",\n      \"module\": \"test1.test2\",\n      \"type\": \"null_resource\",\n      \"name\": \"test2\",\n      \"path\": \"module.test1.module.test2.null_resource.test2\",\n      \"changedAttributes\": {\n        \"id\": {\n          \"new\": {\n            \"type\": \"computed\"\n          }\n        },\n        \"triggers.%\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"1\"\n          }\n        },\n        \"triggers.trigger\": {\n          \"new\": {\n            \"type\": \"string\",\n            \"value\": \"test2\"\n          }\n        }\n      }\n    }\n  ],\n  \"changedDataSources\": []\n}\n"
  },
  {
    "path": "test/unit/data/12-modules.stdout.txt",
    "content": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  + create\n\nTerraform will perform the following actions:\n\n  + null_resource.test0\n      id:               <computed>\n      triggers.%:       \"1\"\n      triggers.trigger: \"test0\"\n\n  + module.test1.null_resource.test1\n      id:               <computed>\n      triggers.%:       \"1\"\n      triggers.trigger: \"test1\"\n\n  + module.test1.module.test2.null_resource.test2\n      id:               <computed>\n      triggers.%:       \"1\"\n      triggers.trigger: \"test2\"\n\n\nPlan: 3 to add, 0 to change, 0 to destroy.\n\n------------------------------------------------------------------------\n\nNote: You didn't specify an \"-out\" parameter to save this plan, so Terraform\ncan't guarantee that exactly these actions will be performed if\n\"terraform apply\" is subsequently run.\n"
  },
  {
    "path": "test/unit/data/13-no-changes.expected.json",
    "content": "{\n  \"errors\": [],\n  \"changedResources\": [],\n  \"changedDataSources\": []\n}\n"
  },
  {
    "path": "test/unit/data/13-no-changes.stdout.txt",
    "content": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\n\n------------------------------------------------------------------------\n\nNo changes. Infrastructure is up-to-date.\n\nThis means that Terraform did not detect any differences between your\nconfiguration and real physical resources that exist. As a result, no\nactions need to be performed.\n"
  },
  {
    "path": "test/unit/data/14-ignore-unchanged-attributes.expected.json",
    "content": "{\n    \"errors\": [],\n    \"changedResources\": [\n      {\n        \"action\": \"update\",\n        \"type\": \"aws_ecs_service\",\n        \"name\": \"sample_app\",\n        \"path\": \"aws_ecs_service.sample_app\",\n        \"changedAttributes\": {\n          \"setting.changed\": {\n            \"old\": {\n              \"type\": \"string\",\n              \"value\": \"A\"\n            },\n            \"new\": {\n              \"type\": \"string\",\n              \"value\": \"B\"\n            }\n          }\n        }\n      }\n    ],\n    \"changedDataSources\": []\n  }\n  "
  },
  {
    "path": "test/unit/data/14-ignore-unchanged-attributes.stdout.txt",
    "content": "Refreshing Terraform state in-memory prior to plan...\nThe refreshed state will be used to calculate this plan, but will not be\npersisted to local or remote state storage.\n\naws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)\n\n------------------------------------------------------------------------\n\nAn execution plan has been generated and is shown below.\nResource actions are indicated with the following symbols:\n  ~ update in-place\n-/+ destroy and then create replacement\n <= read (data resources)\n\nTerraform will perform the following actions:\n\n  ~ aws_ecs_service.sample_app\n      setting.unchanged: \"A\" => \"A\"\n      setting.changed:   \"A\" => \"B\"\n\n\n\nPlan: 0 to add, 1 to change, 0 to destroy.\n\n------------------------------------------------------------------------\n\nThis plan was saved to: terraform.plan\n\nTo perform exactly these actions, run the following command to apply:\n    terraform apply \"terraform.plan\"\n"
  },
  {
    "path": "test/unit/terraform-plan-parser.test.ts",
    "content": "import { promisify } from 'util';\nimport test from 'ava';\nimport * as path from 'path';\nimport * as fs from 'fs';\nimport { parseStdout, ParseResult } from '../../src';\n\nconst readFileAsync = promisify(fs.readFile);\n\nfunction readExpected (dataFile: string): ParseResult {\n  const dataObj: any = require(path.join(__dirname,'data', dataFile + '.expected.json'));\n  return dataObj as ParseResult;\n}\n\nasync function readActual (dataFile: string): Promise<ParseResult> {\n  const stdout = await readFileAsync(path.join(__dirname, 'data', dataFile + '.stdout.txt'),\n    { encoding: 'utf8' });\n  return parseStdout(stdout);\n}\n\nasync function runTest (dataName: string, t: any) {\n  const actual = await readActual(dataName);\n  const expected = readExpected(dataName);\n  t.deepEqual(actual, expected);\n}\n\ntest('should strip ansi color codes', async (t) => {\n  return runTest('00-terraform-plan', t);\n});\n\ntest('should parse terraform output - 01', async (t) => {\n  return runTest('01-terraform-plan', t);\n});\n\ntest('should parse terraform output and be fairly lenient - 02', async (t) => {\n  return runTest('02-terraform-plan', t);\n});\n\ntest('should parse terraform output and support all types of changes - 03', async (t) => {\n  return runTest('03-terraform-plan', t);\n});\n\ntest('should fail gracefully if no magic start string is found', async (t) => {\n  return runTest('04-no-magic-start', t);\n});\n\ntest('should fail gracefully if no magic end string is found', async (t) => {\n  return runTest('05-no-magic-end', t);\n});\n\ntest('should handle unexpected attribute value that is not delimited', async (t) => {\n  return runTest('06-attribute-value-unexpected-delimiter', t);\n});\n\ntest('should ignore invalid resource action line', async (t) => {\n  return runTest('07-invalid-action-line', t);\n});\n\ntest('should ignore attribute with missing name', async (t) => {\n  return runTest('08-no-attribute-name', t);\n});\n\ntest('should handle plan output with Windows line terminator', async (t) => {\n  return runTest('09-terraform-plan-windows-line-end', t);\n});\n\ntest('should handle sample provided in issue #4', async (t) => {\n  return runTest('10-issue-4', t);\n});\n\ntest('should handle tainted resources', async (t) => {\n  return runTest('11-tainted-resource', t);\n});\n\ntest('should handle modules', async (t) => {\n  return runTest('12-modules', t);\n});\n\ntest('should handle no changes', async (t) => {\n  return runTest('13-no-changes', t);\n});\n\ntest('should ignore unchanged attributes', async (t) => {\n  return runTest('14-ignore-unchanged-attributes', t);\n});\n"
  },
  {
    "path": "tools/bin/postbuild",
    "content": "#!/usr/bin/env node\n\n/* eslint-disable security/detect-object-injection */\n\nconst {\n  spawn,\n  chalk,\n  writeJsonFile,\n  readJsonFile,\n  ROOT_DIR,\n  DIST_DIR,\n  ROOT_PACKAGE_JSON_FILE,\n  DIST_PACKAGE_JSON_FILE\n} = require('../build-util');\n\nfunction rewritePackageJsonPaths (obj, keys) {\n  if (!obj) {\n    return;\n  }\n\n  if (!keys) {\n    keys = Object.keys(obj);\n  }\n\n  for (const key of keys) {\n    const value = obj[key];\n    if (typeof value === 'string') {\n      obj[key] = value.replace('./work/dist/', './');\n    }\n  }\n}\n\nasync function run () {\n  await spawn('cp', ['-r', 'LICENSE', 'README.md', 'src', `${DIST_DIR}/`], { cwd: ROOT_DIR });\n\n  const packageObj = await readJsonFile(ROOT_PACKAGE_JSON_FILE);\n\n  // Fix some of the paths\n  rewritePackageJsonPaths(packageObj.bin);\n  rewritePackageJsonPaths(packageObj, [\n    'main',\n    'module',\n    'jsnext:main',\n    'types'\n  ]);\n\n  await writeJsonFile(DIST_PACKAGE_JSON_FILE, packageObj);\n  return packageObj;\n}\n\nrun().then((builtPackage) => {\n  console.log(chalk.bold.green(`Built ${builtPackage.name}@${builtPackage.version}`));\n}).catch((err) => {\n  console.error(chalk.bold.red(`Error building. ${err.stack || err.toString()}`));\n});\n"
  },
  {
    "path": "tools/bin/publish",
    "content": "#!/usr/bin/env node\n\nconst VERSION_TYPE = process.argv[2] || 'patch';\nconst inquirer = require('inquirer');\n\nconst {\n  exec,\n  chalk,\n  writeJsonFile,\n  readJsonFile,\n  ROOT_DIR,\n  DIST_DIR,\n  ROOT_PACKAGE_JSON_FILE,\n  DIST_PACKAGE_JSON_FILE\n} = require('../build-util');\n\n// The possible version values are\n// [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease [--preid=<prerelease-id>] | from-git]\n// Since <newversion> can be almost anything, rather than checking for each\n// possibility, just verify that only a restricted number of characters are\n// provided\nconst ALLOWED_VERSION_PATTERN = /^[.0-9A-Za-z-]+$/\n\nasync function run() {\n\n  const inputs = await inquirer.prompt([\n    {\n      type: 'string',\n      name: 'mfa_token',\n      message: 'NPM MFA Token?',\n    }\n  ]);\n\n  // Sanity check version strings\n  if (!ALLOWED_VERSION_PATTERN.test(VERSION_TYPE)) {\n    throw new Error('Invalid version');\n  }\n\n  await exec(`npm version ${VERSION_TYPE} --no-git-tag-version`, { cwd: DIST_DIR });\n  await exec(`npm publish --access public --otp=\"${inputs.mfa_token}\"`, { cwd: DIST_DIR });\n\n  const rootPackageObj = await readJsonFile(ROOT_PACKAGE_JSON_FILE);\n  const distPackageObj = await readJsonFile(DIST_PACKAGE_JSON_FILE);\n\n  rootPackageObj.version = distPackageObj.version;\n\n  const tagName = `v${rootPackageObj.version}`;\n\n  await writeJsonFile(ROOT_PACKAGE_JSON_FILE, rootPackageObj);\n  await exec('git add .', { cwd: ROOT_DIR });\n  await exec(`git commit -m \"Published ${rootPackageObj.version}\"`, { cwd: ROOT_DIR });\n  await exec('git push', { cwd: ROOT_DIR });\n  await exec(`git tag ${tagName}`, { cwd: ROOT_DIR });\n  await exec(`git push origin ${tagName}`, { cwd: ROOT_DIR });\n\n  return rootPackageObj;\n}\n\nrun().then((publishedPackage) => {\n  console.log(chalk.bold.green(`Published ${publishedPackage.name}@${publishedPackage.version}`));\n}).catch((err) => {\n  console.error(chalk.bold.red(`Error publishing. ${err.stack || err.toString()}`));\n});\n"
  },
  {
    "path": "tools/build-util.js",
    "content": "/* eslint-disable security/detect-child-process */\n/* eslint-disable security/detect-non-literal-fs-filename */\n\nconst path = require('path');\nconst fs = require('fs');\nconst util = require('util');\nconst writeFile = util.promisify(fs.writeFile);\nconst readFile = util.promisify(fs.readFile);\nconst spawn = require('child_process').spawn;\n\nexports.ROOT_DIR = path.normalize(path.join(__dirname, '..'));\nexports.DIST_DIR = path.join(exports.ROOT_DIR, 'work/dist');\nexports.ROOT_PACKAGE_JSON_FILE = path.join(exports.ROOT_DIR, 'package.json');\nexports.DIST_PACKAGE_JSON_FILE = path.join(exports.DIST_DIR, 'package.json');\n\nexports.exec = util.promisify(require('child_process').exec);\nexports.spawn = async function (command, args, options) {\n  const child = spawn.apply(this, arguments);\n  if (!options.stdio) {\n    options.stdio = 'inherit';\n  }\n\n  return new Promise((resolve, reject) => {\n    child.on('close', (code) => {\n      if (code === 0) {\n        resolve();\n      } else {\n        reject(new Error(`${command} return non-zero exit code`));\n      }\n    });\n  });\n};\n\nexports.chalk = require('chalk');\n\nexports.writeJsonFile = async (file, obj) => {\n  return writeFile(file, JSON.stringify(obj, null, '  ') + '\\n', { encoding: 'utf8' });\n};\n\nexports.readJsonFile = async (file) => {\n  return JSON.parse(await readFile(file, {encoding: 'utf8'}));\n};\n"
  },
  {
    "path": "tsconfig-src-cjs.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./work/dist\",\n    \"module\": \"CommonJS\"\n  },\n  \"include\": [\n    \"src/**/*\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig-src-es6.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./work/dist/es6\",\n    \"module\": \"ES6\"\n  },\n  \"include\": [\n    \"src/**/*\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig-src-esnext.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./work/dist/esnext\",\n    \"module\": \"ESNext\"\n  },\n  \"include\": [\n    \"src/**/*\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig-test.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"./work/dist-test\",\n    \"module\": \"CommonJS\",\n    \"inlineSourceMap\": true,\n    \"inlineSources\": true\n  },\n  \"include\": [\n    \"test/**/*\"\n  ]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"baseUrl\": \".\",\n    \"outDir\": \"./work/dist\",\n    \"noUnusedLocals\": true,\n    \"allowSyntheticDefaultImports\": false,\n    \"noImplicitAny\": true,\n    \"noImplicitThis\": true,\n    \"alwaysStrict\": true,\n    \"strictNullChecks\": true,\n    \"strictFunctionTypes\": true,\n    \"allowJs\": false,\n    \"moduleResolution\": \"node\",\n    \"module\": \"ES6\",\n    \"target\": \"ESNext\",\n    \"pretty\": true,\n    \"inlineSourceMap\": false,\n    \"inlineSources\": false,\n    \"paths\": {\n      \"~/*\": [\"./*\"]\n    }\n  },\n  \"include\": [\n    \"src/**/*\",\n    \"test/**/*\"\n  ]\n}\n"
  },
  {
    "path": "tslint.json",
    "content": "{\n  \"extends\": \"tslint-config-semistandard\"\n}\n"
  }
]