[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: ci\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    name: test-${{ matrix.os }}-${{ matrix.deno }}\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        # Test on the oldest supported, the latest stable, and nightly\n        deno: [old, stable, canary]\n        os: [macOS-latest, windows-latest, ubuntu-latest]\n\n    steps:\n      # Some test cases are sensitive to line endings. Disable autocrlf on\n      # Windows to ensure consistent behavior.\n      - name: Disable autocrlf\n        if: runner.os == 'Windows'\n        run: git config --global core.autocrlf false\n\n      - name: Setup repo\n        uses: actions/checkout@v3\n\n      - name: Setup Deno\n        uses: denoland/setup-deno@v1\n        with:\n          # Make sure to keep this in sync with the one defined in version.ts.\n          # Also don't forget to update README.md.\n          deno-version: ${{ matrix.deno == 'old' && '1.46.0' || (matrix.deno == 'stable' && '2.x' || matrix.deno) }}\n\n      - run: deno --version\n\n      - name: Format\n        if: runner.os == 'Linux' && matrix.deno == 'stable'\n        run: deno fmt --check\n\n      - name: Lint\n        if: runner.os == 'Linux' && matrix.deno == 'stable'\n        run: deno lint\n\n      - name: Typecheck\n        if: runner.os == 'Linux' && matrix.deno == 'stable'\n        run: deno check deployctl.ts\n\n      # Skip temporarily (see https://github.com/denoland/deployctl/actions/runs/11500790181/job/32011870448?pr=342#step:8:148)\n      # - name: action/deps.js up-to-date\n      #   if: runner.os == 'Linux' && matrix.deno == 'stable'\n      #   run: |\n      #     # @deno/emit doesn't work if JSR modules are not in the cache.\n      #     # This is a workaround to cache the JSR modules beforehand.\n      #     deno cache ./src/utils/mod.ts\n      #     deno run --allow-read --allow-env --allow-net ./tools/bundle.ts ./src/utils/mod.ts > ./action/latest.deps.js\n      #     diff ./action/latest.deps.js ./action/deps.js\n\n      - name: Run tests\n        # Deno 1.x does not support lockfile v4. To work around this, we append\n        # `--no-lock` in this case.\n        run: deno test -A ${{ matrix.deno == 'old' && '--no-lock' || '' }} tests/ src/\n"
  },
  {
    "path": ".github/workflows/on-release.yml",
    "content": "name: Check on release\n\non:\n  release:\n    types: [created]\n\njobs:\n  check-on-release:\n    runs-on: [ubuntu-latest]\n\n    steps:\n      - name: Setup repo\n        uses: actions/checkout@v3\n\n      - name: Setup Deno\n        uses: denoland/setup-deno@v1\n\n      - name: check version match\n        run: deno task version-match\n        env:\n          RELEASE_TAG: ${{ github.event.release.tag_name }}\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write # The OIDC ID token is used for authentication with JSR.\n    steps:\n      - uses: actions/checkout@v4\n      - run: npx jsr publish\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    name: test-action\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n      contents: read\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n      - name: Deploy to Deno Deploy\n        uses: ./\n        with:\n          project: happy-rat-64\n          root: action/tests\n          entrypoint: hello.ts\n          import-map: ./import_map.json\n      - name: Deploy with single include\n        uses: ./\n        with:\n          project: happy-rat-64\n          root: action/tests\n          entrypoint: include_exclude.ts\n          include: include_exclude.ts\n      - name: Deploy with comma-separated include\n        uses: ./\n        with:\n          project: happy-rat-64\n          root: action\n          entrypoint: tests/include_exclude.ts\n          include: foo, tests/include_exclude.ts,bar\n      - name: Deploy with comma-separated exclude\n        uses: ./\n        with:\n          project: happy-rat-64\n          root: action/tests\n          entrypoint: include_exclude.ts\n          exclude: import_bomb1,import_bomb2\n      - name: Deploy with multiline exclude\n        uses: ./\n        with:\n          project: happy-rat-64\n          root: action/tests\n          entrypoint: include_exclude.ts\n          exclude: |\n            import_bomb1\n            import_bomb2\n      - name: Deploy combine include and exclude\n        uses: ./\n        with:\n          project: happy-rat-64\n          root: action\n          entrypoint: tests/include_exclude.ts\n          include: tests\n          exclude: |\n            tests/import_bomb1\n            tests/import_bomb2\n      - name: Always exclude node_modules directory\n        uses: ./\n        with:\n          project: happy-rat-64\n          root: action/tests/always_exclude_node_modules\n          entrypoint: main.ts\n      - name: Always exclude nested node_modules directory\n        uses: ./\n        with:\n          project: happy-rat-64\n          root: action/tests\n          entrypoint: always_exclude_node_modules/main.ts\n      - name: data URL entrypoint\n        uses: ./\n        with:\n          project: happy-rat-64\n          root: action/tests\n          entrypoint: \"data:,Deno.serve(() => new Response())\"\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"deno.enable\": true,\n  \"deno.lint\": true,\n  \"[javascript]\": {\n    \"editor.defaultFormatter\": \"denoland.vscode-deno\"\n  },\n  \"[javascriptreact]\": {\n    \"editor.defaultFormatter\": \"denoland.vscode-deno\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"denoland.vscode-deno\"\n  },\n  \"[typescriptreact]\": {\n    \"editor.defaultFormatter\": \"denoland.vscode-deno\"\n  }\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Deno Land\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 all\ncopies 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 THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "> [!NOTE]\n> This tool is only used for managing [Deno Deploy Classic](https://docs.deno.com/deploy/classic/) organizations and their projects. New Deno Deploy organizations use the `deno deploy` command built into the Deno Runtime. Learn more about the new `deno deploy` command in the [reference docs](https://docs.deno.com/runtime/reference/cli/deploy/)\n\n# deployctl\n\n`deployctl` is the command line tool for Deno Deploy. This repository also\ncontains the `denoland/deployctl` GitHub Action.\n\n## Prerequisite\n\nYou need to have Deno 1.46.0+ installed (latest version is recommended; just run\n`deno upgrade`)\n\n## Install\n\n```shell\ndeno install -gArf jsr:@deno/deployctl\n```\n\n## Usage\n\nThe easiest way to get started with `deployctl` is to deploy one of the examples\nin the [examples directory](./examples):\n\n```shell\ncd examples/hello-world\ndeployctl deploy\n```\n\nVisit the [deployctl docs](https://docs.deno.com/deploy/manual/deployctl) and\ncheck out the help output to learn all you can do with deployctl:\n\n```shell\ndeployctl -h\n```\n\n## Action Example\n\n```yml\nname: Deploy\n\non: push\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n\n    permissions:\n      id-token: write # This is required to allow the GitHub Action to authenticate with Deno Deploy.\n      contents: read\n\n    steps:\n      - name: Clone repository\n        uses: actions/checkout@v4\n\n      - name: Deploy to Deno Deploy\n        uses: denoland/deployctl@v1\n        with:\n          project: my-project # the name of the project on Deno Deploy\n          entrypoint: main.ts # the entrypoint to deploy\n```\n\nTo learn more about the action, checkout [action readme](./action/README.md).\n"
  },
  {
    "path": "action/README.md",
    "content": "# denoland/deployctl <!-- omit in toc -->\n\nGitHub Actions for deploying to Deno Deploy.\n\n> ⚠ If your project does not require a build step, we recommend you use the\n> [\"Automatic\" deployment mode](https://docs.deno.com/deploy/manual/ci_github#automatic)\n> of our GitHub integration. It is faster and requires no setup.\n\n- [Usage](#usage)\n  - [Permissions](#permissions)\n  - [Inputs](#inputs)\n- [Examples](#examples)\n  - [Deploy everything](#deploy-everything)\n  - [Deploy a directory](#deploy-a-directory)\n  - [Filter content with `include` and `exclude`](#filter-content-with-include-and-exclude)\n  - [Use external or absolute path as an entrypoint](#use-external-or-absolute-path-as-an-entrypoint)\n  - [Use import map](#use-import-map)\n\n## Usage\n\nTo deploy you just need to include the Deno Deploy GitHub Action as a step in\nyour workflow.\n\nYou do **not** need to set up any secrets for this to work.\n\nYou **do** need to link your GitHub repository to your Deno Deploy project. You\nhave to choose the \"GitHub Actions\" deployment mode in your project settings on\nhttps://dash.deno.com. Read\n[Deno Deploy documentation](https://docs.deno.com/deploy/manual/ci_github#github-action)\nfor more information.\n\n### Permissions\n\nYou have to set `id-token: write` permission to authenticate with Deno Deploy.\n\n```yaml\njobs:\n  deploy:\n    permissions:\n      id-token: write # required\n      contents: read\n    steps:\n# your steps here...\n```\n\n### Inputs\n\n```yaml\n- name: Deploy to Deno Deploy\n  uses: denoland/deployctl@v1\n  with:\n    # Name of the project on Deno Deploy\n    # Required.\n    project:\n\n    # Entrypoint location executed by Deno Deploy\n    # The entrypoint can be a relative path or an absolute URL.\n    # If it is a relative path, it will be resolved relative to the `root` directory.\n    # Required.\n    entrypoint:\n\n    # Root directory to deploy\n    # All files and subdirectories will be deployed.\n    # Optional. Default is \"process.cwd()\"\n    root:\n\n    # Filter which files to include in the deployment\n    # It supports a single file, multiple files separated by a comma or by a newline\n    # Optional.\n    include:\n\n    # Filter which files to exclude in the deployment\n    # It supports a single file, multiple files separated by a comma or by a newline\n    # Optional.\n    exclude:\n\n    # Location of an import map\n    # Must be relative to root directory\n    # Optional.\n    import-map:\n```\n\n## Examples\n\n### Deploy everything\n\nAll files and subdirectories in the **working directory** will be deployed.\n\n```yaml\n- name: Deploy to Deno Deploy\n  uses: denoland/deployctl@v1\n  with:\n    project: my-project\n    entrypoint: main.ts\n```\n\n### Deploy a directory\n\nAll files and subdirectories in the **specified directory** will be deployed.\n\n```yaml\n- name: Deploy to Deno Deploy\n  uses: denoland/deployctl@v1\n  with:\n    project: my-project\n    entrypoint: main.ts # the entrypoint is relative to the root directory (path/to/your/directory/main.ts)\n    root: path/to/your/directory\n```\n\n### Filter content with `include` and `exclude`\n\nUse `include` and `exclude` to filter which contents to deploy.\n\n```yaml\n- name: Deploy to Deno Deploy\n  uses: denoland/deployctl@v1\n  with:\n    project: my-project\n    entrypoint: main.ts # the entrypoint must be relative to the root directory\n    include: |\n      main.ts\n      dist\n    exclude: node_modules\n```\n\nYou can set a single file\n\n```yaml\ninclude: main.ts\n```\n\nmultiple files or directories, separated by a comma\n\n```yaml\ninclude: main.ts,dist\n```\n\nor separated by a newline\n\n```yaml\ninclude: |\n  main.ts\n  dist\n```\n\n### Use external or absolute path as an entrypoint\n\n`entrypoint` supports absolute path (`file://`) and external path (`https://`)\n\n```yaml\n- name: Deploy to Deno Deploy\n  uses: denoland/deployctl@v1\n  with:\n    project: my-project\n    entrypoint: https://your-external-path/mod.ts\n```\n\nAn interesting use case is to directly use\n[std/http/file_server.ts](https://deno.land/std/http/file_server.ts) as\nsuggested in\n[Deploy a static site](https://docs.deno.com/deploy/tutorials/static-site)\ntutorial.\n\n```yaml\n- name: Deploy to Deno Deploy\n  uses: denoland/deployctl@v1\n  with:\n    project: my-project\n    entrypoint: https://deno.land/std/http/file_server.ts\n```\n\n### Use import map\n\nYou can specify an [import map](https://github.com/WICG/import-maps).\n\n```yaml\n- name: Deploy to Deno Deploy\n  uses: denoland/deployctl@v1\n  with:\n    project: my-project\n    entrypoint: main.ts\n    import-map: path/to/import-map.json\n```\n"
  },
  {
    "path": "action/deps.js",
    "content": "// deno-fmt-ignore-file\n// deno-lint-ignore-file\n// This code was bundled using `deno task build-action` and it's not recommended to edit it manually\n\nfunction assertPath(path) {\n    if (typeof path !== \"string\") {\n        throw new TypeError(`Path must be a string. Received ${JSON.stringify(path)}`);\n    }\n}\nconst CHAR_FORWARD_SLASH = 47;\nfunction isPathSeparator(code) {\n    return code === 47 || code === 92;\n}\nfunction isWindowsDeviceRoot(code) {\n    return code >= 97 && code <= 122 || code >= 65 && code <= 90;\n}\nfunction assertArg(url) {\n    url = url instanceof URL ? url : new URL(url);\n    if (url.protocol !== \"file:\") {\n        throw new TypeError(\"Must be a file URL.\");\n    }\n    return url;\n}\nfunction fromFileUrl(url) {\n    url = assertArg(url);\n    let path = decodeURIComponent(url.pathname.replace(/\\//g, \"\\\\\").replace(/%(?![0-9A-Fa-f]{2})/g, \"%25\")).replace(/^\\\\*([A-Za-z]:)(\\\\|$)/, \"$1\\\\\");\n    if (url.hostname !== \"\") {\n        path = `\\\\\\\\${url.hostname}${path}`;\n    }\n    return path;\n}\nfunction isAbsolute(path) {\n    assertPath(path);\n    const len = path.length;\n    if (len === 0) return false;\n    const code = path.charCodeAt(0);\n    if (isPathSeparator(code)) {\n        return true;\n    } else if (isWindowsDeviceRoot(code)) {\n        if (len > 2 && path.charCodeAt(1) === 58) {\n            if (isPathSeparator(path.charCodeAt(2))) return true;\n        }\n    }\n    return false;\n}\nclass AssertionError extends Error {\n    constructor(message){\n        super(message);\n        this.name = \"AssertionError\";\n    }\n}\nfunction assert(expr, msg = \"\") {\n    if (!expr) {\n        throw new AssertionError(msg);\n    }\n}\nfunction assertArg1(path) {\n    assertPath(path);\n    if (path.length === 0) return \".\";\n}\nfunction normalizeString(path, allowAboveRoot, separator, isPathSeparator) {\n    let res = \"\";\n    let lastSegmentLength = 0;\n    let lastSlash = -1;\n    let dots = 0;\n    let code;\n    for(let i = 0, len = path.length; i <= len; ++i){\n        if (i < len) code = path.charCodeAt(i);\n        else if (isPathSeparator(code)) break;\n        else code = CHAR_FORWARD_SLASH;\n        if (isPathSeparator(code)) {\n            if (lastSlash === i - 1 || dots === 1) {} else if (lastSlash !== i - 1 && dots === 2) {\n                if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {\n                    if (res.length > 2) {\n                        const lastSlashIndex = res.lastIndexOf(separator);\n                        if (lastSlashIndex === -1) {\n                            res = \"\";\n                            lastSegmentLength = 0;\n                        } else {\n                            res = res.slice(0, lastSlashIndex);\n                            lastSegmentLength = res.length - 1 - res.lastIndexOf(separator);\n                        }\n                        lastSlash = i;\n                        dots = 0;\n                        continue;\n                    } else if (res.length === 2 || res.length === 1) {\n                        res = \"\";\n                        lastSegmentLength = 0;\n                        lastSlash = i;\n                        dots = 0;\n                        continue;\n                    }\n                }\n                if (allowAboveRoot) {\n                    if (res.length > 0) res += `${separator}..`;\n                    else res = \"..\";\n                    lastSegmentLength = 2;\n                }\n            } else {\n                if (res.length > 0) res += separator + path.slice(lastSlash + 1, i);\n                else res = path.slice(lastSlash + 1, i);\n                lastSegmentLength = i - lastSlash - 1;\n            }\n            lastSlash = i;\n            dots = 0;\n        } else if (code === 46 && dots !== -1) {\n            ++dots;\n        } else {\n            dots = -1;\n        }\n    }\n    return res;\n}\nfunction normalize(path) {\n    assertArg1(path);\n    const len = path.length;\n    let rootEnd = 0;\n    let device;\n    let isAbsolute = false;\n    const code = path.charCodeAt(0);\n    if (len > 1) {\n        if (isPathSeparator(code)) {\n            isAbsolute = true;\n            if (isPathSeparator(path.charCodeAt(1))) {\n                let j = 2;\n                let last = j;\n                for(; j < len; ++j){\n                    if (isPathSeparator(path.charCodeAt(j))) break;\n                }\n                if (j < len && j !== last) {\n                    const firstPart = path.slice(last, j);\n                    last = j;\n                    for(; j < len; ++j){\n                        if (!isPathSeparator(path.charCodeAt(j))) break;\n                    }\n                    if (j < len && j !== last) {\n                        last = j;\n                        for(; j < len; ++j){\n                            if (isPathSeparator(path.charCodeAt(j))) break;\n                        }\n                        if (j === len) {\n                            return `\\\\\\\\${firstPart}\\\\${path.slice(last)}\\\\`;\n                        } else if (j !== last) {\n                            device = `\\\\\\\\${firstPart}\\\\${path.slice(last, j)}`;\n                            rootEnd = j;\n                        }\n                    }\n                }\n            } else {\n                rootEnd = 1;\n            }\n        } else if (isWindowsDeviceRoot(code)) {\n            if (path.charCodeAt(1) === 58) {\n                device = path.slice(0, 2);\n                rootEnd = 2;\n                if (len > 2) {\n                    if (isPathSeparator(path.charCodeAt(2))) {\n                        isAbsolute = true;\n                        rootEnd = 3;\n                    }\n                }\n            }\n        }\n    } else if (isPathSeparator(code)) {\n        return \"\\\\\";\n    }\n    let tail;\n    if (rootEnd < len) {\n        tail = normalizeString(path.slice(rootEnd), !isAbsolute, \"\\\\\", isPathSeparator);\n    } else {\n        tail = \"\";\n    }\n    if (tail.length === 0 && !isAbsolute) tail = \".\";\n    if (tail.length > 0 && isPathSeparator(path.charCodeAt(len - 1))) {\n        tail += \"\\\\\";\n    }\n    if (device === undefined) {\n        if (isAbsolute) {\n            if (tail.length > 0) return `\\\\${tail}`;\n            else return \"\\\\\";\n        } else if (tail.length > 0) {\n            return tail;\n        } else {\n            return \"\";\n        }\n    } else if (isAbsolute) {\n        if (tail.length > 0) return `${device}\\\\${tail}`;\n        else return `${device}\\\\`;\n    } else if (tail.length > 0) {\n        return device + tail;\n    } else {\n        return device;\n    }\n}\nfunction join(...paths) {\n    if (paths.length === 0) return \".\";\n    let joined;\n    let firstPart = null;\n    for(let i = 0; i < paths.length; ++i){\n        const path = paths[i];\n        assertPath(path);\n        if (path.length > 0) {\n            if (joined === undefined) joined = firstPart = path;\n            else joined += `\\\\${path}`;\n        }\n    }\n    if (joined === undefined) return \".\";\n    let needsReplace = true;\n    let slashCount = 0;\n    assert(firstPart !== null);\n    if (isPathSeparator(firstPart.charCodeAt(0))) {\n        ++slashCount;\n        const firstLen = firstPart.length;\n        if (firstLen > 1) {\n            if (isPathSeparator(firstPart.charCodeAt(1))) {\n                ++slashCount;\n                if (firstLen > 2) {\n                    if (isPathSeparator(firstPart.charCodeAt(2))) ++slashCount;\n                    else {\n                        needsReplace = false;\n                    }\n                }\n            }\n        }\n    }\n    if (needsReplace) {\n        for(; slashCount < joined.length; ++slashCount){\n            if (!isPathSeparator(joined.charCodeAt(slashCount))) break;\n        }\n        if (slashCount >= 2) joined = `\\\\${joined.slice(slashCount)}`;\n    }\n    return normalize(joined);\n}\nfunction resolve(...pathSegments) {\n    let resolvedDevice = \"\";\n    let resolvedTail = \"\";\n    let resolvedAbsolute = false;\n    for(let i = pathSegments.length - 1; i >= -1; i--){\n        let path;\n        const { Deno: Deno1 } = globalThis;\n        if (i >= 0) {\n            path = pathSegments[i];\n        } else if (!resolvedDevice) {\n            if (typeof Deno1?.cwd !== \"function\") {\n                throw new TypeError(\"Resolved a drive-letter-less path without a CWD.\");\n            }\n            path = Deno1.cwd();\n        } else {\n            if (typeof Deno1?.env?.get !== \"function\" || typeof Deno1?.cwd !== \"function\") {\n                throw new TypeError(\"Resolved a relative path without a CWD.\");\n            }\n            path = Deno1.cwd();\n            if (path === undefined || path.slice(0, 3).toLowerCase() !== `${resolvedDevice.toLowerCase()}\\\\`) {\n                path = `${resolvedDevice}\\\\`;\n            }\n        }\n        assertPath(path);\n        const len = path.length;\n        if (len === 0) continue;\n        let rootEnd = 0;\n        let device = \"\";\n        let isAbsolute = false;\n        const code = path.charCodeAt(0);\n        if (len > 1) {\n            if (isPathSeparator(code)) {\n                isAbsolute = true;\n                if (isPathSeparator(path.charCodeAt(1))) {\n                    let j = 2;\n                    let last = j;\n                    for(; j < len; ++j){\n                        if (isPathSeparator(path.charCodeAt(j))) break;\n                    }\n                    if (j < len && j !== last) {\n                        const firstPart = path.slice(last, j);\n                        last = j;\n                        for(; j < len; ++j){\n                            if (!isPathSeparator(path.charCodeAt(j))) break;\n                        }\n                        if (j < len && j !== last) {\n                            last = j;\n                            for(; j < len; ++j){\n                                if (isPathSeparator(path.charCodeAt(j))) break;\n                            }\n                            if (j === len) {\n                                device = `\\\\\\\\${firstPart}\\\\${path.slice(last)}`;\n                                rootEnd = j;\n                            } else if (j !== last) {\n                                device = `\\\\\\\\${firstPart}\\\\${path.slice(last, j)}`;\n                                rootEnd = j;\n                            }\n                        }\n                    }\n                } else {\n                    rootEnd = 1;\n                }\n            } else if (isWindowsDeviceRoot(code)) {\n                if (path.charCodeAt(1) === 58) {\n                    device = path.slice(0, 2);\n                    rootEnd = 2;\n                    if (len > 2) {\n                        if (isPathSeparator(path.charCodeAt(2))) {\n                            isAbsolute = true;\n                            rootEnd = 3;\n                        }\n                    }\n                }\n            }\n        } else if (isPathSeparator(code)) {\n            rootEnd = 1;\n            isAbsolute = true;\n        }\n        if (device.length > 0 && resolvedDevice.length > 0 && device.toLowerCase() !== resolvedDevice.toLowerCase()) {\n            continue;\n        }\n        if (resolvedDevice.length === 0 && device.length > 0) {\n            resolvedDevice = device;\n        }\n        if (!resolvedAbsolute) {\n            resolvedTail = `${path.slice(rootEnd)}\\\\${resolvedTail}`;\n            resolvedAbsolute = isAbsolute;\n        }\n        if (resolvedAbsolute && resolvedDevice.length > 0) break;\n    }\n    resolvedTail = normalizeString(resolvedTail, !resolvedAbsolute, \"\\\\\", isPathSeparator);\n    return resolvedDevice + (resolvedAbsolute ? \"\\\\\" : \"\") + resolvedTail || \".\";\n}\nconst WHITESPACE_ENCODINGS = {\n    \"\\u0009\": \"%09\",\n    \"\\u000A\": \"%0A\",\n    \"\\u000B\": \"%0B\",\n    \"\\u000C\": \"%0C\",\n    \"\\u000D\": \"%0D\",\n    \"\\u0020\": \"%20\"\n};\nfunction encodeWhitespace(string) {\n    return string.replaceAll(/[\\s]/g, (c)=>{\n        return WHITESPACE_ENCODINGS[c] ?? c;\n    });\n}\nfunction toFileUrl(path) {\n    if (!isAbsolute(path)) {\n        throw new TypeError(\"Must be an absolute path.\");\n    }\n    const [, hostname, pathname] = path.match(/^(?:[/\\\\]{2}([^/\\\\]+)(?=[/\\\\](?:[^/\\\\]|$)))?(.*)/);\n    const url = new URL(\"file:///\");\n    url.pathname = encodeWhitespace(pathname.replace(/%/g, \"%25\"));\n    if (hostname !== undefined && hostname !== \"localhost\") {\n        url.hostname = hostname;\n        if (!url.hostname) {\n            throw new TypeError(\"Invalid hostname.\");\n        }\n    }\n    return url;\n}\nconst regExpEscapeChars = [\n    \"!\",\n    \"$\",\n    \"(\",\n    \")\",\n    \"*\",\n    \"+\",\n    \".\",\n    \"=\",\n    \"?\",\n    \"[\",\n    \"\\\\\",\n    \"^\",\n    \"{\",\n    \"|\"\n];\nconst rangeEscapeChars = [\n    \"-\",\n    \"\\\\\",\n    \"]\"\n];\nfunction _globToRegExp(c, glob, { extended = true, globstar: globstarOption = true, caseInsensitive = false } = {}) {\n    if (glob === \"\") {\n        return /(?!)/;\n    }\n    let newLength = glob.length;\n    for(; newLength > 1 && c.seps.includes(glob[newLength - 1]); newLength--);\n    glob = glob.slice(0, newLength);\n    let regExpString = \"\";\n    for(let j = 0; j < glob.length;){\n        let segment = \"\";\n        const groupStack = [];\n        let inRange = false;\n        let inEscape = false;\n        let endsWithSep = false;\n        let i = j;\n        for(; i < glob.length && !c.seps.includes(glob[i]); i++){\n            if (inEscape) {\n                inEscape = false;\n                const escapeChars = inRange ? rangeEscapeChars : regExpEscapeChars;\n                segment += escapeChars.includes(glob[i]) ? `\\\\${glob[i]}` : glob[i];\n                continue;\n            }\n            if (glob[i] === c.escapePrefix) {\n                inEscape = true;\n                continue;\n            }\n            if (glob[i] === \"[\") {\n                if (!inRange) {\n                    inRange = true;\n                    segment += \"[\";\n                    if (glob[i + 1] === \"!\") {\n                        i++;\n                        segment += \"^\";\n                    } else if (glob[i + 1] === \"^\") {\n                        i++;\n                        segment += \"\\\\^\";\n                    }\n                    continue;\n                } else if (glob[i + 1] === \":\") {\n                    let k = i + 1;\n                    let value = \"\";\n                    while(glob[k + 1] !== undefined && glob[k + 1] !== \":\"){\n                        value += glob[k + 1];\n                        k++;\n                    }\n                    if (glob[k + 1] === \":\" && glob[k + 2] === \"]\") {\n                        i = k + 2;\n                        if (value === \"alnum\") segment += \"\\\\dA-Za-z\";\n                        else if (value === \"alpha\") segment += \"A-Za-z\";\n                        else if (value === \"ascii\") segment += \"\\x00-\\x7F\";\n                        else if (value === \"blank\") segment += \"\\t \";\n                        else if (value === \"cntrl\") segment += \"\\x00-\\x1F\\x7F\";\n                        else if (value === \"digit\") segment += \"\\\\d\";\n                        else if (value === \"graph\") segment += \"\\x21-\\x7E\";\n                        else if (value === \"lower\") segment += \"a-z\";\n                        else if (value === \"print\") segment += \"\\x20-\\x7E\";\n                        else if (value === \"punct\") {\n                            segment += \"!\\\"#$%&'()*+,\\\\-./:;<=>?@[\\\\\\\\\\\\]^_‘{|}~\";\n                        } else if (value === \"space\") segment += \"\\\\s\\v\";\n                        else if (value === \"upper\") segment += \"A-Z\";\n                        else if (value === \"word\") segment += \"\\\\w\";\n                        else if (value === \"xdigit\") segment += \"\\\\dA-Fa-f\";\n                        continue;\n                    }\n                }\n            }\n            if (glob[i] === \"]\" && inRange) {\n                inRange = false;\n                segment += \"]\";\n                continue;\n            }\n            if (inRange) {\n                if (glob[i] === \"\\\\\") {\n                    segment += `\\\\\\\\`;\n                } else {\n                    segment += glob[i];\n                }\n                continue;\n            }\n            if (glob[i] === \")\" && groupStack.length > 0 && groupStack[groupStack.length - 1] !== \"BRACE\") {\n                segment += \")\";\n                const type = groupStack.pop();\n                if (type === \"!\") {\n                    segment += c.wildcard;\n                } else if (type !== \"@\") {\n                    segment += type;\n                }\n                continue;\n            }\n            if (glob[i] === \"|\" && groupStack.length > 0 && groupStack[groupStack.length - 1] !== \"BRACE\") {\n                segment += \"|\";\n                continue;\n            }\n            if (glob[i] === \"+\" && extended && glob[i + 1] === \"(\") {\n                i++;\n                groupStack.push(\"+\");\n                segment += \"(?:\";\n                continue;\n            }\n            if (glob[i] === \"@\" && extended && glob[i + 1] === \"(\") {\n                i++;\n                groupStack.push(\"@\");\n                segment += \"(?:\";\n                continue;\n            }\n            if (glob[i] === \"?\") {\n                if (extended && glob[i + 1] === \"(\") {\n                    i++;\n                    groupStack.push(\"?\");\n                    segment += \"(?:\";\n                } else {\n                    segment += \".\";\n                }\n                continue;\n            }\n            if (glob[i] === \"!\" && extended && glob[i + 1] === \"(\") {\n                i++;\n                groupStack.push(\"!\");\n                segment += \"(?!\";\n                continue;\n            }\n            if (glob[i] === \"{\") {\n                groupStack.push(\"BRACE\");\n                segment += \"(?:\";\n                continue;\n            }\n            if (glob[i] === \"}\" && groupStack[groupStack.length - 1] === \"BRACE\") {\n                groupStack.pop();\n                segment += \")\";\n                continue;\n            }\n            if (glob[i] === \",\" && groupStack[groupStack.length - 1] === \"BRACE\") {\n                segment += \"|\";\n                continue;\n            }\n            if (glob[i] === \"*\") {\n                if (extended && glob[i + 1] === \"(\") {\n                    i++;\n                    groupStack.push(\"*\");\n                    segment += \"(?:\";\n                } else {\n                    const prevChar = glob[i - 1];\n                    let numStars = 1;\n                    while(glob[i + 1] === \"*\"){\n                        i++;\n                        numStars++;\n                    }\n                    const nextChar = glob[i + 1];\n                    if (globstarOption && numStars === 2 && [\n                        ...c.seps,\n                        undefined\n                    ].includes(prevChar) && [\n                        ...c.seps,\n                        undefined\n                    ].includes(nextChar)) {\n                        segment += c.globstar;\n                        endsWithSep = true;\n                    } else {\n                        segment += c.wildcard;\n                    }\n                }\n                continue;\n            }\n            segment += regExpEscapeChars.includes(glob[i]) ? `\\\\${glob[i]}` : glob[i];\n        }\n        if (groupStack.length > 0 || inRange || inEscape) {\n            segment = \"\";\n            for (const c of glob.slice(j, i)){\n                segment += regExpEscapeChars.includes(c) ? `\\\\${c}` : c;\n                endsWithSep = false;\n            }\n        }\n        regExpString += segment;\n        if (!endsWithSep) {\n            regExpString += i < glob.length ? c.sep : c.sepMaybe;\n            endsWithSep = true;\n        }\n        while(c.seps.includes(glob[i]))i++;\n        if (!(i > j)) {\n            throw new Error(\"Assertion failure: i > j (potential infinite loop)\");\n        }\n        j = i;\n    }\n    regExpString = `^${regExpString}$`;\n    return new RegExp(regExpString, caseInsensitive ? \"i\" : \"\");\n}\nconst constants = {\n    sep: \"(?:\\\\\\\\|/)+\",\n    sepMaybe: \"(?:\\\\\\\\|/)*\",\n    seps: [\n        \"\\\\\",\n        \"/\"\n    ],\n    globstar: \"(?:[^\\\\\\\\/]*(?:\\\\\\\\|/|$)+)*\",\n    wildcard: \"[^\\\\\\\\/]*\",\n    escapePrefix: \"`\"\n};\nfunction globToRegExp(glob, options = {}) {\n    return _globToRegExp(constants, glob, options);\n}\nfunction isGlob(str) {\n    const chars = {\n        \"{\": \"}\",\n        \"(\": \")\",\n        \"[\": \"]\"\n    };\n    const regex = /\\\\(.)|(^!|\\*|\\?|[\\].+)]\\?|\\[[^\\\\\\]]+\\]|\\{[^\\\\}]+\\}|\\(\\?[:!=][^\\\\)]+\\)|\\([^|]+\\|[^\\\\)]+\\))/;\n    if (str === \"\") {\n        return false;\n    }\n    let match;\n    while(match = regex.exec(str)){\n        if (match[2]) return true;\n        let idx = match.index + match[0].length;\n        const open = match[1];\n        const close = open ? chars[open] : null;\n        if (open && close) {\n            const n = str.indexOf(close, idx);\n            if (n !== -1) {\n                idx = n + 1;\n            }\n        }\n        str = str.slice(idx);\n    }\n    return false;\n}\nfunction isPosixPathSeparator(code) {\n    return code === 47;\n}\nfunction fromFileUrl1(url) {\n    url = assertArg(url);\n    return decodeURIComponent(url.pathname.replace(/%(?![0-9A-Fa-f]{2})/g, \"%25\"));\n}\nfunction isAbsolute1(path) {\n    assertPath(path);\n    return path.length > 0 && isPosixPathSeparator(path.charCodeAt(0));\n}\nfunction normalize1(path) {\n    assertArg1(path);\n    const isAbsolute = isPosixPathSeparator(path.charCodeAt(0));\n    const trailingSeparator = isPosixPathSeparator(path.charCodeAt(path.length - 1));\n    path = normalizeString(path, !isAbsolute, \"/\", isPosixPathSeparator);\n    if (path.length === 0 && !isAbsolute) path = \".\";\n    if (path.length > 0 && trailingSeparator) path += \"/\";\n    if (isAbsolute) return `/${path}`;\n    return path;\n}\nfunction join1(...paths) {\n    if (paths.length === 0) return \".\";\n    let joined;\n    for(let i = 0, len = paths.length; i < len; ++i){\n        const path = paths[i];\n        assertPath(path);\n        if (path.length > 0) {\n            if (!joined) joined = path;\n            else joined += `/${path}`;\n        }\n    }\n    if (!joined) return \".\";\n    return normalize1(joined);\n}\nfunction resolve1(...pathSegments) {\n    let resolvedPath = \"\";\n    let resolvedAbsolute = false;\n    for(let i = pathSegments.length - 1; i >= -1 && !resolvedAbsolute; i--){\n        let path;\n        if (i >= 0) path = pathSegments[i];\n        else {\n            const { Deno: Deno1 } = globalThis;\n            if (typeof Deno1?.cwd !== \"function\") {\n                throw new TypeError(\"Resolved a relative path without a CWD.\");\n            }\n            path = Deno1.cwd();\n        }\n        assertPath(path);\n        if (path.length === 0) {\n            continue;\n        }\n        resolvedPath = `${path}/${resolvedPath}`;\n        resolvedAbsolute = isPosixPathSeparator(path.charCodeAt(0));\n    }\n    resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute, \"/\", isPosixPathSeparator);\n    if (resolvedAbsolute) {\n        if (resolvedPath.length > 0) return `/${resolvedPath}`;\n        else return \"/\";\n    } else if (resolvedPath.length > 0) return resolvedPath;\n    else return \".\";\n}\nfunction toFileUrl1(path) {\n    if (!isAbsolute1(path)) {\n        throw new TypeError(\"Must be an absolute path.\");\n    }\n    const url = new URL(\"file:///\");\n    url.pathname = encodeWhitespace(path.replace(/%/g, \"%25\").replace(/\\\\/g, \"%5C\"));\n    return url;\n}\nconst constants1 = {\n    sep: \"/+\",\n    sepMaybe: \"/*\",\n    seps: [\n        \"/\"\n    ],\n    globstar: \"(?:[^/]*(?:/|$)+)*\",\n    wildcard: \"[^/]*\",\n    escapePrefix: \"\\\\\"\n};\nfunction globToRegExp1(glob, options = {}) {\n    return _globToRegExp(constants1, glob, options);\n}\nconst osType = (()=>{\n    const { Deno: Deno1 } = globalThis;\n    if (typeof Deno1?.build?.os === \"string\") {\n        return Deno1.build.os;\n    }\n    const { navigator } = globalThis;\n    if (navigator?.appVersion?.includes?.(\"Win\")) {\n        return \"windows\";\n    }\n    return \"linux\";\n})();\nconst isWindows = osType === \"windows\";\nfunction fromFileUrl2(url) {\n    return isWindows ? fromFileUrl(url) : fromFileUrl1(url);\n}\nfunction join2(...paths) {\n    return isWindows ? join(...paths) : join1(...paths);\n}\nfunction normalize2(path) {\n    return isWindows ? normalize(path) : normalize1(path);\n}\nfunction resolve2(...pathSegments) {\n    return isWindows ? resolve(...pathSegments) : resolve1(...pathSegments);\n}\nfunction toFileUrl2(path) {\n    return isWindows ? toFileUrl(path) : toFileUrl1(path);\n}\nfunction globToRegExp2(glob, options = {}) {\n    return options.os === \"windows\" || !options.os && isWindows ? globToRegExp(glob, options) : globToRegExp1(glob, options);\n}\nconst { Deno: Deno1 } = globalThis;\ntypeof Deno1?.noColor === \"boolean\" ? Deno1.noColor : false;\nnew RegExp([\n    \"[\\\\u001B\\\\u009B][[\\\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]+)*|[a-zA-Z\\\\d]+(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]*)*)?\\\\u0007)\",\n    \"(?:(?:\\\\d{1,4}(?:;\\\\d{0,4})*)?[\\\\dA-PR-TXZcf-nq-uy=><~]))\"\n].join(\"|\"), \"g\");\nconst DEFAULT_STRINGIFY_OPTIONS = {\n    verbose: false\n};\nfunction stringify(err, options) {\n    const opts = options === undefined ? DEFAULT_STRINGIFY_OPTIONS : {\n        ...DEFAULT_STRINGIFY_OPTIONS,\n        ...options\n    };\n    if (err instanceof Error) {\n        if (opts.verbose) {\n            return stringifyErrorLong(err);\n        } else {\n            return stringifyErrorShort(err);\n        }\n    }\n    if (typeof err === \"string\") {\n        return err;\n    }\n    return JSON.stringify(err);\n}\nfunction stringifyErrorShort(err) {\n    return `${err.name}: ${err.message}`;\n}\nfunction stringifyErrorLong(err) {\n    const cause = err.cause === undefined ? \"\" : `\\nCaused by ${stringify(err.cause, {\n        verbose: true\n    })}`;\n    if (!err.stack) {\n        return `${err.name}: ${err.message}${cause}`;\n    }\n    return `${err.stack}${cause}`;\n}\nasync function parseEntrypoint(entrypoint, root, diagnosticName = \"entrypoint\") {\n    let entrypointSpecifier;\n    try {\n        if (isURL(entrypoint)) {\n            entrypointSpecifier = new URL(entrypoint);\n        } else {\n            entrypointSpecifier = toFileUrl2(resolve2(root ?? Deno.cwd(), entrypoint));\n        }\n    } catch (err) {\n        throw `Failed to parse ${diagnosticName} specifier '${entrypoint}': ${stringify(err)}`;\n    }\n    if (entrypointSpecifier.protocol === \"file:\") {\n        try {\n            await Deno.lstat(entrypointSpecifier);\n        } catch (err) {\n            throw `Failed to open ${diagnosticName} file at '${entrypointSpecifier}': ${stringify(err)}`;\n        }\n    }\n    return entrypointSpecifier;\n}\nfunction isURL(entrypoint) {\n    return entrypoint.startsWith(\"https://\") || entrypoint.startsWith(\"http://\") || entrypoint.startsWith(\"file://\") || entrypoint.startsWith(\"data:\") || entrypoint.startsWith(\"jsr:\") || entrypoint.startsWith(\"npm:\");\n}\nfunction delay(ms, options = {}) {\n    const { signal, persistent } = options;\n    if (signal?.aborted) return Promise.reject(signal.reason);\n    return new Promise((resolve, reject)=>{\n        const abort = ()=>{\n            clearTimeout(i);\n            reject(signal?.reason);\n        };\n        const done = ()=>{\n            signal?.removeEventListener(\"abort\", abort);\n            resolve();\n        };\n        const i = setTimeout(done, ms);\n        signal?.addEventListener(\"abort\", abort, {\n            once: true\n        });\n        if (persistent === false) {\n            try {\n                Deno.unrefTimer(i);\n            } catch (error) {\n                if (!(error instanceof ReferenceError)) {\n                    throw error;\n                }\n                console.error(\"`persistent` option is only available in Deno\");\n            }\n        }\n    });\n}\nclass TextLineStream extends TransformStream {\n    #currentLine = \"\";\n    constructor(options = {\n        allowCR: false\n    }){\n        super({\n            transform: (chars, controller)=>{\n                chars = this.#currentLine + chars;\n                while(true){\n                    const lfIndex = chars.indexOf(\"\\n\");\n                    const crIndex = options.allowCR ? chars.indexOf(\"\\r\") : -1;\n                    if (crIndex !== -1 && crIndex !== chars.length - 1 && (lfIndex === -1 || lfIndex - 1 > crIndex)) {\n                        controller.enqueue(chars.slice(0, crIndex));\n                        chars = chars.slice(crIndex + 1);\n                        continue;\n                    }\n                    if (lfIndex === -1) break;\n                    const endIndex = chars[lfIndex - 1] === \"\\r\" ? lfIndex - 1 : lfIndex;\n                    controller.enqueue(chars.slice(0, endIndex));\n                    chars = chars.slice(lfIndex + 1);\n                }\n                this.#currentLine = chars;\n            },\n            flush: (controller)=>{\n                if (this.#currentLine === \"\") return;\n                const currentLine = options.allowCR && this.#currentLine.endsWith(\"\\r\") ? this.#currentLine.slice(0, -1) : this.#currentLine;\n                controller.enqueue(currentLine);\n            }\n        });\n    }\n}\nconst VERSION = \"1.13.0\";\nconst { Deno: Deno2 } = globalThis;\nconst noColor = typeof Deno2?.noColor === \"boolean\" ? Deno2.noColor : false;\nlet enabled = !noColor;\nfunction code(open, close) {\n    return {\n        open: `\\x1b[${open.join(\";\")}m`,\n        close: `\\x1b[${close}m`,\n        regexp: new RegExp(`\\\\x1b\\\\[${close}m`, \"g\")\n    };\n}\nfunction run(str, code) {\n    return enabled ? `${code.open}${str.replace(code.regexp, code.open)}${code.close}` : str;\n}\nfunction black(str) {\n    return run(str, code([\n        30\n    ], 39));\n}\nfunction red(str) {\n    return run(str, code([\n        31\n    ], 39));\n}\nfunction green(str) {\n    return run(str, code([\n        32\n    ], 39));\n}\nfunction yellow(str) {\n    return run(str, code([\n        33\n    ], 39));\n}\nfunction blue(str) {\n    return run(str, code([\n        34\n    ], 39));\n}\nfunction magenta(str) {\n    return run(str, code([\n        35\n    ], 39));\n}\nfunction cyan(str) {\n    return run(str, code([\n        36\n    ], 39));\n}\nfunction white(str) {\n    return run(str, code([\n        37\n    ], 39));\n}\nfunction gray(str) {\n    return brightBlack(str);\n}\nfunction brightBlack(str) {\n    return run(str, code([\n        90\n    ], 39));\n}\nnew RegExp([\n    \"[\\\\u001B\\\\u009B][[\\\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]+)*|[a-zA-Z\\\\d]+(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]*)*)?\\\\u0007)\",\n    \"(?:(?:\\\\d{1,4}(?:;\\\\d{0,4})*)?[\\\\dA-PR-TXZcf-nq-uy=><~]))\"\n].join(\"|\"), \"g\");\nconst encoder = new TextEncoder();\nfunction encode(input) {\n    return encoder.encode(input);\n}\nconst __default = [\n    [\n        0x0300,\n        0x036f\n    ],\n    [\n        0x0483,\n        0x0486\n    ],\n    [\n        0x0488,\n        0x0489\n    ],\n    [\n        0x0591,\n        0x05bd\n    ],\n    [\n        0x05bf,\n        0x05bf\n    ],\n    [\n        0x05c1,\n        0x05c2\n    ],\n    [\n        0x05c4,\n        0x05c5\n    ],\n    [\n        0x05c7,\n        0x05c7\n    ],\n    [\n        0x0600,\n        0x0603\n    ],\n    [\n        0x0610,\n        0x0615\n    ],\n    [\n        0x064b,\n        0x065e\n    ],\n    [\n        0x0670,\n        0x0670\n    ],\n    [\n        0x06d6,\n        0x06e4\n    ],\n    [\n        0x06e7,\n        0x06e8\n    ],\n    [\n        0x06ea,\n        0x06ed\n    ],\n    [\n        0x070f,\n        0x070f\n    ],\n    [\n        0x0711,\n        0x0711\n    ],\n    [\n        0x0730,\n        0x074a\n    ],\n    [\n        0x07a6,\n        0x07b0\n    ],\n    [\n        0x07eb,\n        0x07f3\n    ],\n    [\n        0x0901,\n        0x0902\n    ],\n    [\n        0x093c,\n        0x093c\n    ],\n    [\n        0x0941,\n        0x0948\n    ],\n    [\n        0x094d,\n        0x094d\n    ],\n    [\n        0x0951,\n        0x0954\n    ],\n    [\n        0x0962,\n        0x0963\n    ],\n    [\n        0x0981,\n        0x0981\n    ],\n    [\n        0x09bc,\n        0x09bc\n    ],\n    [\n        0x09c1,\n        0x09c4\n    ],\n    [\n        0x09cd,\n        0x09cd\n    ],\n    [\n        0x09e2,\n        0x09e3\n    ],\n    [\n        0x0a01,\n        0x0a02\n    ],\n    [\n        0x0a3c,\n        0x0a3c\n    ],\n    [\n        0x0a41,\n        0x0a42\n    ],\n    [\n        0x0a47,\n        0x0a48\n    ],\n    [\n        0x0a4b,\n        0x0a4d\n    ],\n    [\n        0x0a70,\n        0x0a71\n    ],\n    [\n        0x0a81,\n        0x0a82\n    ],\n    [\n        0x0abc,\n        0x0abc\n    ],\n    [\n        0x0ac1,\n        0x0ac5\n    ],\n    [\n        0x0ac7,\n        0x0ac8\n    ],\n    [\n        0x0acd,\n        0x0acd\n    ],\n    [\n        0x0ae2,\n        0x0ae3\n    ],\n    [\n        0x0b01,\n        0x0b01\n    ],\n    [\n        0x0b3c,\n        0x0b3c\n    ],\n    [\n        0x0b3f,\n        0x0b3f\n    ],\n    [\n        0x0b41,\n        0x0b43\n    ],\n    [\n        0x0b4d,\n        0x0b4d\n    ],\n    [\n        0x0b56,\n        0x0b56\n    ],\n    [\n        0x0b82,\n        0x0b82\n    ],\n    [\n        0x0bc0,\n        0x0bc0\n    ],\n    [\n        0x0bcd,\n        0x0bcd\n    ],\n    [\n        0x0c3e,\n        0x0c40\n    ],\n    [\n        0x0c46,\n        0x0c48\n    ],\n    [\n        0x0c4a,\n        0x0c4d\n    ],\n    [\n        0x0c55,\n        0x0c56\n    ],\n    [\n        0x0cbc,\n        0x0cbc\n    ],\n    [\n        0x0cbf,\n        0x0cbf\n    ],\n    [\n        0x0cc6,\n        0x0cc6\n    ],\n    [\n        0x0ccc,\n        0x0ccd\n    ],\n    [\n        0x0ce2,\n        0x0ce3\n    ],\n    [\n        0x0d41,\n        0x0d43\n    ],\n    [\n        0x0d4d,\n        0x0d4d\n    ],\n    [\n        0x0dca,\n        0x0dca\n    ],\n    [\n        0x0dd2,\n        0x0dd4\n    ],\n    [\n        0x0dd6,\n        0x0dd6\n    ],\n    [\n        0x0e31,\n        0x0e31\n    ],\n    [\n        0x0e34,\n        0x0e3a\n    ],\n    [\n        0x0e47,\n        0x0e4e\n    ],\n    [\n        0x0eb1,\n        0x0eb1\n    ],\n    [\n        0x0eb4,\n        0x0eb9\n    ],\n    [\n        0x0ebb,\n        0x0ebc\n    ],\n    [\n        0x0ec8,\n        0x0ecd\n    ],\n    [\n        0x0f18,\n        0x0f19\n    ],\n    [\n        0x0f35,\n        0x0f35\n    ],\n    [\n        0x0f37,\n        0x0f37\n    ],\n    [\n        0x0f39,\n        0x0f39\n    ],\n    [\n        0x0f71,\n        0x0f7e\n    ],\n    [\n        0x0f80,\n        0x0f84\n    ],\n    [\n        0x0f86,\n        0x0f87\n    ],\n    [\n        0x0f90,\n        0x0f97\n    ],\n    [\n        0x0f99,\n        0x0fbc\n    ],\n    [\n        0x0fc6,\n        0x0fc6\n    ],\n    [\n        0x102d,\n        0x1030\n    ],\n    [\n        0x1032,\n        0x1032\n    ],\n    [\n        0x1036,\n        0x1037\n    ],\n    [\n        0x1039,\n        0x1039\n    ],\n    [\n        0x1058,\n        0x1059\n    ],\n    [\n        0x1160,\n        0x11ff\n    ],\n    [\n        0x135f,\n        0x135f\n    ],\n    [\n        0x1712,\n        0x1714\n    ],\n    [\n        0x1732,\n        0x1734\n    ],\n    [\n        0x1752,\n        0x1753\n    ],\n    [\n        0x1772,\n        0x1773\n    ],\n    [\n        0x17b4,\n        0x17b5\n    ],\n    [\n        0x17b7,\n        0x17bd\n    ],\n    [\n        0x17c6,\n        0x17c6\n    ],\n    [\n        0x17c9,\n        0x17d3\n    ],\n    [\n        0x17dd,\n        0x17dd\n    ],\n    [\n        0x180b,\n        0x180d\n    ],\n    [\n        0x18a9,\n        0x18a9\n    ],\n    [\n        0x1920,\n        0x1922\n    ],\n    [\n        0x1927,\n        0x1928\n    ],\n    [\n        0x1932,\n        0x1932\n    ],\n    [\n        0x1939,\n        0x193b\n    ],\n    [\n        0x1a17,\n        0x1a18\n    ],\n    [\n        0x1b00,\n        0x1b03\n    ],\n    [\n        0x1b34,\n        0x1b34\n    ],\n    [\n        0x1b36,\n        0x1b3a\n    ],\n    [\n        0x1b3c,\n        0x1b3c\n    ],\n    [\n        0x1b42,\n        0x1b42\n    ],\n    [\n        0x1b6b,\n        0x1b73\n    ],\n    [\n        0x1dc0,\n        0x1dca\n    ],\n    [\n        0x1dfe,\n        0x1dff\n    ],\n    [\n        0x200b,\n        0x200f\n    ],\n    [\n        0x202a,\n        0x202e\n    ],\n    [\n        0x2060,\n        0x2063\n    ],\n    [\n        0x206a,\n        0x206f\n    ],\n    [\n        0x20d0,\n        0x20ef\n    ],\n    [\n        0x302a,\n        0x302f\n    ],\n    [\n        0x3099,\n        0x309a\n    ],\n    [\n        0xa806,\n        0xa806\n    ],\n    [\n        0xa80b,\n        0xa80b\n    ],\n    [\n        0xa825,\n        0xa826\n    ],\n    [\n        0xfb1e,\n        0xfb1e\n    ],\n    [\n        0xfe00,\n        0xfe0f\n    ],\n    [\n        0xfe20,\n        0xfe23\n    ],\n    [\n        0xfeff,\n        0xfeff\n    ],\n    [\n        0xfff9,\n        0xfffb\n    ],\n    [\n        0x10a01,\n        0x10a03\n    ],\n    [\n        0x10a05,\n        0x10a06\n    ],\n    [\n        0x10a0c,\n        0x10a0f\n    ],\n    [\n        0x10a38,\n        0x10a3a\n    ],\n    [\n        0x10a3f,\n        0x10a3f\n    ],\n    [\n        0x1d167,\n        0x1d169\n    ],\n    [\n        0x1d173,\n        0x1d182\n    ],\n    [\n        0x1d185,\n        0x1d18b\n    ],\n    [\n        0x1d1aa,\n        0x1d1ad\n    ],\n    [\n        0x1d242,\n        0x1d244\n    ],\n    [\n        0xe0001,\n        0xe0001\n    ],\n    [\n        0xe0020,\n        0xe007f\n    ],\n    [\n        0xe0100,\n        0xe01ef\n    ]\n];\nfunction wcswidth(str, { nul = 0, control = 0 } = {}) {\n    const opts = {\n        nul,\n        control\n    };\n    if (typeof str !== \"string\") return wcwidth(str, opts);\n    let s = 0;\n    for(let i = 0; i < str.length; i++){\n        const n = wcwidth(str.charCodeAt(i), opts);\n        if (n < 0) return -1;\n        s += n;\n    }\n    return s;\n}\nfunction wcwidth(ucs, { nul = 0, control = 0 } = {}) {\n    if (ucs === 0) return nul;\n    if (ucs < 32 || ucs >= 0x7f && ucs < 0xa0) return control;\n    if (bisearch(ucs)) return 0;\n    return 1 + (ucs >= 0x1100 && (ucs <= 0x115f || ucs == 0x2329 || ucs == 0x232a || ucs >= 0x2e80 && ucs <= 0xa4cf && ucs != 0x303f || ucs >= 0xac00 && ucs <= 0xd7a3 || ucs >= 0xf900 && ucs <= 0xfaff || ucs >= 0xfe10 && ucs <= 0xfe19 || ucs >= 0xfe30 && ucs <= 0xfe6f || ucs >= 0xff00 && ucs <= 0xff60 || ucs >= 0xffe0 && ucs <= 0xffe6 || ucs >= 0x20000 && ucs <= 0x2fffd || ucs >= 0x30000 && ucs <= 0x3fffd) ? 1 : 0);\n}\nfunction bisearch(ucs) {\n    let min = 0;\n    let max = __default.length - 1;\n    let mid;\n    if (ucs < __default[0][0] || ucs > __default[max][1]) return false;\n    while(max >= min){\n        mid = Math.floor((min + max) / 2);\n        if (ucs > __default[mid][1]) min = mid + 1;\n        else if (ucs < __default[mid][0]) max = mid - 1;\n        else return true;\n    }\n    return false;\n}\nfunction ansiRegex({ onlyFirst = false } = {}) {\n    const pattern = [\n        \"[\\\\u001B\\\\u009B][[\\\\]()#;?]*(?:(?:(?:[a-zA-Z\\\\d]*(?:;[-a-zA-Z\\\\d\\\\/#&.:=?%@~_]*)*)?\\\\u0007)\",\n        \"(?:(?:\\\\d{1,4}(?:;\\\\d{0,4})*)?[\\\\dA-PR-TZcf-ntqry=><~]))\"\n    ].join(\"|\");\n    return new RegExp(pattern, onlyFirst ? undefined : \"g\");\n}\nfunction isInteractive(stream) {\n    return stream.isTerminal();\n}\n(await Deno.permissions.query({\n    name: \"env\"\n})).state === \"granted\" ? Deno.env.get(\"TERM_PROGRAM\") === \"Apple_Terminal\" : false;\nfunction writeSync(str, writer) {\n    writer.writeSync(encode(str));\n}\nfunction stripAnsi(dirty) {\n    return dirty.replace(ansiRegex(), \"\");\n}\nconst ESC = \"\\u001B[\";\nconst HIDE = \"?25l\";\nconst SHOW = \"?25h\";\nconst UP = \"A\";\nconst RIGHT = \"C\";\nconst CLEAR_LINE = \"2K\";\nfunction cursorSync(action, writer = Deno.stdout) {\n    writeSync(ESC + action, writer);\n}\nfunction hideCursorSync(writer = Deno.stdout) {\n    cursorSync(HIDE, writer);\n}\nfunction showCursorSync(writer = Deno.stdout) {\n    cursorSync(SHOW, writer);\n}\nfunction clearLineSync(writer = Deno.stdout) {\n    cursorSync(CLEAR_LINE, writer);\n}\nfunction goUpSync(y = 1, writer = Deno.stdout) {\n    cursorSync(y + UP, writer);\n}\nfunction goRightSync(x = 1, writer = Deno.stdout) {\n    cursorSync(`${x}${RIGHT}`, writer);\n}\nconst __default1 = {\n    dots: {\n        interval: 80,\n        frames: [\n            \"⠋\",\n            \"⠙\",\n            \"⠹\",\n            \"⠸\",\n            \"⠼\",\n            \"⠴\",\n            \"⠦\",\n            \"⠧\",\n            \"⠇\",\n            \"⠏\"\n        ]\n    },\n    dots2: {\n        interval: 80,\n        frames: [\n            \"⣾\",\n            \"⣽\",\n            \"⣻\",\n            \"⢿\",\n            \"⡿\",\n            \"⣟\",\n            \"⣯\",\n            \"⣷\"\n        ]\n    },\n    dots3: {\n        interval: 80,\n        frames: [\n            \"⠋\",\n            \"⠙\",\n            \"⠚\",\n            \"⠞\",\n            \"⠖\",\n            \"⠦\",\n            \"⠴\",\n            \"⠲\",\n            \"⠳\",\n            \"⠓\"\n        ]\n    },\n    dots4: {\n        interval: 80,\n        frames: [\n            \"⠄\",\n            \"⠆\",\n            \"⠇\",\n            \"⠋\",\n            \"⠙\",\n            \"⠸\",\n            \"⠰\",\n            \"⠠\",\n            \"⠰\",\n            \"⠸\",\n            \"⠙\",\n            \"⠋\",\n            \"⠇\",\n            \"⠆\"\n        ]\n    },\n    dots5: {\n        interval: 80,\n        frames: [\n            \"⠋\",\n            \"⠙\",\n            \"⠚\",\n            \"⠒\",\n            \"⠂\",\n            \"⠂\",\n            \"⠒\",\n            \"⠲\",\n            \"⠴\",\n            \"⠦\",\n            \"⠖\",\n            \"⠒\",\n            \"⠐\",\n            \"⠐\",\n            \"⠒\",\n            \"⠓\",\n            \"⠋\"\n        ]\n    },\n    dots6: {\n        interval: 80,\n        frames: [\n            \"⠁\",\n            \"⠉\",\n            \"⠙\",\n            \"⠚\",\n            \"⠒\",\n            \"⠂\",\n            \"⠂\",\n            \"⠒\",\n            \"⠲\",\n            \"⠴\",\n            \"⠤\",\n            \"⠄\",\n            \"⠄\",\n            \"⠤\",\n            \"⠴\",\n            \"⠲\",\n            \"⠒\",\n            \"⠂\",\n            \"⠂\",\n            \"⠒\",\n            \"⠚\",\n            \"⠙\",\n            \"⠉\",\n            \"⠁\"\n        ]\n    },\n    dots7: {\n        interval: 80,\n        frames: [\n            \"⠈\",\n            \"⠉\",\n            \"⠋\",\n            \"⠓\",\n            \"⠒\",\n            \"⠐\",\n            \"⠐\",\n            \"⠒\",\n            \"⠖\",\n            \"⠦\",\n            \"⠤\",\n            \"⠠\",\n            \"⠠\",\n            \"⠤\",\n            \"⠦\",\n            \"⠖\",\n            \"⠒\",\n            \"⠐\",\n            \"⠐\",\n            \"⠒\",\n            \"⠓\",\n            \"⠋\",\n            \"⠉\",\n            \"⠈\"\n        ]\n    },\n    dots8: {\n        interval: 80,\n        frames: [\n            \"⠁\",\n            \"⠁\",\n            \"⠉\",\n            \"⠙\",\n            \"⠚\",\n            \"⠒\",\n            \"⠂\",\n            \"⠂\",\n            \"⠒\",\n            \"⠲\",\n            \"⠴\",\n            \"⠤\",\n            \"⠄\",\n            \"⠄\",\n            \"⠤\",\n            \"⠠\",\n            \"⠠\",\n            \"⠤\",\n            \"⠦\",\n            \"⠖\",\n            \"⠒\",\n            \"⠐\",\n            \"⠐\",\n            \"⠒\",\n            \"⠓\",\n            \"⠋\",\n            \"⠉\",\n            \"⠈\",\n            \"⠈\"\n        ]\n    },\n    dots9: {\n        interval: 80,\n        frames: [\n            \"⢹\",\n            \"⢺\",\n            \"⢼\",\n            \"⣸\",\n            \"⣇\",\n            \"⡧\",\n            \"⡗\",\n            \"⡏\"\n        ]\n    },\n    dots10: {\n        interval: 80,\n        frames: [\n            \"⢄\",\n            \"⢂\",\n            \"⢁\",\n            \"⡁\",\n            \"⡈\",\n            \"⡐\",\n            \"⡠\"\n        ]\n    },\n    dots11: {\n        interval: 100,\n        frames: [\n            \"⠁\",\n            \"⠂\",\n            \"⠄\",\n            \"⡀\",\n            \"⢀\",\n            \"⠠\",\n            \"⠐\",\n            \"⠈\"\n        ]\n    },\n    dots12: {\n        interval: 80,\n        frames: [\n            \"⢀⠀\",\n            \"⡀⠀\",\n            \"⠄⠀\",\n            \"⢂⠀\",\n            \"⡂⠀\",\n            \"⠅⠀\",\n            \"⢃⠀\",\n            \"⡃⠀\",\n            \"⠍⠀\",\n            \"⢋⠀\",\n            \"⡋⠀\",\n            \"⠍⠁\",\n            \"⢋⠁\",\n            \"⡋⠁\",\n            \"⠍⠉\",\n            \"⠋⠉\",\n            \"⠋⠉\",\n            \"⠉⠙\",\n            \"⠉⠙\",\n            \"⠉⠩\",\n            \"⠈⢙\",\n            \"⠈⡙\",\n            \"⢈⠩\",\n            \"⡀⢙\",\n            \"⠄⡙\",\n            \"⢂⠩\",\n            \"⡂⢘\",\n            \"⠅⡘\",\n            \"⢃⠨\",\n            \"⡃⢐\",\n            \"⠍⡐\",\n            \"⢋⠠\",\n            \"⡋⢀\",\n            \"⠍⡁\",\n            \"⢋⠁\",\n            \"⡋⠁\",\n            \"⠍⠉\",\n            \"⠋⠉\",\n            \"⠋⠉\",\n            \"⠉⠙\",\n            \"⠉⠙\",\n            \"⠉⠩\",\n            \"⠈⢙\",\n            \"⠈⡙\",\n            \"⠈⠩\",\n            \"⠀⢙\",\n            \"⠀⡙\",\n            \"⠀⠩\",\n            \"⠀⢘\",\n            \"⠀⡘\",\n            \"⠀⠨\",\n            \"⠀⢐\",\n            \"⠀⡐\",\n            \"⠀⠠\",\n            \"⠀⢀\",\n            \"⠀⡀\"\n        ]\n    },\n    dots8Bit: {\n        interval: 80,\n        frames: [\n            \"⠀\",\n            \"⠁\",\n            \"⠂\",\n            \"⠃\",\n            \"⠄\",\n            \"⠅\",\n            \"⠆\",\n            \"⠇\",\n            \"⡀\",\n            \"⡁\",\n            \"⡂\",\n            \"⡃\",\n            \"⡄\",\n            \"⡅\",\n            \"⡆\",\n            \"⡇\",\n            \"⠈\",\n            \"⠉\",\n            \"⠊\",\n            \"⠋\",\n            \"⠌\",\n            \"⠍\",\n            \"⠎\",\n            \"⠏\",\n            \"⡈\",\n            \"⡉\",\n            \"⡊\",\n            \"⡋\",\n            \"⡌\",\n            \"⡍\",\n            \"⡎\",\n            \"⡏\",\n            \"⠐\",\n            \"⠑\",\n            \"⠒\",\n            \"⠓\",\n            \"⠔\",\n            \"⠕\",\n            \"⠖\",\n            \"⠗\",\n            \"⡐\",\n            \"⡑\",\n            \"⡒\",\n            \"⡓\",\n            \"⡔\",\n            \"⡕\",\n            \"⡖\",\n            \"⡗\",\n            \"⠘\",\n            \"⠙\",\n            \"⠚\",\n            \"⠛\",\n            \"⠜\",\n            \"⠝\",\n            \"⠞\",\n            \"⠟\",\n            \"⡘\",\n            \"⡙\",\n            \"⡚\",\n            \"⡛\",\n            \"⡜\",\n            \"⡝\",\n            \"⡞\",\n            \"⡟\",\n            \"⠠\",\n            \"⠡\",\n            \"⠢\",\n            \"⠣\",\n            \"⠤\",\n            \"⠥\",\n            \"⠦\",\n            \"⠧\",\n            \"⡠\",\n            \"⡡\",\n            \"⡢\",\n            \"⡣\",\n            \"⡤\",\n            \"⡥\",\n            \"⡦\",\n            \"⡧\",\n            \"⠨\",\n            \"⠩\",\n            \"⠪\",\n            \"⠫\",\n            \"⠬\",\n            \"⠭\",\n            \"⠮\",\n            \"⠯\",\n            \"⡨\",\n            \"⡩\",\n            \"⡪\",\n            \"⡫\",\n            \"⡬\",\n            \"⡭\",\n            \"⡮\",\n            \"⡯\",\n            \"⠰\",\n            \"⠱\",\n            \"⠲\",\n            \"⠳\",\n            \"⠴\",\n            \"⠵\",\n            \"⠶\",\n            \"⠷\",\n            \"⡰\",\n            \"⡱\",\n            \"⡲\",\n            \"⡳\",\n            \"⡴\",\n            \"⡵\",\n            \"⡶\",\n            \"⡷\",\n            \"⠸\",\n            \"⠹\",\n            \"⠺\",\n            \"⠻\",\n            \"⠼\",\n            \"⠽\",\n            \"⠾\",\n            \"⠿\",\n            \"⡸\",\n            \"⡹\",\n            \"⡺\",\n            \"⡻\",\n            \"⡼\",\n            \"⡽\",\n            \"⡾\",\n            \"⡿\",\n            \"⢀\",\n            \"⢁\",\n            \"⢂\",\n            \"⢃\",\n            \"⢄\",\n            \"⢅\",\n            \"⢆\",\n            \"⢇\",\n            \"⣀\",\n            \"⣁\",\n            \"⣂\",\n            \"⣃\",\n            \"⣄\",\n            \"⣅\",\n            \"⣆\",\n            \"⣇\",\n            \"⢈\",\n            \"⢉\",\n            \"⢊\",\n            \"⢋\",\n            \"⢌\",\n            \"⢍\",\n            \"⢎\",\n            \"⢏\",\n            \"⣈\",\n            \"⣉\",\n            \"⣊\",\n            \"⣋\",\n            \"⣌\",\n            \"⣍\",\n            \"⣎\",\n            \"⣏\",\n            \"⢐\",\n            \"⢑\",\n            \"⢒\",\n            \"⢓\",\n            \"⢔\",\n            \"⢕\",\n            \"⢖\",\n            \"⢗\",\n            \"⣐\",\n            \"⣑\",\n            \"⣒\",\n            \"⣓\",\n            \"⣔\",\n            \"⣕\",\n            \"⣖\",\n            \"⣗\",\n            \"⢘\",\n            \"⢙\",\n            \"⢚\",\n            \"⢛\",\n            \"⢜\",\n            \"⢝\",\n            \"⢞\",\n            \"⢟\",\n            \"⣘\",\n            \"⣙\",\n            \"⣚\",\n            \"⣛\",\n            \"⣜\",\n            \"⣝\",\n            \"⣞\",\n            \"⣟\",\n            \"⢠\",\n            \"⢡\",\n            \"⢢\",\n            \"⢣\",\n            \"⢤\",\n            \"⢥\",\n            \"⢦\",\n            \"⢧\",\n            \"⣠\",\n            \"⣡\",\n            \"⣢\",\n            \"⣣\",\n            \"⣤\",\n            \"⣥\",\n            \"⣦\",\n            \"⣧\",\n            \"⢨\",\n            \"⢩\",\n            \"⢪\",\n            \"⢫\",\n            \"⢬\",\n            \"⢭\",\n            \"⢮\",\n            \"⢯\",\n            \"⣨\",\n            \"⣩\",\n            \"⣪\",\n            \"⣫\",\n            \"⣬\",\n            \"⣭\",\n            \"⣮\",\n            \"⣯\",\n            \"⢰\",\n            \"⢱\",\n            \"⢲\",\n            \"⢳\",\n            \"⢴\",\n            \"⢵\",\n            \"⢶\",\n            \"⢷\",\n            \"⣰\",\n            \"⣱\",\n            \"⣲\",\n            \"⣳\",\n            \"⣴\",\n            \"⣵\",\n            \"⣶\",\n            \"⣷\",\n            \"⢸\",\n            \"⢹\",\n            \"⢺\",\n            \"⢻\",\n            \"⢼\",\n            \"⢽\",\n            \"⢾\",\n            \"⢿\",\n            \"⣸\",\n            \"⣹\",\n            \"⣺\",\n            \"⣻\",\n            \"⣼\",\n            \"⣽\",\n            \"⣾\",\n            \"⣿\"\n        ]\n    },\n    line: {\n        interval: 130,\n        frames: [\n            \"-\",\n            \"\\\\\",\n            \"|\",\n            \"/\"\n        ]\n    },\n    line2: {\n        interval: 100,\n        frames: [\n            \"⠂\",\n            \"-\",\n            \"–\",\n            \"—\",\n            \"–\",\n            \"-\"\n        ]\n    },\n    pipe: {\n        interval: 100,\n        frames: [\n            \"┤\",\n            \"┘\",\n            \"┴\",\n            \"└\",\n            \"├\",\n            \"┌\",\n            \"┬\",\n            \"┐\"\n        ]\n    },\n    simpleDots: {\n        interval: 400,\n        frames: [\n            \".  \",\n            \".. \",\n            \"...\",\n            \"   \"\n        ]\n    },\n    simpleDotsScrolling: {\n        interval: 200,\n        frames: [\n            \".  \",\n            \".. \",\n            \"...\",\n            \" ..\",\n            \"  .\",\n            \"   \"\n        ]\n    },\n    star: {\n        interval: 70,\n        frames: [\n            \"✶\",\n            \"✸\",\n            \"✹\",\n            \"✺\",\n            \"✹\",\n            \"✷\"\n        ]\n    },\n    star2: {\n        interval: 80,\n        frames: [\n            \"+\",\n            \"x\",\n            \"*\"\n        ]\n    },\n    flip: {\n        interval: 70,\n        frames: [\n            \"_\",\n            \"_\",\n            \"_\",\n            \"-\",\n            \"`\",\n            \"`\",\n            \"'\",\n            \"´\",\n            \"-\",\n            \"_\",\n            \"_\",\n            \"_\"\n        ]\n    },\n    hamburger: {\n        interval: 100,\n        frames: [\n            \"☱\",\n            \"☲\",\n            \"☴\"\n        ]\n    },\n    growVertical: {\n        interval: 120,\n        frames: [\n            \"▁\",\n            \"▃\",\n            \"▄\",\n            \"▅\",\n            \"▆\",\n            \"▇\",\n            \"▆\",\n            \"▅\",\n            \"▄\",\n            \"▃\"\n        ]\n    },\n    growHorizontal: {\n        interval: 120,\n        frames: [\n            \"▏\",\n            \"▎\",\n            \"▍\",\n            \"▌\",\n            \"▋\",\n            \"▊\",\n            \"▉\",\n            \"▊\",\n            \"▋\",\n            \"▌\",\n            \"▍\",\n            \"▎\"\n        ]\n    },\n    balloon: {\n        interval: 140,\n        frames: [\n            \" \",\n            \".\",\n            \"o\",\n            \"O\",\n            \"@\",\n            \"*\",\n            \" \"\n        ]\n    },\n    balloon2: {\n        interval: 120,\n        frames: [\n            \".\",\n            \"o\",\n            \"O\",\n            \"°\",\n            \"O\",\n            \"o\",\n            \".\"\n        ]\n    },\n    noise: {\n        interval: 100,\n        frames: [\n            \"▓\",\n            \"▒\",\n            \"░\"\n        ]\n    },\n    bounce: {\n        interval: 120,\n        frames: [\n            \"⠁\",\n            \"⠂\",\n            \"⠄\",\n            \"⠂\"\n        ]\n    },\n    boxBounce: {\n        interval: 120,\n        frames: [\n            \"▖\",\n            \"▘\",\n            \"▝\",\n            \"▗\"\n        ]\n    },\n    boxBounce2: {\n        interval: 100,\n        frames: [\n            \"▌\",\n            \"▀\",\n            \"▐\",\n            \"▄\"\n        ]\n    },\n    triangle: {\n        interval: 50,\n        frames: [\n            \"◢\",\n            \"◣\",\n            \"◤\",\n            \"◥\"\n        ]\n    },\n    arc: {\n        interval: 100,\n        frames: [\n            \"◜\",\n            \"◠\",\n            \"◝\",\n            \"◞\",\n            \"◡\",\n            \"◟\"\n        ]\n    },\n    circle: {\n        interval: 120,\n        frames: [\n            \"◡\",\n            \"⊙\",\n            \"◠\"\n        ]\n    },\n    squareCorners: {\n        interval: 180,\n        frames: [\n            \"◰\",\n            \"◳\",\n            \"◲\",\n            \"◱\"\n        ]\n    },\n    circleQuarters: {\n        interval: 120,\n        frames: [\n            \"◴\",\n            \"◷\",\n            \"◶\",\n            \"◵\"\n        ]\n    },\n    circleHalves: {\n        interval: 50,\n        frames: [\n            \"◐\",\n            \"◓\",\n            \"◑\",\n            \"◒\"\n        ]\n    },\n    squish: {\n        interval: 100,\n        frames: [\n            \"╫\",\n            \"╪\"\n        ]\n    },\n    toggle: {\n        interval: 250,\n        frames: [\n            \"⊶\",\n            \"⊷\"\n        ]\n    },\n    toggle2: {\n        interval: 80,\n        frames: [\n            \"▫\",\n            \"▪\"\n        ]\n    },\n    toggle3: {\n        interval: 120,\n        frames: [\n            \"□\",\n            \"■\"\n        ]\n    },\n    toggle4: {\n        interval: 100,\n        frames: [\n            \"■\",\n            \"□\",\n            \"▪\",\n            \"▫\"\n        ]\n    },\n    toggle5: {\n        interval: 100,\n        frames: [\n            \"▮\",\n            \"▯\"\n        ]\n    },\n    toggle6: {\n        interval: 300,\n        frames: [\n            \"ဝ\",\n            \"၀\"\n        ]\n    },\n    toggle7: {\n        interval: 80,\n        frames: [\n            \"⦾\",\n            \"⦿\"\n        ]\n    },\n    toggle8: {\n        interval: 100,\n        frames: [\n            \"◍\",\n            \"◌\"\n        ]\n    },\n    toggle9: {\n        interval: 100,\n        frames: [\n            \"◉\",\n            \"◎\"\n        ]\n    },\n    toggle10: {\n        interval: 100,\n        frames: [\n            \"㊂\",\n            \"㊀\",\n            \"㊁\"\n        ]\n    },\n    toggle11: {\n        interval: 50,\n        frames: [\n            \"⧇\",\n            \"⧆\"\n        ]\n    },\n    toggle12: {\n        interval: 120,\n        frames: [\n            \"☗\",\n            \"☖\"\n        ]\n    },\n    toggle13: {\n        interval: 80,\n        frames: [\n            \"=\",\n            \"*\",\n            \"-\"\n        ]\n    },\n    arrow: {\n        interval: 100,\n        frames: [\n            \"←\",\n            \"↖\",\n            \"↑\",\n            \"↗\",\n            \"→\",\n            \"↘\",\n            \"↓\",\n            \"↙\"\n        ]\n    },\n    arrow2: {\n        interval: 80,\n        frames: [\n            \"⬆️ \",\n            \"↗️ \",\n            \"➡️ \",\n            \"↘️ \",\n            \"⬇️ \",\n            \"↙️ \",\n            \"⬅️ \",\n            \"↖️ \"\n        ]\n    },\n    arrow3: {\n        interval: 120,\n        frames: [\n            \"▹▹▹▹▹\",\n            \"▸▹▹▹▹\",\n            \"▹▸▹▹▹\",\n            \"▹▹▸▹▹\",\n            \"▹▹▹▸▹\",\n            \"▹▹▹▹▸\"\n        ]\n    },\n    bouncingBar: {\n        interval: 80,\n        frames: [\n            \"[    ]\",\n            \"[=   ]\",\n            \"[==  ]\",\n            \"[=== ]\",\n            \"[ ===]\",\n            \"[  ==]\",\n            \"[   =]\",\n            \"[    ]\",\n            \"[   =]\",\n            \"[  ==]\",\n            \"[ ===]\",\n            \"[====]\",\n            \"[=== ]\",\n            \"[==  ]\",\n            \"[=   ]\"\n        ]\n    },\n    bouncingBall: {\n        interval: 80,\n        frames: [\n            \"( ●    )\",\n            \"(  ●   )\",\n            \"(   ●  )\",\n            \"(    ● )\",\n            \"(     ●)\",\n            \"(    ● )\",\n            \"(   ●  )\",\n            \"(  ●   )\",\n            \"( ●    )\",\n            \"(●     )\"\n        ]\n    },\n    smiley: {\n        interval: 200,\n        frames: [\n            \"😄 \",\n            \"😝 \"\n        ]\n    },\n    monkey: {\n        interval: 300,\n        frames: [\n            \"🙈 \",\n            \"🙈 \",\n            \"🙉 \",\n            \"🙊 \"\n        ]\n    },\n    hearts: {\n        interval: 100,\n        frames: [\n            \"💛 \",\n            \"💙 \",\n            \"💜 \",\n            \"💚 \",\n            \"❤️ \"\n        ]\n    },\n    clock: {\n        interval: 100,\n        frames: [\n            \"🕛 \",\n            \"🕐 \",\n            \"🕑 \",\n            \"🕒 \",\n            \"🕓 \",\n            \"🕔 \",\n            \"🕕 \",\n            \"🕖 \",\n            \"🕗 \",\n            \"🕘 \",\n            \"🕙 \",\n            \"🕚 \"\n        ]\n    },\n    earth: {\n        interval: 180,\n        frames: [\n            \"🌍 \",\n            \"🌎 \",\n            \"🌏 \"\n        ]\n    },\n    material: {\n        interval: 17,\n        frames: [\n            \"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"███████▁▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"█████████▁▁▁▁▁▁▁▁▁▁▁\",\n            \"█████████▁▁▁▁▁▁▁▁▁▁▁\",\n            \"██████████▁▁▁▁▁▁▁▁▁▁\",\n            \"███████████▁▁▁▁▁▁▁▁▁\",\n            \"█████████████▁▁▁▁▁▁▁\",\n            \"██████████████▁▁▁▁▁▁\",\n            \"██████████████▁▁▁▁▁▁\",\n            \"▁██████████████▁▁▁▁▁\",\n            \"▁██████████████▁▁▁▁▁\",\n            \"▁██████████████▁▁▁▁▁\",\n            \"▁▁██████████████▁▁▁▁\",\n            \"▁▁▁██████████████▁▁▁\",\n            \"▁▁▁▁█████████████▁▁▁\",\n            \"▁▁▁▁██████████████▁▁\",\n            \"▁▁▁▁██████████████▁▁\",\n            \"▁▁▁▁▁██████████████▁\",\n            \"▁▁▁▁▁██████████████▁\",\n            \"▁▁▁▁▁██████████████▁\",\n            \"▁▁▁▁▁▁██████████████\",\n            \"▁▁▁▁▁▁██████████████\",\n            \"▁▁▁▁▁▁▁█████████████\",\n            \"▁▁▁▁▁▁▁█████████████\",\n            \"▁▁▁▁▁▁▁▁████████████\",\n            \"▁▁▁▁▁▁▁▁████████████\",\n            \"▁▁▁▁▁▁▁▁▁███████████\",\n            \"▁▁▁▁▁▁▁▁▁███████████\",\n            \"▁▁▁▁▁▁▁▁▁▁██████████\",\n            \"▁▁▁▁▁▁▁▁▁▁██████████\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁████████\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\n            \"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\n            \"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\n            \"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\n            \"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\n            \"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\n            \"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\n            \"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\n            \"██████▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\n            \"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"█████████▁▁▁▁▁▁▁▁▁▁▁\",\n            \"█████████▁▁▁▁▁▁▁▁▁▁▁\",\n            \"█████████▁▁▁▁▁▁▁▁▁▁▁\",\n            \"█████████▁▁▁▁▁▁▁▁▁▁▁\",\n            \"███████████▁▁▁▁▁▁▁▁▁\",\n            \"████████████▁▁▁▁▁▁▁▁\",\n            \"████████████▁▁▁▁▁▁▁▁\",\n            \"██████████████▁▁▁▁▁▁\",\n            \"██████████████▁▁▁▁▁▁\",\n            \"▁██████████████▁▁▁▁▁\",\n            \"▁██████████████▁▁▁▁▁\",\n            \"▁▁▁█████████████▁▁▁▁\",\n            \"▁▁▁▁▁████████████▁▁▁\",\n            \"▁▁▁▁▁████████████▁▁▁\",\n            \"▁▁▁▁▁▁███████████▁▁▁\",\n            \"▁▁▁▁▁▁▁▁█████████▁▁▁\",\n            \"▁▁▁▁▁▁▁▁█████████▁▁▁\",\n            \"▁▁▁▁▁▁▁▁▁█████████▁▁\",\n            \"▁▁▁▁▁▁▁▁▁█████████▁▁\",\n            \"▁▁▁▁▁▁▁▁▁▁█████████▁\",\n            \"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\n            \"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\n            \"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\"\n        ]\n    },\n    moon: {\n        interval: 80,\n        frames: [\n            \"🌑 \",\n            \"🌒 \",\n            \"🌓 \",\n            \"🌔 \",\n            \"🌕 \",\n            \"🌖 \",\n            \"🌗 \",\n            \"🌘 \"\n        ]\n    },\n    runner: {\n        interval: 140,\n        frames: [\n            \"🚶 \",\n            \"🏃 \"\n        ]\n    },\n    pong: {\n        interval: 80,\n        frames: [\n            \"▐⠂       ▌\",\n            \"▐⠈       ▌\",\n            \"▐ ⠂      ▌\",\n            \"▐ ⠠      ▌\",\n            \"▐  ⡀     ▌\",\n            \"▐  ⠠     ▌\",\n            \"▐   ⠂    ▌\",\n            \"▐   ⠈    ▌\",\n            \"▐    ⠂   ▌\",\n            \"▐    ⠠   ▌\",\n            \"▐     ⡀  ▌\",\n            \"▐     ⠠  ▌\",\n            \"▐      ⠂ ▌\",\n            \"▐      ⠈ ▌\",\n            \"▐       ⠂▌\",\n            \"▐       ⠠▌\",\n            \"▐       ⡀▌\",\n            \"▐      ⠠ ▌\",\n            \"▐      ⠂ ▌\",\n            \"▐     ⠈  ▌\",\n            \"▐     ⠂  ▌\",\n            \"▐    ⠠   ▌\",\n            \"▐    ⡀   ▌\",\n            \"▐   ⠠    ▌\",\n            \"▐   ⠂    ▌\",\n            \"▐  ⠈     ▌\",\n            \"▐  ⠂     ▌\",\n            \"▐ ⠠      ▌\",\n            \"▐ ⡀      ▌\",\n            \"▐⠠       ▌\"\n        ]\n    },\n    shark: {\n        interval: 120,\n        frames: [\n            \"▐|\\\\____________▌\",\n            \"▐_|\\\\___________▌\",\n            \"▐__|\\\\__________▌\",\n            \"▐___|\\\\_________▌\",\n            \"▐____|\\\\________▌\",\n            \"▐_____|\\\\_______▌\",\n            \"▐______|\\\\______▌\",\n            \"▐_______|\\\\_____▌\",\n            \"▐________|\\\\____▌\",\n            \"▐_________|\\\\___▌\",\n            \"▐__________|\\\\__▌\",\n            \"▐___________|\\\\_▌\",\n            \"▐____________|\\\\▌\",\n            \"▐____________/|▌\",\n            \"▐___________/|_▌\",\n            \"▐__________/|__▌\",\n            \"▐_________/|___▌\",\n            \"▐________/|____▌\",\n            \"▐_______/|_____▌\",\n            \"▐______/|______▌\",\n            \"▐_____/|_______▌\",\n            \"▐____/|________▌\",\n            \"▐___/|_________▌\",\n            \"▐__/|__________▌\",\n            \"▐_/|___________▌\",\n            \"▐/|____________▌\"\n        ]\n    },\n    dqpb: {\n        interval: 100,\n        frames: [\n            \"d\",\n            \"q\",\n            \"p\",\n            \"b\"\n        ]\n    },\n    weather: {\n        interval: 100,\n        frames: [\n            \"☀️ \",\n            \"☀️ \",\n            \"☀️ \",\n            \"🌤 \",\n            \"⛅️ \",\n            \"🌥 \",\n            \"☁️ \",\n            \"🌧 \",\n            \"🌨 \",\n            \"🌧 \",\n            \"🌨 \",\n            \"🌧 \",\n            \"🌨 \",\n            \"⛈ \",\n            \"🌨 \",\n            \"🌧 \",\n            \"🌨 \",\n            \"☁️ \",\n            \"🌥 \",\n            \"⛅️ \",\n            \"🌤 \",\n            \"☀️ \",\n            \"☀️ \"\n        ]\n    },\n    christmas: {\n        interval: 400,\n        frames: [\n            \"🌲\",\n            \"🎄\"\n        ]\n    },\n    grenade: {\n        interval: 80,\n        frames: [\n            \"،   \",\n            \"′   \",\n            \" ´ \",\n            \" ‾ \",\n            \"  ⸌\",\n            \"  ⸊\",\n            \"  |\",\n            \"  ⁎\",\n            \"  ⁕\",\n            \" ෴ \",\n            \"  ⁓\",\n            \"   \",\n            \"   \",\n            \"   \"\n        ]\n    },\n    point: {\n        interval: 125,\n        frames: [\n            \"∙∙∙\",\n            \"●∙∙\",\n            \"∙●∙\",\n            \"∙∙●\",\n            \"∙∙∙\"\n        ]\n    },\n    layer: {\n        interval: 150,\n        frames: [\n            \"-\",\n            \"=\",\n            \"≡\"\n        ]\n    },\n    betaWave: {\n        interval: 80,\n        frames: [\n            \"ρββββββ\",\n            \"βρβββββ\",\n            \"ββρββββ\",\n            \"βββρβββ\",\n            \"ββββρββ\",\n            \"βββββρβ\",\n            \"ββββββρ\"\n        ]\n    }\n};\nlet supported = true;\nif ((await Deno.permissions.query({\n    name: \"env\"\n})).state === \"granted\") {\n    supported = supported && (!!Deno.env.get(\"CI\") || Deno.env.get(\"TERM\") === \"xterm-256color\");\n}\nconst main = {\n    info: blue(\"ℹ\"),\n    success: green(\"✔\"),\n    warning: yellow(\"⚠\"),\n    error: red(\"✖\")\n};\nconst fallbacks = {\n    info: blue(\"i\"),\n    success: green(\"√\"),\n    warning: yellow(\"‼\"),\n    error: red(\"×\")\n};\nconst symbols = supported ? main : fallbacks;\nconst encoder1 = new TextEncoder();\nconst colormap = {\n    black: black,\n    red: red,\n    green: green,\n    yellow: yellow,\n    blue: blue,\n    magenta: magenta,\n    cyan: cyan,\n    white: white,\n    gray: gray\n};\nfunction wait(opts) {\n    if (typeof opts === \"string\") {\n        opts = {\n            text: opts\n        };\n    }\n    return new Spinner({\n        text: opts.text,\n        prefix: opts.prefix ?? \"\",\n        color: opts.color ?? cyan,\n        spinner: opts.spinner ?? \"dots\",\n        hideCursor: opts.hideCursor ?? true,\n        indent: opts.indent ?? 0,\n        interval: opts.interval ?? 100,\n        stream: opts.stream ?? Deno.stdout,\n        enabled: true,\n        discardStdin: true,\n        interceptConsole: opts.interceptConsole ?? true\n    });\n}\nclass Spinner {\n    #opts;\n    isSpinning;\n    #stream;\n    indent;\n    interval;\n    #id = 0;\n    #enabled;\n    #frameIndex;\n    #linesToClear;\n    #linesCount;\n    constructor(opts){\n        this.#opts = opts;\n        this.#stream = this.#opts.stream;\n        this.text = this.#opts.text;\n        this.prefix = this.#opts.prefix;\n        this.color = this.#opts.color;\n        this.spinner = this.#opts.spinner;\n        this.indent = this.#opts.indent;\n        this.interval = this.#opts.interval;\n        this.isSpinning = false;\n        this.#frameIndex = 0;\n        this.#linesToClear = 0;\n        this.#linesCount = 1;\n        this.#enabled = typeof opts.enabled === \"boolean\" ? opts.enabled : isInteractive(this.#stream);\n        if (opts.hideCursor) {\n            addEventListener(\"unload\", ()=>{\n                showCursorSync(this.#stream);\n            });\n        }\n        if (opts.interceptConsole) {\n            this.#interceptConsole();\n        }\n    }\n    #spinner = __default1.dots;\n    #color = cyan;\n    #text = \"\";\n    #prefix = \"\";\n    #interceptConsole() {\n        const methods = [\n            \"log\",\n            \"debug\",\n            \"info\",\n            \"dir\",\n            \"dirxml\",\n            \"warn\",\n            \"error\",\n            \"assert\",\n            \"count\",\n            \"countReset\",\n            \"table\",\n            \"time\",\n            \"timeLog\",\n            \"timeEnd\",\n            \"group\",\n            \"groupCollapsed\",\n            \"groupEnd\",\n            \"clear\",\n            \"trace\",\n            \"profile\",\n            \"profileEnd\",\n            \"timeStamp\"\n        ];\n        for (const method of methods){\n            const original = console[method];\n            console[method] = (...args)=>{\n                if (this.isSpinning) {\n                    this.stop();\n                    this.clear();\n                    original(...args);\n                    this.start();\n                } else {\n                    original(...args);\n                }\n            };\n        }\n    }\n    set spinner(spin) {\n        this.#frameIndex = 0;\n        if (typeof spin === \"string\") this.#spinner = __default1[spin];\n        else this.#spinner = spin;\n    }\n    get spinner() {\n        return this.#spinner;\n    }\n    set color(color) {\n        if (typeof color === \"string\") this.#color = colormap[color];\n        else this.#color = color;\n    }\n    get color() {\n        return this.#color;\n    }\n    set text(value) {\n        this.#text = value;\n        this.updateLines();\n    }\n    get text() {\n        return this.#text;\n    }\n    set prefix(value) {\n        this.#prefix = value;\n        this.updateLines();\n    }\n    get prefix() {\n        return this.#prefix;\n    }\n    #write(data) {\n        this.#stream.writeSync(encoder1.encode(data));\n    }\n    start() {\n        if (!this.#enabled) {\n            if (this.text) {\n                this.#write(`- ${this.text}\\n`);\n            }\n            return this;\n        }\n        if (this.isSpinning) return this;\n        if (this.#opts.hideCursor) {\n            hideCursorSync(this.#stream);\n        }\n        this.isSpinning = true;\n        this.render();\n        this.#id = setInterval(this.render.bind(this), this.interval);\n        return this;\n    }\n    render() {\n        this.clear();\n        this.#write(`${this.frame()}\\n`);\n        this.updateLines();\n        this.#linesToClear = this.#linesCount;\n    }\n    frame() {\n        const { frames } = this.#spinner;\n        let frame = frames[this.#frameIndex];\n        frame = this.#color(frame);\n        this.#frameIndex = ++this.#frameIndex % frames.length;\n        const fullPrefixText = typeof this.prefix === \"string\" && this.prefix !== \"\" ? this.prefix + \" \" : \"\";\n        const fullText = typeof this.text === \"string\" ? \" \" + this.text : \"\";\n        return fullPrefixText + frame + fullText;\n    }\n    clear() {\n        if (!this.#enabled) return;\n        for(let i = 0; i < this.#linesToClear; i++){\n            goUpSync(1, this.#stream);\n            clearLineSync(this.#stream);\n            goRightSync(this.indent - 1, this.#stream);\n        }\n        this.#linesToClear = 0;\n    }\n    updateLines() {\n        let columns = 80;\n        try {\n            columns = Deno.consoleSize().columns ?? columns;\n        } catch  {}\n        const fullPrefixText = typeof this.prefix === \"string\" ? this.prefix + \"-\" : \"\";\n        this.#linesCount = stripAnsi(fullPrefixText + \"--\" + this.text).split(\"\\n\").reduce((count, line)=>{\n            return count + Math.max(1, Math.ceil(wcswidth(line) / columns));\n        }, 0);\n    }\n    stop() {\n        if (!this.#enabled) return;\n        clearInterval(this.#id);\n        this.#id = -1;\n        this.#frameIndex = 0;\n        this.clear();\n        this.isSpinning = false;\n        if (this.#opts.hideCursor) {\n            showCursorSync(this.#stream);\n        }\n    }\n    stopAndPersist(options = {}) {\n        const prefix = options.prefix || this.prefix;\n        const fullPrefix = typeof prefix === \"string\" && prefix !== \"\" ? prefix + \" \" : \"\";\n        const text = options.text || this.text;\n        const fullText = typeof text === \"string\" ? \" \" + text : \"\";\n        this.stop();\n        this.#write(`${fullPrefix}${options.symbol || \" \"}${fullText}\\n`);\n    }\n    succeed(text) {\n        return this.stopAndPersist({\n            symbol: symbols.success,\n            text\n        });\n    }\n    fail(text) {\n        return this.stopAndPersist({\n            symbol: symbols.error,\n            text\n        });\n    }\n    warn(text) {\n        return this.stopAndPersist({\n            symbol: symbols.warning,\n            text\n        });\n    }\n    info(text) {\n        return this.stopAndPersist({\n            symbol: symbols.info,\n            text\n        });\n    }\n}\nlet current = null;\nfunction wait1(param) {\n    if (typeof param === \"string\") {\n        param = {\n            text: param\n        };\n    }\n    param.interceptConsole = false;\n    current = wait({\n        stream: Deno.stderr,\n        ...param\n    });\n    return current;\n}\nfunction interruptSpinner() {\n    current?.stop();\n    const interrupt = new Interrupt(current);\n    current = null;\n    return interrupt;\n}\nclass Interrupt {\n    #spinner;\n    constructor(spinner){\n        this.#spinner = spinner;\n    }\n    resume() {\n        current = this.#spinner;\n        this.#spinner?.start();\n    }\n}\nconst USER_AGENT = `DeployCTL/${VERSION} (${Deno.build.os} ${Deno.osRelease()}; ${Deno.build.arch})`;\nclass APIError extends Error {\n    code;\n    xDenoRay;\n    name = \"APIError\";\n    constructor(code, message, xDenoRay){\n        super(message);\n        this.code = code;\n        this.xDenoRay = xDenoRay;\n    }\n    toString() {\n        let error = `${this.name}: ${this.message}`;\n        if (this.xDenoRay !== null) {\n            error += `\\nx-deno-ray: ${this.xDenoRay}`;\n            error += \"\\nIf you encounter this error frequently,\" + \" contact us at deploy@deno.com with the above x-deno-ray.\";\n        }\n        return error;\n    }\n}\nfunction endpoint() {\n    return Deno.env.get(\"DEPLOY_API_ENDPOINT\") ?? \"https://dash.deno.com\";\n}\nclass API {\n    #endpoint;\n    #authorization;\n    #config;\n    constructor(authorization, endpoint, config){\n        this.#authorization = authorization;\n        this.#endpoint = endpoint;\n        const DEFAULT_CONFIG = {\n            alwaysPrintXDenoRay: false,\n            logger: {\n                debug: (m)=>console.debug(m),\n                info: (m)=>console.info(m),\n                notice: (m)=>console.log(m),\n                warning: (m)=>console.warn(m),\n                error: (m)=>console.error(m)\n            }\n        };\n        this.#config = DEFAULT_CONFIG;\n        this.#config.alwaysPrintXDenoRay = config?.alwaysPrintXDenoRay ?? DEFAULT_CONFIG.alwaysPrintXDenoRay;\n        this.#config.logger = config?.logger ?? DEFAULT_CONFIG.logger;\n    }\n    static fromToken(token) {\n        return new API(`Bearer ${token}`, endpoint());\n    }\n    static withTokenProvisioner(provisioner) {\n        return new API(provisioner, endpoint());\n    }\n    async request(path, opts = {}) {\n        const url = `${this.#endpoint}/api${path}`;\n        const method = opts.method ?? \"GET\";\n        const body = typeof opts.body === \"string\" || opts.body instanceof FormData ? opts.body : JSON.stringify(opts.body);\n        const authorization = typeof this.#authorization === \"string\" ? this.#authorization : `Bearer ${await this.#authorization.get() ?? await this.#authorization.provision()}`;\n        const sudo = Deno.env.get(\"SUDO\");\n        const headers = {\n            \"User-Agent\": USER_AGENT,\n            \"Accept\": opts.accept ?? \"application/json\",\n            \"Authorization\": authorization,\n            ...opts.body !== undefined ? opts.body instanceof FormData ? {} : {\n                \"Content-Type\": \"application/json\"\n            } : {},\n            ...sudo ? {\n                [\"x-deploy-sudo\"]: sudo\n            } : {}\n        };\n        let res = await fetch(url, {\n            method,\n            headers,\n            body\n        });\n        if (this.#config.alwaysPrintXDenoRay) {\n            this.#config.logger.notice(`x-deno-ray: ${res.headers.get(\"x-deno-ray\")}`);\n        }\n        if (res.status === 401 && typeof this.#authorization === \"object\") {\n            headers.Authorization = `Bearer ${await this.#authorization.provision()}`;\n            res = await fetch(url, {\n                method,\n                headers,\n                body\n            });\n        }\n        return res;\n    }\n    async #requestJson(path, opts) {\n        const res = await this.request(path, opts);\n        if (res.headers.get(\"Content-Type\") !== \"application/json\") {\n            const text = await res.text();\n            throw new Error(`Expected JSON, got '${text}'`);\n        }\n        const json = await res.json();\n        if (res.status !== 200) {\n            const xDenoRay = res.headers.get(\"x-deno-ray\");\n            throw new APIError(json.code, json.message, xDenoRay);\n        }\n        return json;\n    }\n    async #requestStream(path, opts) {\n        const res = await this.request(path, opts);\n        if (res.status !== 200) {\n            const json = await res.json();\n            const xDenoRay = res.headers.get(\"x-deno-ray\");\n            throw new APIError(json.code, json.message, xDenoRay);\n        }\n        if (res.body === null) {\n            throw new Error(\"Stream ended unexpectedly\");\n        }\n        const lines = res.body.pipeThrough(new TextDecoderStream()).pipeThrough(new TextLineStream());\n        return async function*() {\n            for await (const line of lines){\n                if (line === \"\") return;\n                yield line;\n            }\n        }();\n    }\n    async #requestJsonStream(path, opts) {\n        const stream = await this.#requestStream(path, opts);\n        return async function*() {\n            for await (const line of stream){\n                yield JSON.parse(line);\n            }\n        }();\n    }\n    async getOrganizationByName(name) {\n        const organizations = await this.#requestJson(`/organizations`);\n        for (const org of organizations){\n            if (org.name === name) {\n                return org;\n            }\n        }\n    }\n    async getOrganizationById(id) {\n        return await this.#requestJson(`/organizations/${id}`);\n    }\n    async createOrganization(name) {\n        const body = {\n            name\n        };\n        return await this.#requestJson(`/organizations`, {\n            method: \"POST\",\n            body\n        });\n    }\n    async listOrganizations() {\n        return await this.#requestJson(`/organizations`);\n    }\n    async getProject(id) {\n        try {\n            return await this.#requestJson(`/projects/${id}`);\n        } catch (err) {\n            if (err instanceof APIError && err.code === \"projectNotFound\") {\n                return null;\n            }\n            throw err;\n        }\n    }\n    async createProject(name, organizationId, envs) {\n        const body = {\n            name,\n            organizationId,\n            envs\n        };\n        return await this.#requestJson(`/projects/`, {\n            method: \"POST\",\n            body\n        });\n    }\n    async renameProject(id, newName) {\n        const body = {\n            name: newName\n        };\n        await this.#requestJson(`/projects/${id}`, {\n            method: \"PATCH\",\n            body\n        });\n    }\n    async deleteProject(id) {\n        try {\n            await this.#requestJson(`/projects/${id}`, {\n                method: \"DELETE\"\n            });\n            return true;\n        } catch (err) {\n            if (err instanceof APIError && err.code === \"projectNotFound\") {\n                return false;\n            }\n            throw err;\n        }\n    }\n    async listProjects(orgId) {\n        const org = await this.#requestJson(`/organizations/${orgId}`);\n        return org.projects;\n    }\n    async getDomains(projectId) {\n        return await this.#requestJson(`/projects/${projectId}/domains`);\n    }\n    async listDeployments(projectId, page, limit) {\n        const query = new URLSearchParams();\n        if (page !== undefined) {\n            query.set(\"page\", page.toString());\n        }\n        if (limit !== undefined) {\n            query.set(\"limit\", limit.toString());\n        }\n        try {\n            const [list, paging] = await this.#requestJson(`/projects/${projectId}/deployments?${query}`);\n            return {\n                list,\n                paging\n            };\n        } catch (err) {\n            if (err instanceof APIError && err.code === \"projectNotFound\") {\n                return null;\n            }\n            throw err;\n        }\n    }\n    async *listAllDeployments(projectId) {\n        let totalPages = 1;\n        let page = 0;\n        while(totalPages > page){\n            const [deployments, paging] = await this.#requestJson(`/projects/${projectId}/deployments/?limit=50&page=${page}`);\n            for (const deployment of deployments){\n                yield deployment;\n            }\n            totalPages = paging.totalPages;\n            page = paging.page + 1;\n        }\n    }\n    async getDeployment(deploymentId) {\n        try {\n            return await this.#requestJson(`/deployments/${deploymentId}`);\n        } catch (err) {\n            if (err instanceof APIError && err.code === \"deploymentNotFound\") {\n                return null;\n            }\n            throw err;\n        }\n    }\n    async deleteDeployment(deploymentId) {\n        try {\n            await this.#requestJson(`/v1/deployments/${deploymentId}`, {\n                method: \"DELETE\"\n            });\n            return true;\n        } catch (err) {\n            if (err instanceof APIError && err.code === \"deploymentNotFound\") {\n                return false;\n            }\n            throw err;\n        }\n    }\n    async redeployDeployment(deploymentId, redeployParams) {\n        try {\n            return await this.#requestJson(`/v1/deployments/${deploymentId}/redeploy?internal=true`, {\n                method: \"POST\",\n                body: redeployParams\n            });\n        } catch (err) {\n            if (err instanceof APIError && err.code === \"deploymentNotFound\") {\n                return null;\n            }\n            throw err;\n        }\n    }\n    getLogs(projectId, deploymentId) {\n        return this.#requestJsonStream(`/projects/${projectId}/deployments/${deploymentId}/logs/`, {\n            accept: \"application/x-ndjson\"\n        });\n    }\n    async queryLogs(projectId, deploymentId, params) {\n        const searchParams = new URLSearchParams({\n            params: JSON.stringify(params)\n        });\n        return await this.#requestJson(`/projects/${projectId}/deployments/${deploymentId}/query_logs?${searchParams.toString()}`);\n    }\n    async projectNegotiateAssets(id, manifest) {\n        return await this.#requestJson(`/projects/${id}/assets/negotiate`, {\n            method: \"POST\",\n            body: manifest\n        });\n    }\n    pushDeploy(projectId, request, files) {\n        const form = new FormData();\n        form.append(\"request\", JSON.stringify(request));\n        for (const bytes of files){\n            form.append(\"file\", new Blob([\n                bytes\n            ]));\n        }\n        return this.#requestJsonStream(`/projects/${projectId}/deployment_with_assets`, {\n            method: \"POST\",\n            body: form\n        });\n    }\n    gitHubActionsDeploy(projectId, request, files) {\n        const form = new FormData();\n        form.append(\"request\", JSON.stringify(request));\n        for (const bytes of files){\n            form.append(\"file\", new Blob([\n                bytes\n            ]));\n        }\n        return this.#requestJsonStream(`/projects/${projectId}/deployment_github_actions`, {\n            method: \"POST\",\n            body: form\n        });\n    }\n    getMetadata() {\n        return this.#requestJson(\"/meta\");\n    }\n    async streamMetering(project) {\n        const streamGen = ()=>this.#requestStream(`/projects/${project}/stats`);\n        let stream = await streamGen();\n        return async function*() {\n            for(;;){\n                try {\n                    for await (const line of stream){\n                        try {\n                            yield JSON.parse(line);\n                        } catch  {}\n                    }\n                } catch (error) {\n                    const interrupt = interruptSpinner();\n                    const spinner = wait1(`Error: ${error}. Reconnecting...`).start();\n                    await delay(5_000);\n                    stream = await streamGen();\n                    spinner.stop();\n                    interrupt.resume();\n                }\n            }\n        }();\n    }\n    async getProjectDatabases(project) {\n        try {\n            return await this.#requestJson(`/projects/${project}/databases`);\n        } catch (err) {\n            if (err instanceof APIError && err.code === \"projectNotFound\") {\n                return null;\n            }\n            throw err;\n        }\n    }\n    async getDeploymentCrons(projectId, deploymentId) {\n        return await this.#requestJson(`/projects/${projectId}/deployments/${deploymentId}/crons`);\n    }\n    async getProjectCrons(projectId) {\n        try {\n            return await this.#requestJson(`/projects/${projectId}/deployments/latest/crons`);\n        } catch (err) {\n            if (err instanceof APIError && err.code === \"deploymentNotFound\") {\n                return null;\n            }\n            throw err;\n        }\n    }\n}\nasync function calculateGitSha1(bytes) {\n    const prefix = `blob ${bytes.byteLength}\\0`;\n    const prefixBytes = new TextEncoder().encode(prefix);\n    const fullBytes = new Uint8Array(prefixBytes.byteLength + bytes.byteLength);\n    fullBytes.set(prefixBytes);\n    fullBytes.set(bytes, prefixBytes.byteLength);\n    const hashBytes = await crypto.subtle.digest(\"SHA-1\", fullBytes);\n    const hashHex = Array.from(new Uint8Array(hashBytes)).map((b)=>b.toString(16).padStart(2, \"0\")).join(\"\");\n    return hashHex;\n}\nfunction include(path, include, exclude) {\n    if (include.length && !include.some((pattern)=>pattern.test(normalize2(path)))) {\n        return false;\n    }\n    if (exclude.length && exclude.some((pattern)=>pattern.test(normalize2(path)))) {\n        return false;\n    }\n    return true;\n}\nasync function walk(cwd, dir, options) {\n    const hashPathMap = new Map();\n    const manifestEntries = await walkInner(cwd, dir, hashPathMap, options);\n    return {\n        manifestEntries,\n        hashPathMap\n    };\n}\nasync function walkInner(cwd, dir, hashPathMap, options) {\n    const entries = {};\n    for await (const file of Deno.readDir(dir)){\n        const path = join2(dir, file.name);\n        const relative = path.slice(cwd.length);\n        if (!file.isDirectory && !include(path.slice(cwd.length + 1), options.include, options.exclude)) {\n            continue;\n        }\n        let entry;\n        if (file.isFile) {\n            const data = await Deno.readFile(path);\n            const gitSha1 = await calculateGitSha1(data);\n            entry = {\n                kind: \"file\",\n                gitSha1,\n                size: data.byteLength\n            };\n            hashPathMap.set(gitSha1, path);\n        } else if (file.isDirectory) {\n            if (relative === \"/.git\") continue;\n            entry = {\n                kind: \"directory\",\n                entries: await walkInner(cwd, path, hashPathMap, options)\n            };\n        } else if (file.isSymlink) {\n            const target = await Deno.readLink(path);\n            entry = {\n                kind: \"symlink\",\n                target\n            };\n        } else {\n            throw new Error(`Unreachable`);\n        }\n        entries[file.name] = entry;\n    }\n    return entries;\n}\nfunction convertPatternToRegExp(pattern) {\n    return isGlob(pattern) ? new RegExp(globToRegExp2(normalize2(pattern)).toString().slice(1, -2)) : new RegExp(`^${normalize2(pattern)}`);\n}\nexport { parseEntrypoint as parseEntrypoint };\nexport { API as API, APIError as APIError };\nexport { convertPatternToRegExp as convertPatternToRegExp, walk as walk };\nexport { fromFileUrl2 as fromFileUrl, resolve2 as resolve };\n\n"
  },
  {
    "path": "action/index.js",
    "content": "import * as core from \"@actions/core\";\nimport * as github from \"@actions/github\";\nimport \"./shim.js\";\nimport {\n  API,\n  APIError,\n  convertPatternToRegExp,\n  fromFileUrl,\n  parseEntrypoint,\n  resolve,\n  walk,\n} from \"./deps.js\";\nimport process from \"node:process\";\n\n// The origin of the server to make Deploy requests to.\nconst ORIGIN = process.env.DEPLOY_API_ENDPOINT ?? \"https://dash.deno.com\";\n\nasync function main() {\n  const projectId = core.getInput(\"project\", { required: true });\n  const entrypoint = core.getInput(\"entrypoint\", { required: true });\n  const importMap = core.getInput(\"import-map\", {});\n  const include = core.getMultilineInput(\"include\", {});\n  const exclude = core.getMultilineInput(\"exclude\", {});\n  const cwd = resolve(process.cwd(), core.getInput(\"root\", {}));\n\n  if (github.context.eventName === \"pull_request\") {\n    const pr = github.context.payload.pull_request;\n    const isPRFromFork = pr.head.repo.id !== pr.base.repo.id;\n    if (isPRFromFork) {\n      core.setOutput(\"deployment-id\", \"\");\n      core.setOutput(\"url\", \"\");\n      core.notice(\n        \"Deployments from forks are currently not supported by Deno Deploy. The deployment was skipped.\",\n        {\n          title: \"Skipped deployment on fork\",\n        },\n      );\n      return;\n    }\n  }\n\n  const aud = new URL(`/projects/${projectId}`, ORIGIN);\n  let token;\n  try {\n    token = await core.getIDToken(aud);\n  } catch {\n    throw \"Failed to get the GitHub OIDC token. Make sure that this job has the required permissions for getting GitHub OIDC tokens (see https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings ).\";\n  }\n  core.info(`Project: ${projectId}`);\n\n  let url = await parseEntrypoint(entrypoint, cwd);\n  if (url.protocol === \"file:\") {\n    const path = fromFileUrl(url);\n    if (!path.startsWith(cwd)) {\n      throw \"Entrypoint must be in the working directory (cwd, or specified root directory).\";\n    }\n    const entrypoint = path.slice(cwd.length);\n    url = new URL(`file:///src${entrypoint}`);\n  }\n  core.info(`Entrypoint: ${url.href}`);\n\n  let importMapUrl = null;\n  if (importMap) {\n    importMapUrl = await parseEntrypoint(importMap, cwd, \"import map\");\n    if (importMapUrl.protocol === \"file:\") {\n      const path = fromFileUrl(importMapUrl);\n      if (!path.startsWith(cwd)) {\n        throw \"Import map must be in the working directory (cwd, or specified root directory).\";\n      }\n      const importMap = path.slice(cwd.length);\n      importMapUrl = new URL(`file:///src${importMap}`);\n    }\n    core.info(`Import map: ${importMapUrl.href}`);\n  }\n\n  core.debug(`Discovering assets in \"${cwd}\"`);\n  const includes = include.flatMap((i) => i.split(\",\")).map((i) => i.trim());\n  const excludes = exclude.flatMap((e) => e.split(\",\")).map((i) => i.trim());\n  // Exclude node_modules by default unless explicitly specified\n  if (!includes.some((i) => i.includes(\"node_modules\"))) {\n    excludes.push(\"**/node_modules\");\n  }\n  const { manifestEntries: entries, hashPathMap: assets } = await walk(\n    cwd,\n    cwd,\n    {\n      include: includes.map(convertPatternToRegExp),\n      exclude: excludes.map(convertPatternToRegExp),\n    },\n  );\n  core.debug(`Discovered ${assets.size} assets`);\n\n  const api = new API(`GitHubOIDC ${token}`, ORIGIN, {\n    alwaysPrintDenoRay: true,\n    logger: core,\n  });\n\n  const neededHashes = await api.projectNegotiateAssets(projectId, {\n    entries,\n  });\n  core.debug(`Determined ${neededHashes.length} need to be uploaded`);\n\n  const files = [];\n  for (const hash of neededHashes) {\n    const path = assets.get(hash);\n    if (path === undefined) {\n      throw `Asset ${hash} not found.`;\n    }\n    const data = await Deno.readFile(path);\n    files.push(data);\n  }\n  const totalSize = files.reduce((acc, file) => acc + file.length, 0);\n  core.info(\n    `Uploading ${neededHashes.length} file(s) (total ${totalSize} bytes)`,\n  );\n\n  const manifest = { entries };\n  core.debug(`Manifest: ${JSON.stringify(manifest, null, 2)}`);\n\n  const req = {\n    url: url.href,\n    importMapUrl: importMapUrl?.href ?? null,\n    manifest,\n    event: github.context.payload,\n  };\n  const progress = await api.gitHubActionsDeploy(projectId, req, files);\n  let deployment;\n  for await (const event of progress) {\n    switch (event.type) {\n      case \"staticFile\": {\n        const percentage = (event.currentBytes / event.totalBytes) * 100;\n        core.info(\n          `Uploading ${files.length} asset(s) (${percentage.toFixed(1)}%)`,\n        );\n        break;\n      }\n      case \"load\": {\n        const progress = event.seen / event.total * 100;\n        core.info(`Deploying... (${progress.toFixed(1)}%)`);\n        break;\n      }\n      case \"uploadComplete\":\n        core.info(\"Finishing deployment...\");\n        break;\n      case \"success\":\n        core.info(\"Deployment complete.\");\n        core.info(\"\\nView at:\");\n        for (const { domain } of event.domainMappings) {\n          core.info(` - https://${domain}`);\n        }\n        deployment = event;\n        break;\n      case \"error\":\n        throw event.ctx;\n    }\n  }\n\n  core.setOutput(\"deployment-id\", deployment.id);\n  const domain = deployment.domainMappings[0].domain;\n  core.setOutput(\"url\", `https://${domain}/`);\n}\n\ntry {\n  await main();\n} catch (error) {\n  if (error instanceof APIError) {\n    core.setFailed(error.toString());\n  } else {\n    core.setFailed(error);\n  }\n}\n"
  },
  {
    "path": "action/package.json",
    "content": "{\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@actions/core\": \"^1.10.0\",\n    \"@actions/github\": \"^5.1.1\",\n    \"@deno/shim-deno\": \"^0.19.2\",\n    \"formdata-polyfill\": \"^4.0.10\",\n    \"undici\": \"^5.11.0\"\n  }\n}\n"
  },
  {
    "path": "action/shim.js",
    "content": "import { fetch as realfetch } from \"undici\";\nimport { Deno } from \"@deno/shim-deno\";\nimport { TransformStream } from \"stream/web\";\nimport { FormData, formDataToBlob } from \"formdata-polyfill/esm.min.js\";\nimport Blob from \"fetch-blob\";\n\nfunction fetch(url, init) {\n  if (init.body instanceof FormData) {\n    init.body = formDataToBlob(init.body, Blob);\n  }\n  return realfetch(url, init);\n}\n\nglobalThis.fetch = fetch;\nglobalThis.Deno = Deno;\nglobalThis.TransformStream = TransformStream;\nglobalThis.FormData = FormData;\nglobalThis.Blob = Blob;\n"
  },
  {
    "path": "action/tests/README.md",
    "content": "These test modules are deployed by the\n[test GHA](../../.github/workflows/test.yml). Assertions are performed as\ndeployment errors.\n"
  },
  {
    "path": "action/tests/always_exclude_node_modules/main.ts",
    "content": "try {\n  await Deno.lstat(new URL(import.meta.resolve(\"./node_modules/import_bomb1\")));\n  throw new Error(\"BOOM!\");\n} catch (e) {\n  if (!(e instanceof Deno.errors.NotFound)) {\n    throw e;\n  }\n}\ntry {\n  await Deno.lstat(new URL(import.meta.resolve(\"./node_modules/import_bomb2\")));\n  throw new Error(\"BOOM!\");\n} catch (e) {\n  if (!(e instanceof Deno.errors.NotFound)) {\n    throw e;\n  }\n}\nDeno.serve(() => new Response(\"Hello World\"));\n"
  },
  {
    "path": "action/tests/hello.ts",
    "content": "import { serve } from \"std/http/server.ts\";\n\nasync function handler(_req: Request) {\n  const text = await Deno.readTextFile(new URL(import.meta.url));\n  return new Response(text, {\n    headers: { \"content-type\": \"text/plain; charset=utf8\" },\n  });\n}\n\nconsole.log(\"Listening on http://localhost:8000\");\nserve(handler);\n"
  },
  {
    "path": "action/tests/import_bomb1",
    "content": ""
  },
  {
    "path": "action/tests/import_bomb2",
    "content": ""
  },
  {
    "path": "action/tests/import_map.json",
    "content": "{\n  \"imports\": {\n    \"std/\": \"https://deno.land/std@0.128.0/\"\n  }\n}\n"
  },
  {
    "path": "action/tests/include_exclude.ts",
    "content": "try {\n  await Deno.lstat(new URL(import.meta.resolve(\"./import_bomb1\")));\n  throw new Error(\"BOOM!\");\n} catch (e) {\n  if (!(e instanceof Deno.errors.NotFound)) {\n    throw e;\n  }\n}\ntry {\n  await Deno.lstat(new URL(import.meta.resolve(\"./import_bomb2\")));\n  throw new Error(\"BOOM!\");\n} catch (e) {\n  if (!(e instanceof Deno.errors.NotFound)) {\n    throw e;\n  }\n}\nDeno.serve(() => new Response(\"Hello World\"));\n"
  },
  {
    "path": "action.yml",
    "content": "name: Deploy to Deno Deploy\ndescription: Deploy your applications to Deno Deploy, right from GitHub Actions\nauthor: Deno Land Inc\n\nbranding:\n  color: gray-dark\n  icon: globe\n\ninputs:\n  project:\n    description: The name or ID of the project to deploy\n    required: true\n  entrypoint:\n    description: The path or URL to the entrypoint file\n    required: true\n  import-map:\n    description: The path or URL to an import map file\n    required: false\n  include:\n    description: Only upload files that match this pattern (multiline and/or comma-separated)\n    required: false\n  exclude:\n    description: Exclude files that match this pattern (multiline and/or comma-separated)\n    required: false\n  root:\n    description: The path to the directory containing the code and assets to upload\n    required: false\n\noutputs:\n  deployment-id:\n    description: The ID of the created deployment\n  url:\n    description: The URL where the deployment is reachable\n\nruns:\n  using: node20\n  main: action/index.js\n"
  },
  {
    "path": "deno.jsonc",
    "content": "{\n  \"name\": \"@deno/deployctl\",\n  \"version\": \"1.13.1\",\n  \"exports\": \"./deployctl.ts\",\n  \"fmt\": {\n    \"exclude\": [\"action/node_modules/\"]\n  },\n  \"lint\": {\n    \"exclude\": [\"action/node_modules/\"]\n  },\n  \"tasks\": {\n    \"test\": \"deno test -A --unstable tests/ src/\",\n    \"build-action\": \"deno run --allow-read --allow-write --allow-net=jsr.io:443 --allow-env ./tools/bundle.ts ./src/utils/mod.ts > ./action/deps.js\",\n    \"version-match\": \"deno run --allow-read --allow-env ./tools/version_match.ts\"\n  },\n  \"imports\": {\n    \"@std/fmt\": \"jsr:@std/fmt@0.217\",\n    \"@std/fmt/colors\": \"jsr:@std/fmt@0.217/colors\",\n    \"@std/path\": \"jsr:@std/path@0.217\",\n    \"@std/flags\": \"jsr:@std/flags@0.217\",\n    \"@std/streams\": \"jsr:@std/streams@0.217\",\n    \"@std/streams/text_line_stream\": \"jsr:@std/streams@0.217/text_line_stream\",\n    \"@std/jsonc\": \"jsr:@std/jsonc@0.217\",\n    \"@std/encoding\": \"jsr:@std/encoding@0.217\",\n    \"@std/async\": \"jsr:@std/async@0.217\",\n    \"@std/async/delay\": \"jsr:@std/async@0.217/delay\",\n    \"@std/dotenv\": \"jsr:@std/dotenv@0.217\",\n    \"@std/semver\": \"jsr:@std/semver@0.217\",\n    \"@std/assert\": \"jsr:@std/assert@0.217\",\n    \"@denosaurs/wait\": \"jsr:@denosaurs/wait@0.2.2\",\n    \"@denosaurs/tty\": \"jsr:@denosaurs/tty@0.2.1\",\n    \"@deno/emit\": \"jsr:@deno/emit@0.46.0\"\n  }\n}\n"
  },
  {
    "path": "deployctl.ts",
    "content": "#!/usr/bin/env -S deno run --allow-read --allow-write --allow-env --allow-net --allow-run --allow-sys --quiet\n\n// Copyright 2021 Deno Land Inc. All rights reserved. MIT license.\n\nimport {\n  greaterOrEqual as semverGreaterThanOrEquals,\n  parse as semverParse,\n} from \"@std/semver\";\nimport { setColorEnabled } from \"@std/fmt/colors\";\nimport { type Args, parseArgs } from \"./src/args.ts\";\nimport { error } from \"./src/error.ts\";\nimport deploySubcommand from \"./src/subcommands/deploy.ts\";\nimport upgradeSubcommand from \"./src/subcommands/upgrade.ts\";\nimport logsSubcommand from \"./src/subcommands/logs.ts\";\nimport topSubcommand from \"./src/subcommands/top.ts\";\nimport projectsSubcommand from \"./src/subcommands/projects.ts\";\nimport deploymentsSubcommand from \"./src/subcommands/deployments.ts\";\nimport apiSubcommand from \"./src/subcommands/api.ts\";\nimport { MINIMUM_DENO_VERSION, VERSION } from \"./src/version.ts\";\nimport { fetchReleases, getConfigPaths } from \"./src/utils/info.ts\";\nimport configFile from \"./src/config_file.ts\";\nimport inferConfig from \"./src/config_inference.ts\";\nimport { wait } from \"./src/utils/spinner.ts\";\n\nconst help = `deployctl ${VERSION}\nCommand line tool for Deno Deploy.\n\nSUBCOMMANDS:\n    deploy      Deploy a script with static files to Deno Deploy\n    projects    Manage projects\n    deployments Manage deployments\n    logs        View logs for the given project\n    top         Monitor projects resource usage in real time\n    upgrade     Upgrade deployctl to the given version (defaults to latest)\n    api         Perform raw HTTP requests against the Deploy API \n\nFor more detailed help on each subcommand, use:\n\n    deployctl <SUBCOMMAND> -h\n`;\n\nif (\n  !semverGreaterThanOrEquals(\n    semverParse(Deno.version.deno),\n    semverParse(MINIMUM_DENO_VERSION),\n  )\n) {\n  error(\n    `The Deno version you are using is too old. Please update to Deno ${MINIMUM_DENO_VERSION} or later. To do this run \\`deno upgrade\\`.`,\n  );\n}\n\nconst args = parseArgs(Deno.args);\n\nsetColoring(args);\n\nif (Deno.stdin.isTerminal()) {\n  let latestVersion;\n  // Get the path to the update information json file.\n  const { updatePath } = getConfigPaths();\n  // Try to read the json file.\n  const updateInfoJson = await Deno.readTextFile(updatePath).catch((error) => {\n    if (error.name == \"NotFound\") return null;\n    console.error(error);\n  });\n  if (updateInfoJson) {\n    const updateInfo = JSON.parse(updateInfoJson) as {\n      lastFetched: number;\n      latest: number;\n    };\n    const moreThanADay =\n      Math.abs(Date.now() - updateInfo.lastFetched) > 24 * 60 * 60 * 1000;\n    // Fetch the latest release if it has been more than a day since the last\n    // time the information about new version is fetched.\n    if (moreThanADay) {\n      fetchReleases();\n    } else {\n      latestVersion = updateInfo.latest;\n    }\n  } else {\n    fetchReleases();\n  }\n\n  // If latestVersion is set we need to inform the user about a new release.\n  if (\n    latestVersion &&\n    !(semverGreaterThanOrEquals(\n      semverParse(VERSION),\n      semverParse(latestVersion.toString()),\n    ))\n  ) {\n    console.error(\n      [\n        `A new release of deployctl is available: ${VERSION} -> ${latestVersion}`,\n        \"To upgrade, run `deployctl upgrade`\",\n        `https://github.com/denoland/deployctl/releases/tag/${latestVersion}\\n`,\n      ].join(\"\\n\"),\n    );\n  }\n}\n\nconst subcommand = args._.shift();\nswitch (subcommand) {\n  case \"deploy\":\n    await setDefaultsFromConfigFile(args);\n    await inferConfig(args);\n    await deploySubcommand(args);\n    break;\n  case \"upgrade\":\n    await setDefaultsFromConfigFile(args);\n    await upgradeSubcommand(args);\n    break;\n  case \"logs\":\n    await setDefaultsFromConfigFile(args);\n    await logsSubcommand(args);\n    break;\n  case \"top\":\n    await setDefaultsFromConfigFile(args);\n    await topSubcommand(args);\n    break;\n  case \"projects\":\n    await setDefaultsFromConfigFile(args);\n    await projectsSubcommand(args);\n    break;\n  case \"deployments\":\n    await setDefaultsFromConfigFile(args);\n    await deploymentsSubcommand(args);\n    break;\n  case \"api\":\n    await apiSubcommand(args);\n    break;\n  default:\n    if (args.version) {\n      console.log(`deployctl ${VERSION}`);\n      Deno.exit(0);\n    }\n    if (args.help) {\n      console.log(help);\n      Deno.exit(0);\n    }\n    console.error(help);\n    Deno.exit(1);\n}\n\nasync function setDefaultsFromConfigFile(args: Args) {\n  const loadFileConfig = !args.version && !args.help;\n  if (loadFileConfig) {\n    const config = await configFile.read(\n      args.config ?? configFile.cwdOrAncestors(),\n    );\n    if (config === null && args.config !== undefined && !args[\"save-config\"]) {\n      error(\n        `Could not find or read the config file '${args.config}'. Use --save-config to create it.`,\n      );\n    }\n    if (config !== null) {\n      wait(\"\").start().info(`Using config file '${config.path()}'`);\n      config.useAsDefaultFor(args);\n      // Set the effective config path for the rest of the execution\n      args.config = config.path();\n    }\n  }\n}\n\nfunction setColoring(args: Args) {\n  switch (args.color) {\n    case \"auto\":\n      setAutoColoring();\n      break;\n    case \"always\":\n      setColorEnabled(true);\n      break;\n    case \"never\":\n      setColorEnabled(false);\n      break;\n    default:\n      wait(\"\").start().warn(\n        `'${args.color}' value for the --color option is not valid. Valid values are 'auto', 'always' and 'never'. Defaulting to 'auto'`,\n      );\n      setAutoColoring();\n  }\n}\n\nfunction setAutoColoring() {\n  if (Deno.stdout.isTerminal()) {\n    setColorEnabled(true);\n  } else {\n    setColorEnabled(false);\n  }\n}\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Examples\n\n- [Hello-World](./hello-world/)\n- [Link Shortener](./link-shortener/)\n- [Fresh Hello-World](./fresh/)\n\nMake sure to visit the\n[Deno Deploy docs](https://docs.deno.com/deploy/tutorials) which has an\nextensive section of tutorials about how to build different use cases.\n"
  },
  {
    "path": "examples/fresh/README.md",
    "content": "# Fresh project\n\nYour new Fresh project is ready to go. You can follow the Fresh \"Getting\nStarted\" guide here: https://fresh.deno.dev/docs/getting-started\n\n### Usage\n\nMake sure to install Deno: https://deno.land/manual/getting_started/installation\n\nThen start the project:\n\n```\ndeno task start\n```\n\nThis will watch the project directory and restart as necessary.\n"
  },
  {
    "path": "examples/fresh/components/Button.tsx",
    "content": "import type { JSX } from \"preact\";\nimport { IS_BROWSER } from \"$fresh/runtime.ts\";\n\nexport function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {\n  return (\n    <button\n      {...props}\n      disabled={!IS_BROWSER || props.disabled}\n      class=\"px-2 py-1 border-gray-500 border-2 rounded bg-white hover:bg-gray-200 transition-colors\"\n    />\n  );\n}\n"
  },
  {
    "path": "examples/fresh/deno.json",
    "content": "{\n  \"lock\": false,\n  \"tasks\": {\n    \"check\": \"deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx\",\n    \"start\": \"deno run -A --watch=static/,routes/ dev.ts\",\n    \"build\": \"deno run -A dev.ts build\",\n    \"preview\": \"deno run -A main.ts\",\n    \"update\": \"deno run -A -r https://fresh.deno.dev/update .\"\n  },\n  \"lint\": {\n    \"rules\": {\n      \"tags\": [\n        \"fresh\",\n        \"recommended\"\n      ]\n    }\n  },\n  \"exclude\": [\n    \"**/_fresh/*\"\n  ],\n  \"imports\": {\n    \"$fresh/\": \"https://deno.land/x/fresh@1.5.4/\",\n    \"preact\": \"https://esm.sh/preact@10.18.1\",\n    \"preact/\": \"https://esm.sh/preact@10.18.1/\",\n    \"preact-render-to-string\": \"https://esm.sh/*preact-render-to-string@6.2.2\",\n    \"@preact/signals\": \"https://esm.sh/*@preact/signals@1.2.1\",\n    \"@preact/signals-core\": \"https://esm.sh/*@preact/signals-core@1.5.0\",\n    \"twind\": \"https://esm.sh/twind@0.16.19\",\n    \"twind/\": \"https://esm.sh/twind@0.16.19/\",\n    \"$std/\": \"https://deno.land/std@0.193.0/\"\n  },\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\"\n  },\n  \"deploy\": {\n    \"exclude\": [],\n    \"include\": [],\n    \"entrypoint\": \"main.ts\"\n  }\n}\n"
  },
  {
    "path": "examples/fresh/dev.ts",
    "content": "#!/usr/bin/env -S deno run -A --watch=static/,routes/\n\nimport dev from \"$fresh/dev.ts\";\nimport config from \"./fresh.config.ts\";\n\nimport \"$std/dotenv/load.ts\";\n\nawait dev(import.meta.url, \"./main.ts\", config);\n"
  },
  {
    "path": "examples/fresh/fresh.config.ts",
    "content": "import { defineConfig } from \"$fresh/server.ts\";\nimport twindPlugin from \"$fresh/plugins/twind.ts\";\nimport twindConfig from \"./twind.config.ts\";\n\nexport default defineConfig({\n  plugins: [twindPlugin(twindConfig)],\n});\n"
  },
  {
    "path": "examples/fresh/fresh.gen.ts",
    "content": "// DO NOT EDIT. This file is generated by Fresh.\n// This file SHOULD be checked into source version control.\n// This file is automatically updated during development when running `dev.ts`.\n\nimport * as $0 from \"./routes/_404.tsx\";\nimport * as $1 from \"./routes/_app.tsx\";\nimport * as $2 from \"./routes/api/joke.ts\";\nimport * as $3 from \"./routes/greet/[name].tsx\";\nimport * as $4 from \"./routes/index.tsx\";\nimport * as $$0 from \"./islands/Counter.tsx\";\n\nconst manifest = {\n  routes: {\n    \"./routes/_404.tsx\": $0,\n    \"./routes/_app.tsx\": $1,\n    \"./routes/api/joke.ts\": $2,\n    \"./routes/greet/[name].tsx\": $3,\n    \"./routes/index.tsx\": $4,\n  },\n  islands: {\n    \"./islands/Counter.tsx\": $$0,\n  },\n  baseUrl: import.meta.url,\n};\n\nexport default manifest;\n"
  },
  {
    "path": "examples/fresh/islands/Counter.tsx",
    "content": "import type { Signal } from \"@preact/signals\";\nimport { Button } from \"../components/Button.tsx\";\n\ninterface CounterProps {\n  count: Signal<number>;\n}\n\nexport default function Counter(props: CounterProps) {\n  return (\n    <div class=\"flex gap-8 py-6\">\n      <Button onClick={() => props.count.value -= 1}>-1</Button>\n      <p class=\"text-3xl\">{props.count}</p>\n      <Button onClick={() => props.count.value += 1}>+1</Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/fresh/main.ts",
    "content": "/// <reference no-default-lib=\"true\" />\n/// <reference lib=\"dom\" />\n/// <reference lib=\"dom.iterable\" />\n/// <reference lib=\"dom.asynciterable\" />\n/// <reference lib=\"deno.ns\" />\n\nimport \"$std/dotenv/load.ts\";\n\nimport { start } from \"$fresh/server.ts\";\nimport manifest from \"./fresh.gen.ts\";\nimport config from \"./fresh.config.ts\";\n\nawait start(manifest, config);\n"
  },
  {
    "path": "examples/fresh/routes/_404.tsx",
    "content": "import { Head } from \"$fresh/runtime.ts\";\n\nexport default function Error404() {\n  return (\n    <>\n      <Head>\n        <title>404 - Page not found</title>\n      </Head>\n      <div class=\"px-4 py-8 mx-auto bg-[#86efac]\">\n        <div class=\"max-w-screen-md mx-auto flex flex-col items-center justify-center\">\n          <img\n            class=\"my-6\"\n            src=\"/logo.svg\"\n            width=\"128\"\n            height=\"128\"\n            alt=\"the Fresh logo: a sliced lemon dripping with juice\"\n          />\n          <h1 class=\"text-4xl font-bold\">404 - Page not found</h1>\n          <p class=\"my-4\">\n            The page you were looking for doesn't exist.\n          </p>\n          <a href=\"/\" class=\"underline\">Go back home</a>\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "examples/fresh/routes/_app.tsx",
    "content": "import type { AppProps } from \"$fresh/server.ts\";\n\nexport default function App({ Component }: AppProps) {\n  return (\n    <html>\n      <head>\n        <meta charset=\"utf-8\" />\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n        <title>fresh-site</title>\n      </head>\n      <body>\n        <Component />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "examples/fresh/routes/api/joke.ts",
    "content": "import type { HandlerContext } from \"$fresh/server.ts\";\n\n// Jokes courtesy of https://punsandoneliners.com/randomness/programmer-jokes/\nconst JOKES = [\n  \"Why do Java developers often wear glasses? They can't C#.\",\n  \"A SQL query walks into a bar, goes up to two tables and says “can I join you?”\",\n  \"Wasn't hard to crack Forrest Gump's password. 1forrest1.\",\n  \"I love pressing the F5 key. It's refreshing.\",\n  \"Called IT support and a chap from Australia came to fix my network connection.  I asked “Do you come from a LAN down under?”\",\n  \"There are 10 types of people in the world. Those who understand binary and those who don't.\",\n  \"Why are assembly programmers often wet? They work below C level.\",\n  \"My favourite computer based band is the Black IPs.\",\n  \"What programme do you use to predict the music tastes of former US presidential candidates? An Al Gore Rhythm.\",\n  \"An SEO expert walked into a bar, pub, inn, tavern, hostelry, public house.\",\n];\n\nexport const handler = (_req: Request, _ctx: HandlerContext): Response => {\n  const randomIndex = Math.floor(Math.random() * JOKES.length);\n  const body = JOKES[randomIndex];\n  return new Response(body);\n};\n"
  },
  {
    "path": "examples/fresh/routes/greet/[name].tsx",
    "content": "import type { PageProps } from \"$fresh/server.ts\";\n\nexport default function Greet(props: PageProps) {\n  return <div>Hello {props.params.name}</div>;\n}\n"
  },
  {
    "path": "examples/fresh/routes/index.tsx",
    "content": "import { useSignal } from \"@preact/signals\";\nimport Counter from \"../islands/Counter.tsx\";\n\nexport default function Home() {\n  const count = useSignal(3);\n  return (\n    <div class=\"px-4 py-8 mx-auto bg-[#86efac]\">\n      <div class=\"max-w-screen-md mx-auto flex flex-col items-center justify-center\">\n        <img\n          class=\"my-6\"\n          src=\"/logo.svg\"\n          width=\"128\"\n          height=\"128\"\n          alt=\"the Fresh logo: a sliced lemon dripping with juice\"\n        />\n        <h1 class=\"text-4xl font-bold\">Welcome to Fresh</h1>\n        <p class=\"my-4\">\n          Try updating this message in the\n          <code class=\"mx-2\">./routes/index.tsx</code> file, and refresh.\n        </p>\n        <Counter count={count} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "examples/fresh/twind.config.ts",
    "content": "import type { Options } from \"$fresh/plugins/twind.ts\";\n\nexport default {\n  selfURL: import.meta.url,\n} as Options;\n"
  },
  {
    "path": "examples/hello-world/deno.json",
    "content": "{\n  \"deploy\": {\n    \"exclude\": [],\n    \"include\": [],\n    \"entrypoint\": \"main.ts\"\n  }\n}\n"
  },
  {
    "path": "examples/hello-world/main.ts",
    "content": "Deno.serve((_req) => new Response(\"Hello World\"));\n"
  },
  {
    "path": "examples/link-shortener/deno.json",
    "content": "{\n  \"deploy\": {\n    \"exclude\": [],\n    \"include\": [],\n    \"entrypoint\": \"main.ts\"\n  }\n}\n"
  },
  {
    "path": "examples/link-shortener/main.ts",
    "content": "const kv = await Deno.openKv();\n\nDeno.serve(async (request: Request) => {\n  // Create short links\n  if (request.method == \"POST\") {\n    const body = await request.text();\n    const { slug, url } = JSON.parse(body);\n    const result = await kv.set([\"links\", slug], url);\n    return new Response(JSON.stringify(result));\n  }\n\n  // Redirect short links\n  const slug = request.url.split(\"/\").pop() || \"\";\n  const url = (await kv.get([\"links\", slug])).value as string;\n  if (url) {\n    return Response.redirect(url, 301);\n  } else {\n    const m = !slug ? \"Please provide a slug.\" : `Slug \"${slug}\" not found`;\n    return new Response(m, { status: 404 });\n  }\n});\n"
  },
  {
    "path": "src/args.ts",
    "content": "// Copyright 2021 Deno Land Inc. All rights reserved. MIT license.\n\nimport { parse } from \"@std/flags\";\n\nexport function parseArgs(args: string[]) {\n  const parsed = parse(args, {\n    alias: {\n      \"help\": \"h\",\n      \"version\": \"V\",\n      \"project\": \"p\",\n    },\n    boolean: [\n      \"help\",\n      \"prod\",\n      \"last\",\n      \"static\",\n      \"version\",\n      \"dry-run\",\n      \"save-config\",\n      \"force\",\n    ],\n    string: [\n      \"project\",\n      \"token\",\n      \"include\",\n      \"exclude\",\n      \"import-map\",\n      \"deployment\",\n      \"since\",\n      \"until\",\n      \"grep\",\n      \"levels\",\n      \"regions\",\n      \"limit\",\n      \"page\",\n      \"config\",\n      \"entrypoint\",\n      \"org\",\n      \"format\",\n      \"color\",\n      \"region\",\n      \"id\",\n      \"prev\",\n      \"next\",\n      \"method\",\n      \"body\",\n      \"db\",\n      \"env\",\n      \"env-file\",\n    ],\n    collect: [\n      \"grep\",\n      \"include\",\n      \"exclude\",\n      \"region\",\n      \"prev\",\n      \"next\",\n      \"env\",\n      \"env-file\",\n    ],\n    default: {\n      static: true,\n      config: Deno.env.get(\"DEPLOYCTL_CONFIG_FILE\"),\n      token: Deno.env.get(\"DENO_DEPLOY_TOKEN\"),\n      org: Deno.env.get(\"DEPLOYCTL_ORGANIZATION\"),\n      color: \"auto\",\n    },\n  });\n  return parsed;\n}\n\nexport type Args = ReturnType<typeof parseArgs>;\n"
  },
  {
    "path": "src/config_file.ts",
    "content": "// Copyright 2021 Deno Land Inc. All rights reserved. MIT license.\n\nimport { cyan, green, magenta, red } from \"@std/fmt/colors\";\nimport * as JSONC from \"@std/jsonc\";\nimport { dirname, extname, join, relative, resolve } from \"@std/path\";\nimport { error } from \"./error.ts\";\nimport { isURL } from \"./utils/entrypoint.ts\";\nimport { wait } from \"./utils/spinner.ts\";\n\nconst DEFAULT_FILENAME = \"deno.json\";\nconst CANDIDATE_FILENAMES = [DEFAULT_FILENAME, \"deno.jsonc\"];\n\n/** Arguments persisted in the deno.json config file */\ninterface ConfigArgs {\n  project?: string;\n  entrypoint?: string;\n  include?: string[];\n  exclude?: string[];\n}\n\nclass ConfigFile {\n  #path: string;\n  #content: { deploy?: ConfigArgs };\n\n  constructor(path: string, content: { deploy?: ConfigArgs }) {\n    this.#path = path;\n    this.#content = {\n      ...content,\n      deploy: content.deploy && this.normalize(content.deploy),\n    };\n  }\n\n  /**\n   * Create a new `ConfigFile` using an object that _at least_ contains the `ConfigArgs`.\n   *\n   * Ignores any property in `args` not meant to be persisted.\n   */\n  static create(path: string, args: ConfigArgs) {\n    const config = new ConfigFile(path, { deploy: {} });\n    // Use override to clean-up args\n    config.override(args);\n    return config;\n  }\n\n  /**\n   * Override the `ConfigArgs` of this ConfigFile.\n   *\n   * Ignores any property in `args` not meant to be persisted.\n   */\n  override(args: ConfigArgs) {\n    const normalizedArgs = this.normalize(args);\n    this.#content.deploy = normalizedArgs;\n  }\n\n  /**\n   * For every arg in `ConfigArgs`, if the `args` argument object does not contain\n   * the arg, fill it with the value in this `ConfigFile`, if any.\n   */\n  useAsDefaultFor(args: ConfigArgs) {\n    for (const [key, thisValue] of Object.entries(this.args())) {\n      // deno-lint-ignore no-explicit-any\n      const argValue = (args as any)[key];\n      if (\n        (argValue === undefined ||\n          Array.isArray(argValue) && argValue.length === 0) && thisValue\n      ) {\n        // deno-lint-ignore no-explicit-any\n        (args as any)[key] = thisValue;\n      }\n    }\n  }\n\n  /** Returns all the differences between this `ConfigArgs` and the one provided as argument.\n   *\n   * The comparison is performed against the JSON output of each config. The \"other\" args are\n   * sematically considered additions in the return value.  Ignores any property in `args` not meant\n   * to be persisted.\n   */\n  diff(args: ConfigArgs): Change[] {\n    const changes = [];\n    const otherConfigOutput =\n      JSON.parse(ConfigFile.create(this.path(), args).toFileContent()).deploy ??\n        {};\n    const thisConfigOutput = JSON.parse(this.toFileContent()).deploy ?? {};\n    // Iterate over the other args as they might include args not yet persisted in the config file\n    for (const [key, otherValue] of Object.entries(otherConfigOutput)) {\n      const thisValue = thisConfigOutput[key];\n      if (Array.isArray(otherValue) && Array.isArray(thisValue)) {\n        if (\n          thisValue.length !== otherValue.length ||\n          !thisValue.every((x, i) => otherValue[i] === x)\n        ) {\n          changes.push({ key, removal: thisValue, addition: otherValue });\n        }\n      } else if (thisValue !== otherValue) {\n        changes.push({ key, removal: thisValue, addition: otherValue });\n      }\n    }\n    return changes;\n  }\n\n  normalize(args: ConfigArgs): ConfigArgs {\n    // Copy object as normalization is internal to the config file\n    const normalizedArgs = {\n      project: args.project,\n      exclude: args.exclude,\n      include: args.include,\n      entrypoint: (args.entrypoint && !isURL(args.entrypoint))\n        ? resolve(args.entrypoint)\n        // Backoff if entrypoint is URL, the user knows what they're doing\n        : args.entrypoint,\n    };\n    return normalizedArgs;\n  }\n\n  /** Return whether the `ConfigFile` has the `deploy` namespace */\n  hasDeployConfig() {\n    return this.#content.deploy !== undefined;\n  }\n\n  static fromFileContent(filepath: string, content: string) {\n    const parsedContent = JSONC.parse(content) as { deploy?: ConfigArgs };\n    const configContent = {\n      ...parsedContent,\n      deploy: parsedContent.deploy && {\n        ...parsedContent.deploy,\n        entrypoint: parsedContent.deploy.entrypoint &&\n          (isURL(parsedContent.deploy.entrypoint)\n            // Backoff if entrypoint is URL, the user knows what they're doing\n            ? parsedContent.deploy.entrypoint\n            // entrypoint must be interpreted as absolute or relative to the config file\n            : resolve(dirname(filepath), parsedContent.deploy.entrypoint)),\n      },\n    };\n    return new ConfigFile(filepath, configContent);\n  }\n\n  toFileContent() {\n    const content = {\n      ...this.#content,\n      deploy: this.#content.deploy && {\n        ...this.#content.deploy,\n        entrypoint: this.#content.deploy.entrypoint &&\n          (isURL(this.#content.deploy.entrypoint)\n            // Backoff if entrypoint is URL, the user knows what they're doing\n            ? this.#content.deploy.entrypoint\n            // entrypoint must be stored relative to the config file\n            : relative(dirname(this.#path), this.#content.deploy.entrypoint)),\n      },\n    };\n    return JSON.stringify(content, null, 2);\n  }\n\n  path() {\n    return this.#path;\n  }\n\n  args() {\n    return (this.#content.deploy ?? {});\n  }\n}\n\nexport default {\n  /** Read a `ConfigFile` from disk */\n  async read(\n    path: string | Iterable<string>,\n  ): Promise<ConfigFile | null> {\n    const paths = typeof path === \"string\" ? [path] : path;\n    for (const filepath of paths) {\n      let content;\n      try {\n        content = await Deno.readTextFile(filepath);\n      } catch {\n        // File not found, try next\n        continue;\n      }\n      try {\n        return ConfigFile.fromFileContent(filepath, content);\n      } catch (e) {\n        error(e);\n      }\n    }\n    // config file not found\n    return null;\n  },\n\n  /**\n   * Write `ConfigArgs` to the config file.\n   *\n   * @param path {string | null} path where to write the config file. If the file already exists and\n   *                             `override` is `true`, its content will be merged with the `args`\n   *                             argument. If null, will default to `DEFAULT_FILENAME`.\n   * @param args {ConfigArgs} args to be upserted into the config file.\n   * @param overwrite {boolean} control whether an existing config file should be overwritten.\n   */\n  maybeWrite: async function (\n    path: string | null,\n    args: ConfigArgs,\n    overwrite: boolean,\n  ): Promise<void> {\n    const pathOrDefault = path ?? DEFAULT_FILENAME;\n    const isJsonc = extname(pathOrDefault) === \".jsonc\";\n    const existingConfig = await this.read(pathOrDefault);\n    const changes = existingConfig?.diff(args) ?? [];\n    let config;\n    if (existingConfig && changes.length === 0) {\n      // There are no changes to write\n      return;\n    } else if (\n      existingConfig && existingConfig.hasDeployConfig() && !overwrite\n    ) {\n      // There are changes to write and there's already some deploy config, we require the --save-config flag\n      wait(\"\").start().info(\n        `Some of the config used differ from the config found in '${existingConfig.path()}'. Use --save-config to overwrite it.`,\n      );\n      return;\n    } else if (existingConfig) {\n      // Either there is no deploy config in the config file or the user is using --save-config flag\n      if (isJsonc) {\n        const msg = overwrite\n          ? `Writing to the config file '${pathOrDefault}' will remove any existing comment and format it as a plain JSON file. Is that ok?`\n          : `I want to store some configuration in '${pathOrDefault}' config file but this will remove any existing comment from it. Is that ok?`;\n        const confirmation = confirm(`${magenta(\"?\")} ${msg}`);\n        if (!confirmation) {\n          const formattedChanges = existingConfig.hasDeployConfig()\n            ? cyan(\n              `  \"deploy\": {\\n     ...\\n${formatChanges(changes, 2, 2)}\\n  }`,\n            )\n            : green(\n              ConfigFile.create(pathOrDefault, args).toFileContent().slice(\n                2,\n                -2,\n              ),\n            );\n          wait({ text: \"\", indent: 3 }).start().info(\n            `I understand. Here's the config I wanted to write:\\n${formattedChanges}`,\n          );\n          return;\n        }\n      }\n      existingConfig.override(args);\n      config = existingConfig;\n    } else {\n      // The config file does not exist. Create a new one.\n      config = ConfigFile.create(pathOrDefault, args);\n    }\n    await Deno.writeTextFile(\n      config.path(),\n      (config satisfies ConfigFile).toFileContent(),\n    );\n    wait(\"\").start().succeed(\n      `${\n        existingConfig ? \"Updated\" : \"Created\"\n      } config file '${config.path()}'.`,\n    );\n  },\n\n  cwdOrAncestors: function* () {\n    let wd = Deno.cwd();\n    while (wd) {\n      for (const filename of CANDIDATE_FILENAMES) {\n        yield join(wd, filename);\n      }\n      const newWd = dirname(wd);\n      if (newWd === wd) {\n        return;\n      } else {\n        wd = newWd;\n      }\n    }\n  },\n};\n\nfunction formatChanges(\n  changes: Change[],\n  indent?: number,\n  gap?: number,\n): string {\n  const removals = [];\n  const additions = [];\n  const padding = \" \".repeat(indent ?? 0);\n  const innerPadding = \" \".repeat(gap ?? 0);\n  for (const { key, removal, addition } of changes) {\n    if (removal !== undefined) {\n      removals.push(red(\n        `${padding}-${innerPadding}\"${key}\": ${JSON.stringify(removal)}`,\n      ));\n    }\n    if (addition !== undefined) {\n      additions.push(green(\n        `${padding}+${innerPadding}\"${key}\": ${JSON.stringify(addition)}`,\n      ));\n    }\n  }\n  return [removals.join(red(\",\\n\")), additions.join(green(\",\\n\"))].join(\"\\n\")\n    .trim();\n}\n\ninterface Change {\n  key: string;\n  removal?: unknown;\n  addition?: unknown;\n}\n"
  },
  {
    "path": "src/config_inference.ts",
    "content": "// Copyright 2021 Deno Land Inc. All rights reserved. MIT license.\n\nimport { magenta } from \"@std/fmt/colors\";\nimport { basename } from \"@std/path/basename\";\nimport { API, APIError, endpoint } from \"./utils/api.ts\";\nimport TokenProvisioner from \"./utils/access_token.ts\";\nimport { wait } from \"./utils/spinner.ts\";\nimport { error } from \"./error.ts\";\nimport organization from \"./utils/organization.ts\";\n\nconst NONAMES = [\"src\", \"lib\", \"code\", \"dist\", \"build\", \"shared\", \"public\"];\n\n/** Arguments inferred from context */\ninterface InferredArgs {\n  project?: string;\n  entrypoint?: string;\n  exclude: string[];\n  include: string[];\n}\n\n/**\n * Infer name of the project.\n *\n * The name of the project is inferred from either of the following options, in order:\n * - If the project is in a git repo, infer `<org-name>-<repo-name>`\n * - Otherwise, use the directory name from where DeployCTL is being executed,\n *   unless the name is useless like \"src\" or \"dist\".\n */\nasync function inferProject(api: API, dryRun: boolean, orgName?: string) {\n  wait(\"\").start().warn(\n    \"No project name or ID provided with either the --project arg or a config file.\",\n  );\n  let projectName = await inferProjectFromOriginUrl() || inferProjectFromCWD();\n  if (!projectName) {\n    return;\n  }\n  if (dryRun) {\n    wait(\"\").start().succeed(\n      `Guessed project name '${projectName}'.`,\n    );\n    wait({ text: \"\", indent: 3 }).start().info(\n      \"This is a dry run. In a live run the guessed name might be different if this one is invalid or already used.\",\n    );\n    return projectName;\n  }\n  const org = orgName\n    ? await organization.getByNameOrCreate(api, orgName)\n    : null;\n  for (;;) {\n    let spinner;\n    if (projectName) {\n      spinner = wait(\n        `Guessing project name '${projectName}': creating project...`,\n      ).start();\n    } else {\n      spinner = wait(\"Creating new project with a random name...\").start();\n    }\n    try {\n      const project = await api.createProject(projectName, org?.id);\n      if (projectName) {\n        spinner.succeed(\n          `Guessed project name '${project.name}'.`,\n        );\n      } else {\n        spinner.succeed(`Created new project '${project.name}'`);\n      }\n      wait({ text: \"\", indent: 3 }).start().info(\n        `You can always change the project name with 'deployctl projects rename new-name' or in ${endpoint()}/projects/${project.name}/settings`,\n      );\n      return project.name;\n    } catch (e) {\n      if (e instanceof APIError && e.code == \"projectNameInUse\") {\n        spinner.stop();\n        spinner = wait(\n          `Guessing project name '${projectName}': this project name is already used. Checking ownership...`,\n        ).start();\n        const hasAccess = projectName &&\n          (await api.getProject(projectName)) !== null;\n        if (hasAccess) {\n          spinner.stop();\n          const confirmation = confirm(\n            `${\n              magenta(\"?\")\n            } Guessing project name '${projectName}': you already own this project. Should I deploy to it?`,\n          );\n          if (confirmation) {\n            return projectName;\n          }\n        }\n        projectName = `${projectName}-${Math.floor(Math.random() * 100)}`;\n        spinner.stop();\n      } else if (e instanceof APIError && e.code == \"slugInvalid\") {\n        // Fallback to random name given by the API\n        projectName = undefined;\n        spinner.stop();\n      } else {\n        spinner.fail(\n          `Guessing project name '${projectName}': Creating project...`,\n        );\n        error(e);\n      }\n    }\n  }\n}\n\nasync function inferProjectFromOriginUrl() {\n  let originUrl = await getOriginUrlUsingGitCmd();\n  if (!originUrl) {\n    originUrl = await getOriginUrlUsingFS();\n  }\n  if (!originUrl) {\n    return;\n  }\n  const result = originUrl.match(\n    /[:\\/]+(?<org>[^\\/]+)\\/(?<repo>[^\\/]+?)(?:\\.git)?$/,\n  )?.groups;\n  if (result) {\n    return `${result.org}-${result.repo}`;\n  }\n}\n\nfunction inferProjectFromCWD() {\n  const projectName = basename(Deno.cwd())\n    .toLowerCase()\n    .replaceAll(/[\\s_]/g, \"-\")\n    .replaceAll(/[^a-z,A-Z,-]/g, \"\")\n    .slice(0, 26);\n  if (NONAMES.every((n) => n !== projectName)) {\n    return projectName;\n  }\n}\n\n/** Try getting the origin remote URL using the git command */\nasync function getOriginUrlUsingGitCmd(): Promise<string | undefined> {\n  try {\n    const cmd = await new Deno.Command(\"git\", {\n      args: [\"remote\", \"get-url\", \"origin\"],\n    }).output();\n    if (cmd.stdout.length !== 0) {\n      return new TextDecoder().decode(cmd.stdout).trim();\n    }\n  } catch (_) {\n    return;\n  }\n}\n\n/** Try getting the origin remote URL reading the .git/config file */\nasync function getOriginUrlUsingFS(): Promise<string | undefined> {\n  // We assume cwd is the root of the repo. We favor false-negatives over false-positives, and this\n  // is a last-resort fallback anyway\n  try {\n    const config: string = await Deno.readTextFile(\".git/config\");\n    const originSectionStart = config.indexOf('[remote \"origin\"]');\n    const originSectionEnd = config.indexOf(\"[\", originSectionStart + 1);\n    return config.slice(originSectionStart, originSectionEnd).match(\n      /url\\s*=\\s*(?<url>.+)/,\n    )\n      ?.groups\n      ?.url\n      ?.trim();\n  } catch {\n    return;\n  }\n}\n\nconst ENTRYPOINT_PATHS = [\"main\", \"index\", \"src/main\", \"src/index\"];\nconst ENTRYPOINT_EXTENSIONS = [\"ts\", \"js\", \"tsx\", \"jsx\"];\n\n/**\n * Infer the entrypoint of the project\n *\n * The current algorithm infers the entrypoint if one and only one of the following\n * files is found:\n * - main.[tsx|ts|jsx|js]\n * - index.[tsx|ts|jsx|js]\n * - src/main.[tsx|ts|jsx|js]\n * - src/index.[tsx|ts|jsx|js]\n */\nasync function inferEntrypoint() {\n  const candidates = [];\n  for (const path of ENTRYPOINT_PATHS) {\n    for (const extension of ENTRYPOINT_EXTENSIONS) {\n      candidates.push(present(`${path}.${extension}`));\n    }\n  }\n  const candidatesPresent = (await Promise.all(candidates)).filter((c) =>\n    c !== undefined\n  );\n  if (candidatesPresent.length === 1) {\n    return candidatesPresent[0];\n  } else {\n    return;\n  }\n}\n\nasync function present(path: string): Promise<string | undefined> {\n  try {\n    await Deno.lstat(path);\n    return path;\n  } catch {\n    return;\n  }\n}\n\nexport default async function inferConfig(\n  args: InferredArgs & {\n    token?: string;\n    help?: boolean;\n    version?: boolean;\n    \"dry-run\"?: boolean;\n    org?: string;\n  },\n) {\n  if (args.help || args.version) {\n    return;\n  }\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  if (args.project === undefined) {\n    args.project = await inferProject(api, !!args[\"dry-run\"], args.org);\n  }\n\n  if (args.entrypoint === undefined) {\n    args.entrypoint = await inferEntrypoint();\n    if (args.entrypoint) {\n      wait(\"\").start().warn(\n        `No entrypoint provided with either the --entrypoint arg or a config file. I've guessed '${args.entrypoint}' for you.`,\n      );\n      wait({ text: \"\", indent: 3 }).start().info(\n        \"Is this wrong? Please let us know in https://github.com/denoland/deployctl/issues/new\",\n      );\n    }\n  }\n\n  if (!args.include.some((i) => i.includes(\"node_modules\"))) {\n    args.exclude.push(\"**/node_modules\");\n  }\n}\n"
  },
  {
    "path": "src/error.ts",
    "content": "// Copyright 2024 Deno Land Inc. All rights reserved. MIT license.\n\nimport { bold, red } from \"@std/fmt/colors\";\n\nexport function error(err: unknown): never {\n  const message = stringify(err);\n  console.error(red(`${bold(\"error\")}: ${message}`));\n  Deno.exit(1);\n}\n\nexport type StringifyOptions = {\n  verbose: boolean;\n};\n\nconst DEFAULT_STRINGIFY_OPTIONS: StringifyOptions = {\n  verbose: false,\n};\n\nexport function stringify(\n  err: unknown,\n  options?: Partial<StringifyOptions>,\n): string {\n  const opts = options === undefined\n    ? DEFAULT_STRINGIFY_OPTIONS\n    : { ...DEFAULT_STRINGIFY_OPTIONS, ...options };\n\n  if (err instanceof Error) {\n    if (opts.verbose) {\n      return stringifyErrorLong(err);\n    } else {\n      return stringifyErrorShort(err);\n    }\n  }\n\n  if (typeof err === \"string\") {\n    return err;\n  }\n\n  return JSON.stringify(err);\n}\n\nfunction stringifyErrorShort(err: Error): string {\n  return `${err.name}: ${err.message}`;\n}\n\nfunction stringifyErrorLong(err: Error): string {\n  const cause = err.cause === undefined\n    ? \"\"\n    : `\\nCaused by ${stringify(err.cause, { verbose: true })}`;\n\n  if (!err.stack) {\n    return `${err.name}: ${err.message}${cause}`;\n  }\n\n  return `${err.stack}${cause}`;\n}\n"
  },
  {
    "path": "src/error_test.ts",
    "content": "// Copyright 2024 Deno Land Inc. All rights reserved. MIT license.\n\nimport { stringify } from \"./error.ts\";\nimport { assert, assertEquals, assertStringIncludes } from \"@std/assert\";\n\nDeno.test(\"stringify string\", () => {\n  assertEquals(stringify(\"test\"), \"test\");\n});\n\nDeno.test(\"stringify number\", () => {\n  assertEquals(stringify(42), \"42\");\n});\n\nDeno.test(\"stringify object\", () => {\n  assertEquals(stringify({ foo: 42 }), '{\"foo\":42}');\n});\n\nDeno.test(\"stringify Error (verbose: false)\", () => {\n  const got = stringify(new Error(\"boom\"));\n  assertEquals(got, \"Error: boom\");\n});\n\nDeno.test(\"stringify Error (verbose: true)\", () => {\n  const got = stringify(new Error(\"boom\"), { verbose: true });\n  assert(got.startsWith(\"Error: boom\\n    at \"), `assert failed: ${got}`);\n});\n\nDeno.test(\"stringify Error with cause (cause is also Error) (verbose: false)\", () => {\n  const e1 = new TypeError(\"e1\");\n  const e2 = new SyntaxError(\"e2\", { cause: e1 });\n  const got = stringify(e2);\n  assertEquals(got, \"SyntaxError: e2\");\n});\n\nDeno.test(\"stringify Error with cause (cause is also Error) (verbose: true)\", () => {\n  const e1 = new TypeError(\"e1\");\n  const e2 = new SyntaxError(\"e2\", { cause: e1 });\n  const got = stringify(e2, { verbose: true });\n\n  assert(\n    got.startsWith(\"SyntaxError: e2\\n    at \"),\n    `assert failed: ${got}`,\n  );\n  assertStringIncludes(got, \"Caused by TypeError: e1\\n    at \");\n});\n\nDeno.test(\"stringify Error with cause (cause is number) (verbose: false)\", () => {\n  const e = new Error(\"e\", { cause: 42 });\n  const got = stringify(e);\n  assertEquals(got, \"Error: e\");\n});\n\nDeno.test(\"stringify Error with cause (cause is number) (verbose: true)\", () => {\n  const e = new Error(\"e\", { cause: 42 });\n  const got = stringify(e, { verbose: true });\n\n  assert(\n    got.startsWith(\"Error: e\\n    at \"),\n    `assert failed: ${got}`,\n  );\n\n  assert(\n    got.endsWith(\"Caused by 42\"),\n    `assert failed: ${got}`,\n  );\n});\n"
  },
  {
    "path": "src/subcommands/api.ts",
    "content": "import type { Args } from \"../args.ts\";\nimport { API } from \"../utils/mod.ts\";\nimport TokenProvisioner from \"../utils/access_token.ts\";\nimport { error } from \"../error.ts\";\nimport { wait } from \"../utils/spinner.ts\";\n\nconst help = `Perform API calls to any endpoint of the Deploy API (ALPHA)\n\nEXAMPLES:\n\nGet the details of an organization:\n\n    deployctl api organizations/04f19625-35d3-4c05-857e-bcaa3b0af374\n\nCreate a project in an organization:\n\n    deployctl api --method=POST --body='{\"name\": \"my-project\"}' organizations/04f19625-35d3-4c05-857e-bcaa3b0af374/projects\n\nYou can find the specification of the API in https://apidocs.deno.com\n\nUSAGE:\n    deployctl api [OPTIONS] <ENDPOINT>\n\nOPTIONS:\n    -h, --help                   Prints this help information\n        --method=<HTTP-METHOD>   HTTP method to use (defaults to GET)\n        --body=<JSON>            Body of the request. The provided string is sent as is to the API\n        --format=<overview|body> Output an overview of the response with the headers and the (possibly truncated) body, or just the body (verbatim). \n                                 Defaults to 'overview' when stdout is a tty, and 'body' otherwise.  \n        --token=<TOKEN>          The API token to use (defaults to auto-provisioned token)\n`;\n\nexport default async function (args: Args): Promise<void> {\n  if (args.help) {\n    console.log(help);\n    Deno.exit(0);\n  }\n  let endpoint = args._.shift()?.toString();\n  if (!endpoint) {\n    error(\n      \"Missing endpoint positional argument. USAGE: deployctl api <endpoint>\",\n    );\n  }\n\n  let format: \"overview\" | \"body\";\n  switch (args.format) {\n    case \"overview\":\n    case \"body\":\n      format = args.format;\n      break;\n    case undefined:\n      format = Deno.stdout.isTerminal() ? \"overview\" : \"body\";\n      break;\n    default:\n      error(\n        `Invalid format '${args.format}'. Supported values for the --format option are 'overview' or 'body'`,\n      );\n  }\n\n  if (!endpoint.startsWith(\"/\")) {\n    endpoint = `/${endpoint}`;\n  }\n  if (!/^\\/v\\d+\\//.test(endpoint)) {\n    endpoint = `/v1${endpoint}`;\n  }\n  const method = (args.method || \"GET\").toUpperCase();\n  const spinner = wait(`Requesting API endpoint '${endpoint}'...`).start();\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  try {\n    const response = await api.request(endpoint, {\n      method,\n      body: args.body,\n    });\n    spinner.succeed(`Received response from the API`);\n    switch (format) {\n      case \"overview\": {\n        const body = response.headers.get(\"Content-Type\") === \"application/json\"\n          ? await response.json()\n          : await response.text();\n        const headers = response.headers;\n        console.log(\"-----[ HEADERS ]-----\");\n        console.log(method, response.url);\n        console.log(\"Status:\", response.status);\n        console.log(headers);\n        console.log(\"-----[ BODY ]--------\");\n        console.log(body);\n        break;\n      }\n      case \"body\": {\n        console.log(await response.text());\n        break;\n      }\n    }\n  } catch (err) {\n    error(err);\n  }\n}\n"
  },
  {
    "path": "src/subcommands/deploy.ts",
    "content": "// Copyright 2021 Deno Land Inc. All rights reserved. MIT license.\n\nimport type { Spinner } from \"@denosaurs/wait\";\nimport { fromFileUrl } from \"@std/path/from_file_url\";\nimport { envVarsFromArgs } from \"../utils/env_vars.ts\";\nimport { wait } from \"../utils/spinner.ts\";\nimport configFile from \"../config_file.ts\";\nimport { error } from \"../error.ts\";\nimport { API, APIError, endpoint } from \"../utils/api.ts\";\nimport type { ManifestEntry } from \"../utils/api_types.ts\";\nimport { parseEntrypoint } from \"../utils/entrypoint.ts\";\nimport {\n  containsEntryInManifest,\n  convertPatternToRegExp,\n  walk,\n} from \"../utils/manifest.ts\";\nimport TokenProvisioner from \"../utils/access_token.ts\";\nimport type { Args as RawArgs } from \"../args.ts\";\nimport organization from \"../utils/organization.ts\";\nimport { relative } from \"@std/path/relative\";\nimport { yellow } from \"@std/fmt/colors\";\n\nconst help = `deployctl deploy\nDeploy a script with static files to Deno Deploy.\n\nBasic usage:\n\n    deployctl deploy\n\nBy default, deployctl will guess the project name based on the Git repo or directory it is in.\nSimilarly, it will guess the entrypoint by looking for files with common entrypoint names (main.ts, src/main.ts, etc).\nAfter the first deployment, the settings used will be stored in a config file (by default deno.json). \n\nYou can specify the project name and/or the entrypoint using the --project and --entrypoint arguments respectively:\n\n    deployctl deploy --project=helloworld --entrypoint=src/entrypoint.ts\n\nBy default, deployctl deploys all the files in the current directory (recursively, except node_modules directories).\nYou can customize this behaviour using the --include and --exclude arguments (also supported in the\nconfig file). Here are some examples:\n\n- Include only source and static files:\n\n    deployctl deploy --include=./src --include=./static\n\n- Include only Typescript files:\n\n    deployctl deploy --include=**/*.ts\n\n- Exclude local tooling and artifacts\n\n    deployctl deploy --exclude=./tools --exclude=./benches\n\nA common pitfall is to not include the source code modules that need to be run (entrypoint and dependencies).\nThe following example will fail because main.ts is not included:\n\n    deployctl deploy --include=./static --entrypoint=./main.ts\n\nThe entrypoint can also be a remote script. A common use case for this is to deploy an static site\nusing std/http/file_server.ts (more details in https://docs.deno.com/deploy/tutorials/static-site ):\n\n    deployctl deploy --entrypoint=jsr:@std/http/file_server\n\nYou can set env variables for deployments to have access using Deno.env. You can use --env to set individual\nenvironment variables, or --env-file to load one or more environment files. These options can be combined\nand used multiple times:\n\n    deployctl deploy --env-file --env-file=.other-env --env=DEPLOYMENT_TS=$(date +%s)\n\nBe aware that the env variables set with --env and --env-file are merged with the env variables configured for the project.\nIf this does not suit your needs, please report your feedback at\nhttps://github.com/denoland/deploy_feedback/issues/\n\nUSAGE:\n    deployctl deploy [OPTIONS] [<ENTRYPOINT>]\n\nOPTIONS:\n        --exclude=<PATH[,PATH]>     Prevent the upload of these comma-separated paths. Can be used multiple times. Globs are supported\n        --include=<PATH[,PATH]>     Only upload files in these comma-separated paths. Can be used multiple times. Globs are supported\n        --import-map=<PATH>         Path to the import map file to use.\n    -h, --help                      Prints this help information\n        --prod                      Create a production deployment (default is preview deployment except the first deployment)\n    -p, --project=<NAME|ID>         The project in which to deploy. If it does not exist yet, it will be created (see --org).\n        --org=<ORG>                 The organization in which to create the project. Defaults to the user's personal organization\n        --entrypoint=<PATH|URL>     The file that Deno Deploy will run. Also available as positional argument, which takes precedence\n        --env=<KEY=VALUE>           Set individual environment variables in a KEY=VALUE format. Can be used multiple times\n        --env-file[=FILE]           Set environment variables using a dotenv file. If the file name is not provided, defaults to '.env'. Can be used multiple times\n        --token=<TOKEN>             The API token to use (defaults to DENO_DEPLOY_TOKEN env var)\n        --dry-run                   Dry run the deployment process.\n        --config=<PATH>             Path to the file from where to load DeployCTL config. Defaults to 'deno.json'\n        --save-config               Persist the arguments used into the DeployCTL config file\n        --color=<auto|always|never> Enable or disable colored output. Defaults to 'auto' (colored when stdout is a tty)\n`;\n\nexport interface Args {\n  help: boolean;\n  static: boolean;\n  prod: boolean;\n  exclude: string[];\n  include: string[];\n  token: string | null;\n  project: string | null;\n  org?: string;\n  entrypoint: string | null;\n  importMap: string | null;\n  dryRun: boolean;\n  config: string | null;\n  saveConfig: boolean;\n}\n\nexport default async function (rawArgs: RawArgs): Promise<void> {\n  const positionalEntrypoint: string | null = typeof rawArgs._[0] === \"string\"\n    ? rawArgs._[0]\n    : null;\n  const args: Args = {\n    help: !!rawArgs.help,\n    static: !!rawArgs.static,\n    prod: !!rawArgs.prod,\n    token: rawArgs.token ? String(rawArgs.token) : null,\n    project: rawArgs.project ? String(rawArgs.project) : null,\n    org: rawArgs.org,\n    entrypoint: positionalEntrypoint !== null\n      ? positionalEntrypoint\n      : rawArgs[\"entrypoint\"]\n      ? String(rawArgs[\"entrypoint\"])\n      : null,\n    importMap: rawArgs[\"import-map\"] ? String(rawArgs[\"import-map\"]) : null,\n    exclude: rawArgs.exclude.flatMap((e) => e.split(\",\")),\n    include: rawArgs.include.flatMap((i) => i.split(\",\")),\n    dryRun: !!rawArgs[\"dry-run\"],\n    config: rawArgs.config ? String(rawArgs.config) : null,\n    saveConfig: !!rawArgs[\"save-config\"],\n  };\n\n  if (args.help) {\n    console.log(help);\n    Deno.exit(0);\n  }\n  if (args.entrypoint === null) {\n    error(\n      \"Unable to guess the entrypoint of this project. Use the --entrypoint argument to provide one.\",\n    );\n  }\n  if (rawArgs._.length > 1) {\n    error(\"Too many positional arguments given.\");\n  }\n  if (args.project === null) {\n    error(\n      \"Unable to guess a project name for this project. Use the --project argument to provide one.\",\n    );\n  }\n\n  const opts = {\n    entrypoint: args.entrypoint,\n    importMapUrl: args.importMap === null\n      ? null\n      : await parseEntrypoint(args.importMap, undefined, \"import map\")\n        .catch((e) => error(e)),\n    static: args.static,\n    prod: args.prod,\n    token: args.token,\n    project: args.project,\n    org: args.org,\n    include: args.include,\n    exclude: args.exclude,\n    dryRun: args.dryRun,\n    config: args.config,\n    saveConfig: args.saveConfig,\n    envVars: await envVarsFromArgs(rawArgs),\n  };\n\n  await deploy(opts);\n}\n\ninterface DeployOpts {\n  entrypoint: string;\n  importMapUrl: URL | null;\n  static: boolean;\n  prod: boolean;\n  exclude: string[];\n  include: string[];\n  token: string | null;\n  project: string;\n  org?: string;\n  dryRun: boolean;\n  config: string | null;\n  saveConfig: boolean;\n  envVars: Record<string, string> | null;\n}\n\nasync function deploy(opts: DeployOpts): Promise<void> {\n  let url = await parseEntrypoint(opts.entrypoint).catch(error);\n  if (opts.dryRun) {\n    wait(\"\").start().info(\"Performing dry run of deployment\");\n  }\n  const projectInfoSpinner = wait(\n    `Fetching project '${opts.project}' information...`,\n  ).start();\n  const api = opts.token\n    ? API.fromToken(opts.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  let projectIsEmpty = false;\n  let project = await api.getProject(opts.project);\n  if (project === null) {\n    const org = opts.org\n      ? await organization.getByNameOrCreate(api, opts.org)\n      : null;\n    projectInfoSpinner.stop();\n    const projectCreationSpinner = wait(\n      `Project '${opts.project}' not found. Creating...`,\n    ).start();\n    try {\n      project = await api.createProject(opts.project, org?.id);\n    } catch (e) {\n      error(e);\n    }\n    projectCreationSpinner.succeed(`Created new project '${opts.project}'.`);\n    wait({ text: \"\", indent: 3 }).start().info(\n      `You can configure the name, env vars, custom domains and more in ${endpoint()}/projects/${project.name}/settings`,\n    );\n    projectIsEmpty = true;\n  } else {\n    if (opts.org && project.organization.name === null) {\n      projectInfoSpinner.fail(\n        `The project is in your personal organization and you requested the org '${opts.org}' in the args`,\n      );\n      Deno.exit(1);\n    } else if (opts.org && project.organization.name !== opts.org) {\n      projectInfoSpinner.fail(\n        `The project is in the organization '${project.organization.name}' and you requested the org '${opts.org}' in the args`,\n      );\n      Deno.exit(1);\n    }\n    const buildsPage = await api.listDeployments(project.id, 0, 1);\n    if (buildsPage === null) {\n      projectInfoSpinner.fail(\"Project deployments details not found.\");\n      return Deno.exit(1);\n    }\n    projectInfoSpinner.succeed(`Deploying to project ${project.name}.`);\n\n    if (buildsPage.list.length === 0) {\n      projectIsEmpty = true;\n    }\n  }\n\n  if (projectIsEmpty) {\n    opts.prod = true;\n    wait({ text: \"\", indent: 3 }).start().info(\n      \"The project does not have a deployment yet. Automatically pushing initial deployment to production (use --prod for further updates).\",\n    );\n  }\n\n  const cwd = Deno.cwd();\n  if (url.protocol === \"file:\") {\n    const path = fromFileUrl(url);\n    if (!path.startsWith(cwd)) {\n      wait(\"\").start().fail(`Entrypoint: ${path}`);\n      error(\"Entrypoint must be in the current working directory.\");\n    } else {\n      wait(\"\").start().succeed(`Entrypoint: ${path}`);\n    }\n    const entrypoint = path.slice(cwd.length);\n    url = new URL(`file:///src${entrypoint}`);\n  }\n  let importMapUrl = opts.importMapUrl;\n  if (importMapUrl && importMapUrl.protocol === \"file:\") {\n    const path = fromFileUrl(importMapUrl);\n    if (!path.startsWith(cwd)) {\n      error(\"Import map must be in the current working directory.\");\n    }\n    const entrypoint = path.slice(cwd.length);\n    importMapUrl = new URL(`file:///src${entrypoint}`);\n  }\n\n  let uploadSpinner: Spinner | null = null;\n  const files = [];\n  let manifest: { entries: Record<string, ManifestEntry> } | undefined =\n    undefined;\n\n  if (opts.static) {\n    wait(\"\").start().info(`Uploading all files from the current dir (${cwd})`);\n    const assetSpinner = wait(\"Finding static assets...\").start();\n    const include = opts.include.map(convertPatternToRegExp);\n    const exclude = opts.exclude.map(convertPatternToRegExp);\n    const { manifestEntries: entries, hashPathMap: assets } = await walk(\n      cwd,\n      cwd,\n      { include, exclude },\n    );\n    assetSpinner.succeed(\n      `Found ${assets.size} asset${assets.size === 1 ? \"\" : \"s\"}.`,\n    );\n\n    // If the import map is specified but not in the manifest, error out.\n    if (\n      opts.importMapUrl !== null &&\n      !containsEntryInManifest(\n        entries,\n        relative(cwd, fromFileUrl(opts.importMapUrl)),\n      )\n    ) {\n      error(\n        `Import map ${opts.importMapUrl} not found in the assets to be uploaded. Please check --include and --exclude options to make sure the import map is included.`,\n      );\n    }\n\n    // If the config file is present but not in the manifest, show a warning\n    // that any import map settings in the config file will not be used.\n    if (\n      opts.importMapUrl === null && opts.config !== null &&\n      !containsEntryInManifest(\n        entries,\n        relative(cwd, opts.config),\n      )\n    ) {\n      wait(\"\").start().warn(\n        yellow(\n          `Config file ${opts.config} not found in the assets to be uploaded; any import map settings in the config file will not be applied during deployment. If this is not your intention, please check --include and --exclude options to make sure the config file is included.`,\n        ),\n      );\n    }\n\n    uploadSpinner = wait(\"Determining assets to upload...\").start();\n    const neededHashes = await api.projectNegotiateAssets(project.id, {\n      entries,\n    });\n\n    for (const hash of neededHashes) {\n      const path = assets.get(hash);\n      if (path === undefined) {\n        error(`Asset ${hash} not found.`);\n      }\n      const data = await Deno.readFile(path);\n      files.push(data);\n    }\n    if (files.length === 0) {\n      uploadSpinner.succeed(\"No new assets to upload.\");\n      uploadSpinner = null;\n    } else {\n      uploadSpinner.text = `${files.length} new asset${\n        files.length === 1 ? \"\" : \"s\"\n      } to upload.`;\n    }\n\n    manifest = { entries };\n  }\n\n  if (opts.dryRun) {\n    uploadSpinner?.succeed(uploadSpinner?.text);\n    return;\n  }\n\n  let deploySpinner: Spinner | null = null;\n  const req = {\n    url: url.href,\n    importMapUrl: importMapUrl ? importMapUrl.href : null,\n    production: opts.prod,\n    manifest,\n  };\n  const progress = await api.pushDeploy(project.id, req, files);\n  try {\n    for await (const event of progress) {\n      switch (event.type) {\n        case \"staticFile\": {\n          const percentage = (event.currentBytes / event.totalBytes) * 100;\n          uploadSpinner!.text = `Uploading ${files.length} asset${\n            files.length === 1 ? \"\" : \"s\"\n          }... (${percentage.toFixed(1)}%)`;\n          break;\n        }\n        case \"load\": {\n          if (uploadSpinner) {\n            uploadSpinner.succeed(\n              `Uploaded ${files.length} new asset${\n                files.length === 1 ? \"\" : \"s\"\n              }.`,\n            );\n            uploadSpinner = null;\n          }\n          if (deploySpinner === null) {\n            deploySpinner = wait(\"Deploying...\").start();\n          }\n          const progress = event.seen / event.total * 100;\n          deploySpinner.text = `Deploying... (${progress.toFixed(1)}%)`;\n          break;\n        }\n        case \"uploadComplete\":\n          deploySpinner!.text = `Finishing deployment...`;\n          break;\n        case \"success\": {\n          let domains;\n          if (opts.envVars) {\n            deploySpinner!.text = \"Setting environment variables...\";\n            // Hack while Deno Deploy implements settings env variables during deployment_with_assets\n            const redeployed = await api.redeployDeployment(event.id, {\n              prod: opts.prod,\n              env_vars: opts.envVars,\n            });\n            // NULL SAFETY: deployment was just created\n            domains = redeployed!.domains;\n            await api.deleteDeployment(event.id);\n          } else {\n            domains = event.domainMappings.map((m) => m.domain);\n          }\n          const deploymentKind = opts.prod ? \"Production\" : \"Preview\";\n          deploySpinner!.succeed(`${deploymentKind} deployment complete.`);\n\n          // We want to store the project id even if user provided project name\n          // to facilitate project renaming.\n          opts.project = project.id;\n          await configFile.maybeWrite(opts.config, opts, opts.saveConfig);\n          console.log(\"\\nView at:\");\n          for (const domain of domains) {\n            console.log(` - https://${domain}`);\n          }\n          break;\n        }\n        case \"error\":\n          if (uploadSpinner) {\n            uploadSpinner.fail(`Upload failed.`);\n            uploadSpinner = null;\n          }\n          if (deploySpinner) {\n            deploySpinner.fail(`Deployment failed.`);\n            deploySpinner = null;\n          }\n          error(event.ctx);\n      }\n    }\n  } catch (err: unknown) {\n    if (err instanceof APIError) {\n      if (uploadSpinner) {\n        uploadSpinner.fail(`Upload failed.`);\n        uploadSpinner = null;\n      }\n      if (deploySpinner) {\n        deploySpinner.fail(`Deployment failed.`);\n        deploySpinner = null;\n      }\n      error(err.toString());\n    }\n    error(String(err));\n  }\n}\n"
  },
  {
    "path": "src/subcommands/deployments.ts",
    "content": "import type { Args } from \"../args.ts\";\nimport { API, endpoint } from \"../utils/api.ts\";\nimport TokenProvisioner from \"../utils/access_token.ts\";\nimport { envVarsFromArgs } from \"../utils/env_vars.ts\";\nimport { wait } from \"../utils/spinner.ts\";\nimport type {\n  Build,\n  BuildsPage,\n  Cron,\n  Database,\n  DeploymentProgressError,\n  Organization,\n  Project,\n} from \"../utils/api_types.ts\";\nimport {\n  bold,\n  cyan,\n  green,\n  magenta,\n  red,\n  stripAnsiCode,\n  yellow,\n} from \"@std/fmt/colors\";\nimport type { Spinner } from \"@denosaurs/wait\";\nimport * as tty from \"@denosaurs/tty\";\nimport { fromFileUrl } from \"@std/path/from_file_url\";\nimport { error } from \"../error.ts\";\nimport { renderCron } from \"../utils/crons.ts\";\nimport { renderTimeDelta } from \"../utils/time.ts\";\n\nconst help = `Manage deployments in Deno Deploy\n\n## SHOW\n\nThe \"deployments show\" subcommand is used to see all the details of a deployment.\n\nThe simplest form of the command will show the details of the production deployment of the project\nyou are currently in (project will be picked up from the config file):\n\n    deployctl deployments show\n\nAnd you can also navigate the list of deployments using --prev and --next. --prev will show you 1 deployment before the current production\ndeployment:\n\n    deployctl deployments show --prev\n\nTo see the deployment before that, you can either add another --prev, or use --prev=2:\n\n    deployctl deployments show --prev --prev\n\nYou can also see the production deployment of any project using --project:\n\n    deployctl deployments show --project=my-other-project\n\nOr just show the details of a specific deployment, of any project, using --id. This can also be combined with --prev and --next too:\n\n    deployctl deployments show --id=p63c39ck5feg --next\n\n## List\n\nThe \"deployments list\" subcommand is used to list the deployments of a project. \n\nThe simplest form of the command will list the first 20 deployments of the project you are currently\nin (project will be picked up from the config file):\n\n    deployctl deployments list\n\nYou can list the rest of the deployments using --page:\n\n    deployctl deployments list --page=2\n\nYou can specify the project to list deployments of with the --project option:\n\n    deployctl deployments list --project=my-other-project\n\n## Redeploy\n\nThe \"deployments redeploy\" subcommand creates a new deployment reusing the build of an existing deployment. \n\nOne important principle to understand when using Deno Deploy is that deployments are immutable. This\nincludes the source code but also the env vars, domain mappings*, the KV database, crons, etc. To\nchange any of these associated resources for an existing deployment, you must redeploy it. \n\nFor example, to promote a preview deployment to production, use the --prod option:\n\n    deployctl deployments redeploy --prod\n\nIf this is a GitHub deployment, it will have 2 databases, one for prod deployments and one for preview deployments.\nWhen promoting a preview deployment to prod, by default it will automatically switch also to the prod database.\nYou can control the database with the --db option:\n\n    deployctl deployments redeploy --prod --db=preview\n\nIf your organization has custom databases, you can also set them by UUID:\n\n    deployctl deployments redeploy --db=5261e096-f9aa-4b72-8440-1c2b5b553def\n\nLastly, environment variables can also be changed using the redeploy functionality. You can use --env to set individual\nenvironment variables, or --env-file to load one or more environment files:\n\n    deployctl deployments redeploy --env-file --env-file=.other-env --env=DEPLOYMENT_TS=$(date +%s)\n\nBe aware that when changing env variables, only the env variables set during the redeployment will be\nused by the new deployment. Currently the project env variables are ignored during redeployment. If\nthis does not suit your needs, please report your feedback at https://github.com/denoland/deploy_feedback/issues/\n\nUSAGE:\n    deployctl deployments <SUBCOMMAND> [OPTIONS]\n\nSUBCOMMANDS:\n    show [ID]     View details of a deployment. Specify the deployment with a positional argument or the --id option; otherwise, it will \n                  show the details of the current production deployment of the project specified in the config file or with the --project option.\n                  Use --next and --prev to fetch the deployments deployed after or before the specified (or production) deployment.\n    list          List the deployments of a project. Specify the project using --project. Pagination can be controlled with --page and --limit.\n    delete [ID]   Delete a deployment. Same options to select the deployment as the show subcommand apply (--id, --project, --next and --prev).\n    redeploy [ID] Create a new deployment reusing the build of an existing deployment. You can change various resources associated with the original\n                  deployment using the options --prod, --db, --env and --env-file\n\nOPTIONS:\n    -h, --help                      Prints this help information\n        --id=<deployment-id>        [show,delete,redeploy] Select a deployment by id.\n    -p, --project=<NAME|ID>         [show,delete,redeploy] Select the production deployment of a project. Ignored if combined with --id\n                                    [list] The project of which to list deployments.\n        --next[=pos]                [show,delete,redeploy] Modifier that selects a deployment deployed chronologically after the deployment selected with --id or --project\n                                    Can be used multiple times (--next --next is the same as --next=2)\n        --prev[=pos]                [show,delete,redeploy] Modifier that selects a deployment deployed chronologically before the deployment selected with --id or --project\n                                    Can be used multiple times (--prev --prev is the same as --prev=2)\n        --page=<num>                [list] Page of the deployments list to fetch\n        --limit=<num>               [list] Amount of deployments to include in the list\n        --prod                      [redeploy] Set the production domain mappings to the new deployment. If the project has prod/preview databases and --db is not set\n                                    this option also controls which database the new deployment uses.\n        --db=<prod|preview|UUID>    [redeploy] Set the database of the new deployment. If not set, will use the preview database if it is a preview deployment and the project\n                                    has a preview database, or production otherwise.\n        --env=<KEY=VALUE>           [redeploy] Set individual environment variables in a KEY=VALUE format. Can be used multiple times\n        --env-file[=FILE]           [redeploy] Set environment variables using a dotenv file. If the file name is not provided, defaults to '.env'. Can be used multiple times.\n        --format=<overview|json>    Output the deployment details in an overview or JSON-encoded. Defaults to 'overview' when stdout is a tty, and 'json' otherwise.\n        --token=<TOKEN>             The API token to use (defaults to DENO_DEPLOY_TOKEN env var)\n        --config=<PATH>             Path to the file from where to load DeployCTL config. Defaults to 'deno.json'\n        --color=<auto|always|never> Enable or disable colored output. Defaults to 'auto' (colored when stdout is a tty)\n        --force                     [delete] Automatically execute the command without waiting for confirmation.\n`;\n\nexport default async function (args: Args): Promise<void> {\n  if (args.help) {\n    console.log(help);\n    Deno.exit(0);\n  }\n  const subcommand = args._.shift();\n  switch (subcommand) {\n    case \"list\":\n      await listDeployments(args);\n      break;\n    case \"show\":\n      await showDeployment(args);\n      break;\n    case \"delete\":\n      await deleteDeployment(args);\n      break;\n    case \"redeploy\":\n      await redeployDeployment(args);\n      break;\n    default:\n      console.error(help);\n      Deno.exit(1);\n  }\n}\n\nasync function listDeployments(args: Args): Promise<void> {\n  if (!args.project) {\n    error(\n      \"No project specified. Use --project to specify the project of which to list the deployments\",\n    );\n  }\n  const relativeNext = args.next.reduce(\n    (prev, next) => prev + parseInt(next || \"1\"),\n    0,\n  );\n  if (Number.isNaN(relativeNext)) {\n    error(\"Value of --next must be a number\");\n  }\n  const relativePrev = args.prev.reduce(\n    (prev, next) => prev + parseInt(next || \"1\"),\n    0,\n  );\n  if (Number.isNaN(relativePrev)) {\n    error(\"Value of --prev must be a number\");\n  }\n  // User-facing page is 1-based. Paging in API is 0-based.\n  const page = parseInt(args.page || \"1\") + relativeNext - relativePrev;\n  if (Number.isNaN(page)) {\n    error(\"Value of --page must be a number\");\n  }\n  if (page < 1) {\n    error(`The page cannot be lower than 1. You asked for page '${page}'`);\n  }\n  const apiPage = page - 1;\n  const limit = args.limit ? parseInt(args.limit) : undefined;\n  if (Number.isNaN(limit)) {\n    error(\"Value of --limit must be a number\");\n  }\n  let format: \"overview\" | \"json\";\n  switch (args.format) {\n    case \"overview\":\n    case \"json\":\n      format = args.format;\n      break;\n    case undefined:\n      format = Deno.stdout.isTerminal() ? \"overview\" : \"json\";\n      break;\n    default:\n      error(\n        `Invalid format '${args.format}'. Supported values for the --format option are 'overview' or 'json'`,\n      );\n  }\n  const spinner = wait(\n    `Fetching page ${page} of the list of deployments of project '${args.project}'...`,\n  )\n    .start();\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  const [buildsPage, project, databases] = await Promise.all([\n    api.listDeployments(\n      args.project,\n      apiPage,\n      limit,\n    ),\n    api.getProject(args.project),\n    api.getProjectDatabases(args.project),\n  ]);\n  if (!buildsPage || !project || !databases) {\n    spinner.fail(\n      `The project '${args.project}' does not exist, or you don't have access to it`,\n    );\n    return Deno.exit(1);\n  }\n  spinner.succeed(\n    `Page ${page} of the list of deployments of the project '${args.project}' is ready`,\n  );\n\n  if (buildsPage.list.length === 0) {\n    wait(\"\").warn(`Page '${page}' is empty`);\n    return;\n  }\n\n  switch (format) {\n    case \"overview\":\n      renderListOverview(\n        api,\n        project,\n        databases,\n        buildsPage,\n      );\n      break;\n    case \"json\":\n      console.log(JSON.stringify(buildsPage.list));\n      break;\n  }\n}\n\n// TODO: Show if active (and maybe some stats?)\nasync function showDeployment(args: Args): Promise<void> {\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n\n  let [deploymentId, projectId, build, project]: [\n    string,\n    string | undefined,\n    Build | null | undefined,\n    Project | null | undefined,\n  ] = await resolveDeploymentId(\n    args,\n    api,\n  );\n  let databases: Database[] | null;\n  let crons: Cron[] | null;\n\n  const spinner = wait(`Fetching deployment '${deploymentId}' details...`)\n    .start();\n\n  // Need to fetch project because the build.project does not include productionDeployment\n  [build, project, databases, crons] = projectId\n    ? await Promise.all([\n      build ? Promise.resolve(build) : api.getDeployment(deploymentId),\n      project ? Promise.resolve(project) : api.getProject(projectId),\n      api.getProjectDatabases(projectId),\n      api.getDeploymentCrons(projectId, deploymentId),\n    ])\n    : await api.getDeployment(deploymentId).then(async (build) =>\n      build\n        ? [\n          build,\n          ...await Promise.all([\n            api.getProject(build.project.id),\n            api.getProjectDatabases(build.project.id),\n            api.getDeploymentCrons(build.project.id, deploymentId),\n          ]),\n        ]\n        : [null, null, null, null]\n    );\n\n  if (!build) {\n    spinner.fail(\n      `The deployment '${deploymentId}' does not exist, or you don't have access to it`,\n    );\n    return Deno.exit(1);\n  }\n  if (!project) {\n    spinner.fail(\n      `The project '${projectId}' does not exist, or you don't have access to it`,\n    );\n    return Deno.exit(1);\n  }\n  if (!databases) {\n    spinner.fail(\n      `Failed to fetch the databases of project '${projectId}'`,\n    );\n    return Deno.exit(1);\n  }\n  if (!crons) {\n    spinner.fail(\n      `Failed to fetch the crons of project '${projectId}'`,\n    );\n    return Deno.exit(1);\n  }\n  let organization = project.organization;\n  if (!organization.name && !organization.members) {\n    // project.organization does not incude members array, and we need it for naming personal orgs\n    organization = await api.getOrganizationById(organization.id);\n  }\n  spinner.succeed(\n    `The details of the deployment '${build.deploymentId}' are ready:`,\n  );\n\n  let format: \"overview\" | \"json\";\n  switch (args.format) {\n    case \"overview\":\n    case \"json\":\n      format = args.format;\n      break;\n    case undefined:\n      format = Deno.stdout.isTerminal() ? \"overview\" : \"json\";\n      break;\n    default:\n      error(\n        `Invalid format '${args.format}'. Supported values for the --format option are 'overview' or 'json'`,\n      );\n  }\n\n  switch (format) {\n    case \"overview\":\n      renderShowOverview(build, project, organization, databases, crons);\n      break;\n    case \"json\":\n      renderShowJson(build, project, organization, databases, crons);\n      break;\n  }\n}\n\nasync function deleteDeployment(args: Args): Promise<void> {\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  const [deploymentId, _projectId, _build, _project] =\n    await resolveDeploymentId(\n      args,\n      api,\n    );\n  const confirmation = args.force ? true : confirm(\n    `${\n      magenta(\"?\")\n    } Are you sure you want to delete the deployment '${deploymentId}'?`,\n  );\n  if (!confirmation) {\n    wait(\"\").fail(\"Delete canceled\");\n    return;\n  }\n  const spinner = wait(`Deleting deployment '${deploymentId}'...`).start();\n  const deleted = await api.deleteDeployment(deploymentId);\n  if (deleted) {\n    spinner.succeed(`Deployment '${deploymentId}' deleted successfully`);\n  } else {\n    spinner.fail(\n      `Deployment '${deploymentId}' not found, or you don't have access to it`,\n    );\n  }\n}\n\nasync function redeployDeployment(args: Args): Promise<void> {\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  let [deploymentId, mProjectId, mBuild, mProject]: [\n    string,\n    string | undefined,\n    Build | null | undefined,\n    Project | null | undefined,\n  ] = await resolveDeploymentId(\n    args,\n    api,\n  );\n  const spinnerPrep = wait(`Preparing redeployment of '${deploymentId}'...`)\n    .start();\n  let mDatabases;\n  [mBuild, mProject, mDatabases] = await Promise.all([\n    mBuild ? Promise.resolve(mBuild) : api.getDeployment(deploymentId),\n    mProject === undefined && mProjectId\n      ? api.getProject(mProjectId)\n      : undefined,\n    mProjectId ? api.getProjectDatabases(mProjectId) : undefined,\n  ]);\n  if (!mBuild) {\n    spinnerPrep.fail(\n      `The deployment '${deploymentId}' does not exist, or you don't have access to it`,\n    );\n    return Deno.exit(1);\n  }\n  const build = mBuild;\n  const projectId = build.project.id;\n  if (mProject === undefined || mDatabases === undefined) {\n    // We didn't have projectId before. Now we do\n    [mProject, mDatabases] = await Promise.all([\n      mProject ? Promise.resolve(mProject) : api.getProject(projectId),\n      mDatabases\n        ? Promise.resolve(mDatabases)\n        : api.getProjectDatabases(projectId),\n    ]);\n  }\n  if (!mProject) {\n    spinnerPrep.fail(\n      `The project '${projectId}' does not exist, or you don't have access to it`,\n    );\n    return Deno.exit(1);\n  }\n  const project = mProject;\n  const databases = mDatabases;\n\n  const alreadyProd =\n    project.productionDeployment?.deploymentId === build.deploymentId;\n  const prod = args.prod ?? alreadyProd;\n\n  const prodDatabase = databases?.find((database) =>\n    deploymentDatabaseEnv(project, database) === \"Production\"\n  );\n  const previewDatabase = databases?.find((database) =>\n    deploymentDatabaseEnv(project, database) === \"Preview\"\n  );\n  const db = resolveDatabase(\n    spinnerPrep,\n    args,\n    prod,\n    project,\n    prodDatabase,\n    previewDatabase,\n  );\n\n  const envVarsToAdd = await envVarsFromArgs(args) || {};\n  const addedEnvs = Object.keys(envVarsToAdd);\n  // If the redeployment sets some env vars, the remaining env vars in the deployment are deleted\n  const envVarsToRemove = build.deployment && addedEnvs.length > 0\n    ? Object.fromEntries(\n      build.deployment.envVars\n        .filter((env) => !addedEnvs.includes(env))\n        // HOME is always set by Deno Deploy\n        .filter((env) => env !== \"HOME\")\n        .map((key) => [key, null]),\n    )\n    : {};\n  const removedEnvs = Object.keys(envVarsToRemove);\n\n  const envVars = {\n    ...envVarsToAdd,\n    ...envVarsToRemove,\n  };\n\n  spinnerPrep.succeed(\n    `Redeployment of deployment '${deploymentId}' is ready to begin:`,\n  );\n\n  const domainMappingDescription = prod\n    ? \"The new deployment will be the new production deployment\"\n    : \"The new deployment will be a preview deployment\";\n\n  wait({ text: \"\", indent: 3 }).start().info(domainMappingDescription);\n  if (db) {\n    const dbTag = db === prodDatabase?.databaseId\n      ? \"production\"\n      : db === previewDatabase?.databaseId\n      ? \"preview\"\n      : \"custom\";\n    wait({ text: \"\", indent: 3 }).start().info(\n      `The new deployment will use the ${dbTag} database '${db}'`,\n    );\n  }\n\n  if (addedEnvs.length === 1) {\n    wait({ text: \"\", indent: 3 }).start().info(\n      `The new deployment will use the env variable ${addedEnvs[0]}`,\n    );\n  } else if (addedEnvs.length > 1) {\n    wait({ text: \"\", indent: 3 }).start().info(\n      `The new deployment will use the env variables ${\n        addedEnvs.slice(0, -1).join(\", \")\n      } and ${addedEnvs.at(-1)}`,\n    );\n  }\n  if (removedEnvs.length === 1) {\n    wait({ text: \"\", indent: 3 }).start().info(\n      `The new deployment will stop using the env variable ${removedEnvs[0]}`,\n    );\n  } else if (removedEnvs.length > 1) {\n    wait({ text: \"\", indent: 3 }).start().info(\n      `The new deployment will stop using the env variables ${\n        removedEnvs.slice(0, -1).join(\", \")\n      } and ${removedEnvs.at(-1)}`,\n    );\n  }\n\n  const spinner = wait(`Redeploying deployment '${deploymentId}'...`).start();\n  const params = {\n    prod,\n    env_vars: envVars,\n    databases: db ? { default: db } : undefined,\n  };\n  const redeployed = await api.redeployDeployment(deploymentId, params);\n  if (redeployed) {\n    spinner.succeed(\n      `Deployment '${deploymentId}' redeployed as '${redeployed.id}' successfully`,\n    );\n  } else {\n    spinner.fail(\n      `Deployment '${deploymentId}' not found, or you don't have access to it`,\n    );\n  }\n}\n\nasync function searchRelativeDeployment(\n  deployments: AsyncGenerator<Build>,\n  deploymentId: string,\n  relativePos: number,\n): Promise<Build | undefined> {\n  const buffer = [];\n  for await (const build of deployments) {\n    if (relativePos === 0) {\n      if (build.deploymentId === deploymentId) {\n        return build;\n      }\n    }\n    if (relativePos > 0) {\n      if (build.deploymentId === deploymentId) {\n        return buffer.pop();\n      }\n    }\n    if (relativePos < 0) {\n      if (buffer.pop()?.deploymentId === deploymentId) {\n        return build;\n      }\n    }\n    buffer.unshift(build);\n    // Truncates array at given length\n    buffer.length = Math.abs(relativePos);\n  }\n}\n\nfunction renderShowOverview(\n  build: Build,\n  project: Project,\n  organization: Organization,\n  databases: Database[],\n  crons: Cron[],\n) {\n  const organizationName = organization.name && cyan(organization.name) ||\n    `${cyan(organization.members[0].user.name)} [personal]`;\n  const buildError = deploymentError(build)?.ctx.replaceAll(/\\s+/g, \" \");\n  const status = deploymentStatus(project, build);\n  const coloredStatus = status === \"Failed\"\n    ? red(bold(status.toUpperCase()))\n    : status === \"Pending\"\n    ? yellow(status)\n    : status === \"Production\"\n    ? green(bold(status))\n    : status;\n  const database = deploymentDatabase(databases, build);\n  const databaseEnv = database\n    ? `${\n      greenProd(deploymentDatabaseEnv(project, database))\n    } (${database.databaseId})`\n    : \"n/a\";\n  const entrypoint = deploymentEntrypoint(build);\n  const domains =\n    build.deployment?.domainMappings.map((domain) => `https://${domain.domain}`)\n      .sort((a, b) => a.length - b.length) ?? [];\n  if (domains.length === 0) {\n    domains.push(\"n/a\");\n  }\n  console.log();\n  console.log(bold(build.deploymentId));\n  console.log(new Array(build.deploymentId.length).fill(\"-\").join(\"\"));\n  console.log(`Status:\\t\\t${coloredStatus}`);\n  if (buildError) {\n    console.log(`Error:\\t\\t${buildError}`);\n  }\n  console.log(\n    `Date:\\t\\t${deploymentRelativeDate(build)} ago (${\n      deploymentLocaleDate(build)\n    })`,\n  );\n  if (\n    build.deployment?.description &&\n    build.deployment.description !== build.relatedCommit?.message\n  ) {\n    console.log(`Description:\\t${build.deployment.description}`);\n  }\n  console.log(`Project:\\t${magenta(project.name)} (${project.id})`);\n  console.log(\n    `Organization:\\t${organizationName} (${project.organizationId})`,\n  );\n  console.log(\n    `Domain(s):\\t${domains.join(\"\\n\\t\\t\")}`,\n  );\n  console.log(`Database:\\t${databaseEnv}`);\n  console.log(`Entrypoint:\\t${entrypoint}`);\n  console.log(\n    `Env Vars:\\t${build.deployment?.envVars.join(\"\\n\\t\\t\") ?? \"n/a\"}`,\n  );\n  if (build.relatedCommit) {\n    console.log(`Git`);\n    console.log(\n      `  Ref:\\t\\t${cyan(build.relatedCommit.branch ?? \"??\")} [${\n        build.relatedCommit.hash.slice(0, 7)\n      }]`,\n    );\n    console.log(\n      `  Message:\\t${build.relatedCommit.message.split(\"\\n\")[0]}`,\n    );\n    console.log(\n      `  Author:\\t${build.relatedCommit.authorName} @${\n        magenta(build.relatedCommit.authorGithubUsername)\n      } [mailto:${cyan(build.relatedCommit.authorEmail)}]`,\n    );\n    console.log(`  Url:\\t\\t${build.relatedCommit.url}`);\n  }\n  // The API only shows the data of the cron in the production deployment regardless of the deployment queried\n  if (status === \"Production\" && crons.length > 0) {\n    console.log(\n      `Crons:\\t\\t${crons.map(renderCron).join(\"\\n\\t\\t\")}`,\n    );\n  }\n}\n\nfunction renderShowJson(\n  build: Build,\n  project: Project,\n  organization: Organization,\n  databases: Database[],\n  crons: Cron[],\n) {\n  console.log(\n    JSON.stringify({ build, project, organization, databases, crons }),\n  );\n}\n\nasync function renderListOverview(\n  api: API,\n  project: Project,\n  databases: Database[],\n  buildsPage: BuildsPage,\n) {\n  const sld = new URL(endpoint()).hostname.split(\".\").at(-2);\n  for (;;) {\n    const table = buildsPage.list.map((build) => {\n      const status = deploymentStatus(project, build);\n      const colorByStatus = (s: string) =>\n        status === \"Failed\"\n          ? red(stripAnsiCode(s))\n          : status === \"Production\"\n          ? green(s)\n          : status === \"Pending\"\n          ? yellow(s)\n          : s;\n      const database = deploymentDatabase(databases, build);\n      const databaseEnv = database\n        ? greenProd(deploymentDatabaseEnv(project, database))\n        : \"n/a\";\n      const relativeDate = stripAnsiCode(\n        deploymentRelativeDate(build).split(\", \")[0].trim(),\n      );\n      const date = `${deploymentLocaleDate(build)} (${relativeDate})`;\n      const row = {\n        Deployment: colorByStatus(build.deploymentId),\n        Date: colorByStatus(date),\n        Status: colorByStatus(status),\n        Database: colorByStatus(databaseEnv),\n        Domain: colorByStatus(\n          !isReady(status)\n            ? \"n/a\"\n            : `https://${project.name}-${build.deploymentId}.${sld}.dev`,\n        ),\n        Entrypoint: colorByStatus(deploymentEntrypoint(build)),\n        ...build.relatedCommit\n          ? {\n            Branch: colorByStatus(build.relatedCommit.branch ?? \"??\"),\n            Commit: colorByStatus(build.relatedCommit.hash.slice(0, 7)),\n          }\n          : {},\n      };\n      return row;\n    });\n    renderTable(table);\n\n    if (buildsPage.paging.page + 1 >= buildsPage.paging.totalPages) {\n      return;\n    }\n    alert(`Press enter to fetch the next page`);\n    tty.goUpSync(1, Deno.stdout);\n    tty.clearDownSync(Deno.stdout);\n    const nextPage = buildsPage.paging.page + 1;\n    const spinner = wait(\n      `Fetching page ${\n        // API page param is 0-based\n        nextPage +\n        1} of the list of deployments of project '${project.name}'...`,\n    )\n      .start();\n    const buildsNextPage = await api.listDeployments(\n      project.id,\n      nextPage,\n      buildsPage.paging.limit,\n    );\n    if (!buildsNextPage) {\n      spinner.fail(\n        `The project '${project.name}' does not exist, or you don't have access to it`,\n      );\n      return Deno.exit(1);\n    }\n    buildsPage = buildsNextPage;\n    spinner.succeed(\n      `Page ${\n        buildsPage.paging.page + 1\n      } of the list of deployments of the project '${project.name}' is ready`,\n    );\n  }\n}\n\nfunction isCurrentProd(project: Project, build: Build): boolean {\n  return project.productionDeployment?.id === build.id;\n}\n\nfunction deploymentError(build: Build): DeploymentProgressError | undefined {\n  return build.logs.find((log): log is DeploymentProgressError =>\n    log.type === \"error\"\n  );\n}\n\nfunction deploymentStatus(\n  project: Project,\n  build: Build,\n): DeploymentStatus {\n  const isError = deploymentError(build) !== undefined;\n  const isPending = !isError &&\n    (build.deployment === null ||\n      build.deployment.domainMappings.length === 0);\n  return isError\n    ? \"Failed\"\n    : isPending\n    ? \"Pending\"\n    : isCurrentProd(project, build)\n    ? \"Production\"\n    : \"Preview\";\n}\n\nfunction isReady(status: DeploymentStatus): boolean {\n  return [\"Production\", \"Preview\"].includes(status);\n}\n\nfunction deploymentDatabase(\n  databases: Database[],\n  build: Build,\n): Database | undefined {\n  return databases.find((db) =>\n    db.databaseId === build.deployment?.kvDatabases[\"default\"]\n  );\n}\n\nfunction deploymentLocaleDate(build: Build): string {\n  return new Date(build.createdAt).toLocaleString(navigator.language, {\n    timeZoneName: \"short\",\n  });\n}\n\nfunction deploymentRelativeDate(build: Build): string {\n  const createdAt = new Date(build.createdAt);\n  return renderTimeDelta(new Date().getTime() - createdAt.getTime());\n}\n\nfunction deploymentEntrypoint(build: Build): string {\n  return build.deployment\n    ? build.deployment.url.startsWith(\"https://\")\n      ? build.deployment.url\n      : fromFileUrl(build.deployment.url).replace(\"/src/\", \"\")\n    : \"n/a\";\n}\n\nfunction deploymentDatabaseEnv(\n  project: Project,\n  database: Database,\n): DatabaseEnv {\n  return project.git && project.git.productionBranch !== database!.branch\n    ? \"Preview\"\n    : \"Production\";\n}\n\nfunction renderTable(table: Record<string, string>[]) {\n  const headers = [];\n  for (const row of table) {\n    for (const [i, key] of Object.keys(row).entries()) {\n      headers[i] = key;\n    }\n  }\n  const widths: number[] = [];\n  for (const rowData of table) {\n    for (const [i, value] of Object.values(rowData).entries()) {\n      widths[i] = Math.max(\n        widths[i] ?? 0,\n        stripAnsiCode(value).length,\n        headers[i].length,\n      );\n      widths[i] = widths[i] + widths[i] % 2;\n    }\n  }\n  const headerRow = headers.map((header, i) => {\n    const pad = \" \".repeat(\n      Math.max(widths[i] - stripAnsiCode(header).length, 0) / 2,\n    );\n    return `${pad}${header}${pad}`.padEnd(widths[i], \" \");\n  }).join(\" \\u2502 \");\n  const divisor = \"\\u2500\".repeat(\n    widths.reduce((prev, next) => prev + next, 0) + (headers.length - 1) * 3,\n  );\n  console.log(`\\u250c\\u2500${divisor}\\u2500\\u2510`);\n  console.log(`\\u2502 ${headerRow} \\u2502`);\n  console.log(`\\u251c\\u2500${divisor}\\u2500\\u2524`);\n  for (const rowData of table) {\n    const row = Array.from(Object.values(rowData).entries(), ([i, cell]) => {\n      const pad = \" \".repeat(widths[i] - stripAnsiCode(cell).length);\n      return `${cell}${pad}`;\n    }).join(\" \\u2502 \");\n    console.log(`\\u2502 ${row} \\u2502`);\n  }\n  console.log(`\\u2514\\u2500${divisor}\\u2500\\u2518`);\n}\n\nasync function resolveDeploymentId(\n  args: Args,\n  api: API,\n): Promise<\n  [DeploymentId, ProjectId | undefined, Build | undefined, Project | undefined]\n> {\n  const deploymentIdArg = args._.shift()?.toString() || args.id;\n  // Ignore --project if user also provided --id\n  const projectIdArg = deploymentIdArg ? undefined : args.project;\n\n  let deploymentId,\n    projectId: string | undefined,\n    build: Build | undefined,\n    project: Project | undefined;\n\n  if (deploymentIdArg) {\n    deploymentId = deploymentIdArg;\n  } else {\n    // Default to showing the production deployment of the project or the last\n    if (!projectIdArg) {\n      error(\n        \"No deployment or project specified. Use --id <deployment-id> or --project <project-name>\",\n      );\n    }\n    projectId = projectIdArg;\n\n    if (args.last) {\n      const spinner = wait(\n        `Searching the last deployment of project '${projectId}'...`,\n      ).start();\n      const buildsPage = await api.listDeployments(projectId, 0, 1);\n      if (!buildsPage) {\n        spinner.fail(\n          `The project '${projectId}' does not exist, or you don't have access to it`,\n        );\n        return Deno.exit(1);\n      }\n      if (buildsPage.list.length === 0) {\n        spinner.fail(\n          `The project '${projectId}' does not have any deployment yet`,\n        );\n        return Deno.exit(1);\n      }\n      deploymentId = buildsPage.list[0].deploymentId;\n      spinner.succeed(\n        `The last deployment of the project '${projectId}' is '${deploymentId}'`,\n      );\n    } else {\n      const spinner = wait(\n        `Searching the production deployment of project '${projectId}'...`,\n      ).start();\n      const maybeProject = await api.getProject(projectId);\n      if (!maybeProject) {\n        spinner.fail(\n          `The project '${projectId}' does not exist, or you don't have access to it`,\n        );\n        return Deno.exit(1);\n      }\n      project = maybeProject;\n      if (!project.productionDeployment) {\n        spinner.fail(\n          `Project '${project.name}' does not have a production deployment. Use --id <deployment-id> to specify the deployment to show`,\n        );\n        return Deno.exit(1);\n      }\n      deploymentId = project.productionDeployment.deploymentId;\n      spinner.succeed(\n        `The production deployment of the project '${project.name}' is '${deploymentId}'`,\n      );\n    }\n  }\n\n  if (args.prev.length !== 0 || args.next.length !== 0) {\n    // Search the deployment relative to the specified deployment\n    if (!projectId) {\n      // Fetch the deployment specified with --id, to know of which project to search the relative deployment\n      // If user didn't use --id, they must have used --project, thus we already know the project-id\n      const spinner_ = wait(`Fetching deployment '${deploymentId}'...`)\n        .start();\n      const specifiedDeployment = await api.getDeployment(deploymentId);\n      if (!specifiedDeployment) {\n        spinner_.fail(\n          `The deployment '${deploymentId}' does not exist, or you don't have access to it`,\n        );\n        return Deno.exit(1);\n      }\n      spinner_.succeed(`Deployment '${deploymentId}' found`);\n      projectId = specifiedDeployment.project.id;\n    }\n    let relativePos = 0;\n    for (const prev of args.prev) {\n      relativePos -= parseInt(prev || \"1\");\n    }\n    for (const next of args.next) {\n      relativePos += parseInt(next || \"1\");\n    }\n    if (Number.isNaN(relativePos)) {\n      error(\"Value of --next and --prev must be a number\");\n    }\n    const relativePosString = relativePos.toLocaleString(navigator.language, {\n      signDisplay: \"exceptZero\",\n    });\n    const spinner = wait(\n      `Searching the deployment ${relativePosString} relative to '${deploymentId}'...`,\n    ).start();\n    const maybeBuild = await searchRelativeDeployment(\n      api.listAllDeployments(projectId),\n      deploymentId,\n      relativePos,\n    );\n    if (!maybeBuild) {\n      spinner.fail(\n        `The deployment '${deploymentId}' does not have a deployment ${relativePosString} relative to it`,\n      );\n      return Deno.exit(1);\n    }\n    build = maybeBuild;\n    spinner.succeed(\n      `The deployment ${relativePosString} relative to '${deploymentId}' is '${build.deploymentId}'`,\n    );\n    deploymentId = build.deploymentId;\n  }\n  return [deploymentId, projectId, build, project];\n}\n\nfunction resolveDatabase(\n  spinner: Spinner,\n  args: Args,\n  prod: boolean,\n  project: Project,\n  prodDatabase: Database | undefined,\n  previewDatabase: Database | undefined,\n): string | undefined {\n  let db;\n  switch (args.db?.toLowerCase().trim()) {\n    case \"prod\":\n    case \"production\": {\n      if (!prodDatabase) {\n        spinner.fail(\n          `Project '${project.name}' does not have a production database`,\n        );\n        return Deno.exit(1);\n      }\n      db = prodDatabase.databaseId;\n      break;\n    }\n    case \"preview\": {\n      if (!previewDatabase) {\n        spinner.fail(\n          `Project '${project.name}' does not have a preview database`,\n        );\n        return Deno.exit(1);\n      }\n      db = previewDatabase.databaseId;\n      break;\n    }\n    default:\n      db = args.db;\n  }\n\n  if (!db) {\n    // For GitHub deployments, Deploy assigns the branch database also during redeployment\n    // Unless the user is explicit about the db, we want to maintain the invariant status == databaseEnv\n    if (prod) {\n      db = prodDatabase?.databaseId;\n    } else {\n      db = previewDatabase?.databaseId;\n    }\n  }\n  return db;\n}\n\nfunction greenProd(s: \"Production\" | string): string {\n  return s === \"Production\" ? green(s) : s;\n}\n\ntype DeploymentStatus = \"Failed\" | \"Pending\" | \"Production\" | \"Preview\";\ntype DatabaseEnv = \"Production\" | \"Preview\";\ntype DeploymentId = string;\ntype ProjectId = string;\n"
  },
  {
    "path": "src/subcommands/logs.ts",
    "content": "// Copyright 2021 Deno Land Inc. All rights reserved. MIT license.\n\nimport type { Args } from \"../args.ts\";\nimport { wait } from \"../utils/spinner.ts\";\nimport { error } from \"../error.ts\";\nimport { API, APIError } from \"../utils/api.ts\";\nimport type { Project } from \"../utils/api_types.ts\";\nimport TokenProvisioner from \"../utils/access_token.ts\";\n\nconst help = `deployctl logs\nView logs for the given project. It supports both live logs where the logs are streamed to the console as they are\ngenerated, and query persisted logs where the logs generated in the past are fetched.\n\nTo show the live logs of a project's latest deployment:\n  deployctl logs --project=helloworld\n  deployctl logs helloworld\n\nTo show the live logs of a particular deployment:\n  deployctl logs --project=helloworld --deployment=1234567890ab\n\nTo show the live error & info level logs of the production deployment generated in particular regions:\n  deployctl logs --project=helloworld --prod --levels=error,info --regions=region1,region2\n\nTo show the logs generated within the past two hours, up until 30 minutes ago, and containing the word \"foo\":\n  [Linux]\n  deployctl logs --project=helloworld --since=$(date -Iseconds --date='2 hours ago') --until=$(date -Iseconds --date='30 minutes ago') --grep=foo\n  [macOS]\n  deployctl logs --project=helloworld --since=$(date -Iseconds -v-2H) --until=$(date -Iseconds -v-30M) --grep=foo\n\nUSAGE:\n    deployctl logs [OPTIONS] [<PROJECT>]\n\nOPTIONS:\n        --deployment=<DEPLOYMENT_ID>  The id of the deployment you want to get the logs (defaults to latest deployment)\n        --prod                        Select the production deployment\n    -p, --project=NAME                The project you want to get the logs\n        --token=TOKEN                 The API token to use (defaults to DENO_DEPLOY_TOKEN env var)\n        --since=<DATETIME>            The start time of the logs you want to get. RFC3339 format (e.g. 2023-07-17T06:10:38+09:00) is supported.\n                                      NOTE: Logs generated over 24 hours ago are not available\n        --until=<DATETIME>            The end time of the logs you want to get. RFC3339 format (e.g. 2023-07-17T06:10:38+09:00) is supported.\n        --grep=<WORD>                 Filter logs by a word\n                                      Multiple words can be specified for AND search. For example, \"--grep=foo --grep=bar\" will match logs containing both \"foo\" and \"bar\"\n        --levels=<LEVELS>             Filter logs by log levels (defaults to all log levels)\n                                      Mutliple levels can be specified, e.g. --levels=info,error\n        --regions=<REGIONS>           Filter logs by regions (defaults to all regions)\n                                      Multiple regions can be specified, e.g. --regions=region1,region2\n        --limit=<LIMIT>               Limit the number of logs to return (defualts to 100)\n                                      This flag is effective only when --since and/or --until is specified\n`;\n\nexport interface LogSubcommandArgs {\n  help: boolean;\n  prod: boolean;\n  token: string | null;\n  deployment: string | null;\n  project: string | null;\n  since: Date | null;\n  until: Date | null;\n  grep: string[];\n  levels: string[] | null;\n  regions: string[] | null;\n  limit: number;\n}\n\ntype LogOptsBase = {\n  prod: boolean;\n  deploymentId: string | null;\n  projectId: string;\n  grep: string[];\n  levels: string[] | null;\n  regions: string[] | null;\n};\ntype LiveLogOpts = LogOptsBase;\ntype QueryLogOpts = LogOptsBase & {\n  since: Date | null;\n  until: Date | null;\n  limit: number;\n};\n\nexport default async function (args: Args): Promise<void> {\n  const logSubcommandArgs = parseArgsForLogSubcommand(args);\n\n  if (logSubcommandArgs.help) {\n    console.log(help);\n    Deno.exit(0);\n  }\n  if (logSubcommandArgs.project === null) {\n    console.error(help);\n    error(\"Missing project ID.\");\n  }\n  if (args._.length > 1) {\n    console.error(help);\n    error(\"Too many positional arguments given.\");\n  }\n\n  if (logSubcommandArgs.prod && logSubcommandArgs.deployment) {\n    error(\n      \"You can't select a deployment and choose production flag at the same time\",\n    );\n  }\n\n  if (\n    logSubcommandArgs.since !== null && logSubcommandArgs.until !== null &&\n    logSubcommandArgs.since >= logSubcommandArgs.until\n  ) {\n    error(\"--since must be earlier than --until\");\n  }\n\n  const api = logSubcommandArgs.token\n    ? API.fromToken(logSubcommandArgs.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  const { regionCodes } = await api.getMetadata();\n  if (logSubcommandArgs.regions !== null) {\n    const invalidRegions = getInvalidRegions(\n      logSubcommandArgs.regions,\n      regionCodes,\n    );\n    if (invalidRegions.length > 0) {\n      invalidRegionError(invalidRegions, regionCodes);\n    }\n  }\n\n  const liveLogMode = logSubcommandArgs.since === null &&\n    logSubcommandArgs.until === null;\n  if (liveLogMode) {\n    await liveLogs(api, {\n      prod: logSubcommandArgs.prod,\n      deploymentId: logSubcommandArgs.deployment,\n      projectId: logSubcommandArgs.project,\n      grep: logSubcommandArgs.grep,\n      levels: logSubcommandArgs.levels,\n      regions: logSubcommandArgs.regions,\n    });\n  } else {\n    await queryLogs(api, {\n      prod: logSubcommandArgs.prod,\n      deploymentId: logSubcommandArgs.deployment,\n      projectId: logSubcommandArgs.project,\n      grep: logSubcommandArgs.grep,\n      levels: logSubcommandArgs.levels,\n      regions: logSubcommandArgs.regions,\n      since: logSubcommandArgs.since,\n      until: logSubcommandArgs.until,\n      limit: logSubcommandArgs.limit,\n    });\n  }\n}\n\nfunction getInvalidRegions(\n  specifiedRegions: string[],\n  availableRegions: string[],\n): string[] {\n  const invalidRegions = [];\n  for (const r of specifiedRegions) {\n    if (!availableRegions.includes(r)) {\n      invalidRegions.push(r);\n    }\n  }\n  return invalidRegions;\n}\n\nfunction invalidRegionError(\n  invalidRegions: string[],\n  availableRegions: string[],\n): never {\n  const invalid = `--regions contains invalid region(s): ${\n    invalidRegions.join(\", \")\n  }`;\n  const availableRegionsList = availableRegions.map((r) => `- ${r}`).join(\"\\n\");\n  const available = `HINT: Available regions are:\\n${availableRegionsList}`;\n\n  error(`${invalid}\\n${available}`);\n}\n\nexport function parseArgsForLogSubcommand(args: Args): LogSubcommandArgs {\n  const DEFAULT_LIMIT = 100;\n  const limit = args.limit ? parseInt(args.limit) : DEFAULT_LIMIT;\n\n  let since: Date | null = null;\n  if (args.since !== undefined) {\n    since = new Date(args.since);\n    if (Number.isNaN(since.valueOf())) {\n      console.error(help);\n      error(\"Invalid format found in --since\");\n    }\n  }\n\n  let until: Date | null = null;\n  if (args.until !== undefined) {\n    until = new Date(args.until);\n    if (Number.isNaN(until.valueOf())) {\n      console.error(help);\n      error(\"Invalid format found in --until\");\n    }\n  }\n\n  let logLevels: string[] | null = null;\n  if (args.levels !== undefined) {\n    logLevels = args.levels.split(\",\");\n  }\n\n  let regions: string[] | null = null;\n  if (args.regions !== undefined) {\n    regions = args.regions.split(\",\");\n  }\n\n  let project: string | null = null;\n  if (args.project !== undefined) {\n    project = args.project;\n  } else if (typeof args._[0] === \"string\") {\n    project = args._[0];\n  }\n\n  return {\n    help: !!args.help,\n    prod: !!args.prod,\n    token: args.token ? String(args.token) : null,\n    deployment: args.deployment ? String(args.deployment) : null,\n    project,\n    since,\n    until,\n    grep: args.grep,\n    levels: logLevels,\n    regions,\n    limit: Number.isNaN(limit) ? DEFAULT_LIMIT : limit,\n  };\n}\n\nasync function fetchProjectInfo(\n  api: API,\n  projectId: string,\n  onFailure: (msg: string) => never,\n): Promise<Project> {\n  const project = await api.getProject(projectId);\n  if (project === null) {\n    onFailure(\"Project not found.\");\n  }\n\n  const projectDeployments = await api.listDeployments(projectId);\n  if (projectDeployments === null) {\n    onFailure(\"Project not found.\");\n  }\n\n  return project;\n}\n\nasync function liveLogs(api: API, opts: LiveLogOpts): Promise<void> {\n  const projectSpinner = wait(\"Fetching project information...\").start();\n  const project = await fetchProjectInfo(api, opts.projectId, (msg) => {\n    projectSpinner.fail(msg);\n    Deno.exit(1);\n  });\n  if (opts.prod) {\n    if (!project.hasProductionDeployment) {\n      projectSpinner.fail(\"This project doesn't have a production deployment\");\n      Deno.exit(1);\n    }\n    opts.deploymentId = project.productionDeployment?.id ?? null;\n  }\n  projectSpinner.succeed(`Project: ${project.name}`);\n  const logs = opts.deploymentId\n    ? await api.getLogs(opts.projectId, opts.deploymentId)\n    : await api.getLogs(opts.projectId, \"latest\");\n  if (logs === null) {\n    projectSpinner.fail(\"Project not found.\");\n    Deno.exit(1);\n  }\n  try {\n    for await (const log of logs) {\n      if (log.type === \"ready\" || log.type === \"ping\") {\n        continue;\n      }\n\n      if (opts.grep.some((word) => !log.message.includes(word))) {\n        continue;\n      }\n\n      if (opts.levels !== null && !opts.levels.includes(log.level)) {\n        continue;\n      }\n\n      if (opts.regions !== null && !opts.regions.includes(log.region)) {\n        continue;\n      }\n\n      printLog(log.level, log.time, log.region, log.message);\n    }\n  } catch (err: unknown) {\n    if (\n      err instanceof APIError\n    ) {\n      error(err.toString());\n    }\n  } finally {\n    console.log(\"%cconnection closed\", \"color: red\");\n  }\n}\n\nasync function queryLogs(api: API, opts: QueryLogOpts): Promise<void> {\n  const projectSpinner = wait(\"Fetching project information...\").start();\n  const project = await fetchProjectInfo(api, opts.projectId, (msg) => {\n    projectSpinner.fail(msg);\n    Deno.exit(1);\n  });\n  if (opts.prod) {\n    if (!project.hasProductionDeployment) {\n      projectSpinner.fail(\"This project doesn't have a production deployment\");\n      Deno.exit(1);\n    }\n    opts.deploymentId = project.productionDeployment?.id ?? null;\n  }\n  projectSpinner.succeed(`Project: ${project.name}`);\n\n  const logSpinner = wait(\"Fetching logs...\").start();\n  try {\n    const { logs } = await api.queryLogs(\n      opts.projectId,\n      opts.deploymentId ?? \"latest\",\n      {\n        regions: opts.regions ?? undefined,\n        levels: opts.levels ?? undefined,\n        since: opts.since?.toISOString(),\n        until: opts.until?.toISOString(),\n        q: opts.grep.length > 0 ? opts.grep : undefined,\n        limit: opts.limit,\n      },\n    );\n\n    if (logs.length === 0) {\n      logSpinner.fail(\"No logs found matching the provided condition\");\n      return;\n    }\n\n    logSpinner.succeed(`Found ${logs.length} logs`);\n    for (const log of logs) {\n      printLog(log.level, log.timestamp, log.region, log.message);\n    }\n  } catch (err: unknown) {\n    logSpinner.fail(\"Failed to fetch logs\");\n    if (err instanceof APIError) {\n      error(err.toString());\n    } else {\n      throw err;\n    }\n  }\n}\n\nfunction printLog(\n  logLevel: string,\n  timestamp: string,\n  region: string,\n  message: string,\n) {\n  const color = getLogColor(logLevel);\n  console.log(\n    `%c${timestamp}   %c${region}%c ${message.trim()}`,\n    \"color: aquamarine\",\n    \"background-color: grey\",\n    `color: ${color}`,\n  );\n}\n\nfunction getLogColor(logLevel: string) {\n  switch (logLevel) {\n    case \"debug\": {\n      return \"grey\";\n    }\n    case \"error\": {\n      return \"red\";\n    }\n    case \"info\": {\n      return \"blue\";\n    }\n    default: {\n      return \"initial\";\n    }\n  }\n}\n"
  },
  {
    "path": "src/subcommands/logs_test.ts",
    "content": "import { parseArgsForLogSubcommand } from \"./logs.ts\";\nimport { assertEquals, assertNotEquals, assertThrows } from \"@std/assert\";\nimport { parseArgs } from \"../args.ts\";\n\nDeno.test(\"parseArgsForLogSubcommand\", async (t) => {\n  const parseHelper = (args: string[]) => {\n    // For this test, the subcommand name should not be included in `args`.\n    assertNotEquals(args.at(0), \"logs\");\n\n    try {\n      return parseArgsForLogSubcommand(parseArgs(args));\n    } catch (e) {\n      // Since Deno v1.44.0, when `Deno.exitCode` was introduced, test cases\n      // with non-zero exit code has been treated as failure, causing some tests\n      // to fail unexpectedly (not sure if this behavior change is intended).\n      // To avoid this, we set `Deno.exitCode` to 0 before giving control back\n      // to each test case.\n      // https://github.com/denoland/deno/pull/23609\n      // deno-lint-ignore no-explicit-any\n      if ((Deno as any).exitCode !== undefined) {\n        // deno-lint-ignore no-explicit-any\n        (Deno as any).exitCode = 0;\n      }\n      throw e;\n    }\n  };\n\n  await t.step(\"specify help\", () => {\n    const got = parseHelper([\"--help\"]);\n    assertEquals(got, {\n      help: true,\n      prod: false,\n      token: null,\n      deployment: null,\n      project: null,\n      since: null,\n      until: null,\n      grep: [],\n      levels: null,\n      regions: null,\n      limit: 100,\n    });\n  });\n\n  await t.step(\"specify since and until\", () => {\n    const since = new Date(Date.now() - 3 * 60 * 60 * 1000); // 3 hours ago\n    const until = new Date(Date.now() - 42 * 60 * 1000); // 42 minutes ago\n    const got = parseHelper([\n      `--since=${since.toISOString()}`,\n      `--until=${until.toISOString()}`,\n    ]);\n    assertEquals(got, {\n      help: false,\n      prod: false,\n      token: null,\n      deployment: null,\n      project: null,\n      since,\n      until,\n      grep: [],\n      levels: null,\n      regions: null,\n      limit: 100,\n    });\n  });\n\n  await t.step(\"specify invalid format in since\", () => {\n    assertThrows(() => parseHelper([\"--since=INVALID\"]), Error, \"exit code: 1\");\n  });\n\n  await t.step(\"specify invalid format in until\", () => {\n    assertThrows(() => parseHelper([\"--until=INVALID\"]), Error, \"exit code: 1\");\n  });\n\n  await t.step(\"complex args\", () => {\n    const until = new Date(Date.now() - 42 * 1000); // 42 seconds ago\n    const got = parseHelper([\n      \"--prod\",\n      \"--token=abc\",\n      \"--project=helloworld\",\n      `--until=${until.toISOString()}`,\n      \"--grep=こんにちは\",\n      \"--levels=info,error\",\n      \"--regions=region1,region2\",\n      \"--limit=42\",\n      \"--grep=hola\",\n    ]);\n    assertEquals(got, {\n      help: false,\n      prod: true,\n      token: \"abc\",\n      deployment: null,\n      project: \"helloworld\",\n      since: null,\n      until,\n      grep: [\"こんにちは\", \"hola\"],\n      levels: [\"info\", \"error\"],\n      regions: [\"region1\", \"region2\"],\n      limit: 42,\n    });\n  });\n\n  await t.step(\"specify project name in a positional argument\", () => {\n    const got = parseHelper([\n      \"--prod\",\n      \"--token=abc\",\n      \"project_name\",\n    ]);\n    assertEquals(got, {\n      help: false,\n      prod: true,\n      token: \"abc\",\n      deployment: null,\n      project: \"project_name\",\n      since: null,\n      until: null,\n      grep: [],\n      levels: null,\n      regions: null,\n      limit: 100,\n    });\n  });\n});\n"
  },
  {
    "path": "src/subcommands/projects.ts",
    "content": "import type { Args } from \"../args.ts\";\nimport { API, APIError, endpoint } from \"../utils/api.ts\";\nimport TokenProvisioner from \"../utils/access_token.ts\";\nimport { wait } from \"../utils/spinner.ts\";\nimport type { Organization, Project } from \"../utils/api_types.ts\";\nimport { bold, green, magenta, red } from \"@std/fmt/colors\";\nimport { error } from \"../error.ts\";\nimport organization from \"../utils/organization.ts\";\nimport { renderCron } from \"../utils/crons.ts\";\nimport { stringify as stringifyError } from \"../error.ts\";\n\nconst help = `Manage projects in Deno Deploy\n\nUSAGE:\n    deployctl projects <SUBCOMMAND> [OPTIONS]\n\nSUBCOMMANDS:\n    list               List the name of all the projects accessible by the user\n    show   [NAME]      View details of a project. Specify the project using the positional argument or the --project option;\n                       otherwise, it will show the details of the project specified in the config file\n                       or try to guess it from the working context\n    delete [NAME]      Delete a project. Specify the project in the same way as the show subcommand\n    create [NAME]      Create a new project. Specify the project name in the same way as the show subcommand \n    rename [OLD] <NEW> Change the name of the project. Specify the project in the same way as the show subcommand\n\n\nOPTIONS:\n    -h, --help                      Prints this help information\n    -p, --project=<NAME|ID>         The project selected. Can also be provided as positional argument\n        --org=<ORG>                 Specify an organization. When creating a project, defaults to the user's personal organization.\n                                    When listing projects, use \"personal\" to filter by the personal organization.\n        --token=<TOKEN>             The API token to use (defaults to DENO_DEPLOY_TOKEN env var)\n        --config=<PATH>             Path to the file from where to load DeployCTL config. Defaults to 'deno.json'\n        --color=<auto|always|never> Enable or disable colored output. Defaults to 'auto' (colored when stdout is a tty)\n        --force                     Automatically execute the command without waiting for confirmation.\n`;\n\nexport default async function (args: Args): Promise<void> {\n  if (args.help) {\n    console.log(help);\n    Deno.exit(0);\n  }\n  const subcommand = args._.shift();\n  switch (subcommand) {\n    case \"list\":\n      await listProjects(args);\n      break;\n    case \"show\":\n      await showProject(args);\n      break;\n    case \"delete\":\n      await deleteProject(args);\n      break;\n    case \"create\":\n      await createProject(args);\n      break;\n    case \"rename\":\n      await renameProject(args);\n      break;\n    default:\n      console.error(help);\n      Deno.exit(1);\n  }\n}\n\nasync function listProjects(args: Args): Promise<void> {\n  const spinner = wait(\"Fetching organizations and projects...\").start();\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  const orgs = (await api.listOrganizations()).filter((org) =>\n    args.org\n      ? (org.name\n        ? org.name === args.org\n        : args.org.toLowerCase() === \"personal\")\n      : true\n  );\n  const data: [Organization, Project[]][] = await Promise.all(\n    orgs.map(async (org) => [org, await api.listProjects(org.id)]),\n  );\n  spinner.succeed(\"Organizations and projects data ready:\");\n  data.sort(([_orga, projectsa], [_orgb, projectsb]) =>\n    projectsb.length - projectsa.length\n  );\n  for (const [org, projects] of data) {\n    if (projects.length === 0) continue;\n    console.log();\n    console.log(\n      org.name && `'${bold(magenta(org.name))}' org:` || \"Personal org:\",\n    );\n    for (const project of projects) {\n      console.log(`    ${green(project.name)}`);\n    }\n  }\n}\n\nasync function showProject(args: Args): Promise<void> {\n  const positionalArg = args._.shift();\n  if (positionalArg) {\n    // Positional arguments supersedes --project flag\n    args.project = positionalArg.toString();\n  }\n  if (!args.project) {\n    error(\n      \"No project specified. Use --project to specify the project of which to show the details\",\n    );\n  }\n  const spinner = wait(`Fetching project '${args.project}'...`).start();\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  const [project, domains, buildsPage, databases, crons] = await Promise.all([\n    api.getProject(args.project),\n    api.getDomains(args.project),\n    api.listDeployments(args.project),\n    api.getProjectDatabases(args.project),\n    api.getProjectCrons(args.project),\n  ]).catch((err) => {\n    if (err instanceof APIError && err.code === \"projectNotFound\") {\n      return [null, null, null, null, null];\n    }\n    throw err;\n  });\n\n  if (!project || !domains || !buildsPage || !databases) {\n    spinner.fail(\n      `The project '${args.project}' does not exist, or you don't have access to it`,\n    );\n    return Deno.exit(1);\n  }\n  const organizationName = project.organization.name\n    ? magenta(project.organization.name)\n    : `${\n      magenta(\n        (await api.getOrganizationById(project.organization.id)).members[0]\n          .user\n          .name,\n      )\n    } [personal]`;\n  spinner.succeed(`Project '${args.project}' found`);\n  console.log();\n  console.log(bold(project.name));\n  console.log(new Array(project.name.length).fill(\"-\").join(\"\"));\n  console.log(`Organization:\\t${organizationName} (${project.organizationId})`);\n  const ingressRoot = new URL(endpoint()).hostname.split(\".\").at(-2);\n  domains.push({\n    domain: `${project.name}.${ingressRoot}.dev`,\n    isValidated: true,\n  });\n  const validatedDomains = domains.filter((domain) => domain.isValidated);\n  console.log(\n    `Domain(s):\\t${\n      validatedDomains.map((domain) => `https://${domain.domain}`).join(\n        \"\\n\\t\\t\",\n      )\n    }`,\n  );\n  console.log(`Dash URL:\\t${endpoint()}/projects/${project.id}`);\n  if (project.type === \"playground\") {\n    console.log(`Playground:\\t${endpoint()}/playground/${project.name}`);\n  }\n  if (project.git) {\n    console.log(\n      `Repository:\\thttps://github.com/${project.git.repository.owner}/${project.git.repository.name}`,\n    );\n  }\n  if (databases.length > 0) {\n    console.log(\n      `Databases:\\t${\n        databases.map((db) => `[${db.branch}] ${db.databaseId}`).join(`\\n\\t\\t`)\n      }`,\n    );\n  }\n  if (crons && crons.length > 0) {\n    console.log(\n      `Crons:\\t\\t${crons.map(renderCron).join(\"\\n\\t\\t\")}`,\n    );\n  }\n  if (buildsPage.list.length > 0) {\n    console.log(\n      `Deployments:${\n        buildsPage.list.map((build, i) =>\n          `${i !== 0 && i % 5 === 0 ? \"\\n\\t\\t\" : \"\\t\"}${\n            build.deployment\n              ? project.productionDeployment?.deployment?.id ===\n                  build.deployment.id\n                ? `${magenta(build.deployment.id)}*`\n                : build.deployment.id\n              : `${red(\"✖\")} (failed)`\n          }`\n        ).join(\"\")\n      }`,\n    );\n  }\n}\n\nasync function deleteProject(args: Args): Promise<void> {\n  const positionalArg = args._.shift();\n  if (positionalArg) {\n    // Positional arguments supersedes --project flag\n    args.project = positionalArg.toString();\n  }\n  if (!args.project) {\n    error(\n      \"No project specified. Use --project to specify the project to delete\",\n    );\n  }\n  const fetchSpinner = wait(`Fetching project '${args.project}' details...`)\n    .start();\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  const project = await api.getProject(args.project);\n  if (!project) {\n    fetchSpinner.fail(\n      `Project '${args.project}' not found, or you don't have access to it`,\n    );\n    return Deno.exit(1);\n  }\n  fetchSpinner.succeed(`Project '${project.name}' (${project.id}) found`);\n  const confirmation = args.force ? true : confirm(\n    `${\n      magenta(\"?\")\n    } Are you sure you want to delete the project '${project.name}'?`,\n  );\n  if (!confirmation) {\n    wait(\"\").fail(\"Delete canceled\");\n    return;\n  }\n  const spinner = wait(`Deleting project '${args.project}'...`).start();\n  const deleted = await api.deleteProject(args.project);\n  if (deleted) {\n    spinner.succeed(`Project '${args.project}' deleted successfully`);\n  } else {\n    spinner.fail(\n      `Project '${args.project}' not found, or you don't have access to it`,\n    );\n  }\n}\n\nasync function createProject(args: Args): Promise<void> {\n  const positionalArg = args._.shift();\n  if (positionalArg) {\n    // Positional arguments supersedes --project flag\n    args.project = positionalArg.toString();\n  }\n  if (!args.project) {\n    error(\n      \"No project specified. Use --project to specify the project to create\",\n    );\n  }\n  const spinner = wait(`Creating project '${args.project}'...`).start();\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  const org = args.org\n    ? await organization.getByNameOrCreate(api, args.org)\n    : null;\n  try {\n    await api.createProject(args.project, org?.id);\n    spinner.succeed(\n      `Project '${args.project}' created successfully ${\n        org ? `in organization '${org.name}'` : \"\"\n      }`,\n    );\n  } catch (error) {\n    spinner.fail(\n      `Cannot create the project '${args.project}': ${\n        stringifyError(error, { verbose: true })\n      }`,\n    );\n  }\n}\n\nasync function renameProject(args: Args): Promise<void> {\n  let currentId = args._.shift()?.toString();\n  let newName: string | null | undefined = args._.shift()?.toString();\n  if (currentId && !newName) {\n    // Only required positional argument is the new name\n    newName = currentId;\n    currentId = undefined;\n  }\n  if (currentId) {\n    // Positional arguments supersedes --project flag\n    args.project = currentId;\n  }\n  if (!args.project) {\n    error(\n      \"no project specified. Use --project to specify the project to rename\",\n    );\n  }\n  const fetchSpinner = wait(`Fetching project '${args.project}' details...`)\n    .start();\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  const project = await api.getProject(args.project);\n  if (!project) {\n    fetchSpinner.fail(\n      `Project ${args.project} not found, or you don't have access to it`,\n    );\n    return Deno.exit(1);\n  }\n  const currentName = project.name;\n  fetchSpinner.succeed(`Project '${currentName}' (${project.id}) found`);\n  if (!newName) {\n    newName = prompt(`${magenta(\"?\")} New name for project '${currentName}':`);\n  }\n  if (!newName) {\n    error(\"project name cannot be empty\");\n  }\n  const spinner = wait(`Renaming project '${currentName}' to '${newName}'...`)\n    .start();\n  try {\n    await api.renameProject(args.project, newName);\n    spinner.succeed(`Project '${currentName}' renamed to '${newName}'`);\n  } catch (error) {\n    spinner.fail(\n      `Cannot rename the project '${currentName}' to '${newName}': ${\n        stringifyError(error, { verbose: true })\n      }`,\n    );\n  }\n}\n"
  },
  {
    "path": "src/subcommands/top.ts",
    "content": "// Copyright 2021 Deno Land Inc. All rights reserved. MIT license.\n\nimport type { Args } from \"../args.ts\";\nimport { API } from \"../utils/api.ts\";\nimport TokenProvisioner from \"../utils/access_token.ts\";\nimport { wait } from \"../utils/spinner.ts\";\nimport * as tty from \"@denosaurs/tty\";\nimport { delay } from \"@std/async/delay\";\nimport { encodeHex } from \"@std/encoding/hex\";\nimport { error } from \"../error.ts\";\nimport type { ProjectStats } from \"../utils/api_types.ts\";\nimport { sha256 } from \"../utils/hashing_encoding.ts\";\nimport { stringify as stringifyError } from \"../error.ts\";\n\nconst help = `\nProject monitoring (ALPHA)\n\nDefinition of the table columns:\n\n    idx         Instance discriminator. Opaque id to discriminate different executions running in the same region.\n    Deployment  The id of the deployment running in the executing instance.\n    Req/min     Requests per minute received by the project.\n    CPU%        Percentage of CPU used by the project.\n    CPU/req     CPU time per request, in milliseconds. \n    RSS/5min    Max RSS used by the project during the last 5 minutes, in MB. \n    Ingress/min Data received by the project per minute, in KB.\n    Egress/min  Data outputed by the project per minute, in KB.\n    KVr/min     KV reads performed by the project per minute.\n    KVw/min     KV writes performed by the project per minute.\n    QSenq/min   Queues enqueues performed by the project per minute.\n    QSdeq/min   Queues dequeues performed by the project per minute.\n\nUSAGE:\n    deployctl top [OPTIONS]\n\nOPTIONS:\n    -h, --help                    Prints this help information\n    -p, --project=<NAME|ID>       The project to monitor.\n        --token=<TOKEN>           The API token to use (defaults to DENO_DEPLOY_TOKEN env var)\n        --config=<PATH>           Path to the file from where to load DeployCTL config. Defaults to 'deno.json'\n        --color=<auto|always|off> Enable colored output. Defaults to 'auto' (colored when stdout is a tty)\n        --format=<table|json>     Output the project stats in a table or JSON-encoded. Defaults to 'table' when stdout is a tty, and 'json' otherwise.\n        --region=<REGION>         Show stats from only specific regions. Can be used multiple times (--region=us-east4 --region=us-west2).\n                                  Can also be a substring (--region=us)\n`;\n\nexport default async function topSubcommand(args: Args) {\n  if (args.help) {\n    console.log(help);\n    Deno.exit(0);\n  }\n  if (!args.project) {\n    error(\n      \"No project specified. Use --project to specify the project of which to stream the stats\",\n    );\n  }\n  let format: \"table\" | \"json\";\n  switch (args.format) {\n    case \"table\":\n    case \"json\":\n      format = args.format;\n      break;\n    case undefined:\n      format = Deno.stdout.isTerminal() ? \"table\" : \"json\";\n      break;\n    default:\n      error(\n        `Invalid format '${args.format}'. Supported values for the --format option are 'table' or 'json'`,\n      );\n  }\n\n  const spinner = wait(\n    `Connecting to the stats stream of project '${args.project}'...`,\n  ).start();\n  const api = args.token\n    ? API.fromToken(args.token)\n    : API.withTokenProvisioner(TokenProvisioner);\n  let stats;\n  try {\n    stats = await api.streamMetering(args.project!);\n  } catch (err) {\n    spinner.fail(\n      `Failed to connect to the stats stream of project '${args.project}': ${\n        stringifyError(err, { verbose: true })\n      }`,\n    );\n    return Deno.exit(1);\n  }\n  spinner.succeed(\n    `Connected to the stats stream of project '${args.project}'`,\n  );\n  if (args.region.length !== 0) {\n    const allStats = stats;\n    const filter = args.region.flatMap((r) => r.split(\",\")).map((r) =>\n      r.trim()\n    );\n    stats = async function* () {\n      for await (const line of allStats) {\n        for (const region of filter) {\n          if (line.region.includes(region)) {\n            yield line;\n            break;\n          }\n        }\n      }\n    }();\n  }\n  switch (format) {\n    case \"table\":\n      return await tabbed(stats);\n    case \"json\":\n      return await json(stats);\n  }\n}\n\nasync function tabbed(stats: AsyncGenerator<ProjectStats, void>) {\n  const table: { [id: string]: { region: string; [other: string]: unknown } } =\n    {};\n  const timeouts: { [id: string]: number } = {};\n  const toDelete: string[] = [];\n  const spinner = wait(\"Streaming...\").start();\n  let previousLength = 0;\n  const renderStream = async function* () {\n    // First render after 1 sec in case there's already data\n    await delay(1_000);\n    yield true;\n    while (true) {\n      await delay(5_000);\n      yield true;\n    }\n  }();\n  try {\n    let next = stats.next();\n    let render = renderStream.next();\n    while (true) {\n      const result = await Promise.race([next, render]);\n      const stat = result.value;\n      if (stat === undefined) {\n        // Only stats stream can end, returning undefined\n        spinner.succeed(\"Stream ended\");\n        return;\n      }\n      if (typeof stat === \"object\") {\n        next = stats.next();\n        const id = encodeHex(\n          await sha256(stat.id + stat.region + stat.deploymentId),\n        )\n          .slice(0, 6);\n        table[id] = {\n          \"deployment\": stat.deploymentId,\n          \"region\": stat.region,\n          \"Req/min\": Math.ceil(stat.requestsPerMinute),\n          \"CPU%\": parseFloat((stat.cpuTimePerSecond / 10).toFixed(2)),\n          \"CPU/req\": parseFloat((stat.cpuTimePerRequest || 0).toFixed(2)),\n          \"RSS/5min\": parseFloat(\n            (stat.maxRss5Minutes / 1_000_000).toFixed(3),\n          ),\n          \"Ingress/min\": parseFloat(\n            (stat.ingressBytesPerMinute / 1_000).toFixed(3),\n          ),\n          \"Egress/min\": parseFloat(\n            (stat.egressBytesPerMinute / 1_000).toFixed(3),\n          ),\n          \"KVr/min\": Math.ceil(stat.kvReadUnitsPerMinute),\n          \"KVw/min\": Math.ceil(stat.kvWriteUnitsPerMinute),\n          \"QSenq/min\": Math.ceil(stat.enqueuePerMinute),\n          \"QSdeq/min\": Math.ceil(stat.dequeuePerMinute),\n        };\n\n        clearTimeout(timeouts[id]);\n        timeouts[id] = setTimeout(\n          (idToDelete: string) => {\n            toDelete.push(idToDelete);\n          },\n          30_000,\n          id,\n        );\n      } else {\n        render = renderStream.next();\n        while (toDelete.length > 0) {\n          const idToDelete = toDelete.pop();\n          if (idToDelete) {\n            delete table[idToDelete];\n          }\n        }\n        const linesToClear = previousLength ? previousLength + 5 : 1;\n        previousLength = Object.keys(table).length;\n        tty.goUpSync(linesToClear, Deno.stdout);\n        tty.clearDownSync(Deno.stdout);\n        const entries = Object.entries(table);\n        // Kinda sort the table\n        entries.sort(([_aid, a], [_bid, b]) =>\n          a.region.localeCompare(b.region)\n        );\n        if (Object.keys(table).length > 0) {\n          console.table(Object.fromEntries(entries));\n        }\n        console.log();\n      }\n    }\n  } catch (error) {\n    spinner.fail(`Stream disconnected: ${error}`);\n    Deno.exit(1);\n  }\n}\n\nasync function json(stats: AsyncGenerator<ProjectStats>) {\n  for await (const stat of stats) {\n    console.log(JSON.stringify(stat));\n  }\n}\n"
  },
  {
    "path": "src/subcommands/upgrade.ts",
    "content": "// Copyright 2021 Deno Land Inc. All rights reserved. MIT license.\n\nimport { error } from \"../error.ts\";\nimport {\n  canParse as semverValid,\n  greaterOrEqual as semverGreaterThanOrEquals,\n  parse as semverParse,\n} from \"@std/semver\";\nimport { VERSION } from \"../version.ts\";\n\nconst help = `deployctl upgrade\nUpgrade deployctl to the given version (defaults to latest).\n\nTo upgrade to latest version:\ndeployctl upgrade\n\nTo upgrade to specific version:\ndeployctl upgrade 1.2.3\n\nThe version is downloaded from https://deno.land/x/deploy/deployctl.ts\n\nUSAGE:\n    deployctl upgrade [OPTIONS] [<version>]\n\nOPTIONS:\n    -h, --help        Prints help information\n\nARGS:\n    <version>         The version to upgrade to (defaults to latest)\n`;\n\nexport interface Args {\n  help: boolean;\n}\n\n// deno-lint-ignore no-explicit-any\nexport default async function (rawArgs: Record<string, any>): Promise<void> {\n  const args: Args = {\n    help: !!rawArgs.help,\n  };\n  const version = typeof rawArgs._[0] === \"string\" ? rawArgs._[0] : null;\n  if (args.help) {\n    console.log(help);\n    Deno.exit();\n  }\n  if (rawArgs._.length > 1) {\n    console.error(help);\n    error(\"Too many positional arguments given.\");\n  }\n  if (version && !semverValid(version)) {\n    error(`The provided version is invalid.`);\n  }\n\n  const { latest, versions } = await getVersions().catch((err: TypeError) => {\n    error(err.message);\n  });\n  if (version && !versions.includes(version)) {\n    error(\n      \"The provided version is not found.\\n\\nVisit https://github.com/denoland/deployctl/releases/ for available releases.\",\n    );\n  }\n\n  if (\n    !version &&\n    semverGreaterThanOrEquals(semverParse(VERSION), semverParse(latest))\n  ) {\n    console.log(\"You're using the latest version.\");\n    Deno.exit();\n  } else {\n    const process = new Deno.Command(Deno.execPath(), {\n      args: [\n        \"install\",\n        \"--allow-read\",\n        \"--allow-write\",\n        \"--allow-env\",\n        \"--allow-net\",\n        \"--allow-run\",\n        \"--allow-sys\",\n        \"--no-check\",\n        \"--force\",\n        \"--quiet\",\n        `https://deno.land/x/deploy@${version ? version : latest}/deployctl.ts`,\n      ],\n    }).spawn();\n    await process.status;\n  }\n}\n\nexport async function getVersions(): Promise<\n  { latest: string; versions: string[] }\n> {\n  const aborter = new AbortController();\n  const timer = setTimeout(() => aborter.abort(), 2500);\n  const response = await fetch(\n    \"https://cdn.deno.land/deploy/meta/versions.json\",\n    { signal: aborter.signal },\n  );\n  if (!response.ok) {\n    throw new Error(\n      \"couldn't fetch the latest version - try again after sometime\",\n    );\n  }\n  const data = await response.json();\n  clearTimeout(timer);\n  return data;\n}\n"
  },
  {
    "path": "src/utils/access_token.ts",
    "content": "import { interruptSpinner, wait } from \"./spinner.ts\";\nimport { error } from \"../error.ts\";\nimport { endpoint, USER_AGENT } from \"./api.ts\";\nimport tokenStorage from \"./token_storage.ts\";\nimport { base64url, sha256 } from \"./hashing_encoding.ts\";\nimport { stringify as stringifyError } from \"../error.ts\";\n\nexport default {\n  get: tokenStorage.get,\n\n  async provision() {\n    // Synchronize provision routine\n    // to prevent multiple authorization flows from triggering concurrently\n    this.provisionPromise ??= provision();\n    const token = await this.provisionPromise;\n    this.provisionPromise = null;\n    return token;\n  },\n  provisionPromise: null as Promise<string> | null,\n\n  revoke: tokenStorage.remove,\n};\n\nasync function provision(): Promise<string> {\n  const spinnerInterrupted = interruptSpinner();\n  wait(\"\").start().info(\"Provisioning a new access token...\");\n  const randomBytes = crypto.getRandomValues(new Uint8Array(32));\n  const claimVerifier = base64url(randomBytes);\n  const claimChallenge = base64url(await sha256(claimVerifier));\n\n  const tokenStream = await fetch(\n    `${endpoint()}/api/signin/cli/access_token`,\n    {\n      method: \"POST\",\n      headers: { \"User-Agent\": USER_AGENT },\n      body: claimVerifier,\n    },\n  );\n  if (!tokenStream.ok) {\n    error(\n      `when requesting an access token: ${await tokenStream.statusText}`,\n    );\n  }\n  const url = `${endpoint()}/signin/cli?claim_challenge=${claimChallenge}`;\n\n  wait(\"\").start().info(`Authorization URL: ${url}`);\n  let openCmd;\n  const args = [];\n  // TODO(arnauorriols): use npm:open or deno.land/x/open when either is compatible\n  switch (Deno.build.os) {\n    case \"darwin\": {\n      openCmd = \"open\";\n      break;\n    }\n    case \"linux\": {\n      openCmd = \"xdg-open\";\n      break;\n    }\n    case \"windows\": {\n      // Windows Start-Process is a cmdlet of PowerShell\n      openCmd = \"PowerShell.exe\";\n      args.push(\"Start-Process\");\n      break;\n    }\n  }\n  args.push(url);\n  let open;\n  if (openCmd !== undefined) {\n    try {\n      open = new Deno.Command(openCmd, {\n        args,\n        stderr: \"piped\",\n        stdout: \"piped\",\n      })\n        .spawn();\n    } catch (error) {\n      wait(\"\").start().warn(\n        \"Unexpected error while trying to open the authorization URL in your default browser. Please report it at https://github.com/denoland/deployctl/issues/new.\",\n      );\n      wait({ text: \"\", indent: 3 }).start().fail(stringifyError(error));\n    }\n  }\n  if (open == undefined) {\n    const warn =\n      \"Cannot open the authorization URL automatically. Please navigate to it manually using your usual browser\";\n    wait(\"\").start().info(warn);\n  } else if (!(await open.status).success) {\n    const warn =\n      \"Failed to open the authorization URL in your default browser. Please navigate to it manually\";\n    wait(\"\").start().warn(warn);\n    let error = new TextDecoder().decode((await open.output()).stderr);\n    const errIndent = 2;\n    const elipsis = \"...\";\n    const maxErrLength = warn.length - errIndent;\n    if (error.length > maxErrLength) {\n      error = error.slice(0, maxErrLength - elipsis.length) + elipsis;\n    }\n    // resulting indentation is 1 less than configured\n    wait({ text: \"\", indent: errIndent + 1 }).start().fail(error);\n  }\n\n  const spinner = wait(\"Waiting for authorization...\").start();\n\n  const tokenOrError = await tokenStream.json();\n\n  if (tokenOrError.error) {\n    error(`could not provision the access token: ${tokenOrError.error}`);\n  }\n\n  await tokenStorage.store(tokenOrError.token);\n  spinner.succeed(\"Token obtained successfully\");\n  spinnerInterrupted.resume();\n  return tokenOrError.token;\n}\n"
  },
  {
    "path": "src/utils/api.ts",
    "content": "import { delay } from \"@std/async/delay\";\nimport { TextLineStream } from \"@std/streams/text_line_stream\";\nimport { VERSION } from \"../version.ts\";\n\nimport type {\n  Build,\n  BuildsPage,\n  Cron,\n  Database,\n  DeploymentProgress,\n  DeploymentV1,\n  Domain,\n  GitHubActionsDeploymentRequest,\n  LiveLog,\n  LogQueryRequestParams,\n  ManifestEntry,\n  Metadata,\n  Organization,\n  PagingInfo,\n  PersistedLog,\n  Project,\n  ProjectStats,\n  PushDeploymentRequest,\n} from \"./api_types.ts\";\nimport { interruptSpinner, wait } from \"./spinner.ts\";\n\nexport const USER_AGENT =\n  `DeployCTL/${VERSION} (${Deno.build.os} ${Deno.osRelease()}; ${Deno.build.arch})`;\n\nexport interface RequestOptions {\n  method?: string;\n  body?: unknown;\n  accept?: string;\n}\n\nexport class APIError extends Error {\n  code: string;\n  xDenoRay: string | null;\n\n  override name = \"APIError\";\n\n  constructor(code: string, message: string, xDenoRay: string | null) {\n    super(message);\n    this.code = code;\n    this.xDenoRay = xDenoRay;\n  }\n\n  override toString() {\n    let error = `${this.name}: ${this.message}`;\n    if (this.xDenoRay !== null) {\n      error += `\\nx-deno-ray: ${this.xDenoRay}`;\n      error += \"\\nIf you encounter this error frequently,\" +\n        \" contact us at deploy@deno.com with the above x-deno-ray.\";\n    }\n    return error;\n  }\n}\n\nexport function endpoint(): string {\n  return Deno.env.get(\"DEPLOY_API_ENDPOINT\") ?? \"https://dash.deno.com\";\n}\n\ninterface TokenProvisioner {\n  /**\n   * Get the access token from a secure local storage or any other cache form.\n   * If there isn't any token cached, returns `null`.\n   */\n  get(): Promise<string | null>;\n  /**\n   * Provision a new access token for DeployCTL\n   */\n  provision(): Promise<string>;\n  /**\n   * Delete the token from cache, forcing a new provision in the next request\n   */\n  revoke(): Promise<void>;\n}\n\ninterface Logger {\n  debug: (message: string) => void;\n  info: (message: string) => void;\n  notice: (message: string) => void;\n  warning: (message: string) => void;\n  error: (message: string) => void;\n}\n\ninterface APIConfig {\n  /**\n   * When enabled, x-deno-ray in responses will always be printed even if the\n   * request is successful.\n   */\n  alwaysPrintXDenoRay: boolean;\n  /**\n   * Logger interface to use for logging certain events\n   */\n  logger: Logger;\n}\n\nexport class API {\n  #endpoint: string;\n  #authorization: string | TokenProvisioner;\n  #config: APIConfig;\n\n  constructor(\n    authorization: string | TokenProvisioner,\n    endpoint: string,\n    config?: Partial<APIConfig>,\n  ) {\n    this.#authorization = authorization;\n    this.#endpoint = endpoint;\n\n    const DEFAULT_CONFIG: APIConfig = {\n      alwaysPrintXDenoRay: false,\n      logger: {\n        debug: (m) => console.debug(m),\n        info: (m) => console.info(m),\n        notice: (m) => console.log(m),\n        warning: (m) => console.warn(m),\n        error: (m) => console.error(m),\n      },\n    };\n\n    this.#config = DEFAULT_CONFIG;\n    this.#config.alwaysPrintXDenoRay = config?.alwaysPrintXDenoRay ??\n      DEFAULT_CONFIG.alwaysPrintXDenoRay;\n    this.#config.logger = config?.logger ?? DEFAULT_CONFIG.logger;\n  }\n\n  static fromToken(token: string) {\n    return new API(`Bearer ${token}`, endpoint());\n  }\n\n  static withTokenProvisioner(provisioner: TokenProvisioner) {\n    return new API(provisioner, endpoint());\n  }\n\n  async request(path: string, opts: RequestOptions = {}): Promise<Response> {\n    const url = `${this.#endpoint}/api${path}`;\n    const method = opts.method ?? \"GET\";\n    const body = typeof opts.body === \"string\" || opts.body instanceof FormData\n      ? opts.body\n      : JSON.stringify(opts.body);\n    const authorization = typeof this.#authorization === \"string\"\n      ? this.#authorization\n      : `Bearer ${\n        await this.#authorization.get() ?? await this.#authorization.provision()\n      }`;\n    const sudo = Deno.env.get(\"SUDO\");\n    const headers = {\n      \"User-Agent\": USER_AGENT,\n      \"Accept\": opts.accept ?? \"application/json\",\n      \"Authorization\": authorization,\n      ...(opts.body !== undefined\n        ? opts.body instanceof FormData\n          ? {}\n          : { \"Content-Type\": \"application/json\" }\n        : {}),\n      ...(sudo ? { [\"x-deploy-sudo\"]: sudo } : {}),\n    };\n    let res = await fetch(url, { method, headers, body });\n    if (this.#config.alwaysPrintXDenoRay) {\n      this.#config.logger.notice(\n        `x-deno-ray: ${res.headers.get(\"x-deno-ray\")}`,\n      );\n    }\n    if (res.status === 401 && typeof this.#authorization === \"object\") {\n      // Token expired or revoked. Provision again and retry\n      headers.Authorization = `Bearer ${await this.#authorization.provision()}`;\n      res = await fetch(url, { method, headers, body });\n    }\n    return res;\n  }\n\n  async #requestJson<T>(path: string, opts?: RequestOptions): Promise<T> {\n    const res = await this.request(path, opts);\n    if (res.headers.get(\"Content-Type\") !== \"application/json\") {\n      const text = await res.text();\n      throw new Error(`Expected JSON, got '${text}'`);\n    }\n    const json = await res.json();\n    if (res.status !== 200) {\n      const xDenoRay = res.headers.get(\"x-deno-ray\");\n      throw new APIError(json.code, json.message, xDenoRay);\n    }\n    return json;\n  }\n\n  async #requestStream(\n    path: string,\n    opts?: RequestOptions,\n  ): Promise<AsyncGenerator<string, void>> {\n    const res = await this.request(path, opts);\n    if (res.status !== 200) {\n      const json = await res.json();\n      const xDenoRay = res.headers.get(\"x-deno-ray\");\n      throw new APIError(json.code, json.message, xDenoRay);\n    }\n    if (res.body === null) {\n      throw new Error(\"Stream ended unexpectedly\");\n    }\n\n    const lines: ReadableStream<string> = res.body\n      .pipeThrough(new TextDecoderStream())\n      .pipeThrough(new TextLineStream());\n    return async function* (): AsyncGenerator<string, void> {\n      for await (const line of lines) {\n        if (line === \"\") return;\n        yield line;\n      }\n    }();\n  }\n\n  async #requestJsonStream<T>(\n    path: string,\n    opts?: RequestOptions,\n  ): Promise<AsyncGenerator<T, void>> {\n    const stream = await this.#requestStream(path, opts);\n    return async function* () {\n      for await (const line of stream) {\n        yield JSON.parse(line);\n      }\n    }();\n  }\n\n  async getOrganizationByName(name: string): Promise<Organization | undefined> {\n    const organizations: Organization[] = await this.#requestJson(\n      `/organizations`,\n    );\n    for (const org of organizations) {\n      if (org.name === name) {\n        return org;\n      }\n    }\n  }\n\n  async getOrganizationById(id: string): Promise<Organization> {\n    return await this.#requestJson(`/organizations/${id}`);\n  }\n\n  async createOrganization(name: string): Promise<Organization> {\n    const body = { name };\n    return await this.#requestJson(\n      `/organizations`,\n      { method: \"POST\", body },\n    );\n  }\n\n  async listOrganizations(): Promise<Organization[]> {\n    return await this.#requestJson(`/organizations`);\n  }\n\n  async getProject(id: string): Promise<Project | null> {\n    try {\n      return await this.#requestJson(`/projects/${id}`);\n    } catch (err) {\n      if (err instanceof APIError && err.code === \"projectNotFound\") {\n        return null;\n      }\n      throw err;\n    }\n  }\n\n  async createProject(\n    name?: string,\n    organizationId?: string,\n    envs?: Record<string, string>,\n  ): Promise<Project> {\n    const body = { name, organizationId, envs };\n    return await this.#requestJson(`/projects/`, { method: \"POST\", body });\n  }\n\n  async renameProject(\n    id: string,\n    newName: string,\n  ): Promise<void> {\n    const body = { name: newName };\n    await this.#requestJson(`/projects/${id}`, { method: \"PATCH\", body });\n  }\n\n  async deleteProject(\n    id: string,\n  ): Promise<boolean> {\n    try {\n      await this.#requestJson(`/projects/${id}`, { method: \"DELETE\" });\n      return true;\n    } catch (err) {\n      if (err instanceof APIError && err.code === \"projectNotFound\") {\n        return false;\n      }\n      throw err;\n    }\n  }\n\n  async listProjects(\n    orgId: string,\n  ): Promise<Project[]> {\n    const org: { projects: Project[] } = await this.#requestJson(\n      `/organizations/${orgId}`,\n    );\n    return org.projects;\n  }\n\n  async getDomains(projectId: string): Promise<Domain[]> {\n    return await this.#requestJson(`/projects/${projectId}/domains`);\n  }\n\n  async listDeployments(\n    projectId: string,\n    page?: number,\n    limit?: number,\n  ): Promise<BuildsPage | null> {\n    const query = new URLSearchParams();\n    if (page !== undefined) {\n      query.set(\"page\", page.toString());\n    }\n    if (limit !== undefined) {\n      query.set(\"limit\", limit.toString());\n    }\n    try {\n      const [list, paging]: [Build[], PagingInfo] = await this.#requestJson(\n        `/projects/${projectId}/deployments?${query}`,\n      );\n      return { list, paging };\n    } catch (err) {\n      if (err instanceof APIError && err.code === \"projectNotFound\") {\n        return null;\n      }\n      throw err;\n    }\n  }\n\n  async *listAllDeployments(\n    projectId: string,\n  ): AsyncGenerator<Build> {\n    let totalPages = 1;\n    let page = 0;\n    while (totalPages > page) {\n      const [deployments, paging]: [Build[], PagingInfo] = await this\n        .#requestJson(\n          `/projects/${projectId}/deployments/?limit=50&page=${page}`,\n        );\n      for (const deployment of deployments) {\n        yield deployment;\n      }\n      totalPages = paging.totalPages;\n      page = paging.page + 1;\n    }\n  }\n\n  async getDeployment(\n    deploymentId: string,\n  ): Promise<Build | null> {\n    try {\n      return await this.#requestJson(`/deployments/${deploymentId}`);\n    } catch (err) {\n      if (err instanceof APIError && err.code === \"deploymentNotFound\") {\n        return null;\n      }\n      throw err;\n    }\n  }\n\n  async deleteDeployment(\n    deploymentId: string,\n  ): Promise<boolean> {\n    try {\n      await this.#requestJson(`/v1/deployments/${deploymentId}`, {\n        method: \"DELETE\",\n      });\n      return true;\n    } catch (err) {\n      if (err instanceof APIError && err.code === \"deploymentNotFound\") {\n        return false;\n      }\n      throw err;\n    }\n  }\n\n  async redeployDeployment(\n    deploymentId: string,\n    redeployParams: {\n      prod?: boolean;\n      env_vars?: Record<string, string | null>;\n      databases?: { default: string };\n    },\n  ): Promise<DeploymentV1 | null> {\n    try {\n      return await this.#requestJson(\n        `/v1/deployments/${deploymentId}/redeploy?internal=true`,\n        {\n          method: \"POST\",\n          body: redeployParams,\n        },\n      );\n    } catch (err) {\n      if (err instanceof APIError && err.code === \"deploymentNotFound\") {\n        return null;\n      }\n      throw err;\n    }\n  }\n\n  getLogs(\n    projectId: string,\n    deploymentId: string,\n  ): Promise<AsyncGenerator<LiveLog>> {\n    return this.#requestJsonStream(\n      `/projects/${projectId}/deployments/${deploymentId}/logs/`,\n      {\n        accept: \"application/x-ndjson\",\n      },\n    );\n  }\n\n  async queryLogs(\n    projectId: string,\n    deploymentId: string,\n    params: LogQueryRequestParams,\n  ): Promise<{ logs: PersistedLog[] }> {\n    const searchParams = new URLSearchParams({\n      params: JSON.stringify(params),\n    });\n    return await this.#requestJson(\n      `/projects/${projectId}/deployments/${deploymentId}/query_logs?${searchParams.toString()}`,\n    );\n  }\n\n  async projectNegotiateAssets(\n    id: string,\n    manifest: { entries: Record<string, ManifestEntry> },\n  ): Promise<string[]> {\n    return await this.#requestJson(`/projects/${id}/assets/negotiate`, {\n      method: \"POST\",\n      body: manifest,\n    });\n  }\n\n  pushDeploy(\n    projectId: string,\n    request: PushDeploymentRequest,\n    files: Uint8Array[],\n  ): Promise<AsyncGenerator<DeploymentProgress>> {\n    const form = new FormData();\n    form.append(\"request\", JSON.stringify(request));\n    for (const bytes of files) {\n      form.append(\"file\", new Blob([bytes]));\n    }\n    return this.#requestJsonStream(\n      `/projects/${projectId}/deployment_with_assets`,\n      { method: \"POST\", body: form },\n    );\n  }\n\n  gitHubActionsDeploy(\n    projectId: string,\n    request: GitHubActionsDeploymentRequest,\n    files: Uint8Array[],\n  ): Promise<AsyncGenerator<DeploymentProgress>> {\n    const form = new FormData();\n    form.append(\"request\", JSON.stringify(request));\n    for (const bytes of files) {\n      form.append(\"file\", new Blob([bytes]));\n    }\n    return this.#requestJsonStream(\n      `/projects/${projectId}/deployment_github_actions`,\n      { method: \"POST\", body: form },\n    );\n  }\n\n  getMetadata(): Promise<Metadata> {\n    return this.#requestJson(\"/meta\");\n  }\n\n  async streamMetering(\n    project: string,\n  ): Promise<AsyncGenerator<ProjectStats, void>> {\n    const streamGen = () => this.#requestStream(`/projects/${project}/stats`);\n    let stream = await streamGen();\n    return async function* () {\n      for (;;) {\n        try {\n          for await (const line of stream) {\n            try {\n              yield JSON.parse(line);\n            } catch {\n              // Stopgap while the streaming errors are fixed\n            }\n          }\n        } catch (error) {\n          // Stopgap while the streaming errors are fixed\n          const interrupt = interruptSpinner();\n          const spinner = wait(`Error: ${error}. Reconnecting...`).start();\n          await delay(5_000);\n          stream = await streamGen();\n          spinner.stop();\n          interrupt.resume();\n        }\n      }\n    }();\n  }\n\n  async getProjectDatabases(project: string): Promise<Database[] | null> {\n    try {\n      return await this.#requestJson(`/projects/${project}/databases`);\n    } catch (err) {\n      if (err instanceof APIError && err.code === \"projectNotFound\") {\n        return null;\n      }\n      throw err;\n    }\n  }\n\n  async getDeploymentCrons(\n    projectId: string,\n    deploymentId: string,\n  ): Promise<Cron[]> {\n    return await this.#requestJson(\n      `/projects/${projectId}/deployments/${deploymentId}/crons`,\n    );\n  }\n\n  async getProjectCrons(\n    projectId: string,\n  ): Promise<Cron[] | null> {\n    try {\n      return await this.#requestJson(\n        `/projects/${projectId}/deployments/latest/crons`,\n      );\n    } catch (err) {\n      // When the project does not have a production deployment, API returns deploymentNotFound\n      if (err instanceof APIError && err.code === \"deploymentNotFound\") {\n        return null;\n      }\n      throw err;\n    }\n  }\n}\n"
  },
  {
    "path": "src/utils/api_types.ts",
    "content": "export interface DomainMapping {\n  domain: string;\n  createdAt: string;\n  updatedAt: string;\n}\n\nexport interface Build {\n  id: string;\n  relatedCommit?: {\n    hash: string;\n    branch?: string;\n    message: string;\n    authorName: string;\n    authorEmail: string;\n    authorGithubUsername: string;\n    url: string;\n  };\n  deployment: Deployment | null;\n  deploymentId: string;\n  project: Project;\n  createdAt: string;\n  logs: DeploymentProgress[];\n}\n\nexport interface Deployment {\n  id: string;\n  description: string;\n  url: string;\n  domainMappings: DomainMapping[];\n  project?: Project;\n  projectId: string;\n  createdAt: string;\n  updatedAt: string;\n  envVars: string[];\n  kvDatabases: Record<string, string>;\n}\n\nexport type DeploymentV1 = {\n  id: string;\n  projectId: string;\n  description?: string;\n  status: \"failed\" | \"pending\" | \"success\";\n  domains: string[];\n  databases: Record<string, string>;\n  createdAt: string;\n  updatedAt: string;\n};\n\nexport interface BuildsPage {\n  list: Build[];\n  paging: PagingInfo;\n}\n\nexport interface Project {\n  id: string;\n  name: string;\n  type: \"git\" | \"playground\";\n  git?: {\n    repository: { owner: string; name: string };\n    productionBranch: string;\n  };\n  productionDeployment?: Build | null;\n  hasProductionDeployment: boolean;\n  organizationId: string;\n  organization: Organization;\n  createdAt: string;\n  updatedAt: string;\n  envVars: string[];\n}\n\nexport type Organization = UserOrganization | NormalOrganization;\n\nexport type UserOrganization = CommonOrganization & {\n  name: null;\n};\n\nexport type NormalOrganization = CommonOrganization & {\n  name: string;\n};\n\nexport interface CommonOrganization {\n  id: string;\n  members: OrganizationMember[];\n}\n\nexport interface OrganizationMember {\n  user: User;\n}\n\nexport interface User {\n  name: string;\n}\n\nexport interface PagingInfo {\n  page: number;\n  count: number;\n  limit: number;\n  totalCount: number;\n  totalPages: number;\n}\n\nexport interface ManifestEntryFile {\n  kind: \"file\";\n  gitSha1: string;\n  size: number;\n}\n\nexport interface ManifestEntryDirectory {\n  kind: \"directory\";\n  entries: Record<string, ManifestEntry>;\n}\n\nexport interface ManifestEntrySymlink {\n  kind: \"symlink\";\n  target: string;\n}\n\nexport type ManifestEntry =\n  | ManifestEntryFile\n  | ManifestEntryDirectory\n  | ManifestEntrySymlink;\n\nexport interface PushDeploymentRequest {\n  url: string;\n  importMapUrl: string | null;\n  production: boolean;\n  manifest?: { entries: Record<string, ManifestEntry> };\n}\n\nexport interface GitHubActionsDeploymentRequest {\n  url: string;\n  importMapUrl: string | null;\n  manifest: { entries: Record<string, ManifestEntry> };\n  event?: unknown;\n}\n\nexport type DeploymentProgress =\n  | DeploymentProgressStaticFile\n  | DeploymentProgressLoad\n  | DeploymentProgressUploadComplete\n  | DeploymentProgressSuccess\n  | DeploymentProgressError;\n\nexport interface DeploymentProgressStaticFile {\n  type: \"staticFile\";\n  currentBytes: number;\n  totalBytes: number;\n}\n\nexport interface DeploymentProgressLoad {\n  type: \"load\";\n  url: string;\n  seen: number;\n  total: number;\n}\n\nexport interface DeploymentProgressUploadComplete {\n  type: \"uploadComplete\";\n}\n\nexport interface DeploymentProgressSuccess extends Deployment {\n  type: \"success\";\n}\n\nexport interface DeploymentProgressError {\n  type: \"error\";\n  code: string;\n  ctx: string;\n}\n\nexport interface LiveLogReady {\n  type: \"ready\";\n}\n\nexport interface LiveLogPing {\n  type: \"ping\";\n}\n\nexport interface LiveLogMessage {\n  type: \"message\";\n  time: string;\n  message: string;\n  level: \"debug\" | \"info\" | \"warning\" | \"error\";\n  region: string;\n}\n\nexport type LiveLog =\n  | LiveLogReady\n  | LiveLogPing\n  | LiveLogMessage;\n\nexport interface LogQueryRequestParams {\n  regions?: string[];\n  levels?: string[];\n  // RFC3339\n  since?: string;\n  // RFC3339\n  until?: string;\n  q?: string[];\n  limit?: number;\n}\n\nexport interface PersistedLog {\n  deploymentId: string;\n  isolateId: string;\n  region: string;\n  level: \"debug\" | \"info\" | \"warning\" | \"error\";\n  // RFC3339\n  timestamp: string;\n  message: string;\n}\n\nexport interface Metadata {\n  regionCodes: string[];\n}\n\nexport interface Domain {\n  domain: string;\n  isValidated: boolean;\n}\n\nexport interface ProjectStats {\n  id: string;\n  region: string;\n  projectId: string;\n  deploymentId: string;\n  uptime: number;\n  requestsPerMinute: number;\n  cpuTimePerSecond: number;\n  cpuTimePerRequest: number;\n  maxRss5Minutes: number;\n  ingressBytesPerMinute: number;\n  egressBytesPerMinute: number;\n  kvReadUnitsPerMinute: number;\n  kvWriteUnitsPerMinute: number;\n  enqueuePerMinute: number;\n  dequeuePerMinute: number;\n}\n\nexport interface Database {\n  branch: string;\n  databaseId: string;\n  bindingName: string;\n  description: string;\n  sizeBytes?: number;\n  availableRegions: string[];\n  createdAt: string;\n  updatedAt: string;\n}\n\nexport interface Cron {\n  cron_spec: {\n    name: string;\n    schedule: string;\n    backoff_schedule?: number;\n  };\n  status?: CronStatus;\n  history: CronExecutionRetry[][];\n}\n\nexport interface CronExecutionRetry {\n  status: \"success\" | \"failure\" | \"executing\";\n  start_ms: number;\n  end_ms: number;\n  error_message?: string;\n  deployment_id: string;\n}\n\ntype CronStatus =\n  | { status: \"unscheduled\" }\n  | { status: \"scheduled\"; deadline_ms: number }\n  | { status: \"executing\"; retries: CronExecutionRetry[] };\n"
  },
  {
    "path": "src/utils/crons.ts",
    "content": "import { green, red, stripAnsiCode } from \"@std/fmt/colors\";\nimport type { Cron, CronExecutionRetry } from \"./api_types.ts\";\nimport { renderTimeDelta } from \"./time.ts\";\nexport function renderCron(cron: Cron): string {\n  return `${cron.cron_spec.name} [${cron.cron_spec.schedule}] ${\n    renderCronStatus(cron)\n  }`;\n}\n\nfunction renderCronStatus(cron: Cron): string {\n  if (!cron.status) {\n    return \"n/a\";\n  }\n  switch (cron.status.status) {\n    case \"unscheduled\":\n      return `${\n        cron.history.length > 0\n          ? `${renderLastCronExecution(cron.history[0][0])} `\n          : \"\"\n      }(unscheduled)`;\n    case \"executing\":\n      if (cron.status.retries.length > 0) {\n        return `${\n          renderLastCronExecution(cron.status.retries[0])\n        } (retrying...)`;\n      } else {\n        return \"(executing...)\";\n      }\n    case \"scheduled\":\n      return `${\n        cron.history.length > 0\n          ? `${renderLastCronExecution(cron.history[0][0])} `\n          : \"\"\n      }(next at ${\n        new Date(cron.status.deadline_ms).toLocaleString(navigator.language, {\n          timeZoneName: \"short\",\n        })\n      })`;\n  }\n}\n\nfunction renderLastCronExecution(execution: CronExecutionRetry): string {\n  const start = new Date(execution.start_ms);\n  const end = new Date(execution.end_ms);\n  const duration = end.getTime() - start.getTime();\n  const status = execution.status === \"success\"\n    ? green(\"succeeded\")\n    : execution.status === \"failure\"\n    ? red(\"failed\")\n    : \"executing\";\n  return `${status} at ${\n    start.toLocaleString(navigator.language, { timeZoneName: \"short\" })\n  } after ${stripAnsiCode(renderTimeDelta(duration))}`;\n}\n"
  },
  {
    "path": "src/utils/entrypoint.ts",
    "content": "import { resolve, toFileUrl } from \"@std/path\";\nimport { stringify as stringifyError } from \"../error.ts\";\n\n/**\n * Parses the entrypoint to a URL.\n * Ensures the file exists when the entrypoint is a local file.\n */\nexport async function parseEntrypoint(\n  entrypoint: string,\n  root?: string,\n  diagnosticName = \"entrypoint\",\n): Promise<URL> {\n  let entrypointSpecifier: URL;\n  try {\n    if (isURL(entrypoint)) {\n      entrypointSpecifier = new URL(entrypoint);\n    } else {\n      entrypointSpecifier = toFileUrl(resolve(root ?? Deno.cwd(), entrypoint));\n    }\n  } catch (err) {\n    throw `Failed to parse ${diagnosticName} specifier '${entrypoint}': ${\n      stringifyError(err)\n    }`;\n  }\n\n  if (entrypointSpecifier.protocol === \"file:\") {\n    try {\n      await Deno.lstat(entrypointSpecifier);\n    } catch (err) {\n      throw `Failed to open ${diagnosticName} file at '${entrypointSpecifier}': ${\n        stringifyError(err)\n      }`;\n    }\n  }\n\n  return entrypointSpecifier;\n}\n\nexport function isURL(entrypoint: string): boolean {\n  return entrypoint.startsWith(\"https://\") ||\n    entrypoint.startsWith(\"http://\") ||\n    entrypoint.startsWith(\"file://\") ||\n    entrypoint.startsWith(\"data:\") ||\n    entrypoint.startsWith(\"jsr:\") ||\n    entrypoint.startsWith(\"npm:\");\n}\n"
  },
  {
    "path": "src/utils/env_vars.ts",
    "content": "import * as dotenv from \"@std/dotenv\";\nimport type { Args } from \"../args.ts\";\n\n/**\n * Obtain the env variables provided by the user with the --env and --env-file options.\n *\n * Both --env and --env-file options can be used multiple times. In case of conflict, the last\n * option takes precedence. Env vars set with --env always takes precedence over envs in env files.\n */\nexport async function envVarsFromArgs(\n  args: Args,\n): Promise<Record<string, string> | null> {\n  const fileEnvs = (await Promise.all(\n    args[\"env-file\"].map((envFile) =>\n      dotenv.load({ ...envFile ? { envPath: envFile } : {} })\n    ),\n  )).reduce((a, b) => Object.assign(a, b), {});\n  const standaloneEnvs = dotenv.parse(args.env.join(\"\\n\"));\n  const envVars = {\n    ...fileEnvs,\n    ...standaloneEnvs,\n  };\n  return Object.keys(envVars).length > 0 ? envVars : null;\n}\n"
  },
  {
    "path": "src/utils/hashing_encoding.ts",
    "content": "export function base64url(binary: Uint8Array): string {\n  const binaryString = Array.from(binary).map((b) => String.fromCharCode(b))\n    .join(\"\");\n  const output = btoa(binaryString);\n  const urlSafeOutput = output\n    .replaceAll(\"=\", \"\")\n    .replaceAll(\"+\", \"-\")\n    .replaceAll(\"/\", \"_\");\n  return urlSafeOutput;\n}\n\nexport async function sha256(randomString: string): Promise<Uint8Array> {\n  return new Uint8Array(\n    await crypto.subtle.digest(\n      \"SHA-256\",\n      new TextEncoder().encode(randomString),\n    ),\n  );\n}\n"
  },
  {
    "path": "src/utils/info.ts",
    "content": "import { join } from \"@std/path/join\";\nimport { getVersions } from \"../subcommands/upgrade.ts\";\n\nexport function getConfigPaths() {\n  const homeDir = Deno.build.os == \"windows\"\n    ? Deno.env.get(\"USERPROFILE\")!\n    : Deno.env.get(\"HOME\")!;\n  const configDir = join(homeDir, \".deno\", \"deployctl\");\n\n  return {\n    configDir,\n    updatePath: join(configDir, \"update.json\"),\n    credentialsPath: join(configDir, \"credentials.json\"),\n  };\n}\n\nexport async function fetchReleases() {\n  try {\n    const { latest } = await getVersions();\n    const updateInfo = { lastFetched: Date.now(), latest };\n    const { updatePath, configDir } = getConfigPaths();\n    await Deno.mkdir(configDir, { recursive: true });\n    await Deno.writeFile(\n      updatePath,\n      new TextEncoder().encode(JSON.stringify(updateInfo, null, 2)),\n    );\n  } catch (_) {\n    // We will try again later when the fetch isn't successful,\n    // so we shouldn't report errors.\n  }\n}\n"
  },
  {
    "path": "src/utils/manifest.ts",
    "content": "import { globToRegExp, isGlob, join, normalize } from \"@std/path\";\nimport type { ManifestEntry } from \"./api_types.ts\";\n\n/** Calculate git object hash, like `git hash-object` does. */\nexport async function calculateGitSha1(bytes: Uint8Array) {\n  const prefix = `blob ${bytes.byteLength}\\0`;\n  const prefixBytes = new TextEncoder().encode(prefix);\n  const fullBytes = new Uint8Array(prefixBytes.byteLength + bytes.byteLength);\n  fullBytes.set(prefixBytes);\n  fullBytes.set(bytes, prefixBytes.byteLength);\n  const hashBytes = await crypto.subtle.digest(\"SHA-1\", fullBytes);\n  const hashHex = Array.from(new Uint8Array(hashBytes))\n    .map((b) => b.toString(16).padStart(2, \"0\"))\n    .join(\"\");\n  return hashHex;\n}\n\nfunction include(\n  path: string,\n  include: RegExp[],\n  exclude: RegExp[],\n): boolean {\n  if (\n    include.length &&\n    !include.some((pattern): boolean => pattern.test(normalize(path)))\n  ) {\n    return false;\n  }\n  if (\n    exclude.length &&\n    exclude.some((pattern): boolean => pattern.test(normalize(path)))\n  ) {\n    return false;\n  }\n  return true;\n}\n\nexport async function walk(\n  cwd: string,\n  dir: string,\n  options: { include: RegExp[]; exclude: RegExp[] },\n): Promise<\n  {\n    manifestEntries: Record<string, ManifestEntry>;\n    hashPathMap: Map<string, string>;\n  }\n> {\n  const hashPathMap = new Map<string, string>();\n  const manifestEntries = await walkInner(cwd, dir, hashPathMap, options);\n  return {\n    manifestEntries,\n    hashPathMap,\n  };\n}\n\nasync function walkInner(\n  cwd: string,\n  dir: string,\n  hashPathMap: Map<string, string>,\n  options: { include: RegExp[]; exclude: RegExp[] },\n): Promise<Record<string, ManifestEntry>> {\n  const entries: Record<string, ManifestEntry> = {};\n  for await (const file of Deno.readDir(dir)) {\n    const path = join(dir, file.name);\n    const relative = path.slice(cwd.length);\n    if (\n      // Do not test directories, because --include=foo/bar must include the directory foo (same goes with --include=*/bar)\n      !file.isDirectory &&\n      !include(\n        path.slice(cwd.length + 1),\n        options.include,\n        options.exclude,\n      )\n    ) {\n      continue;\n    }\n    let entry: ManifestEntry;\n    if (file.isFile) {\n      const data = await Deno.readFile(path);\n      const gitSha1 = await calculateGitSha1(data);\n      entry = {\n        kind: \"file\",\n        gitSha1,\n        size: data.byteLength,\n      };\n      hashPathMap.set(gitSha1, path);\n    } else if (file.isDirectory) {\n      if (relative === \"/.git\") continue;\n      entry = {\n        kind: \"directory\",\n        entries: await walkInner(cwd, path, hashPathMap, options),\n      };\n    } else if (file.isSymlink) {\n      const target = await Deno.readLink(path);\n      entry = {\n        kind: \"symlink\",\n        target,\n      };\n    } else {\n      throw new Error(`Unreachable`);\n    }\n    entries[file.name] = entry;\n  }\n  return entries;\n}\n\n/**\n * Converts a file path pattern, which may be a glob, to a RegExp instance.\n *\n * @param pattern file path pattern which may be a glob\n * @returns a RegExp instance that is equivalent to the given pattern\n */\nexport function convertPatternToRegExp(pattern: string): RegExp {\n  return isGlob(pattern)\n    // slice is used to remove the end-of-string anchor '$'\n    ? new RegExp(globToRegExp(normalize(pattern)).toString().slice(1, -2))\n    : new RegExp(`^${normalize(pattern)}`);\n}\n\n/**\n * Determines if the manifest contains the entry at the given relative path.\n *\n * @param manifestEntries manifest entries to search\n * @param entryRelativePathToLookup a relative path to look up in the manifest\n * @returns `true` if the manifest contains the entry at the given relative path\n */\nexport function containsEntryInManifest(\n  manifestEntries: Record<string, ManifestEntry>,\n  entryRelativePathToLookup: string,\n): boolean {\n  for (const [entryName, entry] of Object.entries(manifestEntries)) {\n    switch (entry.kind) {\n      case \"file\":\n      case \"symlink\": {\n        if (entryName === entryRelativePathToLookup) {\n          return true;\n        }\n        break;\n      }\n      case \"directory\": {\n        if (!entryRelativePathToLookup.startsWith(entryName)) {\n          break;\n        }\n\n        const relativePath = entryRelativePathToLookup.slice(\n          entryName.length + 1,\n        );\n        return containsEntryInManifest(entry.entries, relativePath);\n      }\n      default: {\n        const _: never = entry;\n      }\n    }\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "src/utils/manifest_test.ts",
    "content": "import { dirname, fromFileUrl, join } from \"@std/path\";\nimport { assert, assertEquals, assertFalse } from \"@std/assert\";\nimport type { ManifestEntry } from \"./api_types.ts\";\nimport {\n  containsEntryInManifest,\n  convertPatternToRegExp,\n  walk,\n} from \"./manifest.ts\";\n\nDeno.test({\n  name: \"convertPatternToRegExp\",\n  ignore: Deno.build.os === \"windows\",\n  fn: () => {\n    assertEquals(convertPatternToRegExp(\"foo\"), new RegExp(\"^foo\"));\n    assertEquals(convertPatternToRegExp(\".././foo\"), new RegExp(\"^../foo\"));\n    assertEquals(convertPatternToRegExp(\"*.ts\"), new RegExp(\"^[^/]*\\\\.ts/*\"));\n  },\n});\n\nDeno.test({\n  name: \"walk and containsEntryInManifest\",\n  fn: async (t) => {\n    type Test = {\n      name: string;\n      input: {\n        testdir: string;\n        include: readonly string[];\n        exclude: readonly string[];\n      };\n      expected: {\n        entries: Record<string, ManifestEntry>;\n        containedEntries: readonly string[];\n        notContainedEntries: readonly string[];\n      };\n    };\n\n    const tests: Test[] = [\n      {\n        name: \"single_file\",\n        input: {\n          testdir: \"single_file\",\n          include: [],\n          exclude: [],\n        },\n        expected: {\n          entries: {\n            \"a.txt\": {\n              kind: \"file\",\n              gitSha1: \"78981922613b2afb6025042ff6bd878ac1994e85\",\n              size: 2,\n            },\n          },\n          containedEntries: [\"a.txt\"],\n          notContainedEntries: [\"b.txt\", \".git\", \"deno.json\"],\n        },\n      },\n      {\n        name: \"single_file with include\",\n        input: {\n          testdir: \"single_file\",\n          include: [\"a.txt\"],\n          exclude: [],\n        },\n        expected: {\n          entries: {\n            \"a.txt\": {\n              kind: \"file\",\n              gitSha1: \"78981922613b2afb6025042ff6bd878ac1994e85\",\n              size: 2,\n            },\n          },\n          containedEntries: [\"a.txt\"],\n          notContainedEntries: [\"b.txt\", \".git\", \"deno.json\"],\n        },\n      },\n      {\n        name: \"single_file with include 2\",\n        input: {\n          testdir: \"single_file\",\n          include: [\"*.txt\"],\n          exclude: [],\n        },\n        expected: {\n          entries: {\n            \"a.txt\": {\n              kind: \"file\",\n              gitSha1: \"78981922613b2afb6025042ff6bd878ac1994e85\",\n              size: 2,\n            },\n          },\n          containedEntries: [\"a.txt\"],\n          notContainedEntries: [\"b.txt\", \".git\", \"deno.json\"],\n        },\n      },\n      {\n        name: \"single_file with exclude\",\n        input: {\n          testdir: \"single_file\",\n          include: [],\n          exclude: [\"a.txt\"],\n        },\n        expected: {\n          entries: {},\n          containedEntries: [],\n          notContainedEntries: [\"a.txt\", \"b.txt\", \".git\", \"deno.json\"],\n        },\n      },\n      {\n        name: \"two_levels\",\n        input: {\n          testdir: \"two_levels\",\n          include: [],\n          exclude: [],\n        },\n        expected: {\n          entries: {\n            \"a.txt\": {\n              kind: \"file\",\n              gitSha1: \"78981922613b2afb6025042ff6bd878ac1994e85\",\n              size: 2,\n            },\n            \"inner\": {\n              kind: \"directory\",\n              entries: {\n                \"b.txt\": {\n                  kind: \"file\",\n                  gitSha1: \"61780798228d17af2d34fce4cfbdf35556832472\",\n                  size: 2,\n                },\n              },\n            },\n          },\n          containedEntries: [\"a.txt\", \"inner/b.txt\"],\n          notContainedEntries: [\n            \"b.txt\",\n            \"inner/a.txt\",\n            \".git\",\n            \"deno.json\",\n            \"inner\",\n          ],\n        },\n      },\n      {\n        name: \"two_levels with include\",\n        input: {\n          testdir: \"two_levels\",\n          include: [\"**/b.txt\"],\n          exclude: [],\n        },\n        expected: {\n          entries: {\n            \"inner\": {\n              kind: \"directory\",\n              entries: {\n                \"b.txt\": {\n                  kind: \"file\",\n                  gitSha1: \"61780798228d17af2d34fce4cfbdf35556832472\",\n                  size: 2,\n                },\n              },\n            },\n          },\n          containedEntries: [\"inner/b.txt\"],\n          notContainedEntries: [\n            \"a.txt\",\n            \"b.txt\",\n            \"inner/a.txt\",\n            \".git\",\n            \"deno.json\",\n            \"inner\",\n          ],\n        },\n      },\n      {\n        name: \"two_levels with exclude\",\n        input: {\n          testdir: \"two_levels\",\n          include: [],\n          exclude: [\"*.txt\"],\n        },\n        expected: {\n          entries: {\n            \"inner\": {\n              kind: \"directory\",\n              entries: {\n                \"b.txt\": {\n                  kind: \"file\",\n                  gitSha1: \"61780798228d17af2d34fce4cfbdf35556832472\",\n                  size: 2,\n                },\n              },\n            },\n          },\n          containedEntries: [\"inner/b.txt\"],\n          notContainedEntries: [\n            \"a.txt\",\n            \"b.txt\",\n            \"inner/a.txt\",\n            \".git\",\n            \"deno.json\",\n            \"inner\",\n          ],\n        },\n      },\n      {\n        name: \"complex\",\n        input: {\n          testdir: \"complex\",\n          include: [],\n          exclude: [],\n        },\n        expected: {\n          entries: {\n            \"a.txt\": {\n              kind: \"file\",\n              gitSha1: \"78981922613b2afb6025042ff6bd878ac1994e85\",\n              size: 2,\n            },\n            \"inner1\": {\n              kind: \"directory\",\n              entries: {\n                \"b.txt\": {\n                  kind: \"file\",\n                  gitSha1: \"61780798228d17af2d34fce4cfbdf35556832472\",\n                  size: 2,\n                },\n              },\n            },\n            \"inner2\": {\n              kind: \"directory\",\n              entries: {\n                \"b.txt\": {\n                  kind: \"file\",\n                  gitSha1: \"61780798228d17af2d34fce4cfbdf35556832472\",\n                  size: 2,\n                },\n              },\n            },\n          },\n          containedEntries: [\"a.txt\", \"inner1/b.txt\", \"inner2/b.txt\"],\n          notContainedEntries: [\n            \"b.txt\",\n            \"inner1/a.txt\",\n            \"inner2/a.txt\",\n            \".git\",\n            \"deno.json\",\n            \"inner1\",\n            \"inner2\",\n          ],\n        },\n      },\n    ];\n\n    for (const test of tests) {\n      await t.step({\n        name: test.name,\n        fn: async () => {\n          const { manifestEntries } = await walk(\n            join(\n              fromFileUrl(dirname(import.meta.url)),\n              \"manifest_testdata\",\n              test.input.testdir,\n            ),\n            join(\n              fromFileUrl(dirname(import.meta.url)),\n              \"manifest_testdata\",\n              test.input.testdir,\n            ),\n            {\n              include: test.input.include.map(convertPatternToRegExp),\n              exclude: test.input.exclude.map(convertPatternToRegExp),\n            },\n          );\n          assertEquals(manifestEntries, test.expected.entries);\n\n          for (const entry of test.expected.containedEntries) {\n            const contained = containsEntryInManifest(manifestEntries, entry);\n            assert(\n              contained,\n              `Expected ${entry} to be contained in the manifest`,\n            );\n          }\n\n          for (const entry of test.expected.notContainedEntries) {\n            const contained = containsEntryInManifest(manifestEntries, entry);\n            assertFalse(\n              contained,\n              `Expected ${entry} to *not* be contained in the manifest`,\n            );\n          }\n        },\n      });\n    }\n  },\n});\n"
  },
  {
    "path": "src/utils/manifest_testdata/complex/a.txt",
    "content": "a\n"
  },
  {
    "path": "src/utils/manifest_testdata/complex/inner1/b.txt",
    "content": "b\n"
  },
  {
    "path": "src/utils/manifest_testdata/complex/inner2/b.txt",
    "content": "b\n"
  },
  {
    "path": "src/utils/manifest_testdata/single_file/a.txt",
    "content": "a\n"
  },
  {
    "path": "src/utils/manifest_testdata/two_levels/a.txt",
    "content": "a\n"
  },
  {
    "path": "src/utils/manifest_testdata/two_levels/inner/b.txt",
    "content": "b\n"
  },
  {
    "path": "src/utils/mod.ts",
    "content": "// Export functions used by `action/index.js`\nexport { parseEntrypoint } from \"./entrypoint.ts\";\nexport { API, APIError } from \"./api.ts\";\nexport { convertPatternToRegExp, walk } from \"./manifest.ts\";\nexport { fromFileUrl, resolve } from \"@std/path\";\n"
  },
  {
    "path": "src/utils/organization.ts",
    "content": "import { error } from \"../error.ts\";\nimport type { API } from \"./api.ts\";\nimport type { Organization } from \"./api_types.ts\";\nimport { interruptSpinner, wait } from \"./spinner.ts\";\n\nexport default {\n  getByNameOrCreate: async (\n    api: API,\n    name: string,\n  ): Promise<Organization> => {\n    const interruptedSpinner = interruptSpinner();\n    let org;\n    try {\n      let spinner = wait(\n        `You have specified the organization ${name}. Fetching details...`,\n      ).start();\n      org = await api.getOrganizationByName(name);\n      if (!org) {\n        spinner.stop();\n        spinner = wait(\n          `Organization '${name}' not found. Creating...`,\n        ).start();\n        org = await api.createOrganization(name);\n        spinner.succeed(`Created new organization '${org!.name}'.`);\n      } else {\n        spinner.stop();\n      }\n    } catch (e) {\n      error(e);\n    }\n    interruptedSpinner.resume();\n    return org;\n  },\n};\n"
  },
  {
    "path": "src/utils/spinner.ts",
    "content": "import {\n  type Spinner,\n  type SpinnerOptions,\n  wait as innerWait,\n} from \"@denosaurs/wait\";\n\nlet current: Spinner | null = null;\n\nexport function wait(param: string | SpinnerOptions) {\n  if (typeof param === \"string\") {\n    param = { text: param };\n  }\n  param.interceptConsole = false;\n  current = innerWait({ stream: Deno.stderr, ...param });\n  return current;\n}\n\nexport function interruptSpinner(): Interrupt {\n  current?.stop();\n  const interrupt = new Interrupt(current);\n  current = null;\n  return interrupt;\n}\n\nexport class Interrupt {\n  #spinner: Spinner | null;\n  constructor(spinner: Spinner | null) {\n    this.#spinner = spinner;\n  }\n  resume() {\n    current = this.#spinner;\n    this.#spinner?.start();\n  }\n}\n"
  },
  {
    "path": "src/utils/time.ts",
    "content": "import { yellow } from \"@std/fmt/colors\";\n\nexport function renderTimeDelta(delta: number, language?: string): string {\n  const sinces = [delta];\n  const sinceUnits = [\"milli\"];\n  if (sinces[0] >= 1000) {\n    sinces.push(Math.floor(sinces[0] / 1000));\n    sinces[0] = sinces[0] % 1000;\n    sinceUnits.push(\"second\");\n  }\n  if (sinces[1] >= 60) {\n    sinces.push(Math.floor(sinces[1] / 60));\n    sinces[1] = sinces[1] % 60;\n    sinceUnits.push(\"minute\");\n  }\n\n  if (sinces[2] >= 60) {\n    sinces.push(Math.floor(sinces[2] / 60));\n    sinces[2] = sinces[2] % 60;\n    sinceUnits.push(\"hour\");\n  }\n\n  if (sinces[3] >= 24) {\n    sinces.push(Math.floor(sinces[3] / 24));\n    sinces[3] = sinces[3] % 24;\n    sinceUnits.push(\"day\");\n  }\n\n  if (sinces.length > 1) {\n    // remove millis if there are already seconds\n    sinces.shift();\n    sinceUnits.shift();\n  }\n\n  sinces.reverse();\n  sinceUnits.reverse();\n  let sinceStr = \"\";\n  for (let x = 0; x < sinces.length; x++) {\n    const since = sinces[x];\n    let sinceUnit = sinceUnits[x];\n    if (since === 0) continue;\n    if (sinceStr) {\n      sinceStr += \", \";\n    }\n    if (sinces[x] > 1) {\n      sinceUnit += \"s\";\n    }\n    sinceStr += `${\n      since.toLocaleString(language ?? navigator.language)\n    } ${sinceUnit}`;\n    if (x === 0) {\n      sinceStr = yellow(sinceStr);\n    }\n  }\n  return sinceStr;\n}\n"
  },
  {
    "path": "src/utils/time_test.ts",
    "content": "import { yellow } from \"@std/fmt/colors\";\nimport { assertEquals } from \"@std/assert/assert_equals\";\nimport { renderTimeDelta } from \"./time.ts\";\n\nDeno.test(\"renderTimeDelta returns time in milliseconds if below 1 second\", () => {\n  const result1 = renderTimeDelta(1);\n  assertEquals(result1, yellow(\"1 milli\"));\n  const result2 = renderTimeDelta(999);\n  assertEquals(result2, yellow(\"999 millis\"));\n});\nDeno.test(\"renderTimeDelta returns time only in seconds if above 1 second and below 1 minute\", () => {\n  const result1 = renderTimeDelta(1001);\n  assertEquals(result1, yellow(\"1 second\"));\n  const result2 = renderTimeDelta(59000);\n  assertEquals(result2, yellow(\"59 seconds\"));\n});\n\nDeno.test(\"renderTimeDelta returns time in minutes and seconds if above 1 minute and below 1 hour\", () => {\n  const result1 = renderTimeDelta(60000);\n  assertEquals(result1, `${yellow(\"1 minute\")}`);\n  const result2 = renderTimeDelta(1 * 60 * 60 * 1000 - 1);\n  assertEquals(result2, `${yellow(\"59 minutes\")}, 59 seconds`);\n});\n\nDeno.test(\"renderTimeDelta returns time in hours, minutes and seconds if above 1 hour and below 1 day\", () => {\n  const result1 = renderTimeDelta(1 * 60 * 60 * 1000);\n  assertEquals(result1, `${yellow(\"1 hour\")}`);\n  const result2 = renderTimeDelta(1 * 24 * 60 * 60 * 1000 - 1);\n  assertEquals(result2, `${yellow(\"23 hours\")}, 59 minutes, 59 seconds`);\n});\n\nDeno.test(\"renderTimeDelta returns time in days, hours, minutes and seconds if above 1 day\", () => {\n  const result1 = renderTimeDelta(1 * 24 * 60 * 60 * 1000);\n  assertEquals(result1, `${yellow(\"1 day\")}`);\n  const result2 = renderTimeDelta(1_000_000 * 24 * 60 * 60 * 1000 - 1, \"en-US\");\n  assertEquals(\n    result2,\n    `${yellow(\"999,999 days\")}, 23 hours, 59 minutes, 59 seconds`,\n  );\n});\n"
  },
  {
    "path": "src/utils/token_storage/darwin.ts",
    "content": "import keychain from \"npm:keychain@1.5.0\";\n\nconst KEYCHAIN_CREDS = { account: \"Deno Deploy\", service: \"DeployCTL\" };\n\nexport function getFromKeychain(): Promise<string | null> {\n  return new Promise((resolve, reject) =>\n    keychain.getPassword(\n      KEYCHAIN_CREDS,\n      (err: KeychainError, token: string) => {\n        if (err && err.code !== \"PasswordNotFound\") {\n          reject(err);\n        } else {\n          resolve(token);\n        }\n      },\n    )\n  );\n}\n\nexport function storeInKeyChain(token: string): Promise<void> {\n  return new Promise((resolve, reject) =>\n    keychain.setPassword(\n      { ...KEYCHAIN_CREDS, password: token },\n      (err: KeychainError) => {\n        if (err) {\n          reject(err);\n        } else {\n          resolve();\n        }\n      },\n    )\n  );\n}\n\nexport function removeFromKeyChain(): Promise<void> {\n  return new Promise((resolve, reject) =>\n    keychain.deletePassword(KEYCHAIN_CREDS, (err: KeychainError) => {\n      if (err && err.code !== \"PasswordNotFound\") {\n        reject(err);\n      } else {\n        resolve();\n      }\n    })\n  );\n}\n\ninterface KeychainError {\n  code: string;\n}\n"
  },
  {
    "path": "src/utils/token_storage/fs.ts",
    "content": "import { getConfigPaths } from \"../info.ts\";\n\nexport async function get(): Promise<string | null> {\n  const { credentialsPath } = getConfigPaths();\n  try {\n    const info = await Deno.lstat(credentialsPath);\n    if (!info.isFile || (info.mode !== null && (info.mode & 0o777) !== 0o600)) {\n      throw new Error(\n        \"The credentials file has been tampered with and will be ignored. Please delete it.\",\n      );\n    }\n  } catch (e) {\n    if (e instanceof Deno.errors.NotFound) {\n      return null;\n    } else {\n      throw e;\n    }\n  }\n  try {\n    const token = JSON.parse(await Deno.readTextFile(credentialsPath)).token;\n    return token || null;\n  } catch (_) {\n    throw new Error(\n      `The credentials file has been tampered with and will be ignored. Please delete it.`,\n    );\n  }\n}\n\nexport async function store(token: string): Promise<void> {\n  const { credentialsPath, configDir } = getConfigPaths();\n  await Deno.mkdir(configDir, { recursive: true });\n  await Deno.writeTextFile(\n    credentialsPath,\n    JSON.stringify({ token }, null, 2),\n    { mode: 0o600 },\n  );\n  return Promise.resolve();\n}\n\nexport async function remove(): Promise<void> {\n  const { credentialsPath, configDir } = getConfigPaths();\n  await Deno.mkdir(configDir, { recursive: true });\n  await Deno.writeTextFile(credentialsPath, \"{}\", { mode: 0o600 });\n  return Promise.resolve();\n}\n"
  },
  {
    "path": "src/utils/token_storage/memory.ts",
    "content": "let TOKEN: string | null;\n\nexport function get(): Promise<string | null> {\n  return Promise.resolve(TOKEN);\n}\n\nexport function store(token: string): Promise<void> {\n  TOKEN = token;\n  return Promise.resolve();\n}\n\nexport function remove(): Promise<void> {\n  TOKEN = null;\n  return Promise.resolve();\n}\n"
  },
  {
    "path": "src/utils/token_storage.ts",
    "content": "import { interruptSpinner, wait } from \"./spinner.ts\";\n\ninterface TokenStorage {\n  get: () => Promise<string | null>;\n  store: (token: string) => Promise<void>;\n  remove: () => Promise<void>;\n}\n\nlet defaultMode = false;\n\nlet module: TokenStorage;\nif (Deno.build.os === \"darwin\") {\n  const darwin = await import(\"./token_storage/darwin.ts\");\n  const memory = await import(\"./token_storage/memory.ts\");\n  module = {\n    get: defaultOnError(\n      \"Failed to get token from Keychain. Will provision a new token for this execution but please make sure to fix the issue afterwards.\",\n      memory.get,\n      darwin.getFromKeychain,\n    ),\n    store: defaultOnError(\n      \"Failed to store token into Keychain. Will keep it in memory for the duration of this execution but please make sure to fix the issue afterwards.\",\n      memory.store,\n      darwin.storeInKeyChain,\n    ),\n    remove: defaultOnError(\n      \"Failed to remove token from Keychain\",\n      memory.remove,\n      darwin.removeFromKeyChain,\n    ),\n  };\n} else {\n  const fs = await import(\"./token_storage/fs.ts\");\n  const memory = await import(\"./token_storage/memory.ts\");\n  module = {\n    get: defaultOnError(\n      \"Failed to get token from credentials file. Will provision a new token for this execution but please make sure to fix the issue afterwards.\",\n      memory.get,\n      fs.get,\n    ),\n    store: defaultOnError(\n      \"Failed to store token in credentials file. Will keep it in memory for the duration of this execution but please make sure to fix the issue afterwards.\",\n      memory.store,\n      fs.store,\n    ),\n    remove: defaultOnError(\n      \"Failed to remove token from credentials file\",\n      memory.remove,\n      fs.remove,\n    ),\n  };\n}\nexport default module;\n\nfunction defaultOnError<\n  // deno-lint-ignore no-explicit-any\n  F extends (...args: any) => Promise<any>,\n>(\n  notification: string,\n  defaultFn: (...params: Parameters<F>) => ReturnType<F>,\n  fn: (...params: Parameters<F>) => ReturnType<F>,\n): (...params: Parameters<F>) => ReturnType<F> {\n  return (...params) => {\n    if (defaultMode) {\n      return defaultFn(...params);\n    } else {\n      return fn(...params)\n        .catch((err) => {\n          const spinnerInterrupt = interruptSpinner();\n          wait(\"\").start().warn(notification);\n          let errStr = err.message;\n          if (errStr.length > 90) {\n            errStr = errStr.slice(0, 90) + \"...\";\n          }\n          wait({ text: \"\", indent: 3 }).start().fail(errStr);\n          spinnerInterrupt.resume();\n          defaultMode = true;\n          return defaultFn(...params);\n        }) as ReturnType<F>;\n    }\n  };\n}\n"
  },
  {
    "path": "src/version.ts",
    "content": "export const VERSION = \"1.13.1\";\n\n// Make sure to keep this in sync with the \"old\" version in `ci.yml`\n// Also don't forget to update README.md.\nexport const MINIMUM_DENO_VERSION = \"1.46.0\";\n"
  },
  {
    "path": "tests/config_file_test/config.json",
    "content": "{}\n"
  },
  {
    "path": "tests/config_file_test/config_file_test.ts",
    "content": "import { fromFileUrl } from \"@std/path/from_file_url\";\nimport configFile from \"../../src/config_file.ts\";\nimport { assert, assertEquals } from \"@std/assert\";\n\nDeno.test(\"ConfigFile.diff returns array with additions and removals\", async () => {\n  const config = await configFile.read(\n    fromFileUrl(new URL(import.meta.resolve(\"./config.json\"))),\n  );\n  assert(!!config);\n\n  let changes = config.diff({});\n  assertEquals(changes, []);\n\n  changes = config.diff({ project: \"foo\" });\n  assertEquals(changes, [{\n    key: \"project\",\n    addition: \"foo\",\n    removal: undefined,\n  }]);\n\n  // Using file URLs to avoid dealing with path normalization\n  config.override({ project: \"foo\", entrypoint: \"file://main.ts\" });\n\n  changes = config.diff({ project: \"bar\", entrypoint: \"file://src/main.ts\" });\n  assertEquals(changes, [\n    { key: \"project\", removal: \"foo\", addition: \"bar\" },\n    {\n      key: \"entrypoint\",\n      removal: \"file://main.ts\",\n      addition: \"file://src/main.ts\",\n    },\n  ]);\n});\n\nDeno.test(\"ConfigFile.diff reports inculde and exclude changes when one of the entries changed\", async () => {\n  const config = await configFile.read(\n    fromFileUrl(new URL(import.meta.resolve(\"./config.json\"))),\n  );\n  assert(!!config);\n\n  config.override({ include: [\"foo\", \"bar\"], exclude: [\"fuzz\", \"bazz\"] });\n\n  const changes = config.diff({\n    include: [\"fuzz\", \"bazz\"],\n    exclude: [\"foo\", \"bar\"],\n  });\n  assertEquals(changes, [\n    { key: \"exclude\", addition: [\"foo\", \"bar\"], removal: [\"fuzz\", \"bazz\"] },\n    { key: \"include\", removal: [\"foo\", \"bar\"], addition: [\"fuzz\", \"bazz\"] },\n  ]);\n});\n\nDeno.test(\"ConfigFile.useAsDefaultFor can handle empty array defaults\", async () => {\n  const config = await configFile.read(\n    fromFileUrl(new URL(import.meta.resolve(\"./config_with_include.json\"))),\n  );\n  assert(!!config);\n  assertEquals(config.args().include?.[0], \"**\");\n\n  const args = {\n    include: [],\n  };\n  config.useAsDefaultFor(args);\n\n  assertEquals(args.include[0], \"**\");\n});\n"
  },
  {
    "path": "tests/config_file_test/config_with_include.json",
    "content": "{\n  \"deploy\": {\n    \"include\": [\"**\"]\n  }\n}\n"
  },
  {
    "path": "tests/env_vars_test/.another-env",
    "content": "BAR=bar"
  },
  {
    "path": "tests/env_vars_test/.overlapping-env",
    "content": "FOO=last"
  },
  {
    "path": "tests/env_vars_test/env_vars_test.ts",
    "content": "import { parseArgs } from \"../../src/args.ts\";\nimport { envVarsFromArgs } from \"../../src/utils/env_vars.ts\";\nimport { assert, assertEquals } from \"@std/assert\";\n\nDeno.test(\"envVarsFromArgs gets env variables from multiple --env options\", async () => {\n  const args = parseArgs([\"--env=FOO=foo\", \"--env=BAR=bar\"]);\n  const envVars = await envVarsFromArgs(args);\n  assert(envVars !== null);\n  assertEquals(Object.entries(envVars).length, 2);\n  assertEquals(envVars.FOO, \"foo\");\n  assertEquals(envVars.BAR, \"bar\");\n});\n\nDeno.test(\"envVarsFromArgs last --env option takes precedence when overlapping\", async () => {\n  const args = parseArgs([\"--env=FOO=foo\", \"--env=BAR=bar\", \"--env=FOO=last\"]);\n  const envVars = await envVarsFromArgs(args);\n  assertEquals(envVars?.FOO, \"last\");\n});\n\nDeno.test(\"envVarsFromArgs gets env variables from multiple --env-file options\", async () => {\n  const args = parseArgs([\n    `--env-file=${import.meta.dirname}/.env`,\n    `--env-file=${import.meta.dirname}/.another-env`,\n  ]);\n  const envVars = await envVarsFromArgs(args);\n  assert(envVars !== null);\n  assertEquals(Object.entries(envVars).length, 2);\n  assertEquals(envVars.FOO, \"foo\");\n  assertEquals(envVars.BAR, \"bar\");\n});\n\nDeno.test(\"envVarsFromArgs last --env-file option takes precedence when overlapping\", async () => {\n  const args = parseArgs([\n    `--env-file=${import.meta.dirname}/.env`,\n    `--env-file=${import.meta.dirname}/.another-env`,\n    `--env-file=${import.meta.dirname}/.overlapping-env`,\n  ]);\n  const envVars = await envVarsFromArgs(args);\n  assertEquals(envVars?.FOO, \"last\");\n});\n\nDeno.test(\"envVarsFromArgs --env always takes precedence over --env-file\", async () => {\n  const args = parseArgs([\n    \"--env=FOO=winner\",\n    `--env-file=${import.meta.dirname}/.env`,\n    `--env-file=${import.meta.dirname}/.another-env`,\n    \"--env=BAR=winner\",\n  ]);\n  const envVars = await envVarsFromArgs(args);\n  assertEquals(envVars?.FOO, \"winner\");\n  assertEquals(envVars?.BAR, \"winner\");\n});\n"
  },
  {
    "path": "tests/help_test.ts",
    "content": "import { assert, assertEquals, assertStringIncludes } from \"@std/assert\";\nimport { output, test } from \"./utils.ts\";\n\ntest({ args: [] }, async (proc) => {\n  const [stdout, stderr, { code }] = await output(proc);\n  assertStringIncludes(stderr, \"SUBCOMMANDS:\");\n  assertStringIncludes(stderr, \"deploy \");\n  assertStringIncludes(stderr, \"upgrade \");\n  assertEquals(code, 1);\n  assertEquals(stdout, \"\");\n});\n\ntest({ args: [\"-V\"] }, async (proc) => {\n  const [stdout, stderr, { code }] = await output(proc);\n  assertEquals(stderr, \"\");\n  assertEquals(code, 0);\n  assert(stdout.startsWith(\"deployctl \"));\n});\n\ntest({ args: [\"--version\"] }, async (proc) => {\n  const [stdout, stderr, { code }] = await output(proc);\n  assertEquals(stderr, \"\");\n  assertEquals(code, 0);\n  assert(stdout.startsWith(\"deployctl \"));\n});\n\ntest({ args: [\"-h\"] }, async (proc) => {\n  const [stdout, stderr, { code }] = await output(proc);\n  assertStringIncludes(stdout, \"SUBCOMMANDS:\");\n  assertStringIncludes(stdout, \"deploy \");\n  assertStringIncludes(stdout, \"upgrade \");\n  assertEquals(code, 0);\n  assertEquals(stderr, \"\");\n});\n\ntest({ args: [\"deploy\", \"-h\"] }, async (proc) => {\n  const [stdout, stderr, { code }] = await output(proc);\n  assertStringIncludes(stdout, \"USAGE:\");\n  assertStringIncludes(stdout, \"deployctl deploy\");\n  assertEquals(code, 0);\n  assertEquals(stderr, \"\");\n});\n\ntest({ args: [\"upgrade\", \"-h\"] }, async (proc) => {\n  const [stdout, stderr, { code }] = await output(proc);\n  assertStringIncludes(stdout, \"deployctl upgrade\");\n  assertStringIncludes(stdout, \"USAGE:\");\n  assertStringIncludes(stdout, \"ARGS:\");\n  assertEquals(code, 0);\n  assertEquals(stderr, \"\");\n});\n"
  },
  {
    "path": "tests/utils.ts",
    "content": "import { lessThan as semverLessThan, parse as semverParse } from \"@std/semver\";\nimport { assert } from \"@std/assert/assert\";\nimport { MINIMUM_DENO_VERSION } from \"../src/version.ts\";\n\nexport interface Permissions {\n  net: boolean;\n  read: boolean;\n  write: boolean;\n  env: boolean;\n  run: boolean;\n  sys: boolean;\n}\n\nexport function deployctl(\n  args: string[],\n  permissions: Permissions = {\n    net: true,\n    read: true,\n    write: true,\n    env: true,\n    run: true,\n    sys: true,\n  },\n): Deno.ChildProcess {\n  const deno = [\n    Deno.execPath(),\n    \"run\",\n  ];\n\n  if (permissions?.net) deno.push(\"--allow-net\");\n  if (permissions?.read) deno.push(\"--allow-read\");\n  if (permissions?.write) deno.push(\"--allow-write\");\n  if (permissions?.env) deno.push(\"--allow-env\");\n  if (permissions?.run) deno.push(\"--allow-run\");\n  if (permissions?.sys) deno.push(\"--allow-sys\");\n\n  deno.push(\"--quiet\");\n\n  // Deno 1.x does not support lockfile v4. To work around this, we append\n  // `--no-lock` in this case.\n  const v2 = semverParse(\"2.0.0\");\n  assert(\n    semverLessThan(semverParse(MINIMUM_DENO_VERSION), v2),\n    \"We do not support Deno 1.x anymore. Please remove the `isDeno1` check below in the source code.\",\n  );\n  const isDeno1 = semverLessThan(semverParse(Deno.version.deno), v2);\n  if (isDeno1) {\n    deno.push(\"--no-lock\");\n  }\n\n  deno.push(new URL(\"../deployctl.ts\", import.meta.url).toString());\n\n  const cmd = Deno.build.os == \"linux\"\n    ? [\"bash\", \"-c\", [...deno, ...args].join(\" \")]\n    : [...deno, ...args];\n\n  return new Deno.Command(cmd[0], {\n    args: cmd.slice(1),\n    stdin: \"null\",\n    stdout: \"piped\",\n    stderr: \"piped\",\n  }).spawn();\n}\n\nexport interface TestOptions {\n  args: string[];\n  name?: string;\n  permissions?: Permissions;\n}\n\nexport function test(\n  opts: TestOptions,\n  fn: (proc: Deno.ChildProcess) => void | Promise<void>,\n) {\n  const name = opts.name ?? [\"deployctl\", ...opts.args].join(\" \");\n  Deno.test(name, async () => {\n    const proc = deployctl(opts.args, opts.permissions);\n    await fn(proc);\n  });\n}\n\nexport async function output(\n  proc: Deno.ChildProcess,\n): Promise<[string, string, Deno.CommandStatus]> {\n  const [status, { stdout, stderr }] = await Promise.all([\n    proc.status,\n    proc.output(),\n  ]);\n  return [\n    new TextDecoder().decode(stdout),\n    new TextDecoder().decode(stderr),\n    status,\n  ];\n}\n"
  },
  {
    "path": "tools/bundle.ts",
    "content": "// Copyright 2024 Deno Land Inc. All rights reserved. MIT license.\n\nimport { bundle, type ImportMap } from \"@deno/emit\";\nimport { resolve } from \"@std/path/resolve\";\nimport { parse as parseJsonc } from \"@std/jsonc\";\n\nconst entrypoint = Deno.args[0];\nconst resolvedPath = resolve(Deno.cwd(), entrypoint);\n\nconst configPath = resolve(Deno.cwd(), \"deno.jsonc\");\nconst config = await Deno.readTextFile(configPath);\nconst result = await bundle(resolvedPath, {\n  importMap: parseJsonc(config) as ImportMap,\n});\nconsole.log(`// deno-fmt-ignore-file\n// deno-lint-ignore-file\n// This code was bundled using \\`deno task build-action\\` and it's not recommended to edit it manually\n`);\nconsole.log(result.code);\n"
  },
  {
    "path": "tools/version_match.ts",
    "content": "// Copyright 2023 Deno Land Inc. All rights reserved. MIT license.\n\n// This script ensures that version specifier defined in `src/version.ts`\n// matches the released tag version.\n// Intended to run when a draft release is created on GitHub.\n\nimport { VERSION } from \"../src/version.ts\";\nimport { assertEquals } from \"@std/assert/assert_equals\";\n\nconst releaseTagVersion = Deno.env.get(\"RELEASE_TAG\")!;\nassertEquals(VERSION, releaseTagVersion);\n"
  }
]