├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── cli-bug-report.md │ └── config.yaml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── clear-staging-cloud.yml │ ├── pr.yml │ ├── prepare-release.yml │ ├── release.yml │ └── tests-nightly.yml ├── .gitignore ├── .graphqlrc.yml ├── .husky └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── CODEOWNERS ├── LICENSE ├── README.md ├── docs ├── README.md └── cli │ ├── commands │ ├── app.mdx │ ├── backup.mdx │ ├── environment.mdx │ ├── example.mdx │ ├── github.mdx │ ├── info.mdx │ ├── job.mdx │ ├── login.mdx │ ├── logout.mdx │ ├── open.mdx │ ├── organization.mdx │ ├── project.mdx │ ├── register.mdx │ ├── status.mdx │ ├── storefront.mdx │ ├── task.mdx │ ├── telemetry.mdx │ ├── trigger.mdx │ ├── vercel.mdx │ └── webhook.mdx │ └── overview.mdx ├── justfile ├── package.json ├── pnpm-lock.yaml ├── src ├── cli.ts ├── cli │ ├── app │ │ ├── create.ts │ │ ├── generate.ts │ │ ├── index.ts │ │ ├── install.ts │ │ ├── list.ts │ │ ├── permission.ts │ │ ├── remove.ts │ │ ├── template.ts │ │ ├── token.ts │ │ ├── tunnel.ts │ │ └── uninstall.ts │ ├── backup │ │ ├── create.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── remove.ts │ │ ├── restore.ts │ │ └── show.ts │ ├── configure.ts │ ├── dev │ │ ├── docs.ts │ │ ├── index.ts │ │ ├── info.ts │ │ └── prepare.ts │ ├── env │ │ ├── auth.ts │ │ ├── clear.ts │ │ ├── cors.ts │ │ ├── create.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── maintenance.ts │ │ ├── origins.ts │ │ ├── populate.ts │ │ ├── promote.ts │ │ ├── remove.ts │ │ ├── show.ts │ │ ├── switch.ts │ │ ├── update.ts │ │ └── upgrade.ts │ ├── example.ts │ ├── github │ │ ├── index.ts │ │ └── login.ts │ ├── info.ts │ ├── init.ts │ ├── login.ts │ ├── logout.ts │ ├── open.ts │ ├── organization │ │ ├── index.ts │ │ ├── list.ts │ │ ├── permissions.ts │ │ ├── remove.ts │ │ ├── show.ts │ │ └── switch.ts │ ├── project │ │ ├── create.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── remove.ts │ │ └── show.ts │ ├── register.ts │ ├── status.ts │ ├── storefront │ │ ├── create.ts │ │ ├── deploy.ts │ │ └── index.ts │ ├── task │ │ ├── index.ts │ │ └── list.ts │ ├── telemetry │ │ ├── disable.ts │ │ ├── enable.ts │ │ ├── index.ts │ │ └── status.ts │ ├── trigger.ts │ ├── vercel │ │ ├── index.ts │ │ └── login.ts │ └── webhook │ │ ├── create.ts │ │ ├── dryRun.ts │ │ ├── edit.ts │ │ ├── index.ts │ │ ├── list.ts │ │ └── update.ts ├── config.ts ├── generated │ └── graphql.ts ├── graphql │ ├── mutation │ │ ├── DoAppCreate.graphql │ │ ├── DoAppDelete.graphql │ │ ├── DoAppInstall.graphql │ │ ├── DoAppTokenCreate.graphql │ │ ├── DoAppUpdate.graphql │ │ ├── DoProductUpdate.graphql │ │ ├── DoWebhookCreate.graphql │ │ ├── DoWebhookDryRun.graphql │ │ └── DoWebhookUpdate.graphql │ └── query │ │ ├── GetAppByID.graphql │ │ ├── GetApps.graphql │ │ ├── GetAppsInstallations.graphql │ │ ├── GetPermissionEnum.graphql │ │ ├── GetWebhookAsyncEventEnum.graphql │ │ ├── GetWebhookEventEnum.graphql │ │ ├── GetWebhookList.graphql │ │ ├── GetWebhookSyncEventEnum.graphql │ │ └── Introspection.graphql ├── lib │ ├── common.ts │ ├── config.ts │ ├── deploy.ts │ ├── detectPort.ts │ ├── download.ts │ ├── environment.test.ts │ ├── environment.ts │ ├── git.ts │ ├── images.ts │ ├── index.ts │ ├── instance.ts │ ├── queryEnvironment.ts │ ├── response.ts │ ├── util.ts │ └── vercel.ts ├── middleware │ └── index.ts └── types.ts ├── template ├── event-subscription.graphql └── webhook-handler.ts ├── test ├── __snapshots__ │ └── vercel.test.ts.snap ├── clear_cloud.test.ts ├── functional │ ├── CODEOWNERS │ ├── app │ │ └── create.test.ts │ ├── backup │ │ └── restore.test.ts │ ├── environment │ │ ├── auth.test.ts │ │ ├── cors.test.ts │ │ ├── create.test.ts │ │ ├── list.test.ts │ │ ├── maintenance.test.ts │ │ ├── origins.test.ts │ │ ├── show.test.ts │ │ └── update.test.ts │ ├── project │ │ ├── create.test.ts │ │ └── list.test.ts │ ├── storefront │ │ └── create.test.ts │ └── tunnel.test.ts ├── helper.ts ├── organization │ └── show.test.ts └── vercel.test.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2018, 5 | "sourceType": "module" 6 | }, 7 | "extends": ["airbnb", "plugin:@typescript-eslint/recommended", "prettier"], 8 | "ignorePatterns": ["pnpm-lock.yaml", "dist/"], 9 | "plugins": ["@typescript-eslint"], 10 | "rules": { 11 | "quotes": ["error", "single"], 12 | "import/extensions": "off", // file extension not required when importing 13 | "no-restricted-syntax": "off", 14 | "no-underscore-dangle": "off", 15 | "no-await-in-loop": "off", 16 | "import/first": "warn", 17 | "import/newline-after-import": "warn", 18 | "import/no-duplicates": "warn", 19 | "import/no-unresolved": "off", 20 | "import/order": "warn", 21 | "no-unused-vars": "off", 22 | "@typescript-eslint/no-unused-vars": [ 23 | "warn", // or "error" 24 | { 25 | "argsIgnorePattern": "^_", 26 | "varsIgnorePattern": "^_", 27 | "caughtErrorsIgnorePattern": "^_" 28 | } 29 | ], 30 | "@typescript-eslint/ban-types": "off", 31 | "no-console": "off", 32 | "no-continue": "off", 33 | "operator-linebreak": "off", 34 | "max-len": "off", 35 | "array-callback-return": "off", 36 | "implicit-arrow-linebreak": "off", 37 | "@typescript-eslint/no-non-null-asserted-optional-chain": "off", 38 | "@typescript-eslint/no-non-null-assertion": "off", 39 | "no-restricted-imports": "off", 40 | "no-restricted-exports": "off", 41 | "@typescript-eslint/ban-ts-comment": "off", 42 | "no-use-before-define": "off", 43 | "func-names": "off", // FIXME 44 | "@typescript-eslint/no-explicit-any": "off" // FIXME 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @saleor/extensibility-team-js 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/cli-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: CLI bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug and commend which you used** 11 | 14 | 15 | **To Reproduce** 16 | 23 | 24 | **Current behavior** 25 | 28 | 29 | **Expected behavior** 30 | 33 | 34 | **Screenshots** 35 | 38 | 39 | **Additional context** 40 | 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yaml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Github Discussions 4 | url: https://github.com/saleor/saleor-cli/discussions 5 | about: Ask questions and suggest features here. 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## I want to merge this PR because 2 | 3 | - 4 | 5 | ## Related (issues, PRs, topics) 6 | 7 | - 8 | 9 | ## Steps to test feature 10 | 11 | - 12 | 13 | ## I have: 14 | 15 | - [ ] Tested it locally and it doesn't break existing features 16 | - [ ] Added documentation if public changes are introduced 17 | - [ ] Added tests for my code 18 | -------------------------------------------------------------------------------- /.github/workflows/clear-staging-cloud.yml: -------------------------------------------------------------------------------- 1 | name: Clear staging cloud projects 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 7 * * 0-6' 7 | 8 | repository_dispatch: 9 | types: [automation-tests-event] 10 | 11 | jobs: 12 | run-functional-tests: 13 | if: ${{ github.event_name != 'repository_dispatch' }} 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 10 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 19 | - uses: actions/setup-node@v4.2.0 20 | with: 21 | node-version-file: ".nvmrc" 22 | cache: 'pnpm' 23 | - name: Install dependencies 24 | run: pnpm install 25 | 26 | - name: Prepare saleor CLI 27 | run: pnpm build 28 | 29 | - name: Write config file 30 | id: write-config-file 31 | env: 32 | CLI_ACCESS_TOKEN: ${{ secrets.CLI_ACCESS_TOKEN }} 33 | CLI_GITHUB_TOKEN: ${{ secrets.CLI_GITHUB_TOKEN }} 34 | CLI_VERCEL_TOKEN: ${{ secrets.CLI_VERCEL_TOKEN }} 35 | TEST_COMMAND: 'node dist/saleor.js' 36 | run: echo '{"token":"${{ secrets.CLI_ACCESS_TOKEN }}","telemetry":"false", "github_token":"${{ secrets.CLI_GITHUB_TOKEN }}", "vercel_token":"${{ secrets.CLI_VERCEL_TOKEN }}" }' > ~/.config/saleor.json 37 | 38 | - name: clear cloud projects 39 | env: 40 | SALEOR_CLI_ENV: staging 41 | RUN_FUNCTIONAL_TESTS: true 42 | run: | 43 | pnpm vitest run test/clear_cloud.test.ts 44 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Test PR 2 | on: [pull_request] 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 9 | - uses: actions/setup-node@v4.2.0 10 | with: 11 | node-version-file: ".nvmrc" 12 | cache: pnpm 13 | - name: Install dependencies 14 | run: pnpm install 15 | - name: Check linters 16 | run: pnpm lint 17 | 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 23 | - uses: actions/setup-node@v4.2.0 24 | with: 25 | node-version-file: ".nvmrc" 26 | cache: 'pnpm' 27 | - name: Install dependencies 28 | run: pnpm install 29 | - name: Test 30 | run: pnpm test 31 | 32 | build: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 37 | - uses: actions/setup-node@v4.2.0 38 | with: 39 | node-version-file: ".nvmrc" 40 | cache: pnpm 41 | - uses: actions/cache@v3 42 | with: 43 | path: | 44 | ${{ github.workspace }}/.next/cache 45 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} 46 | # If source files changed but packages didn't, rebuild from a prior cache. 47 | restore-keys: | 48 | ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.json') }}- 49 | - name: Install dependencies 50 | run: pnpm install 51 | - name: Build project 52 | run: pnpm build 53 | -------------------------------------------------------------------------------- /.github/workflows/prepare-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare a Release on GitHub 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | type: choice 7 | description: Which version to prepare a release? 8 | options: 9 | - nextrc 10 | - preminor 11 | - minor 12 | - patch 13 | jobs: 14 | publish: 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - name: Configure the user for Git 21 | run: | 22 | git config user.name "${GITHUB_ACTOR}" 23 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 24 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 25 | - uses: actions/setup-node@v4.2.0 26 | with: 27 | node-version-file: ".nvmrc" 28 | cache: pnpm 29 | - name: Install the dependencies 30 | run: pnpm install 31 | - name: Create a release 32 | if: ${{ ! startsWith(inputs.version, 'pre') && ! startsWith(inputs.version, 'next') }} 33 | run: pnpm release-it ${{ inputs.version }} --no-npm.publish --npm.skipChecks --git.requireBranch="release/*" --github.release --github.draft --github.autoGenerate --github.releaseName='v${version}' --ci 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | - name: Create a pre-release for a new version 38 | if: startsWith(inputs.version, 'pre') 39 | run: pnpm release-it preminor --preRelease=rc --no-npm.publish --npm.skipChecks --git.requireBranch="release/*" --github.release --github.draft --github.autoGenerate --github.releaseName='v${version}' --ci 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | - name: Create a pre-release for a current version 44 | if: startsWith(inputs.version, 'next') 45 | run: pnpm release-it --preRelease=rc --no-npm.publish --npm.skipChecks --git.requireBranch="release-*" --github.release --github.draft --github.autoGenerate --github.releaseName='v${version}' --ci 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | type: choice 7 | description: Which version to release? 8 | options: 9 | - major 10 | - minor 11 | - patch 12 | - preminor 13 | jobs: 14 | publish: 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Configure a Git to commit & tag back to the remote 19 | run: | 20 | git config user.name "${GITHUB_ACTOR}" 21 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 22 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 23 | - uses: actions/setup-node@v4.2.0 24 | with: 25 | node-version-file: ".nvmrc" 26 | cache: pnpm 27 | - name: Install the dependencies 28 | run: pnpm install 29 | - name: Setup the NPM Registry 30 | run: pnpm config set //registry.npmjs.org/:_authToken $NPM_TOKEN 31 | env: 32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | - name: Release for `@saleor/cli` 34 | run: pnpm release ${{ inputs.version }} --ci ${{ startsWith(inputs.version, 'pre') && '--preRelease=rc --npm.tag=next' || '' }} --dry-run ${{ secrets.DRY_RUN || true }} 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | - name: Change package name to `saleor` 39 | run: node -e "let pkg=require('./package.json'); pkg.name='saleor'; require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));" 40 | - name: Release for `saleor` 41 | run: pnpm release --no-increment --ci ${{ startsWith(inputs.version, 'pre') && '--preRelease=rc --npm.tag=next' || '' }} --no-git --dry-run ${{ secrets.DRY_RUN || true }} 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 45 | - name: Change package name to `saleor-cli` 46 | run: node -e "let pkg=require('./package.json'); pkg.name='saleor-cli'; require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));" 47 | - name: Release for `saleor-cli` 48 | run: pnpm release --no-increment --ci ${{ startsWith(inputs.version, 'pre') && '--preRelease=rc --npm.tag=next' || '' }} --no-git --dry-run ${{ secrets.DRY_RUN || true }} 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 52 | - name: Revert the name in `package.json` 53 | run: node -e "let pkg=require('./package.json'); pkg.name='@saleor/cli'; require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));" 54 | - name: Create a draft release on GitHub 55 | run: pnpm release-it --no-increment --no-git --no-npm --github.release --github.draft --github.autoGenerate --github.releaseName='v${version}' --ci 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 59 | 60 | -------------------------------------------------------------------------------- /.github/workflows/tests-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Execute nightly tests 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 3 * * 1-5' 7 | 8 | repository_dispatch: 9 | types: [automation-tests-event] 10 | 11 | jobs: 12 | run-functional-tests: 13 | if: ${{ github.event_name != 'repository_dispatch' }} 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 60 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 19 | - uses: actions/setup-node@v4.2.0 20 | with: 21 | node-version-file: ".nvmrc" 22 | cache: 'pnpm' 23 | - name: Install dependencies 24 | run: pnpm install 25 | 26 | - name: Prepare saleor CLI 27 | run: pnpm build 28 | 29 | - name: Write config file 30 | id: write-config-file 31 | env: 32 | CLI_ACCESS_TOKEN: ${{ secrets.CLI_ACCESS_TOKEN }} 33 | CLI_GITHUB_TOKEN: ${{ secrets.CLI_GITHUB_TOKEN }} 34 | CLI_VERCEL_TOKEN: ${{ secrets.CLI_VERCEL_TOKEN }} 35 | TEST_COMMAND: 'node dist/saleor.js' 36 | run: echo '{"token":"${{ secrets.CLI_ACCESS_TOKEN }}","telemetry":"false","saleor_env":"staging","cloud_api_url":"https://staging-cloud.saleor.io/platform/api", "github_token":"${{ secrets.CLI_GITHUB_TOKEN }}", "vercel_token":"${{ secrets.CLI_VERCEL_TOKEN }}" }' > ~/.config/saleor.json 37 | 38 | - name: pre run 39 | env: 40 | SALEOR_CLI_ENV: staging 41 | RUN_FUNCTIONAL_TESTS: true 42 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 43 | run: | 44 | node dist/saleor.js info 45 | node dist/saleor.js status 46 | 47 | - name: clear cloud projects 48 | env: 49 | SALEOR_CLI_ENV: staging 50 | RUN_FUNCTIONAL_TESTS: true 51 | run: | 52 | pnpm vitest run test/clear_cloud.test.ts 53 | 54 | - name: run tests 55 | env: 56 | RUN_FUNCTIONAL_TESTS: true 57 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 58 | run: | 59 | git config --global user.name 'GA-saleor-cli' 60 | git config --global user.email 'GA-saleor-cli@users.noreply.github.com' 61 | pnpm vitest test/functional -t '^((?!(deploy|tunnel)).)*$' --no-threads 62 | 63 | - name: Notify Slack 64 | if: ${{ failure() }} 65 | uses: rtCamp/action-slack-notify@v2 66 | env: 67 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_SNAP_RESTORE }} 68 | SLACK_USERNAME: FunctionalTestsBot 69 | SLACK_COLOR: ${{ job.status }} 70 | SLACK_TITLE: "Functional tests failed" 71 | SLACK_MESSAGE: "https://github.com/saleor/saleor-cli/actions/workflows/tests-nightly.yml" 72 | MSG_MINIMAL: true 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | package-lock.json 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional stylelint cache 59 | .stylelintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variable files 77 | .env 78 | .env.development.local 79 | .env.test.local 80 | .env.production.local 81 | .env.local 82 | 83 | # parcel-bundler cache (https://parceljs.org/) 84 | .cache 85 | .parcel-cache 86 | 87 | # Next.js build output 88 | .next 89 | out 90 | 91 | # Nuxt.js build / generate output 92 | .nuxt 93 | dist 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | .cache 107 | 108 | # Docusaurus cache and generated files 109 | .docusaurus 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # TernJS port file 121 | .tern-port 122 | 123 | # Stores VSCode versions used for testing VSCode extensions 124 | .vscode-test 125 | 126 | # yarn v2 127 | .yarn/cache 128 | .yarn/unplugged 129 | .yarn/build-state.yml 130 | .yarn/install-state.gz 131 | .pnp.* 132 | 133 | # other 134 | build/ 135 | .vscode/ 136 | binaries/ 137 | .saleor 138 | token.txt 139 | vendor/ 140 | test.js 141 | dummy/ 142 | ..bfg-report 143 | .idea/ 144 | .DS_Store 145 | package/ 146 | 147 | .type-coverage/ 148 | coverage-ts/ 149 | 150 | # Sentry Auth Token 151 | .sentryclirc 152 | -------------------------------------------------------------------------------- /.graphqlrc.yml: -------------------------------------------------------------------------------- 1 | schema: https://vercel.saleor.cloud/graphql/ 2 | documents: src/graphql/**/*.graphql 3 | extensions: 4 | codegen: 5 | overwrite: true 6 | generates: 7 | src/generated/graphql.ts: 8 | plugins: 9 | - typescript-document-nodes 10 | config: 11 | gqlImport: 'graphql-tag#gql' -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | node './dist/saleor.js' dev docs 5 | git add docs/ 6 | npx lint-staged 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .history 2 | build 3 | pnpm-lock.yaml 4 | template/event-subscription.graphql 5 | dist 6 | # To be sure that white spaces are not broken 7 | *.md 8 | *.yml -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @saleor/devtools 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022 Saleor Commerce 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | ------- 32 | 33 | Unless stated otherwise, artwork included in this distribution is licensed 34 | under the Creative Commons Attribution 4.0 International License. 35 | 36 | You can learn more about the permitted use by visiting 37 | https://creativecommons.org/licenses/by/4.0/ 38 | -------------------------------------------------------------------------------- /docs/cli/commands/example.mdx: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | ## Setup an official Saleor example locally 4 | 5 | ### example 6 | 7 | ```sh 8 | $ saleor example --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor example [name] 15 | 16 | Setup an official Saleor example locally 17 | 18 | Positionals: 19 | name [string] [default: "my-saleor-app"] 20 | 21 | Options: 22 | --json Output the data as JSON [boolean] [default: false] 23 | --short Output data as text [boolean] [default: false] 24 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 25 | --dependencies, --deps [boolean] [default: true] 26 | -t, --template, --repo, --repository [string] 27 | -V, --version Show version number [boolean] 28 | -h, --help Show help [boolean] 29 | 30 | Examples: 31 | saleor example auth-sdk Setup the auth-sdk example from saleor/examples on GitHub 32 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/github.mdx: -------------------------------------------------------------------------------- 1 | # github 2 | 3 | ## Integrate with GitHub 4 | 5 | ### github 6 | 7 | ```sh 8 | $ saleor github --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor github [command] 15 | 16 | Integrate with GitHub 17 | 18 | Commands: 19 | saleor github login Add integration for Saleor CLI 20 | 21 | Options: 22 | --json Output the data as JSON [boolean] [default: false] 23 | --short Output data as text [boolean] [default: false] 24 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 25 | -V, --version Show version number [boolean] 26 | -h, --help Show help [boolean] 27 | ``` 28 | 29 | #### github login 30 | 31 | ```sh 32 | $ saleor github login --help 33 | ``` 34 | 35 | Help output: 36 | 37 | ``` 38 | saleor github login 39 | 40 | Add integration for Saleor CLI 41 | 42 | Options: 43 | --json Output the data as JSON [boolean] [default: false] 44 | --short Output data as text [boolean] [default: false] 45 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 46 | -V, --version Show version number [boolean] 47 | -h, --help Show help [boolean] 48 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/info.mdx: -------------------------------------------------------------------------------- 1 | # info 2 | 3 | ## Hello from Saleor 4 | 5 | ### info 6 | 7 | ```sh 8 | $ saleor info --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor info 15 | 16 | Hello from Saleor 17 | 18 | Options: 19 | --json Output the data as JSON [boolean] [default: false] 20 | --short Output data as text [boolean] [default: false] 21 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 22 | -V, --version Show version number [boolean] 23 | -h, --help Show help [boolean] 24 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/job.mdx: -------------------------------------------------------------------------------- 1 | # job 2 | 3 | ## Manage Saleor Cloud tasks 4 | 5 | ### job 6 | 7 | ```sh 8 | $ saleor job --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor job [command] 15 | 16 | Manage Saleor Cloud tasks 17 | 18 | Commands: 19 | saleor job list List tasks 20 | 21 | Options: 22 | --json Output the data as JSON [boolean] [default: false] 23 | --short Output data as text [boolean] [default: false] 24 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 25 | -V, --version Show version number [boolean] 26 | -h, --help Show help [boolean] 27 | ``` 28 | 29 | #### job list 30 | 31 | ```sh 32 | $ saleor job list --help 33 | ``` 34 | 35 | Help output: 36 | 37 | ``` 38 | saleor job list 39 | 40 | List tasks 41 | 42 | Options: 43 | --json Output the data as JSON [boolean] [default: false] 44 | --short Output data as text [boolean] [default: false] 45 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 46 | --env [string] 47 | --page A page number within the paginated result set [number] 48 | --page-size, --page_size Number of results to return per page [number] 49 | --is-blocking, --is_blocking Filter by non/blocking tasks [boolean] 50 | --status Filter by status: active, completed, failed, successful [string] 51 | -V, --version Show version number [boolean] 52 | -h, --help Show help [boolean] 53 | 54 | Examples: 55 | saleor task list 56 | saleor task list my-environment --page=2 57 | saleor task list my-environment --page-size=100 58 | saleor task list my-environment --is-blocking 59 | saleor task list my-environment --status=active 60 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/login.mdx: -------------------------------------------------------------------------------- 1 | # login 2 | 3 | ## Log in to the Saleor Cloud 4 | 5 | ### login 6 | 7 | ```sh 8 | $ saleor login --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor login 15 | 16 | Log in to the Saleor Cloud 17 | 18 | Options: 19 | --json Output the data as JSON [boolean] [default: false] 20 | --short Output data as text [boolean] [default: false] 21 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 22 | --token use with headless flag, create a token at https://cloud.saleor.io/tokens [string] 23 | --headless login without the need for a browser [boolean] [default: false] 24 | -V, --version Show version number [boolean] 25 | -h, --help Show help [boolean] 26 | 27 | Examples: 28 | saleor login 29 | saleor login --headless 30 | saleor login --headless --token=TOKEN 31 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/logout.mdx: -------------------------------------------------------------------------------- 1 | # logout 2 | 3 | ## Log out from the Saleor Cloud 4 | 5 | ### logout 6 | 7 | ```sh 8 | $ saleor logout --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor logout 15 | 16 | Log out from the Saleor Cloud 17 | 18 | Options: 19 | --json Output the data as JSON [boolean] [default: false] 20 | --short Output data as text [boolean] [default: false] 21 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 22 | -V, --version Show version number [boolean] 23 | -h, --help Show help [boolean] 24 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/open.mdx: -------------------------------------------------------------------------------- 1 | # open 2 | 3 | ## Open resource in a browser 4 | 5 | ### open 6 | 7 | ```sh 8 | $ saleor open --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor open [resource] 15 | 16 | Open resource in a browser 17 | 18 | Positionals: 19 | resource [string] [choices: "dashboard", "api", "docs", "docs/api", "docs/apps", "docs/webhooks", "docs/storefront", "docs/cli"] 20 | 21 | Options: 22 | --json Output the data as JSON [boolean] [default: false] 23 | --short Output data as text [boolean] [default: false] 24 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 25 | -V, --version Show version number [boolean] 26 | -h, --help Show help [boolean] 27 | 28 | Examples: 29 | saleor open dashboard Open instance dashboard 30 | saleor open api Open instance GraphQL endpoint 31 | saleor open docs Open Saleor documentation page 32 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/project.mdx: -------------------------------------------------------------------------------- 1 | # project 2 | 3 | ## Manage Saleor Cloud projects 4 | 5 | ### project 6 | 7 | ```sh 8 | $ saleor project --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor project [command] 15 | 16 | Manage Saleor Cloud projects 17 | 18 | Commands: 19 | saleor project list List projects 20 | saleor project create [name] Create a new project 21 | saleor project remove [slug] Remove the project 22 | saleor project show [project] Show a specific project 23 | 24 | Options: 25 | --json Output the data as JSON [boolean] [default: false] 26 | --short Output data as text [boolean] [default: false] 27 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 28 | -V, --version Show version number [boolean] 29 | -h, --help Show help [boolean] 30 | ``` 31 | 32 | #### project list 33 | 34 | ```sh 35 | $ saleor project list --help 36 | ``` 37 | 38 | Help output: 39 | 40 | ``` 41 | saleor project list 42 | 43 | List projects 44 | 45 | Options: 46 | --json Output the data as JSON [boolean] [default: false] 47 | --short Output data as text [boolean] [default: false] 48 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 49 | -V, --version Show version number [boolean] 50 | -h, --help Show help [boolean] 51 | ``` 52 | 53 | #### project create 54 | 55 | ```sh 56 | $ saleor project create --help 57 | ``` 58 | 59 | Help output: 60 | 61 | ``` 62 | saleor project create [name] 63 | 64 | Create a new project 65 | 66 | Positionals: 67 | name name for the new backup [string] 68 | 69 | Options: 70 | --json Output the data as JSON [boolean] [default: false] 71 | --short Output data as text [boolean] [default: false] 72 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 73 | --plan specify the plan [string] 74 | --region specify the region [string] 75 | -V, --version Show version number [boolean] 76 | -h, --help Show help [boolean] 77 | ``` 78 | 79 | #### project remove 80 | 81 | ```sh 82 | $ saleor project remove --help 83 | ``` 84 | 85 | Help output: 86 | 87 | ``` 88 | saleor project remove [slug] 89 | 90 | Remove the project 91 | 92 | Positionals: 93 | slug slug of the project [string] 94 | 95 | Options: 96 | --json Output the data as JSON [boolean] [default: false] 97 | --short Output data as text [boolean] [default: false] 98 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 99 | --force skip confirmation prompt [boolean] 100 | -V, --version Show version number [boolean] 101 | -h, --help Show help [boolean] 102 | ``` 103 | 104 | #### project show 105 | 106 | ```sh 107 | $ saleor project show --help 108 | ``` 109 | 110 | Help output: 111 | 112 | ``` 113 | saleor project show [project] 114 | 115 | Show a specific project 116 | 117 | Positionals: 118 | project [string] 119 | 120 | Options: 121 | --json Output the data as JSON [boolean] [default: false] 122 | --short Output data as text [boolean] [default: false] 123 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 124 | -V, --version Show version number [boolean] 125 | -h, --help Show help [boolean] 126 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/register.mdx: -------------------------------------------------------------------------------- 1 | # register 2 | 3 | ## Create a Saleor Cloud account 4 | 5 | ### register 6 | 7 | ```sh 8 | $ saleor register --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor register 15 | 16 | Create a Saleor Cloud account 17 | 18 | Options: 19 | --json Output the data as JSON [boolean] [default: false] 20 | --short Output data as text [boolean] [default: false] 21 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 22 | --from-cli specify sign up via CLI [boolean] [default: false] 23 | -V, --version Show version number [boolean] 24 | -h, --help Show help [boolean] 25 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/status.mdx: -------------------------------------------------------------------------------- 1 | # status 2 | 3 | ## Show the login status for the systems that CLI depends on 4 | 5 | ### status 6 | 7 | ```sh 8 | $ saleor status --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor status 15 | 16 | Show the login status for the systems that CLI depends on 17 | 18 | Options: 19 | --json Output the data as JSON [boolean] [default: false] 20 | --short Output data as text [boolean] [default: false] 21 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 22 | -V, --version Show version number [boolean] 23 | -h, --help Show help [boolean] 24 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/storefront.mdx: -------------------------------------------------------------------------------- 1 | # storefront 2 | 3 | ## Create a Next.js Storefront 4 | 5 | ### storefront 6 | 7 | ```sh 8 | $ saleor storefront --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor storefront [command] 15 | 16 | Create a Next.js Storefront 17 | 18 | Commands: 19 | saleor storefront create [name] Bootstrap example [name] 20 | 21 | Options: 22 | --json Output the data as JSON [boolean] [default: false] 23 | --short Output data as text [boolean] [default: false] 24 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 25 | -V, --version Show version number [boolean] 26 | -h, --help Show help [boolean] 27 | ``` 28 | 29 | #### storefront create 30 | 31 | ```sh 32 | $ saleor storefront create --help 33 | ``` 34 | 35 | Help output: 36 | 37 | ``` 38 | saleor storefront create [name] 39 | 40 | Bootstrap example [name] 41 | 42 | Positionals: 43 | name [string] [default: "saleor-storefront"] 44 | 45 | Options: 46 | --json Output the data as JSON [boolean] [default: false] 47 | --short Output data as text [boolean] [default: false] 48 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 49 | --demo specify demo process [boolean] [default: false] 50 | --environment specify environment id [string] 51 | -t, --template [string] [default: "saleor/storefront"] 52 | -b, --branch [string] [default: "canary"] 53 | -V, --version Show version number [boolean] 54 | -h, --help Show help [boolean] 55 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/task.mdx: -------------------------------------------------------------------------------- 1 | # task 2 | 3 | ## Manage Saleor Cloud tasks 4 | 5 | ### task 6 | 7 | ```sh 8 | $ saleor task --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor task [command] 15 | 16 | Manage Saleor Cloud tasks 17 | 18 | Commands: 19 | saleor task list List tasks 20 | 21 | Options: 22 | --json Output the data as JSON [boolean] [default: false] 23 | --short Output data as text [boolean] [default: false] 24 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 25 | -V, --version Show version number [boolean] 26 | -h, --help Show help [boolean] 27 | ``` 28 | 29 | #### task list 30 | 31 | ```sh 32 | $ saleor task list --help 33 | ``` 34 | 35 | Help output: 36 | 37 | ``` 38 | saleor task list 39 | 40 | List tasks 41 | 42 | Options: 43 | --json Output the data as JSON [boolean] [default: false] 44 | --short Output data as text [boolean] [default: false] 45 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 46 | --env [string] 47 | --page A page number within the paginated result set [number] 48 | --page-size, --page_size Number of results to return per page [number] 49 | --is-blocking, --is_blocking Filter by non/blocking tasks [boolean] 50 | --status Filter by status: active, completed, failed, successful [string] 51 | -V, --version Show version number [boolean] 52 | -h, --help Show help [boolean] 53 | 54 | Examples: 55 | saleor task list 56 | saleor task list my-environment --page=2 57 | saleor task list my-environment --page-size=100 58 | saleor task list my-environment --is-blocking 59 | saleor task list my-environment --status=active 60 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/telemetry.mdx: -------------------------------------------------------------------------------- 1 | # telemetry 2 | 3 | ## DEPRECATED. Manage telemetry preferences 4 | 5 | ### telemetry 6 | 7 | ```sh 8 | $ saleor telemetry --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor telemetry [command] 15 | 16 | DEPRECATED. Manage telemetry preferences 17 | 18 | Commands: 19 | saleor telemetry disable Disable the telemetry 20 | saleor telemetry enable Enable the telemetry 21 | saleor telemetry status Show the telemetry status [default] 22 | 23 | Options: 24 | --json Output the data as JSON [boolean] [default: false] 25 | --short Output data as text [boolean] [default: false] 26 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 27 | -V, --version Show version number [boolean] 28 | -h, --help Show help [boolean] 29 | ``` 30 | 31 | #### telemetry disable 32 | 33 | ```sh 34 | $ saleor telemetry disable --help 35 | ``` 36 | 37 | Help output: 38 | 39 | ``` 40 | saleor telemetry disable 41 | 42 | Disable the telemetry 43 | 44 | Options: 45 | --json Output the data as JSON [boolean] [default: false] 46 | --short Output data as text [boolean] [default: false] 47 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 48 | -V, --version Show version number [boolean] 49 | -h, --help Show help [boolean] 50 | ``` 51 | 52 | #### telemetry enable 53 | 54 | ```sh 55 | $ saleor telemetry enable --help 56 | ``` 57 | 58 | Help output: 59 | 60 | ``` 61 | saleor telemetry enable 62 | 63 | Enable the telemetry 64 | 65 | Options: 66 | --json Output the data as JSON [boolean] [default: false] 67 | --short Output data as text [boolean] [default: false] 68 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 69 | -V, --version Show version number [boolean] 70 | -h, --help Show help [boolean] 71 | ``` 72 | 73 | #### telemetry status 74 | 75 | ```sh 76 | $ saleor telemetry status --help 77 | ``` 78 | 79 | Help output: 80 | 81 | ``` 82 | saleor telemetry status 83 | 84 | Show the telemetry status 85 | 86 | Options: 87 | --json Output the data as JSON [boolean] [default: false] 88 | --short Output data as text [boolean] [default: false] 89 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 90 | -V, --version Show version number [boolean] 91 | -h, --help Show help [boolean] 92 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/trigger.mdx: -------------------------------------------------------------------------------- 1 | # trigger 2 | 3 | ## This triggers a Saleor event 4 | 5 | ### trigger 6 | 7 | ```sh 8 | $ saleor trigger --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor trigger [event] 15 | 16 | This triggers a Saleor event 17 | 18 | Options: 19 | --json Output the data as JSON [boolean] [default: false] 20 | --short Output data as text [boolean] [default: false] 21 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 22 | --event [string] 23 | --id [string] 24 | -V, --version Show version number [boolean] 25 | -h, --help Show help [boolean] 26 | ``` -------------------------------------------------------------------------------- /docs/cli/commands/vercel.mdx: -------------------------------------------------------------------------------- 1 | # vercel 2 | 3 | ## Integrate with Vercel 4 | 5 | ### vercel 6 | 7 | ```sh 8 | $ saleor vercel --help 9 | ``` 10 | 11 | Help output: 12 | 13 | ``` 14 | saleor vercel [command] 15 | 16 | Integrate with Vercel 17 | 18 | Commands: 19 | saleor vercel login Add integration for Saleor CLI 20 | 21 | Options: 22 | --json Output the data as JSON [boolean] [default: false] 23 | --short Output data as text [boolean] [default: false] 24 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 25 | -V, --version Show version number [boolean] 26 | -h, --help Show help [boolean] 27 | ``` 28 | 29 | #### vercel login 30 | 31 | ```sh 32 | $ saleor vercel login --help 33 | ``` 34 | 35 | Help output: 36 | 37 | ``` 38 | saleor vercel login 39 | 40 | Add integration for Saleor CLI 41 | 42 | Options: 43 | --json Output the data as JSON [boolean] [default: false] 44 | --short Output data as text [boolean] [default: false] 45 | -u, --instance, --url Saleor instance API URL (must start with the protocol, i.e. https:// or http://) [string] 46 | -V, --version Show version number [boolean] 47 | -h, --help Show help [boolean] 48 | ``` -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | alias p := publish 2 | 3 | publish version: 4 | #!/usr/bin/env bash 5 | set -euo pipefail 6 | pnpm compile 7 | newversion=`pnpm version {{version}} -m "Release %s" --tag-version-prefix=` 8 | echo Preparing $newversion 9 | pnpm publish --no-git-checks {{ if version == "prerelease" { "--tag next" } else { "" } }} 10 | node -e "let pkg=require('./package.json'); pkg.name='saleor'; require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));" 11 | pnpm publish --no-git-checks {{ if version == "prerelease" { "--tag next" } else { "" } }} 12 | node -e "let pkg=require('./package.json'); pkg.name='saleor-cli'; require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));" 13 | 14 | -------------------------------------------------------------------------------- /src/cli/app/create.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import Enquirer from 'enquirer'; 4 | import got from 'got'; 5 | import { print } from 'graphql'; 6 | import type { Arguments, CommandBuilder } from 'yargs'; 7 | 8 | import { AppCreate } from '../../generated/graphql.js'; 9 | import { Config } from '../../lib/config.js'; 10 | import { obfuscateArgv, println, validateLength } from '../../lib/util.js'; 11 | import { 12 | useAvailabilityChecker, 13 | useInstanceConnector, 14 | } from '../../middleware/index.js'; 15 | import { WebhookError } from '../../types.js'; 16 | import { choosePermissions, getPermissionsEnum } from './permission.js'; 17 | 18 | const debug = Debug('saleor-cli:app:create'); 19 | 20 | export const command = 'create [name]'; 21 | export const desc = 'Create a new Saleor Local App'; 22 | 23 | export const builder: CommandBuilder = (_) => 24 | _.positional('name', { 25 | type: 'string', 26 | demandOption: false, 27 | desc: 'name for the new Local App', 28 | }) 29 | .option('permissions', { 30 | type: 'array', 31 | demandOption: false, 32 | desc: 'The array of permissions', 33 | }) 34 | .example( 35 | 'saleor app create --name="my saleor app" --permissions=MANAGE_USERS --permissions=MANAGE_STAFF', 36 | '', 37 | ) 38 | .example( 39 | 'saleor app create --name="my saleor app" --organization="organization-slug" --environment="env-id-or-name" --permissions=MANAGE_USERS --permissions=MANAGE_STAFF', 40 | '', 41 | ); 42 | 43 | export const handler = async (argv: Arguments) => { 44 | debug('command arguments: %O', obfuscateArgv(argv)); 45 | 46 | const { name } = (await Enquirer.prompt([ 47 | { 48 | type: 'input', 49 | name: 'name', 50 | message: 'Local App name', 51 | initial: argv.name, 52 | required: true, 53 | skip: !!argv.name, 54 | validate: (value) => validateLength(value, 255), 55 | }, 56 | ])) as { name: string }; 57 | 58 | debug(`Using the name: ${name}`); 59 | 60 | const { instance } = argv; 61 | const headers = await Config.getBearerHeader(); 62 | const availablePermissions = await getPermissionsEnum(instance); 63 | 64 | const permissions = 65 | argv.permissions ?? (await choosePermissions(availablePermissions, 0)); 66 | 67 | const { data }: any = await got 68 | .post(instance, { 69 | headers, 70 | json: { 71 | query: print(AppCreate), 72 | variables: { 73 | name, 74 | permissions, 75 | }, 76 | }, 77 | }) 78 | .json(); 79 | 80 | const { 81 | appCreate: { app, errors }, 82 | } = data; 83 | 84 | if (errors.length) { 85 | throw new Error( 86 | errors.map((e: WebhookError) => `\n ${e.field} - ${e.message}`).join(), 87 | ); 88 | } 89 | 90 | if (argv.json) { 91 | console.log(JSON.stringify(app, null, 2)); 92 | return; 93 | } 94 | 95 | println(chalk('App created with id', chalk.green(app?.id))); 96 | }; 97 | 98 | export const middlewares = [useInstanceConnector, useAvailabilityChecker]; 99 | -------------------------------------------------------------------------------- /src/cli/app/generate.ts: -------------------------------------------------------------------------------- 1 | // FIXME 2 | /* eslint-disable no-case-declarations */ 3 | import Debug from 'debug'; 4 | import { Arguments, CommandBuilder } from 'yargs'; 5 | 6 | import { CommandRemovedError, obfuscateArgv } from '../../lib/util.js'; 7 | import { Options } from '../../types.js'; 8 | 9 | const debug = Debug('saleor-cli:app:generate'); 10 | 11 | export const command = 'generate '; 12 | export const desc = false; 13 | // export const desc = 'Generate a resource for a Saleor App'; 14 | 15 | export const builder: CommandBuilder = (_) => 16 | _.positional('resource', { 17 | type: 'string', 18 | demandOption: true, 19 | choices: ['webhook', 'query', 'mutation', 'subscription'], 20 | }); 21 | 22 | export const handler = async (argv: Arguments) => { 23 | debug('command arguments: %O', obfuscateArgv(argv)); 24 | 25 | throw new CommandRemovedError( 26 | 'This command has been removed\nPlease check documentation for `app` commands at https://github.com/saleor/saleor-cli/tree/main/docs', 27 | ); 28 | }; 29 | 30 | // export const middlewares = [verifyIsSaleorAppDirectory]; 31 | -------------------------------------------------------------------------------- /src/cli/app/index.ts: -------------------------------------------------------------------------------- 1 | import * as create from './create.js'; 2 | import * as generate from './generate.js'; 3 | import * as install from './install.js'; 4 | import * as list from './list.js'; 5 | import * as permission from './permission.js'; 6 | import * as remove from './remove.js'; 7 | import * as template from './template.js'; 8 | import * as token from './token.js'; 9 | import * as tunnel from './tunnel.js'; 10 | import * as uninstall from './uninstall.js'; 11 | 12 | export default function (_: any) { 13 | _.command([ 14 | list, 15 | install, 16 | uninstall, 17 | create, 18 | tunnel, 19 | token, 20 | permission, 21 | template, 22 | remove, 23 | 24 | // no auth needed 25 | generate, 26 | ]) 27 | // .middleware([useToken, useOrganization, useEnvironment]) 28 | .demandCommand(1, 'You need at least one command before moving on'); 29 | } 30 | -------------------------------------------------------------------------------- /src/cli/app/install.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { doSaleorAppInstall } from '../../lib/common.js'; 5 | import { obfuscateArgv, printContext } from '../../lib/util.js'; 6 | import { 7 | useAppConfig, 8 | useAvailabilityChecker, 9 | useInstanceConnector, 10 | } from '../../middleware/index.js'; 11 | import { Options } from '../../types.js'; 12 | 13 | const debug = Debug('saleor-cli:app:install'); 14 | 15 | export const command = 'install'; 16 | export const desc = 'Install a Saleor App by URL'; 17 | 18 | export const builder: CommandBuilder = (_) => 19 | _.option('via-dashboard', { 20 | type: 'boolean', 21 | default: false, 22 | }) 23 | .option('app-name', { 24 | demandOption: false, 25 | type: 'string', 26 | desc: 'Application name', 27 | }) 28 | .option('manifest-URL', { 29 | demandOption: false, 30 | type: 'string', 31 | desc: 'Application Manifest URL', 32 | }) 33 | .example( 34 | 'saleor app install --manifest-URL="https://my-saleor-app.com/api/manifest', 35 | '', 36 | ) 37 | .example( 38 | 'saleor app install --manifest-URL="https://my-saleor-app.com/api/manifest --app-name="Saleor app"', 39 | '', 40 | ) 41 | .example( 42 | 'saleor app install --organization="organization-slug" --environment="env-id-or-name" --app-name="Saleor app" --manifest-URL="https://my-saleor-app.com/api/manifest"', 43 | '', 44 | ); 45 | 46 | export const handler = async (argv: Arguments) => { 47 | debug('command arguments: %O', obfuscateArgv(argv)); 48 | 49 | printContext(argv); 50 | 51 | await doSaleorAppInstall(argv); 52 | 53 | process.exit(0); 54 | }; 55 | 56 | export const middlewares = [ 57 | useAppConfig, 58 | useInstanceConnector, 59 | useAvailabilityChecker, 60 | ]; 61 | -------------------------------------------------------------------------------- /src/cli/app/list.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from '@oclif/core'; 2 | import chalk from 'chalk'; 3 | import Debug from 'debug'; 4 | import got from 'got'; 5 | import { print } from 'graphql'; 6 | import { Arguments, CommandBuilder } from 'yargs'; 7 | 8 | import { GetApps } from '../../generated/graphql.js'; 9 | import { Config } from '../../lib/config.js'; 10 | import { 11 | formatDateTime, 12 | getAppsFromResult, 13 | obfuscateArgv, 14 | } from '../../lib/util.js'; 15 | import { 16 | useAppConfig, 17 | useAvailabilityChecker, 18 | useInstanceConnector, 19 | } from '../../middleware/index.js'; 20 | import { Options } from '../../types.js'; 21 | 22 | const debug = Debug('saleor-cli:app:list'); 23 | 24 | export const command = 'list'; 25 | export const desc = 'List installed Saleor Apps for an environment'; 26 | 27 | export const builder: CommandBuilder = (_) => _.example('saleor app list', ''); 28 | 29 | export const handler = async (argv: Arguments) => { 30 | debug('command arguments: %O', obfuscateArgv(argv)); 31 | 32 | const { ux: cli } = CliUx; 33 | const headers = await Config.getBearerHeader(); 34 | 35 | const { instance, json } = argv; 36 | 37 | debug('Fetching Saleor Apps'); 38 | const { data }: any = await got 39 | .post(instance, { 40 | headers, 41 | json: { 42 | query: print(GetApps), 43 | variables: {}, 44 | }, 45 | }) 46 | .json(); 47 | 48 | const apps = getAppsFromResult(data, json); 49 | 50 | const collection: any[] = apps.map(({ node }: any) => ({ ...node })); 51 | 52 | if (json) { 53 | console.log(JSON.stringify(collection, null, 2)); 54 | return; 55 | } 56 | 57 | cli.table(collection, { 58 | id: { 59 | header: 'ID', 60 | minWidth: 2, 61 | get: ({ id }) => chalk.gray(id), 62 | }, 63 | name: { 64 | header: 'Name', 65 | minWidth: 2, 66 | get: ({ name }) => chalk.cyan(name), 67 | }, 68 | type: { 69 | header: 'URL', 70 | get: ({ type }) => chalk.yellow(type), 71 | }, 72 | isActive: { 73 | header: 'Active?', 74 | minWidth: 2, 75 | get: ({ isActive }) => (isActive ? chalk.green('Yes') : chalk.red('No')), 76 | }, 77 | webhooks: { 78 | header: 'Webhooks #', 79 | minWidth: 2, 80 | get: ({ webhooks }) => (webhooks as string[]).length, 81 | }, 82 | created: { 83 | header: 'Created', 84 | minWidth: 2, 85 | get: ({ created }) => chalk.gray(formatDateTime(created)), 86 | }, 87 | }); 88 | 89 | process.exit(0); 90 | }; 91 | 92 | export const middlewares = [ 93 | useAppConfig, 94 | useInstanceConnector, 95 | useAvailabilityChecker, 96 | ]; 97 | -------------------------------------------------------------------------------- /src/cli/app/remove.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import got from 'got'; 4 | import { print } from 'graphql'; 5 | import type { Arguments, CommandBuilder } from 'yargs'; 6 | 7 | import { AppDelete } from '../../generated/graphql.js'; 8 | import { Config } from '../../lib/config.js'; 9 | import { confirmRemoval, obfuscateArgv, println } from '../../lib/util.js'; 10 | import { 11 | useAvailabilityChecker, 12 | useInstanceConnector, 13 | } from '../../middleware/index.js'; 14 | import { WebhookError } from '../../types.js'; 15 | import { getSaleorApp } from './token.js'; 16 | 17 | const debug = Debug('saleor-cli:app:create'); 18 | 19 | export const command = 'remove [app-id]'; 20 | export const desc = 'Create a new Saleor Local App'; 21 | 22 | export const builder: CommandBuilder = (_) => 23 | _.option('app-id', { 24 | type: 'string', 25 | demandOption: false, 26 | desc: 'The Saleor App id', 27 | }) 28 | .option('force', { 29 | type: 'boolean', 30 | desc: 'skip confirmation prompt', 31 | }) 32 | .example('saleor app remove', '') 33 | .example( 34 | 'saleor app remove --app-id="app-id" --environment="env-id-or-name" --force', 35 | '', 36 | ); 37 | 38 | export const handler = async (argv: Arguments) => { 39 | debug('command arguments: %O', obfuscateArgv(argv)); 40 | 41 | const { instance, appId, json } = argv; 42 | const { app } = await getSaleorApp({ instance, appId, json }); 43 | 44 | const proceed = await confirmRemoval(argv, `app ${app}`); 45 | 46 | if (proceed && app) { 47 | const headers = await Config.getBearerHeader(); 48 | 49 | const { data }: any = await got 50 | .post(instance, { 51 | headers, 52 | json: { 53 | query: print(AppDelete), 54 | variables: { 55 | app, 56 | }, 57 | }, 58 | }) 59 | .json(); 60 | 61 | const { 62 | appDelete: { app: _app, errors }, 63 | } = data; 64 | 65 | if (errors.length) { 66 | throw new Error( 67 | errors.map((e: WebhookError) => `\n ${e.field} - ${e.message}`).join(), 68 | ); 69 | } 70 | 71 | println(chalk('App removed ', chalk.green(_app?.name, '-', _app?.id))); 72 | } 73 | }; 74 | 75 | export const middlewares = [useInstanceConnector, useAvailabilityChecker]; 76 | -------------------------------------------------------------------------------- /src/cli/app/token.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import Enquirer from 'enquirer'; 3 | import got from 'got'; 4 | import { print as gqlPrint } from 'graphql'; 5 | import { Arguments, CommandBuilder } from 'yargs'; 6 | 7 | import { AppTokenCreate, GetApps } from '../../generated/graphql.js'; 8 | import { Config } from '../../lib/config.js'; 9 | import { 10 | contentBox, 11 | getAppsFromResult, 12 | obfuscateArgv, 13 | print, 14 | printContext, 15 | } from '../../lib/util.js'; 16 | import { 17 | useAppConfig, 18 | useAvailabilityChecker, 19 | useInstanceConnector, 20 | } from '../../middleware/index.js'; 21 | import { Options } from '../../types.js'; 22 | 23 | const debug = Debug('saleor-cli:app:token'); 24 | 25 | export const command = 'token'; 26 | export const desc = 'Create a Saleor App token'; 27 | 28 | export const builder: CommandBuilder = (_) => 29 | _.option('app-id', { 30 | type: 'string', 31 | demandOption: false, 32 | desc: 'The Saleor App id', 33 | }) 34 | .example('saleor app token', '') 35 | .example('saleor app token --app-id="app-id"', '') 36 | .example( 37 | 'saleor app token --app-id="app-id=" --organization="organization-slug" --environment="env-id-or-name"', 38 | '', 39 | ); 40 | 41 | export const handler = async (argv: Arguments) => { 42 | debug('command arguments: %O', obfuscateArgv(argv)); 43 | 44 | printContext(argv); 45 | 46 | const { instance, json, short } = argv; 47 | 48 | let appId: string; 49 | 50 | if (!argv.appId) { 51 | const { app } = await getSaleorApp({ instance, json }); 52 | appId = app!; 53 | } else { 54 | appId = argv.appId as string; 55 | } 56 | 57 | debug(`Creating auth token for ${appId}`); 58 | try { 59 | const authToken = await createAppToken(instance, appId); 60 | 61 | if (short) { 62 | print(authToken); 63 | process.exit(0); 64 | } 65 | 66 | if (json) { 67 | print(`{ "token": "${authToken}" }`); 68 | process.exit(0); 69 | } 70 | 71 | console.log(); 72 | contentBox(` ${authToken}`, { title: 'Your Token' }); 73 | } catch (error) { 74 | console.log(error); 75 | } 76 | }; 77 | 78 | export const createAppToken = async (url: string, app: string) => { 79 | const headers = await Config.getBearerHeader(); 80 | 81 | debug(`Getting app token for ${app}`); 82 | const { data }: any = await got 83 | .post(url, { 84 | headers, 85 | json: { 86 | query: gqlPrint(AppTokenCreate), 87 | variables: { app }, 88 | }, 89 | }) 90 | .json(); 91 | 92 | const { 93 | appTokenCreate: { authToken }, 94 | } = data; 95 | return authToken; 96 | }; 97 | 98 | export const getSaleorApp = async ({ 99 | instance, 100 | appId = undefined, 101 | json, 102 | }: { 103 | instance: string; 104 | appId?: string | undefined; 105 | json: boolean | undefined; 106 | }) => { 107 | const headers = await Config.getBearerHeader(); 108 | 109 | debug('Fetching Saleor Apps'); 110 | const { data }: any = await got 111 | .post(instance, { 112 | headers, 113 | json: { 114 | query: gqlPrint(GetApps), 115 | variables: {}, 116 | }, 117 | }) 118 | .json(); 119 | 120 | const apps = getAppsFromResult(data, json); 121 | 122 | debug(`Available apps ${apps}`); 123 | 124 | // return early if appId have been provided and it's found in apps 125 | if (apps.map(({ node }: any) => node.id).includes(appId)) { 126 | return { app: appId, apps }; 127 | } 128 | 129 | const choices = apps.map(({ node }: any) => ({ 130 | name: node.name, 131 | value: node.id, 132 | hint: node.id, 133 | })); 134 | 135 | const { app } = await Enquirer.prompt<{ app: string }>({ 136 | type: 'autocomplete', 137 | name: 'app', 138 | choices, 139 | message: 'Select a Saleor App (start typing) ', 140 | }); 141 | 142 | return { app, apps }; 143 | }; 144 | 145 | export const middlewares = [ 146 | useAppConfig, 147 | useInstanceConnector, 148 | useAvailabilityChecker, 149 | ]; 150 | -------------------------------------------------------------------------------- /src/cli/app/uninstall.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import { Arguments, CommandBuilder } from 'yargs'; 4 | 5 | import { doSaleorAppDelete } from '../../lib/common.js'; 6 | import { 7 | obfuscateArgv, 8 | println, 9 | SaleorAppUninstallError, 10 | } from '../../lib/util.js'; 11 | import { 12 | useAppConfig, 13 | useAvailabilityChecker, 14 | useInstanceConnector, 15 | } from '../../middleware/index.js'; 16 | import { Options } from '../../types.js'; 17 | 18 | const debug = Debug('saleor-cli:app:uninstall'); 19 | 20 | export const command = 'uninstall '; 21 | export const desc = 22 | 'Uninstall a Saleor App by ID. You need to provide `appId`. List available apps and their IDs with `saleor app list`.'; 23 | 24 | export const builder: CommandBuilder = (_) => 25 | _.positional('app-id', { 26 | type: 'string', 27 | demandOption: false, 28 | desc: 'The Saleor App id', 29 | }) 30 | 31 | .example('saleor app uninstall app-id', '') 32 | .example( 33 | 'saleor app uninstall app-id --organization="organization-slug" --environment="env-id-or-name"', 34 | '', 35 | ); 36 | 37 | export const handler = async (argv: Arguments) => { 38 | debug('command arguments: %O', obfuscateArgv(argv)); 39 | 40 | println(`\nUninstalling ${argv.appId} the Saleor App from your Dashboard...`); 41 | 42 | const r = await doSaleorAppDelete(argv); 43 | 44 | if (r.length > 0) { 45 | throw new SaleorAppUninstallError(JSON.stringify(r)); 46 | } else { 47 | println(chalk.green('Success')); 48 | } 49 | }; 50 | 51 | export const middlewares = [ 52 | useAppConfig, 53 | useInstanceConnector, 54 | useAvailabilityChecker, 55 | ]; 56 | -------------------------------------------------------------------------------- /src/cli/backup/create.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import Enquirer from 'enquirer'; 3 | import type { Arguments, CommandBuilder } from 'yargs'; 4 | 5 | import { API, POST } from '../../lib/index.js'; 6 | import { 7 | obfuscateArgv, 8 | showResult, 9 | validateLength, 10 | waitForTask, 11 | } from '../../lib/util.js'; 12 | import { 13 | useBlockingTasksChecker, 14 | useInstanceConnector, 15 | } from '../../middleware/index.js'; 16 | 17 | const debug = Debug('saleor-cli:backup:create'); 18 | 19 | export const command = 'create [name]'; 20 | export const desc = 'Create a new backup'; 21 | 22 | export const builder: CommandBuilder = (_) => 23 | _.option('name', { 24 | type: 'string', 25 | demandOption: false, 26 | desc: 'name for the new backup', 27 | }) 28 | .example('saleor backup create', '') 29 | .example( 30 | 'saleor backup create my-backup --organization="organization-slug"', 31 | '', 32 | ); 33 | 34 | export const handler = async (argv: Arguments) => { 35 | debug('command arguments: %O', obfuscateArgv(argv)); 36 | 37 | const { name } = (await Enquirer.prompt([ 38 | { 39 | type: 'input', 40 | name: 'name', 41 | message: 'Backup name', 42 | initial: argv.name, 43 | required: true, 44 | skip: !!argv.name, 45 | validate: (value) => validateLength(value, 255), 46 | }, 47 | ])) as { name: string }; 48 | 49 | debug(`Using the name: ${name}`); 50 | 51 | const result = (await POST(API.EnvironmentBackup, argv, { 52 | json: { 53 | name, 54 | }, 55 | })) as any; 56 | debug('Backup creation triggered'); 57 | 58 | await waitForTask( 59 | argv, 60 | result.task_id, 61 | `Creating backup ${name}`, 62 | 'Yay! Backup created!', 63 | ); 64 | showResult(result, argv); 65 | }; 66 | 67 | export const middlewares = [useInstanceConnector, useBlockingTasksChecker]; 68 | -------------------------------------------------------------------------------- /src/cli/backup/index.ts: -------------------------------------------------------------------------------- 1 | import * as create from './create.js'; 2 | import * as list from './list.js'; 3 | import * as remove from './remove.js'; 4 | import * as restore from './restore.js'; 5 | import * as show from './show.js'; 6 | 7 | export default function (_: any) { 8 | _.command([list, create, show, remove, restore]).demandCommand( 9 | 1, 10 | 'You need at least one command before moving on', 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/cli/backup/list.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from '@oclif/core'; 2 | import chalk from 'chalk'; 3 | import Debug from 'debug'; 4 | import { Arguments, CommandBuilder } from 'yargs'; 5 | 6 | import { API, GET } from '../../lib/index.js'; 7 | import { 8 | formatDateTime, 9 | obfuscateArgv, 10 | verifyResultLength, 11 | } from '../../lib/util.js'; 12 | import { Backup, Options } from '../../types.js'; 13 | import { 14 | useBlockingTasksChecker, 15 | useOrganization, 16 | useToken, 17 | } from '../../middleware/index.js'; 18 | 19 | const debug = Debug('saleor-cli:backup:list'); 20 | 21 | export const command = 'list'; 22 | export const desc = 'List backups of the organization'; 23 | 24 | export const builder: CommandBuilder = (_) => 25 | _.option('name', { 26 | type: 'string', 27 | demandOption: false, 28 | desc: 'filter the output for name for backup', 29 | }) 30 | .option('latest', { 31 | type: 'boolean', 32 | default: false, 33 | demandOption: false, 34 | desc: 'show only the latest backup', 35 | }) 36 | .example('saleor backup list', '') 37 | .example('saleor backup list --organization="organization-slug"', ''); 38 | 39 | export const handler = async (argv: Arguments) => { 40 | debug('command arguments: %O', obfuscateArgv(argv)); 41 | 42 | debug(`Listing for ${argv.key}`); 43 | const result = (await (GET(API.Backup, argv) as Promise)) 44 | .filter((backup) => { 45 | if (argv.name) { 46 | return backup.name === argv.name; 47 | } 48 | 49 | return true; 50 | }) 51 | .filter((_backup, index) => { 52 | if (argv.latest) { 53 | return index === 0; 54 | } 55 | 56 | return true; 57 | }); 58 | 59 | verifyResultLength(result, 'backup', argv.json); 60 | 61 | if (argv.json) { 62 | console.log(JSON.stringify(result, null, 2)); 63 | return; 64 | } 65 | 66 | const { ux: cli } = CliUx; 67 | 68 | cli.table(result, { 69 | name: { 70 | header: 'Backup', 71 | minWidth: 2, 72 | get: ({ name }) => chalk.cyan(name), 73 | }, 74 | environment_name: { 75 | header: 'Environment', 76 | minWidth: 2, 77 | get: ({ environment_name: environment }) => chalk.green(environment), 78 | }, 79 | version: { 80 | header: 'Ver.', 81 | minWidth: 2, 82 | get: ({ saleor_version: version }) => chalk.yellow(version), 83 | }, 84 | created: { 85 | minWidth: 2, 86 | get: ({ created }) => chalk.gray(formatDateTime(created)), 87 | }, 88 | key: { minWidth: 2 }, 89 | }); 90 | }; 91 | 92 | export const middlewares = [useToken, useOrganization, useBlockingTasksChecker]; 93 | -------------------------------------------------------------------------------- /src/cli/backup/remove.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import type { Arguments, CommandBuilder } from 'yargs'; 4 | 5 | import { API, DELETE } from '../../lib/index.js'; 6 | import { 7 | confirmRemoval, 8 | obfuscateArgv, 9 | printlnSuccess, 10 | } from '../../lib/util.js'; 11 | import { Options } from '../../types.js'; 12 | import { 13 | useBlockingTasksChecker, 14 | useOrganization, 15 | useToken, 16 | } from '../../middleware/index.js'; 17 | 18 | const debug = Debug('saleor-cli:backup:remove'); 19 | 20 | export const command = 'remove '; 21 | export const desc = 'Remove the backup'; 22 | 23 | export const builder: CommandBuilder = (_) => 24 | _.positional('key', { 25 | type: 'string', 26 | demandOption: false, 27 | desc: 'key of the backup', 28 | }) 29 | .option('force', { 30 | type: 'boolean', 31 | desc: 'skip confirmation prompt', 32 | }) 33 | .example('saleor backup remove', '') 34 | .example('saleor backup remove backup-key --force', '') 35 | .example( 36 | 'saleor backup remove backup-key --force --organization="organization-slug"', 37 | '', 38 | ); 39 | 40 | export const handler = async (argv: Arguments) => { 41 | debug('command arguments: %O', obfuscateArgv(argv)); 42 | 43 | const proceed = await confirmRemoval(argv, `backup ${argv.key}`); 44 | 45 | if (proceed) { 46 | (await DELETE(API.Backup, { 47 | ...argv, 48 | })) as any; 49 | 50 | printlnSuccess(chalk.bold('Backup has been successfully removed')); 51 | } 52 | }; 53 | 54 | export const middlewares = [useToken, useOrganization, useBlockingTasksChecker]; 55 | -------------------------------------------------------------------------------- /src/cli/backup/restore.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import Enquirer from 'enquirer'; 3 | import type { Arguments, CommandBuilder } from 'yargs'; 4 | 5 | import { API, PUT } from '../../lib/index.js'; 6 | import { 7 | obfuscateArgv, 8 | promptOrganizationBackup, 9 | waitForTask, 10 | } from '../../lib/util.js'; 11 | import { Options } from '../../types.js'; 12 | import { updateWebhook } from '../webhook/update.js'; 13 | import { 14 | useBlockingTasksChecker, 15 | useInstanceConnector, 16 | } from '../../middleware/index.js'; 17 | 18 | const debug = Debug('saleor-cli:backup:restore'); 19 | 20 | export const command = 'restore [from]'; 21 | export const desc = 'Restore a specific backup'; 22 | 23 | export const builder: CommandBuilder = (_) => 24 | _.option('from', { 25 | type: 'string', 26 | demandOption: false, 27 | desc: 'the key of the snapshot', 28 | }) 29 | .option('skip-webhooks-update', { 30 | type: 'boolean', 31 | demandOption: false, 32 | desc: 'skip webhooks update prompt', 33 | }) 34 | .example('saleor backup restore', '') 35 | .example( 36 | 'saleor backup restore --from="backup-key" --skip-webhooks-update', 37 | '', 38 | ) 39 | .example( 40 | 'saleor backup restore --from="backup-key" --skip-webhooks-update --organization="organization-slug" --environment="env-id-or-name"', 41 | '', 42 | ); 43 | 44 | export const handler = async (argv: Arguments) => { 45 | debug('command arguments: %O', obfuscateArgv(argv)); 46 | 47 | const from = await getBackup(argv); 48 | 49 | debug('Triggering the restore process'); 50 | const result = (await PUT(API.Restore, argv, { 51 | json: { 52 | restore_from: from.value, 53 | }, 54 | })) as any; 55 | 56 | await waitForTask( 57 | argv, 58 | result.task_id, 59 | 'Restoring', 60 | 'Yay! Restore finished!', 61 | ); 62 | 63 | const { update } = await Enquirer.prompt<{ update: string }>({ 64 | type: 'confirm', 65 | name: 'update', 66 | skip: !!argv.skipWebhooksUpdate, 67 | message: 'Would you like to update webhooks targetUrl', 68 | }); 69 | 70 | if (update) { 71 | const { instance } = argv; 72 | await updateWebhook(instance, argv.json); 73 | } 74 | }; 75 | 76 | const getBackup = async (argv: Arguments) => { 77 | if (argv.from) { 78 | return { key: argv.from, value: argv.from }; 79 | } 80 | 81 | const data = await promptOrganizationBackup(argv); 82 | 83 | return data; 84 | }; 85 | 86 | export const middlewares = [useInstanceConnector, useBlockingTasksChecker]; 87 | -------------------------------------------------------------------------------- /src/cli/backup/show.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { API, GET } from '../../lib/index.js'; 5 | import { obfuscateArgv, showResult } from '../../lib/util.js'; 6 | import { Options } from '../../types.js'; 7 | import { 8 | useBlockingTasksChecker, 9 | useOrganization, 10 | useToken, 11 | } from '../../middleware/index.js'; 12 | 13 | const debug = Debug('saleor-cli:backup:show'); 14 | 15 | export const command = 'show [backup|backup-key]'; 16 | export const desc = 'Show a specific backup'; 17 | 18 | export const builder: CommandBuilder = (_) => 19 | _.example('saleor backup show', '') 20 | .example('saleor backup show', '') 21 | .example('saleor backup show backup-key', '') 22 | .example( 23 | 'saleor backup show backup-key --organization="organization-slug"', 24 | '', 25 | ); 26 | 27 | export const handler = async (argv: Arguments) => { 28 | debug('command arguments: %O', obfuscateArgv(argv)); 29 | 30 | const result = (await GET(API.Backup, argv)) as any; 31 | showResult(result, argv); 32 | }; 33 | 34 | export const middlewares = [useToken, useOrganization, useBlockingTasksChecker]; 35 | -------------------------------------------------------------------------------- /src/cli/configure.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from '@oclif/core'; 2 | import chalk from 'chalk'; 3 | import Debug from 'debug'; 4 | import Enquirer from 'enquirer'; 5 | import type { Arguments, CommandBuilder } from 'yargs'; 6 | 7 | import { Config } from '../lib/config.js'; 8 | import { 9 | API, 10 | GET, 11 | defaultCloudApiAuthDomain, 12 | defaultCloudApiUrl, 13 | } from '../lib/index.js'; 14 | import { 15 | formatConfirm, 16 | obfuscateArgv, 17 | promptEnvironment, 18 | promptOrganization, 19 | } from '../lib/util.js'; 20 | import { Options } from '../types.js'; 21 | 22 | const debug = Debug('saleor-cli:configure'); 23 | 24 | export const command = 'configure [token]'; 25 | export const desc = 'Configure Saleor CLI'; 26 | 27 | export const builder: CommandBuilder = (_) => 28 | _.positional('token', { 29 | type: 'string', 30 | demandOption: false, 31 | desc: 'token created at https://cloud.saleor.io/tokens', 32 | }).option('force', { 33 | type: 'boolean', 34 | desc: 'skip additional prompts', 35 | }); 36 | 37 | export const handler = async (argv: Arguments) => { 38 | debug('command arguments: %O', obfuscateArgv(argv)); 39 | 40 | const { token, force } = argv; 41 | const legitToken = await configure(token); 42 | 43 | if (force) { 44 | process.exit(0); 45 | } 46 | 47 | await chooseOrganization(legitToken); 48 | }; 49 | 50 | const validateToken = async (token: string) => { 51 | const user = (await GET(API.User, { token: `Token ${token}` })) as any; 52 | console.log(`${chalk.green('Success')}. Logged as ${user.email}\n`); 53 | }; 54 | 55 | const chooseOrganization = async (token: string | undefined) => { 56 | const organizations = (await GET(API.Organization, { token })) as any[]; 57 | 58 | if (organizations.length) { 59 | const { orgSetup } = (await Enquirer.prompt({ 60 | type: 'confirm', 61 | name: 'orgSetup', 62 | initial: 'yes', 63 | format: formatConfirm, 64 | message: 'Would you like to select a default organization?', 65 | })) as { orgSetup: boolean }; 66 | 67 | if (orgSetup) { 68 | const organization = await promptOrganization({ token }); 69 | await chooseEnv(token, organization.value); 70 | } 71 | } 72 | }; 73 | 74 | const chooseEnv = async ( 75 | token: string | undefined, 76 | organizationSlug: string, 77 | ) => { 78 | const envs = (await GET(API.Environment, { 79 | token, 80 | organization: organizationSlug, 81 | })) as any[]; 82 | 83 | if (envs.length) { 84 | const { envSetup } = (await Enquirer.prompt({ 85 | type: 'confirm', 86 | name: 'envSetup', 87 | initial: 'yes', 88 | format: formatConfirm, 89 | message: 'Would you like to select a default environment', 90 | })) as { envSetup: boolean }; 91 | 92 | if (envSetup) { 93 | const env = await promptEnvironment({ 94 | token, 95 | organization: organizationSlug, 96 | }); 97 | await Config.set('environment_id', env.value); 98 | } 99 | } 100 | }; 101 | 102 | export const configure = async (providedToken: string | undefined) => { 103 | const { ux: cli } = CliUx; 104 | let token = providedToken; 105 | while (!token) token = await cli.prompt('Access Token', { type: 'mask' }); 106 | 107 | await validateToken(token); 108 | Config.reset(); 109 | const header = `Token ${token}`; 110 | await Config.set('token', header); 111 | await Config.set('saleor_env', process.env.SALEOR_CLI_ENV || 'production'); 112 | await Config.set( 113 | 'cloud_api_url', 114 | process.env.SALEOR_CLI_ENV_URL || defaultCloudApiUrl, 115 | ); 116 | await Config.set( 117 | 'cloud_api_auth_domain', 118 | process.env.SALEOR_CLI_ENV_AUTH_DOMAIN || defaultCloudApiAuthDomain, 119 | ); 120 | return header; 121 | }; 122 | -------------------------------------------------------------------------------- /src/cli/dev/index.ts: -------------------------------------------------------------------------------- 1 | import * as docs from './docs.js'; 2 | import * as info from './info.js'; 3 | import * as prepare from './prepare.js'; 4 | 5 | export default function (_: any) { 6 | _.command([prepare, info, docs]).demandCommand( 7 | 1, 8 | 'You need at least one command before moving on', 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/cli/dev/info.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module'; 2 | 3 | import { run } from '../../lib/common.js'; 4 | 5 | const require = createRequire(import.meta.url); 6 | const pkg = require('../package.json'); 7 | 8 | export const command = 'info'; 9 | export const desc = 'Show env info for debugging'; 10 | 11 | export const handler = async () => { 12 | console.log(`Saleor CLI v${pkg.version}`); 13 | 14 | await run( 15 | 'npx', 16 | [ 17 | 'envinfo', 18 | '--system', 19 | '--binaries', 20 | '--browsers', 21 | '--npmPackages', 22 | '{saleor,@saleor/*}', 23 | ], 24 | { cwd: process.cwd() }, 25 | true, 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/cli/dev/prepare.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import got from 'got'; 4 | import { simpleGit } from 'simple-git'; 5 | import { Arguments } from 'yargs'; 6 | 7 | import { run } from '../../lib/common.js'; 8 | import { Config } from '../../lib/config.js'; 9 | import { GitError, obfuscateArgv, printlnSuccess } from '../../lib/util.js'; 10 | import { useGithub } from '../../middleware/index.js'; 11 | 12 | interface Options { 13 | branch?: string; 14 | } 15 | 16 | const debug = Debug('saleor-cli:dev:prepare'); 17 | 18 | export const command = 'prepare [branch|prURL]'; 19 | export const desc = 'Build cli from branch or pull request URL'; 20 | 21 | export const handler = async (argv: Arguments) => { 22 | debug('command arguments: %O', obfuscateArgv(argv)); 23 | 24 | const git = simpleGit(); 25 | 26 | // check if repo is saleor-cli 27 | debug('check if `prepare` is run from the `saleor-cli` repository'); 28 | try { 29 | const repo = (await git.remote(['get-url', 'origin'])) as string; 30 | const repoName = repo.trim().split('/').at(-1); 31 | 32 | if (repoName !== 'saleor-cli.git') { 33 | console.error( 34 | chalk.red( 35 | '\nThis script works only in', 36 | chalk.bold('saleor-cli'), 37 | 'repository.', 38 | '\naYour current repository: ', 39 | chalk.bold(repoName), 40 | ), 41 | ); 42 | throw new GitError(); 43 | } 44 | } catch (err: any) { 45 | if (err.message.match('not a git repository')) { 46 | console.error( 47 | chalk.red( 48 | '\nThis script works only in', 49 | chalk.bold('saleor-cli'), 50 | 'repository', 51 | '\nPlease navigate to your local', 52 | chalk.bold('saleor-cli'), 53 | 'directory', 54 | ), 55 | ); 56 | throw new GitError(); 57 | } 58 | 59 | throw err; 60 | } 61 | 62 | await git.fetch('origin'); 63 | 64 | const { branch: url } = argv; 65 | 66 | // PR 67 | if (url?.match(/https:\/\//)) { 68 | const { github_token: GitHubToken } = await Config.get(); 69 | const number = parseInt(url.split('/').at(-1) || '0', 10); 70 | 71 | console.log(' getting PR'); 72 | 73 | const query = `query getHeadRefName($name: String!, $owner: String!, $number: Int!) { 74 | repository(name: $name, owner: $owner) { 75 | pullRequest(number: $number) { 76 | headRefName 77 | } 78 | } 79 | }`; 80 | 81 | interface GitHubRepository { 82 | repository: { 83 | pullRequest: { 84 | headRefName: string; 85 | }; 86 | }; 87 | } 88 | 89 | debug('Get the SHA of the PR using GitHub GraphQL API'); 90 | const { data } = await got 91 | .post('https://api.github.com/graphql', { 92 | headers: { Authorization: GitHubToken }, 93 | json: { 94 | query, 95 | variables: { 96 | number, 97 | name: 'saleor-cli', 98 | owner: 'saleor', 99 | }, 100 | }, 101 | }) 102 | .json<{ data: GitHubRepository }>(); 103 | 104 | const { 105 | repository: { 106 | pullRequest: { headRefName: branch }, 107 | }, 108 | } = data; 109 | 110 | await git.checkout(`origin/${branch}`); 111 | } 112 | 113 | // branch 114 | if (url && !url?.match(/https:\/\//)) { 115 | console.log(` getting ${url} branch from origin`); 116 | await git.checkout(`origin/${url}`); 117 | } 118 | 119 | // no params, using main 120 | if (!url) { 121 | console.log(' getting main branch from origin'); 122 | await git.pull('origin', 'main'); 123 | } 124 | 125 | console.log(' pnpm i'); 126 | await run('pnpm', ['i', '--ignore-scripts'], { cwd: process.cwd() }); 127 | console.log(' pnpm build'); 128 | await run('pnpm', ['build'], { cwd: process.cwd() }); 129 | 130 | printlnSuccess('prepared'); 131 | console.log('\n run the saleor cli from project root with:'); 132 | console.log(chalk.green('\n node ./dist/saleor.js command-name')); 133 | }; 134 | 135 | export const middlewares = [useGithub]; 136 | -------------------------------------------------------------------------------- /src/cli/env/auth.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import Enquirer from 'enquirer'; 5 | import { API, PATCH } from '../../lib/index.js'; 6 | import { 7 | obfuscateArgv, 8 | println, 9 | printlnSuccess, 10 | validateLength, 11 | } from '../../lib/util.js'; 12 | import { 13 | useBlockingTasksChecker, 14 | useEnvironment, 15 | } from '../../middleware/index.js'; 16 | import { Options } from '../../types.js'; 17 | import { getEnvironment } from '../../lib/environment.js'; 18 | 19 | const debug = Debug('saleor-cli:env:auth'); 20 | 21 | export const command = 'auth [key|environment]'; 22 | export const desc = 'Manage basic auth for a specific environment'; 23 | 24 | export const builder: CommandBuilder = (_) => 25 | _.positional('key', { 26 | type: 'string', 27 | demandOption: false, 28 | desc: 'key of the environment', 29 | }) 30 | .option('login', { 31 | type: 'string', 32 | demandOption: false, 33 | desc: 'basic auth login of the environment', 34 | }) 35 | .option('password', { 36 | alias: 'pass', 37 | type: 'string', 38 | demandOption: false, 39 | desc: 'basic auth password of the environment', 40 | }) 41 | .option('disable', { 42 | type: 'boolean', 43 | demandOption: false, 44 | desc: 'disable basic auth for the environment', 45 | }) 46 | .example('saleor env auth', '') 47 | .example('saleor env auth my-environment', '') 48 | .example( 49 | 'saleor env auth my-environment --login=saleor --password=saleor', 50 | '', 51 | ) 52 | .example('saleor env auth my-environment --disable', ''); 53 | 54 | export const handler = async (argv: Arguments) => { 55 | debug('command arguments: %O', obfuscateArgv(argv)); 56 | 57 | const environment = await getEnvironment(argv); 58 | 59 | if (argv.disable) { 60 | if (!environment.protected) { 61 | println('Basic auth is already disabled for this environment'); 62 | return; 63 | } 64 | 65 | await PATCH(API.Environment, argv, { 66 | json: { 67 | login: null, 68 | password: null, 69 | }, 70 | }); 71 | 72 | printlnSuccess('Basic auth is disabled'); 73 | return; 74 | } 75 | 76 | const json = await Enquirer.prompt<{ 77 | login: string; 78 | password: string; 79 | }>([ 80 | { 81 | type: 'input', 82 | name: 'login', 83 | message: 'Login', 84 | initial: argv.login, 85 | skip: !!argv.login, 86 | validate: (value) => validateLength(value, 127, 'login', true), 87 | }, 88 | { 89 | type: 'password', 90 | name: 'password', 91 | message: 'Password', 92 | initial: argv.password, 93 | skip: !!argv.password, 94 | validate: (value) => validateLength(value, 127, 'password', true), 95 | }, 96 | ]); 97 | 98 | await PATCH(API.Environment, argv, { 99 | json, 100 | }); 101 | 102 | printlnSuccess('Basic auth is enabled'); 103 | }; 104 | 105 | export const middlewares = [useEnvironment, useBlockingTasksChecker]; 106 | -------------------------------------------------------------------------------- /src/cli/env/clear.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { API, GET } from '../../lib/index.js'; 5 | import { obfuscateArgv, waitForTask } from '../../lib/util.js'; 6 | import { useBlockingTasksChecker } from '../../middleware/index.js'; 7 | import { Options } from '../../types.js'; 8 | 9 | const debug = Debug('saleor-cli:env:clear'); 10 | 11 | export const command = 'clear '; 12 | export const desc = 'Clear database for an environment'; 13 | 14 | export const builder: CommandBuilder = (_) => 15 | _.positional('key', { 16 | type: 'string', 17 | demandOption: false, 18 | desc: 'key of the environment', 19 | }) 20 | .example('saleor env clear my-environment', '') 21 | .example( 22 | 'saleor env clear my-environment --organization="organization-slug"', 23 | '', 24 | ); 25 | 26 | export const handler = async (argv: Arguments) => { 27 | debug('command arguments: %O', obfuscateArgv(argv)); 28 | debug('sending the request to Saleor API'); 29 | const result = (await GET(API.ClearDatabase, argv)) as any; 30 | await waitForTask(argv, result.task_id, 'Clearing', 'Yay! Database cleared!'); 31 | }; 32 | 33 | export const middlewares = [useBlockingTasksChecker]; 34 | -------------------------------------------------------------------------------- /src/cli/env/index.ts: -------------------------------------------------------------------------------- 1 | import { useOrganization, useToken } from '../../middleware/index.js'; 2 | import * as auth from './auth.js'; 3 | import * as cleardb from './clear.js'; 4 | import * as cors from './cors.js'; 5 | import * as create from './create.js'; 6 | import * as list from './list.js'; 7 | import * as maintenance from './maintenance.js'; 8 | import * as origins from './origins.js'; 9 | import * as populatedb from './populate.js'; 10 | import * as promote from './promote.js'; 11 | import * as remove from './remove.js'; 12 | import * as show from './show.js'; 13 | import * as change from './switch.js'; // `switch` is a JavaScript keyword 14 | import * as update from './update.js'; 15 | import * as upgrade from './upgrade.js'; 16 | 17 | export default function (_: any) { 18 | _.command([ 19 | auth, 20 | cleardb, 21 | cors, 22 | create, 23 | list, 24 | maintenance, 25 | origins, 26 | populatedb, 27 | promote, 28 | remove, 29 | show, 30 | change, 31 | update, 32 | upgrade, 33 | ]) 34 | .middleware([useToken, useOrganization]) 35 | .demandCommand(1, 'You need at least one command before moving on'); 36 | } 37 | -------------------------------------------------------------------------------- /src/cli/env/list.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from '@oclif/core'; 2 | import chalk from 'chalk'; 3 | import Debug from 'debug'; 4 | import { Arguments, CommandBuilder } from 'yargs'; 5 | 6 | import { API, GET } from '../../lib/index.js'; 7 | import { 8 | formatDateTime, 9 | obfuscateArgv, 10 | verifyResultLength, 11 | } from '../../lib/util.js'; 12 | import { EnvironmentList } from '../../types.js'; 13 | 14 | const debug = Debug('saleor-cli:env:list'); 15 | 16 | export const command = 'list'; 17 | export const desc = 'List environments'; 18 | 19 | export const builder: CommandBuilder = (_) => 20 | _.option('extended', { 21 | type: 'boolean', 22 | default: false, 23 | desc: 'show extended table', 24 | }); 25 | 26 | export const handler = async (argv: Arguments) => { 27 | debug('command arguments: %O', obfuscateArgv(argv)); 28 | 29 | const { extended } = argv; 30 | const result = (await GET(API.Environment, { 31 | ...argv, 32 | environment: '', 33 | })) as any[]; 34 | 35 | // const production = result.filter(({service}) => service.service_type === "SANDBOX") 36 | 37 | verifyResultLength(result, 'environment', argv.json); 38 | 39 | if (argv.json) { 40 | console.log(JSON.stringify(result, null, 2)); 41 | return; 42 | } 43 | 44 | const { ux: cli } = CliUx; 45 | 46 | cli.table( 47 | result, 48 | { 49 | name: { minWidth: 2, get: ({ name }) => chalk.cyan(name) }, 50 | created: { 51 | minWidth: 2, 52 | get: ({ created }) => chalk.gray(formatDateTime(created)), 53 | }, 54 | project: { minWidth: 2, get: (_) => _.project.name }, 55 | service: { 56 | minWidth: 2, 57 | header: 'Ver.', 58 | get: (_) => chalk.yellow(_.service.version), 59 | }, 60 | key: { minWidth: 2 }, 61 | domain: { minWidth: 2, get: ({ domain }) => domain, extended: true }, 62 | service_type: { minWidth: 2, get: ({ service }) => service.service_type }, 63 | }, 64 | { 65 | extended: extended as boolean, 66 | }, 67 | ); 68 | 69 | process.exit(0); 70 | }; 71 | -------------------------------------------------------------------------------- /src/cli/env/maintenance.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import Enquirer from 'enquirer'; 5 | import { getEnvironment } from '../../lib/environment.js'; 6 | import { API, PATCH } from '../../lib/index.js'; 7 | import { obfuscateArgv, println, printlnSuccess } from '../../lib/util.js'; 8 | import { 9 | useBlockingTasksChecker, 10 | useEnvironment, 11 | } from '../../middleware/index.js'; 12 | import { EnvironmentMaintenance } from '../../types.js'; 13 | 14 | const debug = Debug('saleor-cli:env:maintanance'); 15 | 16 | export const command = 'maintenance [key|environment]'; 17 | export const desc = 18 | 'Enable or disable maintenance mode in a specific environment'; 19 | 20 | export const builder: CommandBuilder = (_) => 21 | _.positional('key', { 22 | type: 'string', 23 | demandOption: false, 24 | desc: 'key of the environment', 25 | }) 26 | .option('enable', { 27 | type: 'boolean', 28 | demandOption: false, 29 | desc: 'enable maintenance mode', 30 | }) 31 | .option('disable', { 32 | type: 'boolean', 33 | demandOption: false, 34 | desc: 'disable maintenance mode', 35 | }) 36 | .example('saleor env maintenance', '') 37 | .example('saleor env maintenance my-environment', '') 38 | .example('saleor env maintenance my-environment --enable', '') 39 | .example('saleor env maintenance my-environment --disable', ''); 40 | 41 | export const handler = async (argv: Arguments) => { 42 | debug('command arguments: %O', obfuscateArgv(argv)); 43 | const { enable, disable } = argv; 44 | 45 | if (disable !== undefined) { 46 | await updateEnvironment(argv, { maintenance_mode: false }); 47 | } 48 | 49 | if (enable !== undefined) { 50 | await updateEnvironment(argv, { maintenance_mode: true }); 51 | } 52 | 53 | const { maintenanceMode } = await Enquirer.prompt<{ 54 | maintenanceMode: string; 55 | }>({ 56 | type: 'select', 57 | name: 'maintenanceMode', 58 | choices: [ 59 | { message: 'disable maintenance mode', name: 'disable' }, 60 | { message: 'enable maintenance mode', name: 'enable' }, 61 | ], 62 | message: 'Choose an option', 63 | }); 64 | 65 | await updateEnvironment(argv, { 66 | maintenance_mode: maintenanceMode === 'enable', 67 | }); 68 | }; 69 | 70 | const updateEnvironment = async ( 71 | argv: Arguments, 72 | json: { maintenance_mode: boolean }, 73 | ) => { 74 | const { maintenance_mode: maintenanceMode } = await getEnvironment(argv); 75 | const mode = json.maintenance_mode ? 'enabled' : 'disabled'; 76 | 77 | if (maintenanceMode === json.maintenance_mode) { 78 | println(`Maintenance mode is already ${mode}`); 79 | process.exit(0); 80 | } 81 | 82 | await PATCH(API.Environment, argv, { 83 | json, 84 | }); 85 | 86 | printlnSuccess(`Maintenance mode is ${mode}`); 87 | process.exit(0); 88 | }; 89 | 90 | export const middlewares = [useEnvironment, useBlockingTasksChecker]; 91 | -------------------------------------------------------------------------------- /src/cli/env/origins.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import Enquirer from 'enquirer'; 5 | import { API, PATCH } from '../../lib/index.js'; 6 | import { obfuscateArgv, printlnSuccess } from '../../lib/util.js'; 7 | import { 8 | useBlockingTasksChecker, 9 | useEnvironment, 10 | } from '../../middleware/index.js'; 11 | import { Options } from '../../types.js'; 12 | import { getEnvironment, promptOrigin } from '../../lib/environment.js'; 13 | 14 | const debug = Debug('saleor-cli:env:auth'); 15 | 16 | export const command = 'origins [key|environment]'; 17 | export const desc = 'Manage the environment\'s trusted client origins'; 18 | 19 | export const builder: CommandBuilder = (_) => 20 | _.positional('key', { 21 | type: 'string', 22 | demandOption: false, 23 | desc: 'key of the environment', 24 | }) 25 | .option('origins', { 26 | type: 'array', 27 | demandOption: false, 28 | desc: 'Allowed domains', 29 | }) 30 | .example('saleor env origins', '') 31 | .example( 32 | 'saleor env origins my-environment --origins=https://trusted-origin.com', 33 | '', 34 | ); 35 | 36 | export const handler = async (argv: Arguments) => { 37 | debug('command arguments: %O', obfuscateArgv(argv)); 38 | 39 | const { allowed_client_origins: allowedClientOrigins } = 40 | await getEnvironment(argv); 41 | 42 | if (((argv.origins as undefined | string[]) || [])?.length > 0) { 43 | await PATCH(API.Environment, argv, { 44 | json: { 45 | allowed_client_origins: argv.origins, 46 | }, 47 | }); 48 | 49 | printlnSuccess('Specified trusted origins are allowed'); 50 | return; 51 | } 52 | 53 | const selected: string[] = (allowedClientOrigins as string[]) || []; 54 | let addMore = true; 55 | 56 | const { origins } = await Enquirer.prompt<{ 57 | origins: string; 58 | }>([ 59 | { 60 | type: 'multiselect', 61 | name: 'origins', 62 | message: 63 | 'Define Trusted Origins\n (use the arrows to navigate and the space bar to select)', 64 | choices: [...selected, 'Add a new trusted origin'], 65 | initial: selected, 66 | }, 67 | ]); 68 | 69 | do { 70 | if (origins.length === 0) { 71 | return; 72 | } 73 | if (origins.includes('Add a new trusted origin')) { 74 | const form = await promptOrigin(); 75 | selected.push(form.origin); 76 | addMore = form.addMore; 77 | } else { 78 | addMore = false; 79 | } 80 | } while (addMore); 81 | 82 | const { proceed } = await Enquirer.prompt<{ 83 | proceed: boolean; 84 | }>([ 85 | { 86 | type: 'confirm', 87 | name: 'proceed', 88 | message: `Do you want to set the following trusted origins?\n ${selected.join( 89 | '\n ', 90 | )}`, 91 | }, 92 | ]); 93 | 94 | if (proceed) { 95 | await PATCH(API.Environment, argv, { 96 | json: { 97 | allowed_client_origins: selected, 98 | }, 99 | }); 100 | 101 | printlnSuccess('Specified trusted origins are allowed'); 102 | } 103 | }; 104 | 105 | export const middlewares = [useEnvironment, useBlockingTasksChecker]; 106 | -------------------------------------------------------------------------------- /src/cli/env/populate.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { API, GET } from '../../lib/index.js'; 5 | import { confirmRemoval, obfuscateArgv, waitForTask } from '../../lib/util.js'; 6 | import { 7 | useBlockingTasksChecker, 8 | useEnvironment, 9 | } from '../../middleware/index.js'; 10 | import { Options } from '../../types.js'; 11 | 12 | const debug = Debug('saleor-cli:env:populate'); 13 | 14 | export const command = 'populate [key|environment]'; 15 | export const desc = 'Populate database for an environment'; 16 | 17 | export const builder: CommandBuilder = (_) => 18 | _.positional('key', { 19 | type: 'string', 20 | demandOption: false, 21 | desc: 'key of the environment', 22 | }); 23 | 24 | export const handler = async (argv: Arguments) => { 25 | debug('command arguments: %O', obfuscateArgv(argv)); 26 | 27 | const { environment } = argv; 28 | 29 | const proceed = await confirmRemoval( 30 | argv, 31 | `environment ${environment}`, 32 | 'replace database with a sample for', 33 | ); 34 | 35 | if (proceed) { 36 | const result = (await GET(API.PopulateDatabase, argv)) as any; 37 | await waitForTask( 38 | argv, 39 | result.task_id, 40 | `Populating database: ${argv.environment}`, 41 | 'Yay! Database populated!', 42 | ); 43 | } 44 | }; 45 | 46 | export const middlewares = [useEnvironment, useBlockingTasksChecker]; 47 | -------------------------------------------------------------------------------- /src/cli/env/promote.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { getEnvironment } from '../../lib/environment.js'; 5 | import { API, PUT } from '../../lib/index.js'; 6 | import { 7 | obfuscateArgv, 8 | promptCompatibleVersion, 9 | showResult, 10 | } from '../../lib/util.js'; 11 | import { useEnvironment } from '../../middleware/index.js'; 12 | import { Options } from '../../types.js'; 13 | 14 | const debug = Debug('saleor-cli:env:promote'); 15 | 16 | export const command = 'promote [key|environment]'; 17 | export const desc = 'Promote environment to production'; 18 | 19 | export const builder: CommandBuilder = (_) => 20 | _.positional('key', { 21 | type: 'string', 22 | demandOption: false, 23 | desc: 'key of the environment', 24 | }).option('saleor', { 25 | type: 'string', 26 | desc: 'specify the Saleor version', 27 | }); 28 | 29 | export const handler = async (argv: Arguments) => { 30 | debug('command arguments: %O', obfuscateArgv(argv)); 31 | 32 | const service = await getService(argv); 33 | const result = (await PUT(API.UpgradeEnvironment, argv, { 34 | json: { service: service.value }, 35 | })) as any; 36 | showResult(result, argv); 37 | }; 38 | 39 | const getService = async (argv: Arguments) => { 40 | if (argv.saleor) { 41 | return { key: argv.saleor, value: argv.saleor }; 42 | } 43 | 44 | const env = await getEnvironment(argv); 45 | const data = await promptCompatibleVersion({ 46 | ...argv, 47 | region: env.service.region, 48 | serviceName: `?compatible_with=${env.service.version}`, 49 | service: 'PRODUCTION', 50 | }); 51 | 52 | return data; 53 | }; 54 | 55 | export const middlewares = [useEnvironment]; 56 | -------------------------------------------------------------------------------- /src/cli/env/remove.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import type { Arguments, CommandBuilder } from 'yargs'; 4 | 5 | import { Config } from '../../lib/config.js'; 6 | import { API, DELETE } from '../../lib/index.js'; 7 | import { 8 | confirmRemoval, 9 | obfuscateArgv, 10 | printlnSuccess, 11 | } from '../../lib/util.js'; 12 | import { useEnvironment } from '../../middleware/index.js'; 13 | import { Options, Task } from '../../types.js'; 14 | 15 | const debug = Debug('saleor-cli:env:remove'); 16 | 17 | export const command = 'remove [key|environment]'; 18 | export const desc = 'Delete an environment'; 19 | 20 | export const builder: CommandBuilder = (_) => 21 | _.positional('key', { 22 | type: 'string', 23 | demandOption: false, 24 | desc: 'key of the environment', 25 | }).option('force', { 26 | type: 'boolean', 27 | desc: 'skip confirmation prompt', 28 | }); 29 | 30 | export const handler = async (argv: Arguments) => { 31 | debug('command arguments: %O', obfuscateArgv(argv)); 32 | 33 | const { environment } = argv; 34 | 35 | const proceed = await confirmRemoval(argv, `environment ${environment}`); 36 | 37 | if (proceed && environment) { 38 | (await DELETE(API.Environment, { ...argv, ...{ environment } })) as Task; 39 | await removeCurrentEnvironment(environment); 40 | printlnSuccess(`Environment ${environment} deleted`); 41 | } 42 | }; 43 | 44 | const removeCurrentEnvironment = async (environment: string) => { 45 | const { environment_id: current } = await Config.get(); 46 | 47 | if (environment === current) { 48 | await Config.remove('environment_id'); 49 | console.log( 50 | 'Default environment unset. Use ', 51 | chalk.bold('saleor environment switch'), 52 | ' to choose default one', 53 | ); 54 | } 55 | }; 56 | 57 | export const middlewares = [useEnvironment]; 58 | -------------------------------------------------------------------------------- /src/cli/env/show.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { getEnvironment } from '../../lib/environment.js'; 5 | import { obfuscateArgv, showResult } from '../../lib/util.js'; 6 | import { useEnvironment } from '../../middleware/index.js'; 7 | import { Options } from '../../types.js'; 8 | 9 | const debug = Debug('saleor-cli:env:show'); 10 | 11 | export const command = 'show [key|environment]'; 12 | export const desc = 'Show a specific environment'; 13 | 14 | export const builder: CommandBuilder = (_) => 15 | _.positional('key', { 16 | type: 'string', 17 | demandOption: false, 18 | desc: 'key of the environment', 19 | }); 20 | 21 | export const handler = async (argv: Arguments) => { 22 | debug('command arguments: %O', obfuscateArgv(argv)); 23 | 24 | const result = await getEnvironment(argv); 25 | 26 | showResult(result, argv); 27 | }; 28 | 29 | export const middlewares = [useEnvironment]; 30 | -------------------------------------------------------------------------------- /src/cli/env/switch.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import type { Arguments, CommandBuilder } from 'yargs'; 4 | 5 | import { Config } from '../../lib/config.js'; 6 | import { obfuscateArgv, println, promptEnvironment } from '../../lib/util.js'; 7 | import { verifyEnvironment } from '../../middleware/index.js'; 8 | import { BaseOptions } from '../../types.js'; 9 | 10 | interface Options extends BaseOptions { 11 | key?: string; 12 | } 13 | 14 | const debug = Debug('saleor-cli:env:switch'); 15 | 16 | export const command = 'switch [key|environment]'; 17 | export const desc = 'Make the provided environment the default one'; 18 | 19 | export const builder: CommandBuilder = (_) => 20 | _.positional('key', { 21 | type: 'string', 22 | demandOption: false, 23 | desc: 'key of the environment', 24 | }); 25 | 26 | export const handler = async (argv: Arguments) => { 27 | debug('command arguments: %O', obfuscateArgv(argv)); 28 | 29 | const environment = await getEnvironment(argv); 30 | 31 | await Config.set('environment_id', environment.value); 32 | 33 | println(chalk(chalk.bold('Environment ·'), chalk.cyan(environment.value))); 34 | }; 35 | 36 | const getEnvironment = async (argv: Arguments) => { 37 | const { environment, token, organization } = argv; 38 | 39 | if (environment && token && organization) { 40 | const { key } = await verifyEnvironment(token, organization, environment); 41 | return { name: key, value: key }; 42 | } 43 | 44 | const data = await promptEnvironment(argv); 45 | 46 | return data; 47 | }; 48 | -------------------------------------------------------------------------------- /src/cli/env/update.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import Enquirer from 'enquirer'; 5 | import { getEnvironment } from '../../lib/environment.js'; 6 | import { API, PATCH } from '../../lib/index.js'; 7 | import { 8 | obfuscateArgv, 9 | printlnSuccess, 10 | validateLength, 11 | } from '../../lib/util.js'; 12 | import { 13 | useBlockingTasksChecker, 14 | useEnvironment, 15 | } from '../../middleware/index.js'; 16 | import { Options } from '../../types.js'; 17 | 18 | const debug = Debug('saleor-cli:env:update'); 19 | 20 | export const command = 'update [key|environment]'; 21 | export const desc = 'Update the name of the environment'; 22 | 23 | export const builder: CommandBuilder = (_) => 24 | _.positional('key', { 25 | type: 'string', 26 | demandOption: false, 27 | desc: 'key of the environment', 28 | }) 29 | .option('name', { 30 | type: 'string', 31 | demandOption: false, 32 | desc: 'name of the environment', 33 | }) 34 | .example('saleor env update', '') 35 | .example('saleor env update my-environment', '') 36 | .example('saleor env update my-environment --name=renamed-env', ''); 37 | 38 | export const handler = async (argv: Arguments) => { 39 | debug('command arguments: %O', obfuscateArgv(argv)); 40 | 41 | const environment = await getEnvironment(argv); 42 | 43 | const json = await Enquirer.prompt<{ 44 | name: string; 45 | }>([ 46 | { 47 | type: 'input', 48 | name: 'name', 49 | message: 'name', 50 | initial: argv.name || environment.name, 51 | validate: (value) => validateLength(value, 255), 52 | skip: !!argv.name, 53 | }, 54 | ]); 55 | 56 | await PATCH(API.Environment, argv, { 57 | json, 58 | }); 59 | 60 | printlnSuccess('Environment update triggered'); 61 | }; 62 | 63 | export const middlewares = [useEnvironment, useBlockingTasksChecker]; 64 | -------------------------------------------------------------------------------- /src/cli/env/upgrade.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { getEnvironment } from '../../lib/environment.js'; 5 | import { API, PUT } from '../../lib/index.js'; 6 | import { 7 | obfuscateArgv, 8 | promptCompatibleVersion, 9 | waitForTask, 10 | } from '../../lib/util.js'; 11 | import { useEnvironment } from '../../middleware/index.js'; 12 | import { Options } from '../../types.js'; 13 | 14 | const debug = Debug('saleor-cli:env:upgrade'); 15 | 16 | export const command = 'upgrade [key|environment]'; 17 | export const desc = 'Upgrade a Saleor version in a specific environment'; 18 | 19 | export const builder: CommandBuilder = (_) => 20 | _.positional('key', { 21 | type: 'string', 22 | demandOption: false, 23 | desc: 'key of the environment', 24 | }); 25 | 26 | export const handler = async (argv: Arguments) => { 27 | debug('command arguments: %O', obfuscateArgv(argv)); 28 | 29 | const env = await getEnvironment(argv); 30 | const service = await promptCompatibleVersion({ 31 | ...argv, 32 | region: env.service.region, 33 | serviceName: `?compatible_with=${env.service.version}`, 34 | }); 35 | 36 | const result = (await PUT(API.UpgradeEnvironment, argv, { 37 | json: { service: service.value }, 38 | })) as any; 39 | await waitForTask( 40 | argv, 41 | result.task_id, 42 | 'Upgrading', 43 | 'Yay! Upgrade finished!', 44 | ); 45 | }; 46 | 47 | export const middlewares = [useEnvironment]; 48 | -------------------------------------------------------------------------------- /src/cli/example.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import chalk from 'chalk'; 3 | import Debug from 'debug'; 4 | import Enquirer from 'enquirer'; 5 | import kebabCase from 'lodash.kebabcase'; 6 | import ora from 'ora'; 7 | import replace from 'replace-in-file'; 8 | import sanitize from 'sanitize-filename'; 9 | import type { Arguments, CommandBuilder } from 'yargs'; 10 | 11 | import { getFolderName, run } from '../lib/common.js'; 12 | import { gitCopy } from '../lib/download.js'; 13 | import { getRepositoryContent, setupGitRepository } from '../lib/git.js'; 14 | import { 15 | checkPnpmPresence, 16 | contentBox, 17 | obfuscateArgv, 18 | println, 19 | } from '../lib/util.js'; 20 | import { useToken } from '../middleware/index.js'; 21 | 22 | const debug = Debug('saleor-cli:app:template'); 23 | 24 | export const command = 'example [name]'; 25 | export const desc = 'Setup an official Saleor example locally'; 26 | 27 | export const builder: CommandBuilder = (_) => 28 | _.positional('name', { 29 | type: 'string', 30 | demandOption: true, 31 | default: 'my-saleor-app', 32 | }) 33 | .option('dependencies', { 34 | type: 'boolean', 35 | default: true, 36 | alias: 'deps', 37 | }) 38 | .option('template', { 39 | type: 'string', 40 | alias: ['t', 'repo', 'repository'], 41 | }) 42 | .example( 43 | 'saleor example auth-sdk', 44 | 'Setup the auth-sdk example from saleor/examples on GitHub', 45 | ); 46 | 47 | export const handler = async (argv: Arguments): Promise => { 48 | debug('command arguments: %O', obfuscateArgv(argv)); 49 | 50 | debug('check PNPM presence'); 51 | await checkPnpmPresence('This Saleor App template'); 52 | 53 | const { name, template } = argv; 54 | 55 | let tmpl = template; 56 | 57 | if (!template) { 58 | const examples = await getRepositoryContent( 59 | 'https://api.github.com/repos/saleor/examples/contents', 60 | ); 61 | const choices = examples 62 | .map((e) => ({ name: e.name })) 63 | .filter((e) => e.name.startsWith('example-')) 64 | .map((e) => ({ 65 | name: e.name.replace('example-', ''), 66 | message: e.name.replace('example-', ''), 67 | })); 68 | 69 | const { result } = await Enquirer.prompt<{ result: string }>({ 70 | type: 'select', 71 | name: 'result', 72 | required: true, 73 | choices, 74 | message: 'Select an example', 75 | }); 76 | 77 | tmpl = result; 78 | } 79 | 80 | debug('construct the folder name'); 81 | const target = await getFolderName(sanitize(name)); 82 | const packageName = kebabCase(target); 83 | const dirMsg = `Directory: ${chalk.blue( 84 | path.join(process.env.PWD || '.', target), 85 | )}`; 86 | const appMsg = `Name: ${chalk.blue(packageName)}`; 87 | const templateMsg = `Template: ${chalk.gray( 88 | 'github.com/saleor/example-', 89 | )}${chalk.blue(chalk.underline(tmpl))}`; 90 | 91 | contentBox(` ${templateMsg}\n ${appMsg}\n ${dirMsg}`); 92 | 93 | const spinner = ora('Downloading...').start(); 94 | 95 | const repository = `https://github.com/saleor/example-${tmpl}.git`; 96 | debug(`downloading the example: ${repository}`); 97 | 98 | await gitCopy(repository, target); 99 | 100 | process.chdir(target); 101 | 102 | spinner.text = 'Updating package.json...'; 103 | await replace.replaceInFile({ 104 | files: 'package.json', 105 | from: /"name": "(.*)".*/g, 106 | to: `"name": "${packageName}",`, 107 | }); 108 | 109 | spinner.text = 'Setting up the Git repository...'; 110 | await setupGitRepository(); 111 | 112 | spinner.text = 'Installing dependencies...'; 113 | await run('pnpm', ['i', '--ignore-scripts'], { cwd: process.cwd() }); 114 | await run('pnpm', ['generate'], { cwd: process.cwd() }); 115 | 116 | spinner.succeed( 117 | chalk( 118 | 'Your Saleor example is ready in the', 119 | chalk.yellow(target), 120 | 'directory\n', 121 | ), 122 | ); 123 | 124 | println(' To start your application:\n'); 125 | println(` cd ${target}`); 126 | println(' pnpm dev'); 127 | }; 128 | 129 | export const middlewares = [useToken]; 130 | -------------------------------------------------------------------------------- /src/cli/github/index.ts: -------------------------------------------------------------------------------- 1 | import * as login from './login.js'; 2 | 3 | export default function (_: any) { 4 | _.command([login]).demandCommand( 5 | 1, 6 | 'You need at least one command before moving on', 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/cli/github/login.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import got from 'got'; 4 | import type { CommandBuilder } from 'yargs'; 5 | 6 | import { Config } from '../../lib/config.js'; 7 | import { contentBox, delay, printlnSuccess } from '../../lib/util.js'; 8 | import { 9 | GithubLoginDeviceCodeResponse, 10 | GithubLoginDeviceResponse, 11 | } from '../../types.js'; 12 | 13 | const debug = Debug('saleor-cli:github:login'); 14 | 15 | export const command = 'login'; 16 | export const desc = 'Add integration for Saleor CLI'; 17 | 18 | export const builder: CommandBuilder = (_) => _; 19 | 20 | export const handler = async () => { 21 | const { GithubClientID } = await Config.get(); 22 | 23 | const { 24 | user_code: userCode, 25 | device_code: deviceCode, 26 | verification_uri: verificationUri, 27 | interval, 28 | expires_in: expiresIn, 29 | } = (await got 30 | .post('https://github.com/login/device/code', { 31 | json: { 32 | client_id: GithubClientID, 33 | scope: 'repo', 34 | }, 35 | }) 36 | .json()) as GithubLoginDeviceResponse; 37 | 38 | contentBox(` 39 | ${chalk.bold('Please open the following URL in your browser:')} 40 | 41 | ${verificationUri} 42 | 43 | ${chalk('And enter the following code:', chalk.bold(userCode))}`); 44 | 45 | const pollForAccessToken = async () => { 46 | const { access_token: accessToken } = (await got 47 | .post('https://github.com/login/oauth/access_token', { 48 | json: { 49 | client_id: GithubClientID, 50 | grant_type: 'urn:ietf:params:oauth:grant-type:device_code', 51 | device_code: deviceCode, 52 | }, 53 | }) 54 | .json()) as GithubLoginDeviceCodeResponse; 55 | 56 | if (accessToken) { 57 | await Config.set('github_token', `Bearer ${accessToken}`); 58 | printlnSuccess( 59 | chalk.bold( 60 | 'You\'ve successfully authenticated GitHub with the Saleor CLI!', 61 | ), 62 | ); 63 | process.exit(0); 64 | } 65 | }; 66 | 67 | const expiry = Date.now() - expiresIn * 1000; 68 | 69 | do { 70 | debug(Date.now()); 71 | await delay(interval * 1000); 72 | 73 | try { 74 | await pollForAccessToken(); 75 | } catch { 76 | return; 77 | } 78 | } while (Date.now() > expiry); 79 | }; 80 | -------------------------------------------------------------------------------- /src/cli/info.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module'; 2 | 3 | import { CliUx } from '@oclif/core'; 4 | import chalk from 'chalk'; 5 | import Debug from 'debug'; 6 | import type { CommandBuilder } from 'yargs'; 7 | 8 | import { Config } from '../lib/config.js'; 9 | import { header } from '../lib/images.js'; 10 | import { API, GET, getEnvironment } from '../lib/index.js'; 11 | import { User } from '../types.js'; 12 | 13 | const require = createRequire(import.meta.url); 14 | const pkg = require('../package.json'); 15 | 16 | const debug = Debug('saleor-cli:info'); 17 | 18 | function hasEmail(unknown: unknown): unknown is { email: string } { 19 | return typeof unknown === 'object' && unknown !== null && 'email' in unknown; 20 | } 21 | 22 | export const command = 'info'; 23 | export const desc = 'Hello from Saleor'; 24 | 25 | export const builder: CommandBuilder = (_) => _; 26 | export const handler = async (): Promise => { 27 | debug('showing status'); 28 | 29 | header(pkg.version); 30 | 31 | console.log( 32 | chalk.blue(` 33 | _____ _ ______ ____ _____ 34 | / ____| /\\ | | | ____| / __ \\ | __ \\ 35 | | (___ / \\ | | | |__ | | | | | |__) | 36 | \\___ \\ / /\\ \\ | | | __| | | | | | _ / 37 | ____) | / ____ \\ | |____ | |____ | |__| | | | \\ \\ 38 | |_____/ /_/ \\_\\ |______| |______| \\____/ |_| \\_\\ 39 | `), 40 | ); 41 | 42 | console.log( 43 | chalk.bold.blueBright(` 44 | The commerce API that puts developers first 45 | `), 46 | ); 47 | 48 | console.log('\n'); 49 | 50 | const { ux: cli } = CliUx; 51 | 52 | cli.url(chalk.blue('Website - https://saleor.io/'), 'https://saleor.io/'); 53 | cli.url( 54 | chalk.blue('Console - https://cloud.saleor.io/'), 55 | 'https://cloud.saleor.io/', 56 | ); 57 | cli.url( 58 | chalk.blue('Github - https://github.com/saleor/'), 59 | 'https://github.com/saleor/', 60 | ); 61 | 62 | console.log(''); 63 | 64 | try { 65 | const { token } = await Config.get(); 66 | const user = (await GET(API.User, { token })) as User; 67 | 68 | if (hasEmail(user)) { 69 | const environment = await getEnvironment(); 70 | console.log( 71 | chalk.green( 72 | 'Hello', 73 | user.email, 74 | 'you\'re logged in to Saleor API -', 75 | environment, 76 | ), 77 | ); 78 | } 79 | } catch (e) { 80 | console.log(chalk.blue('You\'re not logged in')); 81 | console.log(' To log in run:'); 82 | console.log(' saleor login'); 83 | } 84 | 85 | console.log(''); 86 | }; 87 | -------------------------------------------------------------------------------- /src/cli/init.ts: -------------------------------------------------------------------------------- 1 | import type { Arguments, CommandBuilder } from 'yargs'; 2 | 3 | type Options = { 4 | name: string; 5 | }; 6 | 7 | export const command = 'init '; 8 | export const desc = 'Initialize '; 9 | 10 | export const builder: CommandBuilder = (_) => 11 | _.positional('name', { type: 'string', demandOption: true }); 12 | 13 | export const handler = (argv: Arguments): void => { 14 | const { name } = argv; 15 | const greeting = `Hello, ${name}!`; 16 | 17 | process.stdout.write(greeting); 18 | process.exit(0); 19 | }; 20 | -------------------------------------------------------------------------------- /src/cli/logout.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | 3 | import { Config } from '../lib/config.js'; 4 | 5 | const debug = Debug('saleor-cli:logout'); 6 | 7 | export const command = 'logout'; 8 | export const desc = 'Log out from the Saleor Cloud'; 9 | 10 | export const handler = (): void => { 11 | debug('resetting the config at `~/.config/saleor.json`'); 12 | Config.reset(); 13 | }; 14 | -------------------------------------------------------------------------------- /src/cli/open.ts: -------------------------------------------------------------------------------- 1 | import Enquirer from 'enquirer'; 2 | import { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { openURL } from '../lib/util.js'; 5 | import { useInstanceConnector } from '../middleware/index.js'; 6 | import { Options } from '../types.js'; 7 | 8 | export const command = 'open [resource]'; 9 | export const desc = 'Open resource in a browser'; 10 | 11 | const resources: Record = { 12 | dashboard: '/dashboard', 13 | api: '/graphql/', 14 | docs: 'https://docs.saleor.io/docs/3.x/', 15 | 'docs/api': 'https://docs.saleor.io/docs/3.x/developer', 16 | 'docs/apps': 17 | 'https://docs.saleor.io/docs/3.x/developer/extending/apps/quickstart', 18 | 'docs/webhooks': 19 | 'https://docs.saleor.io/docs/3.x/developer/extending/webhooks/overview', 20 | 'docs/storefront': 'https://github.com/saleor/storefront', 21 | 'docs/cli': 'https://docs.saleor.io/docs/3.x/cli', 22 | }; 23 | 24 | export const builder: CommandBuilder = (_) => 25 | _.positional('resource', { 26 | type: 'string', 27 | demandOption: true, 28 | choices: Object.keys(resources), 29 | }) 30 | .example('saleor open dashboard', 'Open instance dashboard') 31 | .example('saleor open api', 'Open instance GraphQL endpoint') 32 | .example('saleor open docs', 'Open Saleor documentation page'); 33 | 34 | export const handler = async (argv: Arguments) => { 35 | const choices = Object.keys(resources); 36 | 37 | const { resource } = await Enquirer.prompt<{ resource: string }>({ 38 | type: 'select', 39 | name: 'resource', 40 | choices, 41 | message: 'Choose the resource', 42 | skip: choices.includes(argv.resource as string), 43 | }); 44 | 45 | const path = resources[resource || (argv.resource as string)]; 46 | 47 | if (!path.startsWith('https')) { 48 | const url = await getURL(path, argv); 49 | await openURL(url); 50 | return; 51 | } 52 | 53 | await openURL(path); 54 | }; 55 | 56 | const getURL = async (path: string, argv: Arguments) => { 57 | const _argv = await useInstanceConnector(argv); 58 | const { instance } = _argv; 59 | return `${instance}${path}`; 60 | }; 61 | -------------------------------------------------------------------------------- /src/cli/organization/index.ts: -------------------------------------------------------------------------------- 1 | import { useToken } from '../../middleware/index.js'; 2 | import * as list from './list.js'; 3 | import * as permissions from './permissions.js'; 4 | import * as remove from './remove.js'; 5 | import * as show from './show.js'; 6 | import * as change from './switch.js'; 7 | 8 | export default function (_: any) { 9 | _.command([show, list, remove, permissions, change]) 10 | .middleware(useToken) 11 | .demandCommand(1, 'You need at least one command before moving on'); 12 | } 13 | -------------------------------------------------------------------------------- /src/cli/organization/list.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from '@oclif/core'; 2 | import { format } from 'date-fns'; 3 | import Debug from 'debug'; 4 | import { Arguments } from 'yargs'; 5 | 6 | import { API, GET } from '../../lib/index.js'; 7 | import { obfuscateArgv, verifyResultLength } from '../../lib/util.js'; 8 | import { Options } from '../../types.js'; 9 | 10 | const debug = Debug('saleor-cli:org:list'); 11 | 12 | export const command = 'list'; 13 | export const desc = 'List organizations'; 14 | 15 | export const handler = async (argv: Arguments) => { 16 | debug('command arguments: %O', obfuscateArgv(argv)); 17 | 18 | const result = (await GET(API.Organization, { 19 | ...argv, 20 | organization: '', 21 | })) as any[]; 22 | 23 | verifyResultLength(result, 'organization', argv.json); 24 | 25 | if (argv.json) { 26 | console.log(JSON.stringify(result, null, 2)); 27 | return; 28 | } 29 | 30 | const { ux: cli } = CliUx; 31 | 32 | cli.table(result, { 33 | slug: { minWidth: 2 }, 34 | name: { minWidth: 2 }, 35 | created: { 36 | minWidth: 2, 37 | get: (_) => format(new Date(_.created), 'yyyy-MM-dd HH:mm'), 38 | }, 39 | company_name: { minWidth: 2 }, 40 | owner_email: { minWidth: 2, get: (_) => _.owner.email }, 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/cli/organization/permissions.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { API, GET } from '../../lib/index.js'; 5 | import { obfuscateArgv, showResult } from '../../lib/util.js'; 6 | import { useOrganization } from '../../middleware/index.js'; 7 | import { Options } from '../../types.js'; 8 | 9 | const debug = Debug('saleor-cli:org:permissions'); 10 | 11 | export const command = 'permissions [slug|organization]'; 12 | export const desc = 'List organization permissions'; 13 | 14 | export const builder: CommandBuilder = (_) => 15 | _.positional('slug', { 16 | type: 'string', 17 | demandOption: false, 18 | desc: 'slug of the organization', 19 | }); 20 | 21 | export const handler = async (argv: Arguments) => { 22 | debug('command arguments: %O', obfuscateArgv(argv)); 23 | 24 | const result = (await GET(API.OrganizationPermissions, { 25 | ...argv, 26 | ...{ organization: argv.slug || argv.organization }, 27 | })) as any; 28 | 29 | showResult(result, argv); 30 | }; 31 | 32 | export const middlewares = [useOrganization]; 33 | -------------------------------------------------------------------------------- /src/cli/organization/remove.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import type { Arguments, CommandBuilder } from 'yargs'; 4 | 5 | import { Config } from '../../lib/config.js'; 6 | import { API, DELETE } from '../../lib/index.js'; 7 | import { 8 | confirmRemoval, 9 | obfuscateArgv, 10 | printlnSuccess, 11 | promptOrganization, 12 | } from '../../lib/util.js'; 13 | import { Options } from '../../types.js'; 14 | 15 | const debug = Debug('saleor-cli:org:remove'); 16 | 17 | export const command = 'remove [slug]'; 18 | export const desc = 'Remove the organization'; 19 | 20 | export const builder: CommandBuilder = (_) => 21 | _.positional('slug', { 22 | type: 'string', 23 | demandOption: false, 24 | desc: 'slug of the organization', 25 | }).option('force', { 26 | type: 'boolean', 27 | desc: 'skip confirmation prompt', 28 | }); 29 | 30 | export const handler = async (argv: Arguments) => { 31 | debug('command arguments: %O', obfuscateArgv(argv)); 32 | const organization = argv.slug 33 | ? { name: argv.slug, value: argv.slug } 34 | : await promptOrganization(argv); 35 | const proceed = await confirmRemoval( 36 | argv, 37 | `organization ${organization.name}`, 38 | ); 39 | 40 | if (proceed) { 41 | (await DELETE(API.Organization, { 42 | ...argv, 43 | organization: organization.value, 44 | })) as any; 45 | const { organization_slug: organizationSlug } = await Config.get(); 46 | if (organization.value === organizationSlug) { 47 | await Config.remove('organization_slug'); 48 | await Config.remove('environment_id'); 49 | } 50 | 51 | printlnSuccess(chalk.bold('Organization has been successfully removed')); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/cli/organization/show.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { API, GET } from '../../lib/index.js'; 5 | import { obfuscateArgv, showResult } from '../../lib/util.js'; 6 | import { useOrganization } from '../../middleware/index.js'; 7 | import { Options } from '../../types.js'; 8 | 9 | const debug = Debug('saleor-cli:org:show'); 10 | 11 | export const command = 'show [slug|organization]'; 12 | export const desc = 'Show a specific organization'; 13 | 14 | export const builder: CommandBuilder = (_) => _; 15 | 16 | export const handler = async (argv: Arguments) => { 17 | debug('command arguments: %O', obfuscateArgv(argv)); 18 | const result = (await GET(API.Organization, argv)) as any; 19 | 20 | showResult(result, argv); 21 | }; 22 | 23 | export const middlewares = [useOrganization]; 24 | -------------------------------------------------------------------------------- /src/cli/organization/switch.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import type { Arguments, CommandBuilder } from 'yargs'; 4 | 5 | import { Config } from '../../lib/config.js'; 6 | import { API, GET } from '../../lib/index.js'; 7 | import { 8 | obfuscateArgv, 9 | printlnSuccess, 10 | promptOrganization, 11 | SaleorOrganizationError, 12 | } from '../../lib/util.js'; 13 | import { Options } from '../../types.js'; 14 | 15 | const debug = Debug('saleor-cli:org:switch'); 16 | 17 | export const command = 'switch [slug]'; 18 | export const desc = 'Make the provided organization the default one'; 19 | 20 | export const builder: CommandBuilder = (_) => 21 | _.positional('slug', { 22 | type: 'string', 23 | demandOption: false, 24 | desc: 'slug of the organization', 25 | }); 26 | 27 | export const handler = async (argv: Arguments) => { 28 | debug('command arguments: %O', obfuscateArgv(argv)); 29 | const organization = await getOrganization(argv); 30 | 31 | await Config.set('organization_slug', organization.value); 32 | await Config.remove('environment_id'); 33 | 34 | printlnSuccess( 35 | chalk(chalk.bold('Organization ·'), chalk.cyan(organization.value)), 36 | ); 37 | }; 38 | 39 | const getOrganization = async (argv: Arguments) => { 40 | if (!argv.slug) { 41 | const data = await promptOrganization(argv); 42 | return data; 43 | } 44 | 45 | const organizations = (await GET(API.Organization, argv)) as any[]; 46 | 47 | if (!organizations.map((o) => o.slug).includes(argv.slug)) { 48 | throw new SaleorOrganizationError( 49 | `No organization with slug '${argv.slug}' found`, 50 | ); 51 | } 52 | 53 | return { name: argv.slug, value: argv.slug }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/cli/project/create.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { createProject, obfuscateArgv } from '../../lib/util.js'; 5 | import { ProjectCreate } from '../../types.js'; 6 | 7 | const debug = Debug('saleor-cli:project:create'); 8 | 9 | export const command = 'create [name]'; 10 | export const desc = 'Create a new project'; 11 | 12 | export const builder: CommandBuilder = (_) => 13 | _.positional('name', { 14 | type: 'string', 15 | demandOption: false, 16 | desc: 'name for the new backup', 17 | }) 18 | .option('plan', { 19 | type: 'string', 20 | desc: 'specify the plan', 21 | }) 22 | .option('region', { 23 | type: 'string', 24 | desc: 'specify the region', 25 | }); 26 | 27 | export const handler = async (argv: Arguments) => { 28 | debug('command arguments: %O', obfuscateArgv(argv)); 29 | await createProject(argv); 30 | }; 31 | -------------------------------------------------------------------------------- /src/cli/project/index.ts: -------------------------------------------------------------------------------- 1 | import { useOrganization, useToken } from '../../middleware/index.js'; 2 | import * as create from './create.js'; 3 | import * as list from './list.js'; 4 | import * as remove from './remove.js'; 5 | import * as show from './show.js'; 6 | 7 | export default function (_: any) { 8 | _.command([list, create, remove, show]) 9 | .middleware([useToken, useOrganization]) 10 | .demandCommand(1, 'You need at least one command before moving on'); 11 | } 12 | -------------------------------------------------------------------------------- /src/cli/project/list.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from '@oclif/core'; 2 | import chalk from 'chalk'; 3 | import Debug from 'debug'; 4 | import { Arguments } from 'yargs'; 5 | 6 | import { API, GET } from '../../lib/index.js'; 7 | import { formatDateTime, obfuscateArgv } from '../../lib/util.js'; 8 | import { Options } from '../../types.js'; 9 | 10 | const debug = Debug('saleor-cli:project:list'); 11 | 12 | export const command = 'list'; 13 | export const desc = 'List projects'; 14 | 15 | export const handler = async (argv: Arguments) => { 16 | debug('command arguments: %O', obfuscateArgv(argv)); 17 | const result = (await GET(API.Project, argv)) as any[]; 18 | 19 | if (argv.json) { 20 | console.log(JSON.stringify(result, null, 2)); 21 | return; 22 | } 23 | 24 | const { ux: cli } = CliUx; 25 | 26 | cli.table(result, { 27 | slug: { minWidth: 2 }, 28 | name: { minWidth: 2, get: ({ name }) => chalk.cyan(name) }, 29 | billing_period: { 30 | minWidth: 2, 31 | get: ({ billing_period: billingPeriod }) => 32 | chalk.gray(formatDateTime(billingPeriod.start)), 33 | }, 34 | 35 | region: { minWidth: 2 }, 36 | sandboxes: { 37 | minWidth: 5, 38 | header: '#', 39 | get: (_) => chalk.yellow(_.sandboxes.count), 40 | }, 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /src/cli/project/remove.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import type { Arguments, CommandBuilder } from 'yargs'; 4 | 5 | import { API, DELETE } from '../../lib/index.js'; 6 | import { 7 | confirmRemoval, 8 | obfuscateArgv, 9 | printlnSuccess, 10 | promptProject, 11 | } from '../../lib/util.js'; 12 | import { Options } from '../../types.js'; 13 | 14 | const debug = Debug('saleor-cli:project:remove'); 15 | 16 | export const command = 'remove [slug]'; 17 | export const desc = 'Remove the project'; 18 | 19 | export const builder: CommandBuilder = (_) => 20 | _.positional('slug', { 21 | type: 'string', 22 | demandOption: false, 23 | desc: 'slug of the project', 24 | }).option('force', { 25 | type: 'boolean', 26 | desc: 'skip confirmation prompt', 27 | }); 28 | 29 | export const handler = async (argv: Arguments) => { 30 | debug('command arguments: %O', obfuscateArgv(argv)); 31 | const project = argv.slug 32 | ? { name: argv.slug, value: argv.slug } 33 | : await promptProject(argv); 34 | const proceed = await confirmRemoval(argv, `project ${project.name}`); 35 | 36 | if (proceed) { 37 | (await DELETE(API.Project, { ...argv, project: project.value })) as any; 38 | printlnSuccess(chalk.bold('Project has been successfully removed')); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/cli/project/show.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { Arguments, CommandBuilder } from 'yargs'; 3 | 4 | import { API, GET } from '../../lib/index.js'; 5 | import { obfuscateArgv, showResult } from '../../lib/util.js'; 6 | import { interactiveProject } from '../../middleware/index.js'; 7 | import { Options } from '../../types.js'; 8 | 9 | const debug = Debug('saleor-cli:project:show'); 10 | 11 | export const command = 'show [project]'; 12 | export const desc = 'Show a specific project'; 13 | 14 | export const builder: CommandBuilder = (_) => 15 | _.positional('project', { type: 'string', demandOption: false }); 16 | 17 | export const handler = async (argv: Arguments) => { 18 | debug('command arguments: %O', obfuscateArgv(argv)); 19 | const result = (await GET(API.Project, argv)) as any; 20 | showResult(result, argv); 21 | }; 22 | 23 | export const middlewares = [interactiveProject]; 24 | -------------------------------------------------------------------------------- /src/cli/register.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import Enquirer from 'enquirer'; 4 | import { Arguments, CommandBuilder } from 'yargs'; 5 | 6 | import { canOpen, formatConfirm, obfuscateArgv, openURL } from '../lib/util.js'; 7 | 8 | const debug = Debug('saleor-cli:register'); 9 | 10 | export const command = ['register', 'signup']; 11 | export const desc = 'Create a Saleor Cloud account'; 12 | 13 | export const builder: CommandBuilder = (_) => 14 | _.option('from-cli', { 15 | type: 'boolean', 16 | default: false, 17 | desc: 'specify sign up via CLI', 18 | }); 19 | 20 | export const handler = async (argv: Arguments) => { 21 | debug('command arguments: %O', obfuscateArgv(argv)); 22 | 23 | const link = 'https://cloud.saleor.io/signup'; 24 | 25 | console.log( 26 | `\nUse the following link:\n${chalk.blue( 27 | link, 28 | )}\nto create a Saleor Cloud account.\n`, 29 | ); 30 | 31 | if (!(await canOpen())) { 32 | return; 33 | } 34 | 35 | const { confirm } = await Enquirer.prompt<{ confirm: boolean }>([ 36 | { 37 | type: 'confirm', 38 | name: 'confirm', 39 | required: true, 40 | format: formatConfirm, 41 | message: 'Do you want to open it right now?', 42 | }, 43 | ]); 44 | 45 | if (confirm) { 46 | await openURL(link); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/cli/status.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module'; 2 | 3 | import chalk from 'chalk'; 4 | import Debug from 'debug'; 5 | import got from 'got'; 6 | import type { CommandBuilder } from 'yargs'; 7 | 8 | import { Config } from '../lib/config.js'; 9 | import { getEnvironment } from '../lib/index.js'; 10 | 11 | const require = createRequire(import.meta.url); 12 | const pkg = require('../package.json'); 13 | 14 | const debug = Debug('saleor-cli:status'); 15 | 16 | export const command = 'status'; 17 | export const desc = 'Show the login status for the systems that CLI depends on'; 18 | 19 | export const builder: CommandBuilder = (_) => _; 20 | export const handler = async (): Promise => { 21 | debug('checking status'); 22 | 23 | const { 24 | token, 25 | vercel_token: VercelToken, 26 | github_token: GitHubToken, 27 | } = await Config.get(); 28 | 29 | const environment = await getEnvironment(); 30 | 31 | console.log(`Saleor CLI v${pkg.version}`); 32 | console.log(''); 33 | 34 | console.log( 35 | ` Saleor API: ${ 36 | token 37 | ? chalk.green('Logged', '-', environment) 38 | : `${chalk.red('Not logged')} Run: saleor login` 39 | }`, 40 | ); 41 | 42 | const vercel = await verifyVercelToken(VercelToken); 43 | console.log( 44 | ` Vercel: ${ 45 | vercel 46 | ? chalk.green('Logged', '-', vercel.user?.username, vercel.user?.email) 47 | : `${chalk.red('Not logged')} Run: saleor vercel login` 48 | }`, 49 | ); 50 | 51 | const github = await verifyGithubToken(GitHubToken); 52 | console.log( 53 | ` GitHub: ${ 54 | github 55 | ? chalk.green('Logged', '-', github.login, github.email) 56 | : `${chalk.red('Not logged')} Run: saleor github login` 57 | }`, 58 | ); 59 | }; 60 | 61 | const verifyVercelToken = async (VercelToken: string | undefined) => { 62 | if (!VercelToken) { 63 | return null; 64 | } 65 | 66 | interface Response { 67 | user: { 68 | email: string; 69 | username: string | null; 70 | }; 71 | } 72 | 73 | try { 74 | const data: Response = await got 75 | .get('https://api.vercel.com/v2/user', { 76 | headers: { 77 | 'Content-Type': 'application/json', 78 | Authorization: VercelToken, 79 | }, 80 | }) 81 | .json(); 82 | 83 | return data; 84 | } catch (error) { 85 | await Config.remove('vercel_token'); 86 | await Config.remove('vercel_team_id'); 87 | } 88 | 89 | return null; 90 | }; 91 | 92 | const verifyGithubToken = async (GitHubToken: string | undefined) => { 93 | if (!GitHubToken) { 94 | return null; 95 | } 96 | interface Response { 97 | email: string; 98 | login: string | null; 99 | } 100 | 101 | try { 102 | const data: Response = await got 103 | .get('https://api.github.com/user', { 104 | headers: { 105 | Accept: 'application/vnd.github+json', 106 | Authorization: GitHubToken, 107 | }, 108 | }) 109 | .json(); 110 | 111 | return data; 112 | } catch (error) { 113 | await Config.remove('github_token'); 114 | } 115 | 116 | return null; 117 | }; 118 | -------------------------------------------------------------------------------- /src/cli/storefront/deploy.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import type { CommandBuilder } from 'yargs'; 3 | import { Arguments } from 'yargs'; 4 | import { CommandRemovedError, obfuscateArgv } from '../../lib/util.js'; 5 | import { Deploy } from '../../types.js'; 6 | 7 | const debug = Debug('saleor-cli:storefront:deploy'); 8 | 9 | export const command = 'deploy'; 10 | export const desc = false; 11 | // export const desc = 'Deploy this `react-storefront` to Vercel'; 12 | 13 | export const builder: CommandBuilder = (_) => 14 | _.option('dispatch', { 15 | type: 'boolean', 16 | demandOption: false, 17 | default: false, 18 | desc: 'dispatch deployment and don\'t wait till it ends', 19 | }) 20 | .option('github-prompt', { 21 | type: 'boolean', 22 | default: 'true', 23 | demandOption: false, 24 | desc: 'specify prompt presence for repository creation on Github', 25 | }) 26 | .example('saleor storefront deploy --no-github-prompt', '') 27 | .example( 28 | 'saleor storefront deploy --organization="organization-slug" --environment="env-id-or-name" --no-github-prompt', 29 | '', 30 | ); 31 | 32 | export const handler = async (argv: Arguments) => { 33 | debug('command arguments: %O', obfuscateArgv(argv)); 34 | 35 | throw new CommandRemovedError( 36 | 'This command has been removed\nPlease check documentation for `storefront` commands at https://github.com/saleor/saleor-cli/tree/main/docs', 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/cli/storefront/index.ts: -------------------------------------------------------------------------------- 1 | import * as create from './create.js'; 2 | import * as deploy from './deploy.js'; 3 | 4 | export default function (_: any) { 5 | _.command([create, deploy]).demandCommand( 6 | 1, 7 | 'You need at least one command before moving on', 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/cli/task/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useEnvironment, 3 | useOrganization, 4 | useToken, 5 | } from '../../middleware/index.js'; 6 | import * as list from './list.js'; 7 | 8 | export default function (_: any) { 9 | _.command([list]) 10 | .middleware([useToken, useOrganization, useEnvironment]) 11 | .demandCommand(1, 'You need at least one command before moving on'); 12 | } 13 | -------------------------------------------------------------------------------- /src/cli/task/list.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from '@oclif/core'; 2 | import chalk from 'chalk'; 3 | import Debug from 'debug'; 4 | import { Arguments, CommandBuilder } from 'yargs'; 5 | 6 | import { API, GET } from '../../lib/index.js'; 7 | import { contentBox, formatDateTime, obfuscateArgv } from '../../lib/util.js'; 8 | import { Options, Tasks } from '../../types.js'; 9 | 10 | const parseJobName = (name: string) => { 11 | const [_, type, env, id] = /(\w{3})-(.+)-(\w{32})/g.exec(name) || []; 12 | 13 | return { type, env, id }; 14 | }; 15 | 16 | const debug = Debug('saleor-cli:task:list'); 17 | 18 | export const command = 'list'; 19 | export const desc = 'List tasks'; 20 | 21 | export const builder: CommandBuilder = (_) => 22 | _.option('env', { type: 'string' }) 23 | .option('page', { 24 | type: 'number', 25 | demandOption: false, 26 | desc: 'A page number within the paginated result set', 27 | }) 28 | .option('page-size', { 29 | alias: 'page_size', 30 | type: 'number', 31 | demandOption: false, 32 | desc: 'Number of results to return per page', 33 | }) 34 | .option('is-blocking', { 35 | alias: 'is_blocking', 36 | type: 'boolean', 37 | demandOption: false, 38 | desc: 'Filter by non/blocking tasks', 39 | }) 40 | .option('status', { 41 | type: 'string', 42 | demandOption: false, 43 | desc: 'Filter by status: active, completed, failed, successful', 44 | }) 45 | .example('saleor task list', '') 46 | .example('saleor task list my-environment --page=2', '') 47 | .example('saleor task list my-environment --page-size=100', '') 48 | .example('saleor task list my-environment --is-blocking', '') 49 | .example('saleor task list my-environment --status=active', ''); 50 | 51 | export const handler = async (argv: Arguments) => { 52 | debug('command arguments: %O', obfuscateArgv(argv)); 53 | const _argv = argv; 54 | 55 | const params: string[] = []; 56 | 57 | ['page', 'page_size', 'is_blocking', 'status'].forEach((key: string) => { 58 | if (argv[key]) { 59 | params.push(`${key}=${argv[key]}`); 60 | } 61 | }); 62 | 63 | if (params.length > 0) { 64 | _argv.params = `?${params.join('&')}`; 65 | } 66 | 67 | const result = (await GET(API.Task, _argv)) as Tasks; 68 | 69 | if (argv.json) { 70 | console.log(JSON.stringify(result.results, null, 2)); 71 | return; 72 | } 73 | 74 | const { 75 | _: [name], 76 | } = argv; 77 | 78 | if (name === 'job') { 79 | contentBox( 80 | chalk( 81 | chalk.red('DEPRECATED'), 82 | 'please use', 83 | chalk.green('saleor task list'), 84 | 'command', 85 | ), 86 | ); 87 | } 88 | 89 | const { ux: cli } = CliUx; 90 | 91 | cli.table(result.results, { 92 | type: { 93 | header: 'Type', 94 | minWidth: 2, 95 | get: ({ job_name: jobName }) => { 96 | const { type } = parseJobName(jobName); 97 | 98 | switch (type) { 99 | case 'crt': 100 | return chalk.blue('CREATE'); 101 | case 'bkp': 102 | return chalk.blue('BACKUP'); 103 | case 'rst': 104 | return chalk.blue('RESTORE'); 105 | default: 106 | return chalk.blue(type.toUpperCase()); 107 | } 108 | }, 109 | }, 110 | env: { 111 | header: 'Environment', 112 | minWidth: 2, 113 | get: ({ job_name: jobName }) => parseJobName(jobName).env, 114 | }, 115 | created_at: { 116 | minWidth: 2, 117 | get: ({ created_at: createdAt }) => chalk.gray(formatDateTime(createdAt)), 118 | }, 119 | status: { 120 | minWidth: 2, 121 | get: ({ status }) => { 122 | switch (status) { 123 | case 'SUCCEEDED': 124 | return chalk.green(status); 125 | case 'PENDING': 126 | return chalk.yellow(status); 127 | default: 128 | return status; 129 | } 130 | }, 131 | }, 132 | job_name: { 133 | header: 'ID', 134 | minWidth: 2, 135 | get: ({ job_name: jobName }) => parseJobName(jobName).id, 136 | }, 137 | }); 138 | }; 139 | -------------------------------------------------------------------------------- /src/cli/telemetry/disable.ts: -------------------------------------------------------------------------------- 1 | import { NoCommandBuilderSetup } from '../../lib/index.js'; 2 | 3 | export const command = 'disable'; 4 | export const desc = 'Disable the telemetry'; 5 | 6 | export const builder = NoCommandBuilderSetup; 7 | 8 | export const handler = async () => { 9 | console.log( 10 | 'Telemetry is disabled permanently. This command is deprecated and will be removed.', 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/cli/telemetry/enable.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | 3 | import { NoCommandBuilderSetup } from '../../lib/index.js'; 4 | 5 | export const command = 'enable'; 6 | export const desc = 'Enable the telemetry'; 7 | 8 | export const builder = NoCommandBuilderSetup; 9 | 10 | export const handler = async () => { 11 | console.log( 12 | 'Telemetry is disabled permanently. This command is deprecated and will be removed.', 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/cli/telemetry/index.ts: -------------------------------------------------------------------------------- 1 | import * as disable from './disable.js'; 2 | import * as enable from './enable.js'; 3 | import * as status from './status.js'; 4 | 5 | export default function (_: any) { 6 | _.command([disable, enable, status]).demandCommand( 7 | 1, 8 | 'You need at least one command before moving on', 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/cli/telemetry/status.ts: -------------------------------------------------------------------------------- 1 | import { NoCommandBuilderSetup } from '../../lib/index.js'; 2 | 3 | export const command = ['status', '$0']; 4 | export const desc = 'Show the telemetry status'; 5 | 6 | export const builder = NoCommandBuilderSetup; 7 | 8 | export const handler = async () => { 9 | console.log( 10 | 'Telemetry is disabled. This command is deprecated and will be removed.', 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /src/cli/trigger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import Enquirer from 'enquirer'; 4 | import { request } from 'graphql-request'; 5 | import type { Arguments, CommandBuilder } from 'yargs'; 6 | 7 | import * as SaleorGraphQL from '../generated/graphql.js'; 8 | import { Config } from '../lib/config.js'; 9 | import { DefaultSaleorEndpoint } from '../lib/index.js'; 10 | import { capitalize, obfuscateArgv, SaleorEventError } from '../lib/util.js'; 11 | import { useAppConfig, useInstanceConnector } from '../middleware/index.js'; 12 | import { Options } from '../types.js'; 13 | 14 | const debug = Debug('saleor-cli:trigger'); 15 | 16 | export const command = 'trigger [event]'; 17 | export const desc = 'This triggers a Saleor event'; 18 | 19 | export const builder: CommandBuilder = (_) => 20 | _.option('event', { type: 'string' }).option('id', { type: 'string' }); 21 | 22 | export const handler = async (argv: Arguments) => { 23 | debug('command arguments: %O', obfuscateArgv(argv)); 24 | 25 | const { id, instance } = argv; 26 | let { event } = argv; 27 | 28 | const { 29 | __type: { enumValues }, 30 | } = await request<{ __type: { enumValues: unknown } }>( 31 | DefaultSaleorEndpoint, 32 | SaleorGraphQL.GetWebhookEventEnum, 33 | ); 34 | const choices = enumValues as Record[]; 35 | 36 | if (!event) { 37 | const prompt = new (Enquirer as any).AutoComplete({ 38 | name: 'event', 39 | message: 'Select a web hook event (start typing)', 40 | limit: 10, 41 | choices, 42 | }); 43 | 44 | event = (await prompt.run()) as string; 45 | } 46 | 47 | if (!choices.map((_) => _.name).includes(event.toUpperCase())) { 48 | throw new SaleorEventError('Wrong event name'); 49 | } 50 | 51 | const webhookName = event.toLowerCase().replaceAll('_', '-'); 52 | const operationName = webhookName 53 | .split('-') 54 | .map(capitalize) 55 | .join('') 56 | .slice(0, -1); 57 | 58 | if (!(operationName in SaleorGraphQL)) { 59 | throw new SaleorEventError('Operation not implemented'); 60 | } 61 | 62 | console.log( 63 | `\n GraphQL Operation for ${chalk.underline(event)} available\n`, 64 | ); 65 | 66 | const headers = await Config.getBearerHeader(); 67 | 68 | // FIXME 69 | // check if CLI input matches the GraphQL signature 70 | // e.g. if `id` is required or not 71 | 72 | try { 73 | const result = await request( 74 | instance, 75 | (SaleorGraphQL as any)[operationName], 76 | { 77 | id, 78 | input: {}, 79 | }, 80 | headers, 81 | ); 82 | 83 | console.log(result); 84 | } catch (error: any) { 85 | const { 86 | response: { errors }, 87 | } = error; 88 | 89 | for (const { message } of errors) { 90 | console.error(message); 91 | } 92 | } 93 | 94 | process.exit(0); 95 | }; 96 | 97 | export const middlewares = [useAppConfig, useInstanceConnector]; 98 | -------------------------------------------------------------------------------- /src/cli/vercel/index.ts: -------------------------------------------------------------------------------- 1 | import * as login from './login.js'; 2 | 3 | export default function (_: any) { 4 | _.command([login]).demandCommand( 5 | 1, 6 | 'You need at least one command before moving on', 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/cli/vercel/login.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import chalk from 'chalk'; 3 | import Debug from 'debug'; 4 | import got from 'got'; 5 | import { nanoid } from 'nanoid'; 6 | import { ServerApp } from 'retes'; 7 | import { Response } from 'retes/response'; 8 | import { GET } from 'retes/route'; 9 | 10 | import { Config, SaleorCLIPort } from '../../lib/config.js'; 11 | import { checkPort } from '../../lib/detectPort.js'; 12 | import { NoCommandBuilderSetup } from '../../lib/index.js'; 13 | import { delay, openURL, printlnSuccess, successPage } from '../../lib/util.js'; 14 | 15 | const RedirectURI = `http://localhost:${SaleorCLIPort}/vercel/callback`; 16 | 17 | const debug = Debug('saleor-cli:vercel:login'); 18 | 19 | export const command = 'login'; 20 | export const desc = 'Add integration for Saleor CLI'; 21 | 22 | export const builder = NoCommandBuilderSetup; 23 | 24 | export const handler = async () => { 25 | await checkPort(SaleorCLIPort); 26 | 27 | const generatedState = nanoid(); 28 | const emitter = new EventEmitter(); 29 | 30 | const { VercelClientID, VercelClientSecret } = await Config.get(); 31 | 32 | // const spinner = ora('\nLogging in...').start(); 33 | // await delay(1500); 34 | // spinner.text = '\nLogging in...\n'; 35 | 36 | const QueryParams = new URLSearchParams({ state: generatedState }); 37 | const url = `https://vercel.com/integrations/saleor-cli/new?${QueryParams}`; 38 | await openURL(url); 39 | 40 | const app = new ServerApp([ 41 | GET('/vercel/callback', async ({ params }) => { 42 | const { state, code } = params; 43 | 44 | if (state !== generatedState) { 45 | return Response.BadRequest('Wrong state'); 46 | } 47 | 48 | const Params = { 49 | client_id: VercelClientID, 50 | client_secret: VercelClientSecret, 51 | code, 52 | redirect_uri: RedirectURI, 53 | }; 54 | 55 | try { 56 | const data: any = await got 57 | .post('https://api.vercel.com/v2/oauth/access_token', { 58 | form: Params, 59 | }) 60 | .json(); 61 | 62 | const { access_token: accessToken, team_id: teamId } = data; 63 | 64 | await Config.set('vercel_token', `Bearer ${accessToken}`); 65 | await Config.set('vercel_team_id', teamId); 66 | printlnSuccess(chalk.bold('success')); 67 | } catch (error: any) { 68 | console.log(error.message); 69 | console.log( 70 | chalk( 71 | 'Tip: in some cases', 72 | chalk.green('saleor logout'), 73 | 'followed by', 74 | chalk.green('saleor login'), 75 | 'may help', 76 | ), 77 | ); 78 | 79 | emitter.emit('finish'); 80 | 81 | return { 82 | body: successPage('Login failed!'), 83 | status: 200, 84 | type: 'text/html', 85 | headers: { 86 | 'Content-Type': 'text/html; charset=utf-8', 87 | }, 88 | }; 89 | } 90 | 91 | // spinner.succeed(`You've successfully logged into Saleor Cloud!\n Your access token has been safely stored, and you're ready to go`) 92 | emitter.emit('finish'); 93 | 94 | return { 95 | body: successPage( 96 | 'You\'ve successfully authenticated Vercel with the Saleor CLI!', 97 | ), 98 | status: 200, 99 | type: 'text/html', 100 | headers: { 101 | 'Content-Type': 'text/html; charset=utf-8', 102 | }, 103 | }; 104 | }), 105 | ]); 106 | await app.start(SaleorCLIPort); 107 | 108 | emitter.on('finish', async () => { 109 | await delay(1500); 110 | await app.stop(); 111 | }); 112 | }; 113 | -------------------------------------------------------------------------------- /src/cli/webhook/dryRun.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import Enquirer from 'enquirer'; 4 | import got, { HTTPError } from 'got'; 5 | import { print } from 'graphql'; 6 | import { Arguments, CommandBuilder } from 'yargs'; 7 | 8 | import { WebhookDryRun } from '../../generated/graphql.js'; 9 | import { Config } from '../../lib/config.js'; 10 | import { obfuscateArgv, println } from '../../lib/util.js'; 11 | import { 12 | WebhookDryRun as WebhookDryRunArgs, 13 | WebhookError, 14 | } from '../../types.js'; 15 | 16 | const debug = Debug('saleor-cli:webhook:create'); 17 | 18 | export const command = 'dry-run'; 19 | export const desc = 'Webhook dry run'; 20 | 21 | export const builder: CommandBuilder = (_) => 22 | _.option('object-id', { 23 | type: 'string', 24 | demandOption: false, 25 | desc: 'Object ID to perform dry run on', 26 | }) 27 | .option('query', { 28 | type: 'string', 29 | desc: 'Subscription query', 30 | }) 31 | .example('saleor webhook dry-run', '') 32 | .example( 33 | 'saleor webhook dry-run --query=\'subscription { event { ... on ProductCreated { product { id name } } } }\'', 34 | '', 35 | ) 36 | .example('saleor webhook dry-run --object-id=\'UHJvZHVjdDo3Mg==\'', ''); 37 | 38 | export const handler = async (argv: Arguments) => { 39 | debug('command arguments: %O', obfuscateArgv(argv)); 40 | 41 | const { objectId, query } = await Enquirer.prompt<{ 42 | objectId: string; 43 | query: string; 44 | }>([ 45 | { 46 | type: 'input', 47 | name: 'objectId', 48 | message: 'Object ID to perform Dry Run on', 49 | initial: argv.objectId, 50 | required: true, 51 | skip: !!argv.objectId, 52 | }, 53 | { 54 | type: 'input', 55 | name: 'query', 56 | message: 'Subscription query', 57 | initial: 58 | argv.query || 59 | 'subscription { event { ... on ProductCreated { product { id name } } } }', 60 | required: true, 61 | skip: !!argv.query, 62 | }, 63 | ]); 64 | 65 | const headers = await Config.getBearerHeader(); 66 | 67 | try { 68 | const { data }: any = await got 69 | .post(argv.instance, { 70 | headers, 71 | json: { 72 | query: print(WebhookDryRun), 73 | variables: { 74 | objectId, 75 | query, 76 | }, 77 | }, 78 | }) 79 | .json(); 80 | 81 | const { webhookDryRun } = data; 82 | 83 | println(''); 84 | 85 | if (!webhookDryRun) { 86 | println(chalk.red('Couldn\'t find the provided object')); 87 | return; 88 | } 89 | 90 | const { payload, errors = [] } = webhookDryRun ?? {}; 91 | 92 | if (errors.length) { 93 | throw new Error( 94 | errors.map((e: WebhookError) => `\n ${e.field} - ${e.message}`).join(), 95 | ); 96 | } 97 | 98 | println(payload); 99 | } catch (error) { 100 | if (error instanceof HTTPError) { 101 | const { statusCode } = error.response; 102 | if (statusCode === 400) { 103 | throw new Error( 104 | 'Seems the selected environment doesn\'t support dry run feature. Required Saleor Version ^3.11', 105 | ); 106 | } 107 | } 108 | 109 | throw error; 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /src/cli/webhook/edit.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import Debug from 'debug'; 3 | import Enquirer from 'enquirer'; 4 | import got from 'got'; 5 | import { print } from 'graphql'; 6 | import type { Arguments } from 'yargs'; 7 | 8 | import { WebhookUpdate } from '../../generated/graphql.js'; 9 | import { Config } from '../../lib/config.js'; 10 | import { NoCommandBuilderSetup } from '../../lib/index.js'; 11 | import { 12 | obfuscateArgv, 13 | printlnSuccess, 14 | validatePresence, 15 | } from '../../lib/util.js'; 16 | import { 17 | interactiveSaleorApp, 18 | interactiveWebhook, 19 | } from '../../middleware/index.js'; 20 | import { Options } from '../../types.js'; 21 | 22 | const debug = Debug('saleor-cli:webhook:edit'); 23 | 24 | export const command = 'edit'; 25 | export const desc = 'Edit a webhook'; 26 | 27 | export const builder = NoCommandBuilderSetup; 28 | 29 | export const handler = async (argv: Arguments) => { 30 | debug('command arguments: %O', obfuscateArgv(argv)); 31 | const { environment, webhookID, instance } = argv; 32 | const headers = await Config.getBearerHeader(); 33 | 34 | const query = `query getWebhook($id: ID!) { 35 | webhook(id: $id) { 36 | name 37 | targetUrl 38 | secretKey 39 | } 40 | }`; 41 | 42 | interface WebHook { 43 | webhook: { 44 | name: string; 45 | targetUrl: string; 46 | secretKey: string; 47 | }; 48 | } 49 | 50 | const { 51 | data: { webhook }, 52 | } = await got 53 | .post(instance, { 54 | headers, 55 | json: { 56 | query, 57 | variables: { 58 | id: webhookID, 59 | }, 60 | }, 61 | }) 62 | .json<{ data: WebHook }>(); 63 | 64 | console.log(` Editing the webhook for the ${environment} environment`); 65 | 66 | const form = await Enquirer.prompt<{ 67 | name: string; 68 | targetUrl: string; 69 | secretKey: string; 70 | }>([ 71 | { 72 | type: 'input', 73 | name: 'name', 74 | message: 'Name', 75 | initial: webhook.name, 76 | required: true, 77 | validate: (value) => validatePresence(value), 78 | }, 79 | { 80 | type: 'input', 81 | name: 'targetUrl', 82 | message: 'Target URL', 83 | initial: webhook.targetUrl, 84 | required: true, 85 | validate: (value) => validatePresence(value), 86 | }, 87 | { 88 | type: 'input', 89 | name: 'secretKey', 90 | message: 'Secret', 91 | initial: webhook.secretKey, 92 | }, 93 | ]); 94 | 95 | await got 96 | .post(instance, { 97 | headers, 98 | json: { 99 | query: print(WebhookUpdate), 100 | variables: { 101 | id: webhookID, 102 | input: { 103 | ...form, 104 | }, 105 | }, 106 | }, 107 | }) 108 | .json(); 109 | 110 | printlnSuccess(chalk.bold('Webhook updated')); 111 | 112 | process.exit(0); 113 | }; 114 | 115 | export const middlewares = [interactiveSaleorApp, interactiveWebhook]; 116 | -------------------------------------------------------------------------------- /src/cli/webhook/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | useAppConfig, 3 | useAvailabilityChecker, 4 | useInstanceConnector, 5 | } from '../../middleware/index.js'; 6 | import * as create from './create.js'; 7 | import * as dryRun from './dryRun.js'; 8 | import * as edit from './edit.js'; 9 | import * as list from './list.js'; 10 | import * as update from './update.js'; 11 | 12 | export default function (_: any) { 13 | _.command([list, create, edit, update, dryRun]) 14 | .middleware([useAppConfig, useInstanceConnector, useAvailabilityChecker]) 15 | .demandCommand(1, 'You need at least one command before moving on'); 16 | } 17 | -------------------------------------------------------------------------------- /src/cli/webhook/list.ts: -------------------------------------------------------------------------------- 1 | import { CliUx } from '@oclif/core'; 2 | import chalk from 'chalk'; 3 | import Debug from 'debug'; 4 | import got from 'got'; 5 | import { print } from 'graphql'; 6 | import { Arguments } from 'yargs'; 7 | 8 | import { WebhookList } from '../../generated/graphql.js'; 9 | import { Config } from '../../lib/config.js'; 10 | import { getAppsFromResult, obfuscateArgv } from '../../lib/util.js'; 11 | import { Options } from '../../types.js'; 12 | 13 | const debug = Debug('saleor-cli:webhook:list'); 14 | 15 | export const command = 'list'; 16 | export const desc = 'List webhooks for an environment'; 17 | 18 | export const handler = async (argv: Arguments) => { 19 | debug('command arguments: %O', obfuscateArgv(argv)); 20 | const { instance, json } = argv; 21 | const headers = await Config.getBearerHeader(); 22 | 23 | const { data }: any = await got 24 | .post(instance, { 25 | headers, 26 | json: { 27 | query: print(WebhookList), 28 | }, 29 | }) 30 | .json(); 31 | 32 | const apps = getAppsFromResult(data, json); 33 | 34 | const webhookList = apps.map((app: any) => app.node.webhooks).flat(); 35 | 36 | if (webhookList.length === 0) { 37 | console.log( 38 | chalk.red('\n', ' No webhooks found for this environment', '\n'), 39 | ); 40 | 41 | console.log( 42 | chalk( 43 | ' Create webhook with', 44 | chalk.green('saleor webhook create'), 45 | 'command', 46 | ), 47 | ); 48 | 49 | process.exit(0); 50 | } 51 | 52 | if (json) { 53 | console.log(JSON.stringify(webhookList, null, 2)); 54 | return; 55 | } 56 | 57 | for (const { 58 | node: { name: appName, webhooks }, 59 | } of apps) { 60 | if (webhooks.length === 0) { 61 | continue; 62 | } 63 | 64 | console.log(chalk('\n App:', chalk.bold(appName), '\n')); 65 | 66 | const { ux: cli } = CliUx; 67 | 68 | cli.table(webhooks, { 69 | id: { 70 | header: 'ID', 71 | minWidth: 2, 72 | get: ({ id }) => chalk.gray(id), 73 | }, 74 | name: { 75 | header: 'Name', 76 | minWidth: 2, 77 | get: ({ name }) => chalk.cyan(name), 78 | }, 79 | targetUrl: { 80 | header: 'URL', 81 | get: ({ targetUrl }) => chalk.yellow(targetUrl), 82 | }, 83 | isActive: { 84 | header: 'Active?', 85 | minWidth: 2, 86 | get: ({ isActive }) => 87 | isActive ? chalk.green('Yes') : chalk.red('No'), 88 | }, 89 | syncEvents: { 90 | header: 'Sync', 91 | minWidth: 2, 92 | get: ({ syncEvents }) => (syncEvents as string[]).length, 93 | }, 94 | asyncEvents: { 95 | header: 'Async', 96 | minWidth: 2, 97 | get: ({ asyncEvents }) => (asyncEvents as string[]).length, 98 | }, 99 | }); 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/cli/webhook/update.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug'; 2 | import Enquirer from 'enquirer'; 3 | import got from 'got'; 4 | import { print } from 'graphql'; 5 | import ora from 'ora'; 6 | import { Arguments } from 'yargs'; 7 | 8 | import { WebhookList, WebhookUpdate } from '../../generated/graphql.js'; 9 | import { Config } from '../../lib/config.js'; 10 | import { getAppsFromResult, obfuscateArgv } from '../../lib/util.js'; 11 | import { Options } from '../../types.js'; 12 | 13 | const debug = Debug('saleor-cli:webhook:update'); 14 | 15 | export const command = 'update'; 16 | export const desc = 'Update webhooks for an environment'; 17 | 18 | export const handler = async (argv: Arguments) => { 19 | debug('command arguments: %O', obfuscateArgv(argv)); 20 | await updateWebhook(argv.instance, argv.json); 21 | }; 22 | 23 | export const updateWebhook = async ( 24 | instance: string, 25 | json: boolean | undefined, 26 | ) => { 27 | const headers = await Config.getBearerHeader(); 28 | 29 | const { data }: any = await got 30 | .post(instance, { 31 | headers, 32 | json: { 33 | query: print(WebhookList), 34 | }, 35 | }) 36 | .json(); 37 | 38 | const apps = getAppsFromResult(data, json); 39 | 40 | const { updateAll } = await Enquirer.prompt<{ updateAll: string }>({ 41 | type: 'confirm', 42 | name: 'updateAll', 43 | message: 'Would you like to replace domain for all webhooks targetUrl', 44 | }); 45 | 46 | if (updateAll) { 47 | const { webhooksDomain } = await Enquirer.prompt<{ 48 | webhooksDomain: string; 49 | }>({ 50 | type: 'input', 51 | name: 'webhooksDomain', 52 | message: 'Domain', 53 | initial: 'http://localhost:3000', 54 | }); 55 | 56 | const spinner = ora('Updating...').start(); 57 | 58 | for (const { 59 | node: { webhooks }, 60 | } of apps) { 61 | for (const { id, targetUrl } of webhooks) { 62 | const url = new URL(targetUrl); 63 | const newTargetUrl = `${webhooksDomain}${url.pathname}`; 64 | await runUpdateWebhook(headers, instance, id, newTargetUrl); 65 | } 66 | } 67 | 68 | spinner.succeed('Yay! Webhooks updated'); 69 | } else { 70 | for (const { 71 | node: { webhooks, name: appName }, 72 | } of apps) { 73 | for (const { id, targetUrl, name } of webhooks) { 74 | const { newTargetUrl } = await Enquirer.prompt<{ 75 | newTargetUrl: string; 76 | }>({ 77 | type: 'input', 78 | name: 'newTargetUrl', 79 | message: `App: ${appName}, webhook: ${name} - ${targetUrl}`, 80 | initial: targetUrl, 81 | }); 82 | 83 | const spinner = ora('Updating...').start(); 84 | await runUpdateWebhook(headers, instance, id, newTargetUrl); 85 | spinner.succeed('Updated'); 86 | } 87 | } 88 | } 89 | }; 90 | 91 | const runUpdateWebhook = async ( 92 | headers: Record, 93 | instance: string, 94 | id: string, 95 | targetUrl: string | null, 96 | ) => { 97 | const { errors }: any = await got 98 | .post(instance, { 99 | headers, 100 | json: { 101 | query: print(WebhookUpdate), 102 | variables: { 103 | input: { 104 | targetUrl, 105 | }, 106 | id, 107 | }, 108 | }, 109 | }) 110 | .json(); 111 | 112 | if (errors) { 113 | throw Error('cannot auth'); 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const SaleorStorefrontRepo = 'saleor/storefront'; 2 | export const SaleorStorefrontDefaultBranch = 'canary'; 3 | 4 | export const SaleorAppDefaultBranch = 'main'; 5 | export const SaleorAppRepo = 'saleor/saleor-app-template'; 6 | -------------------------------------------------------------------------------- /src/graphql/mutation/DoAppCreate.graphql: -------------------------------------------------------------------------------- 1 | mutation AppCreate($name: String, $permissions: [PermissionEnum!]) { 2 | appCreate(input: { name: $name, permissions: $permissions }) { 3 | app { 4 | id 5 | name 6 | } 7 | errors { 8 | field 9 | message 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/graphql/mutation/DoAppDelete.graphql: -------------------------------------------------------------------------------- 1 | mutation AppDelete($app: ID!) { 2 | appDelete(id: $app) { 3 | app { 4 | id 5 | name 6 | } 7 | errors { 8 | field 9 | message 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/graphql/mutation/DoAppInstall.graphql: -------------------------------------------------------------------------------- 1 | mutation AppInstall( 2 | $name: String! 3 | $manifestURL: String! 4 | $permissions: [PermissionEnum!] 5 | ) { 6 | appInstall( 7 | input: { 8 | appName: $name 9 | manifestUrl: $manifestURL 10 | permissions: $permissions 11 | } 12 | ) { 13 | appInstallation { 14 | id 15 | status 16 | appName 17 | manifestUrl 18 | } 19 | errors { 20 | field 21 | message 22 | code 23 | permissions 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/graphql/mutation/DoAppTokenCreate.graphql: -------------------------------------------------------------------------------- 1 | mutation AppTokenCreate($app: ID!) { 2 | appTokenCreate(input: { app: $app }) { 3 | authToken 4 | errors { 5 | field 6 | code 7 | message 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/graphql/mutation/DoAppUpdate.graphql: -------------------------------------------------------------------------------- 1 | mutation AppUpdate($app: ID!, $permissions: [PermissionEnum!]) { 2 | appUpdate(id: $app, input: { permissions: $permissions }) { 3 | app { 4 | id 5 | } 6 | errors { 7 | field 8 | message 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/graphql/mutation/DoProductUpdate.graphql: -------------------------------------------------------------------------------- 1 | mutation ProductUpdate($id: ID!, $input: ProductInput!) { 2 | productUpdate(id: $id, input: $input) { 3 | product { 4 | name 5 | } 6 | errors { 7 | field 8 | code 9 | message 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/graphql/mutation/DoWebhookCreate.graphql: -------------------------------------------------------------------------------- 1 | mutation WebhookCreate($input: WebhookCreateInput!) { 2 | webhookCreate(input: $input) { 3 | webhook { 4 | id 5 | } 6 | errors { 7 | field 8 | message 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/graphql/mutation/DoWebhookDryRun.graphql: -------------------------------------------------------------------------------- 1 | mutation WebhookDryRun($objectId: ID!, $query: String!) { 2 | webhookDryRun(objectId: $objectId, query: $query) { 3 | payload 4 | errors { 5 | field 6 | message 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/graphql/mutation/DoWebhookUpdate.graphql: -------------------------------------------------------------------------------- 1 | mutation WebhookUpdate($id: ID!, $input: WebhookUpdateInput!) { 2 | webhookUpdate(id: $id, input: $input) { 3 | webhook { 4 | id 5 | } 6 | errors { 7 | field 8 | code 9 | message 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/graphql/query/GetAppByID.graphql: -------------------------------------------------------------------------------- 1 | query GetAppByID($appID: ID!) { 2 | app(id: $appID) { 3 | id 4 | name 5 | isActive 6 | type 7 | webhooks { 8 | id 9 | name 10 | isActive 11 | targetUrl 12 | syncEvents { 13 | eventType 14 | } 15 | asyncEvents { 16 | eventType 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/graphql/query/GetApps.graphql: -------------------------------------------------------------------------------- 1 | query GetApps { 2 | apps(first: 100, sortBy: { field: CREATION_DATE, direction: DESC }) { 3 | totalCount 4 | edges { 5 | node { 6 | id 7 | name 8 | isActive 9 | type 10 | created 11 | webhooks { 12 | id 13 | } 14 | permissions { 15 | code 16 | name 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/graphql/query/GetAppsInstallations.graphql: -------------------------------------------------------------------------------- 1 | query AppsInstallations { 2 | appsInstallations { 3 | id 4 | manifestUrl 5 | appName 6 | message 7 | status 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/graphql/query/GetPermissionEnum.graphql: -------------------------------------------------------------------------------- 1 | query GetPermissionEnum { 2 | __type(name: "PermissionEnum") { 3 | enumValues { 4 | name 5 | description 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/graphql/query/GetWebhookAsyncEventEnum.graphql: -------------------------------------------------------------------------------- 1 | query GetWebhookAsyncEventEnum { 2 | __type(name: "WebhookEventTypeAsyncEnum") { 3 | enumValues { 4 | name 5 | description 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/graphql/query/GetWebhookEventEnum.graphql: -------------------------------------------------------------------------------- 1 | query GetWebhookEventEnum { 2 | __type(name: "WebhookEventTypeEnum") { 3 | enumValues { 4 | name 5 | description 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/graphql/query/GetWebhookList.graphql: -------------------------------------------------------------------------------- 1 | query WebhookList { 2 | apps(first: 100) { 3 | totalCount 4 | edges { 5 | node { 6 | id 7 | name 8 | isActive 9 | type 10 | webhooks { 11 | id 12 | name 13 | isActive 14 | targetUrl 15 | syncEvents { 16 | eventType 17 | } 18 | asyncEvents { 19 | eventType 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/graphql/query/GetWebhookSyncEventEnum.graphql: -------------------------------------------------------------------------------- 1 | query GetWebhookSyncEventEnum { 2 | __type(name: "WebhookEventTypeSyncEnum") { 3 | enumValues { 4 | name 5 | description 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/graphql/query/Introspection.graphql: -------------------------------------------------------------------------------- 1 | query Introspection { 2 | __schema { 3 | __typename 4 | } 5 | } -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import path from 'path'; 3 | import fs from 'fs-extra'; 4 | 5 | const DefaultConfigFile = path.join(os.homedir(), '.config', 'saleor.json'); 6 | 7 | export const SaleorCLIPort = 5375; 8 | 9 | export type ConfigField = 10 | | 'token' 11 | | 'user_session' 12 | | 'id_token' 13 | | 'access_token' 14 | | 'refresh_token' 15 | | 'organization_slug' 16 | | 'organization_name' 17 | | 'environment_id' 18 | | 'vercel_token' 19 | | 'vercel_team_id' 20 | | 'saleor_env' 21 | | 'cloud_api_url' 22 | | 'cloud_api_auth_domain' 23 | | 'TunnelServerSecret' 24 | | 'VercelClientID' 25 | | 'VercelClientSecret' 26 | | 'SentryDSN' 27 | | 'GithubClientID' 28 | | 'github_token' 29 | | 'lastUpdateCheck'; 30 | 31 | type CacheItem = Record; 32 | 33 | type ConfigProps = Record & Record<'cache', CacheItem>; 34 | 35 | const set = async (field: ConfigField, value: string) => { 36 | await fs.ensureFile(DefaultConfigFile); 37 | const content = await fs.readJSON(DefaultConfigFile, { throws: false }); 38 | 39 | const newContent = { ...content, [field]: value }; 40 | await fs.outputJSON(DefaultConfigFile, newContent, { spaces: '\t' }); 41 | 42 | return newContent; 43 | }; 44 | 45 | const remove = async (field: ConfigField) => { 46 | await fs.ensureFile(DefaultConfigFile); 47 | const content = 48 | (await fs.readJSON(DefaultConfigFile, { throws: false })) || {}; 49 | 50 | delete content[field]; 51 | await fs.outputJSON(DefaultConfigFile, content, { spaces: '\t' }); 52 | 53 | return content; 54 | }; 55 | 56 | const get = async (): Promise => { 57 | await fs.ensureFile(DefaultConfigFile); 58 | const content = await fs.readJSON(DefaultConfigFile, { throws: false }); 59 | return content || {}; 60 | }; 61 | 62 | const reset = async (): Promise => { 63 | await fs.outputJSON(DefaultConfigFile, {}); 64 | }; 65 | 66 | const getBearerHeader = async (): Promise> => { 67 | const { token } = await get(); 68 | 69 | if (token) { 70 | return { 'Authorization-Bearer': token.split(' ').slice(-1)[0] }; 71 | } 72 | 73 | throw new Error('\nYou are not logged in\n'); 74 | }; 75 | 76 | const appendCache = async (key: string, value: string) => { 77 | const content = await get(); 78 | const cache = content.cache || {}; 79 | 80 | cache[key] = [...(cache[key] || []), value]; 81 | 82 | const newContent = { ...content, cache }; 83 | await fs.outputJSON(DefaultConfigFile, newContent, { spaces: '\t' }); 84 | 85 | return newContent; 86 | }; 87 | 88 | export const Config = { get, set, appendCache, reset, remove, getBearerHeader }; 89 | -------------------------------------------------------------------------------- /src/lib/detectPort.ts: -------------------------------------------------------------------------------- 1 | import detectPort from 'detect-port'; 2 | 3 | export const isPortAvailable = async (port: number): Promise => { 4 | const detectedPort = await detectPort(port); 5 | 6 | return port === detectedPort; 7 | }; 8 | 9 | export const checkPort = async (port: number): Promise => { 10 | if (!(await isPortAvailable(port))) { 11 | throw new Error( 12 | '\nSomething is already running at port 3000\nPlease release port 3000 and try again', 13 | ); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/download.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import fs from 'fs-extra'; 3 | import GitURLParse from 'git-url-parse'; 4 | import { simpleGit } from 'simple-git'; 5 | 6 | import { WrongGitURLError } from './util.js'; 7 | 8 | const git = simpleGit(); 9 | 10 | /* eslint-disable import/prefer-default-export */ 11 | export const gitCopy = async (repo: string, dest: string, ref = 'main') => { 12 | try { 13 | const { href: repoURL } = GitURLParse(repo); 14 | await git.clone(repoURL, dest, { '--branch': ref, '--depth': 1 }); 15 | await fs.remove(`${dest}/.git`); 16 | } catch (error) { 17 | throw new WrongGitURLError(`Provided Git URL is invalid: ${repo}`); 18 | } 19 | }; 20 | 21 | export const gitCopySHA = async (repo: string, dest: string, sha: string) => { 22 | try { 23 | const { href: repoURL } = GitURLParse(repo); 24 | await git.clone(repoURL, dest).cwd({ path: dest }); 25 | const target = join(process.cwd(), dest); 26 | await git.cwd({ path: target, root: true }); 27 | await git.reset(['--hard', sha]); 28 | await fs.remove(`${dest}/.git`); 29 | } catch (error) { 30 | throw new WrongGitURLError(`Provided Git URL is invalid: ${repo}`); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/lib/environment.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { rest } from 'msw'; 3 | import { setupServer } from 'msw/node'; 4 | import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; 5 | import { Arguments } from 'yargs'; 6 | 7 | import { Options } from '../types.js'; 8 | import { getEnvironment } from './environment.js'; 9 | 10 | const environment = { 11 | name: 'Test environment', 12 | slug: 'test-environment', 13 | domain: 'test-environment.test.saleor.cloud', 14 | }; 15 | 16 | const argv = { 17 | _: ['test'], 18 | $0: 'saleor', 19 | token: 'XYZ', 20 | organization: 'test_org', 21 | environment: 'test', 22 | instance: 'https://test-environment.test.saleor.cloud/graphql/', 23 | } as Arguments; 24 | 25 | const handlers = [ 26 | rest.get('https://rest.example/path/to/posts', (req, res, ctx) => 27 | res(ctx.status(200), ctx.json(environment)), 28 | ), 29 | rest.get( 30 | `https://cloud.saleor.io/platform/api/organizations/${argv.organization}/environments/${argv.environment}`, 31 | (req, res, ctx) => res(ctx.status(200), ctx.json(environment)), 32 | ), 33 | ]; 34 | 35 | const server = setupServer(...handlers); 36 | server.printHandlers(); 37 | 38 | beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); 39 | afterAll(() => server.close()); 40 | afterEach(() => server.resetHandlers()); 41 | 42 | describe('getEnvironment', () => { 43 | it('should return the environment object from the Cloud API', async () => { 44 | const response = await getEnvironment(argv); 45 | 46 | expect(response).toEqual(environment); 47 | }); 48 | }); 49 | 50 | describe('get Saleor API instance', () => { 51 | it('should return the environment\'s graphql instance', async () => { 52 | const { instance } = argv; 53 | 54 | expect(instance).toEqual( 55 | 'https://test-environment.test.saleor.cloud/graphql/', 56 | ); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/lib/environment.ts: -------------------------------------------------------------------------------- 1 | import { Arguments } from 'yargs'; 2 | 3 | import Enquirer from 'enquirer'; 4 | import { API, GET } from '../lib/index.js'; 5 | import { Environment, Options } from '../types.js'; 6 | import { validateURL } from './util.js'; 7 | 8 | export const getEnvironment = async (argv: Arguments) => 9 | GET(API.Environment, argv) as Promise; 10 | 11 | export const promptOrigin = async () => { 12 | const form = await Enquirer.prompt<{ 13 | origin: string; 14 | addMore: boolean; 15 | }>([ 16 | { 17 | type: 'input', 18 | name: 'origin', 19 | message: 'Origin', 20 | validate: (value) => validateURL(value), 21 | }, 22 | { 23 | type: 'confirm', 24 | name: 'addMore', 25 | message: 'Add another one?', 26 | }, 27 | ]); 28 | 29 | return { 30 | origin: new URL(form.origin).origin, 31 | addMore: form.addMore, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/git.ts: -------------------------------------------------------------------------------- 1 | import Enquirer from 'enquirer'; 2 | import got from 'got'; 3 | import { Ora } from 'ora'; 4 | import { simpleGit } from 'simple-git'; 5 | 6 | import { Config } from './config.js'; 7 | 8 | interface RepositoryContent { 9 | name: string; 10 | sha: string; 11 | path: string; 12 | git_url: string; 13 | url: string; 14 | html_url: string; 15 | } 16 | 17 | export const getRepositoryContent = async (repoPath: string) => { 18 | const { github_token: GitHubToken } = await Config.get(); 19 | 20 | const data: RepositoryContent[] = await got 21 | .get(repoPath, { 22 | headers: { 23 | Accept: 'application/vnd.github+json', 24 | Authorization: GitHubToken, 25 | }, 26 | }) 27 | .json(); 28 | 29 | return data; 30 | }; 31 | 32 | export const getExampleSHA = async (example: string) => { 33 | const examples = await getRepositoryContent( 34 | 'https://api.github.com/repos/saleor/app-examples/contents/examples', 35 | ); 36 | const filtered = examples.filter((e) => e.name === example); 37 | 38 | if (filtered.length === 0) { 39 | const choices = examples.map((e) => ({ 40 | name: e.sha, 41 | message: e.name.split('-').join(' '), 42 | })); 43 | 44 | const { sha } = await Enquirer.prompt<{ sha: string }>({ 45 | type: 'select', 46 | name: 'sha', 47 | required: true, 48 | choices, 49 | message: 'Choose the app example', 50 | }); 51 | 52 | return sha; 53 | } 54 | 55 | const { sha } = filtered[0]; 56 | 57 | return sha; 58 | }; 59 | 60 | export const setupGitRepository = async ( 61 | spinner: Ora | undefined = undefined, 62 | ): Promise => { 63 | if (spinner) { 64 | spinner.text = 'Setting up the Git repository...'; // eslint-disable-line no-param-reassign 65 | } 66 | 67 | const git = simpleGit(); 68 | 69 | // For compatibility with older versions of Git where init --initial-branch is not supported 70 | // await git.init(['--initial-branch', 'main']); 71 | await git.init(); 72 | const refCmd = process.platform === 'win32' ? 'update-ref' : 'symbolic-ref'; 73 | await git.raw(refCmd, 'HEAD', 'refs/heads/main'); 74 | 75 | await git.add('.'); 76 | await git.commit('Initial commit from Saleor CLI'); 77 | }; 78 | -------------------------------------------------------------------------------- /src/lib/images.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | const primaryColor = chalk.blue; 4 | const secondaryColor = chalk.blue; 5 | 6 | /* eslint-disable import/prefer-default-export */ 7 | export const header = (version: string) => { 8 | console.log(` 9 | ${primaryColor( 10 | '░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░', 11 | )} 12 | ${primaryColor( 13 | '░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░', 14 | )} 15 | ${secondaryColor( 16 | '▄█████████████████████████████████████████▀', 17 | )}${primaryColor('░░░░░░░░░░░░░░░░')} 18 | ${secondaryColor('▄███▀')} ${primaryColor( 19 | '░░░░░░░░░░░░░░░', 20 | )} ${secondaryColor('▄███▀')}${primaryColor('░░░░░░░░░░░░░░░')} 21 | ${secondaryColor('▄███▀')} ${primaryColor( 22 | '░░░░░░░░░░░░░░░░░', 23 | )}${secondaryColor('▄███▀')}${primaryColor('░░░░░░░░░░░░░░')} 24 | ${secondaryColor('▄███▀')} ${primaryColor( 25 | '░░░░░░░░░░░░░░░░░░', 26 | )}${secondaryColor('▄███▀')}${primaryColor('░░░░░░░░░░░░')} 27 | ${secondaryColor('▄███▀')} ${secondaryColor( 28 | '▄███▀', 29 | )} 30 | ${secondaryColor( 31 | '▄█████████████████████████████████████████▀', 32 | )} Saleor Commerce CLI v${version} 33 | `); 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/instance.ts: -------------------------------------------------------------------------------- 1 | import ora from 'ora'; 2 | import { print } from 'graphql'; 3 | import { got } from 'got'; 4 | 5 | import { API, GET } from '../lib/index.js'; 6 | import { Environment, Options, Organization } from '../types.js'; 7 | import { Introspection } from '../generated/graphql.js'; 8 | 9 | export const validateInstance = async (instance: string) => { 10 | try { 11 | // verify if instance is a valid URL 12 | const { href } = new URL(instance); 13 | const instanceURL = href; 14 | 15 | // verify if instance is a valid GraphQL endpoint 16 | await got 17 | .post(instanceURL, { 18 | json: { 19 | query: print(Introspection), 20 | }, 21 | }) 22 | .json(); 23 | 24 | return instanceURL; 25 | } catch (error) { 26 | throw new Error('Provided URL is not a valid GraphQL endpoint'); 27 | } 28 | }; 29 | 30 | export const extractDataFromInstance = async (argv: Options) => { 31 | const spinner = ora('Getting instance details...').start(); 32 | const { instance } = argv; 33 | 34 | const { host } = new URL(instance ?? ''); 35 | 36 | const organizations = (await GET(API.Organization, { 37 | ...argv, 38 | organization: '', 39 | })) as Organization[]; 40 | 41 | for (const organization of organizations) { 42 | try { 43 | const environments = (await GET(API.Environment, { 44 | ...argv, 45 | organization: organization.slug, 46 | environment: '', 47 | })) as Environment[]; 48 | 49 | if (environments.map((e) => e.domain).includes(host)) { 50 | const environment = environments.filter((e) => e.domain === host)[0]; 51 | 52 | spinner.succeed(); 53 | return { 54 | organization: organization.slug, 55 | environment: environment.key, 56 | }; 57 | } 58 | } catch (error) { 59 | spinner.fail(); 60 | throw new Error('There was a problem while fetching environment'); 61 | } 62 | } 63 | 64 | spinner.fail(); 65 | throw new Error('The environment not found in the Saleor Cloud'); 66 | }; 67 | -------------------------------------------------------------------------------- /src/lib/queryEnvironment.ts: -------------------------------------------------------------------------------- 1 | import Enquirer from 'enquirer'; 2 | import { got, HTTPError, Response } from 'got'; 3 | import { Arguments } from 'yargs'; 4 | 5 | import { Options, User } from '../types.js'; 6 | import { Config } from './config.js'; 7 | import { API, GET } from './index.js'; 8 | import { validateLength } from './util.js'; 9 | 10 | const isHttpBasicAuthProtected = (response: Response) => { 11 | const { body, headers } = response; 12 | const { 'www-authenticate': authHeader } = headers; 13 | 14 | const auth = (authHeader || '').trim().toLowerCase() === 'basic'; 15 | const unauthorized = 16 | ((body as string) || '').trim().toLowerCase() === 'unauthorized'; 17 | 18 | return auth && unauthorized; 19 | }; 20 | 21 | const getAuth = async (argv: Arguments) => { 22 | console.log('The selected environment is restricted with Basic Auth'); 23 | const { token } = await Config.get(); 24 | const user = (await GET(API.User, { 25 | token, 26 | instance: argv.instance, 27 | })) as User; 28 | 29 | const data = await Enquirer.prompt<{ username: string; password: string }>([ 30 | { 31 | type: 'input', 32 | name: 'username', 33 | message: 'Username', 34 | required: true, 35 | initial: argv.username || user.email, 36 | skip: !!argv.username, 37 | validate: (value: string) => validateLength(value, 128), 38 | }, 39 | { 40 | type: 'password', 41 | name: 'password', 42 | message: 'Password', 43 | required: true, 44 | initial: argv.password, 45 | skip: !!argv.password, 46 | validate: (value: string) => validateLength(value, 128), 47 | }, 48 | ]); 49 | 50 | return data; 51 | }; 52 | 53 | const checkAuth = async (endpoint: string, argv: Arguments) => { 54 | try { 55 | await got.get(endpoint); 56 | return {}; 57 | } catch (error) { 58 | if (error instanceof HTTPError) { 59 | if (isHttpBasicAuthProtected(error.response)) { 60 | const { username, password } = await getAuth(argv); 61 | const encodedAuth = btoa(`${username}:${password}`); 62 | return { Authorization: `Basic ${encodedAuth}` }; 63 | } 64 | throw error; 65 | } else { 66 | throw error; 67 | } 68 | } 69 | }; 70 | 71 | export const POST = async ( 72 | instance: string, 73 | headers: Record, 74 | json: Record, 75 | argv: Arguments, 76 | ) => { 77 | const auth = await checkAuth(instance, argv); 78 | 79 | const { data } = await got 80 | .post(instance, { 81 | headers: { 82 | ...headers, 83 | ...auth, 84 | }, 85 | json, 86 | }) 87 | .json<{ data: unknown; errors: Error[] }>(); 88 | 89 | // FIXME handle errors 90 | 91 | return data; 92 | }; 93 | 94 | export default POST; 95 | -------------------------------------------------------------------------------- /src/lib/response.ts: -------------------------------------------------------------------------------- 1 | import { HTTPError } from 'got'; 2 | 3 | export const isNotFound = (error: unknown) => { 4 | if (error instanceof HTTPError) { 5 | const { statusCode } = error.response; 6 | 7 | if (statusCode === 404) return true; 8 | } 9 | 10 | return false; 11 | }; 12 | 13 | export default isNotFound; 14 | -------------------------------------------------------------------------------- /template/event-subscription.graphql: -------------------------------------------------------------------------------- 1 | subscription ${name}Subscription { 2 | event { 3 | ... on ${name} { 4 | ${operationName} { 5 | id 6 | } 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /template/webhook-handler.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler } from 'next'; 2 | 3 | const handler: NextApiHandler = async (request, response) => { 4 | console.log(request.body); 5 | 6 | response.json({ success: true }); 7 | }; 8 | 9 | export default handler; 10 | -------------------------------------------------------------------------------- /test/__snapshots__/vercel.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Vercel API > snapshot 1`] = ` 4 | { 5 | "foo": "bar", 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /test/clear_cloud.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { clearProjects, listProjects } from './helper'; 4 | 5 | describe('Clean test account on cloud', async () => { 6 | it( 7 | 'should remove all projects', 8 | async () => { 9 | console.log('removing all projects'); 10 | await clearProjects(true); 11 | }, 12 | 1000 * 60 * 5, 13 | ); 14 | 15 | it('should return 0 projects', async () => { 16 | const { output } = await listProjects(); 17 | const projects: [] = JSON.parse(output.join()); 18 | expect(projects.length).toBe(0); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/functional/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @michalina-graczyk -------------------------------------------------------------------------------- /test/functional/app/create.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe, expect, it } from 'vitest'; 2 | 3 | import { 4 | command, 5 | DefaultTriggerResponse, 6 | prepareEnvironment, 7 | testEnvironmentName, 8 | testOrganization, 9 | trigger, 10 | } from '../../helper'; 11 | 12 | const appName = `${testEnvironmentName}-app`; 13 | 14 | beforeAll( 15 | async () => { 16 | await prepareEnvironment(); 17 | }, 18 | 1000 * 60 * 10, 19 | ); 20 | 21 | afterAll( 22 | async () => { 23 | await removeApps(); 24 | }, 25 | 1000 * 60 * 1, 26 | ); 27 | 28 | describe('app create', async () => { 29 | it( 30 | 'should create a Saleor Local App', 31 | async () => { 32 | const params = [ 33 | 'app', 34 | 'create', 35 | appName, 36 | '--permissions=MANAGE_USERS', 37 | `--environment=${testEnvironmentName}`, 38 | `--organization=${testOrganization}`, 39 | ]; 40 | const { exitCode, output } = await trigger( 41 | command, 42 | params, 43 | {}, 44 | { 45 | ...DefaultTriggerResponse, 46 | ...{ output: ['App created with id'] }, 47 | }, 48 | ); 49 | 50 | expect(exitCode).toBe(0); 51 | expect(output.join()).toContain('App created with id'); 52 | }, 53 | 1000 * 60 * 1, 54 | ); 55 | }); 56 | 57 | const getApps = async () => { 58 | const params = [ 59 | 'app', 60 | 'list', 61 | `--environment=${testEnvironmentName}`, 62 | `--organization=${testOrganization}`, 63 | '--json', 64 | ]; 65 | 66 | const { output } = await trigger( 67 | command, 68 | params, 69 | {}, 70 | { 71 | ...DefaultTriggerResponse, 72 | ...{ output: ['[]'] }, 73 | }, 74 | ); 75 | 76 | console.log(output); 77 | return JSON.parse(output.join()); 78 | }; 79 | 80 | const removeApps = async () => { 81 | const apps = await getApps(); 82 | 83 | for (const app of apps) { 84 | const params = [ 85 | 'app', 86 | 'remove', 87 | app.id, 88 | `--environment=${testEnvironmentName}`, 89 | `--organization=${testOrganization}`, 90 | '--force', 91 | ]; 92 | 93 | console.log(`removing backup ${app.id} - ${app.name}`); 94 | await trigger(command, params, {}); 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /test/functional/backup/restore.test.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi'; 2 | import { afterAll, beforeAll, describe, expect, it } from 'vitest'; 3 | 4 | import { 5 | command, 6 | DefaultTriggerResponse, 7 | prepareEnvironment, 8 | testEnvironmentName, 9 | testOrganization, 10 | trigger, 11 | } from '../../helper'; 12 | 13 | const backupName = `${testEnvironmentName}-backup`; 14 | let backupKey = ''; 15 | 16 | beforeAll( 17 | async () => { 18 | await prepareEnvironment(); 19 | const params = [ 20 | 'backup', 21 | 'create', 22 | backupName, 23 | `--organization=${testOrganization}`, 24 | '--json', 25 | ]; 26 | console.log(`creating backup ${backupName}`); 27 | const { output } = await trigger( 28 | command, 29 | params, 30 | {}, 31 | { 32 | ...DefaultTriggerResponse, 33 | ...{ output: ['{"key": "key"}'] }, 34 | }, 35 | ); 36 | const { key } = JSON.parse(stripAnsi(output.join(''))); 37 | backupKey = key; 38 | console.log(`backup created ${key}`); 39 | }, 40 | 1000 * 60 * 10, 41 | ); 42 | 43 | afterAll( 44 | async () => { 45 | await removeBackups(); 46 | }, 47 | 1000 * 60 * 10, 48 | ); 49 | 50 | describe('backup restore', async () => { 51 | it( 52 | 'should restore a backup', 53 | async () => { 54 | const params = [ 55 | 'backup', 56 | 'restore', 57 | backupKey, 58 | '--skip-webhooks-update', 59 | `--environment=${testEnvironmentName}`, 60 | `--organization=${testOrganization}`, 61 | ]; 62 | const { exitCode, err } = await trigger( 63 | command, 64 | params, 65 | {}, 66 | { 67 | ...DefaultTriggerResponse, 68 | ...{ err: ['Yay! Restore finished!'] }, 69 | }, 70 | ); 71 | 72 | expect(exitCode).toBe(0); 73 | expect(err.join()).toContain('Yay! Restore finished!'); 74 | }, 75 | 1000 * 60 * 10, 76 | ); 77 | }); 78 | 79 | const getBackups = async () => { 80 | const params = [ 81 | 'backup', 82 | 'list', 83 | `--organization=${testOrganization}`, 84 | '--json', 85 | ]; 86 | 87 | const { output } = await trigger( 88 | command, 89 | params, 90 | {}, 91 | { 92 | ...DefaultTriggerResponse, 93 | ...{ output: ['[]'] }, 94 | }, 95 | ); 96 | return JSON.parse(output.join('')); 97 | }; 98 | 99 | const removeBackups = async () => { 100 | const backups = await getBackups(); 101 | 102 | for (const backup of backups) { 103 | const params = [ 104 | 'backup', 105 | 'remove', 106 | backup.key, 107 | `--organization=${testOrganization}`, 108 | '--force', 109 | ]; 110 | 111 | console.log(`removing backup ${backup.key} - ${backup.name}`); 112 | await trigger(command, params, {}); 113 | } 114 | }; 115 | -------------------------------------------------------------------------------- /test/functional/environment/auth.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, afterAll } from 'vitest'; 2 | 3 | import { 4 | cleanEnvAfterUpdate, 5 | command, 6 | DefaultTriggerResponse, 7 | getEnvironment, 8 | prepareEnvironment, 9 | testOrganization, 10 | trigger, 11 | waitForBlockingTasks, 12 | } from '../../helper'; 13 | 14 | const envKey = await prepareEnvironment(); 15 | 16 | describe('update auth in environment', async () => { 17 | await waitForBlockingTasks(envKey); 18 | 19 | it('`env auth` enables the basic auth on the environment', async () => { 20 | const params = [ 21 | 'env', 22 | 'auth', 23 | envKey, 24 | '--login=saleor', 25 | '--password=saleor', 26 | `--organization=${testOrganization}`, 27 | ]; 28 | 29 | const { exitCode } = await trigger( 30 | command, 31 | params, 32 | {}, 33 | { 34 | ...DefaultTriggerResponse, 35 | }, 36 | ); 37 | expect(exitCode).toBe(0); 38 | 39 | const environment = await getEnvironment(envKey); 40 | expect(environment.protected).toBeTruthy(); 41 | }); 42 | 43 | await cleanEnvAfterUpdate(envKey); 44 | }); 45 | -------------------------------------------------------------------------------- /test/functional/environment/cors.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, afterAll } from 'vitest'; 2 | 3 | import { 4 | cleanEnvAfterUpdate, 5 | command, 6 | DefaultTriggerResponse, 7 | getEnvironment, 8 | prepareEnvironment, 9 | testOrganization, 10 | trigger, 11 | waitForBlockingTasks, 12 | } from '../../helper'; 13 | 14 | const envKey = await prepareEnvironment(); 15 | 16 | describe('update CORS origins in environment', async () => { 17 | await waitForBlockingTasks(envKey); 18 | 19 | it('`env cors` updates the environments CORS origins', async () => { 20 | const params = [ 21 | 'env', 22 | 'cors', 23 | envKey, 24 | '--selected="https://example.com"', 25 | '--selected="https://test.com"', 26 | '--password=saleor', 27 | `--organization=${testOrganization}`, 28 | ]; 29 | 30 | const { exitCode } = await trigger( 31 | command, 32 | params, 33 | {}, 34 | { 35 | ...DefaultTriggerResponse, 36 | }, 37 | ); 38 | expect(exitCode).toBe(0); 39 | 40 | const environment = await getEnvironment(envKey); 41 | expect(environment.allowed_cors_origins).toHaveLength(2); 42 | expect(environment.allowed_cors_origins).toContain('https://example.com'); 43 | }); 44 | 45 | await cleanEnvAfterUpdate(envKey); 46 | }); 47 | -------------------------------------------------------------------------------- /test/functional/environment/create.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | 3 | import { 4 | clearProjects, 5 | command, 6 | currentDate, 7 | DefaultTriggerResponse, 8 | prepareEnvironment, 9 | testOrganization, 10 | testProjectName, 11 | trigger, 12 | } from '../../helper'; 13 | 14 | const envName = `test-env-${currentDate()}`; 15 | 16 | beforeAll( 17 | async () => { 18 | await clearProjects(true); 19 | await prepareEnvironment(); 20 | }, 21 | 1000 * 60 * 5, 22 | ); 23 | 24 | describe('create new environment', async () => { 25 | it( 26 | 'creates a new environment', 27 | async () => { 28 | const params = [ 29 | 'env', 30 | 'create', 31 | envName, 32 | `--project=${testProjectName}`, 33 | '--database=sample', 34 | '--saleor=saleor-master-staging', 35 | `--domain=${envName}`, 36 | '--skip-restrict', 37 | `--organization=${testOrganization}`, 38 | ]; 39 | const { exitCode } = await trigger(command, params, {}); 40 | expect(exitCode).toBe(0); 41 | }, 42 | 1000 * 60 * 10, 43 | ); 44 | 45 | it('`env list` contains newly created env', async () => { 46 | const params = [ 47 | 'environment', 48 | 'list', 49 | `--organization=${testOrganization}`, 50 | ]; 51 | 52 | const { exitCode, output } = await trigger( 53 | command, 54 | params, 55 | {}, 56 | { 57 | ...DefaultTriggerResponse, 58 | ...{ output: [envName] }, 59 | }, 60 | ); 61 | expect(exitCode).toBe(0); 62 | expect(output.join()).toContain(envName); 63 | }); 64 | 65 | it('`environment show` returns env details', async () => { 66 | const params = [ 67 | 'environment', 68 | 'show', 69 | envName, 70 | `--organization=${testOrganization}`, 71 | ]; 72 | 73 | const { exitCode, output } = await trigger( 74 | command, 75 | params, 76 | {}, 77 | { 78 | ...DefaultTriggerResponse, 79 | ...{ 80 | output: [ 81 | `name: ${envName}`, 82 | `domain: ${envName}.staging.saleor.cloud`, 83 | 'database_population: sample', 84 | ], 85 | }, 86 | }, 87 | ); 88 | expect(exitCode).toBe(0); 89 | expect(output.join()).toContain(`name: ${envName}`); 90 | expect(output.join()).toContain(`domain: ${envName}`); 91 | expect(output.join()).toContain('database_population: sample'); 92 | }); 93 | 94 | it('`environment remove` removes environment', async () => { 95 | const params = [ 96 | 'environment', 97 | 'remove', 98 | envName, 99 | `--organization=${testOrganization}`, 100 | '--force', 101 | ]; 102 | 103 | const { exitCode } = await trigger( 104 | command, 105 | params, 106 | {}, 107 | { 108 | ...DefaultTriggerResponse, 109 | }, 110 | ); 111 | expect(exitCode).toBe(0); 112 | }); 113 | 114 | it('`env list` does not contain newly created env after removal', async () => { 115 | const params = [ 116 | 'environment', 117 | 'list', 118 | `--organization=${testOrganization}`, 119 | ]; 120 | 121 | const { exitCode, output } = await trigger( 122 | command, 123 | params, 124 | {}, 125 | { 126 | ...DefaultTriggerResponse, 127 | }, 128 | ); 129 | expect(exitCode).toBe(0); 130 | expect(output.join()).not.toContain(envName); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /test/functional/environment/list.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, it } from 'vitest'; 2 | 3 | import { 4 | command, 5 | DefaultTriggerResponse, 6 | prepareEnvironment, 7 | testEnvironmentName, 8 | testOrganization, 9 | trigger, 10 | } from '../../helper'; 11 | 12 | beforeAll(async () => { 13 | await prepareEnvironment(); 14 | }); 15 | 16 | describe('show list of existing environments', async () => { 17 | it('returns environment list', async () => { 18 | const params = [ 19 | 'environment', 20 | 'list', 21 | `--organization=${testOrganization}`, 22 | ]; 23 | 24 | const { exitCode, output } = await trigger( 25 | command, 26 | params, 27 | {}, 28 | { 29 | ...DefaultTriggerResponse, 30 | ...{ output: [testEnvironmentName] }, 31 | }, 32 | ); 33 | 34 | expect(exitCode).toBe(0); 35 | expect(output.join()).toContain(testEnvironmentName); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/functional/environment/maintenance.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, afterAll } from 'vitest'; 2 | 3 | import { 4 | cleanEnvAfterUpdate, 5 | command, 6 | DefaultTriggerResponse, 7 | getEnvironment, 8 | prepareEnvironment, 9 | testOrganization, 10 | trigger, 11 | waitForBlockingTasks, 12 | } from '../../helper'; 13 | 14 | const envKey = await prepareEnvironment(); 15 | 16 | describe('update maintenance mode in the environment', async () => { 17 | await waitForBlockingTasks(envKey); 18 | 19 | it('`env maintenance` enables the maintenance mode', async () => { 20 | const params = [ 21 | 'env', 22 | 'maintenance', 23 | envKey, 24 | '--enable', 25 | `--organization=${testOrganization}`, 26 | ]; 27 | 28 | const { exitCode } = await trigger( 29 | command, 30 | params, 31 | {}, 32 | { 33 | ...DefaultTriggerResponse, 34 | }, 35 | ); 36 | expect(exitCode).toBe(0); 37 | 38 | const environment = await getEnvironment(envKey); 39 | expect(environment.maintenance_mode).toBeTruthy(); 40 | }); 41 | 42 | await cleanEnvAfterUpdate(envKey); 43 | }); 44 | -------------------------------------------------------------------------------- /test/functional/environment/origins.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { 4 | cleanEnvAfterUpdate, 5 | command, 6 | DefaultTriggerResponse, 7 | getEnvironment, 8 | prepareEnvironment, 9 | testOrganization, 10 | trigger, 11 | waitForBlockingTasks, 12 | } from '../../helper'; 13 | 14 | const envKey = await prepareEnvironment(); 15 | 16 | describe('update trusted origins in environment', async () => { 17 | await waitForBlockingTasks(envKey); 18 | 19 | it('`env origins` update trusted origins in the environment', async () => { 20 | const params = [ 21 | 'env', 22 | 'origins', 23 | envKey, 24 | '--origins="https://example.com"', 25 | '--origins="https://test.com"', 26 | `--organization=${testOrganization}`, 27 | ]; 28 | 29 | const { exitCode } = await trigger( 30 | command, 31 | params, 32 | {}, 33 | { 34 | ...DefaultTriggerResponse, 35 | }, 36 | ); 37 | expect(exitCode).toBe(0); 38 | 39 | const environment = await getEnvironment(envKey); 40 | expect(environment.allowed_cors_origins).toHaveLength(2); 41 | expect(environment.allowed_cors_origins).toContain('https://example.com'); 42 | }); 43 | 44 | await cleanEnvAfterUpdate(envKey); 45 | }); 46 | -------------------------------------------------------------------------------- /test/functional/environment/show.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { 4 | DefaultTriggerResponse, 5 | prepareEnvironment, 6 | testOrganization, 7 | trigger, 8 | } from '../../helper'; 9 | 10 | describe('storefront show', async () => { 11 | const command = 'saleor'; 12 | const key = await prepareEnvironment(); 13 | 14 | it('should show the environment details', async () => { 15 | const params = ['env', 'show', key, `--organization=${testOrganization}`]; 16 | const { exitCode, output } = await trigger( 17 | command, 18 | params, 19 | {}, 20 | { 21 | ...DefaultTriggerResponse, 22 | ...{ output: [`key: ${key}`] }, 23 | }, 24 | ); 25 | 26 | expect(exitCode).toBe(0); 27 | expect(output.join()).toContain(`key: ${key}`); 28 | }); 29 | 30 | it('should return 1 exit code for invalid env', async () => { 31 | const params = ['env', 'show', 'bla', `--organization=${testOrganization}`]; 32 | const { exitCode } = await trigger( 33 | command, 34 | params, 35 | {}, 36 | { ...DefaultTriggerResponse, ...{ exitCode: 1 } }, 37 | ); 38 | 39 | expect(exitCode).not.toBe(0); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/functional/environment/update.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, afterAll } from 'vitest'; 2 | 3 | import { 4 | cleanEnvAfterUpdate, 5 | command, 6 | DefaultTriggerResponse, 7 | getEnvironment, 8 | newTestEnvironmentName, 9 | prepareEnvironment, 10 | testOrganization, 11 | trigger, 12 | waitForBlockingTasks, 13 | } from '../../helper'; 14 | 15 | const envKey = await prepareEnvironment(); 16 | 17 | describe('update environment', async () => { 18 | await waitForBlockingTasks(envKey); 19 | 20 | it( 21 | '`env update` updates the environment name', 22 | async () => { 23 | const params = [ 24 | 'env', 25 | 'update', 26 | envKey, 27 | `--name=${newTestEnvironmentName}`, 28 | `--organization=${testOrganization}`, 29 | ]; 30 | 31 | const { exitCode } = await trigger( 32 | command, 33 | params, 34 | {}, 35 | { 36 | ...DefaultTriggerResponse, 37 | }, 38 | ); 39 | expect(exitCode).toBe(0); 40 | 41 | const environment = await getEnvironment(envKey); 42 | expect(environment.name).toBe(newTestEnvironmentName); 43 | }, 44 | 1000 * 60 * 10, 45 | ); 46 | 47 | await cleanEnvAfterUpdate(envKey); 48 | }); 49 | -------------------------------------------------------------------------------- /test/functional/project/create.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { 4 | command, 5 | currentDate, 6 | DefaultTriggerResponse, 7 | testOrganization, 8 | trigger, 9 | } from '../../helper'; 10 | 11 | const projectName = `test-project-${currentDate()}`; 12 | 13 | describe('create new project', async () => { 14 | const region = 'us-east-1'; 15 | it( 16 | 'creates a new project', 17 | async () => { 18 | const params = [ 19 | 'project', 20 | 'create', 21 | projectName, 22 | '--plan=dev', 23 | `--region=${region}`, 24 | `--organization=${testOrganization}`, 25 | ]; 26 | const { exitCode } = await trigger(command, params, {}); 27 | expect(exitCode).toBe(0); 28 | }, 29 | 1000 * 60 * 1, 30 | ); 31 | 32 | it( 33 | '`project list` contains newly created project', 34 | async () => { 35 | const params = ['project', 'list', `--organization=${testOrganization}`]; 36 | 37 | const { exitCode, output } = await trigger( 38 | command, 39 | params, 40 | {}, 41 | { 42 | ...DefaultTriggerResponse, 43 | ...{ 44 | output: [projectName], 45 | }, 46 | }, 47 | ); 48 | expect(exitCode).toBe(0); 49 | expect(output.join()).toContain(projectName); 50 | }, 51 | 1000 * 60 * 1, 52 | ); 53 | 54 | it('`project show` returns project details', async () => { 55 | const params = [ 56 | 'project', 57 | 'show', 58 | projectName, 59 | `--organization=${testOrganization}`, 60 | ]; 61 | 62 | const { exitCode, output } = await trigger( 63 | command, 64 | params, 65 | {}, 66 | { 67 | ...DefaultTriggerResponse, 68 | ...{ 69 | output: [ 70 | `name: ${projectName}`, 71 | `slug: ${projectName}`, 72 | `region: ${region}`, 73 | ], 74 | }, 75 | }, 76 | ); 77 | expect(exitCode).toBe(0); 78 | expect(output.join()).toContain(`name: ${projectName}`); 79 | expect(output.join()).toContain(`slug: ${projectName}`); 80 | expect(output.join()).toContain(`region: ${region}`); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/functional/project/list.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { command, testOrganization, trigger } from '../../helper'; 4 | 5 | describe('project list', async () => { 6 | it('returns project list', async () => { 7 | const params = ['project', 'list', `--organization=${testOrganization}`]; 8 | 9 | const { exitCode } = await trigger(command, params, {}); 10 | expect(exitCode).toBe(0); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/functional/storefront/create.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import { afterAll, describe, expect, it } from 'vitest'; 3 | 4 | import { capitalize } from '../../../src/lib/util.js'; 5 | import { 6 | command, 7 | currentDate, 8 | DefaultTriggerResponse, 9 | testOrganization, 10 | trigger, 11 | } from '../../helper'; 12 | 13 | const storefrontName = `storefront-${currentDate()}`; 14 | const demoName = capitalize(storefrontName); 15 | const storefrontCwd = `${process.cwd()}/${storefrontName}`; 16 | 17 | afterAll( 18 | async () => { 19 | await fs.remove(storefrontCwd); 20 | }, 21 | 1000 * 60 * 5, 22 | ); 23 | 24 | describe('storefront create --demo', async () => { 25 | it( 26 | 'should create a storefront with a valid URL', 27 | async () => { 28 | const params = [ 29 | 'storefront', 30 | 'create', 31 | storefrontName, 32 | '--url="https://zaiste.saleor.cloud/graphql/"', 33 | ]; 34 | const { exitCode, output } = await trigger( 35 | command, 36 | params, 37 | {}, 38 | { ...DefaultTriggerResponse, ...{ output: [storefrontName] } }, 39 | ); 40 | 41 | expect(exitCode).toBe(0); 42 | expect(output.join()).toContain(storefrontName); 43 | }, 44 | 1000 * 60 * 10, 45 | ); 46 | 47 | it( 48 | 'should not create a storefront with an invalid URL', 49 | async () => { 50 | const params = [ 51 | 'storefront', 52 | 'create', 53 | storefrontName, 54 | '--url="https://zaiste.saleor.cloud/raphql/"', 55 | ]; 56 | const { exitCode } = await trigger( 57 | command, 58 | params, 59 | {}, 60 | { ...DefaultTriggerResponse, ...{ exitCode: 1 } }, 61 | ); 62 | expect(exitCode).toBe(1); 63 | }, 64 | 1000 * 60 * 10, 65 | ); 66 | }); 67 | 68 | describe('storefront create --demo', async () => { 69 | it( 70 | 'should create a demo storefront', 71 | async () => { 72 | const params = [ 73 | 'storefront', 74 | 'create', 75 | storefrontName, 76 | '--demo', 77 | `--organization=${testOrganization}`, 78 | ]; 79 | const { exitCode, output } = await trigger( 80 | command, 81 | params, 82 | {}, 83 | { ...DefaultTriggerResponse, ...{ output: [storefrontName] } }, 84 | ); 85 | 86 | expect(exitCode).toBe(0); 87 | expect(output.join()).toContain(storefrontName); 88 | }, 89 | 1000 * 60 * 10, 90 | ); 91 | it( 92 | 'should create a demo project', 93 | async () => { 94 | const params = ['project', 'list', `--organization=${testOrganization}`]; 95 | 96 | const { exitCode, output } = await trigger( 97 | command, 98 | params, 99 | {}, 100 | { ...DefaultTriggerResponse, ...{ output: [demoName] } }, 101 | ); 102 | expect(exitCode).toBe(0); 103 | expect(output.join()).toContain(demoName); 104 | }, 105 | 1000 * 60 * 1, 106 | ); 107 | 108 | it( 109 | 'should create a demo environment', 110 | async () => { 111 | const params = ['env', 'list', `--organization=${testOrganization}`]; 112 | 113 | const { exitCode, output } = await trigger( 114 | command, 115 | params, 116 | {}, 117 | { ...DefaultTriggerResponse, ...{ output: [demoName] } }, 118 | ); 119 | 120 | expect(exitCode).toBe(0); 121 | expect(output.join()).toContain(demoName); 122 | }, 123 | 1000 * 60 * 1, 124 | ); 125 | }); 126 | -------------------------------------------------------------------------------- /test/functional/tunnel.test.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import fs from 'fs-extra'; 3 | import got from 'got'; 4 | import kill from 'tree-kill'; 5 | import { afterAll, beforeAll, expect, it } from 'vitest'; 6 | 7 | import { Manifest } from '../../src/lib/common'; 8 | import { delay } from '../../src/lib/util'; 9 | import { 10 | command, 11 | prepareEnvironment, 12 | shouldMockTests, 13 | testEnvironmentName, 14 | testOrganization, 15 | trigger, 16 | } from '../helper'; 17 | 18 | const appName = 'tunnel-app'; 19 | const appCwd = `${process.cwd()}/${appName}`; 20 | 21 | beforeAll( 22 | async () => { 23 | await fs.rm(appCwd, { recursive: true, force: true }); 24 | await prepareEnvironment(); 25 | const params = ['app', 'create', appName]; 26 | console.log(`creating an app ${appName}`); 27 | 28 | await trigger(command, params, {}); 29 | }, 30 | 1000 * 60 * 10, 31 | ); 32 | 33 | afterAll(async () => { 34 | await fs.rm(appCwd, { recursive: true, force: true }); 35 | }); 36 | 37 | it( 38 | 'should start the tunnel and verify if provided URL is accessible', 39 | async () => { 40 | if (shouldMockTests) { 41 | return; 42 | } 43 | 44 | const params = [ 45 | 'app', 46 | 'tunnel', 47 | `--organization=${testOrganization}`, 48 | `--environment=${testEnvironmentName}`, 49 | '--force-install', 50 | ]; 51 | 52 | const app = spawn('pnpm', ['dev'], { cwd: appCwd }); 53 | 54 | // wait for the app to start 55 | await delay(1000 * 30); 56 | 57 | const tunnel = spawn('saleor', params, { cwd: appCwd }); 58 | 59 | const output: string[] = []; 60 | tunnel.stdout.on('data', (data) => { 61 | output.push(data.toString()); 62 | }); 63 | 64 | tunnel.stderr.on('data', (data) => { 65 | if (data.toString().match('✔ Installing')) { 66 | const tunnelUrl = output.join().match('https.*.saleor.live'); 67 | 68 | if (!tunnelUrl) { 69 | throw new Error('Tunnel URL not found'); 70 | } 71 | 72 | console.log(`Tunnel URL: ${tunnelUrl[0]}`); 73 | 74 | checkTunnelUrl(tunnelUrl[0]); 75 | checkManifestName(tunnelUrl[0]); 76 | } 77 | }); 78 | 79 | // wait for the tunnel to start 80 | await delay(1000 * 60 * 2); 81 | 82 | if (tunnel.pid) { 83 | await killPid(tunnel.pid); 84 | } 85 | 86 | if (app.pid) { 87 | await killPid(app.pid); 88 | } 89 | }, 90 | 1000 * 60 * 3, 91 | ); 92 | 93 | const checkTunnelUrl = async (tunnelUrl: string) => { 94 | console.log('tunnelUrl', tunnelUrl); 95 | const { statusCode } = await got.get(tunnelUrl); 96 | expect(statusCode).toBe(200); 97 | }; 98 | 99 | const checkManifestName = async (tunnelUrl: string) => { 100 | const manifest: Manifest = await got.get(`${tunnelUrl}/api/manifest`).json(); 101 | 102 | expect(manifest.name).toBe(appName); 103 | }; 104 | 105 | const killPid = async (pid: number) => 106 | // eslint-disable-next-line no-promise-executor-return 107 | new Promise((resolve) => kill(pid, resolve)); 108 | -------------------------------------------------------------------------------- /test/organization/show.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { DefaultTriggerResponse, testOrganization, trigger } from '../helper'; 4 | 5 | describe('show organization details', async () => { 6 | const command = 'saleor'; 7 | 8 | it('shows organization details', async () => { 9 | const params = ['organization', 'show', testOrganization]; 10 | 11 | const { exitCode, output } = await trigger( 12 | command, 13 | params, 14 | {}, 15 | { 16 | ...DefaultTriggerResponse, 17 | ...{ 18 | output: [`slug: ${testOrganization}`], 19 | }, 20 | }, 21 | ); 22 | expect(exitCode).toBe(0); 23 | expect(output.join()).toContain(`slug: ${testOrganization}`); 24 | }); 25 | 26 | it('should return 1 exit code for invalid organization', async () => { 27 | const params = ['organization', 'show', 'invalid']; 28 | const { exitCode } = await trigger( 29 | command, 30 | params, 31 | {}, 32 | { ...DefaultTriggerResponse, ...{ exitCode: 1 } }, 33 | ); 34 | 35 | expect(exitCode).not.toBe(0); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/vercel.test.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | import { setupServer } from 'msw/node'; 3 | import { 4 | afterAll, 5 | afterEach, 6 | assert, 7 | beforeAll, 8 | describe, 9 | expect, 10 | it, 11 | } from 'vitest'; 12 | 13 | import { Vercel } from '../src/lib/vercel.js'; 14 | 15 | const VercelURL = 'https://api.vercel.com'; 16 | 17 | const myProject1 = { 18 | name: 'my-project-1', 19 | slug: 'my-project-1', 20 | }; 21 | 22 | const handlers = [ 23 | rest.get('https://rest.example/path/to/posts', (req, res, ctx) => 24 | res(ctx.status(200), ctx.json(myProject1)), 25 | ), 26 | rest.get(`${VercelURL}/v9/projects/my-project-1`, (req, res, ctx) => 27 | res(ctx.status(200), ctx.json(myProject1)), 28 | ), 29 | rest.get(`${VercelURL}/v13/deployments/xxx`, (req, res, ctx) => 30 | res(ctx.status(200), ctx.json([])), 31 | ), 32 | ]; 33 | 34 | const server = setupServer(...handlers); 35 | server.printHandlers(); 36 | 37 | const vercel = new Vercel('asdf'); 38 | 39 | beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); 40 | afterAll(() => server.close()); 41 | afterEach(() => server.resetHandlers()); 42 | 43 | describe('Vercel API', () => { 44 | it('returns `id` for single project', async () => { 45 | const r = await vercel.getProject('my-project-1'); 46 | 47 | expect(r).toEqual({ name: 'my-project-1', slug: 'my-project-1' }); 48 | }); 49 | 50 | it('foo', () => { 51 | assert.equal(Math.sqrt(4), 2); 52 | }); 53 | 54 | it('bar', () => { 55 | expect(1 + 1).eq(2); 56 | }); 57 | 58 | it('snapshot', () => { 59 | expect({ foo: 'bar' }).toMatchSnapshot(); 60 | }); 61 | }); 62 | --------------------------------------------------------------------------------