Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: slackapi/node-slack-sdk
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: @slack/web-api@7.0.4
Choose a base ref
...
head repository: slackapi/node-slack-sdk
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 15831833fb34799efc1c6dfef22302a0fa3d4804
Choose a head ref

Commits on Apr 30, 2024

  1. socket-mode: Rewrite to Python(ish) Implementation (#1781)

    Closes #1771
    Fil Maj authored Apr 30, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    476e6a9 View commit details
  2. Publish @slack/socket-mode@2.0.0 (#1785)

    Fil Maj authored Apr 30, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    6840250 View commit details

Commits on May 1, 2024

  1. types: Add slack_file object to image block/element types (#1783)

    Co-authored-by: cfeeney5 <c.feeney5@gmail.com>
    Co-authored-by: Fil Maj <maj.fil@gmail.com>
    3 people authored May 1, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    aea11d0 View commit details

Commits on May 14, 2024

  1. feat: providing a way to disable message content being logged (#1786) -

    fixes #1751
    Parama92 authored May 14, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    b98ef1e View commit details

Commits on May 15, 2024

  1. Add CodeCov reporting back after a 3 year hiatus (#1788)

    Fil Maj authored May 15, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    2c0d6e2 View commit details
  2. Add codecov badges to READMEs.

    Filip Maj committed May 15, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    4d45007 View commit details

Commits on May 23, 2024

  1. Add new @slack/cli-test CLI test utility node bindings for use in a…

    …utomated testing purposes (#1789)
    Fil Maj authored May 23, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    0f38f72 View commit details
  2. Publish @slack/cli-test@0.0.1 (#1791)

    Fil Maj authored May 23, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    393177f View commit details
  3. Actually publish @slack/cli-test@0.0.1

    Filip Maj committed May 23, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    793c6a3 View commit details
  4. cli-test: ensure skipping update is truly the default (#1792)

    Fil Maj authored May 23, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    0fedf14 View commit details
  5. cli-test: more async-friendly / event-driven approach to process moni…

    …toring/management (#1793)
    Fil Maj authored May 23, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    9f3af6b View commit details
  6. Publish @slack/cli-test@0.0.2

    Filip Maj committed May 23, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    5efe7f3 View commit details

Commits on May 27, 2024

  1. cli-test: add app list command (#1794)

    Fil Maj authored May 27, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    11cb80e View commit details
  2. cli-test(trace): include test trace constants for 'datastore count' (#…

    zimeg authored May 27, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    c3725a9 View commit details
  3. cli-test: small internal refactor to use new app.list command (#1797)

    Fil Maj authored May 27, 2024
    Copy the full SHA
    9fb278a View commit details
  4. Publish @slack/cli-test@0.1.0 (#1796)

    Fil Maj authored May 27, 2024
    Copy the full SHA
    65b2332 View commit details

Commits on May 28, 2024

  1. cli-test: add ability to invoke arbitrary CLI commands (#1798)

    Fil Maj authored May 28, 2024
    Copy the full SHA
    a0e01c5 View commit details

Commits on May 29, 2024

  1. cli-test: expose --slackdev flag as qa option to all commands (#1799)

    Fil Maj authored May 29, 2024
    Copy the full SHA
    1938408 View commit details

Commits on May 31, 2024

  1. web-api(docs): prefer filesUploadV2 for uploading files (#1802)

    zimeg authored May 31, 2024
    Copy the full SHA
    eca4d68 View commit details
  2. web-api(docs): fix the sunset year for the files.upload method (#1805)

    zimeg authored May 31, 2024
    Copy the full SHA
    cf80469 View commit details

Commits on Jun 3, 2024

  1. cli-test: adding shell and app.list APIs, exposing QA environment…

    … flags, added more traces available in CLI v2.25.0 (#1804)
    Fil Maj authored Jun 3, 2024
    Copy the full SHA
    17ef7ae View commit details
  2. docs: the cli test package version should include a build metadata an…

    …d not a pre-release suffix.
    Filip Maj committed Jun 3, 2024
    Copy the full SHA
    efb2aaf View commit details
  3. Publish @slack/cli-test@0.2.0+cli.2.25.0 (#1806)

    Fil Maj authored Jun 3, 2024
    Copy the full SHA
    b195311 View commit details

Commits on Jun 5, 2024

  1. Publish @slack/types@2.12.0 (#1808)

    Fil Maj authored Jun 5, 2024
    Copy the full SHA
    bb21de9 View commit details

Commits on Jun 10, 2024

  1. chore: replace master branch refs with main (#1810)

    davidlj95 authored Jun 10, 2024
    Copy the full SHA
    31f27db View commit details
  2. chore: update Github Actions badges syntax (#1811)

    davidlj95 authored Jun 10, 2024
    Copy the full SHA
    e7232b2 View commit details
  3. docs: update Typescript minimum version: v7 + link (#1812)

    davidlj95 authored Jun 10, 2024
    Copy the full SHA
    a771044 View commit details
  4. feat: allow using WebClient APIs without argument (#1809)

    davidlj95 authored Jun 10, 2024
    Copy the full SHA
    9f2935f View commit details

Commits on Jun 12, 2024

  1. web-api: public canvas APIs (#1813)

    Fil Maj authored Jun 12, 2024
    Copy the full SHA
    a2c0fe5 View commit details
  2. Public @slack/web-api@7.1.0 (#1814)

    Fil Maj authored Jun 12, 2024
    Copy the full SHA
    1583183 View commit details
Showing with 4,119 additions and 1,514 deletions.
  1. +1 −1 .github/contributing.md
  2. +1 −1 .github/issue_template.md
  3. +2 −0 .github/maintainers_guide.md
  4. +1 −1 .github/pull_request_template.md
  5. +24 −10 .github/workflows/ci-build.yml
  6. +1 −1 README.md
  7. +28 −0 codecov.yml
  8. +1 −1 docs/_layouts/default.html
  9. +1 −1 docs/_main/about.md
  10. +3 −1 docs/_main/typescript.md
  11. +4 −230 docs/_packages/socket_mode.md
  12. +114 −60 docs/_packages/web_api.md
  13. +2 −2 docs/_packages/webhook.md
  14. +1 −1 examples/greet-and-react/index.js
  15. +0 −15 lerna.json
  16. +7 −15 lint-configs/.eslintrc.js
  17. +2 −0 packages/cli-hooks/README.md
  18. +2 −1 packages/cli-hooks/package.json
  19. +7 −0 packages/cli-test/.c8rc.json
  20. +1 −0 packages/cli-test/.eslintignore
  21. +15 −0 packages/cli-test/.eslintrc.js
  22. +7 −0 packages/cli-test/.gitignore
  23. +3 −0 packages/cli-test/.mocharc.json
  24. +23 −0 packages/cli-test/LICENSE
  25. +74 −0 packages/cli-test/README.md
  26. +59 −0 packages/cli-test/package.json
  27. +88 −0 packages/cli-test/src/cli/cli-process.spec.ts
  28. +115 −0 packages/cli-test/src/cli/cli-process.ts
  29. +26 −0 packages/cli-test/src/cli/command-error.ts
  30. +79 −0 packages/cli-test/src/cli/commands/app.ts
  31. +110 −0 packages/cli-test/src/cli/commands/auth.ts
  32. +82 −0 packages/cli-test/src/cli/commands/collaborator.ts
  33. +64 −0 packages/cli-test/src/cli/commands/create.ts
  34. +84 −0 packages/cli-test/src/cli/commands/env.ts
  35. +37 −0 packages/cli-test/src/cli/commands/external-auth.ts
  36. +36 −0 packages/cli-test/src/cli/commands/function.ts
  37. +28 −0 packages/cli-test/src/cli/commands/manifest.ts
  38. +220 −0 packages/cli-test/src/cli/commands/platform.ts
  39. +183 −0 packages/cli-test/src/cli/commands/trigger.ts
  40. +41 −0 packages/cli-test/src/cli/index.spec.ts
  41. +108 −0 packages/cli-test/src/cli/index.ts
  42. +93 −0 packages/cli-test/src/cli/shell.spec.ts
  43. +222 −0 packages/cli-test/src/cli/shell.ts
  44. +20 −0 packages/cli-test/src/index.spec.ts
  45. +9 −0 packages/cli-test/src/index.ts
  46. +69 −0 packages/cli-test/src/utils/constants.ts
  47. +50 −0 packages/cli-test/src/utils/custom-errors.ts
  48. +24 −0 packages/cli-test/src/utils/logger.ts
  49. +29 −0 packages/cli-test/src/utils/types.ts
  50. +1 −0 packages/cli-test/tsconfig.eslint.json
  51. +28 −0 packages/cli-test/tsconfig.json
  52. +7 −0 packages/logger/.c8rc.json
  53. +0 −1 packages/logger/.gitignore
  54. +0 −14 packages/logger/.nycrc.json
  55. +2 −0 packages/logger/README.md
  56. +4 −4 packages/logger/package.json
  57. +7 −0 packages/oauth/.c8rc.json
  58. +1 −3 packages/oauth/.gitignore
  59. +0 −14 packages/oauth/.nycrc.json
  60. +2 −0 packages/oauth/README.md
  61. +5 −4 packages/oauth/package.json
  62. +7 −0 packages/socket-mode/.c8rc.json
  63. +2 −1 packages/socket-mode/.mocharc.json
  64. +0 −14 packages/socket-mode/.nycrc.json
  65. +15 −30 packages/socket-mode/README.md
  66. +10 −8 packages/socket-mode/package.json
  67. +29 −0 packages/socket-mode/src/SlackWebSocket.spec.ts
  68. +269 −0 packages/socket-mode/src/SlackWebSocket.ts
  69. +0 −258 packages/socket-mode/src/SocketModeClient.spec.js
  70. +284 −0 packages/socket-mode/src/SocketModeClient.spec.ts
  71. +212 −597 packages/socket-mode/src/SocketModeClient.ts
  72. +37 −3 packages/socket-mode/src/SocketModeOptions.ts
  73. +18 −19 packages/socket-mode/src/logger.ts
  74. +81 −4 packages/socket-mode/test/integration.spec.js
  75. +2 −6 packages/socket-mode/tsconfig.json
  76. +1 −1 packages/types/package.json
  77. +124 −34 packages/types/src/block-kit/block-elements.ts
  78. +8 −7 packages/types/src/block-kit/blocks.ts
  79. +42 −0 packages/types/src/block-kit/composition-objects.ts
  80. +7 −0 packages/web-api/.c8rc.json
  81. +0 −14 packages/web-api/.nycrc.json
  82. +2 −0 packages/web-api/README.md
  83. +5 −5 packages/web-api/package.json
  84. +49 −1 packages/web-api/src/WebClient.spec.js
  85. +16 −2 packages/web-api/src/WebClient.ts
  86. +5 −2 packages/web-api/src/errors.ts
  87. +58 −0 packages/web-api/src/methods.ts
  88. +2 −0 packages/web-api/src/types/helpers.ts
  89. +4 −3 packages/web-api/src/types/request/admin/apps.ts
  90. +2 −1 packages/web-api/src/types/request/admin/barriers.ts
  91. +7 −6 packages/web-api/src/types/request/admin/conversations.ts
  92. +2 −1 packages/web-api/src/types/request/admin/emoji.ts
  93. +4 −3 packages/web-api/src/types/request/admin/roles.ts
  94. +2 −1 packages/web-api/src/types/request/admin/teams.ts
  95. +6 −3 packages/web-api/src/types/request/admin/users.ts
  96. +4 −3 packages/web-api/src/types/request/admin/workflows.ts
  97. +4 −2 packages/web-api/src/types/request/api.ts
  98. +2 −1 packages/web-api/src/types/request/apps.ts
  99. +6 −5 packages/web-api/src/types/request/auth.ts
  100. +3 −2 packages/web-api/src/types/request/bots.ts
  101. +91 −0 packages/web-api/src/types/request/canvas.ts
  102. +3 −2 packages/web-api/src/types/request/chat.ts
  103. +6 −4 packages/web-api/src/types/request/conversations.ts
  104. +5 −4 packages/web-api/src/types/request/dnd.ts
  105. +3 −2 packages/web-api/src/types/request/emoji.ts
  106. +1 −0 packages/web-api/src/types/request/index.ts
  107. +2 −2 packages/web-api/src/types/request/openid.ts
  108. +4 −3 packages/web-api/src/types/request/reactions.ts
  109. +2 −1 packages/web-api/src/types/request/reminders.ts
  110. +5 −4 packages/web-api/src/types/request/rtm.ts
  111. +16 −14 packages/web-api/src/types/request/team.ts
  112. +5 −2 packages/web-api/src/types/request/usergroups.ts
  113. +21 −0 packages/web-api/src/types/response/CanvasesAccessDeleteResponse.ts
  114. +21 −0 packages/web-api/src/types/response/CanvasesAccessSetResponse.ts
  115. +22 −0 packages/web-api/src/types/response/CanvasesCreateResponse.ts
  116. +19 −0 packages/web-api/src/types/response/CanvasesDeleteResponse.ts
  117. +20 −0 packages/web-api/src/types/response/CanvasesEditResponse.ts
  118. +24 −0 packages/web-api/src/types/response/CanvasesSectionsLookupResponse.ts
  119. +22 −0 packages/web-api/src/types/response/ConversationsCanvasesCreateResponse.ts
  120. +7 −0 packages/web-api/src/types/response/index.ts
  121. +1 −1 packages/web-api/test/types/methods/admin.apps.test-d.ts
  122. +1 −1 packages/web-api/test/types/methods/admin.barriers.test-d.ts
  123. +2 −2 packages/web-api/test/types/methods/admin.conversations.test-d.ts
  124. +1 −1 packages/web-api/test/types/methods/admin.emoji.test-d.ts
  125. +1 −1 packages/web-api/test/types/methods/admin.roles.test-d.ts
  126. +1 −1 packages/web-api/test/types/methods/admin.teams.test-d.ts
  127. +2 −2 packages/web-api/test/types/methods/admin.users.test-d.ts
  128. +1 −1 packages/web-api/test/types/methods/admin.workflows.test-d.ts
  129. +2 −2 packages/web-api/test/types/methods/api.test-d.ts
  130. +1 −1 packages/web-api/test/types/methods/apps.test-d.ts
  131. +4 −4 packages/web-api/test/types/methods/auth.test-d.ts
  132. +2 −2 packages/web-api/test/types/methods/bots.test-d.ts
  133. +172 −0 packages/web-api/test/types/methods/canvas.test-d.ts
  134. +1 −1 packages/web-api/test/types/methods/chat.test-d.ts
  135. +2 −2 packages/web-api/test/types/methods/conversations.test-d.ts
  136. +3 −3 packages/web-api/test/types/methods/dnd.test-d.ts
  137. +2 −2 packages/web-api/test/types/methods/emoji.test-d.ts
  138. +1 −1 packages/web-api/test/types/methods/openid.test-d.ts
  139. +1 −1 packages/web-api/test/types/methods/reactions.test-d.ts
  140. +1 −1 packages/web-api/test/types/methods/reminders.test-d.ts
  141. +3 −3 packages/web-api/test/types/methods/rtm.test-d.ts
  142. +13 −7 packages/web-api/test/types/methods/team.test-d.ts
  143. +1 −1 packages/web-api/test/types/methods/usergroups.test-d.ts
  144. +7 −0 packages/webhook/.c8rc.json
  145. +0 −14 packages/webhook/.nycrc.json
  146. +2 −0 packages/webhook/README.md
  147. +3 −3 packages/webhook/package.json
2 changes: 1 addition & 1 deletion .github/contributing.md
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@ If the contribution doesn't meet the above criteria, you may fail our automated
3. :herb: Create a new branch and check it out.
4. :crystal_ball: Make your changes and commit them locally. Magic happens here!
5. :arrow_heading_up: Push your new branch to your fork. (e.g. `git push username fix-issue-16`).
6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `master` in this
6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `main` in this
repository.

## Maintainers
2 changes: 1 addition & 1 deletion .github/issue_template.md
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ Describe your issue here.
- [ ] discussion

### Requirements (place an `x` in each of the `[ ]`)
* [ ] I've read and understood the [Contributing guidelines](https://github.com/slackapi/node-slack-sdk/blob/master/.github/contributing.md) and have done my best effort to follow them.
* [ ] I've read and understood the [Contributing guidelines](https://github.com/slackapi/node-slack-sdk/blob/main/.github/contributing.md) and have done my best effort to follow them.
* [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
* [ ] I've searched for any related issues and avoided creating a duplicate issue.

2 changes: 2 additions & 0 deletions .github/maintainers_guide.md
Original file line number Diff line number Diff line change
@@ -165,6 +165,8 @@ Releasing can feel intimidating at first, but rest assured: if you make a mistak
### 🔖 Versioning and Tags
This project is versioned using [Semantic Versioning](http://semver.org/), particularly in the [npm flavor](https://docs.npmjs.com/getting-started/semantic-versioning). Each release is tagged using git. The naming convention for tags is `{package_name}@{version}`. For example, the tag `@slack/web-api@v5.0.0` marks the v5.0.0 release of the `@slack/web-api` package. A single commit will have multiple tags when multiple packages are released simultaneously.

One package that expands upon the standard major.minor.patch version schema typically associated with Semantic Versioning is the `@slack/cli-test` package. This package employs standard major.minor.patch version, in addition to a [build metadata suffix](https://semver.org/#spec-item-10) suffix of the form `+cli.X.Y.Z`, e.g. `0.1.0+cli.2.24.0`. The version after `+cli.` communicates compatibility between the `@slack/cli-test` package and the [Slack Platfrom CLI](https://api.slack.com/automation/quickstart) itself.

### 🪵 Branches
`main` is where active development occurs. Long running named feature branches are occasionally created for collaboration on a feature that has a large scope (because everyone cannot push commits to another person's open Pull Request). After a major version increment, a maintenance branch for the older major version is left open (e.g. `v3`, `v4`, etc).

2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -4,5 +4,5 @@ Describe the goal of this PR. Mention any related Issue numbers.

### Requirements (place an `x` in each `[ ]`)

* [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/node-slack-sdk/blob/master/.github/contributing.md) and have done my best effort to follow them.
* [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/node-slack-sdk/blob/main/.github/contributing.md) and have done my best effort to follow them.
* [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
34 changes: 24 additions & 10 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
@@ -15,14 +15,15 @@ jobs:
matrix:
node-version: [18.x, 20.x]
package:
- packages/cli-hooks
- packages/logger
- packages/oauth
- packages/rtm-api
- packages/socket-mode
- packages/types
- packages/web-api
- packages/webhook
- cli-hooks
- cli-test
- logger
- oauth
- rtm-api
- socket-mode
- types
- web-api
- webhook
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
@@ -32,15 +33,28 @@ jobs:
- name: Get Development Dependencies
run: npm i
- name: Build and Run Tests in Each Package
working-directory: ${{ matrix.package }}
working-directory: packages/${{ matrix.package }}
run: |
npm install || npm list
# depending on which package we are testing, also npm link up other dependent packages
case "$PWD" in
*/webhook) pushd ../types && npm i && popd && npm link ../types;;
*/web-api) pushd ../types && npm i && popd && npm link ../types && pushd ../logger && npm i && popd && npm link ../logger;;
*/oauth) pushd ../logger && npm i && popd && npm link ../logger && pushd ../web-api && npm i && popd && npm link ../web-api;;
# TODO: add socket-mode here once new major version released
*/socket-mode) pushd ../logger && npm i && popd && npm link ../logger && pushd ../web-api && npm i && popd && npm link ../web-api;;
*) ;; # default
esac
npm test
- name: Check for coverage report existence
id: check_coverage
uses: andstor/file-existence-action@v3
with:
files: packages/${{ matrix.package }}/coverage/lcov.info
- name: Upload code coverage
if: matrix.node-version == '20.x' && steps.check_coverage.outputs.files_exists == 'true'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: packages/${{ matrix.package }}/coverage
flags: ${{ matrix.package }}
verbose: true
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Node Slack SDK

[![build-ci](https://github.com/slackapi/node-slack-sdk/workflows/Node.js/badge.svg)](https://github.com/slackapi/node-slack-sdk/actions?query=workflow%3A%22Node.js%22)
[![build-ci](https://github.com/slackapi/node-slack-sdk/actions/workflows/ci-build.yml/badge.svg)](https://github.com/slackapi/node-slack-sdk/actions/workflows/ci-build.yml)
<!-- TODO: npm versions with scoped packages: https://github.com/rvagg/nodei.co/issues/24 -->
___

28 changes: 28 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
comment:
layout: "condensed_header, diff, flags"

flags:
cli-hooks:
paths:
- packages/cli-hooks/**
carryforward: true
logger:
paths:
- packages/logger/**
carryforward: true
oauth:
paths:
- packages/oauth/**
carryforward: true
socket-mode:
paths:
- packages/socket-mode/**
carryforward: true
web-api:
paths:
- packages/web-api/**
carryforward: true
webhook:
paths:
- packages/webhook/**
carryforward: true
2 changes: 1 addition & 1 deletion docs/_layouts/default.html
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@

<ul class="sidebar_menu_list footer_nav">
<li><a href="{{ site.code_of_conduct_url }}">Code of Conduct</a></li>
<li><a href="https://github.com/{{ site.github_username }}/{{ site.repo_name }}/blob/master/.github/contributing.md">Contributing</a></li>
<li><a href="https://github.com/{{ site.github_username }}/{{ site.repo_name }}/blob/main/.github/contributing.md">Contributing</a></li>
<li><a href="{{ site.cla_url }}">Contributor License Agreement</a></li>
</ul>

2 changes: 1 addition & 1 deletion docs/_main/about.md
Original file line number Diff line number Diff line change
@@ -8,5 +8,5 @@ hidden: true
{{ site.title }} is proudly maintained with :sparkling_heart: by the Slack Platform team

* [Code of Conduct]({{ site.code_of_conduct_url }})
* [Contributing](https://github.com/{{ site.github_username }}/{{ site.repo_name }}/blob/master/.github/contributing.md)
* [Contributing](https://github.com/{{ site.github_username }}/{{ site.repo_name }}/blob/main/.github/contributing.md)
* [Contributor License Agreement]({{ site.cla_url }})
4 changes: 3 additions & 1 deletion docs/_main/typescript.md
Original file line number Diff line number Diff line change
@@ -12,7 +12,9 @@ This page helps describe how to use this package from a project that also uses T

### Minimum version

The latest major versions of the `@slack/web-api`, `@slack/rtm-api`, and `@slack/webhook` packages (v6.x) are supported to build against the minimum TypeScript version v4.1.0. See also: https://slack.dev/node-slack-sdk/tutorials/migrating-to-v6
Latest major versions of `@slack/web-api`, `@slack/rtm-api`, and `@slack/webhook` packages are supported to build against TypeScript version 5.3.x. You can try to use a greater minor version of Typescript like 5.4 or above, but beware that [API Breaking Changes](https://github.com/microsoft/TypeScript/wiki/API-Breaking-Changes) can be introduced in minor Typescript versions that break compatibility.

The v6 versions of `@slack/web-api`, `@slack/rtm-api`, and `@slack/webhook` packages are supported to build against the minimum TypeScript version v4.1.0. See also [v5 to v6 migration guide](https://slack.dev/node-slack-sdk/tutorials/migrating-to-v6) for more details.

The v5 versions of `@slack/web-api`, `@slack/rtm-api`, and `@slack/webhook` packages are supported to build against TypeScript v3.3.0 or higher. The v4 versions of the `@slack/web-api`, `@slack/rtm-api`, and `@slack/webhook` packages are supported to build against TypeScript v2.7.0 or higher.

234 changes: 4 additions & 230 deletions docs/_packages/socket_mode.md
Original file line number Diff line number Diff line change
@@ -7,133 +7,11 @@ anchor_links_header: Usage

# Slack Socket Mode

For baseline package documentation, please see the project's [`README`](https://github.com/slackapi/node-slack-sdk/tree/main/packages/socket-mode#readme) or the [documentation on npm](https://www.npmjs.com/package/@slack/socket-mode).

## Installation
The following contain additional examples that may be useful for consumers.

```shell
$ npm install @slack/socket-mode
```

---

### Prerequisites

This package requires a Slack app with an **App-level token**.

To create a new Slack app, head over to [api.slack.com/apps/new](https://api.slack.com/apps?new_app=1).

To generate an **App Token** for your app, on your app's page on [api.slack.com/apps](https://api.slack.com/apps), under the main **Basic Information** page, scroll down to **App-Level Tokens**. Click **Generate Token and Scopes**, add a name for your app token, and click **Add Scope**. Choose `connections:write` and then click **Generate**. Copy and safely store the generated token!

### Initialize the client

This package is designed to support [**Socket Mode**](https://api.slack.com/socket-mode), which allows your app to receive events from Slack over a WebSocket connection.

The package exports a `SocketModeClient` class. Your app will create an instance of the class for each workspace it communicates with.

```javascript
const { SocketModeClient } = require('@slack/socket-mode');

// Read a token from the environment variables
const appToken = process.env.SLACK_APP_TOKEN;

// Initialize
const socketModeClient = new SocketModeClient({ appToken });
```

### Connect to Slack

After your client establishes a connection, your app can send data to and receive data from Slack. Connecting is as easy as calling the `.start()` method.

```javascript
const { SocketModeClient } = require('@slack/socket-mode');
const appToken = process.env.SLACK_APP_TOKEN;

const socketModeClient = new SocketModeClient({ appToken });

(async () => {
// Connect to Slack
await socketModeClient.start();
})();
```
---

### Listen for an event

Bolt apps register [listener functions](https://slack.dev/bolt-js/reference#listener-functions), which are triggered when a specific event type is received by the client.

If you've used Node's [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) pattern
before, then you're already familiar with how this works, since the client is an `EventEmitter`.

The `event` argument passed to the listener is an object. Its content corresponds to the [type of
event](https://api.slack.com/events) it's registered for.

```javascript
const { SocketModeClient } = require('@slack/socket-mode');
const appToken = process.env.SLACK_APP_TOKEN;

const socketModeClient = new SocketModeClient({ appToken });

// Attach listeners to events by type. See: https://api.slack.com/events/message
socketModeClient.on('message', async ({ event }) => {
console.log(event);
});

(async () => {
await socketModeClient.start();
})();
```

---

### Send a message

To respond to events and send messages back into Slack, we recommend using the `@slack/web-api` package with a `bot token`.

```javascript
const { SocketModeClient } = require('@slack/socket-mode');
const { WebClient } = require('@slack/web-api');

const appToken = process.env.SLACK_APP_TOKEN;
const socketModeClient = new SocketModeClient({ appToken });

const { WebClient } = require('@slack/web-api');
const webClient = new WebClient(process.env.SLACK_BOT_TOKEN);

// Attach listeners to events by type. See: https://api.slack.com/events/message
socketModeClient.on('member_joined_channel', async ({event, body, ack}) => {
try {
// send acknowledgement back to slack over the socketMode websocket connection
// this is so slack knows you have received the event and are processing it
await ack();
await webClient.chat.postMessage({
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `Welcome to the channel, <@${event.user}>. We're here to help. Let us know if you have an issue.`,
},
accessory: {
type: 'button',
text: {
type: 'plain_text',
text: 'Get Help',
},
value: 'get_help',
},
},
],
channel: event.channel,
});
} catch (error) {
console.log('An error occurred', error);
}
});
```
---


### Listen for Interactivity Events
## Listen for Interactivity Events

To receive interactivity events such as shorcut invocations, button clicks, and modal data submission, your listener can subscribe to "interactive" events.

@@ -182,108 +60,4 @@ socketModeClient.on('slash_commands', async ({ body, ack }) => {
});
```

When your app has multiple interactive events or slash commands, you will need to include your own routing logic. This is a good time to consider using Slack's Bolt framework, which provides an easier ways to register listeners for events and user actions. You can learn more in [Bolt's Socket Mode documentation](https://slack.dev/bolt-js/concepts#socket-mode).

---

### Lifecycle events

The client's connection to Slack has a lifecycle. This means the client can be seen as a state machine which transitions through a few states as it connects, disconnects, reconnects, and synchronizes with Slack. The client emits an event for each state it transitions to throughout its lifecycle. If your app simply needs to know whether the client is connected or not, the `.connected` boolean property can be checked.

In the table below, the client's states are listed, which are also the names of the events you can use to observe the transition to that state. The table also includes descriptions for the states and arguments that a listener would receive.

| Event Name | Arguments | Description |
|-----------------|-----------------|-------------|
| `connecting` | | The client is in the process of connecting to the platform. |
| `authenticated` | `(connectData)` - the response from `apps.connections.open` | The client has authenticated with the platform. This is a sub-state of `connecting`. |
| `connected` | | The client is connected to the platform and incoming events will start being emitted. |
| `ready` | | The client is ready to send outgoing messages. This is a sub-state of `connected` |
| `disconnecting` | | The client is no longer connected to the platform and cleaning up its resources. It will soon transition to `disconnected`. |
| `reconnecting` | | The client is no longer connected to the platform and cleaning up its resources. It will soon transition to `connecting`. |
| `disconnected` | `(error)` | The client is not connected to the platform. This is a steady state - no attempt to connect is occurring. The `error` argument will be `undefined` when the client initiated the disconnect (normal). |

The client also emits events that are part of its lifecycle, but aren't states. Instead, they represent specific moments that might be helpful to your app. The following table lists these events, their description, and includes the arguments that a listener would receive.

| Event Name | Arguments | Description |
|-----------------|-----------|-------------|
| `error` | `(error)` | An error has occurred. See [error handling](#handle-errors) for details. |
| `slack_event` | `(eventType, event)` | An incoming Slack event has been received. |
| `unable_to_socket_mode_start` | `(error)` | A problem occurred while connecting, a reconnect may or may not occur. |

---

### Logging

The `SocketModeClient` will log interesting information to the console by default. You can use the `logLevel` to decide how much or what kind of information should be output. There are a few possible log levels, which you can find in the `LogLevel` export. By default, the value is set to `LogLevel.INFO`. While you're in development, it's sometimes helpful to set this to the most verbose: `LogLevel.DEBUG`.

```javascript
// Import LogLevel from the package
const { SocketModeClient, LogLevel } = require('@slack/socket-mode');
const appToken = process.env.SLACK_APP_TOKEN;

// Log level is one of the options you can set in the constructor
const socketModeClient = new SocketModeClient({
appToken,
logLevel: LogLevel.DEBUG,
});

(async () => {
await socketModeClient.start();
})();
```

All the log levels, in order of most to least information are: `DEBUG`, `INFO`, `WARN`, and `ERROR`.

<details>
<summary markdown="span">
<strong><i>Sending log output somewhere besides the console</i></strong>
</summary>

You can also choose to have logs sent to a custom logger using the `logger` option. A custom logger needs to implement specific methods (known as the `Logger` interface):

| Method | Parameters | Return type |
|--------------|-------------------|-------------|
| `setLevel()` | `level: LogLevel` | `void` |
| `setName()` | `name: string` | `void` |
| `debug()` | `...msgs: any[]` | `void` |
| `info()` | `...msgs: any[]` | `void` |
| `warn()` | `...msgs: any[]` | `void` |
| `error()` | `...msgs: any[]` | `void` |

A very simple custom logger might ignore the name and level, and write all messages to a file.

```javascript
const { createWriteStream } = require('fs');
const logWritable = createWriteStream('/var/my_log_file'); // Not shown: close this stream

const socketModeClient = new SocketModeClient({
appToken,
// Creating a logger as a literal object. It's more likely that you'd create a class.
logger: {
debug(...msgs): { logWritable.write('debug: ' + JSON.stringify(msgs)); },
info(...msgs): { logWritable.write('info: ' + JSON.stringify(msgs)); },
warn(...msgs): { logWritable.write('warn: ' + JSON.stringify(msgs)); },
error(...msgs): { logWritable.write('error: ' + JSON.stringify(msgs)); },
setLevel(): { },
setName(): { },
},
});

(async () => {
await socketModeClient.start();
})();
```
</details>

---

## Requirements

This package supports Node v14 and higher. It's highly recommended to use [the latest LTS version of
node](https://github.com/nodejs/Release#release-schedule), and the documentation is written using syntax and features from that version.

## Getting Help

If you get stuck, we're here to help. The following are the best ways to get assistance working through your issue:

* [Issue Tracker](http://github.com/slackapi/node-slack-sdk/issues) for questions, feature requests, bug reports and general discussion related to these packages. Try searching before you create a new issue.
When your app has multiple interactive events or slash commands, you will need to include your own routing logic. This is a good time to consider using Slack's Bolt framework, which provides an easier way to register listeners for events and user actions. You can learn more in [Bolt's Socket Mode documentation](https://slack.dev/bolt-js/concepts#socket-mode).
174 changes: 114 additions & 60 deletions docs/_packages/web_api.md
Original file line number Diff line number Diff line change
@@ -466,68 +466,52 @@ retrying the API call. If you'd like to opt out of that behavior, set the `rejec
---

### Upload a file
As of @slack/web-api v6.8.0, we have introduced a modified way to upload files.

We've received many reports on the performance issue of the existing `files.upload` API. So, to cope with the problem, our Platform team decided to unlock a new way to upload files to Slack via public APIs. To utilize the new approach, developers need to implement the following steps on their code side:
Blobs of binary data, sometimes structured, sometimes called a "file", can be useful to upload and share to a channel.
Such files can be uploaded and shared to a channel with the nifty `filesUploadV2` API:

* Call WebClient#files.getUploadURLExternal() method to receive a URL to use for each file
* Perform an HTTP POST request to the URL you received in step 1 for each file
* Call WebClient#files.completeUploadExternal() method with the pairs of file ID and title to complete the whole process, plus share the files in a channel
* If you would like the full metadata of the files, call WebClient#files.info() method for each file

We understand that writing the above code requires many lines of code. Also, existing WebClient#files.upload() users have to take a certain amount of time for migration. To mitigate the pain, we've added a wrapper method named WebClient#files.uploadV2().

Also, in addition to the performance improvements, another good news is that 3rd party apps can now upload multiple files at a time!

See the following code examples demonstrating how the wrapper method works:

##### Legacy way
```javascript
const { WebClient } = require('@slack/web-api');

const token = process.env.SLACK_TOKEN;
const web = new WebClient(token);

const result = await web.files.upload({
file: './path/to/logo.png', // also accepts Buffer or ReadStream
const result = await web.filesUploadV2({
channel_id: 'C0123456789',
initial_comment: 'Here is the new company logo!',
file: './path/to/logo.png',
filename: 'logo.png',
channels: 'C12345',
initial_comment: 'Here is the new company logo',
});

// `result` contains information about the uploaded file
console.log('File uploaded: ', result.file.id);
console.log('File uploaded:', result.files);
```

#### New way
```javascript
const { WebClient } = require('@slack/web-api');
The `file` parameter accepts either the path to a file, a Buffer, or a ReadStream. Details on handling different forms
of binary data follow soon!

const token = process.env.SLACK_TOKEN;
const web = new WebClient(token);
const result = await web.files.uploadV2({
file: './path/to/logo.png', // also accepts Buffer or ReadStream
filename: 'logo.png',
// Note that channels still works but going with channel_id="C12345" is recommended.
// channels="C111,C222" is no longer supported. In this case, an exception will be thrown
channels: 'C12345',
initial_comment: 'Here is the new company logo',
});
The `channel_id` and `initial_comment` aren't required, but the file either won't be shared to a channel or it won't be
posted with a message if these aren't included.

// `result may contain multiple files uploaded
console.log('File(s) uploaded: ', result.files);
```
In a successful response, the `result.files` contains an array of [shared files](https://api.slack.com/types/file).
These files are "private" and available to just the `token` holder if no `channel_id` is included in the request, and
are marked "public" when shared to a provided `channel_id`.

#### New way - upload multiple files
```javascript
const { WebClient } = require('@slack/web-api');
Multiple files can also be uploaded at once, without needing to write a loop, using the `files_uploads` parameter. This
accepts similar attributes as a single file:

const token = process.env.SLACK_TOKEN;
const web = new WebClient(token);
const result = await web.files.uploadV2({
initial_comment: 'Here are the new company assets!',
* `alt_text`: A description of images for a screen-reader.
* `content`: The file contents as a string. If omitted, `file` must be provided.
* `file`: The file path or data to upload. If omitted, `content` must be provided.
* `filename`: The name of the file.
* `filetype`: A file type identifier. [Reference](https://api.slack.com/types/file#file_types).
* `snippet_type`: Syntax type of the snippet being uploaded. E.g. `python`.
* `title`: The title of the file.

```javascript
const result = await web.filesUploadV2({
channel_id: 'C0123456789',
thread_ts: '1223313423434.131321',
channel_id: 'C12345',
initial_comment: 'Here are the new company assets!',
file_uploads: [
{
file: './path/to/logo.png',
@@ -538,18 +522,22 @@ const result = await web.files.uploadV2({
filename: 'logo-sm.png',
},
],
});
```
});
```

### Handling binary data
Several methods, `files.uploadV2`, `files.upload (legacy)`, and `users.setPhoto`, allow you to upload a file. In Node, there are a few
ways you might be dealing with files, or more generally, binary data. When you have the whole file in memory (like when
you've just generated or processed an image), then in Node you'd have a `Buffer` that contains that binary data. Or,
when you are reading the file from disk or a network (like when you have a path to file name), then you'd typically have
a `ReadStream`. The client can handle both of these binary data types for you, and it looks like any other API call.
Notice a `thread_ts` even snuck into that last example just for those threaded uploaded needs.

The following example shows how you can use [`files.upload`](https://api.slack.com/methods/files.upload) to upload a
file that is read from disk (as a `ReadStream`).
**Handling binary data:**

Several methods, `filesUploadV2`, `files.upload (deprecated)`, and `users.setPhoto`, allow you to upload a file. In
Node, there are a few ways you might be dealing with files, or more generally, binary data. When you have the whole file
in memory (like when you've just generated or processed an image), then in Node you'd have a
[`Buffer`](https://nodejs.org/api/buffer.html) that contains that binary data. Or, when you are reading the file from
disk or a network (like when you have a path to file name), then you'd typically have a
[`ReadStream`](https://nodejs.org/api/fs.html#class-fsreadstream). The client can handle both of these binary data types
for you, and it looks like any other API call.

The following example shows how you can use `filesUploadV2` to upload a file that is read from disk (as a `ReadStream`).

```javascript
const { createReadStream } = require('fs');
@@ -562,22 +550,88 @@ const web = new WebClient(token);
const filename = 'test_file.csv';

(async () => {
// Just use the `file` argument as the documentation suggests
// See: https://api.slack.com/methods/files.upload
const result = await web.files.upload({
const result = await web.filesUploadV2({
filename,
// You can use a ReadStream or a Buffer for the file option
// This file is located in the current directory (`process.pwd()`), so the relative path resolves
file: createReadStream(`./${fileName}`),
})
});

// `res` contains information about the uploaded file
console.log('File uploaded: ', result.file.id);
console.log('File uploaded:', result.files);
})();
```

In the example above, you could also use a `Buffer` object as the value for the `file` property of the options object.

**Migrating from the `files.upload` API method:**

Prior to @slack/web-api v6.8.0, `files.upload` was the recommended way to upload files. Release @slack/web-api v6.8.0
introduced the now recommended `filesUploadV2` above.

At that time we were receiving many reports on the performance issue of the prior `files.upload` API. So, to cope with
the problem, our Platform team decided to unlock a new way to upload files to Slack via public APIs.

To utilize this new approach, developers need to implement the following steps on their code side:

* Call WebClient#files.getUploadURLExternal() method to receive a URL to use for each file
* Perform an HTTP POST request to the URL you received in step 1 for each file
* Call WebClient#files.completeUploadExternal() method with the pairs of file ID and title to complete the whole
process, plus share the files in a channel
* If you would like the full metadata of the files, call WebClient#files.info() method for each file

We understand that writing the above code requires many lines of code. Also, existing WebClient#files.upload() users
have to take a certain amount of time for migration. To mitigate the pain, we've added a wrapper method named
WebClient#filesUploadV2().

Also, in addition to the performance improvements, another good news is that 3rd party apps can now upload multiple
files at a time!

Changes needed to migrate from `files.upload` to `filesUploadV2` are detailed after this example of a `files.upload`
call:

```javascript
const { WebClient } = require('@slack/web-api');

const token = process.env.SLACK_TOKEN;
const web = new WebClient(token);

const result = await web.files.upload({
file: './path/to/logo.png',
filename: 'logo.png',
channels: 'C0123456789',
initial_comment: 'Here is the new company logo!',
});

console.log('File uploaded:', result.file);
```

To update that snippet to `filesUploadV2`, the following changes are needed:

* Replace `web.files.upload` with `web.filesUploadV2`
* Swap `channels` for `channel_id` and ensure only one channel is provided
* Change `result.file` to `result.files`

```javascript
const { WebClient } = require('@slack/web-api');

const token = process.env.SLACK_TOKEN;
const web = new WebClient(token);

const result = await web.filesUploadV2({
file: './path/to/logo.png',
filename: 'logo.png',
channel_id: 'C0123456789',
initial_comment: 'Here is the new company logo!',
});

console.log('File uploaded:', result.files);
```

Please note
[the planned sunset date](https://api.slack.com/changelog/2024-04-a-better-way-to-upload-files-is-here-to-stay) of
2025-03-11 for the `files.upload` method if you're using it at this time. Migrating when possible is recommended and
`filesUploadV2` offers advantages that we're excited to share!

---

### Proxy requests with a custom agent
4 changes: 2 additions & 2 deletions docs/_packages/webhook.md
Original file line number Diff line number Diff line change
@@ -8,9 +8,9 @@ order: 6

# Slack Incoming Webhooks

[![build-ci](https://github.com/slackapi/node-slack-sdk/workflows/CI%20Build/badge.svg)](https://github.com/slackapi/node-slack-sdk/actions?query=workflow%3A%22CI+Build%22)
[![build-ci](https://github.com/slackapi/node-slack-sdk/actions/workflows/ci-build.yml/badge.svg)](https://github.com/slackapi/node-slack-sdk/actions/workflows/ci-build.yml)
<!-- TODO: per-flag badge https://docs.codecov.io/docs/flags#section-flag-badges-and-graphs -->
[![codecov](https://codecov.io/gh/slackapi/node-slack-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/slackapi/node-slack-sdk)
[![codecov](https://codecov.io/gh/slackapi/node-slack-sdk/branch/main/graph/badge.svg)](https://codecov.io/gh/slackapi/node-slack-sdk)
<!-- TODO: npm versions with scoped packages: https://github.com/rvagg/nodei.co/issues/24 -->

The `@slack/webhook` package contains a helper for making requests to Slack's [Incoming
2 changes: 1 addition & 1 deletion examples/greet-and-react/index.js
Original file line number Diff line number Diff line change
@@ -147,5 +147,5 @@ function logConfigurationError(envVarNames) {
const description = envVarNames.length > 1 ?
`${envVarNames.join(', ')} environment variables` :
`${envVarNames[0]} environment variable`;
console.log(`***\nCould not start up the application. Have you set your ${description}?\n\nSee https://github.com/slackapi/node-slack-sdk/blob/master/examples/greet-and-react/README.md#run-locally-or-\n`);
console.log(`***\nCould not start up the application. Have you set your ${description}?\n\nSee https://github.com/slackapi/node-slack-sdk/blob/main/examples/greet-and-react/README.md#run-locally-or-\n`);
}
15 changes: 0 additions & 15 deletions lerna.json

This file was deleted.

22 changes: 7 additions & 15 deletions lint-configs/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -225,7 +225,7 @@ module.exports = {
},
{
selector: 'parameter',
format: ['camelCase'],
format: ['camelCase', 'snake_case'],
leadingUnderscore: 'allow',
},
{
@@ -235,11 +235,11 @@ module.exports = {
},
{
selector: 'typeProperty',
format: ['snake_case', 'camelCase'],
format: ['snake_case', 'camelCase', 'UPPER_CASE'],
},
{
'selector': 'objectLiteralProperty',
format: ['camelCase', 'snake_case', 'PascalCase'],
format: ['camelCase', 'snake_case', 'PascalCase', 'UPPER_CASE'],
},
{
selector: ['enumMember'],
@@ -250,17 +250,7 @@ module.exports = {
// Allow cyclical imports. Turning this rule on is mainly a way to manage the performance concern for linting
// time. Our projects are not large enough to warrant this. Overrides AirBnB styles.
'import/no-cycle': 'off',

// Prevent importing submodules of other modules. Using the internal structure of a module exposes
// implementation details that can potentially change in breaking ways. Overrides AirBnB styles.
'import/no-internal-modules': ['error', {
// Use the following option to set a list of allowable globs in this project.
allow: [
'**/middleware/*', // the src/middleware directory doesn't export a module, it's just a namespace.
'**/receivers/*', // the src/receivers directory doesn't export a module, it's just a namespace.
'**/types/**/*', // type heirarchies should be used however one wants
],
}],
'import/prefer-default-export': 'off',

// Remove the minProperties option for enforcing line breaks between braces. The AirBnB config sets this to 4,
// which is arbitrary and not backed by anything specific in the style guide. If we just remove it, we can
@@ -271,7 +261,7 @@ module.exports = {
},
},
{
files: ['src/**/*.spec.ts'],
files: ['src/**/*.spec.ts', 'src/**/*.spec.js'],
rules: {
// Test-specific rules
// ---
@@ -302,6 +292,8 @@ module.exports = {
'@typescript-eslint/no-non-null-assertion': 'off',
// Using ununamed functions (e.g., null logger) in tests is fine
'func-names': 'off',
// Some packages, like socket-mode, use classes, and to test constructors, it may be useful to just create a new Whatever()
'no-new': 'off',
// In tests, don't force constructing a Symbol with a descriptor, as
// it's probably just for tests
'symbol-description': 'off',
2 changes: 2 additions & 0 deletions packages/cli-hooks/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Slack CLI Hooks

[![codecov](https://codecov.io/gh/slackapi/node-slack-sdk/graph/badge.svg?token=OcQREPvC7r&flag=cli-hooks)](https://codecov.io/gh/slackapi/node-slack-sdk)

The `@slack/cli-hooks` package contains scripts that implement the contract
between the [Slack CLI][cli] and [Bolt for JavaScript][bolt].

3 changes: 2 additions & 1 deletion packages/cli-hooks/package.json
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@
"eslint-plugin-node": "^11.1.0",
"mocha": "^10.2.0",
"shx": "^0.3.4",
"sinon": "^17.0.1"
"sinon": "^17.0.1",
"typescript": "5.4.5"
}
}
7 changes: 7 additions & 0 deletions packages/cli-test/.c8rc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"include": ["src/**/*.ts"],
"exclude": ["**/*.spec.ts"],
"reporter": ["lcov", "text"],
"all": false,
"cache": true
}
1 change: 1 addition & 0 deletions packages/cli-test/.eslintignore
15 changes: 15 additions & 0 deletions packages/cli-test/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const slackEslintConfig = require('../../lint-configs/.eslintrc');
// Same eslint config as the other packages in this monorepo, with some package-specific tweaks listed below

// Don't check object property name convention; we want to allow kebab case for CLI flags which is not easy to support in eslint
const overrideIndex = slackEslintConfig.overrides.findIndex((o) => o.files.includes('**/*.js') && o.files.includes('**/*.ts'));
const selectorIndex = slackEslintConfig.overrides[overrideIndex].rules['@typescript-eslint/naming-convention'].findIndex((s) => s.selector == 'objectLiteralProperty');
slackEslintConfig.overrides[overrideIndex].rules['@typescript-eslint/naming-convention'][selectorIndex] = {
'selector': 'objectLiteralProperty',
format: null
};

// Otherwise export the common config.
module.exports = {
...slackEslintConfig,
}
7 changes: 7 additions & 0 deletions packages/cli-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Node and NPM stuff
/node_modules
package-lock.json
dist

# Coverage carryover
/coverage
3 changes: 3 additions & 0 deletions packages/cli-test/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"require": ["ts-node/register", "source-map-support/register"]
}
23 changes: 23 additions & 0 deletions packages/cli-test/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
MIT License

Copyright (c) 2014- Slack Technologies, LLC

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

74 changes: 74 additions & 0 deletions packages/cli-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# @slack/cli-test

[![codecov](https://codecov.io/gh/slackapi/node-slack-sdk/graph/badge.svg?token=OcQREPvC7r&flag=cli-test)](https://codecov.io/gh/slackapi/node-slack-sdk)

This library is designed to automate the [Slack Platform Command Line Interface][cli] (CLI). It provides a programmatic way to interact with the CLI using node.js and is used primarily for end-to-end (E22) testing.

# Requirements

1. Ensure the [Slack CLI][cli] is installed on your system.
2. Export the path to the CLI binary as a `SLACK_CLI_PATH` environment variable.

# Quickstart

1. Install the package

```bash
npm install @slack/cli-test
```

2. Set the path to the CLI executable using the environment variable `SLACK_CLI_PATH`
- supply a link to a binary on the global path, like `slack-cli`
- it will default to using `slack` otherwise
3. Import and use `SlackCLI` to automate the CLI!

```ts
import { SlackCLI } from '@slack/cli-test';
...
const createOutput = await SlackCLI.createAppFromTemplate('slackapi/deno-hello-world');
```

# API / Usage

This package exports the following:

1. `SlackCLI` - an object containing a variety of methods to interact with the CLI
- methods are named after [Slack CLI commands][commands], e.g. `SlackCLI.deploy()`
2. `SlackCLIProcess` - a class that can be instantiated that exposes the ability to run arbitrary commands, with optional global flags as well as command-specific flags.
3. `SlackTracerId` - trace IDs to verify CLI command output
- see available exported IDs on `SlackTracerId` object
- to enable the CLI to show this output, any CLI commands executed by this library are invoked with the environment variable set: `SLACK_TEST_TRACE=true`

```ts
// Import available objects from the package
import { SlackCLI, SlackTracerId } from '@slack/cli-test';

describe('Login with the CLI', () => {
it('can successfully follow the feedback survey link', async function () {
// `login --no-prompt` to get challenge
const loginChallengeResult = await SlackCLI.loginNoPrompt();

// Submit auth ticket in Slack UI
const challenge = await submitCLIAuthTicket(
loginUrlToMyWorkspace,
loginChallengeResult.authTicketSlashCommand
);

// login with challenge and auth ticket
const loginChallengeExchangeResult = await SlackCLI.loginChallengeExchange(
challenge,
loginChallengeResult.authTicket
);
});
});
```

## Configuration

| Environment Variable | Required | Note |
| --------------------- | -------- | ------------------------------------------------------------------------------ |
| `SLACK_CLI_PATH` | yes | path to Slack CLI binary |
| `SLACK_CLI_LOG_LEVEL` | no | default: `info`. [Logger levels](https://github.com/winstonjs/winston#logging) |

[cli]: https://api.slack.com/automation/quickstart
[commands]: https://api.slack.com/automation/cli/commands
59 changes: 59 additions & 0 deletions packages/cli-test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "@slack/cli-test",
"version": "0.2.0+cli.2.25.0",
"description": "Node.js bindings for the Slack CLI for use in automated testing",
"author": "Salesforce, Inc.",
"license": "MIT",
"keywords": ["slack", "cli", "test"],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist/**/*"
],
"engines": {
"node": ">=18.15.5"
},
"repository": "slackapi/node-slack-sdk",
"publishConfig": {
"access": "public"
},
"bugs": {
"url": "https://github.com/slackapi/node-slack-sdk/issues"
},
"scripts": {
"lint": "eslint --ext .ts src",
"build": "npm run build:clean && tsc",
"build:clean": "shx rm -rf ./dist ./coverage",
"prepare": "npm run build",
"mocha": "cross-env SLACK_CLI_PATH=/doesnt/matter mocha --config .mocharc.json src/*.spec.ts src/**/*.spec.ts",
"test": "npm run lint && npm run build && c8 npm run mocha"
},
"dependencies": {
"tree-kill": "^1.2.2",
"winston": "^3.8.2"
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.6",
"@types/chai": "^4.3.16",
"@types/mocha": "^10.0.6",
"@types/node": "^18.15.5",
"@types/sinon": "^17.0.3",
"@typescript-eslint/eslint-plugin": "^6",
"@typescript-eslint/parser": "^6",
"c8": "^9.1.0",
"chai": "^4.4.1",
"cross-env": "^7.0.3",
"eslint": "^8",
"eslint-config-airbnb-base": "^15",
"eslint-config-airbnb-typescript": "^17",
"eslint-plugin-import": "^2",
"eslint-plugin-import-newlines": "^1",
"eslint-plugin-jsdoc": "^48",
"eslint-plugin-node": "^11",
"mocha": "^10.4.0",
"shx": "^0.3.4",
"sinon": "^18.0.0",
"ts-node": "^10.9.2",
"typescript": "5.0.4"
}
}
88 changes: 88 additions & 0 deletions packages/cli-test/src/cli/cli-process.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { assert } from 'chai';
import sinon from 'sinon';
import { shell } from './shell';
import { SlackCLIProcess } from './cli-process';

describe('SlackCLIProcess class', () => {
const sandbox = sinon.createSandbox();
let spawnProcessSpy: sinon.SinonStub;

beforeEach(() => {
spawnProcessSpy = sandbox.stub(shell, 'spawnProcess');
sandbox.stub(shell, 'checkIfFinished');
});
afterEach(() => {
sandbox.restore();
});

describe('constructor', () => {
it('should throw if `SLACK_CLI_PATH` env variable is falsy', () => {
const orig = process.env.SLACK_CLI_PATH;
delete process.env.SLACK_CLI_PATH;
assert.throws(() => {
new SlackCLIProcess('help');
});
process.env.SLACK_CLI_PATH = orig;
});
});

describe('CLI flag handling', () => {
describe('global options', () => {
it('should map qa or dev options to --slackdev', async () => {
let cmd = new SlackCLIProcess('help', { qa: true });
await cmd.execAsync();
sandbox.assert.calledWithMatch(spawnProcessSpy, '--slackdev');
spawnProcessSpy.resetHistory();
cmd = new SlackCLIProcess('help');
await cmd.execAsync();
sandbox.assert.neverCalledWithMatch(spawnProcessSpy, '--slackdev');
spawnProcessSpy.resetHistory();
cmd = new SlackCLIProcess('help', { dev: true });
await cmd.execAsync();
sandbox.assert.calledWithMatch(spawnProcessSpy, '--slackdev');
});
it('should default to passing --skip-update but allow overriding that', async () => {
let cmd = new SlackCLIProcess('help');
await cmd.execAsync();
sandbox.assert.calledWithMatch(spawnProcessSpy, '--skip-update');
spawnProcessSpy.resetHistory();
cmd = new SlackCLIProcess('help', { skipUpdate: false });
await cmd.execAsync();
sandbox.assert.neverCalledWithMatch(spawnProcessSpy, '--skip-update');
spawnProcessSpy.resetHistory();
cmd = new SlackCLIProcess('help', { skipUpdate: true });
await cmd.execAsync();
sandbox.assert.calledWithMatch(spawnProcessSpy, '--skip-update');
spawnProcessSpy.resetHistory();
cmd = new SlackCLIProcess('help', {}); // empty global options; so undefined skipUpdate option
await cmd.execAsync();
sandbox.assert.calledWithMatch(spawnProcessSpy, '--skip-update');
});
});
describe('command options', () => {
it('should pass command-level key/value options to command in the form `--<key> value`', async () => {
const cmd = new SlackCLIProcess('help', {}, { '--awesome': 'yes' });
await cmd.execAsync();
sandbox.assert.calledWithMatch(spawnProcessSpy, '--awesome yes');
});
it('should only pass command-level key option if value is true in the form `--key`', async () => {
const cmd = new SlackCLIProcess('help', {}, { '--no-prompt': true });
await cmd.execAsync();
sandbox.assert.calledWithMatch(spawnProcessSpy, '--no-prompt');
});
it('should not pass command-level key option if value is falsy', async () => {
let cmd = new SlackCLIProcess('help', {}, { '--no-prompt': false });
await cmd.execAsync();
sandbox.assert.neverCalledWithMatch(spawnProcessSpy, '--no-prompt');
spawnProcessSpy.resetHistory();
cmd = new SlackCLIProcess('help', {}, { '--no-prompt': '' });
await cmd.execAsync();
sandbox.assert.neverCalledWithMatch(spawnProcessSpy, '--no-prompt');
spawnProcessSpy.resetHistory();
cmd = new SlackCLIProcess('help', {}, { '--no-prompt': undefined });
await cmd.execAsync();
sandbox.assert.neverCalledWithMatch(spawnProcessSpy, '--no-prompt');
});
});
});
});
115 changes: 115 additions & 0 deletions packages/cli-test/src/cli/cli-process.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { SpawnOptionsWithoutStdio } from 'node:child_process';
import { shell } from './shell';
import type { ShellProcess } from '../utils/types';
/*
* some parameters used in the 'shell' calls that are CLI-specific and probably should not exist there:
* @param skipUpdate skip auto update notification
*/

export interface SlackCLIGlobalOptions {
/**
* @description Whether the command should interact with dev.slack (`--slackdev`)
*/
dev?: boolean;
/**
* @description Whether the command should interact with dev.slack (`--slackdev`)
*/
qa?: boolean;
/**
* @description Whether the CLI should skip updating (`--skip-update`). Defaults to `true`.
*/
skipUpdate?: boolean;
/**
* @description workspace or organization name or ID to scope command to
*/
team?: string;
}

export type SlackCLICommandOptions = Record<string, string | boolean | undefined>;

export class SlackCLIProcess {
/**
* @description The CLI command to invoke
*/
public command: string;

/**
* @description The global CLI options to pass to the command
*/
public globalOptions: SlackCLIGlobalOptions | undefined;

/**
* @description The CLI command-specific options to pass to the command
*/
public commandOptions: SlackCLICommandOptions | undefined;

public constructor(command: string, globalOptions?: SlackCLIGlobalOptions, commandOptions?: SlackCLICommandOptions) {
if (!process.env.SLACK_CLI_PATH) {
throw new Error('`SLACK_CLI_PATH` environment variable not found! Aborting!');
}
this.command = command;
this.globalOptions = globalOptions;
this.commandOptions = commandOptions;
}

/**
* @description Executes the command asynchronously, returning the process details once the process finishes executing
*/
public async execAsync(shellOpts?: Partial<SpawnOptionsWithoutStdio>): Promise<ShellProcess> {
const cmd = this.assembleShellInvocation();
const proc = shell.spawnProcess(cmd, shellOpts);
await shell.checkIfFinished(proc);
return proc;
}

/**
* @description Executes the command asynchronously, returning the process details once the process finishes executing
*/
public async execAsyncUntilOutputPresent(
output: string,
shellOpts?: Partial<SpawnOptionsWithoutStdio>,
): Promise<ShellProcess> {
const cmd = this.assembleShellInvocation();
const proc = shell.spawnProcess(cmd, shellOpts);
await shell.waitForOutput(output, proc);
return proc;
}

/**
* @description Executes the command synchronously, returning the process standard output
*/
public execSync(shellOpts?: Partial<SpawnOptionsWithoutStdio>): string {
const cmd = this.assembleShellInvocation();
return shell.runCommandSync(cmd, shellOpts);
}

private assembleShellInvocation(): string {
let cmd = `${process.env.SLACK_CLI_PATH}`;
if (this.globalOptions) {
const opts = this.globalOptions;
if (opts.qa || opts.dev) {
cmd += ' --slackdev';
}
if (opts.skipUpdate || opts.skipUpdate === undefined) {
cmd += ' --skip-update';
}
if (opts.team) {
cmd += ` --team ${opts.team}`;
}
} else {
cmd += ' --skip-update';
}
cmd += ` ${this.command}`;
if (this.commandOptions) {
Object.entries(this.commandOptions).forEach(([key, value]) => {
if (key && value) {
cmd += ` ${key}`;
if (value !== true) {
cmd += ` ${value}`;
}
}
});
}
return cmd;
}
}
26 changes: 26 additions & 0 deletions packages/cli-test/src/cli/command-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CustomError } from '../utils/custom-errors';

// TODO: this error wrapper should maybe look at official node docs for how to extend errors
// https://nodejs.org/api/errors.html#errors
/**
* Error handler for Lib
* @param error error object to wrap
* @param command command used
* @param additionalInfo any extra info
* @returns The wrapped CustomError object
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function commandError(error: any, command: string, additionalInfo?: string): CustomError {
// Specify error name, if it's a generic Error
if (error.name) {
// eslint-disable-next-line no-param-reassign
error.name = error.name.toString() === 'Error' ? 'commandError' : 'Error';
}

// Create new error and return it
const newError = new CustomError(error.message, error.name, error.stack, {
command,
additionalInfo,
});
return newError;
}
79 changes: 79 additions & 0 deletions packages/cli-test/src/cli/commands/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { SlackCLIProcess } from '../cli-process';
import commandError from '../command-error';

/**
* `slack app delete`
* @param appPath path to app
* @param teamFlag team domain for the function's app
* @returns command output
*/
export const del = async function appDelete(
appPath: string,
teamFlag: string,
options?: { isLocalApp?: boolean, qa?: boolean },
): Promise<string> {
// TODO: breaking change, separate params vs single-param-object, probably should reflect global vs command CLI flags
const appEnvironment = options?.isLocalApp ? 'local' : 'deployed';
const cmd = new SlackCLIProcess('app delete --force', { team: teamFlag, qa: options?.qa }, {
'--app': appEnvironment,
});
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'appDelete');
}
};

/**
* `slack app install`
* @param appPath path to app
* @param teamFlag team domain where the app will be installed
* @returns command output
*/
export const install = async function workspaceInstall(
appPath: string,
teamFlag: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: breaking change, separate params vs single-param-object, probably should reflect global vs command CLI flags
const cmd = new SlackCLIProcess('app install', { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'appInstall');
}
};

/**
* `slack app list`
* @param appPath path to app
* @returns command output
*/
export const list = async function appList(
appPath: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate parameters vs single-param-object
const cmd = new SlackCLIProcess('app list', options);
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'appList');
}
};

// TODO: (breaking change): rename properties of this default export to match actual command names
export default {
workspaceDelete: del,
workspaceInstall: install,
workspaceList: list,
};
110 changes: 110 additions & 0 deletions packages/cli-test/src/cli/commands/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { ShellProcess } from '../../utils/types';
import { SlackCLIProcess, SlackCLIGlobalOptions, SlackCLICommandOptions } from '../cli-process';
import commandError from '../command-error';

export default {
/**
* `slack login --no-prompt`
*/
loginNoPrompt: async function loginNoPrompt(options?: { qa?: boolean }): Promise<{
/**
* Command output
*/
shellOutput: ShellProcess['output'];
/**
* Slash command with auth ticket, e.g. '/slackauthticket MTMxNjgxMDUtYTYwOC00NzRhLWE3M2YtMjVmZTQyMjc1MDg4'
*/
authTicketSlashCommand: string;
/**
* An auth ticket is a A UUID sequence granted by Slack to a CLI auth requestor.
* That ticket must then be submitted via slash command by a user logged in to Slack and permissions accepted to be
* granted a token for use.
*/
authTicket: string;
}> {
const cmd = new SlackCLIProcess('login', options, {
'--no-prompt': true,
});
try {
const proc = await cmd.execAsync();

// Get auth token
const authTicketSlashCommand = proc.output.match('/slackauthticket(.*)')![0];
const authTicket = authTicketSlashCommand.split(' ')[1];

return {
shellOutput: proc.output,
authTicketSlashCommand,
authTicket,
};
} catch (error) {
throw commandError(
error,
this.loginNoPrompt.name,
'Error running command. \nTip: You must have no active authenticated sessions in cli',
);
}
},

// TODO: (breaking change) inconsistent use of object-as-params vs. separate parameters
/**
* `slack login --no-prompt --challenge --ticket`
* @param challenge challenge string from UI
* @param authTicket authTicket string from loginNoPrompt
* @param options
* @returns
*/
loginChallengeExchange: async function loginChallengeExchange(
challenge: string,
authTicket: string,
options?: {
qa?: boolean;
},
): Promise<string> {
const cmd = new SlackCLIProcess('login', options, {
'--no-prompt': true,
'--challenge': challenge,
'--ticket': authTicket,
});
try {
const proc = await cmd.execAsync();
return proc.output;
} catch (error) {
throw commandError(
error,
this.loginChallengeExchange.name,
'Error running command. \nTip: You must be authenticated in Slack client and have valid challenge and authTicket',
);
}
},

/**
* `slack logout`
* @returns command output
*/
logout: async function logout(options?: {
// TODO: (breaking change) the two flags here are mutually exclusive; model better using an `|` of types
/** team domain to logout from */
teamFlag?: string;
/** perform the logout for all authentications */
allWorkspaces?: boolean;
qa?: boolean;
}): Promise<string> {
// TODO: (breaking change) inconsistent use of object-as-params vs. separate parameters
// Create the command with workspaces to logout of
const globalOpts: SlackCLIGlobalOptions = { qa: options?.qa };
const cmdOpts: SlackCLICommandOptions = {};
if (options?.teamFlag) {
globalOpts.team = options.teamFlag;
} else if (options?.allWorkspaces) {
cmdOpts['--all'] = true;
}
const cmd = new SlackCLIProcess('logout', globalOpts, cmdOpts);
try {
const proc = await cmd.execAsync();
return proc.output;
} catch (error) {
throw commandError(error, 'logout');
}
},
};
82 changes: 82 additions & 0 deletions packages/cli-test/src/cli/commands/collaborator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { SlackCLIProcess } from '../cli-process';
import commandError from '../command-error';

/**
* `slack collaborators add`
* @param appPath path to app
* @param teamFlag team domain to add collaborators to
* @param collaboratorEmail email of the user to be added as a collaborator
* @returns command output
*/
export const add = async function collaboratorsAdd(
appPath: string,
teamFlag: string,
collaboratorEmail: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate parameters vs single-param-object
const cmd = new SlackCLIProcess(`collaborators add ${collaboratorEmail}`, { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'collaboratorsAdd');
}
};

/**
* `slack collaborators list`
* @param appPath path to app
* @param teamFlag team domain to list collaborators for
* @returns command output
*/
export const list = async function collaboratorsList(
appPath: string,
teamFlag: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate parameters vs single-param-object
const cmd = new SlackCLIProcess('collaborators list', { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'collaboratorsList');
}
};

/**
* `slack collaborators remove`
* @param appPath path to app
* @param teamFlag team domain to remove collaborators from
* @param collaboratorEmail email of the user to be removed as a collaborator
* @returns command output
*/
export const remove = async function collaboratorsRemove(
appPath: string,
teamFlag: string,
collaboratorEmail: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate parameters vs single-param-object
const cmd = new SlackCLIProcess(`collaborators remove ${collaboratorEmail}`, { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'collaboratorsRemove');
}
};

// TODO: (breaking change): rename properties of this default export to match actual command names
export default {
collaboratorsAdd: add,
collaboratorsList: list,
collaboratorsRemove: remove,
};
64 changes: 64 additions & 0 deletions packages/cli-test/src/cli/commands/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { SpawnOptionsWithoutStdio } from 'node:child_process';
import { SlackCLIProcess, SlackCLICommandOptions, SlackCLIGlobalOptions } from '../cli-process';
import commandError from '../command-error';

/**
* `slack create`
* @param opts generic command options to pass to `create`
* @returns command output
*/
export const create = async function create(
appName?: string, // TODO: bad arg name. it should be app path, because this is effectively how it is used
globalOpts?: SlackCLIGlobalOptions,
commandOpts?: SlackCLICommandOptions,
shellOpts?: SpawnOptionsWithoutStdio,
): Promise<string> {
// TODO: single object param vs separate params (breaking change)
let cmdStr = 'create';
if (appName) {
cmdStr += ` ${appName}`;
}
const cmd = new SlackCLIProcess(cmdStr, globalOpts, commandOpts);
try {
const proc = await cmd.execAsync(shellOpts);
return proc.output;
} catch (error) {
throw commandError(error, 'create');
}
};

// TODO: (breaking change) remove this method
/**
* `slack create` using a template
* Creates an app from a specified template string.
* @param templateString template string (ex: `slack-samples/deno-hello-world`)
* @param appName desired app name
* @param branchName the branch to clone (default: `main`)
* @returns command output
*/
export const createAppFromTemplate = async function createAppFromTemplate({
templateString,
appName = '',
branchName = 'main',
shellOpts = {},
}: {
templateString: string;
appName?: string; // TODO: bad arg name. it should be app path, because this is effectively how it is used
branchName?: string;
shellOpts?: SpawnOptionsWithoutStdio;
}): Promise<string> {
try {
return await create(appName, {}, {
'--template': templateString,
'--branch': branchName,
}, shellOpts);
} catch (error) {
throw commandError(error, 'createAppFromTemplate');
}
};

// TODO: (breaking change): rename properties of this default export to match actual command names
export default {
createAppFromTemplate,
createApp: create,
};
84 changes: 84 additions & 0 deletions packages/cli-test/src/cli/commands/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { SlackCLIProcess } from '../cli-process';
import commandError from '../command-error';

/**
* `slack env add`
* @param appPath path to app
* @param teamFlag team domain to add env var to
* @param secretKey environment variable key
* @param secretValue environment variable value
* @returns command output
*/
export const add = async function envAdd(
appPath: string,
teamFlag: string,
secretKey: string,
secretValue: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate parameters vs single-param-object
const cmd = new SlackCLIProcess(`env add ${secretKey} ${secretValue}`, { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'envAdd');
}
};

/**
* `slack env list`
* @param appPath path to app
* @param teamFlag team domain to list env vars for
* @returns command output
*/
export const list = async function envList(
appPath: string,
teamFlag: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate parameters vs single-param-object
const cmd = new SlackCLIProcess('env list', { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'envList');
}
};

/**
* `slack env remove`
* @param appPath path to app
* @param teamFlag team domain to remove env var from
* @param secretKey environment variable key
* @returns command output
*/
export const remove = async function envRemove(
appPath: string,
teamFlag: string,
secretKey: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate parameters vs single-param-object
const cmd = new SlackCLIProcess(`env remove ${secretKey}`, { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'envRemove');
}
};

// TODO: (breaking change): rename properties of this default export to match actual command names
export default {
envAdd: add,
envList: list,
envRemove: remove,
};
37 changes: 37 additions & 0 deletions packages/cli-test/src/cli/commands/external-auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { SlackCLIProcess } from '../cli-process';
import commandError from '../command-error';

/**
* `slack external-auth`
* @param appPath path to app
* @param teamFlag team domain of the relevant app
* @param provider provider to add external auth for
* @param flags specification of external-auth, e.g. add or add-secret
* @returns command output
*/
export const externalAuth = async function externalAuth(
appPath: string,
teamFlag: string,
provider: string,
flags: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate parameters vs single-param-object
// TODO: this is a generic entry point to the `external-auth` suite of commands, and today `flags` is abused to
// specify the actual sub-command. easy, but lazy, not sure if best approach
const cmd = new SlackCLIProcess(`external-auth ${flags}`, { team: teamFlag, qa: options?.qa }, {
'--provider': provider,
});
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'externalAuth');
}
};

export default {
externalAuth,
};
36 changes: 36 additions & 0 deletions packages/cli-test/src/cli/commands/function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { SlackCLIProcess } from '../cli-process';
import commandError from '../command-error';

// TODO: the "flag" param throughout here should be done in a better way.
// Perhaps expose the SlackCommandOptions type directly?

/**
* `slack function access`
* @param appPath path to app
* @param teamFlag team domain for the function's app
* @param flags specification of function distribution, i.e. --name greeting_function --app-collaborators
* @returns command output
*/
export const access = async function functionAccess(
appPath: string,
teamFlag: string,
flags: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: breaking change, separate params vs single-param-object
const cmd = new SlackCLIProcess(`function access ${flags}`, { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'functionAccess');
}
};

// TODO: (breaking change): rename properties of this default export to match actual command names
export default {
functionDistribute: access, // TODO: brekaing change remove this, is now called 'function access'
functionAccess: access,
};
28 changes: 28 additions & 0 deletions packages/cli-test/src/cli/commands/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { SlackCLIProcess } from '../cli-process';
import commandError from '../command-error';

/**
* `slack manifest validate`
* @param appPath path to app
* @returns command output
*/
export const validate = async function manifestValidate(
appPath: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: breaking change, separate params vs single-param-object
const cmd = new SlackCLIProcess('manifest validate', options);
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'manifestValidate');
}
};

// TODO: (breaking change): rename properties of this default export to match actual command names
export default {
manifestValidate: validate,
};
220 changes: 220 additions & 0 deletions packages/cli-test/src/cli/commands/platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import logger from '../../utils/logger';
import { SlackCLIProcess } from '../cli-process';
import { shell } from '../shell';
import type { ShellProcess } from '../../utils/types';
import commandError from '../command-error';
import { SlackTracerId } from '../../utils/constants';

// TODO: the options for these methods could be DRYed up

export default {
/**
* `slack platform activity`
* @param flag
* @returns command output
*/
activity: async function activity({
appPath,
teamFlag,
flag,
localApp = true,
qa = false,
}: {
/** Path to app */
appPath: string;
/** workspace or organization name or ID to deploy the app to */
teamFlag: string;
/** Arbitrary flags to provide to the command */
flag?: string;
/** Whether to operate on the local or deployed app */
localApp?: boolean; // TODO: this option is provided inconsistently across commands (breaking change)
/** Whether to operate against --slackdev or production */
qa?: boolean; // TODO: this option is provided inconsistently across commands (breaking change)
}): Promise<string> {
const appEnvironment = localApp ? 'local' : 'deployed';
const cmd = new SlackCLIProcess(`activity ${flag}`, { team: teamFlag, qa }, {
'--app': appEnvironment,
});
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'activity');
}
},

/**
* waits for a specified sequence then returns the shell
* At the specific point where the sequence is found to continue with test
* @returns command output
*/
activityTailStart: async function activityTailStart({
appPath,
teamFlag,
stringToWaitFor,
localApp = true,
qa = false,
}: {
/** Path to app */
appPath: string;
/** workspace or organization name or ID to deploy the app to */
teamFlag: string;
/** expected string to be present in the output before this function returns */
stringToWaitFor: string;
/** Whether to operate on the local or deployed app */
localApp?: boolean;
/** Whether to operate against --slackdev or production */
qa?: boolean; // TODO: this option is provided inconsistently across commands (breaking change)
}): Promise<ShellProcess> {
const appEnvironment = localApp ? 'local' : 'deployed';
const cmd = new SlackCLIProcess('activity --tail', { team: teamFlag, qa }, {
'--app': appEnvironment,
});
try {
const proc = await cmd.execAsyncUntilOutputPresent(stringToWaitFor, {
cwd: appPath,
});
return proc;
} catch (error) {
throw commandError(error, 'activityTailStart');
}
},

/**
* waits for a specified string in the `activity` output, kills the process then returns the output
* @returns command output
*/
activityTailStop: async function activityTailStop({
/** The ShellProcess to check */
proc,
stringToWait,
}: {
proc: ShellProcess;
/** expected string to be present in the output before process is killed */
stringToWait: string;
}): Promise<string> {
return new Promise((resolve, reject) => {
// Wait for output
shell.waitForOutput(stringToWait, proc).then(() => {
// kill the shell process
shell.kill(proc).then(() => {
resolve(proc.output);
}, (err) => {
const msg = `activityTailStop command failed to kill process: ${err}`;
logger.warn(msg);
reject(new Error(msg));
});
}, reject);
});
},

/**
* `slack deploy`
*/
deploy: async function deploy({
appPath,
teamFlag,
hideTriggers = true,
orgWorkspaceGrantFlag,
qa = false,
}: {
/** Path to app */
appPath: string;
/** workspace or organization name or ID to deploy the app to */
teamFlag: string;
/** hides output and prompts related to triggers. Defaults to `true`. */
hideTriggers?: boolean;
/**
* Org workspace ID, or the string `all` to request access to all workspaces in the org,
* to request grant access to in AAA scenarios
*/
orgWorkspaceGrantFlag?: string;
/** Whether to operate against --slackdev or production */
qa?: boolean; // TODO: this option is provided inconsistently across commands (breaking change)
}): Promise<string> {
const cmd = new SlackCLIProcess('deploy', { team: teamFlag, qa }, {
'--hide-triggers': hideTriggers,
'--org-workspace-grant': orgWorkspaceGrantFlag,
});
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'deploy');
}
},

/**
* start `slack run`
* - `runStop` must be used to stop `run` process
* @returns shell object to kill it explicitly in the test case
*/
runStart: async function runStart({
appPath,
teamFlag,
cleanup = true,
hideTriggers = true,
orgWorkspaceGrantFlag,
qa = false,
}: {
/** Path to app */
appPath: string;
/** workspace or organization name or ID to deploy the app to */
teamFlag: string;
/** delete the app after `run` completes */
cleanup?: boolean;
/** hides output and prompts related to triggers. Defaults to `true`. */
hideTriggers?: boolean;
/**
* Org workspace ID, or the string `all` to request access to all workspaces in the org,
* to request grant access to in AAA scenarios
*/
orgWorkspaceGrantFlag?: string;
/** Whether to operate against --slackdev or production */
qa?: boolean; // TODO: this option is provided inconsistently across commands (breaking change)
}): Promise<ShellProcess> {
const cmd = new SlackCLIProcess('run', { team: teamFlag, qa }, {
'--cleanup': cleanup,
'--hide-triggers': hideTriggers,
'--org-workspace-grant': orgWorkspaceGrantFlag,
});
try {
const proc = await cmd.execAsyncUntilOutputPresent('Connected, awaiting events', {
cwd: appPath,
});
return proc;
} catch (error) {
throw commandError(error, 'runStart');
}
},

/**
* stop `slack run`
* @param shell object with process to kill
* @param teamName to check that app was deleted from that team
*/
runStop: async function runStop(proc: ShellProcess, teamName?: string): Promise<void> {
// TODO: teamName param should be changed to something else. 'wait for shutdown' or some such (breaking change)
return new Promise((resolve, reject) => {
// kill the shell process
shell.kill(proc).then(() => {
if (teamName) {
// TODO: this is messed up. does not match to parameter name at all - team name has nothing to do with this.
// Check if local app was deleted automatically, if --cleanup was passed to `runStart`
// Wait for the output to verify process stopped
shell.waitForOutput(SlackTracerId.SLACK_TRACE_PLATFORM_RUN_STOP, proc).then(resolve, reject);
} else {
resolve();
}
}, (err) => {
const msg = `runStop command failed to kill process: ${err}`;
logger.warn(msg);
reject(new Error(msg));
});
});
},
};
183 changes: 183 additions & 0 deletions packages/cli-test/src/cli/commands/trigger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { SlackCLIProcess } from '../cli-process';
import commandError from '../command-error';

// TODO: the "flag" param throughout here should be done in a better way.
// Perhaps expose the SlackCommandOptions type directly?

/**
* `slack trigger access`
* @param appPath path to app
* @param teamFlag team domain of the updating trigger
* @param flags specification of trigger access, e.g. --trigger-id Ft0143UPTAV8 --everyone
* @returns command output
*/
export const access = async function triggerAccess(
appPath: string,
teamFlag: string,
flags: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate params vs. single-param-object
// TODO: access requires --trigger-id so add that to parameters (breaking change)
const cmd = new SlackCLIProcess(`trigger access ${flags}`, { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'triggerAccess');
}
};
/**
* `slack trigger create`
* @returns command output
*/
export const create = async function triggerCreate({
appPath,
teamFlag,
flag,
orgWorkspaceGrantFlag,
options,
}: {
/** path to app */
appPath: string;
/** team domain where the trigger will be created */
teamFlag: string;
/** any additional flags to provide i.e. method of trigger creation + ref, e.g. --trigger-def triggers/add-pin.json */
flag: string;
/** supplies additional workspace within an org to grant app access to as part of install */
orgWorkspaceGrantFlag?: string;
options?: { // TODO: must be a better way of exposing these options
/** Local app for local run sessions */
localApp?: boolean;
/** Whether to run against --slackdev or production */
qa?: boolean;
};
}): Promise<string> {
const appEnvironment = options?.localApp ? 'local' : 'deployed';
const cmd = new SlackCLIProcess(`trigger create ${flag}`, { team: teamFlag, qa: options?.qa }, {
'--app': appEnvironment,
'--org-workspace-grant': orgWorkspaceGrantFlag,
});
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'triggerCreate');
}
};

/**
* `slack trigger delete`
* @param appPath path to the app
* @param teamFlag team domain to delete trigger from
* @param flag
* @returns command output
*/
export const del = async function triggerDelete(
appPath: string,
teamFlag: string,
flag: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate params vs. single-param-object
// TODO: delete requires --trigger-id so add that to parameters (breaking change)
const cmd = new SlackCLIProcess(`trigger delete ${flag}`, { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'triggerDelete');
}
};

/**
* `slack trigger info`
* @param appPath path to the app
* @param teamFlag team domain of the trigger
* @param flag arbitrary additional flags
* @returns command output
*/
export const info = async function triggerInfo(
appPath: string,
teamFlag: string,
flag: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: getting trigger info necessitates passing a trigger ID, so that should be exposed in the parameters here
// TODO: (breaking change) separate params vs. single-param-object
const cmd = new SlackCLIProcess(`trigger info ${flag}`, { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'triggerInfo');
}
};

/**
* `slack trigger list`
* @param appPath path to app
* @param teamFlag team domain for listing all triggers
* @param flag arbitrary additional flags to pass
* @returns command output
*/
export const list = async function triggerList(
appPath: string,
teamFlag: string,
flag: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate params vs. single-param-object
const cmd = new SlackCLIProcess(`trigger list ${flag}`, { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'triggerList');
}
};

/**
* `slack trigger update`
* @param appPath path to the app
* @param teamFlag team domain for the updating trigger
* @param flag arbitrary additional flags to pass to command
* @returns command output
*/
export const update = async function triggerUpdate(
appPath: string,
teamFlag: string,
flag: string,
options?: { qa?: boolean },
): Promise<string> {
// TODO: (breaking change) separate params vs. single-param-object
const cmd = new SlackCLIProcess(`trigger update ${flag}`, { team: teamFlag, qa: options?.qa });
try {
const proc = await cmd.execAsync({
cwd: appPath,
});
return proc.output;
} catch (error) {
throw commandError(error, 'triggerUpdate');
}
};

// TODO: (breaking change): rename properties of this default export to match actual command names
export default {
triggerAccess: access,
triggerCreate: create,
triggerDelete: del,
triggerInfo: info,
triggerList: list,
triggerUpdate: update,
};
41 changes: 41 additions & 0 deletions packages/cli-test/src/cli/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import sinon from 'sinon';
import logger from '../utils/logger';
import { SlackCLI } from './index';

describe('cli module', () => {
const sandbox = sinon.createSandbox();
let logoutSpy: sinon.SinonStub;
let warnSpy: sinon.SinonStub;
let deleteSpy: sinon.SinonStub;

beforeEach(() => {
logoutSpy = sandbox.stub(SlackCLI, 'logout').resolves();
warnSpy = sandbox.stub(logger, 'warn');
sandbox.stub(SlackCLI.app, 'list').resolves('This thing has so many apps you would not believe');
deleteSpy = sandbox.stub(SlackCLI.app, 'delete').resolves();
});
afterEach(() => {
sandbox.restore();
});

describe('stopSession method', () => {
it('should invoke logout', async () => {
await SlackCLI.stopSession({ appTeamID: 'T123' });
sandbox.assert.called(logoutSpy);
});
it('should warn if logout failed', async () => {
logoutSpy.rejects('boomsies');
await SlackCLI.stopSession({ appTeamID: 'T123' });
sandbox.assert.calledWithMatch(warnSpy, 'boomsies');
});
it('should attempt to delete app if appPath is provided', async () => {
await SlackCLI.stopSession({ appTeamID: 'T123', appPath: '/some/path' });
sandbox.assert.called(deleteSpy);
});
it('should warn if app deletion fails', async () => {
deleteSpy.rejects('explosions');
await SlackCLI.stopSession({ appTeamID: 'T123', appPath: '/some/path' });
sandbox.assert.calledWithMatch(warnSpy, 'explosions');
});
});
});
108 changes: 108 additions & 0 deletions packages/cli-test/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import logger from '../utils/logger';
import appCommands from './commands/app';
import authCommands from './commands/auth';
import collaboratorCommands from './commands/collaborator';
import createCommands from './commands/create';
import envCommands from './commands/env';
import externalAuthCommands from './commands/external-auth';
import functionCommands from './commands/function';
import manifestCommands from './commands/manifest';
import platformCommands from './commands/platform';
import triggerCommands from './commands/trigger';

/**
* Set of functions to spawn and interact with Slack Platform CLI processes and commands
*/
export const SlackCLI = {
...appCommands,
app: {
delete: appCommands.workspaceDelete,
install: appCommands.workspaceInstall,
list: appCommands.workspaceList,
},
...authCommands,
auth: authCommands,
...collaboratorCommands, // TODO: (breaking change) remove, mimic same 'namespacing' as the actual CLI
collaborators: {
add: collaboratorCommands.collaboratorsAdd,
list: collaboratorCommands.collaboratorsList,
remove: collaboratorCommands.collaboratorsRemove,
},
...createCommands,
...envCommands, // TODO: (breaking change) remove, mimic same 'namespacing' as the actual CLI
env: {
add: envCommands.envAdd,
list: envCommands.envList,
remove: envCommands.envRemove,
},
...externalAuthCommands,
...functionCommands,
function: {
access: functionCommands.functionAccess,
},
...manifestCommands, // TODO: (breaking change) remove, mimic same 'namespacing' as the actual CLI
manifest: {
validate: manifestCommands.manifestValidate,
},
...platformCommands,
platform: platformCommands,
...triggerCommands, // TODO: (breaking change) remove, mimic same 'namespacing' as the actual CLI
trigger: {
access: triggerCommands.triggerAccess,
create: triggerCommands.triggerCreate,
delete: triggerCommands.triggerDelete,
info: triggerCommands.triggerInfo,
list: triggerCommands.triggerList,
update: triggerCommands.triggerUpdate,
},

/**
* Delete app and Log out of all sessions
* @param options
*/
stopSession: async function stopSession({
appPath,
appTeamID,
isLocalApp,
qa,
}: {
// TODO: (breaking change) model these types better, if appPath isn't provided then appTeamId
// and isLocalApp are not needed either
/** Path to app. If not provided, will not interact with any app */
appPath?: string;
/** Team domain or ID where app is installed */
appTeamID: string;
isLocalApp?: boolean;
qa?: boolean;
}): Promise<void> {
if (appPath) {
// List instances of app installation if app path provided
const installedAppsOutput = await SlackCLI.app.list(appPath, { qa });
// If app is installed
if (!installedAppsOutput.includes('This project has no apps')) {
// Soft app delete
try {
await SlackCLI.app.delete(appPath, appTeamID, { isLocalApp, qa });
} catch (error) {
logger.warn(`stopSession could not delete app gracefully, continuing. Error: ${error}`);
}

// Delete app.json file. Needed for retries. Otherwise asks for collaborator, if old file is present
fs.rmSync(path.join(appPath, '.slack'), {
force: true,
recursive: true,
});
}
}

// Log out if logged in
try {
await SlackCLI.logout({ allWorkspaces: true, qa });
} catch (error) {
// TODO: maybe should error instead? this seems pretty bad
logger.warn(`Could not logout gracefully. Error: ${error}`);
}
},
};
93 changes: 93 additions & 0 deletions packages/cli-test/src/cli/shell.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { assert } from 'chai';
import sinon from 'sinon';
import child from 'child_process';
import stream from 'stream';
import EventEmitter from 'events';
import { shell } from './shell';
import type { ShellProcess } from '../utils/types';

describe('shell module', () => {
const sandbox = sinon.createSandbox();
let spawnSpy: sinon.SinonStub;
let spawnProcess: child.ChildProcessWithoutNullStreams;
let runSpy: sinon.SinonStub;
let runOutput: child.SpawnSyncReturns<Buffer>;

beforeEach(() => {
spawnProcess = new EventEmitter() as child.ChildProcessWithoutNullStreams;
spawnProcess.stdout = new EventEmitter() as stream.Readable;
spawnProcess.stderr = new EventEmitter() as stream.Readable;
spawnProcess.stdin = new stream.Writable();
spawnSpy = sandbox.stub(child, 'spawn').returns(spawnProcess);
runOutput = { pid: 1337, output: [], stdout: Buffer.from([]), stderr: Buffer.from([]), status: 0, signal: null };
runSpy = sandbox.stub(child, 'spawnSync').returns(runOutput);
sandbox.stub(shell, 'kill').resolves(true);
});
afterEach(() => {
sandbox.restore();
});

describe('spawnProcess method', () => {
it('should invoke `assembleShellEnv` and pass as child_process.spawn `env` parameter', () => {
const fakeEnv = { HEY: 'yo' };
const assembleSpy = sandbox.stub(shell, 'assembleShellEnv').returns(fakeEnv);
const fakeCmd = 'echo "hi"';
shell.spawnProcess(fakeCmd);
sandbox.assert.calledOnce(assembleSpy);
sandbox.assert.calledWithMatch(spawnSpy, fakeCmd, sinon.match({ shell: true, env: fakeEnv }));
});
it('should raise bubble error details up', () => {
spawnSpy.throws(new Error('this is bat country'));
assert.throw(() => {
shell.spawnProcess('about to explode');
}, /this is bat country/);
});
});

describe('runCommandSync method', () => {
it('should invoke `assembleShellEnv` and pass as child_process.spawnSync `env` parameter', () => {
const fakeEnv = { HEY: 'yo' };
const assembleSpy = sandbox.stub(shell, 'assembleShellEnv').returns(fakeEnv);
const fakeCmd = 'echo "hi"';
shell.runCommandSync(fakeCmd);
sandbox.assert.calledOnce(assembleSpy);
sandbox.assert.calledWithMatch(runSpy, fakeCmd, sinon.match({ shell: true, env: fakeEnv }));
});
it('should raise bubble error details up', () => {
runSpy.throws(new Error('this is bat country'));
assert.throw(() => {
shell.runCommandSync('about to explode');
}, /this is bat country/);
});
});

describe('checkIfFinished method', () => {
beforeEach(() => {
});
it('should resolve if underlying process raises a `close` event', (done) => {
const proc: ShellProcess = {
process: spawnProcess,
output: '',
finished: true,
command: 'echo "hi"',
};
shell.checkIfFinished(proc).then(done);
spawnProcess.emit('close', 0);
});
it('should reject if underlying process raises an `error` event', (done) => {
const proc: ShellProcess = {
process: spawnProcess,
output: '',
finished: true,
command: 'echo "hi"',
};
shell.checkIfFinished(proc).then(() => {
assert.fail('checkIfFinished resolved unexpectedly');
}, (err) => {
assert.include(err.message, 'boom');
done();
});
spawnProcess.emit('error', new Error('boom'));
});
});
});
222 changes: 222 additions & 0 deletions packages/cli-test/src/cli/shell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import child from 'child_process';
import treekill from 'tree-kill';
import logger from '../utils/logger';
import { timeouts } from '../utils/constants';
import type { ShellProcess } from '../utils/types';

export const shell = {
/**
* Spawns a shell command
* - Start child process with the command
* - Listen to data output events and collect them
* @param command The command to run, e.g. `echo "hi"`
* @param shellOpts Options to customize shell execution
* @returns command output
*/
spawnProcess: function spawnProcess(
command: string,
shellOpts?: Partial<child.SpawnOptionsWithoutStdio>,
): ShellProcess {
try {
// Start child process
const childProcess = child.spawn(`${command}`, {
shell: true,
env: shell.assembleShellEnv(),
...shellOpts,
});

// Set shell object
const sh: ShellProcess = {
process: childProcess,
output: '',
finished: false,
command,
};

// Log command
logger.info(`CLI Command started: ${sh.command}`);

// If is deploy command

// Listen to data event that returns all the output and collect it
// eslint-disable-next-line @typescript-eslint/no-explicit-any
childProcess.stdout.on('data', (data: any) => {
sh.output += this.removeANSIcolors(data.toString());
logger.verbose(`Output: ${this.removeANSIcolors(data.toString())}`);
});

// Collect error output
// eslint-disable-next-line @typescript-eslint/no-explicit-any
childProcess.stderr.on('data', (data: any) => {
sh.output += this.removeANSIcolors(data.toString());
logger.error(`Error: ${this.removeANSIcolors(data.toString())}`);
});

// Set the finished flag to true on close event
childProcess.on('close', () => {
sh.finished = true;
logger.info(`CLI Command finished: ${sh.command}`);
});

return sh;
} catch (error) {
throw new Error(`spawnProcess failed!\nCommand: ${command}\nError: ${error}`);
}
},

/**
* Run shell command synchronously
* - Execute child process with the command
* - Wait for the command to complete and return the standard output
* @param command cli command, e.g. <cli> --version or any shell command
* @param shellOpts various shell spawning options available to customize
* @returns command stdout
*/
runCommandSync: function runSyncCommand(
command: string,
shellOpts?: Partial<child.SpawnOptionsWithoutStdio>,
): string {
try {
// Log command
logger.info(`CLI Command started: ${command}`);

// Start child process
const result = child.spawnSync(`${command}`, {
shell: true,
env: shell.assembleShellEnv(),
...shellOpts,
});

// Log command
logger.info(`CLI Command finished: ${command}`);

// TODO: this method only returns stdout and not stderr...
return this.removeANSIcolors(result.stdout.toString());
} catch (error) {
throw new Error(`runCommandSync failed!\nCommand: ${command}\nError: ${error}`);
}
},

/**
* Logic to wait for child process to finish executing
* - Check if the close event was emitted, else wait for 1 sec
* - Error out if > 30 sec
* @param shell shell object
*/
checkIfFinished: async function checkIfFinished(proc: ShellProcess): Promise<void> {
return new Promise((resolve, reject) => {
let timeout: NodeJS.Timeout;

const killIt = (reason: string) => {
shell.kill(proc).then(() => {
reject(new Error(`${reason}\nCommand: ${proc.command}, output: ${proc.output}`));
}, (err) => {
reject(new Error(`${reason}\nCommand: ${proc.command}, output: ${proc.output}\nAlso errored killing process: ${err.message}`));
});
};

const closeHandler = (code: number | null, signal: NodeJS.Signals | null) => {
clearTimeout(timeout);
logger.debug(`CLI Command "${proc.command}" closed with code ${code}, signal ${signal}`);
resolve();
};

const errorHandler = (err: Error) => {
clearTimeout(timeout);
proc.process.off('close', closeHandler);
logger.error(`CLI Command "${proc.command}" errored with ${err}`);
killIt(`Command raised an error: ${err.message}`);
};

// Timeout the process if necessary
timeout = setTimeout(() => {
// Remove process event listeners
proc.process.off('close', closeHandler);
proc.process.off('error', errorHandler);
killIt(`shell.checkIfFinished timed out after ${timeouts.waitingGlobal} ms.`);
}, timeouts.waitingGlobal);

proc.process.on('close', closeHandler);
proc.process.on('error', errorHandler);
});
},

/**
* Sleep function used to wait for cli to finish executing
*/
sleep: function sleep(timeout = 1000): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
},

/**
* Remove all the ANSI color and style encoding
* @param text string
*/
removeANSIcolors: function removeANSIcolors(text: string): string {
const cleanText = text.replace(
// eslint-disable-next-line no-control-regex
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
'',
);
return cleanText;
},

/**
* Wait for output
* @param expString expected string
* @param shell
*/
waitForOutput: async function waitForOutput(
expString: string,
proc: ShellProcess,
): Promise<void> {
const delay = 1000;
let waitedFor = 0;
let timedOut = false;
while (!proc.output.includes(expString)) {
// eslint-disable-next-line no-await-in-loop
await this.sleep(delay);
waitedFor += delay;
if (waitedFor > timeouts.waitingAction) {
timedOut = true;
break;
}
}
return new Promise((resolve, reject) => {
if (timedOut) {
// Kill the process
const reason = `shell.waitForOutput timed out after ${waitedFor} ms. \nExpected output to include: ${expString}\nActual: ${proc.output}`;
shell.kill(proc).then(() => {
reject(new Error(`${reason}\nCommand: ${proc.command}, output: ${proc.output}`));
}, (err) => {
reject(new Error(`${reason}\nCommand: ${proc.command}, output: ${proc.output}\nAlso errored killing process: ${err.message}`));
});
} else {
resolve();
}
});
},
assembleShellEnv: function assembleShellEnv(): Record<string, string | undefined> {
const spawnedEnv = { ...process.env };
// Always enable test trace output
spawnedEnv.SLACK_TEST_TRACE = 'true';
// Skip prompts for AAA request and directly send a request
spawnedEnv.SLACK_AUTO_REQUEST_AAA = 'true';
// Never post to metrics store
spawnedEnv.SLACK_DISABLE_TELEMETRY = 'true';
return spawnedEnv;
},
kill: async function kill(proc: ShellProcess): Promise<boolean> {
return new Promise((resolve, reject) => {
treekill(proc.process.pid!, (err) => {
if (err) {
reject(new Error(`Failed to kill command "${proc.command}": errored with ${err.message}\nOutput: ${proc.output}`));
} else {
resolve(true);
}
});
});
},
};
20 changes: 20 additions & 0 deletions packages/cli-test/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { assert } from 'chai';
import sinon from 'sinon';
import { SlackCLI, SlackTracerId } from '.';

describe('main module', () => {
const sandbox = sinon.createSandbox();

afterEach(() => {
sandbox.restore();
});

describe('exports', () => {
it('should export a SlackCLI object', () => {
assert.isObject(SlackCLI);
});
it('should export a SlackTracerId object', () => {
assert.isObject(SlackTracerId);
});
});
});
9 changes: 9 additions & 0 deletions packages/cli-test/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export { SlackCLI } from './cli';
export { SlackCLIProcess } from './cli/cli-process';
export { SlackTracerId, SlackProduct } from './utils/constants';
export { shell } from './cli/shell';

// Check for cli binary path
if (!process.env.SLACK_CLI_PATH) {
throw new Error('Environment variable `SLACK_CLI_PATH` is not set!');
}
69 changes: 69 additions & 0 deletions packages/cli-test/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// TODO: would be nice to add JSDocs for each of these to describe when they appear in CLI output
/**
* This file should be kept up to date with the source of truth in the Slack CLI repo
*/
export const SlackTracerId = {
SLACK_TRACE_ADMIN_APPROVAL_REQUEST_PENDING: 'SLACK_TRACE_ADMIN_APPROVAL_REQUEST_PENDING',
SLACK_TRACE_ADMIN_APPROVAL_REQUEST_REASON_SUBMITTED: 'SLACK_TRACE_ADMIN_APPROVAL_REQUEST_REASON_SUBMITTED',
SLACK_TRACE_ADMIN_APPROVAL_REQUEST_SEND_ERROR: 'SLACK_TRACE_ADMIN_APPROVAL_REQUEST_SEND_ERROR',
SLACK_TRACE_ADMIN_APPROVAL_REQUEST_SHOULD_SEND: 'SLACK_TRACE_ADMIN_APPROVAL_REQUEST_SHOULD_SEND',
SLACK_TRACE_ADMIN_APPROVAL_REQUIRED: 'SLACK_TRACE_ADMIN_APPROVAL_REQUIRED',
SLACK_TRACE_AUTH_LIST_COUNT: 'SLACK_TRACE_AUTH_LIST_COUNT',
SLACK_TRACE_AUTH_LIST_INFO: 'SLACK_TRACE_AUTH_LIST_INFO',
SLACK_TRACE_AUTH_LIST_SUCCESS: 'SLACK_TRACE_AUTH_LIST_SUCCESS',
SLACK_TRACE_AUTH_LOGIN_START: 'SLACK_TRACE_AUTH_LOGIN_START',
SLACK_TRACE_AUTH_LOGIN_SUCCESS: 'SLACK_TRACE_AUTH_LOGIN_SUCCESS',
SLACK_TRACE_AUTH_LOGOUT_START: 'SLACK_TRACE_AUTH_LOGOUT_START',
SLACK_TRACE_AUTH_LOGOUT_SUCCESS: 'SLACK_TRACE_AUTH_LOGOUT_SUCCESS',
SLACK_TRACE_AUTH_REVOKE_START: 'SLACK_TRACE_AUTH_REVOKE_START',
SLACK_TRACE_AUTH_REVOKE_SUCCESS: 'SLACK_TRACE_AUTH_REVOKE_SUCCESS',
SLACK_TRACE_COLLABORATOR_ADD_COLLABORATOR: 'SLACK_TRACE_COLLABORATOR_ADD_COLLABORATOR',
SLACK_TRACE_COLLABORATOR_ADD_SUCCESS: 'SLACK_TRACE_COLLABORATOR_ADD_SUCCESS',
SLACK_TRACE_COLLABORATOR_LIST_COLLABORATOR: 'SLACK_TRACE_COLLABORATOR_LIST_COLLABORATOR',
SLACK_TRACE_COLLABORATOR_LIST_COUNT: 'SLACK_TRACE_COLLABORATOR_LIST_COUNT',
SLACK_TRACE_COLLABORATOR_LIST_SUCCESS: 'SLACK_TRACE_COLLABORATOR_LIST_SUCCESS',
SLACK_TRACE_COLLABORATOR_REMOVE_COLLABORATOR: 'SLACK_TRACE_COLLABORATOR_REMOVE_COLLABORATOR',
SLACK_TRACE_COLLABORATOR_REMOVE_SUCCESS: 'SLACK_TRACE_COLLABORATOR_REMOVE_SUCCESS',
SLACK_TRACE_CREATE_DEPENDENCIES_SUCCESS: 'SLACK_TRACE_CREATE_DEPENDENCIES_SUCCESS',
SLACK_TRACE_CREATE_ERROR: 'SLACK_TRACE_CREATE_ERROR',
SLACK_TRACE_CREATE_PROJECT_PATH: 'SLACK_TRACE_CREATE_PROJECT_PATH',
SLACK_TRACE_CREATE_START: 'SLACK_TRACE_CREATE_START',
SLACK_TRACE_CREATE_SUCCESS: 'SLACK_TRACE_CREATE_SUCCESS',
SLACK_TRACE_CREATE_TEMPLATE_OPTIONS: 'SLACK_TRACE_CREATE_TEMPLATE_OPTIONS',
SLACK_TRACE_DATASTORE_COUNT_DATASTORE: 'SLACK_TRACE_DATASTORE_COUNT_DATASTORE',
SLACK_TRACE_DATASTORE_COUNT_SUCCESS: 'SLACK_TRACE_DATASTORE_COUNT_SUCCESS',
SLACK_TRACE_DATASTORE_COUNT_TOTAL: 'SLACK_TRACE_DATASTORE_COUNT_TOTAL',
SLACK_TRACE_FEEDBACK_MESSAGE: 'SLACK_TRACE_FEEDBACK_MESSAGE',
SLACK_TRACE_MANIFEST_VALIDATE_SUCCESS: 'SLACK_TRACE_MANIFEST_VALIDATE_SUCCESS',
SLACK_TRACE_PLATFORM_DEPLOY_SUCCESS: 'SLACK_TRACE_PLATFORM_DEPLOY_SUCCESS',
SLACK_TRACE_PLATFORM_RUN_READY: 'SLACK_TRACE_PLATFORM_RUN_READY',
SLACK_TRACE_PLATFORM_RUN_START: 'SLACK_TRACE_PLATFORM_RUN_START',
SLACK_TRACE_PLATFORM_RUN_STOP: 'SLACK_TRACE_PLATFORM_RUN_STOP',
SLACK_TRACE_TRIGGERS_ACCESS_ERROR: 'SLACK_TRACE_TRIGGERS_ACCESS_ERROR',
SLACK_TRACE_TRIGGERS_ACCESS_SUCCESS: 'SLACK_TRACE_TRIGGERS_ACCESS_SUCCESS',
SLACK_TRACE_TRIGGERS_CREATE_SUCCESS: 'SLACK_TRACE_TRIGGERS_CREATE_SUCCESS',
SLACK_TRACE_TRIGGERS_CREATE_URL: 'SLACK_TRACE_TRIGGERS_CREATE_URL',
};

export const SlackProduct = {
FREE: 'FREE',
PRO: 'PRO',
BUSINESS_PLUS: 'PLUS',
ENTERPRISE: 'ENTERPRISE',
ENTERPRISE_SANDBOX: 'ENTERPRISE_SANDBOX',
ENTERPRISE_SELECT: 'ENTERPRISE_SELECT',
};

/**
* See https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 for more info on keypress encodings
*/
export const keyboardPress = {
ENTER: '\r\n',
DOWN_ARROW: '\u001b[B',
UP_ARROW: '\u001b[A',
};

export const timeouts = {
waitingGlobal: 30000,
waitingAction: 10000,
};
50 changes: 50 additions & 0 deletions packages/cli-test/src/utils/custom-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// TODO: refactor this error class:
// - reuse nodejs error `cause` to encode the 'wrapping' behaviour this class intends to implement https://nodejs.org/api/errors.html#errorcause
// - instead of `name`, consider reusing node error `code` https://nodejs.org/api/errors.html#errorcode
// - review how stack traces present themselves and possibly consider using `captureStackTrace` https://nodejs.org/api/errors.html#errorcapturestacktracetargetobject-constructoropt

/**
* Custom error class for cli methods
*/
export class CustomError extends Error {
public name: string;

public command: string | undefined;

public additionalInfo: string | undefined;

/**
* Inherit and create new instance of default Error class
* @param message
* @param name
* @param stack
* @param options
*/
public constructor(
message: string,
name: string,
stack: string | undefined,
options?: {
/**
* Command used
*/
command?: string;
/**
* Any additional info
*/
additionalInfo?: string;
},
) {
super(message);
this.name = name;
this.stack = stack;
this.command = options?.command;
this.additionalInfo = options?.additionalInfo;

// Set a more readable error message
this.message = `${this.name}: ${this.command}; ${this.additionalInfo}`;

// Set the prototype explicitly
Object.setPrototypeOf(this, CustomError.prototype);
}
}
24 changes: 24 additions & 0 deletions packages/cli-test/src/utils/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createLogger, format, transports, Logger } from 'winston';

// Configure CLI log level
// Winston logging levels, see: https://github.com/winstonjs/winston#logging
const loggerLevel = process.env.SLACK_CLI_LOG_LEVEL || 'info';

// Create custom logging format
const logPrintFormat = format.printf(
({ level, message, label, timestamp }) => `${timestamp} - [${label}] - ${level}: ${message}`,
);

// Create logger
const logger: Logger = createLogger({
level: loggerLevel,
format: format.combine(
format.label({ label: 'Slack CLI' }),
format.timestamp(),
format.colorize(),
logPrintFormat,
),
transports: [new transports.Console()],
});

export default logger;
29 changes: 29 additions & 0 deletions packages/cli-test/src/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ChildProcessWithoutNullStreams } from 'child_process';

export const SlackProduct = {
FREE: 'FREE',
PRO: 'PRO',
BUSINESS_PLUS: 'PLUS',
ENTERPRISE: 'ENTERPRISE',
ENTERPRISE_SANDBOX: 'ENTERPRISE_SANDBOX',
ENTERPRISE_SELECT: 'ENTERPRISE_SELECT',
};

export interface ShellProcess {
/**
* Child process object
*/
process: ChildProcessWithoutNullStreams;
/**
* Command output
*/
output: string;
/**
* Process state
*/
finished: boolean;
/**
* Command string
*/
command: string;
}
1 change: 1 addition & 0 deletions packages/cli-test/tsconfig.eslint.json
28 changes: 28 additions & 0 deletions packages/cli-test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "dist",
"sourceMap": true,
},
"extends": "@tsconfig/recommended/tsconfig.json",
"include": [
"src/**/*"
],
"jsdoc": {
"out": "support/jsdoc",
"access": "public"
},
"exclude": [
"src/**/*.js"
],
"ts-node": {
"esm": true
}
}
7 changes: 7 additions & 0 deletions packages/logger/.c8rc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"include": ["src/*.ts"],
"exclude": ["**/*.spec.js"],
"reporter": ["lcov", "text"],
"all": false,
"cache": true
}
1 change: 0 additions & 1 deletion packages/logger/.gitignore
Original file line number Diff line number Diff line change
@@ -6,5 +6,4 @@
/dist

# coverage
/.nyc_output
/coverage
14 changes: 0 additions & 14 deletions packages/logger/.nycrc.json

This file was deleted.

2 changes: 2 additions & 0 deletions packages/logger/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Slack Logger

[![codecov](https://codecov.io/gh/slackapi/node-slack-sdk/graph/badge.svg?token=OcQREPvC7r&flag=logger)](https://codecov.io/gh/slackapi/node-slack-sdk)

The `@slack/logger` package is intended to be used as a simple logging interface that supports verbosity levels.

## Requirements
8 changes: 4 additions & 4 deletions packages/logger/package.json
Original file line number Diff line number Diff line change
@@ -28,12 +28,12 @@
"scripts": {
"prepare": "npm run build",
"build": "npm run build:clean && tsc",
"build:clean": "shx rm -rf ./dist ./coverage ./.nyc_output",
"build:clean": "shx rm -rf ./dist ./coverage",
"lint": "eslint --ext .ts src",
"mocha": "mocha --config .mocharc.json src/*.spec.js",
"test:unit": "npm run build && npm run mocha",
"test": "npm run lint && npm run test:unit",
"coverage": "npm run build && nyc --reporter=text-summary npm run mocha",
"test": "npm run lint && npm run coverage",
"coverage": "npm run build && c8 npm run mocha",
"ref-docs:model": "api-extractor run"
},
"dependencies": {
@@ -45,6 +45,7 @@
"@types/mocha": "^10.0.1",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.0",
"c8": "^9.1.0",
"chai": "^4.3.8",
"eslint": "^8.47.0",
"eslint-config-airbnb-base": "^15.0.0",
@@ -54,7 +55,6 @@
"eslint-plugin-jsdoc": "^46.5.0",
"eslint-plugin-node": "^11.1.0",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"shx": "^0.3.2",
"sinon": "^15.2.0",
"source-map-support": "^0.5.21",
7 changes: 7 additions & 0 deletions packages/oauth/.c8rc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"include": ["src/*.ts"],
"exclude": ["**/*.spec.js", "**/*.spec.ts"],
"reporter": ["lcov", "text"],
"all": false,
"cache": true
}
4 changes: 1 addition & 3 deletions packages/oauth/.gitignore
Original file line number Diff line number Diff line change
@@ -6,7 +6,5 @@ package-lock.json
/dist

# coverage
/.nyc_output
/coverage
*.lcov
tmp/
tmp/
14 changes: 0 additions & 14 deletions packages/oauth/.nycrc.json

This file was deleted.

2 changes: 2 additions & 0 deletions packages/oauth/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Slack OAuth

[![codecov](https://codecov.io/gh/slackapi/node-slack-sdk/graph/badge.svg?token=OcQREPvC7r&flag=oauth)](https://codecov.io/gh/slackapi/node-slack-sdk)

The `@slack/oauth` package makes it simple to setup the OAuth flow for Slack apps. It supports [V2 OAuth](https://api.slack.com/authentication/oauth-v2) for Slack Apps as well as [V1 OAuth](https://api.slack.com/docs/oauth) for [Classic Slack apps](https://api.slack.com/authentication/quickstart). Slack apps that are installed in multiple workspaces, like in the App Directory or in an Enterprise Grid, will need to implement OAuth and store information about each of those installations (such as access tokens).

The package handles URL generation, state verification, and authorization code exchange for access tokens. It also provides an interface for easily plugging in your own database for saving and retrieving installation data.
9 changes: 5 additions & 4 deletions packages/oauth/package.json
Original file line number Diff line number Diff line change
@@ -30,10 +30,11 @@
"scripts": {
"prepare": "npm run build",
"build": "npm run build:clean && tsc",
"build:clean": "shx rm -rf ./dist ./coverage ./.nyc_output",
"build:clean": "shx rm -rf ./dist ./coverage",
"lint": "eslint --fix --ext .ts src",
"test": "npm run build && npm run lint && npm run test:mocha",
"test:mocha": "nyc --reporter=text-summary mocha --config .mocharc.json src/*.spec.js src/**/*.spec.js src/*.spec.ts src/**/*.spec.ts",
"test": "npm run lint && npm run coverage",
"coverage": "npm run build && c8 npm run test:mocha",
"test:mocha": "mocha --config .mocharc.json src/*.spec.js src/**/*.spec.js src/*.spec.ts src/**/*.spec.ts",
"ref-docs:model": "api-extractor run",
"watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build"
},
@@ -52,6 +53,7 @@
"@types/sinon": "^17",
"@typescript-eslint/eslint-plugin": "^6",
"@typescript-eslint/parser": "^6",
"c8": "^9.1.0",
"chai": "^4",
"eslint": "^8",
"eslint-config-airbnb-base": "^15",
@@ -61,7 +63,6 @@
"eslint-plugin-jsdoc": "^48",
"eslint-plugin-node": "^11",
"mocha": "^10",
"nyc": "^15",
"rewiremock": "^3",
"shx": "^0.3.2",
"sinon": "^17",
7 changes: 7 additions & 0 deletions packages/socket-mode/.c8rc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"include": ["src/*.ts"],
"exclude": ["**/*.spec.ts"],
"reporter": ["lcov", "text"],
"all": false,
"cache": true
}
3 changes: 2 additions & 1 deletion packages/socket-mode/.mocharc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"extension": ["ts"],
"require": ["ts-node/register", "source-map-support/register"],
"timeout": 3000
}
}
14 changes: 0 additions & 14 deletions packages/socket-mode/.nycrc.json

This file was deleted.

45 changes: 15 additions & 30 deletions packages/socket-mode/README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
# Slack Socket Mode

[![codecov](https://codecov.io/gh/slackapi/node-slack-sdk/graph/badge.svg?token=OcQREPvC7r&flag=socket-mode)](https://codecov.io/gh/slackapi/node-slack-sdk)

This package is designed to support [**Socket Mode**][socket-mode], which allows your app to receive events from Slack over a WebSocket connection.

## Requirements

This package supports Node v18 and higher. It's highly recommended to use [the latest LTS version of
node](https://github.com/nodejs/Release#release-schedule), and the documentation is written using syntax and features from that version.

## Installation

```shell
$ npm install @slack/socket-mode
```

<!-- START: Remove before copying into the docs directory -->

## Usage

These examples show the most common features of `Socket Mode`. You'll find even more extensive [documentation on the
package's website](https://slack.dev/node-slack-sdk/socket-mode) and our [api site][socket-mode].

<!-- END: Remove before copying into the docs directory -->

---

### Initialize the client

This package is designed to support [**Socket Mode**][socket-mode], which allows your app to receive events from Slack over a WebSocket connection.

The package exports a `SocketModeClient` class. Your app will create an instance of the class for each workspace it communicates with. Creating an instance requires an [**app-level token**][app-token] from Slack. Apps connect to the **Socket Mode** API using an [**app-level token**][app-token], which starts with `xapp`.

Note: **Socket Mode** requires the `connections:write` scope. Navigate to your [app configuration](https://api.slack.com/apps) and go to the **OAuth and Permissions** section to add the scope.
Note: **Socket Mode** requires the `connections:write` scope. Navigate to your [app configuration](https://api.slack.com/apps) and go to the bottom of the **Basic Information** section to create an App token with the `connections:write` scope.


```javascript
@@ -38,7 +36,7 @@ const client = new SocketModeClient({appToken});

### Connect to Slack

After your client establishes a connection, your app can send data to and receive data from Slack. Connecting is as easy as calling the `.start()` method.
Connecting is as easy as calling the `.start()` method.

```javascript
const { SocketModeClient } = require('@slack/socket-mode');
@@ -51,16 +49,13 @@ const socketModeClient = new SocketModeClient({appToken});
await socketModeClient.start();
})();
```
---

### Listen for an event

Bolt apps register [listener functions](https://slack.dev/bolt-js/reference#listener-functions), which are triggered when a specific event type is received by the client.

If you've used Node's [`EventEmitter`](https://nodejs.org/api/events.html#events_class_eventemitter) pattern
before, then you're already familiar with how this works, since the client is an `EventEmitter`.

The `event` argument passed to the listener is an object. Its content corresponds to the [type of
The `event` argument passed to the listener is an object. Its contents correspond to the [type of
event](https://api.slack.com/events) it's registered for.

```javascript
@@ -79,8 +74,6 @@ socketModeClient.on('message', (event) => {
})();
```

---

### Send a message

To respond to events and send messages back into Slack, we recommend using the `@slack/web-api` package with a [bot token](https://api.slack.com/authentication/token-types#bot).
@@ -127,18 +120,16 @@ socketModeClient.on('member_joined_channel', async ({event, body, ack}) => {
await socketModeClient.start();
})();
```
---

### Lifecycle events

The client's connection to Slack has a lifecycle. This means the client can be seen as a state machine which transitions through a few states as it connects, disconnects, reconnects, and synchronizes with Slack. The client emits an event for each state it transitions to throughout its lifecycle. If your app simply needs to know whether the client is connected or not, the `.connected` boolean property can be checked.
The client's connection to Slack has a lifecycle. This means the client can be seen as a state machine which transitions through a few states as it connects, disconnects and possibly reconnects with Slack. The client emits an event for each state it transitions to throughout its lifecycle. If your app simply needs to know whether the client is connected or not, the `.connected` boolean property can be checked.

In the table below, the client's states are listed, which are also the names of the events you can use to observe the transition to that state. The table also includes descriptions for the states and arguments that a listener would receive.

| Event Name | Arguments | Description |
|-----------------|-----------------|-------------|
| `connecting` | | The client is in the process of connecting to the platform. |
| `authenticated` | `(connectData)` - the response from `apps.connections.open` | The client has authenticated with the platform. This is a sub-state of `connecting`. |
| `connected` | | The client is connected to the platform and incoming events will start being emitted. |
| `disconnecting` | | The client is no longer connected to the platform and cleaning up its resources. It will soon transition to `disconnected`. |
| `reconnecting` | | The client is no longer connected to the platform and cleaning up its resources. It will soon transition to `connecting`. |
@@ -148,15 +139,14 @@ The client also emits events that are part of its lifecycle, but aren't states.

| Event Name | Arguments | Description |
|-----------------|-----------|-------------|
| `error` | `(error)` | An error has occurred. See [error handling](#handle-errors) for details. |
| `error` | `(error)` | An error has occurred. |
| `slack_event` | `(eventType, event)` | An incoming Slack event has been received. |
| `unable_to_socket_mode_start` | `(error)` | A problem occurred while connecting, a reconnect may or may not occur. |

---

### Logging

The `SocketModeClient` will log interesting information to the console by default. You can use the `logLevel` to decide how much or what kind of information should be output. There are a few possible log levels, which you can find in the `LogLevel` export. By default, the value is set to `LogLevel.INFO`. While you're in development, it's sometimes helpful to set this to the most verbose: `LogLevel.DEBUG`.
The `SocketModeClient` will log information to the console by default. You can use the `logLevel` to decide how much or what kind of information should be output. There are a few possible log levels, which you can find in the `LogLevel` export. By default, the value is set to `LogLevel.INFO`. While you're in development, it's sometimes helpful to set this to the most verbose: `LogLevel.DEBUG`.

```javascript
// Import LogLevel from the package
@@ -221,11 +211,6 @@ const socketModeClient = new SocketModeClient({

---

## Requirements

This package supports Node v18 and higher. It's highly recommended to use [the latest LTS version of
node](https://github.com/nodejs/Release#release-schedule), and the documentation is written using syntax and features from that version.

## Getting Help

If you get stuck, we're here to help. The following are the best ways to get assistance working through your issue:
18 changes: 10 additions & 8 deletions packages/socket-mode/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@slack/socket-mode",
"version": "1.3.3",
"version": "2.0.0",
"description": "Official library for using the Slack Platform's Socket Mode API",
"author": "Slack Technologies, LLC",
"license": "MIT",
@@ -18,8 +18,8 @@
"state",
"connection"
],
"main": "dist/index.js",
"types": "./dist/index.d.ts",
"main": "dist/src/index.js",
"types": "./dist/src/index.d.ts",
"files": [
"dist/**/*"
],
@@ -38,12 +38,13 @@
"scripts": {
"prepare": "npm run build",
"build": "npm run build:clean && tsc",
"build:clean": "shx rm -rf ./dist ./coverage ./.nyc_output",
"build:clean": "shx rm -rf ./dist ./coverage",
"lint": "eslint --ext .ts src",
"mocha": "mocha --config .mocharc.json src/*.spec.js",
"test:unit": "mocha --config .mocharc.json src/**/*.spec.ts",
"test:coverage": "c8 npm run test:unit",
"test:integration": "mocha --config .mocharc.json test/integration.spec.js",
"test": "npm run lint && npm run build && nyc --reporter=text-summary npm run mocha && npm run test:integration",
"watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build"
"test": "npm run lint && npm run build && npm run test:coverage && npm run test:integration",
"watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm test"
},
"dependencies": {
"@slack/logger": "^4",
@@ -60,6 +61,7 @@
"@types/sinon": "^17",
"@typescript-eslint/eslint-plugin": "^6",
"@typescript-eslint/parser": "^6",
"c8": "^9.1.0",
"chai": "^4",
"eslint": "^8",
"eslint-config-airbnb-base": "^15",
@@ -69,7 +71,7 @@
"eslint-plugin-jsdoc": "^48",
"eslint-plugin-node": "^11",
"mocha": "^10",
"nyc": "^15",
"nodemon": "^3.1.0",
"shx": "^0.3.2",
"sinon": "^17",
"source-map-support": "^0.5.21",
29 changes: 29 additions & 0 deletions packages/socket-mode/src/SlackWebSocket.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { assert } from 'chai';
import sinon from 'sinon';
import EventEmitter from 'eventemitter3';
import { ConsoleLogger } from '@slack/logger';
import logModule from './logger';
import { SlackWebSocket } from './SlackWebSocket';

describe('SlackWebSocket', () => {
const sandbox = sinon.createSandbox();

afterEach(() => {
sandbox.restore();
});

describe('constructor', () => {
let logFactory: sinon.SinonStub;
beforeEach(() => {
logFactory = sandbox.stub(logModule, 'getLogger');
});
it('should set a default logger if none provided', () => {
new SlackWebSocket({ url: 'https://whatever.com', client: new EventEmitter(), clientPingTimeoutMS: 1, serverPingTimeoutMS: 1 });
assert.isTrue(logFactory.called);
});
it('should not set a default logger if one provided', () => {
new SlackWebSocket({ url: 'https://whatever.com', client: new EventEmitter(), clientPingTimeoutMS: 1, serverPingTimeoutMS: 1, logger: new ConsoleLogger() });
assert.isFalse(logFactory.called);
});
});
});
Loading