From 74ed94d5c84485af28813edcbcd0296741d85f6a Mon Sep 17 00:00:00 2001 From: Rui Martins Date: Tue, 12 May 2026 14:53:14 +0100 Subject: [PATCH 1/2] chore: merge main into next (#331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Brings the `next` prerelease branch up to date with `main` (1.63.0). `next` had been pinned at 1.60.3 with the `td goal` work and the `@doist/todoist-sdk` next-channel pin (`10.2.0-next.1`); meanwhile main shipped the `@doist/cli-core` integration, the auth refactor, project `--parent`/`reorder`, task `--no-labels`, and a handful of dep bumps. Once this merges, the release workflow will publish a new `…-next.N` prerelease to npm under the `next` dist-tag. ## Conflict resolutions - `package.json` — kept `@doist/todoist-sdk` at `10.2.0-next.1` (still the latest on the `next` dist-tag); took main's `@napi-rs/keyring 1.3.0` and new `@doist/cli-core 0.9.0`. - `package-lock.json` — regenerated against the resolved `package.json`. - `src/lib/oauth.test.ts` — accepted main's deletion. The whole OAuth module (`oauth.ts`, `pkce.ts`, `oauth-server.test.ts`) was replaced by `@doist/cli-core/auth` on main. The only next-only change to this file was a regex relaxation (commit 9d86c4c) — see below. - `src/lib/migrate-auth.test.ts` — relaxed the `doist-version` regex to allow the prerelease suffix (`1.x.y-next.z`), matching the fix already on `next` in `oauth.test.ts` and `usage-tracking.test.ts`. Without this, `migrate-auth.test.ts` would fail under the next-channel publish. ## Test plan - [x] `npm run type-check` clean - [x] `npm run check` (oxlint + oxfmt) clean - [x] `npm test` — 1587/1587 pass - [ ] Release workflow publishes a `…-next.N` prerelease after merge --------- Co-authored-by: Scott Lovegrove Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: doist-release-bot[bot] <272237451+doist-release-bot[bot]@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Miroslav Holec --- AGENTS.md | 2 +- CHANGELOG.md | 42 ++ CODEBASE.md | 42 +- README.md | 1 + package-lock.json | 589 ++++++++++++---------- package.json | 16 +- skills/todoist-cli/SKILL.md | 29 +- src/commands/apps/apps.test.ts | 20 +- src/commands/apps/list.ts | 11 +- src/commands/auth/auth.test.ts | 272 ++++------ src/commands/auth/index.ts | 39 +- src/commands/auth/login.test.ts | 145 ++++++ src/commands/auth/login.ts | 138 +++-- src/commands/auth/token-view.ts | 18 + src/commands/changelog.test.ts | 142 +----- src/commands/changelog.ts | 108 +--- src/commands/config/config.test.ts | 2 +- src/commands/config/view.ts | 8 +- src/commands/doctor.test.ts | 2 +- src/commands/doctor.ts | 19 +- src/commands/folder/index.ts | 11 +- src/commands/hc/hc.test.ts | 50 ++ src/commands/hc/index.ts | 1 + src/commands/hc/search.ts | 13 +- src/commands/project/create.ts | 14 +- src/commands/project/helpers.ts | 108 +++- src/commands/project/index.ts | 28 + src/commands/project/project.test.ts | 234 --------- src/commands/project/reorder.test.ts | 304 +++++++++++ src/commands/project/reorder.ts | 117 +++++ src/commands/project/update.test.ts | 459 +++++++++++++++++ src/commands/project/update.ts | 70 ++- src/commands/skill/skill.test.ts | 9 + src/commands/task/index.ts | 1 + src/commands/task/task.test.ts | 54 ++ src/commands/task/update.ts | 10 +- src/commands/update/action.ts | 151 ------ src/commands/update/index.ts | 28 +- src/commands/update/switch.ts | 42 -- src/commands/update/update.test.ts | 509 ++----------------- src/commands/user/index.ts | 1 + src/commands/user/list.ts | 44 +- src/commands/user/user.test.ts | 37 +- src/index.ts | 4 +- src/lib/api/projects-sync.ts | 18 + src/lib/{oauth-server.ts => auth-html.ts} | 129 +---- src/lib/auth-provider.test.ts | 57 +++ src/lib/auth-provider.ts | 64 +++ src/lib/auth-store.test.ts | 183 +++++++ src/lib/auth-store.ts | 143 ++++++ src/lib/auth.test.ts | 15 + src/lib/auth.ts | 14 +- src/lib/config.test.ts | 73 +++ src/lib/config.ts | 116 ++--- src/lib/errors.ts | 32 +- src/lib/global-args.ts | 236 ++++----- src/lib/markdown.ts | 23 +- src/lib/migrate-auth.test.ts | 2 +- src/lib/oauth-scopes.ts | 35 ++ src/lib/oauth-server.test.ts | 66 --- src/lib/oauth.test.ts | 101 ---- src/lib/oauth.ts | 75 --- src/lib/options.ts | 6 +- src/lib/order.ts | 11 + src/lib/output.test.ts | 26 + src/lib/output.ts | 62 +-- src/lib/pkce.test.ts | 59 --- src/lib/pkce.ts | 22 - src/lib/skills/content.ts | 27 +- src/lib/skills/index.ts | 5 + src/lib/spinner.test.ts | 375 ++------------ src/lib/spinner.ts | 144 +----- src/lib/update.ts | 62 +-- src/types/marked-terminal-renderer.d.ts | 16 - vitest.config.ts | 9 + 75 files changed, 3133 insertions(+), 3017 deletions(-) create mode 100644 src/commands/auth/login.test.ts create mode 100644 src/commands/auth/token-view.ts create mode 100644 src/commands/project/reorder.test.ts create mode 100644 src/commands/project/reorder.ts create mode 100644 src/commands/project/update.test.ts delete mode 100644 src/commands/update/action.ts delete mode 100644 src/commands/update/switch.ts create mode 100644 src/lib/api/projects-sync.ts rename src/lib/{oauth-server.ts => auth-html.ts} (92%) create mode 100644 src/lib/auth-provider.test.ts create mode 100644 src/lib/auth-provider.ts create mode 100644 src/lib/auth-store.test.ts create mode 100644 src/lib/auth-store.ts create mode 100644 src/lib/config.test.ts delete mode 100644 src/lib/oauth-server.test.ts delete mode 100644 src/lib/oauth.test.ts delete mode 100644 src/lib/oauth.ts create mode 100644 src/lib/order.ts delete mode 100644 src/lib/pkce.test.ts delete mode 100644 src/lib/pkce.ts delete mode 100644 src/types/marked-terminal-renderer.d.ts diff --git a/AGENTS.md b/AGENTS.md index ded2e581..1f8d57c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,7 @@ Use this to verify changes work before committing. - **Implicit `view` subcommand edge case:** `td project ` defaults to `td project view `. If a project/task name matches a subcommand name (e.g., `"list"`), the subcommand wins — users must use `td project view list`. - **API response shape:** client returns `{ results: T[], nextCursor? }` — always destructure. - **Priority mapping:** CLI uses `"p1"`–`"p4"` strings; API uses 4=p1 (highest), 1=p4 (lowest). Use `parsePriority` from `src/lib/task-list.ts`, never hand-roll. -- **Errors:** throw `CliError(code, message, hints?)` from `src/lib/errors.ts` for anything user-facing. The global `parseAsync().catch` in `src/index.ts` renders it correctly in JSON and pretty modes. +- **Errors:** throw `CliError(code, message, hints?)` from `src/lib/errors.ts` for anything user-facing. The global `parseAsync().catch` in `src/index.ts` renders it correctly in JSON and pretty modes. The same handler also matches `BaseCliError` (re-exported from `src/lib/errors.ts`) so errors thrown by `@doist/cli-core` helpers route through the same formatter. ## Testing diff --git a/CHANGELOG.md b/CHANGELOG.md index daddd01a..5017441b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +## [1.63.0](https://github.com/Doist/todoist-cli/compare/v1.62.2...v1.63.0) (2026-05-12) + +### Features + +* **project:** add `--parent` on update and new `reorder` command ([#330](https://github.com/Doist/todoist-cli/issues/330)) ([f32f56a](https://github.com/Doist/todoist-cli/commit/f32f56adefee35943cd9fc43b592b6ad75591240)) + +## [1.62.2](https://github.com/Doist/todoist-cli/compare/v1.62.1...v1.62.2) (2026-05-11) + +### Bug Fixes + +* **task:** add --no-labels flag to clear labels on update ([#327](https://github.com/Doist/todoist-cli/issues/327)) ([20d13af](https://github.com/Doist/todoist-cli/commit/20d13afac05970b98551cbf802ee47473e6d15b8)), closes [#326](https://github.com/Doist/todoist-cli/issues/326) + +## [1.62.1](https://github.com/Doist/todoist-cli/compare/v1.62.0...v1.62.1) (2026-05-09) + +### Bug Fixes + +* **deps:** bump @doist/todoist-sdk to 10.1.3 for Node 26 gzip fix ([#320](https://github.com/Doist/todoist-cli/issues/320)) ([022d634](https://github.com/Doist/todoist-cli/commit/022d634df37d2aa22e8c6f78c2450c7f7dfedf92)) + +## [1.62.0](https://github.com/Doist/todoist-cli/compare/v1.61.2...v1.62.0) (2026-05-07) + +### Features + +* GitHub Copilot Skill Installation Instructions ([#315](https://github.com/Doist/todoist-cli/issues/315)) ([8704f4b](https://github.com/Doist/todoist-cli/commit/8704f4b61e3653f7cb28120497799a55a5a9fd57)) + +## [1.61.2](https://github.com/Doist/todoist-cli/compare/v1.61.1...v1.61.2) (2026-05-06) + +### Bug Fixes + +* **deps:** update dependency marked to v18.0.3 ([#306](https://github.com/Doist/todoist-cli/issues/306)) ([63d75a3](https://github.com/Doist/todoist-cli/commit/63d75a340dedc308b291fe35e90f5273ae0f730f)) + +## [1.61.1](https://github.com/Doist/todoist-cli/compare/v1.61.0...v1.61.1) (2026-05-05) + +### Bug Fixes + +* **deps:** update dependency @napi-rs/keyring to v1.3.0 ([#309](https://github.com/Doist/todoist-cli/issues/309)) ([1c264eb](https://github.com/Doist/todoist-cli/commit/1c264eb5b97cdc59b9d9f00e106aad0d0fd63058)) + +## [1.61.0](https://github.com/Doist/todoist-cli/compare/v1.60.3...v1.61.0) (2026-05-05) + +### Features + +* **auth:** add `td auth token view` for script and agent use ([#311](https://github.com/Doist/todoist-cli/issues/311)) ([fa6d6b6](https://github.com/Doist/todoist-cli/commit/fa6d6b6f53c549d3ee92058aceccee4b299a86f2)) + ## [1.60.3](https://github.com/Doist/todoist-cli/compare/v1.60.2...v1.60.3) (2026-05-02) ### Bug Fixes diff --git a/CODEBASE.md b/CODEBASE.md index 9244855a..b1e546ab 100644 --- a/CODEBASE.md +++ b/CODEBASE.md @@ -52,11 +52,9 @@ src/ │ ├─ api/ # SDK wrapper + typed helpers (core, filters, workspaces, │ │ # notifications, reminders, stats, user-settings, uploads) │ └─ skills/ # content.ts (SKILL_CONTENT), create-installer.ts -├─ test-support/ -│ ├─ mock-api.ts # createMockApi() — vitest mocks of every SDK method -│ └─ fixtures.ts # Sample task/project/label/section fixtures -└─ types/ - └─ marked-terminal-renderer.d.ts # Type declarations for marked-terminal-renderer +└─ test-support/ + ├─ mock-api.ts # createMockApi() — vitest mocks of every SDK method + └─ fixtures.ts # Sample task/project/label/section fixtures ``` ## Architecture flow @@ -126,7 +124,18 @@ New subcommand? Copy a sibling in the target group, wire it in that group's - **`config.ts`** — `~/.config/todoist-cli/config.json` read/write, `AuthMode`, `UpdateChannel`, `AUTH_FLAG_ORDER` - **`secure-store.ts`** — `@napi-rs/keyring` wrapper (OS credential manager) -- **`oauth-server.ts` / `oauth.ts` / `oauth-scopes.ts` / `pkce.ts`** — OAuth flow +- **`auth-provider.ts`** — `createTodoistAuthProvider()`: `@doist/cli-core` + PKCE `AuthProvider` adapter with a Todoist-specific `validateToken` + (calls `getUser`, builds `auth_mode` / `auth_scope` / `auth_flags`) +- **`auth-store.ts`** — `createTodoistTokenStore()`: cli-core + `TokenStore` adapter over `auth.ts`'s multi-user primitives. + Also exports `toTodoistAccount` / `accountToUpsertInput` mappers (shared + account shape) and `getLastStorageResult()` for surfacing keyring-fallback + warnings after `set()`. +- **`auth-html.ts`** — branded HTML pages for the cli-core OAuth callback + (`renderAuthSuccessPage` / `renderAuthErrorPage`) +- **`oauth-scopes.ts`** — opt-in OAuth scope registry, `parseScopesOption`, + `extractAdditionalScopes`, `resolveAuthScope`, `formatScopesHelp` - **`output.ts`** — `formatTaskRow`, `formatTaskView`, `formatJson`, `formatNdjson`, `formatPaginatedJson`, `formatDueDate`, `formatPriority`, `formatError`, `formatErrorJson`, `printDryRun` @@ -199,9 +208,14 @@ Token lookup order (see `src/lib/auth.ts` — `getApiToken()` / `probeApiToken() into secure-store on first read when present 3. OS credential manager via `src/lib/secure-store.ts` -`td auth login` runs a full OAuth PKCE flow (`src/lib/oauth-server.ts`, -`DEFAULT_PORT = 8765` with a small fallback range, browser launch). Scopes -are opt-in: `--read-only` for a read-only token, +`td auth login` runs through `@doist/cli-core`'s OAuth runtime +(`attachLoginCommand` → `runOAuthFlow`). The Todoist-local pieces live in +`src/lib/auth-provider.ts` (PKCE provider + `validateToken`) and +`src/lib/auth-store.ts` (multi-user `TokenStore` adapter); the command is +attached in `src/commands/auth/login.ts`. cli-core wires the standard flags +(`--read-only`, `--callback-port`, `--json`, `--ndjson`) and binds the local +callback server on port `8765` with a small fallback range. Scopes are +opt-in: `--read-only` for a read-only token, `--additional-scopes=app-management,backups` to broaden. ## Testing @@ -212,6 +226,11 @@ are opt-in: `--read-only` for a read-only token, vitest-mocked versions of every SDK method. Use factories from `src/test-support/fixtures.ts` — do NOT hand-build mock entities. - **Pattern:** mock `getApi` via `vi.mock`, then `program.parseAsync(['node','td','',…])`. +- **`@doist/cli-core` inlining:** `vitest.config.ts` lists `@doist/cli-core` in + `server.deps.inline` so `vi.doMock('node:fs/promises', …)` / + `vi.doMock('node:os', …)` reach cli-core's compiled imports. Without it + vitest treats the package as external and Node's native resolver bypasses + the mock substitution, breaking the `auth` / `migrate-auth` suites. ## Build & release @@ -263,7 +282,10 @@ file, or a token stored in the OS credential manager via `td auth login`. - Mutating commands (`add`/`create`/`update`): always support `--json` emitting `formatJson(result, entityType)` — see AGENTS.md - User-facing errors: throw `CliError(code, message, hints?)` from - `src/lib/errors.ts`; global `parseAsync().catch` in `src/index.ts` renders it + `src/lib/errors.ts`; the global `parseAsync().catch` in `src/index.ts` + renders it. The same handler also catches `BaseCliError` (re-exported + from `src/lib/errors.ts`) so errors thrown by `@doist/cli-core` helpers + route through the same path - Global flags handled in `src/lib/global-args.ts` — check `isJsonMode()` etc. before printing diff --git a/README.md b/README.md index 98c0a9f5..0d123d84 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Install skills for your coding agent: ```bash td skill install claude-code td skill install codex +td skill install copilot td skill install cursor td skill install gemini td skill install pi diff --git a/package-lock.json b/package-lock.json index c8dda9c9..de0143eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,25 @@ { "name": "@doist/todoist-cli", - "version": "1.60.3", + "version": "1.63.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@doist/todoist-cli", - "version": "1.60.3", + "version": "1.63.0", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@doist/todoist-sdk": "10.2.0-next.1", - "@napi-rs/keyring": "1.2.0", + "@doist/cli-core": "0.9.0", + "@doist/todoist-sdk": "10.2.0-next.2", + "@napi-rs/keyring": "1.3.0", "@pnpm/tabtab": "0.5.4", "chalk": "5.6.2", "commander": "14.0.3", "date-fns": "4.1.0", - "marked": "18.0.2", + "marked": "18.0.3", "marked-terminal-renderer": "2.2.0", - "open": "11.0.0", - "yocto-spinner": "1.1.0" + "open": "11.0.0" }, "bin": { "td": "dist/index.js" @@ -31,8 +31,8 @@ "@types/node": "25.6.0", "conventional-changelog-conventionalcommits": "9.3.1", "lefthook": "2.1.6", - "oxfmt": "0.47.0", - "oxlint": "1.62.0", + "oxfmt": "0.48.0", + "oxlint": "1.63.0", "semantic-release": "25.0.3", "typescript": "6.0.3", "vitest": "4.1.5" @@ -135,10 +135,47 @@ "node": ">=0.1.90" } }, + "node_modules/@doist/cli-core": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@doist/cli-core/-/cli-core-0.9.0.tgz", + "integrity": "sha512-38uTt+DSFCMuuPkdWIl9Dm7XrjGlwG15eI0DrnaKEYm+AGizzP6tY7m1AZAT5aQXAJOCU6uYXAvqaqpni+PcLg==", + "license": "MIT", + "dependencies": { + "chalk": "5.6.2", + "yocto-spinner": "1.1.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "peerDependencies": { + "commander": ">=14", + "marked": ">=18", + "marked-terminal-renderer": ">=2", + "open": ">=10", + "vitest": ">=4.1" + }, + "peerDependenciesMeta": { + "commander": { + "optional": true + }, + "marked": { + "optional": true + }, + "marked-terminal-renderer": { + "optional": true + }, + "open": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, "node_modules/@doist/todoist-sdk": { - "version": "10.2.0-next.1", - "resolved": "https://registry.npmjs.org/@doist/todoist-sdk/-/todoist-sdk-10.2.0-next.1.tgz", - "integrity": "sha512-E1DOmoYFd6tkFOsxsFhcaNd+eO2DUvaJarMmX4keeh+ltNvLb7NqPBR1cT4OVQqqw9JMDv3FumCXoQuOXgFgPw==", + "version": "10.2.0-next.2", + "resolved": "https://registry.npmjs.org/@doist/todoist-sdk/-/todoist-sdk-10.2.0-next.2.tgz", + "integrity": "sha512-DuyAeu8tNe8X9xj11aPPkVKcjPKT/1OMR7aeFn+wvqI97nmd8mffObKse1qlAHwQS2/f+Z02gS6L9hWJYjUmWw==", "license": "MIT", "dependencies": { "camelcase": "6.3.0", @@ -146,7 +183,7 @@ "form-data": "4.0.5", "ts-custom-error": "^3.2.0", "undici": "^7.16.0", - "uuid": "11.1.0", + "uuid": "11.1.1", "zod": "4.3.6" }, "engines": { @@ -762,7 +799,7 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@keyv/serialize": { @@ -772,9 +809,9 @@ "license": "MIT" }, "node_modules/@napi-rs/keyring": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.2.0.tgz", - "integrity": "sha512-d0d4Oyxm+v980PEq1ZH2PmS6cvpMIRc17eYpiU47KgW+lzxklMu6+HOEOPmxrpnF/XQZ0+Q78I2mgMhbIIo/dg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring/-/keyring-1.3.0.tgz", + "integrity": "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA==", "license": "MIT", "engines": { "node": ">= 10" @@ -784,24 +821,24 @@ "url": "https://github.com/sponsors/Brooooooklyn" }, "optionalDependencies": { - "@napi-rs/keyring-darwin-arm64": "1.2.0", - "@napi-rs/keyring-darwin-x64": "1.2.0", - "@napi-rs/keyring-freebsd-x64": "1.2.0", - "@napi-rs/keyring-linux-arm-gnueabihf": "1.2.0", - "@napi-rs/keyring-linux-arm64-gnu": "1.2.0", - "@napi-rs/keyring-linux-arm64-musl": "1.2.0", - "@napi-rs/keyring-linux-riscv64-gnu": "1.2.0", - "@napi-rs/keyring-linux-x64-gnu": "1.2.0", - "@napi-rs/keyring-linux-x64-musl": "1.2.0", - "@napi-rs/keyring-win32-arm64-msvc": "1.2.0", - "@napi-rs/keyring-win32-ia32-msvc": "1.2.0", - "@napi-rs/keyring-win32-x64-msvc": "1.2.0" + "@napi-rs/keyring-darwin-arm64": "1.3.0", + "@napi-rs/keyring-darwin-x64": "1.3.0", + "@napi-rs/keyring-freebsd-x64": "1.3.0", + "@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0", + "@napi-rs/keyring-linux-arm64-gnu": "1.3.0", + "@napi-rs/keyring-linux-arm64-musl": "1.3.0", + "@napi-rs/keyring-linux-riscv64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-gnu": "1.3.0", + "@napi-rs/keyring-linux-x64-musl": "1.3.0", + "@napi-rs/keyring-win32-arm64-msvc": "1.3.0", + "@napi-rs/keyring-win32-ia32-msvc": "1.3.0", + "@napi-rs/keyring-win32-x64-msvc": "1.3.0" } }, "node_modules/@napi-rs/keyring-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-CA83rDeyONDADO25JLZsh3eHY8yTEtm/RS6ecPsY+1v+dSawzT9GywBMu2r6uOp1IEhQs/xAfxgybGAFr17lSA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-arm64/-/keyring-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ==", "cpu": [ "arm64" ], @@ -815,9 +852,9 @@ } }, "node_modules/@napi-rs/keyring-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.2.0.tgz", - "integrity": "sha512-dBHjtKRCj4ByfnfqIKIJLo3wueQNJhLRyuxtX/rR4K/XtcS7VLlRD01XXizjpre54vpmObj63w+ZpHG+mGM8uA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-darwin-x64/-/keyring-darwin-x64-1.3.0.tgz", + "integrity": "sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw==", "cpu": [ "x64" ], @@ -831,9 +868,9 @@ } }, "node_modules/@napi-rs/keyring-freebsd-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.2.0.tgz", - "integrity": "sha512-DPZFr11pNJSnaoh0dzSUNF+T6ORhy3CkzUT3uGixbA71cAOPJ24iG8e8QrLOkuC/StWrAku3gBnth2XMWOcR3Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-freebsd-x64/-/keyring-freebsd-x64-1.3.0.tgz", + "integrity": "sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw==", "cpu": [ "x64" ], @@ -847,9 +884,9 @@ } }, "node_modules/@napi-rs/keyring-linux-arm-gnueabihf": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.2.0.tgz", - "integrity": "sha512-8xv6DyEMlvRdqJzp4F39RLUmmTQsLcGYYv/3eIfZNZN1O5257tHxTrFYqAsny659rJJK2EKeSa7PhrSibQqRWQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm-gnueabihf/-/keyring-linux-arm-gnueabihf-1.3.0.tgz", + "integrity": "sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ==", "cpu": [ "arm" ], @@ -863,9 +900,9 @@ } }, "node_modules/@napi-rs/keyring-linux-arm64-gnu": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.2.0.tgz", - "integrity": "sha512-Pu2V6Py+PBt7inryEecirl+t+ti8bhZphjP+W68iVaXHUxLdWmkgL9KI1VkbRHbx5k8K5Tew9OP218YfmVguIA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-gnu/-/keyring-linux-arm64-gnu-1.3.0.tgz", + "integrity": "sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg==", "cpu": [ "arm64" ], @@ -879,9 +916,9 @@ } }, "node_modules/@napi-rs/keyring-linux-arm64-musl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.2.0.tgz", - "integrity": "sha512-8TDymrpC4P1a9iDEaegT7RnrkmrJN5eNZh3Im3UEV5PPYGtrb82CRxsuFohthCWQW81O483u1bu+25+XA4nKUw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-arm64-musl/-/keyring-linux-arm64-musl-1.3.0.tgz", + "integrity": "sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw==", "cpu": [ "arm64" ], @@ -895,9 +932,9 @@ } }, "node_modules/@napi-rs/keyring-linux-riscv64-gnu": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.2.0.tgz", - "integrity": "sha512-awsB5XI1MYL7fwfjMDGmKOWvNgJEO7mM7iVEMS0fO39f0kVJnOSjlu7RHcXAF0LOx+0VfF3oxbWqJmZbvRCRHw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-riscv64-gnu/-/keyring-linux-riscv64-gnu-1.3.0.tgz", + "integrity": "sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ==", "cpu": [ "riscv64" ], @@ -911,9 +948,9 @@ } }, "node_modules/@napi-rs/keyring-linux-x64-gnu": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.2.0.tgz", - "integrity": "sha512-8E+7z4tbxSJXxIBqA+vfB1CGajpCDRyTyqXkBig5NtASrv4YXcntSo96Iah2QDR5zD3dSTsmbqJudcj9rKKuHQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-gnu/-/keyring-linux-x64-gnu-1.3.0.tgz", + "integrity": "sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A==", "cpu": [ "x64" ], @@ -927,9 +964,9 @@ } }, "node_modules/@napi-rs/keyring-linux-x64-musl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.2.0.tgz", - "integrity": "sha512-8RZ8yVEnmWr/3BxKgBSzmgntI7lNEsY7xouNfOsQkuVAiCNmxzJwETspzK3PQ2FHtDxgz5vHQDEBVGMyM4hUHA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-linux-x64-musl/-/keyring-linux-x64-musl-1.3.0.tgz", + "integrity": "sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw==", "cpu": [ "x64" ], @@ -943,9 +980,9 @@ } }, "node_modules/@napi-rs/keyring-win32-arm64-msvc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.2.0.tgz", - "integrity": "sha512-AoqaDZpQ6KPE19VBLpxyORcp+yWmHI9Xs9Oo0PJ4mfHma4nFSLVdhAubJCxdlNptHe5va7ghGCHj3L9Akiv4cQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-arm64-msvc/-/keyring-win32-arm64-msvc-1.3.0.tgz", + "integrity": "sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw==", "cpu": [ "arm64" ], @@ -959,9 +996,9 @@ } }, "node_modules/@napi-rs/keyring-win32-ia32-msvc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.2.0.tgz", - "integrity": "sha512-EYL+EEI6bCsYi3LfwcQdnX3P/R76ENKNn+3PmpGheBsUFLuh0gQuP7aMVHM4rTw6UVe+L3vCLZSptq/oeacz0A==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-ia32-msvc/-/keyring-win32-ia32-msvc-1.3.0.tgz", + "integrity": "sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ==", "cpu": [ "ia32" ], @@ -975,9 +1012,9 @@ } }, "node_modules/@napi-rs/keyring-win32-x64-msvc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.2.0.tgz", - "integrity": "sha512-xFlx/TsmqmCwNU9v+AVnEJgoEAlBYgzFF5Ihz1rMpPAt4qQWWkMd4sCyM1gMJ1A/GnRqRegDiQpwaxGUHFtFbA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@napi-rs/keyring-win32-x64-msvc/-/keyring-win32-x64-msvc-1.3.0.tgz", + "integrity": "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg==", "cpu": [ "x64" ], @@ -1170,16 +1207,16 @@ "version": "0.124.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@oxfmt/binding-android-arm-eabi": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.47.0.tgz", - "integrity": "sha512-KrMQRdMi/upr81qT4ijK6X6BNp6jqpMY7FwILQnwIy9QLc3qpnhUx5rsCLGzn4ewsCQ0CNAspN2ogmP1GXLyLw==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.48.0.tgz", + "integrity": "sha512-uwqk+/KhQvBIpULD8SMM/zAafMRC/+DV/xsEQjkkIsJ/kLmEI/2bxonVowcYTiXqqZ/a0FEW8DPkZY3VvwELDA==", "cpu": [ "arm" ], @@ -1194,9 +1231,9 @@ } }, "node_modules/@oxfmt/binding-android-arm64": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.47.0.tgz", - "integrity": "sha512-r4ixS/PeUpAFKgrpDoZ5pSkthjZzVzKd95525Aazj+aOv9H4ulK5zYHGb7wFY5n5kZxHK8TbOJUZgoEb1ohddQ==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.48.0.tgz", + "integrity": "sha512-VUCiKuXK5+McVssgHEJdrcGK7hRJzrRb36zm9/jwzMholyYt4BgXhw5Nm1V1DX6Ce717Zi/1jk432b/tgmQgtQ==", "cpu": [ "arm64" ], @@ -1211,9 +1248,9 @@ } }, "node_modules/@oxfmt/binding-darwin-arm64": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.47.0.tgz", - "integrity": "sha512-CLWxiKpMl+195cm09CuaWEhJK0CirRkoMa07aR9+9AFPat2LfIKtwx1JqxZM0MTvcMe6+adlJNdVL6jdInvq3g==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.48.0.tgz", + "integrity": "sha512-IkKp8rnIyQLW6Jt+6jragCbUVYSayk55lapiprLjIVvt4NczLyO/nwX2GgefLQ5iaBdfS8UEAFgCs/pLO6Cl0w==", "cpu": [ "arm64" ], @@ -1228,9 +1265,9 @@ } }, "node_modules/@oxfmt/binding-darwin-x64": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.47.0.tgz", - "integrity": "sha512-Xq5fjTYDC50faUeLSm0rZdBqoTgleXEdD7NpJdARtQIczkCJn3xNjMUSQQkUmh4CtxkKTNL68lytcOK3e/osgg==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.48.0.tgz", + "integrity": "sha512-+aFuhsGIuvnoOjXyKVHMhPKJZR1kQkAl8QyrKoMlA7yJsSTC3N0Asl53La8TChSHhW8epToQ/Q0nvLmEmfNmLg==", "cpu": [ "x64" ], @@ -1245,9 +1282,9 @@ } }, "node_modules/@oxfmt/binding-freebsd-x64": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.47.0.tgz", - "integrity": "sha512-QOU9ZIJ52p5askcEC0QJvvr8trHAWoonul8bgISo6gYUL3s50zkqafBYcNAr9LJZQbsZtPfIWHk9+5+nUp1qJQ==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.48.0.tgz", + "integrity": "sha512-fbqzQL8FjI9gGnktI7RIo0dksDziTAYBy7xlI7jU7eID5fxLF/25fS4Xj6GydD8Y5oWHL83U4NK160QaOAxtyg==", "cpu": [ "x64" ], @@ -1262,9 +1299,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.47.0.tgz", - "integrity": "sha512-oJxDM1aBhPvz9gmElBv8UpxyiqhwfjcbrSxT5F0xtuUzY6dQI27/AQPIt3eu3Z5Yvn0kQl5R7MA3Z+MbnRvCBw==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.48.0.tgz", + "integrity": "sha512-hn4i0zhAyTiB3ZHjQfYUZkDvrbVkohw1S7pySWxWUoZ87HnkDoTFThj7QTxk40hNPOTUP0vHbPRNamFIv1HBJQ==", "cpu": [ "arm" ], @@ -1279,9 +1316,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm-musleabihf": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.47.0.tgz", - "integrity": "sha512-g8Lh50VS4ibGz2q6v7r9UZY4D0dM16SdrFYOMzhqIoCwGcai8VMIRUAcqn1/jlCsOOzUXJ741+kCeJt0cofakQ==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.48.0.tgz", + "integrity": "sha512-R4WBD9qF3QM9hqgdAa+fBGXmquTvDUujrPQ36t2Sjk8RPOSKGHDeN7l/khr10hqbQaOq9KCgPHG9ubNET/X/RQ==", "cpu": [ "arm" ], @@ -1296,9 +1333,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-gnu": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.47.0.tgz", - "integrity": "sha512-YrNT1vQ0asaXoRbrvYENPqmBfOQ9Xr8enPNOULeYfg44VjCcrUowFy5QZr+WawE0zyP8cH9e9Gxxg0fDEFzhcg==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.48.0.tgz", + "integrity": "sha512-5bVdwSwlm1M8wbYCorLOxWxUBw/8tBvHYyQNIfwWVPwOJaj5vg1APSGJQVpwJfV5VNE9PSrR91UKEpoNwHhqUA==", "cpu": [ "arm64" ], @@ -1313,9 +1350,9 @@ } }, "node_modules/@oxfmt/binding-linux-arm64-musl": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.47.0.tgz", - "integrity": "sha512-IxtQC/sbBi4ubbY+MdwdanRWrG9InQJVZqyMsBa5IUaQcnSg86gQme574HxXMC1p4bo4YhV99zQ+wNnGCvEgzw==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.48.0.tgz", + "integrity": "sha512-vCS3Fk7gFslTqE1lUE2IlroyVV7u/9SmMA/uBqDoshuck2psGWcjW0ePyPZI3rM3+qtf2pDaMVIKMHozraifuw==", "cpu": [ "arm64" ], @@ -1330,9 +1367,9 @@ } }, "node_modules/@oxfmt/binding-linux-ppc64-gnu": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.47.0.tgz", - "integrity": "sha512-EWXEhOMbWO0q6eJSbu0QLkU8cKi0ljlYLngeDs2Ocu/pm1rrLwyQiYzlFbdnMRURI4w9ndr1sI9rSbhlJ5o23Q==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.48.0.tgz", + "integrity": "sha512-gKtfFfueUClXDumyoHUbymqRf7prHejOOyzJK0eIJn93GF9JBdFHdo60TM1ZBHxkEwZvjuOgHmKtneKbEOc/Eg==", "cpu": [ "ppc64" ], @@ -1347,9 +1384,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-gnu": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.47.0.tgz", - "integrity": "sha512-tZrjS11TUiDuEpRaqdk8K9F9xETRyKXfuZKmdeW+Gj7coBnm7+8sBEfyt033EAFEQSlkniAXvBLh+Qja2ioGBQ==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.48.0.tgz", + "integrity": "sha512-SYt0UhOvZD/UwZz9sXq6J2uAw8o24f5VZpLB2DH01f6MevshmlgakQlZe2lwek2sZJkd07eLu7mZa0g7yeiw7Q==", "cpu": [ "riscv64" ], @@ -1364,9 +1401,9 @@ } }, "node_modules/@oxfmt/binding-linux-riscv64-musl": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.47.0.tgz", - "integrity": "sha512-KBFy+2CFKUCZzYwX2ZOPQKck1vjQbz+hextuc19G4r0WRJwadfAeuQMQRQvB+Ivc8brlbOVg7et8K7E467440g==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.48.0.tgz", + "integrity": "sha512-JLbrwck2AopG4ud/XklZO5N+qxGC7cS7ROvXZVNfx0MCLDDL2kGOLvzuWORkVjnjAM0CMAfIMU2zNBtQbM+4dw==", "cpu": [ "riscv64" ], @@ -1381,9 +1418,9 @@ } }, "node_modules/@oxfmt/binding-linux-s390x-gnu": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.47.0.tgz", - "integrity": "sha512-REUPFKVGSiK99B+9eaPhluEVglzaoj/SMykNC5SUiV2RSsBfV5lWN7Y0iCIc251Wz3GaeAGZsJ/zj3gjarxdFg==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.48.0.tgz", + "integrity": "sha512-mdxt5L8OQLxkQH+JVpdC/lknZNe0lX4hlO3d8+xvw2wToo+iDrid9tiGOd5bmHfUVd5wVhrUry0qlu5vq66NkQ==", "cpu": [ "s390x" ], @@ -1398,9 +1435,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-gnu": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.47.0.tgz", - "integrity": "sha512-KVftVSVEDeIfRW3TIeLe3aNI/iY4m1fu5mDwHcisKMZSCMKLkrhFsjowC7o9RoqNPxbbglm2+/6KAKBIts2t0Q==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.48.0.tgz", + "integrity": "sha512-oEz1BQwMrV7OMEFx/3VPDU3n9TM0AnxpktDYXjEg5i6nTX87wo18wSfBvkl4tzAICdKtoAQAdBIl7Y7hsPlx5w==", "cpu": [ "x64" ], @@ -1415,9 +1452,9 @@ } }, "node_modules/@oxfmt/binding-linux-x64-musl": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.47.0.tgz", - "integrity": "sha512-DTsmGEaA2860Aq5VUyDO8/MT9NFxwVL93RnRYmpMwK6DsSkThmvEpqoUDDljziEpAedMRG19SCogrNbINSbLUQ==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.48.0.tgz", + "integrity": "sha512-g2SKTTurP5mWjd8Ecait0erYqmltL4IqW1EwttM25BxM6NiTt4ubobJYMR1uox1V2QgG4UfHH10CGRvWlUixjw==", "cpu": [ "x64" ], @@ -1432,9 +1469,9 @@ } }, "node_modules/@oxfmt/binding-openharmony-arm64": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.47.0.tgz", - "integrity": "sha512-8r5BDro7fLOBoq1JXHLVSs55OlrxQhEso4HVo0TcY7OXJUPYfjPoOaYL5us+yIwqyP9rQwN+rxuiNFSmaxSuOQ==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.48.0.tgz", + "integrity": "sha512-CIg24VgheEpvolHL2gQuax5qcQ602bRMHrJ9g8XsQr3iVj9aSPgopigBKuMqrXsupwkrU+RQCn5cG8PgFntR6w==", "cpu": [ "arm64" ], @@ -1449,9 +1486,9 @@ } }, "node_modules/@oxfmt/binding-win32-arm64-msvc": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.47.0.tgz", - "integrity": "sha512-qtz/gzm8IjSPUlseZ0ofW8zyHLoZsuP5HTfcGGkWkUblB89JT8GNYH3ICqjbDsqsGqXum0/ZndXTFplSdXFIcg==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.48.0.tgz", + "integrity": "sha512-zeaWkcxcEULwkGF3I/HgEvcDPN8buYDrxibBUa/IFh5Vmwyge+KpLO+hEwSovW349H0O/C0Z2kaFmEzEDm00/Q==", "cpu": [ "arm64" ], @@ -1466,9 +1503,9 @@ } }, "node_modules/@oxfmt/binding-win32-ia32-msvc": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.47.0.tgz", - "integrity": "sha512-5vIcdcIDE7nCx+MXN6sm8kbC4zajDB31E86rez4i45iHNH/2NjdKlJ720xcHTr3eeiMcttCGPHPhE1TjtBDGZw==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.48.0.tgz", + "integrity": "sha512-yiEKnIAGvx5CyZQOlMaNlZkAbwT7/Quk0j3WLt+PR5hK+qYjPTRRJYDfD77wCBPLvEYAG41v4KG3iL0H+uxoxg==", "cpu": [ "ia32" ], @@ -1483,9 +1520,9 @@ } }, "node_modules/@oxfmt/binding-win32-x64-msvc": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.47.0.tgz", - "integrity": "sha512-Sr59Y5ms54ONBjxFeWhVlGyQcHXxcl9DxC23f6yXlRkcos7LXBLoO+KDfxexjHIOZh7cWqrWduzvUjJ+pHp8cQ==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.48.0.tgz", + "integrity": "sha512-GSD2+7t2UoVMV2NgxXypa4bKewflPMAjYnF0Xw9/ht82ZfafAHhb8STwrEd7wlH2PFogt5zw3WVCxYJaHUdbeQ==", "cpu": [ "x64" ], @@ -1500,9 +1537,9 @@ } }, "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.62.0.tgz", - "integrity": "sha512-pKsthNECyvJh8lPTICz6VcwVy2jOqdhhsp1rlxCkhgZR47aKvXPmaRWQDv+zlXpRae4qm1MaaTnutkaOk5aofg==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.63.0.tgz", + "integrity": "sha512-A9xLtQt7i0OA1PoB/meog6kikXI9CdwEp7ZwQqmgnpKn3G3b1orvTDy8CQ6T7w1HvDrgWGB78PkFKcWgibcTCg==", "cpu": [ "arm" ], @@ -1517,9 +1554,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.62.0.tgz", - "integrity": "sha512-b1AUNViByvgmR2xJDubvLIr+dSuu3uraG7bsAoKo+xrpspPvu6RIn6Fhr2JUhobfep3jwUTy18Huco6GkwdvGQ==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.63.0.tgz", + "integrity": "sha512-SQo+ZMvdR9l3CxZp5W5gFNxSiDxclY6lOzzNpKYLF8asESpm3Pwumx0gER5T7aHLF1/2BAAtLD3DiDkdgy4V1A==", "cpu": [ "arm64" ], @@ -1534,9 +1571,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.62.0.tgz", - "integrity": "sha512-iG+Tvf70UJ6otfwFYIHk36Sjq9cpPP5YLxkoggANNRtzgi3Tj3g8q6Ybqi6AtkU3+yg9QwF7bDCkCS6bbL4PCg==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.63.0.tgz", + "integrity": "sha512-6W82XjJDTmMnjg30427l0dufpnyLoq7wEukKdM6/g2VIybRVuQiBVh43EA4b+UxZ3+tLcKm+Or/pXGNgLCEU8g==", "cpu": [ "arm64" ], @@ -1551,9 +1588,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.62.0.tgz", - "integrity": "sha512-oOWI6YPPr5AJUx+yIDlxmuUbQjS5gZX3OH3QisawYvsZgLiQVvZtR0rPBcJTxLWqt2ClrWg0DlSrlUiG5SQNHg==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.63.0.tgz", + "integrity": "sha512-CnWd/YCuVG5W1BYkjJEVbJG11o526O9qAwBEQM+nh8K19CRFUkFdROXCyYkGmroHEYQe4vgQ6+lh3550Lp35Xw==", "cpu": [ "x64" ], @@ -1568,9 +1605,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.62.0.tgz", - "integrity": "sha512-dLP33T7VLCmLVv4cvjkVX+rmkcwNk2UfxmsZPNur/7BQHoQR60zJ7XLiRvNUawlzn0u8ngCa3itjEG73MAMa/w==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.63.0.tgz", + "integrity": "sha512-a4eZAqrmtajqcxfdAzC+l7g3PaE3V8hpAYqqeD3fTxLXOMFdK3eNTZrU80n4dDEVm0JXy1aL5PqvqWldBl6zYA==", "cpu": [ "x64" ], @@ -1585,9 +1622,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.62.0.tgz", - "integrity": "sha512-fl//LWNks6qo9chNY60UDYyIwtp7a5cEx4Y/rHPjaarhuwqx6jtbzEpD5V5AqmdL4a6Y5D8zeXg5HF2Cr0QmSQ==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.63.0.tgz", + "integrity": "sha512-tYUtU9TdbU3uXF5D62g5zXJ13iniFGhXQx5vp9cyEjGdbSAY3VdFBSaldYvyoDmgMZ0ZYuwQP1Y4t2Fhejwa0w==", "cpu": [ "arm" ], @@ -1602,9 +1639,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.62.0.tgz", - "integrity": "sha512-i5vkAuxvueTODV3J2dL61/TXewDHhMFKvtD156cIsk7GsdfiAu7zW7kY0NJXhKeFHeiMZIh7eFNjkPYH6J47HQ==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.63.0.tgz", + "integrity": "sha512-I5r3twFf776UZg9dmRo2xbrKt00tTkORXEVe0ctg4vdTkQvJAjiCHxnbAU2HL1AiJ9cqADA76MAliuilsAWnvg==", "cpu": [ "arm" ], @@ -1619,9 +1656,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.62.0.tgz", - "integrity": "sha512-QwN19LLuIGuOjEflSeJkZmOTfBdBMlTmW8xbMf8TZhjd//cxVNYQPq75q7oKZBJc6hRx3gY7sX0Egc8cEIFZYg==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.63.0.tgz", + "integrity": "sha512-t7ltUkg6FFh4b564QyGir8xIj/QZbXu8FlcRkcyW9+ztr/mfRHlvUOFd95pJCXi9s/L5DrUeWWgpXRS+V+6igQ==", "cpu": [ "arm64" ], @@ -1636,9 +1673,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.62.0.tgz", - "integrity": "sha512-8eCy3FCDuWUM5hWujAv6heMvfZPbcCOU3SdQUAkixZLu5bSzOkNfirJiLGoQFO943xceOKkiQRMQNzH++jM3WA==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.63.0.tgz", + "integrity": "sha512-Q5mmZy/XWjuYFUuQyYjOvZ5U/JkKEwnpir6hGxhh6HcdP0V/BKxLo8dqkfF/t7r7AguB17dfS/8+go5AQDRR6g==", "cpu": [ "arm64" ], @@ -1653,9 +1690,9 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.62.0.tgz", - "integrity": "sha512-NjQ7K7tpTPDe9J+yq8p/s/J0E7lRCkK2uDBDqvT4XIT6f4Z0tlnr59OBg/WcrmVHER1AbrcfyxhGTXgcG8ytWg==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.63.0.tgz", + "integrity": "sha512-uBGtuZ0TzLB4x5wVa82HGNvYqY8buwDhyCnCP0R0gkk9szqVsP0MeTtD5HX7EsEuFIt+aYmYxuxeVxs3nTSwtQ==", "cpu": [ "ppc64" ], @@ -1670,9 +1707,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.62.0.tgz", - "integrity": "sha512-oKZed9gmSwze29dEt3/Wnsv6l/Ygw/FUst+8Kfpv2SGeS/glEoTGZAMQw37SVyzFV76UTHJN2snGgxK2t2+8ow==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.63.0.tgz", + "integrity": "sha512-h4s6FwxE+9MeA181o0dnDwHP32Y/bG8EiB/vrD6Ib+AMt6haigDc/0bUtI/sLmQDBMJnUfaCmtSSrEAqjtEVrA==", "cpu": [ "riscv64" ], @@ -1687,9 +1724,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.62.0.tgz", - "integrity": "sha512-gBjBxQ+9lGpAYq+ELqw0w8QXsBnkZclFc7GRX2r0LnEVn3ZTEqeIKpKcGjucmp76Q53bvJD0i4qBWBhcfhSfGA==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.63.0.tgz", + "integrity": "sha512-2EaNcCBR8Mcjl5ARtuN3BdEpVkX7KpjSjMGZ/mJMIeaXgTtdz5ytg2VwygMSStA/k0ixfvZFoZOfjDEcouV5vQ==", "cpu": [ "riscv64" ], @@ -1704,9 +1741,9 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.62.0.tgz", - "integrity": "sha512-Ew2Kxs9EQ9/mbAIJ2hvocMC0wsOu6YKzStI2eFBDt+Td5O8seVC/oxgRIHqCcl5sf5ratA1nozQBAuv7tphkHg==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.63.0.tgz", + "integrity": "sha512-p4hlf/fd7TrYYl3QrWWD0GocqJefwMu3cHQhmi2FvEB/YOvFb5DZN3SMBaPi7B1TM5DeypkEtrVib674q1KKPg==", "cpu": [ "s390x" ], @@ -1721,9 +1758,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.62.0.tgz", - "integrity": "sha512-5z25jcAA0gfKyVwz71A0VXgaPlocPoTAxhlv/hgoK6tlCrfoNuw7haWbDHvGMfjXhdic4EqVXGRv5XsTqFnbRQ==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.63.0.tgz", + "integrity": "sha512-Vgq9rkRVcPcjbcH+ihYTfpeR7vCXfqpd+z5ItTGc0yYUV59L5ceHYN1iV4H9bKGV7Rn5hkVc7x3mSvHegduENA==", "cpu": [ "x64" ], @@ -1738,9 +1775,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.62.0.tgz", - "integrity": "sha512-IWpHmMB6ZDllPvqWDkG6AmXrN7JF5e/c4g/0PuURsmlK+vHoYZPB70rr4u1bn3I4LsKCSpqqfveyx6UCOC8wdg==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.63.0.tgz", + "integrity": "sha512-3/Lkq/ncooA61rorrC+ZQed1Bc4VpGj+WnGsp58zmxKgvZ2vhreu+dcVyr3mX8NUpq7mfZ4gDDTou/yrF1Pd7A==", "cpu": [ "x64" ], @@ -1755,9 +1792,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.62.0.tgz", - "integrity": "sha512-fjlSxxrD5pA594vkyikCS9MnPRjQawW6/BLgyTYkO+73wwPlYjkcZ7LSd974l0Q2zkHQmu4DPvJFLYA7o8xrxQ==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.63.0.tgz", + "integrity": "sha512-0/EdD/6hDkx5Mfd769PTjvEM8mZ/6Dfukp1dBCL/2PjlIVGEtYdNZyok6ChqYPsT9JcFnlQnUeQzO0/1L/oC9w==", "cpu": [ "arm64" ], @@ -1772,9 +1809,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.62.0.tgz", - "integrity": "sha512-EiFXr8loNS0Ul3Gu80+9nr1T8jRmnKocqmHHg16tj5ZqTgUXyb97l2rrspVHdDluyFn9JfR4PoJFdNzw4paHww==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.63.0.tgz", + "integrity": "sha512-wb0CUkN8ngwPiRQBjD1Cj0LsHeNvm+Xt6YBHDMtj2DVQVD6Oj8Ri7g6BD+KICf6LaBqZlmzOvy6nF9E/8yyGOg==", "cpu": [ "arm64" ], @@ -1789,9 +1826,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.62.0.tgz", - "integrity": "sha512-IgOFvL73li1bFgab+hThXYA0N2Xms2kV2MvZN95cebV+fmrZ9AVui1JSxfeeqRLo3CpPxKZlzhyq4G0cnaAvIw==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.63.0.tgz", + "integrity": "sha512-BX5iq+ovdNlVYhSn5qPMUIT0uwAwt2lmEnCnzK+Gkhw4DovIvhGb96OFhV8yzQNUnQxn/xGkOR+X+BLrLDNm8w==", "cpu": [ "ia32" ], @@ -1806,9 +1843,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.62.0.tgz", - "integrity": "sha512-6hMpyDWQ2zGA1OXFKBrdYMUveUCO8UJhkO6JdwZPd78xIdHZNhjx+pib+4fC2Cljuhjyl0QwA2F3df/bs4Bp6A==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.63.0.tgz", + "integrity": "sha512-QeN/WELOfsXMeYwxvfgQrl6CbVftYUCZsGXHjXQd5Trccm8+i4gmtxaOui4xbJQaiDlviF8F3yLSBloQUeFsfA==", "cpu": [ "x64" ], @@ -2143,7 +2180,7 @@ "version": "1.0.0-rc.15", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@sec-ant/readable-stream": { @@ -2919,7 +2956,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tokenizer/inflate": { @@ -2960,7 +2997,7 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/deep-eql": "*", @@ -2971,14 +3008,14 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/http-cache-semantics": { @@ -3008,7 +3045,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", @@ -3026,7 +3063,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/spy": "4.1.5", @@ -3053,7 +3090,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tinyrainbow": "^3.1.0" @@ -3066,7 +3103,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/utils": "4.1.5", @@ -3080,7 +3117,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "4.1.5", @@ -3096,7 +3133,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" @@ -3106,7 +3143,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "4.1.5", @@ -3288,7 +3325,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -3517,7 +3554,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -3921,7 +3958,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/core-util-is": { @@ -4106,7 +4143,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4399,7 +4436,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/es-object-atoms": { @@ -4455,7 +4492,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -4493,7 +4530,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" @@ -4896,9 +4933,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5624,7 +5661,7 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, + "devOptional": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -6075,7 +6112,7 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -6100,9 +6137,9 @@ } }, "node_modules/marked": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.2.tgz", - "integrity": "sha512-NsmlUYBS/Zg57rgDWMYdnre6OTj4e+qq/JS2ot3KrYLSoHLw+sDu0Nm1ZGpRgYAq6c+b1ekaY5NzVchMCQnzcg==", + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.3.tgz", + "integrity": "sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -6273,7 +6310,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -8297,7 +8334,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, + "devOptional": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" @@ -8346,9 +8383,9 @@ } }, "node_modules/oxfmt": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.47.0.tgz", - "integrity": "sha512-OFbkbzxKCpooQEnRmpTDnuwTX8KHXzZTQ4Df/hz85fpS67Pl+lxPEFvUtin56HIIS0B1k4X8oIzTXRZPufA2CA==", + "version": "0.48.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.48.0.tgz", + "integrity": "sha512-AVaLh+7XeGx+R1zfFV+f6VV61nT2MWVJXVUDhbTm5LBWGyNt64xAyh3NYYyjeY2WykNt9AvqSQLPHcbWquYF9g==", "dev": true, "license": "MIT", "dependencies": { @@ -8364,31 +8401,31 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxfmt/binding-android-arm-eabi": "0.47.0", - "@oxfmt/binding-android-arm64": "0.47.0", - "@oxfmt/binding-darwin-arm64": "0.47.0", - "@oxfmt/binding-darwin-x64": "0.47.0", - "@oxfmt/binding-freebsd-x64": "0.47.0", - "@oxfmt/binding-linux-arm-gnueabihf": "0.47.0", - "@oxfmt/binding-linux-arm-musleabihf": "0.47.0", - "@oxfmt/binding-linux-arm64-gnu": "0.47.0", - "@oxfmt/binding-linux-arm64-musl": "0.47.0", - "@oxfmt/binding-linux-ppc64-gnu": "0.47.0", - "@oxfmt/binding-linux-riscv64-gnu": "0.47.0", - "@oxfmt/binding-linux-riscv64-musl": "0.47.0", - "@oxfmt/binding-linux-s390x-gnu": "0.47.0", - "@oxfmt/binding-linux-x64-gnu": "0.47.0", - "@oxfmt/binding-linux-x64-musl": "0.47.0", - "@oxfmt/binding-openharmony-arm64": "0.47.0", - "@oxfmt/binding-win32-arm64-msvc": "0.47.0", - "@oxfmt/binding-win32-ia32-msvc": "0.47.0", - "@oxfmt/binding-win32-x64-msvc": "0.47.0" + "@oxfmt/binding-android-arm-eabi": "0.48.0", + "@oxfmt/binding-android-arm64": "0.48.0", + "@oxfmt/binding-darwin-arm64": "0.48.0", + "@oxfmt/binding-darwin-x64": "0.48.0", + "@oxfmt/binding-freebsd-x64": "0.48.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.48.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.48.0", + "@oxfmt/binding-linux-arm64-gnu": "0.48.0", + "@oxfmt/binding-linux-arm64-musl": "0.48.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.48.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.48.0", + "@oxfmt/binding-linux-riscv64-musl": "0.48.0", + "@oxfmt/binding-linux-s390x-gnu": "0.48.0", + "@oxfmt/binding-linux-x64-gnu": "0.48.0", + "@oxfmt/binding-linux-x64-musl": "0.48.0", + "@oxfmt/binding-openharmony-arm64": "0.48.0", + "@oxfmt/binding-win32-arm64-msvc": "0.48.0", + "@oxfmt/binding-win32-ia32-msvc": "0.48.0", + "@oxfmt/binding-win32-x64-msvc": "0.48.0" } }, "node_modules/oxlint": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.62.0.tgz", - "integrity": "sha512-1uFkg6HakjsGIpW9wNdeW4/2LOHW9MEkoWjZUTUfQtIHyLIZPYt00w3Sg+H3lH+206FgBPHBbW5dVE5l2ExECQ==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.63.0.tgz", + "integrity": "sha512-9TGXetdjgIHOJ9OiReomP7nnrMkV9HxC1xM2ramJSLQpzxjsAJtQwa4wqkJN2f/uCrqZuJseFuSlWDdvcruveg==", "dev": true, "license": "MIT", "bin": { @@ -8401,28 +8438,28 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.62.0", - "@oxlint/binding-android-arm64": "1.62.0", - "@oxlint/binding-darwin-arm64": "1.62.0", - "@oxlint/binding-darwin-x64": "1.62.0", - "@oxlint/binding-freebsd-x64": "1.62.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.62.0", - "@oxlint/binding-linux-arm-musleabihf": "1.62.0", - "@oxlint/binding-linux-arm64-gnu": "1.62.0", - "@oxlint/binding-linux-arm64-musl": "1.62.0", - "@oxlint/binding-linux-ppc64-gnu": "1.62.0", - "@oxlint/binding-linux-riscv64-gnu": "1.62.0", - "@oxlint/binding-linux-riscv64-musl": "1.62.0", - "@oxlint/binding-linux-s390x-gnu": "1.62.0", - "@oxlint/binding-linux-x64-gnu": "1.62.0", - "@oxlint/binding-linux-x64-musl": "1.62.0", - "@oxlint/binding-openharmony-arm64": "1.62.0", - "@oxlint/binding-win32-arm64-msvc": "1.62.0", - "@oxlint/binding-win32-ia32-msvc": "1.62.0", - "@oxlint/binding-win32-x64-msvc": "1.62.0" + "@oxlint/binding-android-arm-eabi": "1.63.0", + "@oxlint/binding-android-arm64": "1.63.0", + "@oxlint/binding-darwin-arm64": "1.63.0", + "@oxlint/binding-darwin-x64": "1.63.0", + "@oxlint/binding-freebsd-x64": "1.63.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.63.0", + "@oxlint/binding-linux-arm-musleabihf": "1.63.0", + "@oxlint/binding-linux-arm64-gnu": "1.63.0", + "@oxlint/binding-linux-arm64-musl": "1.63.0", + "@oxlint/binding-linux-ppc64-gnu": "1.63.0", + "@oxlint/binding-linux-riscv64-gnu": "1.63.0", + "@oxlint/binding-linux-riscv64-musl": "1.63.0", + "@oxlint/binding-linux-s390x-gnu": "1.63.0", + "@oxlint/binding-linux-x64-gnu": "1.63.0", + "@oxlint/binding-linux-x64-musl": "1.63.0", + "@oxlint/binding-openharmony-arm64": "1.63.0", + "@oxlint/binding-win32-arm64-msvc": "1.63.0", + "@oxlint/binding-win32-ia32-msvc": "1.63.0", + "@oxlint/binding-win32-x64-msvc": "1.63.0" }, "peerDependencies": { - "oxlint-tsgolint": ">=0.18.0" + "oxlint-tsgolint": ">=0.22.1" }, "peerDependenciesMeta": { "oxlint-tsgolint": { @@ -8693,14 +8730,14 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { @@ -8788,7 +8825,7 @@ "version": "8.5.12", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -9100,7 +9137,7 @@ "version": "1.0.0-rc.15", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.124.0", @@ -9591,7 +9628,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/signal-exit": { @@ -9757,7 +9794,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -9820,14 +9857,14 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/std-env": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/stream-combiner2": { @@ -10180,7 +10217,7 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tinycolor2": { @@ -10193,7 +10230,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -10203,7 +10240,7 @@ "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -10220,7 +10257,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -10238,7 +10275,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -10261,7 +10298,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=14.0.0" @@ -10497,9 +10534,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -10524,7 +10561,7 @@ "version": "8.0.8", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", @@ -10602,7 +10639,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -10615,7 +10652,7 @@ "version": "4.1.5", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@vitest/expect": "4.1.5", @@ -10705,7 +10742,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -10740,7 +10777,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "siginfo": "^2.0.0", diff --git a/package.json b/package.json index aac3b341..5a49fcf2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@doist/todoist-cli", - "version": "1.60.3", + "version": "1.63.0", "description": "TypeScript CLI for Todoist", "type": "module", "main": "dist/index.js", @@ -51,16 +51,16 @@ "CHANGELOG.md" ], "dependencies": { - "@doist/todoist-sdk": "10.2.0-next.1", - "@napi-rs/keyring": "1.2.0", + "@doist/cli-core": "0.9.0", + "@doist/todoist-sdk": "10.2.0-next.2", + "@napi-rs/keyring": "1.3.0", "@pnpm/tabtab": "0.5.4", "chalk": "5.6.2", "commander": "14.0.3", "date-fns": "4.1.0", - "marked": "18.0.2", + "marked": "18.0.3", "marked-terminal-renderer": "2.2.0", - "open": "11.0.0", - "yocto-spinner": "1.1.0" + "open": "11.0.0" }, "devDependencies": { "@semantic-release/changelog": "6.0.3", @@ -69,8 +69,8 @@ "@types/node": "25.6.0", "conventional-changelog-conventionalcommits": "9.3.1", "lefthook": "2.1.6", - "oxfmt": "0.47.0", - "oxlint": "1.62.0", + "oxfmt": "0.48.0", + "oxlint": "1.63.0", "semantic-release": "25.0.3", "typescript": "6.0.3", "vitest": "4.1.5" diff --git a/skills/todoist-cli/SKILL.md b/skills/todoist-cli/SKILL.md index bb38ced9..b3b3f0a3 100644 --- a/skills/todoist-cli/SKILL.md +++ b/skills/todoist-cli/SKILL.md @@ -5,7 +5,7 @@ compatibility: "Requires the td CLI (@doist/todoist-cli) to be installed and aut license: MIT metadata: author: Doist - version: "1.60.3" + version: "1.63.0" --- # Todoist CLI (td) @@ -40,11 +40,18 @@ td auth login --read-only --additional-scopes=app-management td auth login --additional-scopes=backups td auth login --read-only --additional-scopes=backups td auth login --additional-scopes=app-management,backups +td auth login --callback-port 9000 # override the OAuth callback port +td auth login --json # emit the new account record as JSON +td auth login --ndjson # one-line newline-delimited JSON td auth token td auth status +TOKEN=$(td auth token view) +TOKEN=$(td auth token view --user you@example.com) td auth logout ``` +`td auth login` accepts `--callback-port ` (default `8765`, with a small fallback range when the port is busy) and the standard `--json` / `--ndjson` machine-output flags. Use `--json` / `--ndjson` to capture the newly stored account record (id, email, auth metadata) for scripts; warnings about keyring fallback are written to stderr so stdout stays parseable. + Opt-in OAuth scopes are requested via `--additional-scopes=` (comma-separated). Run `td auth login --help` for the full list. Currently supported: - `app-management` — adds the `dev:app_console` scope (manage your registered Todoist apps — rotate secrets, edit webhooks, etc.). Required by `td apps list` and `td apps view`. @@ -54,6 +61,8 @@ Combine freely with `--read-only` to keep data access read-only while still gran Tokens are stored in the OS credential manager when available, with fallback to `~/.config/todoist-cli/config.json`. `TODOIST_API_TOKEN` takes precedence over stored credentials. +`td auth token view` writes the stored token to stdout for use in scripts. **Always capture it into a shell variable** (e.g. `TOKEN=$(td auth token view)`) — never invoke it bare in an agent transcript or piped to a shell that echoes its output, since that would leak the secret. Honors `--user ` for multi-account installs and refuses when `TODOIST_API_TOKEN` is set in the environment (the token is already available there). + ## Multi-user The CLI can hold credentials for multiple Todoist accounts at once. @@ -61,6 +70,7 @@ The CLI can hold credentials for multiple Todoist accounts at once. ```bash td auth login # adds the account; first one becomes default td user list # all stored accounts (with default marker) +td user list --json # array of accounts with auth metadata (--ndjson also supported) td user use # set the default account (alias: td user default) td user current # show the active account td user remove # delete an account (and its token) @@ -74,7 +84,7 @@ Resolution order: `--user ` > `user.defaultUser` from config > the only sto - Daily views: `td today`, `td inbox`, `td upcoming`, `td completed`, `td activity` - Task lifecycle: `td task list/view/add/quickadd/update/reschedule/move/complete/uncomplete/delete/browse` (alias: `td task qa` for `quickadd`) -- Projects: `td project list/view/create/update/archive/unarchive/archived/delete/move/join/browse/collaborators/permissions` +- Projects: `td project list/view/create/update/archive/unarchive/archived/delete/move/reorder/join/browse/collaborators/permissions` - Project analytics: `td project progress/health/health-context/activity-stats/analyze-health` - Goals: `td goal list/view/create/update/delete/complete/uncomplete/link/unlink` - Organization: `td label ...`, `td filter ...`, `td section ...`, `td folder ...`, `td workspace ...` @@ -134,7 +144,7 @@ Choosing between `task add` and `task quickadd`: Useful task flags: - `--stdin` on `task add` reads the task description from stdin; on `task quickadd` (and the top-level `td add`) it reads the full natural-language text from stdin. - `--parent`, `--section`, `--project`, `--workspace`, `--assignee`, `--labels`, `--due`, `--deadline`, `--duration`, and `--priority` cover most task workflows. -- `td task complete --forever` stops recurrence; `td task update --no-due` clears the due date and `--no-deadline` clears deadlines; `td task move --no-parent` and `--no-section` detach from hierarchy. +- `td task complete --forever` stops recurrence; `td task update --no-due` clears the due date, `--no-deadline` clears deadlines, and `--no-labels` removes all labels; `td task move --no-parent` and `--no-section` detach from hierarchy. ### Projects And Workspaces ```bash @@ -147,6 +157,15 @@ td project create --name "New Project" --color blue td project update "Roadmap" --favorite td project update "Roadmap" --folder "Engineering" td project update "Roadmap" --no-folder +td project update "Roadmap" --parent "Engineering" +td project update "Roadmap" --no-parent +td project update "Roadmap" --parent "Engineering" --json +td project update "Roadmap" --parent "Engineering" --dry-run +td project reorder "Roadmap" --before "Marketing" +td project reorder "Roadmap" --after "Marketing" +td project reorder "Roadmap" --position 0 +td project reorder "Roadmap" --position 2 --json +td project reorder "Roadmap" --before "Marketing" --dry-run td project archive "Roadmap" td project unarchive "Roadmap" td project move "Roadmap" --to-workspace "Acme" --folder "Engineering" --visibility team --yes @@ -267,6 +286,7 @@ td reminder location get id:456 td hc td hc --help td hc locale --set-default pt-br +td hc search "filters" --ndjson # one article per line for scripts (--json also supported) td hc view https://www.todoist.com/help/articles/introduction-to-filters-V98wIH ``` @@ -338,9 +358,10 @@ td doctor --offline td doctor --json td update --check +td update --check --json td update --channel td update switch --stable -td update switch --pre-release +td update switch --pre-release --json td changelog --count 10 ``` diff --git a/src/commands/apps/apps.test.ts b/src/commands/apps/apps.test.ts index f5caf0a5..cebbe0db 100644 --- a/src/commands/apps/apps.test.ts +++ b/src/commands/apps/apps.test.ts @@ -1,3 +1,4 @@ +import { describeEmptyMachineOutput } from '@doist/cli-core/testing' import { TodoistRequestError } from '@doist/todoist-sdk' import { Command } from 'commander' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -106,16 +107,15 @@ describe('apps list', () => { consoleSpy.mockRestore() }) - it('shows "No apps found." when empty', async () => { - const program = createProgram() - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - - mockApi.getApps.mockResolvedValue([]) - - await program.parseAsync(['node', 'td', 'apps', 'list']) - - expect(consoleSpy).toHaveBeenCalledWith('No apps found.') - consoleSpy.mockRestore() + describeEmptyMachineOutput('empty machine output contract', { + setup: () => { + mockApi.getApps.mockResolvedValue([]) + }, + run: async (extraArgs) => { + const program = createProgram() + await program.parseAsync(['node', 'td', 'apps', 'list', ...extraArgs]) + }, + humanMessage: 'No apps found.', }) it('outputs full JSON with --json flag', async () => { diff --git a/src/commands/apps/list.ts b/src/commands/apps/list.ts index 6c7b8e9d..a4a125d4 100644 --- a/src/commands/apps/list.ts +++ b/src/commands/apps/list.ts @@ -1,3 +1,4 @@ +import { printEmpty } from '@doist/cli-core' import chalk from 'chalk' import { getApi } from '../../lib/api/core.js' @@ -10,6 +11,11 @@ export async function listApps(options: ListAppsOptions = {}): Promise { const api = await getApi() const apps = await api.getApps() + if (apps.length === 0) { + printEmpty({ options, message: 'No apps found.' }) + return + } + if (options.json) { console.log(JSON.stringify(apps, null, 2)) return @@ -20,11 +26,6 @@ export async function listApps(options: ListAppsOptions = {}): Promise { return } - if (apps.length === 0) { - console.log('No apps found.') - return - } - for (const app of apps) { console.log(`${app.displayName} ${chalk.dim(`(id:${app.id})`)}`) console.log(` ${chalk.dim(`Client ID: ${app.clientId}`)}`) diff --git a/src/commands/auth/auth.test.ts b/src/commands/auth/auth.test.ts index 4cb79693..24935fcf 100644 --- a/src/commands/auth/auth.test.ts +++ b/src/commands/auth/auth.test.ts @@ -11,6 +11,7 @@ vi.mock('../../lib/auth.js', async (importOriginal) => { getAuthMetadata: vi.fn(), listStoredUsers: vi.fn(), readConfig: vi.fn(), + resolveActiveUser: vi.fn(), } }) @@ -23,34 +24,20 @@ vi.mock('../../lib/api/core.js', () => ({ // Mock chalk to avoid colors in tests vi.mock('chalk') -// Mock PKCE module -vi.mock('../../lib/pkce.js', () => ({ - generateCodeVerifier: vi.fn(() => 'test_code_verifier'), - generateCodeChallenge: vi.fn(() => 'test_code_challenge'), - generateState: vi.fn(() => 'test_state'), -})) - -// Mock OAuth server -vi.mock('../../lib/oauth-server.js', () => ({ - startCallbackServer: vi.fn(), - OAUTH_REDIRECT_URI: 'http://localhost:8765/callback', -})) - -// Mock OAuth module -vi.mock('../../lib/oauth.js', async (importOriginal) => { - const actual = await importOriginal() +// Mock cli-core's login registrar so the login subcommand never actually +// drives the OAuth flow. The original todoist-local OAuth tests were dropped +// when `pkce` / `oauth` / `oauth-server` moved to cli-core; cli-core has its +// own runtime + registrar tests. +vi.mock('@doist/cli-core/auth', async (importOriginal) => { + const actual = await importOriginal() return { ...actual, - buildAuthorizationUrl: vi.fn(() => 'https://todoist.com/oauth/authorize?test=1'), - exchangeCodeForToken: vi.fn(), + attachLoginCommand: vi.fn((parent: { command: (name: string) => unknown }) => + parent.command('login'), + ), } }) -// Mock open module -vi.mock('open', () => ({ - default: vi.fn(), -})) - // Mock readline for interactive token input vi.mock('node:readline', () => ({ createInterface: vi.fn(() => { @@ -63,18 +50,19 @@ vi.mock('node:readline', () => ({ })) import { createInterface, type Interface } from 'node:readline' -import open from 'open' import { createApiForToken, getApi } from '../../lib/api/core.js' import { NoTokenError, + TOKEN_ENV_VAR, clearApiToken, getAuthMetadata, listStoredUsers, readConfig, + resolveActiveUser, upsertUser, } from '../../lib/auth.js' -import { startCallbackServer } from '../../lib/oauth-server.js' -import { buildAuthorizationUrl, exchangeCodeForToken } from '../../lib/oauth.js' +import { resetGlobalArgs } from '../../lib/global-args.js' +import { UserNotFoundError } from '../../lib/users.js' import { createMockApi } from '../../test-support/mock-api.js' import { registerAuthCommand } from './index.js' @@ -85,12 +73,9 @@ const mockClearApiToken = vi.mocked(clearApiToken) const mockGetAuthMetadata = vi.mocked(getAuthMetadata) const mockListStoredUsers = vi.mocked(listStoredUsers) const mockReadConfig = vi.mocked(readConfig) +const mockResolveActiveUser = vi.mocked(resolveActiveUser) const mockGetApi = vi.mocked(getApi) const mockCreateApiForToken = vi.mocked(createApiForToken) -const mockStartCallbackServer = vi.mocked(startCallbackServer) -const mockBuildAuthorizationUrl = vi.mocked(buildAuthorizationUrl) -const mockExchangeCodeForToken = vi.mocked(exchangeCodeForToken) -const mockOpen = vi.mocked(open) const TEST_USER = { id: '12345', @@ -251,153 +236,9 @@ describe('auth command', () => { }) }) - describe('login subcommand (OAuth flow)', () => { - function setupOAuthFlow(authCode: string, accessToken: string) { - mockStartCallbackServer.mockResolvedValue({ - promise: Promise.resolve(authCode), - port: 8765, - cleanup: vi.fn(), - }) - mockExchangeCodeForToken.mockResolvedValue(accessToken) - stubProbeApiForUser() - mockUpsertUser.mockResolvedValue({ storage: 'secure-store', replaced: false }) - mockOpen.mockResolvedValue({} as Awaited>) - } - - it('completes OAuth flow successfully', async () => { - const program = createProgram() - setupOAuthFlow('oauth_auth_code_123', 'oauth_access_token_456') - - await program.parseAsync(['node', 'td', 'auth', 'login']) - - expect(mockOpen).toHaveBeenCalledWith('https://todoist.com/oauth/authorize?test=1') - expect(mockStartCallbackServer).toHaveBeenCalledWith('test_state') - expect(mockExchangeCodeForToken).toHaveBeenCalledWith( - 'oauth_auth_code_123', - 'test_code_verifier', - 8765, - ) - expect(mockCreateApiForToken).toHaveBeenCalledWith('oauth_access_token_456') - expect(mockUpsertUser).toHaveBeenCalledWith({ - id: TEST_USER.id, - email: TEST_USER.email, - token: 'oauth_access_token_456', - authMode: 'read-write', - authScope: 'data:read_write,data:delete,project:delete', - authFlags: [], - }) - expect(consoleSpy).toHaveBeenCalledWith('✓', `Logged in as ${TEST_USER.email}`) - }) - - it('uses read-only scope when --read-only is set', async () => { - const program = createProgram() - setupOAuthFlow('oauth_auth_code_123', 'oauth_access_token_456') - - await program.parseAsync(['node', 'td', 'auth', 'login', '--read-only']) - - expect(mockBuildAuthorizationUrl).toHaveBeenCalledWith( - 'test_code_challenge', - 'test_state', - { readOnly: true, additionalScopes: [], port: 8765 }, - ) - expect(mockUpsertUser).toHaveBeenCalledWith( - expect.objectContaining({ - authMode: 'read-only', - authScope: 'data:read', - authFlags: ['read-only'], - }), - ) - }) - - it('appends app-management scope', async () => { - const program = createProgram() - setupOAuthFlow('oauth_auth_code_app', 'oauth_access_token_app') - - await program.parseAsync([ - 'node', - 'td', - 'auth', - 'login', - '--additional-scopes=app-management', - ]) - - expect(mockUpsertUser).toHaveBeenCalledWith( - expect.objectContaining({ - authScope: 'data:read_write,data:delete,project:delete,dev:app_console', - authFlags: ['app-management'], - }), - ) - }) - - it('combines read-only with backups scope', async () => { - const program = createProgram() - setupOAuthFlow('c', 't') - - await program.parseAsync([ - 'node', - 'td', - 'auth', - 'login', - '--additional-scopes=backups', - '--read-only', - ]) - - expect(mockUpsertUser).toHaveBeenCalledWith( - expect.objectContaining({ - authMode: 'read-only', - authScope: 'data:read,backups:read', - authFlags: ['read-only', 'backups'], - }), - ) - }) - - it('shows "Updated credentials for" when re-logging in to the same account', async () => { - const program = createProgram() - setupOAuthFlow('c', 't') - mockUpsertUser.mockResolvedValue({ storage: 'secure-store', replaced: true }) - - await program.parseAsync(['node', 'td', 'auth', 'login']) - - expect(consoleSpy).toHaveBeenCalledWith( - '✓', - `Updated credentials for ${TEST_USER.email}`, - ) - }) - - it('rejects an unknown scope', async () => { - const program = createProgram() - mockStartCallbackServer.mockResolvedValue({ - promise: new Promise(() => {}), - port: 8765, - cleanup: vi.fn(), - }) - - await expect( - program.parseAsync(['node', 'td', 'auth', 'login', '--additional-scopes=nonsense']), - ).rejects.toThrow(/Unknown scope/) - expect(mockOpen).not.toHaveBeenCalled() - expect(mockUpsertUser).not.toHaveBeenCalled() - }) - - it('cleanup runs on OAuth callback error', async () => { - const program = createProgram() - const mockCleanup = vi.fn() - - mockStartCallbackServer.mockResolvedValue({ - promise: Promise.reject(new Error('OAuth callback timed out')), - port: 8765, - cleanup: mockCleanup, - }) - mockOpen.mockResolvedValue({} as Awaited>) - - await expect(program.parseAsync(['node', 'td', 'auth', 'login'])).rejects.toThrow( - 'OAuth callback timed out', - ) - - expect(mockCleanup).toHaveBeenCalled() - expect(mockUpsertUser).not.toHaveBeenCalled() - }) - }) + // login subcommand: handled by @doist/cli-core/auth tests now. + // The pre-extraction OAuth flow tests lived here and were dropped along + // with `pkce.ts` / `oauth.ts` / `oauth-server.ts`. describe('status subcommand', () => { it('shows authenticated status when logged in', async () => { @@ -498,4 +339,81 @@ describe('auth command', () => { expect(consoleSpy).toHaveBeenCalledWith('✓', 'Logged out') }) }) + + describe('token view subcommand', () => { + let originalEnvToken: string | undefined + + beforeEach(() => { + originalEnvToken = process.env[TOKEN_ENV_VAR] + delete process.env[TOKEN_ENV_VAR] + }) + + afterEach(() => { + if (originalEnvToken === undefined) { + delete process.env[TOKEN_ENV_VAR] + } else { + process.env[TOKEN_ENV_VAR] = originalEnvToken + } + }) + + it('prints the bare token to stdout', async () => { + const program = createProgram() + mockResolveActiveUser.mockResolvedValue({ + id: TEST_USER.id, + email: TEST_USER.email, + token: 'stored_token_abc123456', + authMode: 'read-write', + source: 'secure-store', + }) + + await program.parseAsync(['node', 'td', 'auth', 'token', 'view']) + + expect(mockResolveActiveUser).toHaveBeenCalled() + expect(consoleSpy).toHaveBeenCalledTimes(1) + expect(consoleSpy).toHaveBeenCalledWith('stored_token_abc123456') + }) + + it('refuses when TODOIST_API_TOKEN is set', async () => { + const program = createProgram() + process.env[TOKEN_ENV_VAR] = 'env_token_value' + + await expect( + program.parseAsync(['node', 'td', 'auth', 'token', 'view']), + ).rejects.toHaveProperty('code', 'TOKEN_FROM_ENV') + expect(mockResolveActiveUser).not.toHaveBeenCalled() + expect(consoleSpy).not.toHaveBeenCalled() + }) + + it('propagates NoTokenError when no users are stored', async () => { + const program = createProgram() + mockResolveActiveUser.mockRejectedValue(new NoTokenError()) + + await expect( + program.parseAsync(['node', 'td', 'auth', 'token', 'view']), + ).rejects.toHaveProperty('code', 'NO_TOKEN') + expect(consoleSpy).not.toHaveBeenCalled() + }) + + it('propagates UserNotFoundError when --user ref does not match', async () => { + const program = createProgram() + mockResolveActiveUser.mockRejectedValue(new UserNotFoundError('missing@example.com')) + + // `--user ` is parsed from process.argv by the global-args + // layer (not commander) and stripped before commander sees the + // argv. Stub process.argv to mirror production wiring so the + // test exercises the same code path as a real invocation. + const originalArgv = process.argv + process.argv = ['node', 'td', 'auth', 'token', 'view', '--user', 'missing@example.com'] + resetGlobalArgs() + try { + await expect( + program.parseAsync(['node', 'td', 'auth', 'token', 'view']), + ).rejects.toHaveProperty('code', 'USER_NOT_FOUND') + } finally { + process.argv = originalArgv + resetGlobalArgs() + } + expect(consoleSpy).not.toHaveBeenCalled() + }) + }) }) diff --git a/src/commands/auth/index.ts b/src/commands/auth/index.ts index 8009e1eb..44e5de7a 100644 --- a/src/commands/auth/index.ts +++ b/src/commands/auth/index.ts @@ -1,32 +1,33 @@ -import { Command } from 'commander' -import { formatScopesHelp } from '../../lib/oauth-scopes.js' -import { loginWithOAuth } from './login.js' +import type { Command } from 'commander' +import { attachTodoistLoginCommand } from './login.js' import { logout } from './logout.js' import { showStatus } from './status.js' +import { viewToken } from './token-view.js' import { loginWithToken } from './token.js' export function registerAuthCommand(program: Command): void { const auth = program.command('auth').description('Manage authentication') - auth.command('login') - .description('Authenticate with Todoist via OAuth') - .option('--read-only', 'Authenticate with read-only scope (data:read)') - .option( - '--additional-scopes ', - 'Comma-separated opt-in OAuth scopes (see list below). The flag may be repeated; every occurrence is merged.', - // Commander treats this as a scalar by default, so repeated uses - // (`--additional-scopes=a --additional-scopes=b`) would silently - // drop earlier values. Concatenate into one comma-separated string - // and let parseScopesOption split/dedupe/validate as usual. - (value: string, prev: string | undefined) => (prev ? `${prev},${value}` : value), - ) - .addHelpText('after', formatScopesHelp()) - .action(loginWithOAuth) + attachTodoistLoginCommand(auth) - auth.command('token [token]') - .description('Save API token for CLI authentication') + // `token` is a hybrid: it accepts a positional `[token]` (save) and also + // exposes subcommands (`view`). Commander matches subcommand names before + // falling through to the parent action, so `td auth token view` always + // dispatches to the `view` subcommand — `view` is never treated as a + // literal token value. Real Todoist tokens are 40-char hex strings, so + // this disambiguation is safe in practice. + const tokenCmd = auth + .command('token [token]') + .description('Save API token for CLI authentication (or use a subcommand: `view`)') .action(loginWithToken) + tokenCmd + .command('view') + .description( + 'Print the stored API token for the active user (or --user ) to stdout for use in scripts', + ) + .action(viewToken) + auth.command('status') .description('Show current authentication status') .option('--json', 'Output as JSON') diff --git a/src/commands/auth/login.test.ts b/src/commands/auth/login.test.ts new file mode 100644 index 00000000..76186616 --- /dev/null +++ b/src/commands/auth/login.test.ts @@ -0,0 +1,145 @@ +import { Command } from 'commander' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('../../lib/auth.js', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + upsertUser: vi.fn(), + clearApiToken: vi.fn(), + loadTokenForStoredUser: vi.fn(), + } +}) + +vi.mock('../../lib/api/core.js', () => ({ + getApi: vi.fn(), + createApiForToken: vi.fn(), +})) + +vi.mock('chalk') + +// Capture (but don't execute) the options handed to cli-core's +// `attachLoginCommand` so tests can invoke the Todoist-local callbacks +// (`resolveScopes`, `onSuccess`) directly — cli-core's own tests don't cover +// the mapping logic, so the local glue would otherwise go unverified. +const capturedAttachOptions: Array<{ options: Record }> = [] + +vi.mock('@doist/cli-core/auth', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + attachLoginCommand: vi.fn( + (parent: { command: (name: string) => Command }, options: Record) => { + capturedAttachOptions.push({ options }) + return parent.command('login') + }, + ), + } +}) + +import { attachTodoistLoginCommand } from './login.js' + +type AttachOptions = { + resolveScopes: (ctx: { readOnly: boolean; flags: Record }) => string[] + onSuccess: (ctx: { + account: { id: string; label?: string } + view: { json: boolean; ndjson: boolean } + flags: Record + }) => void | Promise + store: { getLastStorageResult: () => unknown } +} + +function attachAndCapture(): AttachOptions { + capturedAttachOptions.length = 0 + const program = new Command() + program.exitOverride() + attachTodoistLoginCommand(program) + return capturedAttachOptions[capturedAttachOptions.length - 1].options as AttachOptions +} + +const ACCOUNT = { + id: '12345', + email: 'you@example.com', + label: 'you@example.com', + auth_mode: 'read-write' as const, + auth_scope: 'data:read_write,data:delete,project:delete', + auth_flags: undefined, +} + +describe('attachTodoistLoginCommand: resolveScopes callback', () => { + afterEach(() => { + capturedAttachOptions.length = 0 + }) + + it('returns the read-write base scope set when nothing is overridden', () => { + expect(attachAndCapture().resolveScopes({ readOnly: false, flags: {} })).toEqual([ + 'data:read_write', + 'data:delete', + 'project:delete', + ]) + }) + + it('combines --read-only with --additional-scopes in canonical order', () => { + expect( + attachAndCapture().resolveScopes({ + readOnly: true, + flags: { additionalScopes: 'backups,app-management' }, + }), + ).toEqual(['data:read', 'dev:app_console', 'backups:read']) + }) +}) + +describe('attachTodoistLoginCommand: onSuccess output formatting', () => { + let consoleSpy: ReturnType + let errorSpy: ReturnType + + beforeEach(() => { + consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + }) + + afterEach(() => { + consoleSpy.mockRestore() + errorSpy.mockRestore() + capturedAttachOptions.length = 0 + }) + + it('prints the human "Signed in" confirmation in plain mode', async () => { + const opts = attachAndCapture() + await opts.onSuccess({ account: ACCOUNT, view: { json: false, ndjson: false }, flags: {} }) + + const printed = consoleSpy.mock.calls.map((c: unknown[]) => c.join(' ')).join('\n') + expect(printed).toContain('Signed in to Todoist as') + expect(printed).toContain('you@example.com') + }) + + it.each([ + ['--json', { json: true, ndjson: false }], + ['--ndjson', { json: false, ndjson: true }], + ])('emits a machine-output envelope in %s mode', async (_label, view) => { + const opts = attachAndCapture() + await opts.onSuccess({ account: ACCOUNT, view, flags: {} }) + + expect(consoleSpy).toHaveBeenCalledTimes(1) + const parsed = JSON.parse((consoleSpy.mock.calls[0][0] as string).trim()) + expect(parsed).toMatchObject({ + displayName: 'Todoist', + account: { id: ACCOUNT.id, email: ACCOUNT.email }, + }) + }) + + it('surfaces keyring-fallback warnings via stderr, keeping --json stdout clean', async () => { + const opts = attachAndCapture() + const warning = + 'system credential manager unavailable; token saved as plaintext in /tmp/c.json' + opts.store.getLastStorageResult = () => ({ storage: 'config-file', warning }) + + await opts.onSuccess({ account: ACCOUNT, view: { json: true, ndjson: false }, flags: {} }) + + // stdout carries only the JSON envelope; warning lands on stderr so it + // doesn't break consumers piping the output into `jq`. + expect(consoleSpy).toHaveBeenCalledTimes(1) + expect(() => JSON.parse(consoleSpy.mock.calls[0][0] as string)).not.toThrow() + expect(errorSpy.mock.calls.map((c: unknown[]) => c.join(' ')).join('\n')).toContain(warning) + }) +}) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index d6ce61be..a73b1187 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -1,63 +1,95 @@ +import { formatJson, formatNdjson } from '@doist/cli-core' +import { attachLoginCommand } from '@doist/cli-core/auth' import chalk from 'chalk' +import type { Command } from 'commander' import open from 'open' -import { createApiForToken } from '../../lib/api/core.js' -import { type AuthFlag, upsertUser } from '../../lib/auth.js' -import { type AdditionalScopeFlag, parseScopesOption } from '../../lib/oauth-scopes.js' -import { startCallbackServer } from '../../lib/oauth-server.js' -import { buildAuthorizationUrl, exchangeCodeForToken, resolveAuthScope } from '../../lib/oauth.js' -import { generateCodeChallenge, generateCodeVerifier, generateState } from '../../lib/pkce.js' +import { renderAuthErrorPage, renderAuthSuccessPage } from '../../lib/auth-html.js' +import { createTodoistAuthProvider } from '../../lib/auth-provider.js' +import { createTodoistTokenStore, type TodoistAccount } from '../../lib/auth-store.js' +import { + extractAdditionalScopes, + formatScopesHelp, + resolveAuthScope, +} from '../../lib/oauth-scopes.js' import { logTokenStorageResult } from './helpers.js' -export async function loginWithOAuth( - options: { readOnly?: boolean; additionalScopes?: string } = {}, -): Promise { - const additionalScopes: AdditionalScopeFlag[] = options.additionalScopes - ? parseScopesOption(options.additionalScopes) - : [] +const TODOIST_CALLBACK_PORT = 8765 +const TODOIST_CALLBACK_PORT_FALLBACK = 5 - const codeVerifier = generateCodeVerifier() - const codeChallenge = generateCodeChallenge(codeVerifier) - const state = generateState() +/** + * Attach `td auth login` via cli-core's generic `attachLoginCommand`. The + * registrar wires `--read-only`, `--callback-port`, `--json`, `--ndjson` and + * drives `runOAuthFlow`; the bits below stay todoist-local: scope resolution + * (comma-joined, custom validators), branded HTML, multi-user store via + * `createTodoistTokenStore`, and the human-mode success line. + * + * `--additional-scopes` is attached after the registrar so the option lands on + * the same Commander view; cli-core surfaces it through the `flags` argument + * to `resolveScopes`. + */ +export function attachTodoistLoginCommand(auth: Command): Command { + const store = createTodoistTokenStore() - console.log('Opening browser for Todoist authorization...') + const login = attachLoginCommand(auth, { + provider: createTodoistAuthProvider(), + store, + preferredPort: TODOIST_CALLBACK_PORT, + portFallbackCount: TODOIST_CALLBACK_PORT_FALLBACK, + resolveScopes: ({ readOnly, flags }) => { + const additionalScopes = extractAdditionalScopes(flags) + // resolveAuthScope returns the comma-separated string Todoist expects; + // split into the array cli-core's PKCE provider re-joins (the provider + // is configured with `scopeSeparator: ','`). + return resolveAuthScope({ readOnly, additionalScopes }).split(',') + }, + renderSuccess: renderAuthSuccessPage, + renderError: renderAuthErrorPage, + openBrowser: async (url) => { + await open(url) + }, + onSuccess: ({ account, view }) => { + const storage = store.getLastStorageResult() - const { promise: callbackPromise, port, cleanup } = await startCallbackServer(state) - const authUrl = buildAuthorizationUrl(codeChallenge, state, { - readOnly: options.readOnly, - additionalScopes, - port, - }) - - try { - await open(authUrl) - console.log(chalk.dim('Waiting for authorization...')) - - const code = await callbackPromise - console.log(chalk.dim('Exchanging code for token...')) + if (view.json) { + console.log(formatJson({ displayName: 'Todoist', account })) + } else if (view.ndjson) { + console.log(formatNdjson([{ displayName: 'Todoist', account }])) + } else { + const label = account.label ?? account.id + console.log(`${chalk.green('✓')} Signed in to Todoist as ${chalk.cyan(label)}`) + } - const accessToken = await exchangeCodeForToken(code, codeVerifier, port) - const authFlags: AuthFlag[] = [] - if (options.readOnly) authFlags.push('read-only') - authFlags.push(...additionalScopes) - - // Identify the user behind the new token before persisting. - const probeApi = createApiForToken(accessToken) - const user = await probeApi.getUser() - - const result = await upsertUser({ - id: user.id, - email: user.email, - token: accessToken, - authMode: options.readOnly ? 'read-only' : 'read-write', - authScope: resolveAuthScope({ readOnly: options.readOnly, additionalScopes }), - authFlags, - }) + // Surface keyring-fallback warnings regardless of view mode so a + // silent plaintext-storage fallback never goes unreported. + // `logTokenStorageResult` writes warnings to stderr, keeping the + // `--json` / `--ndjson` stdout envelope clean; the human "stored + // securely" confirmation is suppressed in machine-output mode. + if (storage) { + if (view.json || view.ndjson) { + if (storage.warning) { + console.error(chalk.yellow('Warning:'), storage.warning) + } + } else { + logTokenStorageResult( + storage, + 'Token stored securely in the system credential manager', + ) + } + } + }, + }) - const verb = result.replaced ? 'Updated credentials for' : 'Logged in as' - console.log(chalk.green('✓'), `${verb} ${user.email}`) - logTokenStorageResult(result, 'Token stored securely in the system credential manager') - } catch (error) { - cleanup() - throw error - } + return login + .description('Authenticate with Todoist via OAuth') + .option( + '--additional-scopes ', + 'Comma-separated opt-in OAuth scopes (see list below). The flag may be repeated; every occurrence is merged.', + // Commander treats this as a scalar by default, so repeated uses + // (`--additional-scopes=a --additional-scopes=b`) would silently drop + // earlier values. Concatenate into one comma-separated string and let + // parseScopesOption split/dedupe/validate as usual. + (value: string, prev: string | undefined) => + prev && prev.length > 0 ? `${prev},${value}` : value, + ) + .addHelpText('after', formatScopesHelp()) } diff --git a/src/commands/auth/token-view.ts b/src/commands/auth/token-view.ts new file mode 100644 index 00000000..ecd2ecd0 --- /dev/null +++ b/src/commands/auth/token-view.ts @@ -0,0 +1,18 @@ +import { resolveActiveUser, TOKEN_ENV_VAR } from '../../lib/auth.js' +import { CliError } from '../../lib/errors.js' + +export async function viewToken(): Promise { + if (process.env[TOKEN_ENV_VAR]) { + throw new CliError( + 'TOKEN_FROM_ENV', + `Refusing to print token: ${TOKEN_ENV_VAR} is set in the environment.`, + [ + `The token is already available in your environment as $${TOKEN_ENV_VAR}.`, + `Unset ${TOKEN_ENV_VAR} to print the stored token instead.`, + ], + ) + } + + const resolved = await resolveActiveUser() + console.log(resolved.token) +} diff --git a/src/commands/changelog.test.ts b/src/commands/changelog.test.ts index a971ab92..52a67c96 100644 --- a/src/commands/changelog.test.ts +++ b/src/commands/changelog.test.ts @@ -2,154 +2,62 @@ import { Command } from 'commander' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('node:fs/promises') -vi.mock('chalk') import { readFile } from 'node:fs/promises' +import packageJson from '../../package.json' with { type: 'json' } import { registerChangelogCommand } from './changelog.js' const mockReadFile = vi.mocked(readFile) const SAMPLE_CHANGELOG = `# Changelog -All notable changes to this project will be documented in this file. - -## [1.5.0](https://example.com) (2026-03-15) - -### Features -* feature five - -## [1.4.0](https://example.com) (2026-03-14) - -### Features -* feature four - -## [1.3.0](https://example.com) (2026-03-13) - -### Bug Fixes -* fix three - -## [1.2.0](https://example.com) (2026-03-12) - -### Features -* feature two - -## [1.1.0](https://example.com) (2026-03-11) +## [9.9.0](https://example.com) (2026-05-09) ### Features -* feature one +* delegated to cli-core -## [1.0.0](https://example.com) (2026-03-10) +## [9.8.0](https://example.com) (2026-05-08) ### Features -* initial release +* prior release ` -function createProgram() { - const program = new Command() - program.exitOverride() - registerChangelogCommand(program) - return program -} - -describe('changelog command', () => { - let consoleSpy: ReturnType - let consoleErrorSpy: ReturnType +describe('changelog wrapper', () => { + let logSpy: ReturnType beforeEach(() => { - consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - process.exitCode = undefined + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) }) afterEach(() => { vi.restoreAllMocks() - process.exitCode = undefined + mockReadFile.mockReset() }) - it('shows last 5 versions by default', async () => { + it('passes the todoist CHANGELOG.md path through to cli-core', async () => { mockReadFile.mockResolvedValue(SAMPLE_CHANGELOG) + const program = new Command() + program.exitOverride() + registerChangelogCommand(program) - const program = createProgram() - await program.parseAsync(['node', 'td', 'changelog']) + await program.parseAsync(['node', 'td', 'changelog', '-n', '1']) - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('1.5.0')) - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('1.1.0')) - // Should show "view full changelog" link since there are 6 versions - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('View full changelog')) + expect(mockReadFile).toHaveBeenCalledTimes(1) + const [path] = mockReadFile.mock.calls[0] + expect(String(path)).toMatch(/\/CHANGELOG\.md$/) }) - it('includes latest version when changelog has no preamble', async () => { - const noPreambleChangelog = `## [2.0.0](https://example.com) (2026-03-20) - -### Features -* new major version - -## [1.5.0](https://example.com) (2026-03-15) - -### Features -* feature five -` - mockReadFile.mockResolvedValue(noPreambleChangelog) - - const program = createProgram() - await program.parseAsync(['node', 'td', 'changelog']) - - const output = consoleSpy.mock.calls[0][0] as string - expect(output).toContain('2.0.0') - expect(output).toContain('1.5.0') - }) - - it('respects --count option', async () => { + it('emits a footer link pointing at the todoist repo and current version', async () => { mockReadFile.mockResolvedValue(SAMPLE_CHANGELOG) + const program = new Command() + program.exitOverride() + registerChangelogCommand(program) - const program = createProgram() - await program.parseAsync(['node', 'td', 'changelog', '-n', '2']) - - const output = consoleSpy.mock.calls[0][0] as string - expect(output).toContain('1.5.0') - expect(output).toContain('1.4.0') - expect(output).not.toContain('1.3.0') - }) - - it('handles fewer entries than requested', async () => { - const shortChangelog = `# Changelog - -## [1.1.0](https://example.com) (2026-03-11) - -### Features -* only version -` - mockReadFile.mockResolvedValue(shortChangelog) - - const program = createProgram() - await program.parseAsync(['node', 'td', 'changelog']) - - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('1.1.0')) - // Should NOT show "view full changelog" link since all versions are shown - expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining('View full changelog')) - }) - - it('handles missing changelog file', async () => { - mockReadFile.mockRejectedValue(new Error('ENOENT')) - - const program = createProgram() - await program.parseAsync(['node', 'td', 'changelog']) - - expect(consoleErrorSpy).toHaveBeenCalledWith( - expect.anything(), - expect.stringContaining('Could not read changelog file'), - ) - expect(process.exitCode).toBe(1) - }) - - it('handles invalid count', async () => { - const program = createProgram() - await program.parseAsync(['node', 'td', 'changelog', '-n', 'abc']) + await program.parseAsync(['node', 'td', 'changelog', '-n', '1']) - expect(consoleErrorSpy).toHaveBeenCalledWith( - expect.anything(), - expect.stringContaining('Count must be a positive number'), + const all = logSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n') + expect(all).toContain( + `View full changelog: https://github.com/Doist/todoist-cli/blob/v${packageJson.version}/CHANGELOG.md`, ) - expect(process.exitCode).toBe(1) }) }) diff --git a/src/commands/changelog.ts b/src/commands/changelog.ts index e682b3b1..3632a006 100644 --- a/src/commands/changelog.ts +++ b/src/commands/changelog.ts @@ -1,109 +1,15 @@ -import { readFile } from 'node:fs/promises' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' -import chalk from 'chalk' -import { Command } from 'commander' +import { registerChangelogCommand as registerCoreChangelogCommand } from '@doist/cli-core/commands' +import type { Command } from 'commander' import packageJson from '../../package.json' with { type: 'json' } const CHANGELOG_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'CHANGELOG.md') -const CHANGELOG_URL = `https://github.com/Doist/todoist-cli/blob/v${packageJson.version}/CHANGELOG.md` - -function formatInline(text: string): string { - return text - .replace(/\*\*([^*]+)\*\*/g, (_, content) => chalk.bold(content)) - .replace(/`([^`]+)`/g, (_, code) => chalk.cyan(code)) -} - -function formatForTerminal(text: string): string { - return text - .split('\n') - .map((line) => { - // Version headers: ## 1.25.0 (date) → bold green - if (line.startsWith('## ')) { - return chalk.green.bold(line.slice(3)) - } - // Section headers: ### Features → bold - if (line.startsWith('### ')) { - return chalk.bold(line.slice(4)) - } - // Bullet items: * description → dimmed bullet + text - if (line.startsWith('* ')) { - return ` ${chalk.dim('•')} ${formatInline(line.slice(2))}` - } - return formatInline(line) - }) - .join('\n') -} - -function cleanChangelog(text: string): string { - return ( - text - // Version headers: ## [1.25.0](https://...) (date) → ## 1.25.0 (date) - .replace(/## \[([^\]]+)\]\([^)]*\)/g, '## $1') - // Remove commit hash links: ([abc1234](https://...)) - .replace(/ \([a-f0-9]{7}\)/g, '') - .replace(/ \(\[[a-f0-9]{7}\]\([^)]*\)\)/g, '') - // Issue/PR links: [#151](https://...) → #151 - .replace(/\[#(\d+)\]\([^)]*\)/g, '#$1') - // Remove **deps:** dependency update lines (not useful to end users) - .replace(/^\* \*\*deps:\*\*.*$/gm, '') - // Remove **scope:** prefixes but keep the text: **task:** foo → foo - .replace(/\*\*[\w-]+:\*\* /g, '') - // Clean up blank lines left by removed dep lines - .replace(/\n{3,}/g, '\n\n') - // Remove section headers left empty after filtering (e.g. ### Bug Fixes with no items) - .replace(/### [\w ]+\n\n(?=##|$)/gm, '') - ) -} - -function parseChangelog(content: string, count: number): { text: string; hasMore: boolean } { - const sections = content.split(/\n(?=## \[)/) - const versionSections = sections.filter((s) => s.startsWith('## [')) - const selected = versionSections.slice(0, count) - - if (selected.length === 0) { - return { text: 'No changelog entries found.', hasMore: false } - } - - return { - text: cleanChangelog(selected.join('\n').trimEnd()), - hasMore: versionSections.length > count, - } -} - -interface ChangelogOptions { - count: string -} - -export async function changelogAction(options: ChangelogOptions): Promise { - const count = parseInt(options.count, 10) - if (isNaN(count) || count < 1) { - console.error(chalk.red('Error:'), 'Count must be a positive number') - process.exitCode = 1 - return - } - - let content: string - try { - content = await readFile(CHANGELOG_PATH, 'utf-8') - } catch { - console.error(chalk.red('Error:'), 'Could not read changelog file') - process.exitCode = 1 - return - } - - const { text, hasMore } = parseChangelog(content, count) - console.log(formatForTerminal(text)) - - if (hasMore) { - console.log(chalk.dim(`\nView full changelog: ${CHANGELOG_URL}`)) - } -} export function registerChangelogCommand(program: Command): void { - program - .command('changelog') - .description('Show recent changelog entries') - .option('-n, --count ', 'Number of versions to show', '5') - .action(changelogAction) + registerCoreChangelogCommand(program, { + path: CHANGELOG_PATH, + repoUrl: 'https://github.com/Doist/todoist-cli', + version: packageJson.version, + }) } diff --git a/src/commands/config/config.test.ts b/src/commands/config/config.test.ts index 49c693cf..d4f2581b 100644 --- a/src/commands/config/config.test.ts +++ b/src/commands/config/config.test.ts @@ -2,7 +2,7 @@ import { Command } from 'commander' import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('../../lib/config.js', () => ({ - CONFIG_PATH: '/tmp/fake-todoist-cli/config.json', + getConfigPath: () => '/tmp/fake-todoist-cli/config.json', readConfigStrict: vi.fn(), readConfig: vi.fn().mockResolvedValue({}), })) diff --git a/src/commands/config/view.ts b/src/commands/config/view.ts index d48da2bb..fb23d436 100644 --- a/src/commands/config/view.ts +++ b/src/commands/config/view.ts @@ -7,7 +7,7 @@ import { type StoredUser, TOKEN_ENV_VAR, } from '../../lib/auth.js' -import { type Config, CONFIG_PATH, readConfigStrict } from '../../lib/config.js' +import { type Config, getConfigPath, readConfigStrict } from '../../lib/config.js' import { SECURE_STORE_DESCRIPTION, SecureStoreUnavailableError } from '../../lib/secure-store.js' import { getDefaultUserId, NoUserSelectedError } from '../../lib/users.js' @@ -99,7 +99,7 @@ function formatConfigView( defaultUserId: string | undefined, ): string { const lines: string[] = [] - lines.push(`${chalk.dim('Config file:')} ${CONFIG_PATH}`) + lines.push(`${chalk.dim('Config file:')} ${getConfigPath()}`) lines.push('') // Active-user line: who would the next command run as? @@ -214,7 +214,9 @@ export async function viewConfig(options: ViewConfigOptions): Promise { const defaultUserId = getDefaultUserId(config) if (read.state === 'missing' && token.state === 'missing' && users.length === 0) { - console.log(`${chalk.dim('Config file:')} ${CONFIG_PATH} ${chalk.dim('(not created yet)')}`) + console.log( + `${chalk.dim('Config file:')} ${getConfigPath()} ${chalk.dim('(not created yet)')}`, + ) return } diff --git a/src/commands/doctor.test.ts b/src/commands/doctor.test.ts index 117483b7..3afdb4d5 100644 --- a/src/commands/doctor.test.ts +++ b/src/commands/doctor.test.ts @@ -56,7 +56,7 @@ vi.mock('../lib/auth.js', async (importOriginal) => { const actual = await importOriginal() return { ...actual, - CONFIG_PATH: '/tmp/test-config.json', + getConfigPath: () => '/tmp/test-config.json', probeApiToken: vi.fn(), } }) diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index c40bdc59..c220b91f 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -4,7 +4,7 @@ import { Command } from 'commander' import packageJson from '../../package.json' with { type: 'json' } import { createApiForToken } from '../lib/api/core.js' import { - CONFIG_PATH, + getConfigPath, listStoredUsers, NoTokenError, readConfig, @@ -113,16 +113,17 @@ function checkNodeVersion(): DoctorCheck | null { } async function checkConfigFile(): Promise { + const path = getConfigPath() try { - const content = await readFile(CONFIG_PATH, 'utf-8') + const content = await readFile(path, 'utf-8') const parsed = JSON.parse(content) if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { return { name: 'config', status: 'fail', - message: `Config file must contain a JSON object (${CONFIG_PATH})`, - details: { path: CONFIG_PATH }, + message: `Config file must contain a JSON object (${path})`, + details: { path }, } } @@ -133,9 +134,9 @@ async function checkConfigFile(): Promise { status: issues.length > 0 ? 'warn' : 'pass', message: issues.length > 0 - ? `Config file is readable but ${issues.join('; ')} (${CONFIG_PATH})` - : `Config file is readable (${CONFIG_PATH})`, - details: { path: CONFIG_PATH, exists: true, issues }, + ? `Config file is readable but ${issues.join('; ')} (${path})` + : `Config file is readable (${path})`, + details: { path, exists: true, issues }, } } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { @@ -146,8 +147,8 @@ async function checkConfigFile(): Promise { return { name: 'config', status: 'fail', - message: `Could not read config file ${CONFIG_PATH}: ${message}`, - details: { path: CONFIG_PATH }, + message: `Could not read config file ${path}: ${message}`, + details: { path }, } } } diff --git a/src/commands/folder/index.ts b/src/commands/folder/index.ts index 96456fb4..6bd146fa 100644 --- a/src/commands/folder/index.ts +++ b/src/commands/folder/index.ts @@ -2,22 +2,13 @@ import { Command } from 'commander' import { CURSOR_DESCRIPTION } from '../../lib/constants.js' import { CliError } from '../../lib/errors.js' import type { PaginatedViewOptions } from '../../lib/options.js' +import { parseOrderArg } from '../../lib/order.js' import { createFolder } from './create.js' import { deleteFolder } from './delete.js' import { listFolders } from './list.js' import { updateFolder } from './update.js' import { viewFolder } from './view.js' -function parseOrderArg(val: string): number { - const n = Number(val) - if (!Number.isInteger(n) || n < 0) { - throw new CliError('INVALID_ORDER', `Invalid order value: "${val}"`, [ - 'Order must be a non-negative integer (e.g., 0 for top of list)', - ]) - } - return n -} - export function registerFolderCommand(program: Command): void { const folder = program .command('folder') diff --git a/src/commands/hc/hc.test.ts b/src/commands/hc/hc.test.ts index 2d7f26fc..8cbed8d8 100644 --- a/src/commands/hc/hc.test.ts +++ b/src/commands/hc/hc.test.ts @@ -1,3 +1,4 @@ +import { describeEmptyMachineOutput } from '@doist/cli-core/testing' import { Command } from 'commander' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -83,6 +84,55 @@ describe('hc command', () => { ) }) + describeEmptyMachineOutput('hc search empty machine output contract', { + setup: () => { + fetchSpy.mockResolvedValue(createJsonResponse({ results: [] })) + }, + run: async (extraArgs) => { + await createProgram().parseAsync(['node', 'td', 'hc', 'search', 'nope', ...extraArgs]) + }, + humanMessage: 'No Help Center articles found for "nope".', + }) + + it('emits one JSON value per line for --ndjson with results', async () => { + fetchSpy.mockResolvedValue( + createJsonResponse({ + results: [ + { + id: 205348301, + title: 'Set reminders for your tasks', + html_url: + 'https://get.todoist.help/hc/en-us/articles/205348301-set-reminders', + snippet: 'reminder snippet', + body: '

Body excluded from --ndjson

', + }, + { + id: 360000269065, + title: 'Manage your notifications in Todoist', + html_url: + 'https://get.todoist.help/hc/en-us/articles/360000269065-manage-your-notifications', + snippet: 'notifications snippet', + body: '

Body excluded from --ndjson

', + }, + ], + }), + ) + + const program = createProgram() + await program.parseAsync(['node', 'td', 'hc', 'search', 'todoist', '--ndjson']) + + const output = consoleSpy.mock.calls[0][0] as string + const lines = output.split('\n') + expect(lines).toHaveLength(2) + const first = JSON.parse(lines[0]) + const second = JSON.parse(lines[1]) + expect(first.id).toBe('205348301') + expect(first.title).toBe('Set reminders for your tasks') + expect(first).not.toHaveProperty('body') + expect(second.id).toBe('360000269065') + expect(output.endsWith('\n')).toBe(false) + }) + it('returns bounded JSON search results without article bodies', async () => { fetchSpy.mockResolvedValue( createJsonResponse({ diff --git a/src/commands/hc/index.ts b/src/commands/hc/index.ts index 26f15e64..0cce74d4 100644 --- a/src/commands/hc/index.ts +++ b/src/commands/hc/index.ts @@ -52,6 +52,7 @@ Notes: .option('--locale ', LOCALE_OPTION_DESCRIPTION) .option('--limit ', 'Number of results to return (default: 10, max: 25)') .option('--json', 'Output as JSON') + .option('--ndjson', 'Output as newline-delimited JSON') .action((query, options) => { if (!query) { searchCmd.help() diff --git a/src/commands/hc/search.ts b/src/commands/hc/search.ts index 45b89628..4160c9d4 100644 --- a/src/commands/hc/search.ts +++ b/src/commands/hc/search.ts @@ -1,3 +1,4 @@ +import { formatJson, formatNdjson, printEmpty } from '@doist/cli-core' import chalk from 'chalk' import { searchHelpCenter } from '../../lib/help-center.js' import { withSpinner } from '../../lib/spinner.js' @@ -5,6 +6,7 @@ import { resolveDefaultHelpCenterLocale } from './locale.js' export interface SearchHelpCenterOptions { json?: boolean + ndjson?: boolean limit?: string locale?: string } @@ -19,13 +21,18 @@ export async function searchHelpCenterArticles( searchHelpCenter(trimmedQuery, { locale, limit: options.limit }), ) + if (results.length === 0) { + printEmpty({ options, message: `No Help Center articles found for "${trimmedQuery}".` }) + return + } + if (options.json) { - console.log(JSON.stringify(results, null, 2)) + console.log(formatJson(results)) return } - if (results.length === 0) { - console.log(`No Help Center articles found for "${trimmedQuery}".`) + if (options.ndjson) { + console.log(formatNdjson(results)) return } diff --git a/src/commands/project/create.ts b/src/commands/project/create.ts index 29cd753e..2a34e642 100644 --- a/src/commands/project/create.ts +++ b/src/commands/project/create.ts @@ -1,10 +1,9 @@ -import { isWorkspaceProject, type ColorKey, type ProjectViewStyle } from '@doist/todoist-sdk' +import type { ColorKey, ProjectViewStyle } from '@doist/todoist-sdk' import chalk from 'chalk' import { getApi } from '../../lib/api/core.js' -import { CliError } from '../../lib/errors.js' import { isQuiet } from '../../lib/global-args.js' import { formatJson, printDryRun } from '../../lib/output.js' -import { resolveProjectRef } from '../../lib/refs.js' +import { resolvePersonalParent } from './helpers.js' export interface CreateOptions { name: string @@ -32,14 +31,7 @@ export async function createProject(options: CreateOptions): Promise { let parentId: string | undefined if (options.parent) { - const parentProject = await resolveProjectRef(api, options.parent) - if (isWorkspaceProject(parentProject)) { - throw new CliError( - 'WORKSPACE_NO_SUBPROJECTS', - 'Workspace projects do not support sub-projects.', - ['Sub-projects are only supported for personal projects.'], - ) - } + const parentProject = await resolvePersonalParent(api, options.parent) parentId = parentProject.id } diff --git a/src/commands/project/helpers.ts b/src/commands/project/helpers.ts index 8c9615d0..8150e05f 100644 --- a/src/commands/project/helpers.ts +++ b/src/commands/project/helpers.ts @@ -1,3 +1,109 @@ -import type { ProjectViewStyle } from '@doist/todoist-sdk' +import { + isWorkspaceProject, + type PersonalProject, + type ProjectViewStyle, + type TodoistApi, +} from '@doist/todoist-sdk' +import type { Project } from '../../lib/api/core.js' +import { CliError } from '../../lib/errors.js' +import { paginate } from '../../lib/pagination.js' +import { resolveProjectRef } from '../../lib/refs.js' export const VIEW_STYLE_CHOICES: ProjectViewStyle[] = ['list', 'board', 'calendar'] + +/** + * Resolve a project reference for use as a parent. Rejects workspace projects + * — only personal projects can have sub-projects. + */ +export async function resolvePersonalParent( + api: TodoistApi, + parentRef: string, +): Promise { + const parentProject = await resolveProjectRef(api, parentRef) + if (isWorkspaceProject(parentProject)) { + throw new CliError( + 'WORKSPACE_NO_SUBPROJECTS', + 'Workspace projects cannot be used as a parent.', + ['Sub-projects are only supported under personal projects.'], + ) + } + return parentProject +} + +/** + * Load every personal project the user has access to (paginated). Used by + * commands that need to traverse the project hierarchy in memory rather than + * making N round trips. + */ +export async function loadPersonalProjects(api: TodoistApi): Promise { + const { results } = await paginate( + (cursor, limit) => api.getProjects({ cursor: cursor ?? undefined, limit }), + { limit: Number.MAX_SAFE_INTEGER, startCursor: undefined }, + ) + return results.filter((p): p is PersonalProject => !isWorkspaceProject(p)) +} + +/** + * Returns true if `candidateId` is a descendant of `ancestorId` within the + * given project set. Walks the parent chain in memory; bails on cycles. + */ +export function isDescendantOf( + projects: PersonalProject[], + candidateId: string, + ancestorId: string, +): boolean { + const byId = new Map(projects.map((p) => [p.id, p])) + let current = byId.get(candidateId) + const visited = new Set() + while (current?.parentId && !visited.has(current.id)) { + visited.add(current.id) + if (current.parentId === ancestorId) return true + current = byId.get(current.parentId) + } + return false +} + +export function isPersonal(p: Project): p is PersonalProject { + return !isWorkspaceProject(p) +} + +/** + * Resolve a project reference against an in-memory personal-project list, + * avoiding extra round trips when the caller has already paginated the + * full list (e.g. `project reorder`). Mirrors the matching rules of + * `resolveProjectRef` (id prefix → exact name → substring name) but is + * scoped to personal projects only. + */ +export function resolvePersonalFromList(projects: PersonalProject[], ref: string): PersonalProject { + if (!ref.trim()) { + throw new CliError('INVALID_PROJECT', 'project reference cannot be empty.') + } + if (ref.startsWith('id:')) { + const id = ref.slice(3) + const match = projects.find((p) => p.id === id) + if (!match) { + throw new CliError('PROJECT_NOT_FOUND', `Personal project "${ref}" not found.`) + } + return match + } + const lower = ref.toLowerCase() + const exact = projects.filter((p) => p.name.toLowerCase() === lower) + if (exact.length === 1) return exact[0] + if (exact.length > 1) { + throw new CliError( + 'AMBIGUOUS_PROJECT', + `Multiple projects match "${ref}" exactly:`, + exact.slice(0, 5).map((p) => `"${p.name}" (id:${p.id})`), + ) + } + const partial = projects.filter((p) => p.name.toLowerCase().includes(lower)) + if (partial.length === 1) return partial[0] + if (partial.length > 1) { + throw new CliError( + 'AMBIGUOUS_PROJECT', + `Multiple projects match "${ref}":`, + partial.slice(0, 5).map((p) => `"${p.name}" (id:${p.id})`), + ) + } + throw new CliError('PROJECT_NOT_FOUND', `Personal project "${ref}" not found.`) +} diff --git a/src/commands/project/index.ts b/src/commands/project/index.ts index a5f16a52..ad1a62b7 100644 --- a/src/commands/project/index.ts +++ b/src/commands/project/index.ts @@ -1,6 +1,7 @@ import { Command, Option } from 'commander' import { withCaseInsensitiveChoices } from '../../lib/completion.js' import { CURSOR_DESCRIPTION } from '../../lib/constants.js' +import { parseOrderArg } from '../../lib/order.js' import { showProjectActivityStats } from './activity-stats.js' import { analyzeHealth } from './analyze-health.js' import { archiveProject } from './archive.js' @@ -18,6 +19,7 @@ import { listProjects } from './list.js' import { moveProject } from './move.js' import { showPermissions } from './permissions.js' import { showProjectProgress } from './progress.js' +import { reorderProject } from './reorder.js' import { unarchiveProject } from './unarchive.js' import { updateProject } from './update.js' import { viewProject } from './view.js' @@ -126,6 +128,8 @@ Examples: .option('--no-favorite', 'Remove from favorites') .option('--folder ', 'Move into a folder by name or id:xxx (workspace projects only)') .option('--no-folder', 'Remove from folder (workspace projects only)') + .option('--parent ', 'Re-nest under a personal parent project (name or id:xxx)') + .option('--no-parent', 'Move to top level (no parent)') .addOption( withCaseInsensitiveChoices( new Option('--view-style