diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf8a091b..38df6b30 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [20.x, 22.x, 24.x, 25.x] + node-version: [22.x, 24.x, 26.x] os: [ubuntu-latest, windows-latest, macos-latest] timeout-minutes: 5 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a24d3d4..6f3ba892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# 8.0.0 (Unpublished) + +## ⚠️ Breaking changes + +- Minimum Node.js version is now 22.13.0 (previously 20, which is now EOL). +- Removed commands that were deprecated in 7.0.0. There are alternatives for all removed commands. See the 7.0.0 release notes for details. +- Remove CommonJS exports. The package is now ESM-only. Use `import` instead of `require()`. + +## 🚩 Enhancements + +- Add support for project-level `calibre.json`, e.g.: `{ "site": "my-site-slug" }` to avoid having to pass `--site` for every command. +- Commands that have pagination now support an `--all` flag, which will automatically paginate through all results and return a complete list. +- Use consistent JSON error output. +- Use consistent table formatting. +- Use consistent date parsing. + +## 🧹 Housekeeping + +- Replaced `chalk` with Node.js built-in `node:util` `styleText` for terminal colouring. +- Replaced `ora` with `nanospinner` for terminal spinners. +- Removed `log-symbols` dependency in favour of Unicode characters. +- Updated CI matrix to test Node.js 22, 24, and 26. + # 7.0.0 (2026-05-07) ## ⚠️ Major release with breaking changes diff --git a/__tests__/cli/__snapshots__/metric-list.test.js.snap b/__tests__/cli/__snapshots__/metric-list.test.js.snap index 23468ca7..427a00df 100644 --- a/__tests__/cli/__snapshots__/metric-list.test.js.snap +++ b/__tests__/cli/__snapshots__/metric-list.test.js.snap @@ -3,6 +3,6 @@ exports[`metric-list lists metrics 1`] = ` "2 metrics IDENTIFIER | NAME | CATEGORY | GOOD | POOR | RECOMMENDED -first-contentful-paint | First Contentful Paint | Performance | > 1.80 sec | < 3.00 sec | ✔ -largest-contentful-paint | Largest Contentful Paint | Performance | > 2.50 sec | < 4.00 sec | ✔ " +first-contentful-paint | First Contentful Paint | Performance | > 1.80 sec | < 3.00 sec | ✔ +largest-contentful-paint | Largest Contentful Paint | Performance | > 2.50 sec | < 4.00 sec | ✔ " `; diff --git a/__tests__/cli/__snapshots__/site.test.js.snap b/__tests__/cli/__snapshots__/site.test.js.snap index f25b99a1..49c4b9f7 100644 --- a/__tests__/cli/__snapshots__/site.test.js.snap +++ b/__tests__/cli/__snapshots__/site.test.js.snap @@ -1,41 +1,23 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`missing site argument 1`] = ` +exports[`missing subcommand 1`] = ` " - Missing required argument: site + Not enough non-option arguments: got 0, need at least 1 - calibre site pages [options] + calibre site -Options: - - --help Show help [boolean] - --version Show version number [boolean] - --site A unique slug identifying each Site. You can find it in Site - Settings → General or by using the calibre site list command. - [string] [required] - --count The number of items to return (default: 25, maximum: 500). - [default: 25] - --cursor The cursor to fetch records after - --json Outputs the results of the command in JSON format." -`; - -exports[`missing site argument value 1`] = ` -" +Manage your Sites. - Not enough arguments following: site +Commands: - calibre site pages [options] + calibre site create [options] Add a Site for Calibre to monitor. + calibre site list List all Sites you are tracking in + Calibre. + calibre site delete [options] Delete a selected Site. Options: --help Show help [boolean] - --version Show version number [boolean] - --site A unique slug identifying each Site. You can find it in Site - Settings → General or by using the calibre site list command. - [string] [required] - --count The number of items to return (default: 25, maximum: 500). - [default: 25] - --cursor The cursor to fetch records after - --json Outputs the results of the command in JSON format." + --version Show version number [boolean]" `; diff --git a/__tests__/cli/crux/__snapshots__/history.test.js.snap b/__tests__/cli/crux/__snapshots__/history.test.js.snap new file mode 100644 index 00000000..710969e6 --- /dev/null +++ b/__tests__/cli/crux/__snapshots__/history.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`crux history displays crux history 1`] = ` +"PERIOD | METRIC | P75 +31-Jan-2024 | Largest Contentful Paint | 2.10 sec +31-Dec-2023 | Largest Contentful Paint | 2.30 sec +31-Jan-2024 | Cumulative Layout Shift | 0.05 +31-Dec-2023 | Cumulative Layout Shift | 0.08 " +`; diff --git a/__tests__/cli/crux/__snapshots__/summary.test.js.snap b/__tests__/cli/crux/__snapshots__/summary.test.js.snap index 9af00678..07527c6a 100644 --- a/__tests__/cli/crux/__snapshots__/summary.test.js.snap +++ b/__tests__/cli/crux/__snapshots__/summary.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`crux summary displays crux summary 1`] = ` -"Form factors: desktop 4500.0% | phone 5000.0% | tablet 500.0% +"Form factors: desktop 45.0% | phone 50.0% | tablet 5.0% METRIC | P75 Largest Contentful Paint | 2.10 sec diff --git a/__tests__/cli/crux/__snapshots__/url.test.js.snap b/__tests__/cli/crux/__snapshots__/url.test.js.snap new file mode 100644 index 00000000..99948c5d --- /dev/null +++ b/__tests__/cli/crux/__snapshots__/url.test.js.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`crux url displays crux url details 1`] = ` +" +METRIC | P75 +Largest Contentful Paint | 2.10 sec +Cumulative Layout Shift | 0.05 + +History: +PERIOD | METRIC | P75 +31-Jan-2024 | Largest Contentful Paint | 2.10 sec" +`; diff --git a/__tests__/cli/crux/__snapshots__/urls.test.js.snap b/__tests__/cli/crux/__snapshots__/urls.test.js.snap new file mode 100644 index 00000000..222efa8f --- /dev/null +++ b/__tests__/cli/crux/__snapshots__/urls.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`crux urls lists crux urls 1`] = ` +"URL | ASSESSMENT | CRUXLARGESTCONTENTFULPAINT | CRUXCUMULATIVELAYOUTSHIFT +https://example.com/page1 | Good | 2.10 sec | 0.05 +https://example.com/page2 | Needs Improvement | 3.50 sec | 0.15 " +`; diff --git a/__tests__/cli/crux/history.test.js b/__tests__/cli/crux/history.test.js new file mode 100644 index 00000000..f1531cda --- /dev/null +++ b/__tests__/cli/crux/history.test.js @@ -0,0 +1,19 @@ +import { + runCLI, + setupIntegrationServer, + teardownIntegrationServer +} from '../../utils' + +import cruxHistory from '../../fixtures/cruxHistory.json' + +describe('crux history', () => { + beforeAll(async () => await setupIntegrationServer(cruxHistory)) + afterAll(async () => await teardownIntegrationServer()) + + test('displays crux history', async () => { + const out = await runCLI({ + args: 'crux history --site=test' + }) + expect(out).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/crux/url.test.js b/__tests__/cli/crux/url.test.js new file mode 100644 index 00000000..e1a2608f --- /dev/null +++ b/__tests__/cli/crux/url.test.js @@ -0,0 +1,19 @@ +import { + runCLI, + setupIntegrationServer, + teardownIntegrationServer +} from '../../utils' + +import cruxUrl from '../../fixtures/cruxUrl.json' + +describe('crux url', () => { + beforeAll(async () => await setupIntegrationServer(cruxUrl)) + afterAll(async () => await teardownIntegrationServer()) + + test('displays crux url details', async () => { + const out = await runCLI({ + args: 'crux url 123e4567-e89b-12d3-a456-426614174000 --site=test' + }) + expect(out).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/crux/urls.test.js b/__tests__/cli/crux/urls.test.js new file mode 100644 index 00000000..6c9e48d7 --- /dev/null +++ b/__tests__/cli/crux/urls.test.js @@ -0,0 +1,19 @@ +import { + runCLI, + setupIntegrationServer, + teardownIntegrationServer +} from '../../utils' + +import cruxUrls from '../../fixtures/cruxUrls.json' + +describe('crux urls', () => { + beforeAll(async () => await setupIntegrationServer(cruxUrls)) + afterAll(async () => await teardownIntegrationServer()) + + test('lists crux urls', async () => { + const out = await runCLI({ + args: 'crux urls --site=test' + }) + expect(out).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/deploy/__snapshots__/create.test.js.snap b/__tests__/cli/deploy/__snapshots__/create.test.js.snap new file mode 100644 index 00000000..9b3c25af --- /dev/null +++ b/__tests__/cli/deploy/__snapshots__/create.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`deploy create creates deploy 1`] = `""`; diff --git a/__tests__/cli/deploy/__snapshots__/delete.test.js.snap b/__tests__/cli/deploy/__snapshots__/delete.test.js.snap new file mode 100644 index 00000000..cba974f8 --- /dev/null +++ b/__tests__/cli/deploy/__snapshots__/delete.test.js.snap @@ -0,0 +1,6 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`deploy delete deletes deploy 1`] = ` +"- Connecting to Calibre +✔ Deploy deleted: 8c68b9f6-b5bf-49bc-abd7-896db6338818" +`; diff --git a/__tests__/cli/site/__snapshots__/deploys.test.js.snap b/__tests__/cli/deploy/__snapshots__/list.test.js.snap similarity index 92% rename from __tests__/cli/site/__snapshots__/deploys.test.js.snap rename to __tests__/cli/deploy/__snapshots__/list.test.js.snap index b46a971d..a8f4076c 100644 --- a/__tests__/cli/site/__snapshots__/deploys.test.js.snap +++ b/__tests__/cli/deploy/__snapshots__/list.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`site deploys lists all deploys 1`] = ` +exports[`deploy list lists all deploys 1`] = ` "3 deploys UUID | REVISION | REPOSITORY | USERNAME | CREATED 8c68b9f6-b5bf-49bc-abd7-896db6338818 | v1 | https://github.com/calibreapp/app | Deployer | 12:00AM 17-Jan-2019 diff --git a/__tests__/cli/deploy/create.test.js b/__tests__/cli/deploy/create.test.js new file mode 100644 index 00000000..e0a96b8a --- /dev/null +++ b/__tests__/cli/deploy/create.test.js @@ -0,0 +1,19 @@ +import { + runCLI, + setupIntegrationServer, + teardownIntegrationServer +} from '../../utils' + +import createDeploy from '../../fixtures/createDeploy.json' + +describe('deploy create', () => { + beforeAll(async () => await setupIntegrationServer(createDeploy)) + afterAll(async () => await teardownIntegrationServer()) + + test('creates deploy', async () => { + const out = await runCLI({ + args: 'deploy create --site=test --revision=abc123def456' + }) + expect(out).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/site/delete-deploy.test.js b/__tests__/cli/deploy/delete.test.js similarity index 60% rename from __tests__/cli/site/delete-deploy.test.js rename to __tests__/cli/deploy/delete.test.js index 144be74f..233e5e78 100644 --- a/__tests__/cli/site/delete-deploy.test.js +++ b/__tests__/cli/deploy/delete.test.js @@ -6,21 +6,13 @@ import { import deleteDeploy from '../../fixtures/deleteDeploy.json' -describe('site delete-deploy', () => { +describe('deploy delete', () => { beforeAll(async () => await setupIntegrationServer(deleteDeploy)) afterAll(async () => await teardownIntegrationServer()) - test('requires uuid', async () => { - const out = await runCLI({ - args: 'site delete-deploy --site=test', - testForError: true - }) - expect(out).toMatchSnapshot() - }) - test('deletes deploy', async () => { const out = await runCLI({ - args: 'site delete-deploy --site=test --uuid=1', + args: 'deploy delete --site=test --uuid=1', testForError: true }) expect(out).toMatchSnapshot() diff --git a/__tests__/cli/site/deploys.test.js b/__tests__/cli/deploy/list.test.js similarity index 77% rename from __tests__/cli/site/deploys.test.js rename to __tests__/cli/deploy/list.test.js index bee0e1bd..2670bc3d 100644 --- a/__tests__/cli/site/deploys.test.js +++ b/__tests__/cli/deploy/list.test.js @@ -6,12 +6,14 @@ import { import listDeploys from '../../fixtures/listDeploys.json' -describe('site deploys', () => { +describe('deploy list', () => { beforeAll(async () => await setupIntegrationServer(listDeploys)) afterAll(async () => await teardownIntegrationServer()) test('lists all deploys', async () => { - const out = await runCLI({ args: 'site deploys --site=test' }) + const out = await runCLI({ + args: 'deploy list --site=test' + }) expect(out).toMatchSnapshot() }) }) diff --git a/__tests__/cli/deprecation.test.js b/__tests__/cli/deprecation.test.js deleted file mode 100644 index 44673aa6..00000000 --- a/__tests__/cli/deprecation.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import { - runCLI, - setupIntegrationServer, - teardownIntegrationServer -} from '../utils' - -import listPages from '../fixtures/listPages.json' - -describe('deprecated commands', () => { - beforeAll(async () => await setupIntegrationServer(listPages)) - afterAll(async () => await teardownIntegrationServer()) - - test('site pages shows deprecation warning on stderr', async () => { - const stderr = await runCLI({ - args: 'site pages --site=test', - testForError: true - }) - expect(stderr).toContain('[calibre:deprecated]') - expect(stderr).toContain('synthetic pages') - }) - - test('site pages still returns valid output on stdout', async () => { - const stdout = await runCLI({ - args: 'site pages --site=test' - }) - expect(stdout).toBeTruthy() - }) -}) diff --git a/__tests__/cli/rum/__snapshots__/config.test.js.snap b/__tests__/cli/rum/__snapshots__/config.test.js.snap new file mode 100644 index 00000000..35eeba5f --- /dev/null +++ b/__tests__/cli/rum/__snapshots__/config.test.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`rum config displays rum config 1`] = ` +" Enabled: true + Sample Rate: 1 + Allowed Origins: https://example.com, https://www.example.com + Endpoint: https://rum.calibreapp.com/collect + Identifier: rum_abc123 + Data Retention: 12 months (max: 24) + Exclude EU: false" +`; diff --git a/__tests__/cli/rum/__snapshots__/history.test.js.snap b/__tests__/cli/rum/__snapshots__/history.test.js.snap new file mode 100644 index 00000000..d75f9412 --- /dev/null +++ b/__tests__/cli/rum/__snapshots__/history.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`rum history displays rum history 1`] = ` +"DATE | LCP | CLS | INP | FCP | TTFB | SESSIONS +15-Jan-2024 | 2.10 sec | 0.05 | 180 ms | 1.80 sec | 800 ms | 1250 +14-Jan-2024 | 2.30 sec | 0.08 | 200 ms | 1.90 sec | 850 ms | 1180 " +`; diff --git a/__tests__/cli/rum/__snapshots__/pages.test.js.snap b/__tests__/cli/rum/__snapshots__/pages.test.js.snap new file mode 100644 index 00000000..171ef56c --- /dev/null +++ b/__tests__/cli/rum/__snapshots__/pages.test.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`rum pages displays rum pages 1`] = ` +"PATH | SESSIONS | LCP | CLS | INP +/ | 5000 | 2.10 sec | 0.05 | 180 ms +/about | 1200 | 2.50 sec | 0.1 | 220 ms +/contact | 800 | 1.90 sec | 0.03 | 150 ms" +`; diff --git a/__tests__/cli/site/pages.test.js b/__tests__/cli/rum/config.test.js similarity index 50% rename from __tests__/cli/site/pages.test.js rename to __tests__/cli/rum/config.test.js index bf1b9567..47c30051 100644 --- a/__tests__/cli/site/pages.test.js +++ b/__tests__/cli/rum/config.test.js @@ -4,15 +4,15 @@ import { teardownIntegrationServer } from '../../utils' -import listPages from '../../fixtures/listPages.json' +import rumConfig from '../../fixtures/rumConfig.json' -describe('site snapshots', () => { - beforeAll(async () => await setupIntegrationServer(listPages)) +describe('rum config', () => { + beforeAll(async () => await setupIntegrationServer(rumConfig)) afterAll(async () => await teardownIntegrationServer()) - test('lists all snapshots', async () => { + test('displays rum config', async () => { const out = await runCLI({ - args: 'site pages --site=test' + args: 'rum config --site=test' }) expect(out).toMatchSnapshot() }) diff --git a/__tests__/cli/rum/history.test.js b/__tests__/cli/rum/history.test.js new file mode 100644 index 00000000..3d2ba59c --- /dev/null +++ b/__tests__/cli/rum/history.test.js @@ -0,0 +1,19 @@ +import { + runCLI, + setupIntegrationServer, + teardownIntegrationServer +} from '../../utils' + +import rumHistory from '../../fixtures/rumHistory.json' + +describe('rum history', () => { + beforeAll(async () => await setupIntegrationServer(rumHistory)) + afterAll(async () => await teardownIntegrationServer()) + + test('displays rum history', async () => { + const out = await runCLI({ + args: 'rum history --site=test' + }) + expect(out).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/rum/pages.test.js b/__tests__/cli/rum/pages.test.js new file mode 100644 index 00000000..fa541cea --- /dev/null +++ b/__tests__/cli/rum/pages.test.js @@ -0,0 +1,19 @@ +import { + runCLI, + setupIntegrationServer, + teardownIntegrationServer +} from '../../utils' + +import rumPages from '../../fixtures/rumPages.json' + +describe('rum pages', () => { + beforeAll(async () => await setupIntegrationServer(rumPages)) + afterAll(async () => await teardownIntegrationServer()) + + test('displays rum pages', async () => { + const out = await runCLI({ + args: 'rum pages --site=test' + }) + expect(out).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/site.test.js b/__tests__/cli/site.test.js index 6ae99de5..b3f8be69 100644 --- a/__tests__/cli/site.test.js +++ b/__tests__/cli/site.test.js @@ -1,11 +1,6 @@ import { runCLI } from '../utils' -test('missing site argument', async () => { - const out = await runCLI({ args: 'site pages', testForError: true }) - expect(out).toMatchSnapshot() -}) - -test('missing site argument value', async () => { - const out = await runCLI({ args: 'site pages --site', testForError: true }) +test('missing subcommand', async () => { + const out = await runCLI({ args: 'site', testForError: true }) expect(out).toMatchSnapshot() }) diff --git a/__tests__/cli/site/__snapshots__/create.test.js.snap b/__tests__/cli/site/__snapshots__/create.test.js.snap new file mode 100644 index 00000000..fe9c0f84 --- /dev/null +++ b/__tests__/cli/site/__snapshots__/create.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`site create creates site 1`] = `""`; diff --git a/__tests__/cli/site/__snapshots__/delete-deploy.test.js.snap b/__tests__/cli/site/__snapshots__/delete-deploy.test.js.snap deleted file mode 100644 index 06a7a358..00000000 --- a/__tests__/cli/site/__snapshots__/delete-deploy.test.js.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`site delete-deploy deletes deploy 1`] = ` -"[calibre:deprecated] "site delete-deploy" has moved to "deploy delete" -- Connecting to Calibre -✔ Deploy deleted: 8c68b9f6-b5bf-49bc-abd7-896db6338818" -`; - -exports[`site delete-deploy requires uuid 1`] = ` -" - - Missing required argument: uuid - - calibre site delete-deploy [options] - -Options: - - --help Show help [boolean] - --version Show version number [boolean] - --site A unique slug identifying each Site. You can find it in Site - Settings → General or by using the calibre site list command. - [string] [required] - --uuid The UUID of the deploy. [required] - --confirm Use this flag to confirm the deletion of the selected deploy. - --json Outputs the results of the command in JSON format." -`; diff --git a/__tests__/cli/site/__snapshots__/delete-snapshot.test.js.snap b/__tests__/cli/site/__snapshots__/delete-snapshot.test.js.snap deleted file mode 100644 index 0f627053..00000000 --- a/__tests__/cli/site/__snapshots__/delete-snapshot.test.js.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`site delete-snapshot deletes snapshot 1`] = ` -"[calibre:deprecated] "site delete-snapshot" has moved to "synthetic delete-snapshot" -- Connecting to Calibre -✔ Snapshot deleted: 14" -`; - -exports[`site delete-snapshot requires id 1`] = ` -" - - Missing required argument: id - - calibre site delete-snapshot [options] - -Options: - - --help Show help [boolean] - --version Show version number [boolean] - --site A unique slug identifying each Site. You can find it in Site - Settings → General or by using the calibre site list command. - [string] [required] - --id The id of the Snapshot. [required] - --confirm Use this flag to confirm the deletion of the selected Snapshot. - --json Outputs the results of the command in JSON format." -`; diff --git a/__tests__/cli/site/__snapshots__/delete.test.js.snap b/__tests__/cli/site/__snapshots__/delete.test.js.snap new file mode 100644 index 00000000..51e0f7fa --- /dev/null +++ b/__tests__/cli/site/__snapshots__/delete.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`site delete deletes site 1`] = `""`; diff --git a/__tests__/cli/site/__snapshots__/get-snapshot-metrics.test.js.snap b/__tests__/cli/site/__snapshots__/get-snapshot-metrics.test.js.snap deleted file mode 100644 index bbf3b066..00000000 --- a/__tests__/cli/site/__snapshots__/get-snapshot-metrics.test.js.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`snapshot metrics returns no snapshot error 1`] = ` -"[calibre:deprecated] "site get-snapshot-metrics" has moved to "synthetic get-snapshot-metrics" -- Connecting to Calibre -✖ Connecting to Calibre - - - Error: Snapshot not found - - calibre site get-snapshot-metrics [options] - -Options: - - --help Show help [boolean] - --version Show version number [boolean] - --site A unique slug identifying each Site. You can find it in Site - Settings → General or by using the calibre site list command. - [string] [required] - --snapshot The id of a Snapshot. [required] - --json Outputs the results of the command in JSON format. - --csv Outputs the results of the command in CSV format." -`; diff --git a/__tests__/cli/site/__snapshots__/list.test.js.snap b/__tests__/cli/site/__snapshots__/list.test.js.snap new file mode 100644 index 00000000..ee0039af --- /dev/null +++ b/__tests__/cli/site/__snapshots__/list.test.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`site list lists all sites 1`] = ` +"2 sites +SLUG | NAME | MONITORING | CREATED +calibre | Calibre | synthetic crux rum | 10:30AM 15-Jan-2023 +example | Example Site | synthetic | 2:15PM 20-Mar-2024 " +`; diff --git a/__tests__/cli/site/__snapshots__/pages.test.js.snap b/__tests__/cli/site/__snapshots__/pages.test.js.snap deleted file mode 100644 index 95b3f7eb..00000000 --- a/__tests__/cli/site/__snapshots__/pages.test.js.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`site snapshots lists all snapshots 1`] = ` -"20 pages -UUID | NAME | URL -85b54dfe-6e12-4fc7-99ca-1422f3700fdb | Home | -6e95b705-0340-4b83-856d-ca7db00d6031 | About | -83c48abc-4df6-421a-af62-2e241e62daf9 | About | -cebc2df0-19f9-45ed-a1b3-776aba593a5e | Page 0 | -e7cfd3e3-64c7-4429-9420-1cded0065955 | Page 1 | -7291d0ab-1108-46ef-839a-557efc872756 | Page 2 | -f2e289c4-ee5b-41c9-baf1-18ad796eea27 | Page 3 | -8f6d9254-09f9-42db-a44a-b405ef9884b4 | Page 4 | -d4aaebcf-0343-4f4b-8c27-9bfa6f056f41 | Page 5 | -323a3713-c159-4007-816a-1ec133e5f94e | Page 6 | -3319b9ce-8464-4834-a584-21cedb154e54 | Page 7 | -9f1e4d8a-f50a-4571-877f-35341cc1a588 | Page 8 | -ef090fd1-3598-4820-b519-3e5c59f3afb2 | Page 9 | -4f8b55b4-04c4-4679-8370-70c5ea1746e2 | Page 10 | -eedfc447-ecbb-4f1e-a0d5-7c0eb736001c | Page 11 | -ba46b351-dff0-45c3-9aa9-c738985034d3 | Page 12 | -649878bc-e8a4-4201-9fa6-687c6d874db7 | Page 13 | -5242679c-1379-4128-a265-198b138069c6 | Page 14 | -47c587f1-f372-434e-bea6-8a10960e9d53 | Page 15 | -fb4dcfec-64e0-4671-beab-f505f7502f33 | Page 16 | -To see pages after Page 16, run: calibre site pages --site=calibre --cursor=MjA" -`; diff --git a/__tests__/cli/site/__snapshots__/pull-request-review.test.js.snap b/__tests__/cli/site/__snapshots__/pull-request-review.test.js.snap deleted file mode 100644 index 77f3c1df..00000000 --- a/__tests__/cli/site/__snapshots__/pull-request-review.test.js.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`display pull request review displays help 1`] = ` -"calibre site pull-request-review - -Options: - - --help Show help [boolean] - --version Show version number [boolean] - --site A unique slug identifying each Site. You can find it in Site - Settings → General or by using the calibre site list command. - [string] [required] - --json Outputs the results of the command in JSON format. - --markdown Outputs the results of the command in Markdown format." -`; - -exports[`display pull request review displays review 1`] = ` -"# Markdown here - -" -`; - -exports[`display pull request review displays review json 1`] = ` -"{ - "title": "My Pull Request", - "status": "completed", - "branch": "author/branch-name", - "markdownReport": "# Markdown here" -}" -`; - -exports[`display pull request review displays review markdown 1`] = `"# Markdown here"`; diff --git a/__tests__/cli/site/__snapshots__/snapshots.test.js.snap b/__tests__/cli/site/__snapshots__/snapshots.test.js.snap deleted file mode 100644 index be9ddce1..00000000 --- a/__tests__/cli/site/__snapshots__/snapshots.test.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`site snapshots lists all snapshots 1`] = ` -"2 snapshots -ID | URL | REF | CLIENT | STATUS -14 | http://localhost:3000/calibre/calibre/snapshots/14 | | | Completed 12:00AM 6-Feb-2019 -13 | http://localhost:3000/calibre/calibre/snapshots/13 | | | Completed 12:22AM 30-Jan-2019 -To see snapshots after 13, run: calibre site snapshots --site=calibre --cursor=MjU==" -`; diff --git a/__tests__/cli/site/create.test.js b/__tests__/cli/site/create.test.js new file mode 100644 index 00000000..c8a66021 --- /dev/null +++ b/__tests__/cli/site/create.test.js @@ -0,0 +1,19 @@ +import { + runCLI, + setupIntegrationServer, + teardownIntegrationServer +} from '../../utils' + +import createSite from '../../fixtures/createSite.json' + +describe('site create', () => { + beforeAll(async () => await setupIntegrationServer(createSite)) + afterAll(async () => await teardownIntegrationServer()) + + test('creates site', async () => { + const out = await runCLI({ + args: 'site create "New Site" --url=https://new-site.com --location=Sydney' + }) + expect(out).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/site/delete.test.js b/__tests__/cli/site/delete.test.js new file mode 100644 index 00000000..7e306d60 --- /dev/null +++ b/__tests__/cli/site/delete.test.js @@ -0,0 +1,19 @@ +import { + runCLI, + setupIntegrationServer, + teardownIntegrationServer +} from '../../utils' + +import deleteSite from '../../fixtures/deleteSite.json' + +describe('site delete', () => { + beforeAll(async () => await setupIntegrationServer(deleteSite)) + afterAll(async () => await teardownIntegrationServer()) + + test('deletes site', async () => { + const out = await runCLI({ + args: 'site delete test-site' + }) + expect(out).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/site/get-snapshot-metrics.test.js b/__tests__/cli/site/get-snapshot-metrics.test.js deleted file mode 100644 index 615d4869..00000000 --- a/__tests__/cli/site/get-snapshot-metrics.test.js +++ /dev/null @@ -1,19 +0,0 @@ -import { - runCLI, - setupIntegrationServer, - teardownIntegrationServer -} from '../../utils' - -import snapshotMetrics from '../../fixtures/snapshotMetrics-no-snapshot.json' - -describe('snapshot metrics', () => { - beforeAll(async () => await setupIntegrationServer(snapshotMetrics)) - afterAll(async () => await teardownIntegrationServer()) - - test('returns no snapshot error', async () => { - const out = await runCLI({ - args: 'site get-snapshot-metrics --site=test --snapshot=1000' - }) - expect(out).toMatchSnapshot() - }) -}) diff --git a/__tests__/cli/site/list.test.js b/__tests__/cli/site/list.test.js new file mode 100644 index 00000000..7a6ef3ae --- /dev/null +++ b/__tests__/cli/site/list.test.js @@ -0,0 +1,19 @@ +import { + runCLI, + setupIntegrationServer, + teardownIntegrationServer +} from '../../utils' + +import listSites from '../../fixtures/listSites.json' + +describe('site list', () => { + beforeAll(async () => await setupIntegrationServer(listSites)) + afterAll(async () => await teardownIntegrationServer()) + + test('lists all sites', async () => { + const out = await runCLI({ + args: 'site list' + }) + expect(out).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/site/snapshots.test.js b/__tests__/cli/site/snapshots.test.js deleted file mode 100644 index a85eec22..00000000 --- a/__tests__/cli/site/snapshots.test.js +++ /dev/null @@ -1,19 +0,0 @@ -import { - runCLI, - setupIntegrationServer, - teardownIntegrationServer -} from '../../utils' - -import listSnapshots from '../../fixtures/listSnapshots.json' - -describe('site snapshots', () => { - beforeAll(async () => await setupIntegrationServer(listSnapshots)) - afterAll(async () => await teardownIntegrationServer()) - - test('lists all snapshots', async () => { - const out = await runCLI({ - args: 'site snapshots --site=test' - }) - expect(out).toMatchSnapshot() - }) -}) diff --git a/__tests__/cli/synthetic/__snapshots__/connections.test.js.snap b/__tests__/cli/synthetic/__snapshots__/connections.test.js.snap new file mode 100644 index 00000000..ca56d58d --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/connections.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic connections lists connections 1`] = ` +"3 emulated connection speeds +IDENTIFIER +cable +LTE +regular3G " +`; diff --git a/__tests__/cli/synthetic/__snapshots__/create-page.test.js.snap b/__tests__/cli/synthetic/__snapshots__/create-page.test.js.snap new file mode 100644 index 00000000..0ee038a9 --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/create-page.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic create-page creates page 1`] = `""`; diff --git a/__tests__/cli/synthetic/__snapshots__/create-snapshot.test.js.snap b/__tests__/cli/synthetic/__snapshots__/create-snapshot.test.js.snap new file mode 100644 index 00000000..494e70f8 --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/create-snapshot.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic create-snapshot creates snapshot 1`] = `""`; diff --git a/__tests__/cli/synthetic/__snapshots__/create-test-profile.test.js.snap b/__tests__/cli/synthetic/__snapshots__/create-test-profile.test.js.snap new file mode 100644 index 00000000..3741d9bb --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/create-test-profile.test.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic create-test-profile creates test profile 1`] = ` +"⬢ New Profile (profile-uuid-123) +Created: 10:30AM 15-Jan-2024 +Updated: 10:30AM 15-Jan-2024 + +• Desktop +• Cable" +`; diff --git a/__tests__/cli/synthetic/__snapshots__/delete-page.test.js.snap b/__tests__/cli/synthetic/__snapshots__/delete-page.test.js.snap new file mode 100644 index 00000000..7a25fb3a --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/delete-page.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic delete-page deletes page 1`] = `""`; diff --git a/__tests__/cli/synthetic/__snapshots__/delete-snapshot.test.js.snap b/__tests__/cli/synthetic/__snapshots__/delete-snapshot.test.js.snap new file mode 100644 index 00000000..329c5a9c --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/delete-snapshot.test.js.snap @@ -0,0 +1,6 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic delete-snapshot deletes snapshot 1`] = ` +"- Connecting to Calibre +✔ Snapshot deleted: 14" +`; diff --git a/__tests__/cli/synthetic/__snapshots__/delete-test-profile.test.js.snap b/__tests__/cli/synthetic/__snapshots__/delete-test-profile.test.js.snap new file mode 100644 index 00000000..766f3d8c --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/delete-test-profile.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic delete-test-profile deletes test profile 1`] = `""`; diff --git a/__tests__/cli/synthetic/__snapshots__/devices.test.js.snap b/__tests__/cli/synthetic/__snapshots__/devices.test.js.snap new file mode 100644 index 00000000..73c91613 --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/devices.test.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic devices lists devices 1`] = ` +"2 test devices +IDENTIFIER | NAME | TYPE | SCREEN WIDTH | SCREEN HEIGHT | STATUS +Desktop | Desktop | desktop | 1920 | 1080 | Current +Pixel5 | Pixel 5 | mobile | 393 | 851 | Current" +`; diff --git a/__tests__/cli/synthetic/__snapshots__/download-artifacts.test.js.snap b/__tests__/cli/synthetic/__snapshots__/download-artifacts.test.js.snap new file mode 100644 index 00000000..26d4b275 --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/download-artifacts.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic download-artifacts downloads artifacts 1`] = `"The "path" argument must be of type string. Received undefined"`; diff --git a/__tests__/cli/synthetic/__snapshots__/get-snapshot-metrics.test.js.snap b/__tests__/cli/synthetic/__snapshots__/get-snapshot-metrics.test.js.snap index 6b6a85ad..985fc373 100644 --- a/__tests__/cli/synthetic/__snapshots__/get-snapshot-metrics.test.js.snap +++ b/__tests__/cli/synthetic/__snapshots__/get-snapshot-metrics.test.js.snap @@ -2,23 +2,5 @@ exports[`synthetic get-snapshot-metrics returns no snapshot error 1`] = ` "- Connecting to Calibre -✖ Connecting to Calibre - - - Error: Snapshot not found - - calibre synthetic get-snapshot-metrics [options] - -Get all metrics of a given Snapshot. - -Options: - - --help Show help [boolean] - --version Show version number [boolean] - --site A unique slug identifying each Site. You can find it in Site - Settings → General or by using the calibre site list command. - [string] [required] - --snapshot The id of a Snapshot. [required] - --json Outputs the results of the command in JSON format. - --csv Outputs the results of the command in CSV format." +Snapshot not found" `; diff --git a/__tests__/cli/synthetic/__snapshots__/locations.test.js.snap b/__tests__/cli/synthetic/__snapshots__/locations.test.js.snap new file mode 100644 index 00000000..c9969fe7 --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/locations.test.js.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic locations lists locations 1`] = ` +"2 test locations +IDENTIFIER | NAME | IPV4 ADDRESS +Sydney | Sydney | 1.2.3.4 +London | London | 5.6.7.8 " +`; diff --git a/__tests__/cli/synthetic/__snapshots__/metrics.test.js.snap b/__tests__/cli/synthetic/__snapshots__/metrics.test.js.snap new file mode 100644 index 00000000..536029be --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/metrics.test.js.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic metrics lists metrics 1`] = ` +" +Metric history + + +First Contentful Paint + +Home +Desktop - Home + + Min: 1.80 sec + Max: 1.80 sec + Median: 1.80 sec + p95: 1.80 sec + + +" +`; diff --git a/__tests__/cli/synthetic/__snapshots__/pages.test.js.snap b/__tests__/cli/synthetic/__snapshots__/pages.test.js.snap index 7471ad5a..23462c91 100644 --- a/__tests__/cli/synthetic/__snapshots__/pages.test.js.snap +++ b/__tests__/cli/synthetic/__snapshots__/pages.test.js.snap @@ -23,5 +23,5 @@ ba46b351-dff0-45c3-9aa9-c738985034d3 | Page 12 | 5242679c-1379-4128-a265-198b138069c6 | Page 14 | 47c587f1-f372-434e-bea6-8a10960e9d53 | Page 15 | fb4dcfec-64e0-4671-beab-f505f7502f33 | Page 16 | -To see pages after Page 16, run: calibre site pages --site=calibre --cursor=MjA" +To see pages after Page 16, run: calibre synthetic pages --site=test --cursor=MjA" `; diff --git a/__tests__/cli/synthetic/__snapshots__/pull-request-review.test.js.snap b/__tests__/cli/synthetic/__snapshots__/pull-request-review.test.js.snap new file mode 100644 index 00000000..895931ba --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/pull-request-review.test.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic pull-request-review displays help 1`] = ` +"calibre synthetic pull-request-review + +See the results of a Pull Request Review. + +Options: + + --help Show help [boolean] + --version Show version number [boolean] + --site The Site slug. Resolved from calibre.json if present, or pass + explicitly. You can find it in Site Settings → General or by using + the calibre site list command [string] [default: "calibre-app"] + --json Outputs the results of the command in JSON format. + --markdown Outputs the results of the command in Markdown format." +`; + +exports[`synthetic pull-request-review displays review 1`] = ` +"# Markdown here + +" +`; + +exports[`synthetic pull-request-review displays review json 1`] = ` +"{ + "title": "My Pull Request", + "status": "completed", + "branch": "author/branch-name", + "markdownReport": "# Markdown here" +}" +`; + +exports[`synthetic pull-request-review displays review markdown 1`] = `"# Markdown here"`; diff --git a/__tests__/cli/site/__snapshots__/pull-request-reviews.test.js.snap b/__tests__/cli/synthetic/__snapshots__/pull-request-reviews.test.js.snap similarity index 64% rename from __tests__/cli/site/__snapshots__/pull-request-reviews.test.js.snap rename to __tests__/cli/synthetic/__snapshots__/pull-request-reviews.test.js.snap index cefb0a00..07136629 100644 --- a/__tests__/cli/site/__snapshots__/pull-request-reviews.test.js.snap +++ b/__tests__/cli/synthetic/__snapshots__/pull-request-reviews.test.js.snap @@ -1,25 +1,27 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`list pull request reviews displays help 1`] = ` -"calibre site pull-request-reviews [options] +exports[`synthetic pull-request-reviews displays help 1`] = ` +"calibre synthetic pull-request-reviews [options] + +List Pull Request Reviews for a selected Site. Options: --help Show help [boolean] --version Show version number [boolean] - --site A unique slug identifying each Site. You can find it in Site - Settings → General or by using the calibre site list command. - [string] [required] + --site The Site slug. Resolved from calibre.json if present, or pass + explicitly. You can find it in Site Settings → General or by using + the calibre site list command [string] [default: "calibre-app"] --json Outputs the results of the command in JSON format." `; -exports[`list pull request reviews lists pull request reviews 1`] = ` +exports[`synthetic pull-request-reviews lists pull request reviews 1`] = ` "TITLE | BRANCH | SHA | STATUS | CREATED First Test Pull Request | author/my-branch | 241ccbb… | completed | 6:48AM 22-Aug-2023 Second Test Pull Request | author/my-branch-2 | 78f9a44… | completed | 7:31AM 15-Aug-2023" `; -exports[`list pull request reviews lists pull request reviews as json 1`] = ` +exports[`synthetic pull-request-reviews lists pull request reviews as json 1`] = ` "[ { "title": "First Test Pull Request", diff --git a/__tests__/cli/synthetic/__snapshots__/snapshots.test.js.snap b/__tests__/cli/synthetic/__snapshots__/snapshots.test.js.snap index a2c2c0f9..f1c69f52 100644 --- a/__tests__/cli/synthetic/__snapshots__/snapshots.test.js.snap +++ b/__tests__/cli/synthetic/__snapshots__/snapshots.test.js.snap @@ -5,5 +5,5 @@ exports[`synthetic snapshots lists all snapshots 1`] = ` ID | URL | REF | CLIENT | STATUS 14 | http://localhost:3000/calibre/calibre/snapshots/14 | | | Completed 12:00AM 6-Feb-2019 13 | http://localhost:3000/calibre/calibre/snapshots/13 | | | Completed 12:22AM 30-Jan-2019 -To see snapshots after 13, run: calibre site snapshots --site=calibre --cursor=MjU==" +To see snapshots after 13, run: calibre synthetic snapshots --site=test --cursor=MjU==" `; diff --git a/__tests__/cli/synthetic/__snapshots__/test-profiles.test.js.snap b/__tests__/cli/synthetic/__snapshots__/test-profiles.test.js.snap new file mode 100644 index 00000000..d5be9ac0 --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/test-profiles.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic test-profiles lists test profiles 1`] = ` +" + +UUID | NAME | DEVICE | CONNECTION | JAVASCRIPT DISABLED | COOKIES +prof1 | Default | Desktop | Cable | No | None +prof2 | Mobile | Pixel 5 | 4G | No | session" +`; diff --git a/__tests__/cli/synthetic/__snapshots__/update-page.test.js.snap b/__tests__/cli/synthetic/__snapshots__/update-page.test.js.snap new file mode 100644 index 00000000..0f3e36b9 --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/update-page.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic update-page updates page 1`] = `""`; diff --git a/__tests__/cli/synthetic/__snapshots__/update-test-profile.test.js.snap b/__tests__/cli/synthetic/__snapshots__/update-test-profile.test.js.snap new file mode 100644 index 00000000..6b218498 --- /dev/null +++ b/__tests__/cli/synthetic/__snapshots__/update-test-profile.test.js.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`synthetic update-test-profile updates test profile 1`] = ` +"⬢ Updated Profile (profile-uuid-123) +Created: 10:30AM 15-Jan-2024 +Updated: 11:45AM 15-Jan-2024 + +• Desktop +• Cable" +`; diff --git a/__tests__/cli/synthetic/connections.test.js b/__tests__/cli/synthetic/connections.test.js new file mode 100644 index 00000000..44bc7c1a --- /dev/null +++ b/__tests__/cli/synthetic/connections.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import listConnections from '../../fixtures/listConnections.json' +describe('synthetic connections', () => { + beforeAll(async () => await setupIntegrationServer(listConnections)) + afterAll(async () => await teardownIntegrationServer()) + test('lists connections', async () => { + expect(await runCLI({args: 'synthetic connections --site=test'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/synthetic/create-page.test.js b/__tests__/cli/synthetic/create-page.test.js new file mode 100644 index 00000000..f90172b5 --- /dev/null +++ b/__tests__/cli/synthetic/create-page.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import createPage from '../../fixtures/createPage.json' +describe('synthetic create-page', () => { + beforeAll(async () => await setupIntegrationServer(createPage)) + afterAll(async () => await teardownIntegrationServer()) + test('creates page', async () => { + expect(await runCLI({args: 'synthetic create-page "New Page" --url=https://example.com/new --site=test'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/synthetic/create-snapshot.test.js b/__tests__/cli/synthetic/create-snapshot.test.js new file mode 100644 index 00000000..7c5e64fb --- /dev/null +++ b/__tests__/cli/synthetic/create-snapshot.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import createSnapshot from '../../fixtures/createSnapshot.json' +describe('synthetic create-snapshot', () => { + beforeAll(async () => await setupIntegrationServer(createSnapshot)) + afterAll(async () => await teardownIntegrationServer()) + test('creates snapshot', async () => { + expect(await runCLI({args: 'synthetic create-snapshot page-uuid-123 --site=test'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/synthetic/create-test-profile.test.js b/__tests__/cli/synthetic/create-test-profile.test.js new file mode 100644 index 00000000..08c80785 --- /dev/null +++ b/__tests__/cli/synthetic/create-test-profile.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import createTestProfile from '../../fixtures/createTestProfile.json' +describe('synthetic create-test-profile', () => { + beforeAll(async () => await setupIntegrationServer(createTestProfile)) + afterAll(async () => await teardownIntegrationServer()) + test('creates test profile', async () => { + expect(await runCLI({args: 'synthetic create-test-profile "New Profile" --site=test'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/synthetic/delete-page.test.js b/__tests__/cli/synthetic/delete-page.test.js new file mode 100644 index 00000000..691db941 --- /dev/null +++ b/__tests__/cli/synthetic/delete-page.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import deletePage from '../../fixtures/deletePage.json' +describe('synthetic delete-page', () => { + beforeAll(async () => await setupIntegrationServer(deletePage)) + afterAll(async () => await teardownIntegrationServer()) + test('deletes page', async () => { + expect(await runCLI({args: 'synthetic delete-page --uuid=page-uuid-123 --site=test --confirm'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/site/delete-snapshot.test.js b/__tests__/cli/synthetic/delete-snapshot.test.js similarity index 60% rename from __tests__/cli/site/delete-snapshot.test.js rename to __tests__/cli/synthetic/delete-snapshot.test.js index 4a55288c..f810fc91 100644 --- a/__tests__/cli/site/delete-snapshot.test.js +++ b/__tests__/cli/synthetic/delete-snapshot.test.js @@ -6,21 +6,13 @@ import { import deleteSnapshot from '../../fixtures/deleteSnapshot.json' -describe('site delete-snapshot', () => { +describe('synthetic delete-snapshot', () => { beforeAll(async () => await setupIntegrationServer(deleteSnapshot)) afterAll(async () => await teardownIntegrationServer()) - test('requires id', async () => { - const out = await runCLI({ - args: 'site delete-snapshot --site=test', - testForError: true - }) - expect(out).toMatchSnapshot() - }) - test('deletes snapshot', async () => { const out = await runCLI({ - args: 'site delete-snapshot --site=test --id=1', + args: 'synthetic delete-snapshot --site=test --id=1', testForError: true }) expect(out).toMatchSnapshot() diff --git a/__tests__/cli/synthetic/delete-test-profile.test.js b/__tests__/cli/synthetic/delete-test-profile.test.js new file mode 100644 index 00000000..25d3f725 --- /dev/null +++ b/__tests__/cli/synthetic/delete-test-profile.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import deleteTestProfile from '../../fixtures/deleteTestProfile.json' +describe('synthetic delete-test-profile', () => { + beforeAll(async () => await setupIntegrationServer(deleteTestProfile)) + afterAll(async () => await teardownIntegrationServer()) + test('deletes test profile', async () => { + expect(await runCLI({args: 'synthetic delete-test-profile --uuid=profile-uuid-123 --site=test --confirm'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/synthetic/devices.test.js b/__tests__/cli/synthetic/devices.test.js new file mode 100644 index 00000000..f3ef3bd2 --- /dev/null +++ b/__tests__/cli/synthetic/devices.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import listDevices from '../../fixtures/listDevices.json' +describe('synthetic devices', () => { + beforeAll(async () => await setupIntegrationServer(listDevices)) + afterAll(async () => await teardownIntegrationServer()) + test('lists devices', async () => { + expect(await runCLI({args: 'synthetic devices --site=test'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/synthetic/download-artifacts.test.js b/__tests__/cli/synthetic/download-artifacts.test.js new file mode 100644 index 00000000..36e14ba5 --- /dev/null +++ b/__tests__/cli/synthetic/download-artifacts.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import downloadArtifacts from '../../fixtures/downloadArtifacts.json' +describe('synthetic download-artifacts', () => { + beforeAll(async () => await setupIntegrationServer(downloadArtifacts)) + afterAll(async () => await teardownIntegrationServer()) + test('downloads artifacts', async () => { + expect(await runCLI({args: 'synthetic download-artifacts --id=14 --site=test'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/synthetic/locations.test.js b/__tests__/cli/synthetic/locations.test.js new file mode 100644 index 00000000..79f9f9dd --- /dev/null +++ b/__tests__/cli/synthetic/locations.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import listLocations from '../../fixtures/listLocations.json' +describe('synthetic locations', () => { + beforeAll(async () => await setupIntegrationServer(listLocations)) + afterAll(async () => await teardownIntegrationServer()) + test('lists locations', async () => { + expect(await runCLI({args: 'synthetic locations --site=test'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/synthetic/metrics.test.js b/__tests__/cli/synthetic/metrics.test.js new file mode 100644 index 00000000..451d199e --- /dev/null +++ b/__tests__/cli/synthetic/metrics.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import timeSeriesMetrics from '../../fixtures/timeSeriesMetrics.json' +describe('synthetic metrics', () => { + beforeAll(async () => await setupIntegrationServer(timeSeriesMetrics)) + afterAll(async () => await teardownIntegrationServer()) + test('lists metrics', async () => { + expect(await runCLI({args: 'synthetic metrics --site=test'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/site/pull-request-review.test.js b/__tests__/cli/synthetic/pull-request-review.test.js similarity index 71% rename from __tests__/cli/site/pull-request-review.test.js rename to __tests__/cli/synthetic/pull-request-review.test.js index 85843de2..53ec9afb 100644 --- a/__tests__/cli/site/pull-request-review.test.js +++ b/__tests__/cli/synthetic/pull-request-review.test.js @@ -6,34 +6,34 @@ import { import getPullRequestReviewByBranch from '../../fixtures/getPullRequestReviewByBranch.json' -describe('display pull request review', () => { +describe('synthetic pull-request-review', () => { beforeAll(async () => await setupIntegrationServer(getPullRequestReviewByBranch)) afterAll(async () => await teardownIntegrationServer()) test('displays help', async () => { const out = await runCLI({ - args: 'site pull-request-review --help' + args: 'synthetic pull-request-review --help' }) expect(out).toMatchSnapshot() }) test('displays review', async () => { const out = await runCLI({ - args: 'site pull-request-review "author/my-pr" --site=test' + args: 'synthetic pull-request-review "author/my-pr" --site=test' }) expect(out).toMatchSnapshot() }) test('displays review json', async () => { const out = await runCLI({ - args: 'site pull-request-review "author/my-pr" --site=test --json' + args: 'synthetic pull-request-review "author/my-pr" --site=test --json' }) expect(out).toMatchSnapshot() }) test('displays review markdown', async () => { const out = await runCLI({ - args: 'site pull-request-review "author/my-pr" --site=test --markdown' + args: 'synthetic pull-request-review "author/my-pr" --site=test --markdown' }) expect(out).toMatchSnapshot() }) diff --git a/__tests__/cli/site/pull-request-reviews.test.js b/__tests__/cli/synthetic/pull-request-reviews.test.js similarity index 75% rename from __tests__/cli/site/pull-request-reviews.test.js rename to __tests__/cli/synthetic/pull-request-reviews.test.js index 65adf7fa..986ae5c7 100644 --- a/__tests__/cli/site/pull-request-reviews.test.js +++ b/__tests__/cli/synthetic/pull-request-reviews.test.js @@ -6,27 +6,27 @@ import { import listPullRequestReviews from '../../fixtures/listPullRequestReviews.json' -describe('list pull request reviews', () => { +describe('synthetic pull-request-reviews', () => { beforeAll(async () => await setupIntegrationServer(listPullRequestReviews)) afterAll(async () => await teardownIntegrationServer()) test('displays help', async () => { const out = await runCLI({ - args: 'site pull-request-reviews --help' + args: 'synthetic pull-request-reviews --help' }) expect(out).toMatchSnapshot() }) test('lists pull request reviews', async () => { const out = await runCLI({ - args: 'site pull-request-reviews --site=test' + args: 'synthetic pull-request-reviews --site=test' }) expect(out).toMatchSnapshot() }) test('lists pull request reviews as json', async () => { const out = await runCLI({ - args: 'site pull-request-reviews --site=test --json' + args: 'synthetic pull-request-reviews --site=test --json' }) expect(out).toMatchSnapshot() }) diff --git a/__tests__/cli/synthetic/test-profiles.test.js b/__tests__/cli/synthetic/test-profiles.test.js new file mode 100644 index 00000000..a432ae95 --- /dev/null +++ b/__tests__/cli/synthetic/test-profiles.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import listTestProfiles from '../../fixtures/listTestProfiles.json' +describe('synthetic test-profiles', () => { + beforeAll(async () => await setupIntegrationServer(listTestProfiles)) + afterAll(async () => await teardownIntegrationServer()) + test('lists test profiles', async () => { + expect(await runCLI({args: 'synthetic test-profiles --site=test'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/synthetic/update-page.test.js b/__tests__/cli/synthetic/update-page.test.js new file mode 100644 index 00000000..7e77cd9d --- /dev/null +++ b/__tests__/cli/synthetic/update-page.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import updatePage from '../../fixtures/updatePage.json' +describe('synthetic update-page', () => { + beforeAll(async () => await setupIntegrationServer(updatePage)) + afterAll(async () => await teardownIntegrationServer()) + test('updates page', async () => { + expect(await runCLI({args: 'synthetic update-page --uuid=page-uuid-123 --name="Updated Page" --site=test'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/synthetic/update-test-profile.test.js b/__tests__/cli/synthetic/update-test-profile.test.js new file mode 100644 index 00000000..69238746 --- /dev/null +++ b/__tests__/cli/synthetic/update-test-profile.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import updateTestProfile from '../../fixtures/updateTestProfile.json' +describe('synthetic update-test-profile', () => { + beforeAll(async () => await setupIntegrationServer(updateTestProfile)) + afterAll(async () => await teardownIntegrationServer()) + test('updates test profile', async () => { + expect(await runCLI({args: 'synthetic update-test-profile --uuid=profile-uuid-123 --name="Updated Profile" --site=test'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/team/__snapshots__/list.test.js.snap b/__tests__/cli/team/__snapshots__/list.test.js.snap index acd2e914..8cd51fa0 100644 --- a/__tests__/cli/team/__snapshots__/list.test.js.snap +++ b/__tests__/cli/team/__snapshots__/list.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`team list lists all teams 1`] = ` -"2 sites +"2 teams SLUG | NAME | DESCRIPTION calibre | Calibre | Default team engineering | Monitors | Engineering team" diff --git a/__tests__/cli/test/__snapshots__/download-artifacts.test.js.snap b/__tests__/cli/test/__snapshots__/download-artifacts.test.js.snap new file mode 100644 index 00000000..8a24c867 --- /dev/null +++ b/__tests__/cli/test/__snapshots__/download-artifacts.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`test download-artifacts downloads test artifacts 1`] = `"The "path" argument must be of type string. Received undefined"`; diff --git a/__tests__/cli/test/__snapshots__/list.test.js.snap b/__tests__/cli/test/__snapshots__/list.test.js.snap new file mode 100644 index 00000000..ba57880b --- /dev/null +++ b/__tests__/cli/test/__snapshots__/list.test.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`test list lists tests 1`] = ` +"♤ calibre test runs + +UUID | URL | DEVICE | CONNECTION | LOCATION | STATUS +test-1 | example.com/test1 | Desktop | Cable | SYD | Completed 10:30AM 15-Jan-2024 +test-2 | example.com/test2 | Pixel 5 | 4G | LON | Completed 9:20AM 14-Jan-2024 " +`; diff --git a/__tests__/cli/test/__snapshots__/show.test.js.snap b/__tests__/cli/test/__snapshots__/show.test.js.snap new file mode 100644 index 00000000..063252be --- /dev/null +++ b/__tests__/cli/test/__snapshots__/show.test.js.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`test show shows test details 1`] = ` +"# Test Results + +## Performance Metrics + + * LCP: 2.1s + * FCP: 1.8s + +All tests passed successfully. + +" +`; diff --git a/__tests__/cli/test/download-artifacts.test.js b/__tests__/cli/test/download-artifacts.test.js new file mode 100644 index 00000000..250dee01 --- /dev/null +++ b/__tests__/cli/test/download-artifacts.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import testDownloadArtifacts from '../../fixtures/testDownloadArtifacts.json' +describe('test download-artifacts', () => { + beforeAll(async () => await setupIntegrationServer(testDownloadArtifacts)) + afterAll(async () => await teardownIntegrationServer()) + test('downloads test artifacts', async () => { + expect(await runCLI({args: 'test download-artifacts test-1'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/test/list.test.js b/__tests__/cli/test/list.test.js new file mode 100644 index 00000000..a5311754 --- /dev/null +++ b/__tests__/cli/test/list.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import testList from '../../fixtures/testList.json' +describe('test list', () => { + beforeAll(async () => await setupIntegrationServer(testList)) + afterAll(async () => await teardownIntegrationServer()) + test('lists tests', async () => { + expect(await runCLI({args: 'test list'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/cli/test/show.test.js b/__tests__/cli/test/show.test.js new file mode 100644 index 00000000..71ff55db --- /dev/null +++ b/__tests__/cli/test/show.test.js @@ -0,0 +1,9 @@ +import {runCLI,setupIntegrationServer,teardownIntegrationServer} from '../../utils' +import testShow from '../../fixtures/testShow.json' +describe('test show', () => { + beforeAll(async () => await setupIntegrationServer(testShow)) + afterAll(async () => await teardownIntegrationServer()) + test('shows test details', async () => { + expect(await runCLI({args: 'test show test-1'})).toMatchSnapshot() + }) +}) diff --git a/__tests__/fixtures/createDeploy.json b/__tests__/fixtures/createDeploy.json new file mode 100644 index 00000000..8e7eead8 --- /dev/null +++ b/__tests__/fixtures/createDeploy.json @@ -0,0 +1,10 @@ +{ + "data": { + "createDeploy": { + "uuid": "8c68b9f6-b5bf-49bc-abd7-896db6338999", + "revision": "abc123def456", + "repository": "https://github.com/calibreapp/example", + "username": "deployuser" + } + } +} diff --git a/__tests__/fixtures/createPage.json b/__tests__/fixtures/createPage.json new file mode 100644 index 00000000..e9abee3b --- /dev/null +++ b/__tests__/fixtures/createPage.json @@ -0,0 +1 @@ +{"data":{"createPage":{"uuid":"page-uuid-123","name":"New Page","url":"https://example.com/new"}}} diff --git a/__tests__/fixtures/createSite.json b/__tests__/fixtures/createSite.json new file mode 100644 index 00000000..bf1469a5 --- /dev/null +++ b/__tests__/fixtures/createSite.json @@ -0,0 +1,9 @@ +{ + "data": { + "createSite": { + "slug": "new-site", + "name": "New Site", + "url": "https://new-site.com" + } + } +} diff --git a/__tests__/fixtures/createSnapshot.json b/__tests__/fixtures/createSnapshot.json new file mode 100644 index 00000000..2804ad0f --- /dev/null +++ b/__tests__/fixtures/createSnapshot.json @@ -0,0 +1 @@ +{"data":{"createSnapshot":{"iid":15,"htmlUrl":"https://calibreapp.com/test/15"}}} diff --git a/__tests__/fixtures/createTestProfile.json b/__tests__/fixtures/createTestProfile.json new file mode 100644 index 00000000..17e51352 --- /dev/null +++ b/__tests__/fixtures/createTestProfile.json @@ -0,0 +1 @@ +{"data":{"createTestProfile":{"uuid":"profile-uuid-123","name":"New Profile","createdAt":"2024-01-15T10:30:00Z","updatedAt":"2024-01-15T10:30:00Z","device":{"title":"Desktop"},"bandwidth":{"title":"Cable"},"jsIsDisabled":false,"cookies":[]}}} diff --git a/__tests__/fixtures/cruxHistory.json b/__tests__/fixtures/cruxHistory.json new file mode 100644 index 00000000..69cde6c0 --- /dev/null +++ b/__tests__/fixtures/cruxHistory.json @@ -0,0 +1,52 @@ +{ + "data": { + "organisation": { + "site": { + "cruxHistory": [ + { + "metric": { + "name": "cruxLargestContentfulPaint", + "label": "Largest Contentful Paint", + "formatter": "humanDuration" + }, + "values": [ + { + "collectionPeriodStart": "2024-01-01T00:00:00Z", + "collectionPeriodEnd": "2024-01-31T23:59:59Z", + "p75Value": 2100, + "p75Grading": "good" + }, + { + "collectionPeriodStart": "2023-12-01T00:00:00Z", + "collectionPeriodEnd": "2023-12-31T23:59:59Z", + "p75Value": 2300, + "p75Grading": "average" + } + ] + }, + { + "metric": { + "name": "cruxCumulativeLayoutShift", + "label": "Cumulative Layout Shift", + "formatter": "unitless" + }, + "values": [ + { + "collectionPeriodStart": "2024-01-01T00:00:00Z", + "collectionPeriodEnd": "2024-01-31T23:59:59Z", + "p75Value": 0.05, + "p75Grading": "good" + }, + { + "collectionPeriodStart": "2023-12-01T00:00:00Z", + "collectionPeriodEnd": "2023-12-31T23:59:59Z", + "p75Value": 0.08, + "p75Grading": "average" + } + ] + } + ] + } + } + } +} diff --git a/__tests__/fixtures/cruxSummary.json b/__tests__/fixtures/cruxSummary.json index b4880b4c..b447f3c2 100644 --- a/__tests__/fixtures/cruxSummary.json +++ b/__tests__/fixtures/cruxSummary.json @@ -4,9 +4,9 @@ "site": { "cruxCvwAssessment": "good", "cruxFormFactorDensity": { - "desktop": 45, - "phone": 50, - "tablet": 5 + "desktop": 0.45, + "phone": 0.50, + "tablet": 0.05 }, "cruxAggregateMetrics": [ { diff --git a/__tests__/fixtures/cruxUrl.json b/__tests__/fixtures/cruxUrl.json new file mode 100644 index 00000000..969315b4 --- /dev/null +++ b/__tests__/fixtures/cruxUrl.json @@ -0,0 +1,57 @@ +{ + "data": { + "organisation": { + "site": { + "cruxUrl": { + "uuid": "123e4567-e89b-12d3-a456-426614174000", + "url": "https://example.com/page", + "name": "Example Page", + "status": "active", + "cruxCvwAssessment": "good", + "cruxFormFactorDensity": { + "desktop": 0.45, + "phone": 0.50, + "tablet": 0.05 + }, + "cruxAggregateMetrics": [ + { + "name": "cruxLargestContentfulPaint", + "value": 2100, + "grading": "good", + "metric": { + "label": "Largest Contentful Paint", + "formatter": "humanDuration" + } + }, + { + "name": "cruxCumulativeLayoutShift", + "value": 0.05, + "grading": "good", + "metric": { + "label": "Cumulative Layout Shift", + "formatter": "unitless" + } + } + ], + "cruxHistory": [ + { + "metric": { + "name": "cruxLargestContentfulPaint", + "label": "Largest Contentful Paint", + "formatter": "humanDuration" + }, + "values": [ + { + "collectionPeriodStart": "2024-01-01T00:00:00Z", + "collectionPeriodEnd": "2024-01-31T23:59:59Z", + "p75Value": 2100, + "p75Grading": "good" + } + ] + } + ] + } + } + } + } +} diff --git a/__tests__/fixtures/cruxUrls.json b/__tests__/fixtures/cruxUrls.json new file mode 100644 index 00000000..9ed81858 --- /dev/null +++ b/__tests__/fixtures/cruxUrls.json @@ -0,0 +1,70 @@ +{ + "data": { + "organisation": { + "site": { + "cruxUrlsList": { + "edges": [ + { + "node": { + "uuid": "123e4567-e89b-12d3-a456-426614174000", + "url": "https://example.com/page1", + "name": "Page 1", + "status": "active", + "cruxCvwAssessment": "good", + "cruxAggregateMetrics": [ + { + "name": "cruxLargestContentfulPaint", + "value": 2100, + "grading": "good", + "metric": { + "label": "Largest Contentful Paint", + "formatter": "humanDuration" + } + }, + { + "name": "cruxCumulativeLayoutShift", + "value": 0.05, + "grading": "good", + "metric": { + "label": "Cumulative Layout Shift", + "formatter": "unitless" + } + } + ] + } + }, + { + "node": { + "uuid": "223e4567-e89b-12d3-a456-426614174001", + "url": "https://example.com/page2", + "name": "Page 2", + "status": "active", + "cruxCvwAssessment": "average", + "cruxAggregateMetrics": [ + { + "name": "cruxLargestContentfulPaint", + "value": 3500, + "grading": "average", + "metric": { + "label": "Largest Contentful Paint", + "formatter": "humanDuration" + } + }, + { + "name": "cruxCumulativeLayoutShift", + "value": 0.15, + "grading": "average", + "metric": { + "label": "Cumulative Layout Shift", + "formatter": "unitless" + } + } + ] + } + } + ] + } + } + } + } +} diff --git a/__tests__/fixtures/deletePage.json b/__tests__/fixtures/deletePage.json new file mode 100644 index 00000000..72d60aff --- /dev/null +++ b/__tests__/fixtures/deletePage.json @@ -0,0 +1 @@ +{"data":{"deletePage":{"success":true}}} diff --git a/__tests__/fixtures/deleteSite.json b/__tests__/fixtures/deleteSite.json new file mode 100644 index 00000000..6d8b663d --- /dev/null +++ b/__tests__/fixtures/deleteSite.json @@ -0,0 +1,7 @@ +{ + "data": { + "deleteSite": { + "success": true + } + } +} diff --git a/__tests__/fixtures/deleteTestProfile.json b/__tests__/fixtures/deleteTestProfile.json new file mode 100644 index 00000000..69b7fba2 --- /dev/null +++ b/__tests__/fixtures/deleteTestProfile.json @@ -0,0 +1 @@ +{"data":{"deleteTestProfile":{"success":true}}} diff --git a/__tests__/fixtures/downloadArtifacts.json b/__tests__/fixtures/downloadArtifacts.json new file mode 100644 index 00000000..b48236a9 --- /dev/null +++ b/__tests__/fixtures/downloadArtifacts.json @@ -0,0 +1 @@ +{"data":{"organisation":{"site":{"snapshot":{"tests":[{"page":{"uuid":"page-1","name":"Home"},"testProfile":{"uuid":"prof-1","name":"Desktop"},"image":"http://localhost:5678/artifacts/14/image.jpg","video":"http://localhost:5678/artifacts/14/render.mp4","har":"http://localhost:5678/artifacts/14/requests.har","lighthouse":"http://localhost:5678/artifacts/14/lighthouse.json"}]}}}}} diff --git a/__tests__/fixtures/listConnections.json b/__tests__/fixtures/listConnections.json new file mode 100644 index 00000000..3ff5f60b --- /dev/null +++ b/__tests__/fixtures/listConnections.json @@ -0,0 +1 @@ +{"data":{"__type":{"enumValues":[{"name":"cable"},{"name":"LTE"},{"name":"regular3G"}]}}} diff --git a/__tests__/fixtures/listDevices.json b/__tests__/fixtures/listDevices.json new file mode 100644 index 00000000..1681ac4f --- /dev/null +++ b/__tests__/fixtures/listDevices.json @@ -0,0 +1 @@ +{"data":{"emulatedDevices":[{"tag":"Desktop","title":"Desktop","screenWidth":1920,"screenHeight":1080,"type":"desktop","isDiscontinued":false},{"tag":"Pixel5","title":"Pixel 5","screenWidth":393,"screenHeight":851,"type":"mobile","isDiscontinued":false}]}} diff --git a/__tests__/fixtures/listLocations.json b/__tests__/fixtures/listLocations.json new file mode 100644 index 00000000..f3f5d327 --- /dev/null +++ b/__tests__/fixtures/listLocations.json @@ -0,0 +1 @@ +{"data":{"locations":[{"name":"Sydney","shortName":"SYD","emoji":"🇦🇺","tag":"Sydney","agents":[{"ipv4":"1.2.3.4"}]},{"name":"London","shortName":"LON","emoji":"🇬🇧","tag":"London","agents":[{"ipv4":"5.6.7.8"}]}]}} diff --git a/__tests__/fixtures/listSites.json b/__tests__/fixtures/listSites.json new file mode 100644 index 00000000..ab8945e4 --- /dev/null +++ b/__tests__/fixtures/listSites.json @@ -0,0 +1,30 @@ +{ + "data": { + "organisation": { + "sites": [ + { + "slug": "calibre", + "name": "Calibre", + "url": "https://calibreapp.com", + "createdAt": "2023-01-15T10:30:00Z", + "monitoringStatus": { + "synthetic": null, + "crux": null, + "rum": null + } + }, + { + "slug": "example", + "name": "Example Site", + "url": "https://example.com", + "createdAt": "2024-03-20T14:15:00Z", + "monitoringStatus": { + "synthetic": null, + "crux": "CrUX not enabled", + "rum": "RUM not enabled" + } + } + ] + } + } +} diff --git a/__tests__/fixtures/listTestProfiles.json b/__tests__/fixtures/listTestProfiles.json new file mode 100644 index 00000000..8d513a69 --- /dev/null +++ b/__tests__/fixtures/listTestProfiles.json @@ -0,0 +1 @@ +{"data":{"organisation":{"site":{"testProfiles":[{"uuid":"prof1","name":"Default","device":{"title":"Desktop"},"bandwidth":{"title":"Cable"},"jsIsDisabled":false,"cookies":[]},{"uuid":"prof2","name":"Mobile","device":{"title":"Pixel 5"},"bandwidth":{"title":"4G"},"jsIsDisabled":false,"cookies":[{"name":"session"}]}]}}}} diff --git a/__tests__/fixtures/rumConfig.json b/__tests__/fixtures/rumConfig.json new file mode 100644 index 00000000..70192a47 --- /dev/null +++ b/__tests__/fixtures/rumConfig.json @@ -0,0 +1,22 @@ +{ + "data": { + "organisation": { + "site": { + "rumConfig": { + "enabled": true, + "sampleRate": 1.0, + "allowedOrigins": ["https://example.com", "https://www.example.com"], + "endpoint": "https://rum.calibreapp.com/collect", + "identifier": "rum_abc123", + "dataRetentionMonths": 12, + "maxDataRetentionMonths": 24, + "excludeEu": false, + "createdAt": "2023-01-01T00:00:00Z", + "updatedAt": "2024-01-01T00:00:00Z", + "suspendedAt": null, + "systemSuspended": false + } + } + } + } +} diff --git a/__tests__/fixtures/rumHistory.json b/__tests__/fixtures/rumHistory.json new file mode 100644 index 00000000..238b63f0 --- /dev/null +++ b/__tests__/fixtures/rumHistory.json @@ -0,0 +1,47 @@ +{ + "data": { + "organisation": { + "site": { + "rum": { + "metrics": [ + {"value": "lcp", "formatter": "humanDuration"}, + {"value": "cls", "formatter": "unitless"}, + {"value": "inp", "formatter": "humanDuration"}, + {"value": "fcp", "formatter": "humanDuration"}, + {"value": "ttfb", "formatter": "humanDuration"} + ], + "history": [ + { + "date": "2024-01-15T00:00:00Z", + "lcp": 2100, + "lcpGrading": "good", + "cls": 0.05, + "clsGrading": "good", + "inp": 180, + "inpGrading": "good", + "fcp": 1800, + "fcpGrading": "good", + "ttfb": 800, + "ttfbGrading": "average", + "sessionCount": 1250 + }, + { + "date": "2024-01-14T00:00:00Z", + "lcp": 2300, + "lcpGrading": "average", + "cls": 0.08, + "clsGrading": "average", + "inp": 200, + "inpGrading": "average", + "fcp": 1900, + "fcpGrading": "good", + "ttfb": 850, + "ttfbGrading": "average", + "sessionCount": 1180 + } + ] + } + } + } + } +} diff --git a/__tests__/fixtures/rumPages.json b/__tests__/fixtures/rumPages.json new file mode 100644 index 00000000..2d3f7ab2 --- /dev/null +++ b/__tests__/fixtures/rumPages.json @@ -0,0 +1,48 @@ +{ + "data": { + "organisation": { + "site": { + "rum": { + "totalCount": 3, + "metrics": [ + {"value": "lcp", "formatter": "humanDuration"}, + {"value": "cls", "formatter": "unitless"}, + {"value": "inp", "formatter": "humanDuration"} + ], + "aggregate": [ + { + "path": "/", + "sessionCount": 5000, + "lcp": 2100, + "lcpGrading": "good", + "cls": 0.05, + "clsGrading": "good", + "inp": 180, + "inpGrading": "good" + }, + { + "path": "/about", + "sessionCount": 1200, + "lcp": 2500, + "lcpGrading": "average", + "cls": 0.10, + "clsGrading": "average", + "inp": 220, + "inpGrading": "average" + }, + { + "path": "/contact", + "sessionCount": 800, + "lcp": 1900, + "lcpGrading": "good", + "cls": 0.03, + "clsGrading": "good", + "inp": 150, + "inpGrading": "good" + } + ] + } + } + } + } +} diff --git a/__tests__/fixtures/testDownloadArtifacts.json b/__tests__/fixtures/testDownloadArtifacts.json new file mode 100644 index 00000000..e176eaa7 --- /dev/null +++ b/__tests__/fixtures/testDownloadArtifacts.json @@ -0,0 +1 @@ +{"data":{"organisation":{"singlePageTest":{"uuid":"test-1","har":"http://localhost:5678/artifacts/test-1/requests.har","lighthouse":"http://localhost:5678/artifacts/test-1/lighthouse.json","image":"http://localhost:5678/artifacts/test-1/image.jpg","video":"http://localhost:5678/artifacts/test-1/render.mp4"}}}} diff --git a/__tests__/fixtures/testList.json b/__tests__/fixtures/testList.json new file mode 100644 index 00000000..84966b93 --- /dev/null +++ b/__tests__/fixtures/testList.json @@ -0,0 +1 @@ +{"data":{"organisation":{"singlePageTests":[{"uuid":"test-1","url":"https://example.com/test1","formattedTestUrl":"example.com/test1","status":"completed","updatedAt":"2024-01-15T10:30:00Z","isPrivate":false,"device":{"title":"Desktop"},"connection":{"title":"Cable"},"location":{"emoji":"🇦🇺","shortName":"SYD"}},{"uuid":"test-2","url":"https://example.com/test2","formattedTestUrl":"example.com/test2","status":"completed","updatedAt":"2024-01-14T09:20:00Z","isPrivate":false,"device":{"title":"Pixel 5"},"connection":{"title":"4G"},"location":{"emoji":"🇬🇧","shortName":"LON"}}]}}} diff --git a/__tests__/fixtures/testShow.json b/__tests__/fixtures/testShow.json new file mode 100644 index 00000000..2f7f480a --- /dev/null +++ b/__tests__/fixtures/testShow.json @@ -0,0 +1 @@ +{"data":{"organisation":{"singlePageTest":{"uuid":"test-1","url":"https://example.com","formattedTestUrl":"example.com","status":"completed","updatedAt":"2024-01-15T10:30:00Z","isPrivate":false,"runtimeError":null,"markdownReport":"# Test Results\n\n## Performance Metrics\n\n- LCP: 2.1s\n- FCP: 1.8s\n\nAll tests passed successfully.","metrics":[{"name":"largest-contentful-paint","label":"Largest Contentful Paint","value":2100}],"device":{"title":"Desktop"},"connection":{"title":"Cable"},"location":{"name":"Sydney","emoji":"🇦🇺"}}}}} diff --git a/__tests__/fixtures/timeSeriesMetrics.json b/__tests__/fixtures/timeSeriesMetrics.json new file mode 100644 index 00000000..7ab54c9b --- /dev/null +++ b/__tests__/fixtures/timeSeriesMetrics.json @@ -0,0 +1 @@ +{"data":{"organisation":{"site":{"timeSeries":{"times":[{"name":"Test 1","snapshot":14,"timestamp":"2024-01-15T10:30:00Z"}],"series":[{"name":"Desktop - Home","profile":"Desktop","page":"Home","measurement":"first-contentful-paint","values":[1800]}],"pages":[{"uuid":"page-1","name":"Home","url":"https://example.com"}],"testProfiles":[{"uuid":"prof-1","name":"Desktop","jsIsDisabled":false,"hasDeviceEmulation":false,"hasBandwidthEmulation":false,"isMobile":false}],"metrics":[{"name":"first-contentful-paint","label":"First Contentful Paint","formatter":"humanDuration","docsPath":"/docs/metrics/first-contentful-paint"}],"csv":""}}}}} diff --git a/__tests__/fixtures/updatePage.json b/__tests__/fixtures/updatePage.json new file mode 100644 index 00000000..0624c802 --- /dev/null +++ b/__tests__/fixtures/updatePage.json @@ -0,0 +1 @@ +{"data":{"updatePage":{"uuid":"page-uuid-123","name":"Updated Page"}}} diff --git a/__tests__/fixtures/updateTestProfile.json b/__tests__/fixtures/updateTestProfile.json new file mode 100644 index 00000000..3cb62b3f --- /dev/null +++ b/__tests__/fixtures/updateTestProfile.json @@ -0,0 +1 @@ +{"data":{"updateTestProfile":{"uuid":"profile-uuid-123","name":"Updated Profile","createdAt":"2024-01-15T10:30:00Z","updatedAt":"2024-01-15T11:45:00Z","device":{"title":"Desktop"},"bandwidth":{"title":"Cable"},"jsIsDisabled":false,"cookies":[]}}} diff --git a/__tests__/utils.js b/__tests__/utils.js index e3269f56..be5ff9a2 100644 --- a/__tests__/utils.js +++ b/__tests__/utils.js @@ -36,6 +36,24 @@ const startServer = (mockResponse, resolve, reject) => { res.status(200).json(mockResponse) }) + // Mock artifact downloads to prevent 404s + app.get(/^\/artifacts\/.*/, (req, res) => { + const path = req.path + if (path.endsWith('.jpg')) { + res.set('Content-Type', 'image/jpeg') + res.send(Buffer.from('mock-image-data')) + } else if (path.endsWith('.mp4')) { + res.set('Content-Type', 'video/mp4') + res.send(Buffer.from('mock-video-data')) + } else if (path.endsWith('.har')) { + res.json({ log: { version: '1.2', entries: [] } }) + } else if (path.endsWith('.json')) { + res.json({ mockLighthouseData: true }) + } else { + res.status(404).send('Not Found') + } + }) + server = app.listen(5678, (err) => { if (err) { reject(err) diff --git a/__tests__/utils/api-error.test.js b/__tests__/utils/api-error.test.js index dc4817aa..3e588bdb 100644 --- a/__tests__/utils/api-error.test.js +++ b/__tests__/utils/api-error.test.js @@ -1,4 +1,4 @@ -import { humaniseError } from '../../src/utils/api-error' +import { humaniseError, formatJsonError } from '../../src/utils/api-error' import erroredSite from '../fixtures/createSiteError.json' @@ -24,4 +24,15 @@ describe('apiError', () => { it('error string', () => { expect(humaniseError('Error string')).toMatchSnapshot() }) + + it('formatJsonError outputs structured JSON to stderr', () => { + const original = console.error + const calls = [] + console.error = (...args) => calls.push(args) + formatJsonError({ message: 'Something went wrong' }) + console.error = original + expect(calls).toEqual([ + [JSON.stringify({ error: 'Something went wrong' })] + ]) + }) }) diff --git a/__tests__/utils/deprecation.test.js b/__tests__/utils/deprecation.test.js deleted file mode 100644 index 3170ac77..00000000 --- a/__tests__/utils/deprecation.test.js +++ /dev/null @@ -1,44 +0,0 @@ -import { deprecatedHandler } from '../../src/utils/deprecation.js' - -describe('deprecatedHandler', () => { - let stderrOutput - const originalWrite = process.stderr.write - - beforeEach(() => { - stderrOutput = '' - process.stderr.write = chunk => { - stderrOutput += chunk - } - delete process.env.CALIBRE_SUPPRESS_DEPRECATIONS - }) - - afterEach(() => { - process.stderr.write = originalWrite - delete process.env.CALIBRE_SUPPRESS_DEPRECATIONS - }) - - test('prints deprecation warning to stderr', async () => { - let calledWith = null - const inner = args => { - calledWith = args - } - const handler = deprecatedHandler('site pages', 'synthetic pages', inner) - await handler({ site: 'test' }) - expect(stderrOutput).toContain('[calibre:deprecated]') - expect(stderrOutput).toContain('site pages') - expect(stderrOutput).toContain('synthetic pages') - expect(calledWith).toEqual({ site: 'test' }) - }) - - test('suppresses warning when CALIBRE_SUPPRESS_DEPRECATIONS is set', async () => { - process.env.CALIBRE_SUPPRESS_DEPRECATIONS = '1' - let called = false - const inner = () => { - called = true - } - const handler = deprecatedHandler('site pages', 'synthetic pages', inner) - await handler({ site: 'test' }) - expect(stderrOutput).toBe('') - expect(called).toBe(true) - }) -}) diff --git a/__tests__/utils/pagination.test.js b/__tests__/utils/pagination.test.js new file mode 100644 index 00000000..f08c093d --- /dev/null +++ b/__tests__/utils/pagination.test.js @@ -0,0 +1,82 @@ +import { fetchAll } from '../../src/utils/pagination' + +describe('fetchAll', () => { + it('returns items from a single page', async () => { + const calls = [] + const fetchFn = async args => { + calls.push(args) + return { + items: [{ id: 1 }, { id: 2 }], + pageInfo: { hasNextPage: false, endCursor: null } + } + } + + const results = await fetchAll(fetchFn, { site: 'test' }) + + expect(results).toEqual([{ id: 1 }, { id: 2 }]) + expect(calls).toHaveLength(1) + expect(calls[0]).toEqual({ + site: 'test', + count: 100, + cursor: undefined + }) + }) + + it('accumulates items across multiple pages', async () => { + const calls = [] + const responses = [ + { + items: [{ id: 1 }], + pageInfo: { hasNextPage: true, endCursor: 'cursor-1' } + }, + { + items: [{ id: 2 }], + pageInfo: { hasNextPage: true, endCursor: 'cursor-2' } + }, + { + items: [{ id: 3 }], + pageInfo: { hasNextPage: false, endCursor: null } + } + ] + const fetchFn = async args => { + calls.push(args) + return responses[calls.length - 1] + } + + const results = await fetchAll(fetchFn, { site: 'test' }) + + expect(results).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }]) + expect(calls).toHaveLength(3) + expect(calls[0]).toEqual({ + site: 'test', + count: 100, + cursor: undefined + }) + expect(calls[1]).toEqual({ + site: 'test', + count: 100, + cursor: 'cursor-1' + }) + expect(calls[2]).toEqual({ + site: 'test', + count: 100, + cursor: 'cursor-2' + }) + }) + + it('returns an empty array when the first page is empty', async () => { + const calls = [] + const fetchFn = async args => { + calls.push(args) + return { + items: [], + pageInfo: { hasNextPage: false, endCursor: null } + } + } + + const results = await fetchAll(fetchFn, { site: 'test' }) + + expect(results).toEqual([]) + expect(calls).toHaveLength(1) + }) +}) diff --git a/__tests__/utils/project-config.test.js b/__tests__/utils/project-config.test.js new file mode 100644 index 00000000..3a3fcf08 --- /dev/null +++ b/__tests__/utils/project-config.test.js @@ -0,0 +1,60 @@ +import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs' +import { join } from 'path' +import { tmpdir } from 'os' + +import { + findConfigFile, + loadProjectConfig, + CONFIG_FILENAME +} from '../../src/utils/project-config' + +describe('project-config', () => { + let tempDir + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'calibre-test-')) + }) + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }) + }) + + describe('findConfigFile', () => { + it('finds calibre.json in the current directory', () => { + const configPath = join(tempDir, CONFIG_FILENAME) + writeFileSync(configPath, JSON.stringify({ site: 'my-site' })) + + expect(findConfigFile(tempDir)).toBe(configPath) + }) + + it('finds calibre.json in a parent directory', () => { + const configPath = join(tempDir, CONFIG_FILENAME) + writeFileSync(configPath, JSON.stringify({ site: 'my-site' })) + + const nested = join(tempDir, 'a', 'b', 'c') + mkdirSync(nested, { recursive: true }) + + expect(findConfigFile(nested)).toBe(configPath) + }) + + it('returns null when no config exists', () => { + expect(findConfigFile(tempDir)).toBeNull() + }) + }) + + describe('loadProjectConfig', () => { + it('loads and parses config correctly', () => { + const config = { site: 'my-site' } + writeFileSync( + join(tempDir, CONFIG_FILENAME), + JSON.stringify(config) + ) + + expect(loadProjectConfig(tempDir)).toEqual(config) + }) + + it('returns empty object when no config exists', () => { + expect(loadProjectConfig(tempDir)).toEqual({}) + }) + }) +}) diff --git a/eslint.config.js b/eslint.config.js index 11d374cf..e6a84780 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,7 +5,7 @@ import pluginJest from 'eslint-plugin-jest' export default [ { - ignores: ['coverage/', 'examples/', 'dist/', 'eslint.config.js'] + ignores: ['coverage/', 'examples/', 'eslint.config.js'] }, js.configs.recommended, node.configs['flat/recommended-module'], @@ -21,6 +21,10 @@ export default [ rules: { 'no-console': 0, 'n/shebang': 0, + 'n/no-unsupported-features/node-builtins': [ + 'error', + { ignores: ['util.styleText'] } + ], semi: [2, 'never'], quotes: [2, 'single', 'avoid-escape'], complexity: 0, diff --git a/jest.config.js b/jest.config.js index 16270ded..7e164dad 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,5 @@ export default { testEnvironment: 'node', setupFiles: ['./.jest-setup.js'], testTimeout: 5000, - preset: 'ts-jest/presets/default-esm', - modulePathIgnorePatterns: ['./dist'] + preset: 'ts-jest/presets/default-esm' } diff --git a/package-lock.json b/package-lock.json index 104e29d5..723eddcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { "name": "calibre", - "version": "7.0.0", + "version": "8.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "calibre", - "version": "7.0.0", + "version": "8.0.0", "license": "MIT", "dependencies": { "@json2csv/node": "^7.0.3", - "chalk": "^5.6.2", "columnify": "^1.6.0", "configstore": "^8.0.0", "cookiefile": "^1.0.10", @@ -18,10 +17,9 @@ "graphql-request": "^7.4.0", "humanize": "0.0.9", "listr": "^0.14.3", - "log-symbols": "^7.0.1", "marked": "^15.0.12", "marked-terminal": "^7.3.0", - "ora": "^9.4.0", + "nanospinner": "^1.2.2", "p-throttle": "^8.0.0", "simple-update-notifier": "^2.0.0", "stats-percentile": "^3.3.0", @@ -32,7 +30,6 @@ }, "devDependencies": { "@types/jest": "^30.0.0", - "esbuild": "^0.27.7", "eslint": "^9.36.0", "eslint-plugin-jest": "^29.15.2", "eslint-plugin-n": "^17.24.0", @@ -46,7 +43,7 @@ "typescript": "^5.2.2" }, "engines": { - "node": ">= 20" + "node": ">= 22.13.0" } }, "node_modules/@babel/code-frame": { @@ -633,448 +630,6 @@ "license": "0BSD", "optional": true }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -3478,18 +3033,6 @@ "node": ">=10" } }, - "node_modules/cli-spinners": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.3.0.tgz", - "integrity": "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==", - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-table3": { "version": "0.6.5", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", @@ -4106,48 +3649,6 @@ "node": ">= 0.4" } }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4876,9 +4377,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "license": "MIT", "engines": { "node": ">=18" @@ -5380,18 +4881,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5462,6 +4951,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -7216,22 +6706,6 @@ "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", - "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", @@ -7498,18 +6972,6 @@ "node": ">=4" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -7561,6 +7023,15 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nanospinner": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/nanospinner/-/nanospinner-1.2.2.tgz", + "integrity": "sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1" + } + }, "node_modules/napi-postinstall": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", @@ -7754,105 +7225,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ora": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz", - "integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==", - "license": "MIT", - "dependencies": { - "chalk": "^5.6.2", - "cli-cursor": "^5.0.0", - "cli-spinners": "^3.2.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.1.0", - "log-symbols": "^7.0.1", - "stdin-discarder": "^0.3.2", - "string-width": "^8.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/string-width": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", - "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8068,7 +7440,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -8682,6 +8053,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -8803,18 +8175,6 @@ "node": ">= 0.8" } }, - "node_modules/stdin-discarder": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", - "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -9778,6 +9138,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 611a6c4f..918bb569 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,22 @@ { "name": "calibre", - "version": "7.0.0", + "version": "8.0.0", "engines": { - "node": ">= 20" + "node": ">= 22.13.0" }, "type": "module", "description": "Performance monitoring with Synthetic testing, Chrome UX Report, and Real User Metrics", "author": "calibreapp.com", - "main": "dist/index.js", "bin": { "calibre": "src/cli.js" }, "exports": { - ".": { - "import": "./index.js", - "require": "./dist/index.cjs" - }, + ".": "./index.js", "./cli-commands": "./src/cli-commands.js", "./cli-metadata": "./src/cli-metadata.js" }, "dependencies": { "@json2csv/node": "^7.0.3", - "chalk": "^5.6.2", "columnify": "^1.6.0", "configstore": "^8.0.0", "cookiefile": "^1.0.10", @@ -29,10 +24,9 @@ "graphql-request": "^7.4.0", "humanize": "0.0.9", "listr": "^0.14.3", - "log-symbols": "^7.0.1", "marked": "^15.0.12", "marked-terminal": "^7.3.0", - "ora": "^9.4.0", + "nanospinner": "^1.2.2", "p-throttle": "^8.0.0", "simple-update-notifier": "^2.0.0", "stats-percentile": "^3.3.0", @@ -40,7 +34,6 @@ }, "devDependencies": { "@types/jest": "^30.0.0", - "esbuild": "^0.27.7", "eslint": "^9.36.0", "eslint-plugin-jest": "^29.15.2", "eslint-plugin-n": "^17.24.0", @@ -57,8 +50,6 @@ "ci": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --no-colors --verbose=false", "test": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand --no-colors --verbose=false", "lint": "eslint .", - "clean": "rm -rf dist/", - "build": "npm run clean && esbuild index.js --bundle --platform=node --outfile=dist/index.cjs", "generate-cli-md": "node ./generate-cli-md.js > CLI_COMMANDS.md" }, "keywords": [ diff --git a/src/api/crux.js b/src/api/crux.js index 4b1de59f..097939b1 100644 --- a/src/api/crux.js +++ b/src/api/crux.js @@ -19,6 +19,9 @@ const SUMMARY_QUERY = ` query GetCruxSummary($site: String!, $formFactor: CruxFormFactor) { organisation { site(slug: $site) { + monitoringStatus { + crux + } cruxCvwAssessment(formFactor: $formFactor) cruxFormFactorDensity { desktop diff --git a/src/api/rum.js b/src/api/rum.js index d04f591b..f85cea54 100644 --- a/src/api/rum.js +++ b/src/api/rum.js @@ -29,6 +29,9 @@ const SUMMARY_QUERY = ` query GetRumSummary($site: String!, $metrics: [MetricTag!]!, $filter: RumFilterInput) { organisation { site(slug: $site) { + monitoringStatus { + rum + } rum(metrics: $metrics, filter: $filter) { liveVisitors distinctCountriesCount diff --git a/src/cli-commands.js b/src/cli-commands.js index 3123e6e8..e6bc3234 100644 --- a/src/cli-commands.js +++ b/src/cli-commands.js @@ -9,45 +9,6 @@ import * as Test from './cli/test.js' import * as Token from './cli/token.js' import * as MetricList from './cli/metric-list.js' -import * as Connections from './cli/synthetic/connections.js' -import * as Devices from './cli/synthetic/devices.js' -import * as Locations from './cli/synthetic/locations.js' - -import { deprecatedHandler } from './utils/deprecation.js' - -const deprecatedCommands = [ - { - command: 'connection-list', - describe: false, - builder: Connections.builder, - handler: deprecatedHandler( - 'connection-list', - 'synthetic connections', - Connections.handler - ) - }, - { - command: 'device-list', - describe: false, - builder: Devices.builder, - handler: deprecatedHandler( - 'device-list', - 'synthetic devices', - Devices.handler - ) - }, - { - command: 'location-list', - describe: false, - builder: Locations.builder, - handler: deprecatedHandler( - 'location-list', - 'synthetic locations', - Locations.handler - ) - } -] - const commands = [ Site, Synthetic, @@ -58,8 +19,7 @@ const commands = [ Team, Token, Request, - MetricList, - ...deprecatedCommands + MetricList ] export default commands diff --git a/src/cli.js b/src/cli.js index 7e246ffd..07f45968 100755 --- a/src/cli.js +++ b/src/cli.js @@ -5,7 +5,7 @@ import fs from 'fs' import yargs from 'yargs' import { hideBin } from 'yargs/helpers' import updateNotifier from 'simple-update-notifier' -import chalk from 'chalk' +import { styleText } from 'node:util' const pkg = JSON.parse( // eslint-disable-next-line security/detect-non-literal-fs-filename @@ -17,7 +17,7 @@ updateNotifier({ pkg }) import commands from './cli-commands.js' yargs(hideBin(process.argv)) - .usage(`${chalk.bold('♠ calibre')} subcommand [options]`) + .usage(`${styleText('bold', '♠ calibre')} subcommand [options]`) .scriptName('calibre') .commands(commands) .demandCommand() @@ -25,9 +25,9 @@ yargs(hideBin(process.argv)) .strictCommands() .help('help') .updateStrings({ - 'Commands:': chalk.grey('Commands:\n'), - 'Options:': chalk.grey('Options:\n'), - 'Examples:': chalk.grey('Examples:\n') + 'Commands:': styleText('gray', 'Commands:\n'), + 'Options:': styleText('gray', 'Options:\n'), + 'Examples:': styleText('gray', 'Examples:\n') }) .version(pkg.version) .example( @@ -39,16 +39,22 @@ yargs(hideBin(process.argv)) .fail((message, error, yargs) => { console.error( '\n\n', - chalk.bold.red(message ? message : error), + styleText(['bold', 'red'], message ? message : error), '\n\n', yargs.help() ) if (error && process.env.DEBUG) { - console.error('\n\n', chalk.bold('--- Stack trace below')) + console.error('\n\n', styleText('bold', '--- Stack trace below')) throw error } // eslint-disable-next-line n/no-process-exit process.exit(1) }) - .epilogue('For more information on Calibre, see https://calibreapp.com').argv + .epilogue('For more information on Calibre, see https://calibreapp.com') + .parseAsync() + .catch(err => { + console.error(styleText(['bold', 'red'], err.message || String(err))) + // eslint-disable-next-line n/no-process-exit + process.exit(1) + }) diff --git a/src/cli/connection-list.js b/src/cli/connection-list.js deleted file mode 100644 index 6cdae409..00000000 --- a/src/cli/connection-list.js +++ /dev/null @@ -1,50 +0,0 @@ -import chalk from 'chalk' -import ora from 'ora' -import columnify from 'columnify' - -import { list } from '../api/connection.js' -import { humaniseError } from '../utils/api-error.js' -import { options } from '../utils/cli.js' - -const main = async args => { - let index - let spinner - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - try { - index = await list(args) - if (args.json) return console.log(JSON.stringify(index, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } - - spinner.stop() - console.log(`${chalk.bold(index.length)} emulated connection speeds`) - - const rows = index.map(row => { - return { - identifier: chalk.cyan(row.name) - } - }) - - console.log( - columnify(rows, { - columnSplitter: ' | ', - truncate: true, - maxLineWidth: 'auto' - }) - ) -} - -const command = 'connection-list' -const describe = 'List all available network connection speeds.' -const handler = main -const builder = { - json: options.json -} - -export { command, describe, handler, builder } diff --git a/src/cli/crux/history.js b/src/cli/crux/history.js index bb2c36c9..e7b517f4 100644 --- a/src/cli/crux/history.js +++ b/src/cli/crux/history.js @@ -1,9 +1,9 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { format as dateFormat } from 'date-fns' import { history } from '../../api/crux.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' import { cruxOptions } from '../../utils/crux-options.js' import { format } from '../../utils/formatters/index.js' @@ -13,7 +13,7 @@ const main = async args => { let result let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { @@ -24,19 +24,17 @@ const main = async args => { }) if (args.json) return console.log(JSON.stringify(result, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } if (!result.cruxHistory || result.cruxHistory.length === 0) { - spinner.fail( - 'No CrUX data available for this site. CrUX requires sufficient Chrome user traffic.' - ) + spinner.stop() return } - spinner.succeed('CrUX History') + spinner.success({ text: 'CrUX History' }) const rows = [] for (const metricHistory of result.cruxHistory) { @@ -60,7 +58,9 @@ const main = async args => { console.log( columnify(rows, { - columnSplitter: ' | ' + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' }) ) } diff --git a/src/cli/crux/summary.js b/src/cli/crux/summary.js index 2f4d6b32..a055fe8c 100644 --- a/src/cli/crux/summary.js +++ b/src/cli/crux/summary.js @@ -1,8 +1,8 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { summary } from '../../api/crux.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' import { cruxOptions } from '../../utils/crux-options.js' import { format } from '../../utils/formatters/index.js' @@ -12,26 +12,29 @@ const main = async args => { let result let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { result = await summary({ site: args.site, formFactor: args.formFactor }) if (args.json) return console.log(JSON.stringify(result, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } if (!result.cruxAggregateMetrics || result.cruxAggregateMetrics.length === 0) { - spinner.fail( - 'No CrUX data available for this site. CrUX requires sufficient Chrome user traffic.' - ) + if (result.monitoringStatus?.crux) { + spinner.error({ text: result.monitoringStatus.crux }) + } else { + spinner.stop() + } + process.exitCode = 2 return } - spinner.succeed(`CrUX Assessment: ${formatGrading(result.cruxCvwAssessment)}`) + spinner.success({ text: `CrUX Assessment: ${formatGrading(result.cruxCvwAssessment)}` }) if (result.cruxFormFactorDensity) { const d = result.cruxFormFactorDensity @@ -54,7 +57,9 @@ const main = async args => { console.log( columnify(rows, { - columnSplitter: ' | ' + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' }) ) } diff --git a/src/cli/crux/url.js b/src/cli/crux/url.js index f8b5d9b3..8342539f 100644 --- a/src/cli/crux/url.js +++ b/src/cli/crux/url.js @@ -1,9 +1,9 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { format as dateFormat } from 'date-fns' import { url as fetchUrl } from '../../api/crux.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' import { cruxOptions } from '../../utils/crux-options.js' import { format } from '../../utils/formatters/index.js' @@ -13,7 +13,7 @@ const main = async args => { let result let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { @@ -25,20 +25,18 @@ const main = async args => { }) if (args.json) return console.log(JSON.stringify(result, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } if (!result.cruxUrl) { - spinner.fail( - 'No CrUX data available for this URL. CrUX requires sufficient Chrome user traffic.' - ) + spinner.stop() return } const urlData = result.cruxUrl - spinner.succeed(`${urlData.url} — ${formatGrading(urlData.cruxCvwAssessment)}`) + spinner.success({ text: `${urlData.url} — ${formatGrading(urlData.cruxCvwAssessment)}` }) if (urlData.cruxAggregateMetrics && urlData.cruxAggregateMetrics.length > 0) { console.log('') @@ -54,7 +52,9 @@ const main = async args => { console.log( columnify(metricRows, { - columnSplitter: ' | ' + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' }) ) } @@ -85,7 +85,9 @@ const main = async args => { console.log( columnify(historyRows, { - columnSplitter: ' | ' + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' }) ) } diff --git a/src/cli/crux/urls.js b/src/cli/crux/urls.js index a5b3ec85..4d61f9cc 100644 --- a/src/cli/crux/urls.js +++ b/src/cli/crux/urls.js @@ -1,8 +1,8 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { urls } from '../../api/crux.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' import { cruxOptions } from '../../utils/crux-options.js' import { format } from '../../utils/formatters/index.js' @@ -12,15 +12,15 @@ const main = async args => { let result let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { result = await urls({ site: args.site, formFactor: args.formFactor }) if (args.json) return console.log(JSON.stringify(result, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } @@ -29,13 +29,11 @@ const main = async args => { !result.cruxUrlsList.edges || result.cruxUrlsList.edges.length === 0 ) { - spinner.fail( - 'No CrUX URL data available for this site. CrUX requires sufficient Chrome user traffic.' - ) + spinner.stop() return } - spinner.succeed(`${result.cruxUrlsList.edges.length} CrUX URLs`) + spinner.success({ text: `${result.cruxUrlsList.edges.length} CrUX URLs` }) const rows = result.cruxUrlsList.edges.map(({ node }) => { const row = { @@ -59,7 +57,9 @@ const main = async args => { console.log( columnify(rows, { - columnSplitter: ' | ' + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' }) ) } diff --git a/src/cli/deploy/create.js b/src/cli/deploy/create.js index 1be44e8f..fb0dbd2b 100644 --- a/src/cli/deploy/create.js +++ b/src/cli/deploy/create.js @@ -1,25 +1,25 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { create } from '../../api/deploy.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async function (args) { let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { const response = await create(args) - if (!args.json) spinner.succeed(`Deploy created: ${response.uuid}`) + if (!args.json) spinner.success({ text: `Deploy created: ${response.uuid}` }) // Return result if (args.json) return console.log(JSON.stringify(response, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/deploy/delete.js b/src/cli/deploy/delete.js index 8125568e..86ad7a47 100644 --- a/src/cli/deploy/delete.js +++ b/src/cli/deploy/delete.js @@ -1,14 +1,14 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { destroy } from '../../api/deploy.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async function (args) { let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } if (process.stdout.isTTY && !args.confirm) { @@ -19,13 +19,13 @@ const main = async function (args) { try { const response = await destroy(args) - if (!args.json) spinner.succeed(`Deploy deleted: ${response.uuid}`) + if (!args.json) spinner.success({ text: `Deploy deleted: ${response.uuid}` }) // Return result if (args.json) return console.log(JSON.stringify(response, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail(humaniseError(e)) + if (args.json) return formatJsonError(e) + spinner.stop() return Error(humaniseError(e)) } } diff --git a/src/cli/deploy/list.js b/src/cli/deploy/list.js index 39117486..53a6548a 100644 --- a/src/cli/deploy/list.js +++ b/src/cli/deploy/list.js @@ -1,60 +1,99 @@ -import chalk from 'chalk' -import ora from 'ora' +import { styleText } from 'node:util' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { format as dateFormat } from 'date-fns' import { list } from '../../api/deploy.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' +import { fetchAll } from '../../utils/pagination.js' const main = async args => { - let index + if (args.all && args.cursor) { + throw new Error('Cannot use --all with --cursor.') + } + let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { - index = await list(args) - if (args.json) return console.log(JSON.stringify(index, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } + if (args.all) { + const deploys = await fetchAll( + async a => { + const res = await list(a) + return { items: res.deploys, pageInfo: res.pageInfo } + }, + args + ) - spinner.stop() - console.log(`${chalk.bold(index.deploys.length)} deploys`) + if (!args.json) spinner.stop() + if (args.json) return console.log(JSON.stringify(deploys, null, 2)) - const deploys = index.deploys.map( - ({ uuid, revision, repository, username, createdAt }) => { - return { - uuid: chalk.grey(uuid), - revision, - repository, - username, - created: dateFormat(new Date(createdAt), 'h:mma d-MMM-yyyy') - } - } - ) + console.log(`${styleText('bold', String(deploys.length))} deploys`) + + const rows = deploys.map( + ({ uuid, revision, repository, username, createdAt }) => { + return { + uuid: styleText('gray', uuid), + revision, + repository, + username, + created: dateFormat(new Date(createdAt), 'h:mma d-MMM-yyyy') + } + } + ) - console.log( - columnify(deploys, { - columnSplitter: ' | ', - truncate: true, - maxLineWidth: 'auto' - }) - ) + console.log( + columnify(rows, { + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' + }) + ) + } else { + const index = await list(args) + if (args.json) return console.log(JSON.stringify(index, null, 2)) - if (index.pageInfo.hasNextPage) { - const lastDeploy = index.deploys[index.deploys.length - 1] - console.log( - `To see deploys after ${ - lastDeploy.revision || lastDeploy.id - }, run: calibre deploy list --site=${args.site} --cursor=${ - index.pageInfo.endCursor - }` - ) + spinner.stop() + console.log(`${styleText('bold', String(index.deploys.length))} deploys`) + + const rows = index.deploys.map( + ({ uuid, revision, repository, username, createdAt }) => { + return { + uuid: styleText('gray', uuid), + revision, + repository, + username, + created: dateFormat(new Date(createdAt), 'h:mma d-MMM-yyyy') + } + } + ) + + console.log( + columnify(rows, { + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' + }) + ) + + if (index.pageInfo.hasNextPage) { + const lastDeploy = index.deploys[index.deploys.length - 1] + console.log( + `To see deploys after ${ + lastDeploy.revision || lastDeploy.id + }, run: calibre deploy list --site=${args.site} --cursor=${ + index.pageInfo.endCursor + }` + ) + } + } + } catch (e) { + if (args.json) return formatJsonError(e) + if (spinner) spinner.stop() + throw new Error(humaniseError(e)) } } @@ -65,7 +104,12 @@ const builder = { site: options.site, count: options.count, cursor: options.cursor, - json: options.json + json: options.json, + all: { + describe: 'Fetch all deploys (overrides --count and --cursor).', + type: 'boolean', + default: false + } } export { command, describe, handler, builder } diff --git a/src/cli/device-list.js b/src/cli/device-list.js deleted file mode 100644 index 5c404ce5..00000000 --- a/src/cli/device-list.js +++ /dev/null @@ -1,55 +0,0 @@ -import chalk from 'chalk' -import ora from 'ora' -import columnify from 'columnify' - -import { list } from '../api/device.js' -import { humaniseError } from '../utils/api-error.js' -import { options } from '../utils/cli.js' - -const main = async args => { - let index - let spinner - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - try { - index = await list(args) - if (args.json) return console.log(JSON.stringify(index, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } - - spinner.stop() - console.log(`${chalk.bold(index.length)} test devices`) - - const rows = index.map(row => { - return { - identifier: chalk.cyan(row.tag), - name: row.title, - type: row.type, - 'screen width': row.screenWidth, - 'screen height': row.screenHeight, - status: row.isDiscontinued ? 'Discontinued' : 'Current' - } - }) - - console.log( - columnify(rows, { - columnSplitter: ' | ', - truncate: true, - maxLineWidth: 'auto' - }) - ) -} - -const command = 'device-list' -const describe = 'List all available test devices.' -const handler = main -const builder = { - json: options.json -} - -export { command, describe, handler, builder } diff --git a/src/cli/location-list.js b/src/cli/location-list.js deleted file mode 100644 index 5715d589..00000000 --- a/src/cli/location-list.js +++ /dev/null @@ -1,52 +0,0 @@ -import chalk from 'chalk' -import ora from 'ora' -import columnify from 'columnify' - -import { list } from '../api/location.js' -import { humaniseError } from '../utils/api-error.js' -import { options } from '../utils/cli.js' - -const main = async args => { - let index - let spinner - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - try { - index = await list(args) - if (args.json) return console.log(JSON.stringify(index, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } - - spinner.stop() - console.log(`${chalk.bold(index.length)} test locations`) - - const rows = index.map(row => { - return { - identifier: chalk.cyan(row.tag), - name: row.name, - 'ipv4 address': row.agents.map(a => a.ipv4).join(', ') - } - }) - - console.log( - columnify(rows, { - columnSplitter: ' | ', - truncate: true, - maxLineWidth: 'auto' - }) - ) -} - -const command = 'location-list' -const describe = 'List all available test locations.' -const handler = main -const builder = { - json: options.json -} - -export { command, describe, handler, builder } diff --git a/src/cli/metric-list.js b/src/cli/metric-list.js index f61508db..5b08e757 100644 --- a/src/cli/metric-list.js +++ b/src/cli/metric-list.js @@ -1,10 +1,9 @@ -import chalk from 'chalk' -import ora from 'ora' +import { styleText } from 'node:util' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' -import logSymbols from 'log-symbols' import { list } from '../api/metric.js' -import { humaniseError } from '../utils/api-error.js' +import { humaniseError, formatJsonError } from '../utils/api-error.js' import { format } from '../utils/formatters/index.js' import { options } from '../utils/cli.js' @@ -12,24 +11,24 @@ const main = async args => { let index let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { index = await list(args) if (args.json) return console.log(JSON.stringify(index, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } spinner.stop() - console.log(`${chalk.bold(index.length)} metrics`) + console.log(`${styleText('bold', String(index.length))} metrics`) const rows = index.map(row => { return { - identifier: chalk.cyan(row.value), + identifier: styleText('cyan', row.value), name: row.label, category: row.category?.label || '—', good: `${row.budgetThreshold === 'GreaterThan' ? '<' : '>'} ${format({ @@ -41,7 +40,7 @@ const main = async args => { value: row.poorStop })}`, recommended: row.recommended - ? chalk.bold.green(`${logSymbols.success}`) + ? styleText(['bold', 'green'], '\u2714') : null } }) diff --git a/src/cli/request.js b/src/cli/request.js index 75202127..ffb121c9 100644 --- a/src/cli/request.js +++ b/src/cli/request.js @@ -1,19 +1,18 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { request } from '../api/graphql.js' import { humaniseError } from '../utils/api-error.js' const main = async args => { let spinner - spinner = ora('Connecting to Calibre') - spinner.color = 'magenta' + spinner = createSpinner('Connecting to Calibre') spinner.start() let result try { result = await request(args) } catch (e) { - spinner.fail() + spinner.stop() throw new Error(humaniseError(e)) } diff --git a/src/cli/rum/config.js b/src/cli/rum/config.js index d16684f6..5879ee2d 100644 --- a/src/cli/rum/config.js +++ b/src/cli/rum/config.js @@ -1,31 +1,31 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { config } from '../../api/rum.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async args => { let result let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { result = await config({ site: args.site }) if (args.json) return console.log(JSON.stringify(result, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } if (!result) { - spinner.fail('RUM is not configured for this site.') + spinner.stop() return } - spinner.succeed('RUM Configuration') + spinner.success({ text: 'RUM Configuration' }) console.log(` Enabled: ${result.enabled}`) console.log(` Sample Rate: ${result.sampleRate}`) diff --git a/src/cli/rum/history.js b/src/cli/rum/history.js index 7094a39b..59899267 100644 --- a/src/cli/rum/history.js +++ b/src/cli/rum/history.js @@ -1,10 +1,10 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { format as dateFormat } from 'date-fns' import { history } from '../../api/rum.js' import { format } from '../../utils/formatters/index.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' import { rumFilterOptions } from '../../utils/rum-options.js' import { colorByGrading } from '../../views/grading.js' @@ -13,26 +13,24 @@ const main = async args => { let result let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { result = await history(args) if (args.json) return console.log(JSON.stringify(result, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } if (!result.history || result.history.length === 0) { - spinner.fail( - 'No RUM history available. Check that RUM is enabled for this site with: calibre rum config --site=' - ) + spinner.stop() return } - spinner.succeed('RUM History') + spinner.success({ text: 'RUM History' }) const formatters = new Map( (result.metrics || []).map(m => [m.value, m.formatter]) @@ -62,7 +60,9 @@ const main = async args => { console.log( columnify(rows, { - columnSplitter: ' | ' + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' }) ) } diff --git a/src/cli/rum/pages.js b/src/cli/rum/pages.js index ea6fff44..849e3154 100644 --- a/src/cli/rum/pages.js +++ b/src/cli/rum/pages.js @@ -1,8 +1,8 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { pages } from '../../api/rum.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' import { rumFilterOptions } from '../../utils/rum-options.js' import { format } from '../../utils/formatters/index.js' @@ -12,26 +12,24 @@ const main = async args => { let result let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { result = await pages(args) if (args.json) return console.log(JSON.stringify(result, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } if (!result.aggregate || result.aggregate.length === 0) { - spinner.fail( - 'No RUM page data available. Check that RUM is enabled for this site with: calibre rum config --site=' - ) + spinner.stop() return } - spinner.succeed(`${result.totalCount} pages`) + spinner.success({ text: `${result.totalCount} pages` }) const formatters = new Map( (result.metrics || []).map(m => [m.value, m.formatter]) @@ -58,7 +56,9 @@ const main = async args => { console.log( columnify(rows, { - columnSplitter: ' | ' + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' }) ) diff --git a/src/cli/rum/summary.js b/src/cli/rum/summary.js index cdae421e..b4c1e75d 100644 --- a/src/cli/rum/summary.js +++ b/src/cli/rum/summary.js @@ -1,8 +1,8 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { summary } from '../../api/rum.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' import { rumFilterOptions } from '../../utils/rum-options.js' import { format } from '../../utils/formatters/index.js' @@ -21,26 +21,29 @@ const main = async args => { let result let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { result = await summary(args) if (args.json) return console.log(JSON.stringify(result, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } if (!result.aggregate || result.aggregate.length === 0) { - spinner.fail( - 'No RUM data available. Check that RUM is enabled for this site with: calibre rum config --site=' - ) + if (result.monitoringStatus?.rum) { + spinner.error({ text: result.monitoringStatus.rum }) + } else { + spinner.stop() + } + process.exitCode = 2 return } - spinner.succeed('RUM Summary') + spinner.success({ text: 'RUM Summary' }) console.log( `Live visitors: ${result.liveVisitors} | Countries: ${result.distinctCountriesCount} | Sessions: ${result.aggregate[0].sessionCount}` @@ -75,7 +78,9 @@ const main = async args => { console.log( columnify(rows, { - columnSplitter: ' | ' + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' }) ) } diff --git a/src/cli/site.js b/src/cli/site.js index f7012407..b7bb9d70 100644 --- a/src/cli/site.js +++ b/src/cli/site.js @@ -2,238 +2,11 @@ import * as SiteCreate from './site/create.js' import * as SiteList from './site/list.js' import * as SiteDelete from './site/delete.js' -import * as SyntheticPages from './synthetic/pages.js' -import * as SyntheticCreatePage from './synthetic/create-page.js' -import * as SyntheticUpdatePage from './synthetic/update-page.js' -import * as SyntheticDeletePage from './synthetic/delete-page.js' -import * as SyntheticSnapshots from './synthetic/snapshots.js' -import * as SyntheticCreateSnapshot from './synthetic/create-snapshot.js' -import * as SyntheticDeleteSnapshot from './synthetic/delete-snapshot.js' -import * as SyntheticDownloadArtifacts from './synthetic/download-artifacts.js' -import * as SyntheticGetSnapshotMetrics from './synthetic/get-snapshot-metrics.js' -import * as SyntheticMetrics from './synthetic/metrics.js' -import * as SyntheticTestProfiles from './synthetic/test-profiles.js' -import * as SyntheticCreateTestProfile from './synthetic/create-test-profile.js' -import * as SyntheticUpdateTestProfile from './synthetic/update-test-profile.js' -import * as SyntheticDeleteTestProfile from './synthetic/delete-test-profile.js' -import * as SyntheticPullRequestReviews from './synthetic/pull-request-reviews.js' -import * as SyntheticCreatePullRequestReview from './synthetic/create-pull-request-review.js' -import * as SyntheticPullRequestReview from './synthetic/pull-request-review.js' - -import * as DeployList from './deploy/list.js' -import * as DeployCreate from './deploy/create.js' -import * as DeployDelete from './deploy/delete.js' - -import { deprecatedHandler } from '../utils/deprecation.js' - const commands = [SiteCreate, SiteList, SiteDelete] -const deprecatedCommands = [ - { - command: 'pages [options]', - describe: false, - builder: SyntheticPages.builder, - handler: deprecatedHandler( - 'site pages', - 'synthetic pages', - SyntheticPages.handler - ) - }, - { - command: 'create-page [options]', - describe: false, - builder: SyntheticCreatePage.builder, - handler: deprecatedHandler( - 'site create-page', - 'synthetic create-page', - SyntheticCreatePage.handler - ) - }, - { - command: 'update-page [options]', - describe: false, - builder: SyntheticUpdatePage.builder, - handler: deprecatedHandler( - 'site update-page', - 'synthetic update-page', - SyntheticUpdatePage.handler - ) - }, - { - command: 'delete-page [options]', - describe: false, - builder: SyntheticDeletePage.builder, - handler: deprecatedHandler( - 'site delete-page', - 'synthetic delete-page', - SyntheticDeletePage.handler - ) - }, - { - command: 'snapshots [options]', - describe: false, - builder: SyntheticSnapshots.builder, - handler: deprecatedHandler( - 'site snapshots', - 'synthetic snapshots', - SyntheticSnapshots.handler - ) - }, - { - command: 'create-snapshot [options]', - describe: false, - builder: SyntheticCreateSnapshot.builder, - handler: deprecatedHandler( - 'site create-snapshot', - 'synthetic create-snapshot', - SyntheticCreateSnapshot.handler - ) - }, - { - command: 'delete-snapshot [options]', - describe: false, - builder: SyntheticDeleteSnapshot.builder, - handler: deprecatedHandler( - 'site delete-snapshot', - 'synthetic delete-snapshot', - SyntheticDeleteSnapshot.handler - ) - }, - { - command: 'download-snapshot-artifacts [options]', - describe: false, - builder: SyntheticDownloadArtifacts.builder, - handler: deprecatedHandler( - 'site download-snapshot-artifacts', - 'synthetic download-artifacts', - SyntheticDownloadArtifacts.handler - ) - }, - { - command: 'get-snapshot-metrics [options]', - describe: false, - builder: SyntheticGetSnapshotMetrics.builder, - handler: deprecatedHandler( - 'site get-snapshot-metrics', - 'synthetic get-snapshot-metrics', - SyntheticGetSnapshotMetrics.handler - ) - }, - { - command: 'metrics [options]', - describe: false, - builder: SyntheticMetrics.builder, - handler: deprecatedHandler( - 'site metrics', - 'synthetic metrics', - SyntheticMetrics.handler - ) - }, - { - command: 'test-profiles [options]', - describe: false, - builder: SyntheticTestProfiles.builder, - handler: deprecatedHandler( - 'site test-profiles', - 'synthetic test-profiles', - SyntheticTestProfiles.handler - ) - }, - { - command: 'create-test-profile [options]', - describe: false, - builder: SyntheticCreateTestProfile.builder, - handler: deprecatedHandler( - 'site create-test-profile', - 'synthetic create-test-profile', - SyntheticCreateTestProfile.handler - ) - }, - { - command: 'update-test-profile [options]', - describe: false, - builder: SyntheticUpdateTestProfile.builder, - handler: deprecatedHandler( - 'site update-test-profile', - 'synthetic update-test-profile', - SyntheticUpdateTestProfile.handler - ) - }, - { - command: 'delete-test-profile [options]', - describe: false, - builder: SyntheticDeleteTestProfile.builder, - handler: deprecatedHandler( - 'site delete-test-profile', - 'synthetic delete-test-profile', - SyntheticDeleteTestProfile.handler - ) - }, - { - command: 'pull-request-reviews [options]', - describe: false, - builder: SyntheticPullRequestReviews.builder, - handler: deprecatedHandler( - 'site pull-request-reviews', - 'synthetic pull-request-reviews', - SyntheticPullRequestReviews.handler - ) - }, - { - command: 'create-pull-request-review [options]', - describe: false, - builder: SyntheticCreatePullRequestReview.builder, - handler: deprecatedHandler( - 'site create-pull-request-review', - 'synthetic create-pull-request-review', - SyntheticCreatePullRequestReview.handler - ) - }, - { - command: 'pull-request-review ', - describe: false, - builder: SyntheticPullRequestReview.builder, - handler: deprecatedHandler( - 'site pull-request-review', - 'synthetic pull-request-review', - SyntheticPullRequestReview.handler - ) - }, - { - command: 'deploys [options]', - describe: false, - builder: DeployList.builder, - handler: deprecatedHandler( - 'site deploys', - 'deploy list', - DeployList.handler - ) - }, - { - command: 'create-deploy [options]', - describe: false, - builder: DeployCreate.builder, - handler: deprecatedHandler( - 'site create-deploy', - 'deploy create', - DeployCreate.handler - ) - }, - { - command: 'delete-deploy [options]', - describe: false, - builder: DeployDelete.builder, - handler: deprecatedHandler( - 'site delete-deploy', - 'deploy delete', - DeployDelete.handler - ) - } -] - const command = 'site ' const desc = 'Manage your Sites.' -const builder = yargs => yargs.commands([...commands, ...deprecatedCommands]) +const builder = yargs => yargs.commands(commands) const handler = () => {} export { command, desc, builder, handler, commands } diff --git a/src/cli/site/create-deploy.js b/src/cli/site/create-deploy.js deleted file mode 100644 index 6970d489..00000000 --- a/src/cli/site/create-deploy.js +++ /dev/null @@ -1,45 +0,0 @@ -import ora from 'ora' - -import { create } from '../../api/deploy.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async function (args) { - let spinner - - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - try { - const response = await create(args) - if (!args.json) spinner.succeed(`Deploy created: ${response.uuid}`) - - // Return result - if (args.json) return console.log(JSON.stringify(response, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } -} - -const command = 'create-deploy [options]' -const describe = 'Create a deployment.' -const builder = { - site: options.site, - revision: { - describe: - 'The source control revision id of the code you are deploying. It could be a git hash or a tag name.' - }, - repository: { - describe: - 'The base URL of the repository containing the source code being deployed (e.g. https://github.com/calibreapp/app).' - }, - username: { describe: 'The username of who deployed the code.' }, - json: options.json -} - -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/create-page.js b/src/cli/site/create-page.js deleted file mode 100644 index 124eddd5..00000000 --- a/src/cli/site/create-page.js +++ /dev/null @@ -1,48 +0,0 @@ -import { URL } from 'url' -import ora from 'ora' - -import { create } from '../../api/page.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async function (args) { - let spinner - - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - try { - new URL(args.url) - } catch { - return new Error('Please enter a valid URL') - } - - try { - const response = await create(args) - if (!args.json) - spinner.succeed(`Page added: ${response.name} (${response.uuid})`) - - // Return result - if (args.json) return console.log(JSON.stringify(response, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } -} - -const command = 'create-page [options]' -const describe = 'Add a Page to an existing Site tracked by Calibre.' -const builder = { - site: options.site, - url: { - demandOption: true, - requiresArg: true, - describe: 'The URL of the Page.' - }, - json: options.json -} -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/create-pull-request-review.js b/src/cli/site/create-pull-request-review.js deleted file mode 100644 index 741d2eab..00000000 --- a/src/cli/site/create-pull-request-review.js +++ /dev/null @@ -1,128 +0,0 @@ -import { promises as fs } from 'fs' -import ora from 'ora' - -import { - create, - waitForReviewCompletion -} from '../../api/pull-request-review.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' -import formatMarkdownResult from '../../views/markdown.js' - -const print = function (args, pullRequestReviewResponse) { - if (args.json) { - return console.log(JSON.stringify(pullRequestReviewResponse, null, 2)) - } - - if (args.markdown) { - return console.log(pullRequestReviewResponse.markdownReport) - } - - return console.log( - formatMarkdownResult(pullRequestReviewResponse.markdownReport) - ) -} - -const main = async function (args) { - let spinner - - if (!args.json && !args.markdown) { - spinner = ora('Connecting to Calibre').start() - } - - if (args.configPath) { - // eslint-disable-next-line security/detect-non-literal-fs-filename - args.config = await fs.readFile(args.configPath, 'utf8') - } - - try { - const response = await create(args) - - if (!args.waitForResult) { - spinner.succeed(`Pull Request Review queued: ${args.branch}.`) - - console.log( - `View progress by running \`calibre site pull-request-review ${args.branch} --site=${args.site}\`` - ) - - return print(args, response) - } else { - if (spinner) { - spinner.succeed(`Pull Request Review queued: ${args.branch}`) - spinner = ora('Waiting for Pull Request Review to complete').start() - } - - const completedResponse = await waitForReviewCompletion( - args.site, - args.branch - ) - - if (spinner) { - spinner.succeed('Pull Request Review completed') - } - - print(args, completedResponse) - - if ( - args.failOnUnmetBudget && - completedResponse.metricBudgetStatus === 'unmet' - ) { - throw new Error('Pull Request Review failed due to unmet budget') - } - } - } catch (e) { - if (args.json || args.markdown) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } -} - -const command = 'create-pull-request-review [options]' -const describe = 'Create a Pull Request Review of a preview deployment.' -const builder = { - title: { - describe: 'e.g. "My Pull Request"', - demandOption: true, - requiresArg: true - }, - site: options.site, - url: { - describe: - 'The base URL of the preview deployment (e.g.: https://my-pull-request-123.example.com).', - demandOption: true, - requiresArg: true - }, - branch: { - describe: - 'The branch of the preview deployment. e.g.: "my-pull-request-123".', - demandOption: true, - requiresArg: true - }, - sha: { - describe: - 'The source control revision of the deployed code. e.g.: 9c72279.', - demandOption: true, - requiresArg: true - }, - configPath: { - describe: 'Path to a Calibre YAML config file.' - }, - waitForResult: { - describe: 'Wait for pull request to be evaluated before returning.', - type: 'boolean', - default: false - }, - failOnUnmetBudget: { - describe: - 'Return a command failure if any existing budget is exceeded. (Requires --waitForResult to be set.)', - type: 'boolean', - default: false - }, - - json: options.json, - markdown: options.markdown -} - -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/create-snapshot.js b/src/cli/site/create-snapshot.js deleted file mode 100644 index 807e87a9..00000000 --- a/src/cli/site/create-snapshot.js +++ /dev/null @@ -1,36 +0,0 @@ -import ora from 'ora' - -import { create } from '../../api/snapshot.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async function (args) { - let spinner - - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - try { - const response = await create(args) - if (!args.json) spinner.succeed(`Snapshot created: ${response.iid}`) - - // Return result - if (args.json) return console.log(JSON.stringify(response, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } -} - -const command = 'create-snapshot [options]' -const describe = 'Create a Snapshot.' -const builder = { - site: options.site, - ref: { describe: 'Set a reference to the Snapshot.' }, - json: options.json -} -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/create-test-profile.js b/src/cli/site/create-test-profile.js deleted file mode 100644 index 3c2b06ea..00000000 --- a/src/cli/site/create-test-profile.js +++ /dev/null @@ -1,69 +0,0 @@ -import ora from 'ora' -import cookiefile from 'cookiefile' - -import formatProfile from '../../views/test-profile.js' -import { create } from '../../api/test-profile.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const { CookieMap } = cookiefile - -const main = async function (args) { - let spinner - let cookies = [] - - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - if (args.cookieJar) { - const jar = new CookieMap(args.cookieJar) - - for (const cookie of jar.values()) { - const { name, value, domain, path, httpOnly, https } = cookie - cookies.push({ name, value, domain, path, httpOnly, secure: https }) - } - } - - try { - const response = await create({ ...args, cookies }) - if (!args.json) { - spinner.succeed('Created Test Profile') - console.log(formatProfile(response)) - } - - // Return result - if (args.json) return console.log(JSON.stringify(response, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - - spinner.fail() - throw new Error(humaniseError(e)) - } -} - -const command = 'create-test-profile [options]' -const describe = 'Add a Test Profile to a Site.' -const builder = { - site: options.site, - device: { - describe: 'Choose the emulated test device.', - default: 'Desktop' - }, - connection: { - describe: 'Choose the emulated network connection speed.' - }, - javascript: { - type: 'boolean', - describe: 'Turn JavaScript execution on or off.', - default: true - }, - 'cookie-jar': { - describe: - 'Set cookies by specifying a path to a Netscape formatted cookie jar file.' - }, - json: options.json -} -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/create.js b/src/cli/site/create.js index 8a9623e6..09f3a8ab 100644 --- a/src/cli/site/create.js +++ b/src/cli/site/create.js @@ -1,15 +1,15 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { URL } from 'url' import { create } from '../../api/site.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async function (args) { let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } const { name, location, url, team, schedule, interval } = args @@ -61,13 +61,13 @@ const main = async function (args) { }) if (!args.json) { - spinner.succeed(`${site.name} added to Calibre`) + spinner.success({ text: `${site.name} added to Calibre` }) } else { return console.log(JSON.stringify(site, null, 2)) } } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/site/delete-deploy.js b/src/cli/site/delete-deploy.js deleted file mode 100644 index b0e4aa51..00000000 --- a/src/cli/site/delete-deploy.js +++ /dev/null @@ -1,49 +0,0 @@ -import ora from 'ora' - -import { destroy } from '../../api/deploy.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async function (args) { - let spinner - - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - if (process.stdout.isTTY && !args.confirm) { - return new Error( - 'Add the --confirm flag to confirm the immediate and irreversible deletion of this deploy.' - ) - } - - try { - const response = await destroy(args) - if (!args.json) spinner.succeed(`Deploy deleted: ${response.uuid}`) - - // Return result - if (args.json) return console.log(JSON.stringify(response, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail(humaniseError(e)) - return Error(humaniseError(e)) - } -} - -const command = 'delete-deploy [options]' -const describe = 'Delete a deploy from a selected Site.' -const builder = { - site: options.site, - uuid: { - demandOption: true, - requiresArg: true, - describe: 'The UUID of the deploy.' - }, - confirm: { - describe: 'Use this flag to confirm the deletion of the selected deploy.' - }, - json: options.json -} -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/delete-page.js b/src/cli/site/delete-page.js deleted file mode 100644 index 2846a5a6..00000000 --- a/src/cli/site/delete-page.js +++ /dev/null @@ -1,50 +0,0 @@ -import ora from 'ora' - -import { destroy } from '../../api/page.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async function (args) { - let spinner - - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - if (process.stdout.isTTY && !args.confirm) { - return new Error( - 'Add the --confirm flag to confirm the immediate and irreversible deletion of this Page.' - ) - } - - try { - const response = await destroy(args) - if (!args.json) - spinner.succeed(`Page deleted: ${response.name} (${response.uuid})`) - - // Return result - if (args.json) return console.log(JSON.stringify(response, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } -} - -const command = 'delete-page [options]' -const describe = 'Delete a Page from a selected Site.' -const builder = { - site: options.site, - uuid: { - demandOption: true, - requiresArg: true, - describe: 'The UUID of the Page.' - }, - confirm: { - describe: 'Use this flag to confirm the deletion of the selected Page.' - }, - json: options.json -} -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/delete-snapshot.js b/src/cli/site/delete-snapshot.js deleted file mode 100644 index f3920ace..00000000 --- a/src/cli/site/delete-snapshot.js +++ /dev/null @@ -1,49 +0,0 @@ -import ora from 'ora' - -import { destroy } from '../../api/snapshot.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async function (args) { - let spinner - - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - if (process.stdout.isTTY && !args.confirm) { - return new Error( - 'Add the --confirm flag to confirm the immediate and irreversible deletion of this Snapshot.' - ) - } - - try { - const response = await destroy(args) - if (!args.json) spinner.succeed(`Snapshot deleted: ${response.iid}`) - - // Return result - if (args.json) return console.log(JSON.stringify(response, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail(humaniseError(e)) - return Error(humaniseError(e)) - } -} - -const command = 'delete-snapshot [options]' -const describe = 'Delete a Snapshot from a selected Site.' -const builder = { - site: options.site, - id: { - demandOption: true, - requiresArg: true, - describe: 'The id of the Snapshot.' - }, - confirm: { - describe: 'Use this flag to confirm the deletion of the selected Snapshot.' - }, - json: options.json -} -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/delete-test-profile.js b/src/cli/site/delete-test-profile.js deleted file mode 100644 index a06a74d5..00000000 --- a/src/cli/site/delete-test-profile.js +++ /dev/null @@ -1,53 +0,0 @@ -import ora from 'ora' - -import { destroy } from '../../api/test-profile.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async function (args) { - let spinner - - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - if (process.stdout.isTTY && !args.confirm) { - return new Error( - 'Add the --confirm flag to confirm the immediate and irreversible deletion of this Test Profile.' - ) - } - - try { - const response = await destroy(args) - if (!args.json) - spinner.succeed( - `Test Profile deleted: ${response.name} (${response.uuid})` - ) - - // Return result - if (args.json) return console.log(JSON.stringify(response, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } -} - -const command = 'delete-test-profile [options]' -const describe = 'Delete a Test Profile from a Site.' -const builder = { - site: options.site, - uuid: { - demandOption: true, - requiresArg: true, - describe: 'The UUID of the Test Profile.' - }, - confirm: { - describe: - 'Use this flag to confirm the deletion of the selected Test Profile.' - }, - json: options.json -} -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/delete.js b/src/cli/site/delete.js index 5180221c..c4390559 100644 --- a/src/cli/site/delete.js +++ b/src/cli/site/delete.js @@ -1,14 +1,14 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { destroy } from '../../api/site.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async function (args) { let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } if (process.stdout.isTTY && !args.confirm) { @@ -19,13 +19,13 @@ const main = async function (args) { try { const response = await destroy(args) - if (!args.json) spinner.succeed(`Site deleted: ${response.name}`) + if (!args.json) spinner.success({ text: `Site deleted: ${response.name}` }) // Return result if (args.json) return console.log(JSON.stringify(response, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/site/deploys.js b/src/cli/site/deploys.js deleted file mode 100644 index f20cd021..00000000 --- a/src/cli/site/deploys.js +++ /dev/null @@ -1,71 +0,0 @@ -import chalk from 'chalk' -import ora from 'ora' -import columnify from 'columnify' -import { format as dateFormat } from 'date-fns' - -import { list } from '../../api/deploy.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async args => { - let index - let spinner - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - try { - index = await list(args) - if (args.json) return console.log(JSON.stringify(index, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } - - spinner.stop() - console.log(`${chalk.bold(index.deploys.length)} deploys`) - - const deploys = index.deploys.map( - ({ uuid, revision, repository, username, createdAt }) => { - return { - uuid: chalk.grey(uuid), - revision, - repository, - username, - created: dateFormat(new Date(createdAt), 'h:mma d-MMM-yyyy') - } - } - ) - - console.log( - columnify(deploys, { - columnSplitter: ' | ', - truncate: true, - maxLineWidth: 'auto' - }) - ) - - if (index.pageInfo.hasNextPage) { - const lastDeploy = index.deploys[index.deploys.length - 1] - console.log( - `To see deploys after ${ - lastDeploy.revision || lastDeploy.id - }, run: calibre site deploys --site=calibre --cursor=${ - index.pageInfo.endCursor - }` - ) - } -} - -const command = 'deploys [options]' -const describe = 'List all deployments for a Site.' -const handler = main -const builder = { - site: options.site, - count: options.count, - cursor: options.cursor, - json: options.json -} - -export { command, describe, handler, builder } diff --git a/src/cli/site/download-snapshot-artifacts.js b/src/cli/site/download-snapshot-artifacts.js deleted file mode 100644 index 7dc5bc63..00000000 --- a/src/cli/site/download-snapshot-artifacts.js +++ /dev/null @@ -1,182 +0,0 @@ -import fs from 'node:fs' -import path from 'node:path' -import chalk from 'chalk' -import listr from 'listr' - -import download from '../../utils/download.js' -import { fetchArtifacts } from '../../api/snapshot.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async args => { - const directories = [ - process.cwd(), - 'snapshot-artifacts', - args.site, - String(args.id) - ] - const rootPath = path.join(...directories) - - const manifest = { - site: args.site, - snapshotId: args.id, - rootPath, - tests: [] - } - - try { - const response = await fetchArtifacts(args) - - if (args.json) return console.log(JSON.stringify(response, null, 2)) - if (!response.snapshot) throw new Error('Snapshot not found') - - let tasks = [ - { - title: `Creating directory for artifacts: ${path.relative( - process.cwd(), - rootPath - )}`, - task: () => { - // eslint-disable-next-line security/detect-non-literal-fs-filename - fs.mkdirSync(directories.join(path.sep), { recursive: true }) - return Promise.resolve() - } - }, - { - title: `Fetching artifacts for ${chalk.bold( - `Snapshot #${args.id} -- ${response.snapshot.tests.length} tests` - )}`, - task: () => Promise.resolve() - } - ] - - for (const test of response.snapshot.tests) { - const { page, testProfile: profile } = test - const profileDirectoryPath = directories - .concat(`${page.name.replace(path.sep, '')}-${page.uuid}`) - .concat([`${profile.name.replace(path.sep, '')}-${profile.uuid}`]) - .join(path.sep) - - // eslint-disable-next-line security/detect-non-literal-fs-filename - const profileDirectory = fs.mkdirSync(profileDirectoryPath, { - recursive: true - }) - - const testManifest = { - page: { - uuid: page.uuid, - name: page.name - }, - profile: { - uuid: profile.uuid, - name: profile.name - } - } - - tasks.push({ - title: `Downloading artifacts (Page: ${page.name}) (Test Profile: ${profile.name})`, - - task: () => { - const subtasks = [ - { - title: 'Screenshot', - skip: () => { - if (!test.image) return 'No screenshot available' - }, - task: () => { - const screenshotPath = path.join(profileDirectory, 'image.jpg') - testManifest.screenshotPath = path.relative( - rootPath, - screenshotPath - ) - return download(test.image, screenshotPath) - } - }, - { - title: 'MP4 Video Render', - skip: () => { - if (!test.video) return 'No MP4 Video Render available' - }, - task: () => { - const mp4VideoRenderPath = path.join( - profileDirectory, - 'render.mp4' - ) - testManifest.mp4VideoRenderPath = path.relative( - rootPath, - mp4VideoRenderPath - ) - return download(test.video, mp4VideoRenderPath) - } - }, - { - title: 'HAR', - skip: () => { - if (!test.har) return 'No HAR available' - }, - task: () => { - const harPath = path.join(profileDirectory, 'requests.har') - testManifest.harPath = path.relative(rootPath, harPath) - return download(test.har, harPath) - } - }, - { - title: 'Lighthouse Report', - skip: () => { - if (!test.lighthouse) return 'No Lighthouse Report available' - }, - task: () => { - const lighthousePath = path.join( - profileDirectory, - 'lighthouse.json' - ) - testManifest.lighthousePath = path.relative( - rootPath, - lighthousePath - ) - return download(test.lighthouse, lighthousePath) - } - } - ] - - return new listr(subtasks, { concurrent: true }) - } - }) - - manifest.tests.push(testManifest) - } - - tasks.push({ - title: 'Saving manifest file', - task: () => { - const manifestPath = path.join(...directories, 'manifest.json') - // eslint-disable-next-line security/detect-non-literal-fs-filename - return fs.writeFile(manifestPath, JSON.stringify(manifest), error => { - if (error) throw error - }) - } - }) - - await new listr(tasks).run() - - console.log(`Saved artifacts to ${path.relative(process.cwd(), rootPath)}`) - } catch (error) { - throw new Error(humaniseError(error)) - } -} - -const command = 'download-snapshot-artifacts [options]' -const describe = - 'Download the artifacts of a Snapshot to ./snapshot-artifacts/. Includes: lighthouse.json, render progress screenshots, render progress MP4 video, HAR file (request log) and all other metrics and data available through the Calibre interface.' -const builder = { - site: options.site, - id: { - demandOption: true, - requiresArg: true, - describe: 'The id of the Snapshot.' - }, - json: options.json -} -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/get-snapshot-metrics.js b/src/cli/site/get-snapshot-metrics.js deleted file mode 100644 index 47c84d7a..00000000 --- a/src/cli/site/get-snapshot-metrics.js +++ /dev/null @@ -1,111 +0,0 @@ -import ora from 'ora' -import { AsyncParser } from '@json2csv/node' - -import { snapshot } from '../../api/snapshot-metrics.js' -import formatSnapshot from '../../views/snapshot-metrics.js' -import { options } from '../../utils/cli.js' -import { humaniseError } from '../../utils/api-error.js' - -const formatCSV = payload => { - let data = [] - - payload.snapshot.tests.forEach(test => { - const testProfile = payload.testProfiles.find( - profile => profile.uuid === test.testProfile.uuid - ) - - test.measurements.forEach(measurement => { - data.push({ - Timestamp: payload.snapshot.createdAt, - PageName: test.page.name, - PageURL: test.page.url, - MetricName: measurement.name, - MetricLabel: measurement.label, - MetricValue: measurement.value, - SnapshotSequenceId: payload.snapshot.sequenceId, - TestProfileId: testProfile.uuid, - TestProfileName: testProfile.name, - DeviceName: testProfile.device && testProfile.device.title, - BandwidthName: testProfile.bandwidth && testProfile.bandwidth.title, - isMobile: testProfile.isMobile, - hasDeviceEmulation: testProfile.hasDeviceEmulation, - hasBandwidthEmulation: testProfile.hasBandwidthEmulation - }) - }) - }) - - const fields = [ - 'Timestamp', - 'PageName', - 'PageURL', - 'MetricName', - 'MetricLabel', - 'MetricValue', - 'SnapshotSequenceId', - 'TestProfileId', - 'TestProfileName', - 'DeviceName', - 'BandwidthName', - 'isMobile', - 'hasDeviceEmulation', - 'hasBandwidthEmulation' - ] - - const parser = new AsyncParser({ - fields, - }, {}, {}) - - return parser.parse(data).promise() -} - -const main = async args => { - let spinner - if (!args.json && !args.csv) { - spinner = ora('Connecting to Calibre').start() - } - - try { - const payload = await snapshot({ - site: args.site, - snapshotId: args.snapshot - }) - - if (!payload.snapshot) throw new Error('Snapshot not found') - if (!payload.snapshot.tests.length) throw new Error('No data found') - - if (args.json) return console.log(JSON.stringify(payload, null, 2)) - if (args.csv) return console.log(await formatCSV(payload)) - - spinner.stop() - - console.log( - formatSnapshot({ - snapshot: payload.snapshot, - testProfiles: payload.testProfiles - }) - ) - } catch (e) { - if (args.json || args.csv) { - console.error(e) - } else { - spinner.fail() - throw new Error(humaniseError(e)) - } - } -} - -const command = 'get-snapshot-metrics [options]' -const describe = 'Get all metrics of a given Snapshot.' -const builder = { - site: options.site, - snapshot: { - demandOption: true, - requiresArg: true, - describe: 'The id of a Snapshot.' - }, - json: options.json, - csv: options.csv -} -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/list.js b/src/cli/site/list.js index 15f5232d..28a25e45 100644 --- a/src/cli/site/list.js +++ b/src/cli/site/list.js @@ -1,50 +1,52 @@ -import chalk from 'chalk' -import ora from 'ora' +import { styleText } from 'node:util' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { format as dateFormat } from 'date-fns' import { list } from '../../api/site.js' import { options } from '../../utils/cli.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' const main = async args => { let index let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { index = await list(args) if (args.json) return console.log(JSON.stringify(index, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } spinner.stop() - console.log(`${chalk.bold(index.length)} sites`) + console.log(`${styleText('bold', String(index.length))} sites`) const rows = index.map(row => { const statuses = [] if (row.monitoringStatus) { - if (row.monitoringStatus.synthetic === null) statuses.push(chalk.green('synthetic')) - if (row.monitoringStatus.crux === null) statuses.push(chalk.green('crux')) - if (row.monitoringStatus.rum === null) statuses.push(chalk.green('rum')) + if (row.monitoringStatus.synthetic === null) statuses.push(styleText('green', 'synthetic')) + if (row.monitoringStatus.crux === null) statuses.push(styleText('green', 'crux')) + if (row.monitoringStatus.rum === null) statuses.push(styleText('green', 'rum')) } return { - slug: chalk.grey(row.slug), + slug: styleText('gray', row.slug), name: row.name, - monitoring: statuses.join(' ') || chalk.grey('—'), + monitoring: statuses.join(' ') || styleText('gray', '—'), created: dateFormat(new Date(row.createdAt), 'h:mma d-MMM-yyyy') } }) console.log( columnify(rows, { - columnSplitter: ' | ' + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' }) ) } diff --git a/src/cli/site/metrics.js b/src/cli/site/metrics.js deleted file mode 100644 index dda3c845..00000000 --- a/src/cli/site/metrics.js +++ /dev/null @@ -1,68 +0,0 @@ -import ora from 'ora' -import { subDays } from 'date-fns' -import { parseISO } from 'date-fns' - -import { humaniseError } from '../../utils/api-error.js' -import { list } from '../../api/time-series.js' -import formatPulseTimeline from '../../views/pulse-timeline.js' -import { options } from '../../utils/cli.js' - -const main = async args => { - let spinner - if (!args.json && !args.csv) { - spinner = ora('Connecting to Calibre').start() - } - - let to, from - if (args['30-day']) { - to = new Date() - from = subDays(new Date(), 30) - } else { - if (args.to) to = parseISO(args.to) - if (args.from) from = parseISO(args.from) - } - - const variables = { - site: args.site, - pages: args.pages, - measurements: args.metrics, - profiles: args.profiles, - from, - to - } - - try { - const { csv, ...timeSeries } = await list(variables) - - if (args.csv) return console.log(csv) - if (args.json) return console.log(JSON.stringify(timeSeries, null, 2)) - - spinner.stop() - console.log(formatPulseTimeline(timeSeries)) - } catch (e) { - if (args.json) return console.error(e) - if (args.csv) return console.error('Error', e) - - spinner.fail(humaniseError(e)) - throw new Error(humaniseError(e)) - } -} -const command = 'metrics [options]' -const describe = 'Get time-series metrics for a selected Site.' -const builder = { - site: options.site, - pages: options.pages, - profiles: options.profiles, - metrics: options.metrics, - json: options.json, - csv: options.csv, - from: options.from, - to: options.to, - '30-day': { - describe: - 'Get the last 30 days of metrics. Without this flag, CLI will use the to and from values.' - } -} -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/pages.js b/src/cli/site/pages.js deleted file mode 100644 index a20726ec..00000000 --- a/src/cli/site/pages.js +++ /dev/null @@ -1,66 +0,0 @@ -import chalk from 'chalk' -import ora from 'ora' -import columnify from 'columnify' - -import { list } from '../../api/page.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async args => { - let index - let spinner - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - try { - index = await list(args) - if (args.json) return console.log(JSON.stringify(index, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } - - spinner.stop() - console.log(`${chalk.bold(index.pages.length)} pages`) - - const rows = index.pages.map(row => { - return { - uuid: chalk.grey(row.uuid), - name: row.name, - url: row.url - } - }) - - console.log( - columnify(rows, { - columnSplitter: ' | ', - truncate: true, - maxLineWidth: 'auto' - }) - ) - - if (index.pageInfo.hasNextPage) { - const lastPage = rows[rows.length - 1] - console.log( - `To see pages after ${ - lastPage.name || lastPage.uuid - }, run: calibre site pages --site=calibre --cursor=${ - index.pageInfo.endCursor - }` - ) - } -} - -const command = 'pages [options]' -const describe = 'List Pages for a selected Site.' -const handler = main -const builder = { - site: options.site, - count: options.count, - cursor: options.cursor, - json: options.json -} - -export { command, describe, handler, builder } diff --git a/src/cli/site/pull-request-review.js b/src/cli/site/pull-request-review.js deleted file mode 100644 index 8a78fd0f..00000000 --- a/src/cli/site/pull-request-review.js +++ /dev/null @@ -1,39 +0,0 @@ -import ora from 'ora' - -import { getPRReviewByBranch } from '../../api/pull-request-review.js' -import formatTest from '../../views/markdown.js' -import { options } from '../../utils/cli.js' -import { humaniseError } from '../../utils/api-error.js' - -const main = async args => { - let spinner - - if (!args.json && !args.markdown) { - spinner = ora('Connecting to Calibre').start() - spinner.text = 'Fetching Pull Request Review' - } - - try { - const [response] = await getPRReviewByBranch(args.site, args.branch) - - if (args.json) return console.log(JSON.stringify(response, null, 2)) - if (args.markdown) return console.log(response.markdownReport) - - spinner.stop() - console.log(formatTest(response.markdownReport)) - } catch (e) { - spinner.fail() - throw new Error(humaniseError(e)) - } -} - -const command = 'pull-request-review ' -const describe = 'See the results of a Pull Request Review.' -const handler = main -const builder = { - site: options.site, - json: options.json, - markdown: options.markdown -} - -export { command, describe, builder, handler } diff --git a/src/cli/site/pull-request-reviews.js b/src/cli/site/pull-request-reviews.js deleted file mode 100644 index b672978a..00000000 --- a/src/cli/site/pull-request-reviews.js +++ /dev/null @@ -1,61 +0,0 @@ -import ora from 'ora' -import columnify from 'columnify' -import { format as dateFormat } from 'date-fns' - -import { list } from '../../api/pull-request-review.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async args => { - let pullRequestReviews - let spinner - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - try { - pullRequestReviews = await list(args) - if (args.json) - return console.log(JSON.stringify(pullRequestReviews, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } - - spinner.stop() - const rows = pullRequestReviews.map(row => { - return { - title: row.title, - branch: row.branch, - sha: row.sha, - status: row.status, - created: dateFormat(new Date(row.createdAt), 'h:mma d-MMM-yyyy') - } - }) - - console.log( - columnify(rows, { - columnSplitter: ' | ', - truncate: true, - config: { - title: { - maxWidth: 40 - }, - sha: { - maxWidth: 8 - } - } - }) - ) -} - -const command = 'pull-request-reviews [options]' -const describe = 'List Pull Request Reviews for a selected Site.' -const handler = main -const builder = { - site: options.site, - json: options.json -} - -export { command, describe, handler, builder } diff --git a/src/cli/site/snapshots.js b/src/cli/site/snapshots.js deleted file mode 100644 index feb5b2a4..00000000 --- a/src/cli/site/snapshots.js +++ /dev/null @@ -1,74 +0,0 @@ -import chalk from 'chalk' -import ora from 'ora' -import columnify from 'columnify' -import { format as dateFormat } from 'date-fns' - -import { list } from '../../api/snapshot.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const titleize = string => string.charAt(0).toUpperCase() + string.substring(1) - -const main = async args => { - let index - let spinner - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - try { - index = await list(args) - if (args.json) return console.log(JSON.stringify(index, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } - - spinner.stop() - console.log(`${chalk.bold(index.snapshots.length)} snapshots`) - - const rows = index.snapshots.map(row => { - return { - id: chalk.grey(row.iid), - url: row.htmlUrl, - ref: row.ref, - client: row.client, - status: `${row.status ? titleize(row.status) : ''} ${dateFormat( - new Date(row.createdAt), - 'h:mma d-MMM-yyyy' - )}` - } - }) - - console.log( - columnify(rows, { - columnSplitter: ' | ', - truncate: true, - maxLineWidth: 'auto' - }) - ) - - if (index.pageInfo.hasNextPage) { - const lastSnapshot = rows[rows.length - 1] - console.log( - `To see snapshots after ${ - lastSnapshot.ref || lastSnapshot.id - }, run: calibre site snapshots --site=calibre --cursor=${ - index.pageInfo.endCursor - }` - ) - } -} - -const command = 'snapshots [options]' -const describe = 'List selected Snapshots for a Site.' -const handler = main -const builder = { - site: options.site, - count: options.count, - cursor: options.cursor, - json: options.json -} - -export { command, describe, handler, builder } diff --git a/src/cli/site/test-profiles.js b/src/cli/site/test-profiles.js deleted file mode 100644 index 8b76bc1c..00000000 --- a/src/cli/site/test-profiles.js +++ /dev/null @@ -1,57 +0,0 @@ -import chalk from 'chalk' -import ora from 'ora' -import columnify from 'columnify' - -import { list } from '../../api/test-profile.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async args => { - let index - let spinner - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - try { - index = await list(args) - if (args.json) return console.log(JSON.stringify(index, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } - - spinner.stop() - - console.log('\n') - - const rows = index.map(row => { - return { - uuid: chalk.cyan(row.uuid), - name: row.name, - device: row.device ? row.device.title : 'Desktop', - connection: row.bandwidth ? row.bandwidth.title : 'Not Throttled', - 'javascript disabled': row.jsIsDisabled ? 'Yes' : 'No', - cookies: row.cookies.map(cookie => cookie.name).join(', ') || 'None' - } - }) - - console.log( - columnify(rows, { - columnSplitter: ' | ', - truncate: true, - maxLineWidth: 'auto' - }) - ) -} - -const command = 'test-profiles [options]' -const describe = 'List all Test Profiles for a Site.' -const handler = main -const builder = { - site: options.site, - json: options.json -} - -export { command, describe, handler, builder } diff --git a/src/cli/site/update-page.js b/src/cli/site/update-page.js deleted file mode 100644 index 1a9cbc02..00000000 --- a/src/cli/site/update-page.js +++ /dev/null @@ -1,53 +0,0 @@ -import { URL } from 'url' -import ora from 'ora' - -import { update } from '../../api/page.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const main = async function (args) { - let spinner - - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - if (args.url) { - try { - new URL(args.url) - } catch { - return new Error('Please enter a valid URL') - } - } - - try { - const response = await update(args) - if (!args.json) - spinner.succeed(`Page updated: ${response.name} (${response.uuid})`) - - // Return result - if (args.json) return console.log(JSON.stringify(response, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } -} - -const command = 'update-page [options]' -const describe = 'Update the name or URL of a Page.' -const builder = { - uuid: { - demandOption: true, - requiresArg: true, - describe: 'The UUID of the Page.' - }, - name: { describe: 'Update the name of the Page.' }, - url: { describe: 'Update the URL of the Page.' }, - site: options.site, - json: options.json -} - -const handler = main - -export { command, describe, builder, handler } diff --git a/src/cli/site/update-test-profile.js b/src/cli/site/update-test-profile.js deleted file mode 100644 index 7ca5fafb..00000000 --- a/src/cli/site/update-test-profile.js +++ /dev/null @@ -1,75 +0,0 @@ -import ora from 'ora' -import cookiefile from 'cookiefile' - -import formatProfile from '../../views/test-profile.js' -import { update } from '../../api/test-profile.js' -import { humaniseError } from '../../utils/api-error.js' -import { options } from '../../utils/cli.js' - -const { CookieMap } = cookiefile - -const main = async function (args) { - let spinner - let cookies = [] - - if (!args.json) { - spinner = ora('Connecting to Calibre').start() - } - - if (args.cookieJar) { - const jar = new CookieMap(args.cookieJar) - - for (const cookie of jar.values()) { - const { name, value, domain, path, httpOnly, https } = cookie - cookies.push({ name, value, domain, path, httpOnly, secure: https }) - } - } - - try { - const response = await update({ ...args, cookies }) - if (!args.json) { - spinner.succeed( - `Test Profile updated: ${response.name} (${response.uuid})` - ) - console.log(formatProfile(response)) - } - - // Return result - if (args.json) return console.log(JSON.stringify(response, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - - spinner.fail() - throw new Error(humaniseError(e)) - } -} -const command = 'update-test-profile [options]' -const describe = - 'Update Test Profile settings. Only changes specified attributes.' -const builder = { - uuid: { - demandOption: true, - requiresArg: true, - describe: 'The UUID of the Test Profile.' - }, - device: { - describe: 'Set the emulated device.' - }, - connection: { - describe: 'Set the emulated network connection speed.' - }, - site: options.site, - json: options.json, - javascript: { - type: 'boolean', - describe: 'Turn JavaScript execution on or off', - default: true - }, - 'cookie-jar': { - describe: - 'Set cookies by specifying a path to a Netscape formatted cookie jar file.' - } -} - -const handler = main -export { command, describe, builder, handler } diff --git a/src/cli/synthetic/connections.js b/src/cli/synthetic/connections.js index fc711d7f..28ea60b1 100644 --- a/src/cli/synthetic/connections.js +++ b/src/cli/synthetic/connections.js @@ -1,33 +1,33 @@ -import chalk from 'chalk' -import ora from 'ora' +import { styleText } from 'node:util' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { list } from '../../api/connection.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async args => { let index let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { index = await list(args) if (args.json) return console.log(JSON.stringify(index, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } spinner.stop() - console.log(`${chalk.bold(index.length)} emulated connection speeds`) + console.log(`${styleText('bold', String(index.length))} emulated connection speeds`) const rows = index.map(row => { return { - identifier: chalk.cyan(row.name) + identifier: styleText('cyan', row.name) } }) diff --git a/src/cli/synthetic/create-page.js b/src/cli/synthetic/create-page.js index 124eddd5..a47110cd 100644 --- a/src/cli/synthetic/create-page.js +++ b/src/cli/synthetic/create-page.js @@ -1,15 +1,15 @@ import { URL } from 'url' -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { create } from '../../api/page.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async function (args) { let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { @@ -21,13 +21,13 @@ const main = async function (args) { try { const response = await create(args) if (!args.json) - spinner.succeed(`Page added: ${response.name} (${response.uuid})`) + spinner.success({ text: `Page added: ${response.name} (${response.uuid})` }) // Return result if (args.json) return console.log(JSON.stringify(response, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/synthetic/create-pull-request-review.js b/src/cli/synthetic/create-pull-request-review.js index 900736f5..714bff9b 100644 --- a/src/cli/synthetic/create-pull-request-review.js +++ b/src/cli/synthetic/create-pull-request-review.js @@ -1,11 +1,11 @@ import { promises as fs } from 'fs' -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { create, waitForReviewCompletion } from '../../api/pull-request-review.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' import formatMarkdownResult from '../../views/markdown.js' @@ -27,7 +27,7 @@ const main = async function (args) { let spinner if (!args.json && !args.markdown) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } if (args.configPath) { @@ -39,7 +39,7 @@ const main = async function (args) { const response = await create(args) if (!args.waitForResult) { - spinner.succeed(`Pull Request Review queued: ${args.branch}.`) + spinner.success({ text: `Pull Request Review queued: ${args.branch}.` }) console.log( `View progress by running \`calibre synthetic pull-request-review ${args.branch} --site=${args.site}\`` @@ -48,8 +48,8 @@ const main = async function (args) { return print(args, response) } else { if (spinner) { - spinner.succeed(`Pull Request Review queued: ${args.branch}`) - spinner = ora('Waiting for Pull Request Review to complete').start() + spinner.success({ text: `Pull Request Review queued: ${args.branch}` }) + spinner = createSpinner('Waiting for Pull Request Review to complete').start() } const completedResponse = await waitForReviewCompletion( @@ -58,7 +58,7 @@ const main = async function (args) { ) if (spinner) { - spinner.succeed('Pull Request Review completed') + spinner.success({ text: 'Pull Request Review completed' }) } print(args, completedResponse) @@ -71,8 +71,8 @@ const main = async function (args) { } } } catch (e) { - if (args.json || args.markdown) return console.error(e) - spinner.fail() + if (args.json || args.markdown) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/synthetic/create-snapshot.js b/src/cli/synthetic/create-snapshot.js index 807e87a9..3a9e1467 100644 --- a/src/cli/synthetic/create-snapshot.js +++ b/src/cli/synthetic/create-snapshot.js @@ -1,25 +1,25 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { create } from '../../api/snapshot.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async function (args) { let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { const response = await create(args) - if (!args.json) spinner.succeed(`Snapshot created: ${response.iid}`) + if (!args.json) spinner.success({ text: `Snapshot created: ${response.iid}` }) // Return result if (args.json) return console.log(JSON.stringify(response, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/synthetic/create-test-profile.js b/src/cli/synthetic/create-test-profile.js index 3c2b06ea..6ad870a2 100644 --- a/src/cli/synthetic/create-test-profile.js +++ b/src/cli/synthetic/create-test-profile.js @@ -1,9 +1,9 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import cookiefile from 'cookiefile' import formatProfile from '../../views/test-profile.js' import { create } from '../../api/test-profile.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const { CookieMap } = cookiefile @@ -13,7 +13,7 @@ const main = async function (args) { let cookies = [] if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } if (args.cookieJar) { @@ -28,16 +28,16 @@ const main = async function (args) { try { const response = await create({ ...args, cookies }) if (!args.json) { - spinner.succeed('Created Test Profile') + spinner.success({ text: 'Created Test Profile' }) console.log(formatProfile(response)) } // Return result if (args.json) return console.log(JSON.stringify(response, null, 2)) } catch (e) { - if (args.json) return console.error(e) + if (args.json) return formatJsonError(e) - spinner.fail() + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/synthetic/delete-page.js b/src/cli/synthetic/delete-page.js index 2846a5a6..c659e130 100644 --- a/src/cli/synthetic/delete-page.js +++ b/src/cli/synthetic/delete-page.js @@ -1,14 +1,14 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { destroy } from '../../api/page.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async function (args) { let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } if (process.stdout.isTTY && !args.confirm) { @@ -20,13 +20,13 @@ const main = async function (args) { try { const response = await destroy(args) if (!args.json) - spinner.succeed(`Page deleted: ${response.name} (${response.uuid})`) + spinner.success({ text: `Page deleted: ${response.name} (${response.uuid})` }) // Return result if (args.json) return console.log(JSON.stringify(response, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/synthetic/delete-snapshot.js b/src/cli/synthetic/delete-snapshot.js index f3920ace..98e3a6da 100644 --- a/src/cli/synthetic/delete-snapshot.js +++ b/src/cli/synthetic/delete-snapshot.js @@ -1,14 +1,14 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { destroy } from '../../api/snapshot.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async function (args) { let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } if (process.stdout.isTTY && !args.confirm) { @@ -19,13 +19,13 @@ const main = async function (args) { try { const response = await destroy(args) - if (!args.json) spinner.succeed(`Snapshot deleted: ${response.iid}`) + if (!args.json) spinner.success({ text: `Snapshot deleted: ${response.iid}` }) // Return result if (args.json) return console.log(JSON.stringify(response, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail(humaniseError(e)) + if (args.json) return formatJsonError(e) + spinner.stop() return Error(humaniseError(e)) } } diff --git a/src/cli/synthetic/delete-test-profile.js b/src/cli/synthetic/delete-test-profile.js index a06a74d5..11dacc81 100644 --- a/src/cli/synthetic/delete-test-profile.js +++ b/src/cli/synthetic/delete-test-profile.js @@ -1,14 +1,14 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { destroy } from '../../api/test-profile.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async function (args) { let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } if (process.stdout.isTTY && !args.confirm) { @@ -20,15 +20,15 @@ const main = async function (args) { try { const response = await destroy(args) if (!args.json) - spinner.succeed( - `Test Profile deleted: ${response.name} (${response.uuid})` - ) + spinner.success({ + text: `Test Profile deleted: ${response.name} (${response.uuid})` + }) // Return result if (args.json) return console.log(JSON.stringify(response, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/synthetic/devices.js b/src/cli/synthetic/devices.js index dcb09a05..c20b474c 100644 --- a/src/cli/synthetic/devices.js +++ b/src/cli/synthetic/devices.js @@ -1,33 +1,33 @@ -import chalk from 'chalk' -import ora from 'ora' +import { styleText } from 'node:util' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { list } from '../../api/device.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async args => { let index let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { index = await list(args) if (args.json) return console.log(JSON.stringify(index, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } spinner.stop() - console.log(`${chalk.bold(index.length)} test devices`) + console.log(`${styleText('bold', String(index.length))} test devices`) const rows = index.map(row => { return { - identifier: chalk.cyan(row.tag), + identifier: styleText('cyan', row.tag), name: row.title, type: row.type, 'screen width': row.screenWidth, diff --git a/src/cli/synthetic/download-artifacts.js b/src/cli/synthetic/download-artifacts.js index 23473ead..ab8d7dbd 100644 --- a/src/cli/synthetic/download-artifacts.js +++ b/src/cli/synthetic/download-artifacts.js @@ -1,6 +1,6 @@ import fs from 'node:fs' import path from 'node:path' -import chalk from 'chalk' +import { styleText } from 'node:util' import listr from 'listr' import download from '../../utils/download.js' @@ -43,7 +43,7 @@ const main = async args => { } }, { - title: `Fetching artifacts for ${chalk.bold( + title: `Fetching artifacts for ${styleText('bold', `Snapshot #${args.id} -- ${response.snapshot.tests.length} tests` )}`, task: () => Promise.resolve() diff --git a/src/cli/synthetic/get-snapshot-metrics.js b/src/cli/synthetic/get-snapshot-metrics.js index 47c84d7a..0c260107 100644 --- a/src/cli/synthetic/get-snapshot-metrics.js +++ b/src/cli/synthetic/get-snapshot-metrics.js @@ -1,10 +1,10 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { AsyncParser } from '@json2csv/node' import { snapshot } from '../../api/snapshot-metrics.js' import formatSnapshot from '../../views/snapshot-metrics.js' import { options } from '../../utils/cli.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' const formatCSV = payload => { let data = [] @@ -61,7 +61,7 @@ const formatCSV = payload => { const main = async args => { let spinner if (!args.json && !args.csv) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { @@ -86,9 +86,9 @@ const main = async args => { ) } catch (e) { if (args.json || args.csv) { - console.error(e) + formatJsonError(e) } else { - spinner.fail() + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/synthetic/locations.js b/src/cli/synthetic/locations.js index 8b49bba4..4eed2063 100644 --- a/src/cli/synthetic/locations.js +++ b/src/cli/synthetic/locations.js @@ -1,33 +1,33 @@ -import chalk from 'chalk' -import ora from 'ora' +import { styleText } from 'node:util' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { list } from '../../api/location.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async args => { let index let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { index = await list(args) if (args.json) return console.log(JSON.stringify(index, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } spinner.stop() - console.log(`${chalk.bold(index.length)} test locations`) + console.log(`${styleText('bold', String(index.length))} test locations`) const rows = index.map(row => { return { - identifier: chalk.cyan(row.tag), + identifier: styleText('cyan', row.tag), name: row.name, 'ipv4 address': row.agents.map(a => a.ipv4).join(', ') } diff --git a/src/cli/synthetic/metrics.js b/src/cli/synthetic/metrics.js index dda3c845..ec7b21ba 100644 --- a/src/cli/synthetic/metrics.js +++ b/src/cli/synthetic/metrics.js @@ -1,8 +1,8 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { subDays } from 'date-fns' import { parseISO } from 'date-fns' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { list } from '../../api/time-series.js' import formatPulseTimeline from '../../views/pulse-timeline.js' import { options } from '../../utils/cli.js' @@ -10,7 +10,7 @@ import { options } from '../../utils/cli.js' const main = async args => { let spinner if (!args.json && !args.csv) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } let to, from @@ -40,10 +40,10 @@ const main = async args => { spinner.stop() console.log(formatPulseTimeline(timeSeries)) } catch (e) { - if (args.json) return console.error(e) + if (args.json) return formatJsonError(e) if (args.csv) return console.error('Error', e) - spinner.fail(humaniseError(e)) + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/synthetic/pages.js b/src/cli/synthetic/pages.js index a20726ec..5b2f02f7 100644 --- a/src/cli/synthetic/pages.js +++ b/src/cli/synthetic/pages.js @@ -1,55 +1,90 @@ -import chalk from 'chalk' -import ora from 'ora' +import { styleText } from 'node:util' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { list } from '../../api/page.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' +import { fetchAll } from '../../utils/pagination.js' const main = async args => { - let index + if (args.all && args.cursor) { + throw new Error('Cannot use --all with --cursor.') + } + let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { - index = await list(args) - if (args.json) return console.log(JSON.stringify(index, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } + if (args.all) { + const pages = await fetchAll( + async a => { + const res = await list(a) + return { items: res.pages, pageInfo: res.pageInfo } + }, + args + ) - spinner.stop() - console.log(`${chalk.bold(index.pages.length)} pages`) + if (!args.json) spinner.stop() + if (args.json) return console.log(JSON.stringify(pages, null, 2)) - const rows = index.pages.map(row => { - return { - uuid: chalk.grey(row.uuid), - name: row.name, - url: row.url - } - }) + console.log(`${styleText('bold', String(pages.length))} pages`) + + const rows = pages.map(row => { + return { + uuid: styleText('gray', row.uuid), + name: row.name, + url: row.url + } + }) - console.log( - columnify(rows, { - columnSplitter: ' | ', - truncate: true, - maxLineWidth: 'auto' - }) - ) + console.log( + columnify(rows, { + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' + }) + ) + } else { + const index = await list(args) + if (args.json) return console.log(JSON.stringify(index, null, 2)) - if (index.pageInfo.hasNextPage) { - const lastPage = rows[rows.length - 1] - console.log( - `To see pages after ${ - lastPage.name || lastPage.uuid - }, run: calibre site pages --site=calibre --cursor=${ - index.pageInfo.endCursor - }` - ) + spinner.stop() + console.log(`${styleText('bold', String(index.pages.length))} pages`) + + const rows = index.pages.map(row => { + return { + uuid: styleText('gray', row.uuid), + name: row.name, + url: row.url + } + }) + + console.log( + columnify(rows, { + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' + }) + ) + + if (index.pageInfo.hasNextPage) { + const lastPage = rows[rows.length - 1] + console.log( + `To see pages after ${ + lastPage.name || lastPage.uuid + }, run: calibre synthetic pages --site=${args.site} --cursor=${ + index.pageInfo.endCursor + }` + ) + } + } + } catch (e) { + if (args.json) return formatJsonError(e) + if (spinner) spinner.stop() + throw new Error(humaniseError(e)) } } @@ -60,7 +95,12 @@ const builder = { site: options.site, count: options.count, cursor: options.cursor, - json: options.json + json: options.json, + all: { + describe: 'Fetch all pages (overrides --count and --cursor).', + type: 'boolean', + default: false + } } export { command, describe, handler, builder } diff --git a/src/cli/synthetic/pull-request-review.js b/src/cli/synthetic/pull-request-review.js index 8a78fd0f..0d264ff1 100644 --- a/src/cli/synthetic/pull-request-review.js +++ b/src/cli/synthetic/pull-request-review.js @@ -1,4 +1,4 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { getPRReviewByBranch } from '../../api/pull-request-review.js' import formatTest from '../../views/markdown.js' @@ -9,8 +9,8 @@ const main = async args => { let spinner if (!args.json && !args.markdown) { - spinner = ora('Connecting to Calibre').start() - spinner.text = 'Fetching Pull Request Review' + spinner = createSpinner('Connecting to Calibre').start() + spinner.update({ text: 'Fetching Pull Request Review' }) } try { @@ -22,7 +22,7 @@ const main = async args => { spinner.stop() console.log(formatTest(response.markdownReport)) } catch (e) { - spinner.fail() + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/synthetic/pull-request-reviews.js b/src/cli/synthetic/pull-request-reviews.js index b672978a..d66e46c3 100644 --- a/src/cli/synthetic/pull-request-reviews.js +++ b/src/cli/synthetic/pull-request-reviews.js @@ -1,16 +1,16 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { format as dateFormat } from 'date-fns' import { list } from '../../api/pull-request-review.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async args => { let pullRequestReviews let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { @@ -18,8 +18,8 @@ const main = async args => { if (args.json) return console.log(JSON.stringify(pullRequestReviews, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } @@ -38,6 +38,7 @@ const main = async args => { columnify(rows, { columnSplitter: ' | ', truncate: true, + maxLineWidth: 'auto', config: { title: { maxWidth: 40 diff --git a/src/cli/synthetic/snapshots.js b/src/cli/synthetic/snapshots.js index feb5b2a4..4b9bf8c3 100644 --- a/src/cli/synthetic/snapshots.js +++ b/src/cli/synthetic/snapshots.js @@ -1,63 +1,107 @@ -import chalk from 'chalk' -import ora from 'ora' +import { styleText } from 'node:util' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { format as dateFormat } from 'date-fns' import { list } from '../../api/snapshot.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' +import { fetchAll } from '../../utils/pagination.js' const titleize = string => string.charAt(0).toUpperCase() + string.substring(1) const main = async args => { - let index + if (args.all && args.cursor) { + throw new Error('Cannot use --all with --cursor.') + } + let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { - index = await list(args) - if (args.json) return console.log(JSON.stringify(index, null, 2)) - } catch (e) { - if (args.json) return console.error(e) - spinner.fail() - throw new Error(humaniseError(e)) - } + if (args.all) { + const snapshots = await fetchAll( + async a => { + const res = await list(a) + return { items: res.snapshots, pageInfo: res.pageInfo } + }, + args + ) - spinner.stop() - console.log(`${chalk.bold(index.snapshots.length)} snapshots`) + if (!args.json) spinner.stop() + if (args.json) return console.log(JSON.stringify(snapshots, null, 2)) - const rows = index.snapshots.map(row => { - return { - id: chalk.grey(row.iid), - url: row.htmlUrl, - ref: row.ref, - client: row.client, - status: `${row.status ? titleize(row.status) : ''} ${dateFormat( - new Date(row.createdAt), - 'h:mma d-MMM-yyyy' - )}` - } - }) + console.log( + `${styleText('bold', String(snapshots.length))} snapshots` + ) + + const rows = snapshots.map(row => { + return { + id: styleText('gray', String(row.iid)), + url: row.htmlUrl, + ref: row.ref, + client: row.client, + status: `${row.status ? titleize(row.status) : ''} ${dateFormat( + new Date(row.createdAt), + 'h:mma d-MMM-yyyy' + )}` + } + }) - console.log( - columnify(rows, { - columnSplitter: ' | ', - truncate: true, - maxLineWidth: 'auto' - }) - ) + console.log( + columnify(rows, { + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' + }) + ) + } else { + const index = await list(args) + if (args.json) return console.log(JSON.stringify(index, null, 2)) - if (index.pageInfo.hasNextPage) { - const lastSnapshot = rows[rows.length - 1] - console.log( - `To see snapshots after ${ - lastSnapshot.ref || lastSnapshot.id - }, run: calibre site snapshots --site=calibre --cursor=${ - index.pageInfo.endCursor - }` - ) + spinner.stop() + console.log( + `${styleText('bold', String(index.snapshots.length))} snapshots` + ) + + const rows = index.snapshots.map(row => { + return { + id: styleText('gray', String(row.iid)), + url: row.htmlUrl, + ref: row.ref, + client: row.client, + status: `${row.status ? titleize(row.status) : ''} ${dateFormat( + new Date(row.createdAt), + 'h:mma d-MMM-yyyy' + )}` + } + }) + + console.log( + columnify(rows, { + columnSplitter: ' | ', + truncate: true, + maxLineWidth: 'auto' + }) + ) + + if (index.pageInfo.hasNextPage) { + const lastSnapshot = rows[rows.length - 1] + console.log( + `To see snapshots after ${ + lastSnapshot.ref || lastSnapshot.id + }, run: calibre synthetic snapshots --site=${args.site} --cursor=${ + index.pageInfo.endCursor + }` + ) + } + } + } catch (e) { + if (args.json) return formatJsonError(e) + if (spinner) spinner.stop() + throw new Error(humaniseError(e)) } } @@ -68,7 +112,12 @@ const builder = { site: options.site, count: options.count, cursor: options.cursor, - json: options.json + json: options.json, + all: { + describe: 'Fetch all snapshots (overrides --count and --cursor).', + type: 'boolean', + default: false + } } export { command, describe, handler, builder } diff --git a/src/cli/synthetic/test-profiles.js b/src/cli/synthetic/test-profiles.js index 8b76bc1c..d9cd9b32 100644 --- a/src/cli/synthetic/test-profiles.js +++ b/src/cli/synthetic/test-profiles.js @@ -1,24 +1,24 @@ -import chalk from 'chalk' -import ora from 'ora' +import { styleText } from 'node:util' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { list } from '../../api/test-profile.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async args => { let index let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { index = await list(args) if (args.json) return console.log(JSON.stringify(index, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } @@ -28,7 +28,7 @@ const main = async args => { const rows = index.map(row => { return { - uuid: chalk.cyan(row.uuid), + uuid: styleText('cyan', row.uuid), name: row.name, device: row.device ? row.device.title : 'Desktop', connection: row.bandwidth ? row.bandwidth.title : 'Not Throttled', diff --git a/src/cli/synthetic/update-page.js b/src/cli/synthetic/update-page.js index 1a9cbc02..e612ff4b 100644 --- a/src/cli/synthetic/update-page.js +++ b/src/cli/synthetic/update-page.js @@ -1,15 +1,15 @@ import { URL } from 'url' -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { update } from '../../api/page.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const main = async function (args) { let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } if (args.url) { @@ -23,13 +23,13 @@ const main = async function (args) { try { const response = await update(args) if (!args.json) - spinner.succeed(`Page updated: ${response.name} (${response.uuid})`) + spinner.success({ text: `Page updated: ${response.name} (${response.uuid})` }) // Return result if (args.json) return console.log(JSON.stringify(response, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/synthetic/update-test-profile.js b/src/cli/synthetic/update-test-profile.js index 7ca5fafb..a06894d7 100644 --- a/src/cli/synthetic/update-test-profile.js +++ b/src/cli/synthetic/update-test-profile.js @@ -1,9 +1,9 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import cookiefile from 'cookiefile' import formatProfile from '../../views/test-profile.js' import { update } from '../../api/test-profile.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const { CookieMap } = cookiefile @@ -13,7 +13,7 @@ const main = async function (args) { let cookies = [] if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } if (args.cookieJar) { @@ -28,18 +28,18 @@ const main = async function (args) { try { const response = await update({ ...args, cookies }) if (!args.json) { - spinner.succeed( - `Test Profile updated: ${response.name} (${response.uuid})` - ) + spinner.success({ + text: `Test Profile updated: ${response.name} (${response.uuid})` + }) console.log(formatProfile(response)) } // Return result if (args.json) return console.log(JSON.stringify(response, null, 2)) } catch (e) { - if (args.json) return console.error(e) + if (args.json) return formatJsonError(e) - spinner.fail() + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/team/list.js b/src/cli/team/list.js index 50c3ade2..6336aed4 100644 --- a/src/cli/team/list.js +++ b/src/cli/team/list.js @@ -1,33 +1,33 @@ -import chalk from 'chalk' -import ora from 'ora' +import { styleText } from 'node:util' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { list } from '../../api/team.js' import { options } from '../../utils/cli.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' const main = async args => { let index let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { index = await list(args) if (args.json) return console.log(JSON.stringify(index, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } spinner.stop() - console.log(`${chalk.bold(index.length)} sites`) + console.log(`${styleText('bold', String(index.length))} teams`) const rows = index.map(row => { return { - slug: chalk.grey(row.slug), + slug: styleText('gray', row.slug), name: row.name, description: row.description } diff --git a/src/cli/test/create.js b/src/cli/test/create.js index 57b39088..87d7104a 100644 --- a/src/cli/test/create.js +++ b/src/cli/test/create.js @@ -1,12 +1,13 @@ import { URL } from 'url' -import ora from 'ora' +import { createSpinner } from 'nanospinner' +import { parseISO, isValid } from 'date-fns' import cookiefile from 'cookiefile' import fs from 'fs' import { create, waitForTest } from '../../api/test.js' import formatTest from '../../views/markdown.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' import { options } from '../../utils/cli.js' const { CookieMap } = cookiefile @@ -18,7 +19,7 @@ const main = async function (args) { let blockedHosts = [] if (!args.json && !args.markdown) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } if (args.cookieJar) { @@ -83,11 +84,11 @@ const main = async function (args) { } if (args.expiresAt) { - const date = new Date(args.expiresAt).valueOf() + const date = parseISO(args.expiresAt) - if (isNaN(date)) { + if (!isValid(date)) { throw new Error( - 'Please enter a valid `expiresAt` ISO8601 date time string' + 'Please enter a valid ISO8601 date string, e.g. 2025-12-31 or 2025-12-31T23:59:59Z' ) } } @@ -102,7 +103,7 @@ const main = async function (args) { }) if (!args.json && !args.markdown && !args.waitForTest) { - spinner.succeed(`Test scheduled: ${formattedTestUrl}`) + spinner.success({ text: `Test scheduled: ${formattedTestUrl}` }) } else { const test = await waitForTest(uuid) @@ -112,16 +113,16 @@ const main = async function (args) { console.log(test.markdownReport) } else { if (test.status == 'completed') { - spinner.succeed(`Test complete: ${formattedTestUrl}`) + spinner.success({ text: `Test complete: ${formattedTestUrl}` }) console.log(formatTest(test.markdownReport)) } else { - spinner.fail('Test complete') + spinner.stop() } } } } catch (e) { - if (args.json || args.markdown) return console.error(e) - spinner.fail() + if (args.json || args.markdown) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } } @@ -155,7 +156,7 @@ const builder = { }, expiresAt: { describe: - 'Set a future UTC date time string (ISO8601). After this date, the test will be automatically deleted. (Min=24 hrs, Max=2 years) e.g.: 2025-12-31T23:59:59Z', + 'Set a future UTC date time string (ISO8601). After this date, the test will be automatically deleted. (Min=24 hrs, Max=2 years) e.g. 2025-12-31 or 2025-12-31T23:59:59Z', type: 'string', default: new Date(Date.now() + 31556952000).toISOString(), defaultDescription: 'Default: Expires 1 year from creation date.' diff --git a/src/cli/test/list.js b/src/cli/test/list.js index f582b162..086f774a 100644 --- a/src/cli/test/list.js +++ b/src/cli/test/list.js @@ -1,13 +1,13 @@ import { URL } from 'url' -import chalk from 'chalk' -import ora from 'ora' +import { styleText } from 'node:util' +import { createSpinner } from 'nanospinner' import columnify from 'columnify' import { format as dateFormat } from 'date-fns' import { getList } from '../../api/test.js' import { options } from '../../utils/cli.js' -import { humaniseError } from '../../utils/api-error.js' +import { humaniseError, formatJsonError } from '../../utils/api-error.js' const titleize = string => string.charAt(0).toUpperCase() + string.substring(1) @@ -15,27 +15,27 @@ const main = async args => { let index let spinner if (!args.json) { - spinner = ora('Connecting to Calibre').start() + spinner = createSpinner('Connecting to Calibre').start() } try { index = await getList() if (args.json) return console.log(JSON.stringify(index, null, 2)) } catch (e) { - if (args.json) return console.error(e) - spinner.fail() + if (args.json) return formatJsonError(e) + spinner.stop() throw new Error(humaniseError(e)) } spinner.stop() - console.log(`${chalk.bold('♤ calibre')} test runs\n`) + console.log(`${styleText('bold', '♤ calibre')} test runs\n`) const rows = index.map(row => { const url = new URL(row.url) const formattedTestUrl = `${url.hostname}${url.pathname}` return { - uuid: chalk.grey(row.uuid), + uuid: styleText('gray', row.uuid), url: formattedTestUrl, device: row.device ? row.device.title : 'Desktop', connection: row.connection ? row.connection.title : 'Not Throttled', diff --git a/src/cli/test/show.js b/src/cli/test/show.js index fccc3af6..e1c16493 100644 --- a/src/cli/test/show.js +++ b/src/cli/test/show.js @@ -1,4 +1,4 @@ -import ora from 'ora' +import { createSpinner } from 'nanospinner' import { getTestByUuid } from '../../api/test.js' import formatTest from '../../views/markdown.js' @@ -9,8 +9,8 @@ const main = async args => { let spinner if (!args.json && !args.markdown) { - spinner = ora('Connecting to Calibre').start() - spinner.text = 'Downloading test results' + spinner = createSpinner('Connecting to Calibre').start() + spinner.update({ text: 'Downloading test results' }) } try { @@ -26,7 +26,7 @@ const main = async args => { spinner.stop() console.log(formatTest(response.markdownReport)) } catch (e) { - spinner.fail() + spinner.stop() throw new Error(humaniseError(e)) } } diff --git a/src/cli/token/remove.js b/src/cli/token/remove.js index f14c2f57..5a85d926 100644 --- a/src/cli/token/remove.js +++ b/src/cli/token/remove.js @@ -1,15 +1,14 @@ import Configstore from 'configstore' -import chalk from 'chalk' -import logSymbols from 'log-symbols' +import { styleText } from 'node:util' const config = new Configstore('calibre') const main = async () => { if (config.get('token')) { config.set('token', null) - console.log(`${chalk.bold.green(`${logSymbols.success} API Token removed.`)}\n`) + console.log(`${styleText(['bold', 'green'], '\u2714 API Token removed.')}\n`) } else { - console.log(`${chalk.bold.red(`${logSymbols.error} API Token is not set.`)}\n`) + console.log(`${styleText(['bold', 'red'], '\u2716 API Token is not set.')}\n`) } } diff --git a/src/cli/token/set.js b/src/cli/token/set.js index ec63966a..736db8a9 100644 --- a/src/cli/token/set.js +++ b/src/cli/token/set.js @@ -1,12 +1,11 @@ import Configstore from 'configstore' -import chalk from 'chalk' -import logSymbols from 'log-symbols' +import { styleText } from 'node:util' const config = new Configstore('calibre') const main = async ({ token }) => { config.set('token', token) - console.log(`${chalk.bold.green(`${logSymbols.success} API Token saved!`)}\n`) + console.log(`${styleText(['bold', 'green'], '\u2714 API Token saved!')}\n`) } const command = 'set ' diff --git a/src/utils/api-error.js b/src/utils/api-error.js index 556c19a0..0d37f1ec 100644 --- a/src/utils/api-error.js +++ b/src/utils/api-error.js @@ -21,4 +21,9 @@ const humaniseError = apiError => { } } -export { handleError, humaniseError } +const formatJsonError = e => { + const message = humaniseError(e) + console.error(JSON.stringify({ error: message })) +} + +export { handleError, humaniseError, formatJsonError } diff --git a/src/utils/cli.js b/src/utils/cli.js index ab5dd63c..278de8f0 100644 --- a/src/utils/cli.js +++ b/src/utils/cli.js @@ -1,8 +1,13 @@ +import { loadProjectConfig } from './project-config.js' + +const projectConfig = loadProjectConfig() + const options = { site: { - demandOption: true, + demandOption: !projectConfig.site, + default: projectConfig.site, describe: - 'A unique slug identifying each Site. You can find it in Site Settings → General or by using the calibre site list command.', + 'The Site slug. Resolved from calibre.json if present, or pass explicitly. You can find it in Site Settings → General or by using the calibre site list command', type: 'string', requiresArg: true }, diff --git a/src/utils/deprecation.js b/src/utils/deprecation.js deleted file mode 100644 index 81a88727..00000000 --- a/src/utils/deprecation.js +++ /dev/null @@ -1,16 +0,0 @@ -import chalk from 'chalk' - -const deprecatedHandler = (oldCommand, newCommand, handler) => { - return async args => { - if (!process.env.CALIBRE_SUPPRESS_DEPRECATIONS) { - process.stderr.write( - chalk.yellow( - `[calibre:deprecated] "${oldCommand}" has moved to "${newCommand}"\n` - ) - ) - } - return handler(args) - } -} - -export { deprecatedHandler } diff --git a/src/utils/formatters/grade-score.js b/src/utils/formatters/grade-score.js index 29043123..585b5020 100644 --- a/src/utils/formatters/grade-score.js +++ b/src/utils/formatters/grade-score.js @@ -1,9 +1,9 @@ -import chalk from 'chalk' +import { styleText } from 'node:util' const toGradeScore = score => { - if (score >= 90) return chalk.green.bold(score) - if (score > 50 && score < 90) return chalk.yellow.bold(score) - if (score <= 50) return chalk.red.bold(score) + if (score >= 90) return styleText(['bold', 'green'], score) + if (score > 50 && score < 90) return styleText(['bold', 'yellow'], score) + if (score <= 50) return styleText(['bold', 'red'], score) return 'N/A' } diff --git a/src/utils/pagination.js b/src/utils/pagination.js new file mode 100644 index 00000000..a51c9a59 --- /dev/null +++ b/src/utils/pagination.js @@ -0,0 +1,18 @@ +const fetchAll = async (fetchFn, args) => { + let allResults = [] + let cursor = undefined + const batchArgs = { ...args, count: 100 } + + while (true) { + const result = await fetchFn({ ...batchArgs, cursor }) + + allResults = allResults.concat(result.items) + + if (!result.pageInfo.hasNextPage) break + cursor = result.pageInfo.endCursor + } + + return allResults +} + +export { fetchAll } diff --git a/src/utils/project-config.js b/src/utils/project-config.js new file mode 100644 index 00000000..e74e68aa --- /dev/null +++ b/src/utils/project-config.js @@ -0,0 +1,27 @@ +import { existsSync, readFileSync } from 'fs' +import { resolve, dirname } from 'path' + +const CONFIG_FILENAME = 'calibre.json' + +const findConfigFile = (startDir = process.cwd()) => { + let dir = resolve(startDir) + + while (true) { + const candidate = resolve(dir, CONFIG_FILENAME) + if (existsSync(candidate)) return candidate + + const parent = dirname(dir) + if (parent === dir) return null + dir = parent + } +} + +const loadProjectConfig = (startDir = process.cwd()) => { + const configPath = findConfigFile(startDir) + if (!configPath) return {} + + const content = readFileSync(configPath, 'utf8') + return JSON.parse(content) +} + +export { findConfigFile, loadProjectConfig, CONFIG_FILENAME } diff --git a/src/views/grade-table.js b/src/views/grade-table.js index 54c2a41f..c93dd021 100644 --- a/src/views/grade-table.js +++ b/src/views/grade-table.js @@ -54,7 +54,8 @@ const view = measurements => { return columnify(rows, { columnSplitter: ' | ', - maxLineWidth: '80', + truncate: true, + maxLineWidth: 'auto', config: { score: { align: 'right' diff --git a/src/views/grading.js b/src/views/grading.js index 817c80f9..5042d495 100644 --- a/src/views/grading.js +++ b/src/views/grading.js @@ -1,4 +1,4 @@ -import chalk from 'chalk' +import { styleText } from 'node:util' const formatGrading = grading => { if (!grading) return '—' @@ -6,21 +6,21 @@ const formatGrading = grading => { switch (grading) { case 'good': case 'GOOD': - return chalk.green('Good') + return styleText('green', 'Good') case 'average': case 'needs-improvement': case 'NEEDS_IMPROVEMENT': - return chalk.yellow('Needs Improvement') + return styleText('yellow', 'Needs Improvement') case 'poor': case 'POOR': - return chalk.red('Poor') + return styleText('red', 'Poor') case 'pass': - return chalk.green('Pass') + return styleText('green', 'Pass') case 'fail': - return chalk.red('Fail') + return styleText('red', 'Fail') case 'ungraded': case 'not_assessed': - return chalk.grey('—') + return styleText('gray', '—') default: return grading } @@ -34,14 +34,14 @@ const colorByGrading = (value, grading) => { switch (grading) { case 'good': case 'GOOD': - return chalk.green(display) + return styleText('green', display) case 'average': case 'needs-improvement': case 'NEEDS_IMPROVEMENT': - return chalk.yellow(display) + return styleText('yellow', display) case 'poor': case 'POOR': - return chalk.red(display) + return styleText('red', display) default: return display } diff --git a/src/views/markdown.js b/src/views/markdown.js index 9b583d72..bbb9ffa9 100644 --- a/src/views/markdown.js +++ b/src/views/markdown.js @@ -1,4 +1,4 @@ -import chalk from 'chalk' +import { styleText } from 'node:util' import { marked } from 'marked' import markedTerminal from 'marked-terminal' @@ -10,10 +10,10 @@ const view = markdownReport => { renderer: new markedTerminal({ emoji: true, tab: 2, - paragraph: chalk.reset, - table: chalk.reset, - heading: chalk.bold.hex('#5f5fff'), - href: chalk.hex('#008080'), + paragraph: text => styleText('reset', text), + table: text => styleText('reset', text), + heading: text => styleText(['bold', 'magenta'], text), + href: text => styleText('cyan', text), tableOptions: { style: { head: ['bold'] diff --git a/src/views/pulse-timeline.js b/src/views/pulse-timeline.js index 4c6f96ef..7296124b 100644 --- a/src/views/pulse-timeline.js +++ b/src/views/pulse-timeline.js @@ -1,4 +1,4 @@ -import chalk from 'chalk' +import { styleText } from 'node:util' import percentile from 'stats-percentile' import { format } from '../utils/formatters/index.js' @@ -46,7 +46,7 @@ ${set.name} }) return ` -${chalk.blue.bold(measurement.label)} +${styleText(['bold', 'blue'], measurement.label)} ${ metrics.length ? metrics.join('\n') @@ -56,7 +56,7 @@ ${ }) return ` -${chalk.bold('Metric history')} +${styleText('bold', 'Metric history')} ${timeSeries.length ? 'There is no data for this time period' : ''} ${metrics.join('\n')} ` diff --git a/src/views/snapshot-metrics.js b/src/views/snapshot-metrics.js index 421f612b..2260e02c 100644 --- a/src/views/snapshot-metrics.js +++ b/src/views/snapshot-metrics.js @@ -1,4 +1,4 @@ -import chalk from 'chalk' +import { styleText } from 'node:util' import { format as dateFormat } from 'date-fns' import gradeTable from '../views/grade-table.js' @@ -7,7 +7,7 @@ const testTable = ({ test, testProfiles }) => { const testProfile = testProfiles.find(tp => tp.uuid == test.testProfile.uuid) return ` -${chalk.bold(test.page.name)} ${chalk.grey(`(${test.page.url})`)} +${styleText('bold', test.page.name)} ${styleText('gray', `(${test.page.url})`)} Tested with: ${testProfile.name} ${gradeTable(test.measurements)} @@ -16,8 +16,8 @@ ${gradeTable(test.measurements)} const view = ({ snapshot, testProfiles }) => { let intro = [ - chalk.bold(`Snapshot #${snapshot.iid}`), - chalk.grey(dateFormat(new Date(snapshot.createdAt), 'h:mma d-MMM-yyyy')) + styleText('bold', `Snapshot #${snapshot.iid}`), + styleText('gray', dateFormat(new Date(snapshot.createdAt), 'h:mma d-MMM-yyyy')) ] const view = snapshot.tests.map(test => testTable({ test, testProfiles })) @@ -27,11 +27,11 @@ ${intro.join('\n')} ${view.join('\n\n')} -${chalk.bold(`View the full report ${snapshot.htmlUrl}`)} +${styleText('bold', `View the full report ${snapshot.htmlUrl}`)} -To view all captured metrics of this test, re-run this command with the ${chalk.cyan( +To view all captured metrics of this test, re-run this command with the ${styleText('cyan', '--json' - )} or ${chalk.cyan('--csv')} flag. + )} or ${styleText('cyan', '--csv')} flag. ` } diff --git a/src/views/test-profile.js b/src/views/test-profile.js index 6197bd88..401b17ad 100644 --- a/src/views/test-profile.js +++ b/src/views/test-profile.js @@ -1,4 +1,4 @@ -import chalk from 'chalk' +import { styleText } from 'node:util' import { format as dateFormat } from 'date-fns' const cookies = cookies => { @@ -28,11 +28,11 @@ const cookies = cookies => { const view = profile => { return ` -${chalk.bold.cyan(`⬢ ${profile.name}`)} (${profile.uuid}) -${chalk.grey( +${styleText(['bold', 'cyan'], `⬢ ${profile.name}`)} (${profile.uuid}) +${styleText('gray', `Created: ${dateFormat(new Date(profile.createdAt), 'h:mma d-MMM-yyyy')}` )} -${chalk.grey( +${styleText('gray', `Updated: ${dateFormat(new Date(profile.updatedAt), 'h:mma d-MMM-yyyy')}` )}