From 1feae375086fef1679925043b4c5eca1eefc6380 Mon Sep 17 00:00:00 2001 From: Markeljan Sokoli Date: Thu, 5 Mar 2026 15:37:19 -0500 Subject: [PATCH] Update npm publishing settings --- .changeset/README.md | 8 + .changeset/config.json | 11 ++ .changeset/green-bulldogs-camp.md | 5 + .github/workflows/ci.yml | 35 ++++ .github/workflows/release.yml | 53 ++++++ README.md | 103 +++++----- bin/tcomp.cjs | 4 +- biome.jsonc | 7 + build.ts | 12 +- bun.lock | 294 +++++++++++++++++++++++++++++ docs/releasing.md | 34 ++++ lefthook.yml | 12 ++ package.json | 25 ++- src/args.ts | 35 +++- src/cli.ts | 174 +++++++++++------ src/codex-auth.ts | 48 +++-- src/globals.d.ts | 1 - src/interactive.ts | 36 ++-- src/openai.ts | 246 +++++++++++++++--------- src/prompt.ts | 8 +- src/shell-install.ts | 21 ++- src/shell.ts | 6 + src/types.ts | 6 +- src/user-config.ts | 22 ++- src/version.ts | 2 +- test/args.test.ts | 12 +- test/config-and-auth-paths.test.ts | 50 +++-- test/shell-install.test.ts | 21 ++- test/shell.test.ts | 20 +- tsconfig.json | 1 - 30 files changed, 1018 insertions(+), 294 deletions(-) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .changeset/green-bulldogs-camp.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 biome.jsonc create mode 100644 bun.lock create mode 100644 docs/releasing.md create mode 100644 lefthook.yml diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..654c6d4 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets). + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md). diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..ba0f177 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.3/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/green-bulldogs-camp.md b/.changeset/green-bulldogs-camp.md new file mode 100644 index 0000000..5ffa98d --- /dev/null +++ b/.changeset/green-bulldogs-camp.md @@ -0,0 +1,5 @@ +--- +"terminal-complete": patch +--- + +Adopt Ultracite with Biome + Lefthook, apply lint-driven source updates, and add automated Changesets-based release/publish workflows for npm with Trusted Publishing (OIDC). Also add richer npm metadata and GitHub links. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..72a85b1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint and type/style checks + run: bun run check + + - name: Run tests + run: bun test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c09b0e4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: Release + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + id-token: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node (for npm publish) + uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Validate before release + run: bun run ci + + - name: Create release PR or publish + uses: changesets/action@v1 + with: + version: bun run version-packages + publish: bun run release + commit: "ci: version packages" + title: "ci: version packages" + createGithubReleases: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_CONFIG_PROVENANCE: true diff --git a/README.md b/README.md index 541752b..68e18d4 100644 --- a/README.md +++ b/README.md @@ -2,63 +2,59 @@ AI CLI for turning natural language into terminal commands with `tcomp`. -## Requirements +[![npm version](https://img.shields.io/npm/v/terminal-complete)](https://www.npmjs.com/package/terminal-complete) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/license/mit/) +[![CI](https://github.com/Markeljan/terminal-complete/actions/workflows/ci.yml/badge.svg)](https://github.com/Markeljan/terminal-complete/actions/workflows/ci.yml) -- `zsh` or `bash` -- `bun` (runtime required by npm-installed `tcomp` launcher) +## Install (Bun) -## Build binary +Global install: ```bash -bun run build.ts +bun add -g terminal-complete +tcomp --help ``` -Outputs: - -- `dist/terminal-complete` -- `dist/tcomp` - -## Run tests +One-off usage without install: ```bash -bun test +bunx terminal-complete --help ``` -## Local install for testing +## Links -Option A (npm-style): +- npm package: https://www.npmjs.com/package/terminal-complete +- GitHub repository: https://github.com/Markeljan/terminal-complete +- Issue tracker: https://github.com/Markeljan/terminal-complete/issues -```bash -npm link -tcomp --help -``` +## Requirements + +- `zsh` or `bash` +- `bun` -Option B (binary symlink): +## Quick Start + +First run setup: ```bash -mkdir -p "$HOME/bin" -ln -sf "$(pwd)/dist/tcomp" "$HOME/bin/tcomp" -ln -sf "$(pwd)/dist/terminal-complete" "$HOME/bin/terminal-complete" -export PATH="$HOME/bin:$PATH" +tcomp ``` -## First run - -On first install, running `tcomp` (or any `tcomp `) starts setup automatically. +Or run setup directly: ```bash -tcomp +tcomp setup ``` Setup flow: -- Shows welcome and requirement checks. -- Offers shell integration install first (`Y/n`, default is `Yes`). -- Lets you choose provider auth: - - `codex` (OpenAI OAuth via Codex CLI, with browser or device login) - - `openai` (API key) +- checks environment and shell support +- offers shell integration install +- sets up provider auth: + - `codex`: OpenAI OAuth via Codex CLI (browser or device flow) + - `openai`: OpenAI API key -After shell integration install, `tcomp` prints exactly what to run to activate it in your current shell: +After shell integration install, run: ```bash # zsh @@ -68,7 +64,7 @@ source ~/.zshrc source ~/.bashrc ``` -## Practical usage examples +## Common Usage ```bash tcomp find all files larger than 500MB under this directory @@ -77,21 +73,34 @@ tcomp show git commits from last 7 days grouped by author tcomp -e safely remove docker images that are dangling ``` -General assistant mode: +General assistant (non-command) mode: ```bash tcomp -p explain when to use rsync vs scp ``` -## Provider management +## Setup and Config Commands + +Run full onboarding: + +```bash +tcomp setup +``` + +Run onboarding for a specific provider: + +```bash +tcomp setup codex +tcomp setup openai +``` -Show active provider and available actions: +Show current configuration/status: ```bash tcomp config ``` -Run setup for a specific provider: +Re-run provider auth/config: ```bash tcomp config codex @@ -105,20 +114,22 @@ tcomp use codex tcomp use openai ``` +## Common Flags + +- `--explain`, `-e`: print explanation/risk to stderr +- `--prompt`, `-p`: general assistant mode (not command generation) +- `--help`, `-h`: show help +- `--version`, `-v`: show version + ## Commands +- `tcomp ` - `tcomp setup [codex|openai]` - `tcomp config [codex|openai]` - `tcomp use ` - `tcomp help` - `tcomp version` -## npm publish checklist - -```bash -bun test -npm pack --dry-run -npm publish --access public -``` +## Maintainers -If you also want standalone binaries for release artifacts, run `bun run build.ts` separately and upload `dist/tcomp` + `dist/terminal-complete`. +Release and npm publishing docs: `docs/releasing.md` diff --git a/bin/tcomp.cjs b/bin/tcomp.cjs index 71fb9e6..e681f61 100755 --- a/bin/tcomp.cjs +++ b/bin/tcomp.cjs @@ -14,7 +14,9 @@ const result = spawnSync("bun", [cliEntrypoint, ...args], { if (result.error) { if (result.error.code === "ENOENT") { - console.error("tcomp requires Bun runtime. Install Bun: https://bun.sh/docs/installation"); + console.error( + "tcomp requires Bun runtime. Install Bun: https://bun.sh/docs/installation" + ); process.exit(1); } console.error(result.error.message); diff --git a/biome.jsonc b/biome.jsonc new file mode 100644 index 0000000..288103b --- /dev/null +++ b/biome.jsonc @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "extends": ["ultracite/biome/core"], + "javascript": { + "globals": ["Bun", "BUILD_VERSION", "BUILD_TIME"] + } +} diff --git a/build.ts b/build.ts index 4588b6f..1a1cb64 100644 --- a/build.ts +++ b/build.ts @@ -1,5 +1,5 @@ -import { basename } from "node:path"; import { mkdir } from "node:fs/promises"; +import { basename } from "node:path"; type BuildTarget = | "bun-darwin-x64" @@ -17,8 +17,8 @@ type BuildTarget = | "bun-windows-arm64"; interface BuildCliOptions { - target?: BuildTarget; debug: boolean; + target?: BuildTarget; } function parseBuildOptions(argv: string[]): BuildCliOptions { @@ -45,8 +45,12 @@ function parseBuildOptions(argv: string[]): BuildCliOptions { return options; } -async function compileBinary(outfile: string, target: BuildTarget | undefined, debug: boolean) { - const pkg = await Bun.file("./package.json").json() as { version?: string }; +async function compileBinary( + outfile: string, + target: BuildTarget | undefined, + debug: boolean +) { + const pkg = (await Bun.file("./package.json").json()) as { version?: string }; const version = pkg.version ?? "0.0.0-dev"; const buildTime = new Date().toISOString(); diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..df68a4c --- /dev/null +++ b/bun.lock @@ -0,0 +1,294 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "terminal-complete", + "devDependencies": { + "@biomejs/biome": "2.4.0", + "@changesets/cli": "^2.30.0", + "lefthook": "^2.1.2", + "ultracite": "7.2.4", + }, + }, + }, + "packages": { + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@biomejs/biome": ["@biomejs/biome@2.4.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.0", "@biomejs/cli-darwin-x64": "2.4.0", "@biomejs/cli-linux-arm64": "2.4.0", "@biomejs/cli-linux-arm64-musl": "2.4.0", "@biomejs/cli-linux-x64": "2.4.0", "@biomejs/cli-linux-x64-musl": "2.4.0", "@biomejs/cli-win32-arm64": "2.4.0", "@biomejs/cli-win32-x64": "2.4.0" }, "bin": { "biome": "bin/biome" } }, "sha512-iluT61cORUDIC5i/y42ljyQraCemmmcgbMLLCnYO+yh+2hjTmcMFcwY8G0zTzWCsPb3t3AyKc+0t/VuhPZULUg=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-L+YpOtPSuU0etomfvFTPWRsa7+8ejaJL3yaROEoT/96HDJbR6OsvZQk0C8JUYou+XFdP+JcGxqZknkp4n934RA=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Aq+S7ffpb5ynTyLgtnEjG+W6xuTd2F7FdC7J6ShpvRhZwJhjzwITGF9vrqoOnw0sv1XWkt2Q1Rpg+hleg/Xg7Q=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-u2p54IhvNAWB+h7+rxCZe3reNfQYFK+ppDw+q0yegrGclFYnDPZAntv/PqgUacpC3uxTeuWFgWW7RFe3lHuxOA=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1rhDUq8sf7xX3tg7vbnU3WVfanKCKi40OXc4VleBMzRStmQHdeBY46aFP6VdwEomcVjyNiu+Zcr3LZtAdrZrjQ=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WVFOhsnzhrbMGOSIcB9yFdRV2oG2KkRRhIZiunI9gJqSU3ax9ErdnTxRfJUxZUI9NbzVxC60OCXNcu+mXfF/Tw=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Omo0xhl63z47X+CrE5viEWKJhejJyndl577VoXg763U/aoATrK3r5+8DPh02GokWPeODX1Hek00OtjjooGan9w=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-aqRwW0LJLV1v1NzyLvLWQhdLmDSAV1vUh+OBdYJaa8f28XBn5BZavo+WTfqgEzALZxlNfBmu6NGO6Al3MbCULw=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-g47s+V+OqsGxbSZN3lpav6WYOk0PIc3aCBAq+p6dwSynL3K5MA6Cg6nkzDOlu28GEHwbakW+BllzHCJCxnfK5Q=="], + + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.0", "", { "dependencies": { "@changesets/config": "^3.1.3", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ=="], + + "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.9", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ=="], + + "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="], + + "@changesets/cli": ["@changesets/cli@2.30.0", "", { "dependencies": { "@changesets/apply-release-plan": "^7.1.0", "@changesets/assemble-release-plan": "^6.0.9", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.3", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/get-release-plan": "^4.0.15", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA=="], + + "@changesets/config": ["@changesets/config@3.1.3", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/logger": "^0.1.1", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw=="], + + "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="], + + "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ=="], + + "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.15", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.9", "@changesets/config": "^3.1.3", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g=="], + + "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], + + "@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="], + + "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="], + + "@changesets/parse": ["@changesets/parse@0.4.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^4.1.1" } }, "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A=="], + + "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="], + + "@changesets/read": ["@changesets/read@0.6.7", "", { "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.3", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA=="], + + "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="], + + "@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="], + + "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], + + "@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="], + + "@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="], + + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + + "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], + + "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + + "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + + "citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], + + "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "lefthook": ["lefthook@2.1.2", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.1.2", "lefthook-darwin-x64": "2.1.2", "lefthook-freebsd-arm64": "2.1.2", "lefthook-freebsd-x64": "2.1.2", "lefthook-linux-arm64": "2.1.2", "lefthook-linux-x64": "2.1.2", "lefthook-openbsd-arm64": "2.1.2", "lefthook-openbsd-x64": "2.1.2", "lefthook-windows-arm64": "2.1.2", "lefthook-windows-x64": "2.1.2" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-HdAMl4g47kbWSkrUkCx3Kucq54omFS6piMJtXwXNtmCAfB40UaybTJuYtFW4hNzZ5SvaEimtxTp7P/MNIkEfsA=="], + + "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AgHu93YuJtj1l9bcKlCbo4Tg8N8xFl9iD6BjXCGaGMu46LSjFiXbJFlkUdpgrL8fIbwoCjJi5FNp3POpqs4Wdw=="], + + "lefthook-darwin-x64": ["lefthook-darwin-x64@2.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-exooc9Ectz13OLJJOXM9AzaFQbqzf9QCF8JuVvGfbr4RYABYK+BwwtydjlPQrA76/n/h4tsS11MH5bBULnLkYA=="], + + "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.1.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-E1QMlJPEU21n9eewv6ePfh+JmoTSg5R1jaYcKCky10kfbMdohNucI3xV91F2LcerE+p3UejKDqr/1wWO2RMGeQ=="], + + "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.1.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/5zp+x8055Thj46x9S7hgnneZxvWhHQvPWkkgISCab1Lh6eLrbxvhE1qTb1lU3DqTnNmH9NeXdq1xPHc9uGluA=="], + + "lefthook-linux-arm64": ["lefthook-linux-arm64@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK5FvDTkwKO7tOznY8iEZzuTsM1jXMZAG5BMRs7olN1k1K6m2unR6oKABP0hCd0wDErK6DZKDJDJfB564Rzqtw=="], + + "lefthook-linux-x64": ["lefthook-linux-x64@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-4eOtz4PNh8GbJ+nA8YVDfW/eMirQWdZqMP/V/MVtoVBGobf6oXvvuDOySvAPOgNYEFN0Boegytmuji/851Vstg=="], + + "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.1.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-lJXRJ6iJIBKwomuNBA3CUNSclj2/rKuxGAQoUra214B92VB6jL9zaY5YEs6h/ie9jQrzSnllEeg7xyDIsuVCrQ=="], + + "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.1.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-GyOje4W0DIqkmR7/Of5D+mZ0vWqMvtGAVedtJR6d1239xNeMzCS8Q+/a3O1xigceZa5xhlqq0BWlssB/QYPQnA=="], + + "lefthook-windows-arm64": ["lefthook-windows-arm64@2.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-MZKMqTULEpX/8N3fKXAR0A9RjsGKkEEY0japLqrHOIpxsJXry1DRz0FvQo2kkY4WW3rtFegV9m6eesOymuDrUg=="], + + "lefthook-windows-x64": ["lefthook-windows-x64@2.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-NZUgObuaSxc0EXAwC/CzkMf7TuQc++GGIk6TLPdaUpoSsNSJSZEwBVz5DtFB1cG+eMkfO/wOKplls+yjimTTtQ=="], + + "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + + "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="], + + "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], + + "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + + "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + + "prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ultracite": ["ultracite@7.2.4", "", { "dependencies": { "@clack/prompts": "^1.0.1", "commander": "^14.0.3", "deepmerge": "^4.3.1", "glob": "^13.0.3", "jsonc-parser": "^3.3.1", "nypm": "^0.6.5" }, "peerDependencies": { "oxlint": "^1.0.0" }, "optionalPeers": ["oxlint"], "bin": { "ultracite": "dist/index.js" } }, "sha512-3b2g2pwZMDSd+PBK8pxYCjEoYaqVSgXD22HS22BxR8GfbmRXJ3VpNu5Z5lNywIxZXsJleWdpGPHibOPYr/xmKg=="], + + "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], + + "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + + "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + } +} diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..0c78238 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,34 @@ +# Releasing and npm Publishing + +This repository uses [Changesets](https://github.com/changesets/changesets) for versioning, changelogs, tags, GitHub Releases, and npm publishing. + +## One-time repository setup + +1. In npm package settings, enable Trusted Publishing for this GitHub repository/workflow. +2. Keep `main` protected (require PRs and green CI before merge). +3. No `NPM_TOKEN` secret is required when Trusted Publishing is configured correctly. + +## Daily release workflow + +1. Every user-facing change ships with a changeset file (`bun run changeset`). +2. Merge PRs to `main`. +3. The `Release` workflow automatically: + - opens/updates a version PR when changesets are pending + - publishes to npm when that version PR is merged + - creates git tags and GitHub releases (for example `v0.4.1`) + +## Tag and release best practices + +- Let Changesets create version commits and tags; avoid manual `npm version` or manual git tags. +- Keep release commits scoped to version/changelog changes only. +- Prefer stable releases from `main`; use prereleases intentionally with Changesets pre mode when needed. + +## Manual fallback (maintainer only) + +```bash +bun run ci +bun run version-packages +git add -A +git commit -m "ci: version packages" +bun run release +``` diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..0e19112 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,12 @@ +pre-commit: + jobs: + - run: bun x ultracite fix + glob: + - "**/*.js" + - "**/*.jsx" + - "**/*.ts" + - "**/*.tsx" + - "**/*.json" + - "**/*.jsonc" + - "**/*.css" + stage_fixed: true diff --git a/package.json b/package.json index bbae35b..1f70afe 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,14 @@ "name": "terminal-complete", "version": "0.4.0", "description": "AI-powered terminal command generator CLI (Bun)", + "homepage": "https://github.com/Markeljan/terminal-complete#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/Markeljan/terminal-complete.git" + }, + "bugs": { + "url": "https://github.com/Markeljan/terminal-complete/issues" + }, "type": "module", "license": "MIT", "keywords": [ @@ -32,7 +40,20 @@ "dev": "bun run src/cli.ts", "build": "bun run build.ts", "smoke": "bun run src/cli.ts --help", + "check": "ultracite check", + "fix": "ultracite fix", "test": "bun test", - "prepublishOnly": "bun test" + "ci": "bun run check && bun test", + "changeset": "changeset", + "version-packages": "changeset version", + "release": "changeset publish", + "prepublishOnly": "bun run ci", + "prepare": "lefthook install" + }, + "devDependencies": { + "@biomejs/biome": "2.4.0", + "@changesets/cli": "^2.30.0", + "lefthook": "^2.1.2", + "ultracite": "7.2.4" } -} \ No newline at end of file +} diff --git a/src/args.ts b/src/args.ts index 8dd2442..dbb3974 100644 --- a/src/args.ts +++ b/src/args.ts @@ -1,16 +1,16 @@ import type { ProviderName } from "./types"; export interface SuggestModeArgs { + explain: boolean; mode: "suggest"; prompt: string; - explain: boolean; promptMode: boolean; } export interface SetupModeArgs { + legacyAlias?: "auth" | "init"; mode: "setup"; provider?: ProviderName; - legacyAlias?: "auth" | "init"; } export interface ConfigModeArgs { @@ -72,13 +72,16 @@ export function parseArgs(argv: string[]): ParsedArgs { function parseSetupArgs( argv: string[], - legacyAlias?: SetupModeArgs["legacyAlias"], + legacyAlias?: SetupModeArgs["legacyAlias"] ): SetupModeArgs | HelpModeArgs { if (argv.length === 0) { return { mode: "setup", legacyAlias }; } - if (argv.length === 1 && (argv[0] === "-h" || argv[0] === "--help" || argv[0] === "help")) { + if ( + argv.length === 1 && + (argv[0] === "-h" || argv[0] === "--help" || argv[0] === "help") + ) { return { mode: "help" }; } @@ -86,7 +89,9 @@ function parseSetupArgs( return { mode: "setup", provider: parseProvider(argv[0]), legacyAlias }; } - throw new ArgParseError("Unknown setup option. Use: tcomp setup [codex|openai]"); + throw new ArgParseError( + "Unknown setup option. Use: tcomp setup [codex|openai]" + ); } function parseConfigArgs(argv: string[]): ConfigModeArgs | HelpModeArgs { @@ -94,7 +99,10 @@ function parseConfigArgs(argv: string[]): ConfigModeArgs | HelpModeArgs { return { mode: "config" }; } - if (argv.length === 1 && (argv[0] === "-h" || argv[0] === "--help" || argv[0] === "help")) { + if ( + argv.length === 1 && + (argv[0] === "-h" || argv[0] === "--help" || argv[0] === "help") + ) { return { mode: "help" }; } @@ -102,11 +110,16 @@ function parseConfigArgs(argv: string[]): ConfigModeArgs | HelpModeArgs { return { mode: "config", provider: parseProvider(argv[0]) }; } - throw new ArgParseError("Unknown config option. Use: tcomp config [codex|openai]"); + throw new ArgParseError( + "Unknown config option. Use: tcomp config [codex|openai]" + ); } function parseUseArgs(argv: string[]): UseModeArgs | HelpModeArgs { - if (argv.length === 1 && (argv[0] === "-h" || argv[0] === "--help" || argv[0] === "help")) { + if ( + argv.length === 1 && + (argv[0] === "-h" || argv[0] === "--help" || argv[0] === "help") + ) { return { mode: "help" }; } @@ -152,7 +165,7 @@ function parseSuggestArgs(argv: string[]): SuggestModeArgs | HelpModeArgs { const prompt = positionals.join(" ").trim(); if (!prompt) { throw new ArgParseError( - 'Missing prompt. For general prompts, use "tcomp --prompt " (or "tcomp -p ").', + 'Missing prompt. For general prompts, use "tcomp --prompt " (or "tcomp -p ").' ); } @@ -169,7 +182,9 @@ function parseProvider(input: string): ProviderName { if (value === "codex" || value === "openai") { return value; } - throw new ArgParseError(`Unsupported provider: ${input} (expected codex or openai)`); + throw new ArgParseError( + `Unsupported provider: ${input} (expected codex or openai)` + ); } export function helpText(): string { diff --git a/src/cli.ts b/src/cli.ts index 4a4714b..e7e7fb2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,18 +1,28 @@ #!/usr/bin/env bun import { basename } from "node:path"; import { - parseArgs, ArgParseError, - helpText, type ConfigModeArgs, + helpText, + type ParsedArgs, + parseArgs, type SetupModeArgs, type UseModeArgs, } from "./args"; import { loadCodexChatGPTAuth, runCodexCliAuthAction } from "./codex-auth"; -import { askLine, canPromptInteractively, confirm, selectWithArrows, type SelectOption } from "./interactive"; +import { + askLine, + canPromptInteractively, + confirm, + type SelectOption, + selectWithArrows, +} from "./interactive"; import { generatePromptResponse, generateSuggestion } from "./openai"; import { isSupportedShell, type SupportedShell } from "./shell"; -import { installShellIntegration, isShellIntegrationInstalled } from "./shell-install"; +import { + installShellIntegration, + isShellIntegrationInstalled, +} from "./shell-install"; import type { ProviderName, RuntimeContext, Suggestion } from "./types"; import { loadUserConfig, saveUserConfig } from "./user-config"; import { APP_BUILD_TIME, VERSION } from "./version"; @@ -21,6 +31,8 @@ const COLOR_ENABLED = process.env.NO_COLOR === undefined && process.env.TERM !== "dumb" && Boolean(process.stdout.isTTY || process.stderr.isTTY); +const HELP_HEADER_REGEX = + /^(Usage:|Practical examples:|Common flags \(prompt mode\):)$/; function color(text: string, ansiCode: string): string { if (!COLOR_ENABLED) { @@ -106,7 +118,7 @@ function colorizeHelp(text: string): string { return text .split("\n") .map((line) => { - if (/^(Usage:|Practical examples:|Common flags \(prompt mode\):)$/.test(line)) { + if (HELP_HEADER_REGEX.test(line)) { return heading(line); } if (line.startsWith(" tcomp ")) { @@ -126,8 +138,8 @@ function printSuggestionHuman(suggestion: Suggestion, explain: boolean) { printFailure(reason); printInfo( `For general prompts, use ${commandText("tcomp --prompt ")} (or ${commandText( - "tcomp -p ", - )}).`, + "tcomp -p " + )}).` ); process.exit(2); } @@ -138,7 +150,7 @@ function printSuggestionHuman(suggestion: Suggestion, explain: boolean) { } if (suggestion.risk !== "low" || suggestion.needsConfirmation) { console.error( - `${label("risk:")} ${suggestion.risk}${suggestion.needsConfirmation ? " (confirm before running)" : ""}`, + `${label("risk:")} ${suggestion.risk}${suggestion.needsConfirmation ? " (confirm before running)" : ""}` ); } } @@ -150,18 +162,24 @@ function setupRequirementErrors(): string[] { const errors: string[] = []; if (!process.versions.bun) { - errors.push("Bun runtime is required. Install Bun: https://bun.sh/docs/installation"); + errors.push( + "Bun runtime is required. Install Bun: https://bun.sh/docs/installation" + ); } const shell = shellNameFromEnv(); if (!isSupportedShell(shell)) { - errors.push(`zsh or bash is required for setup. Detected SHELL=\"${shell}\".`); + errors.push( + `zsh or bash is required for setup. Detected SHELL="${shell}".` + ); } return errors; } -function defaultProvider(config: Awaited>): ProviderName { +function defaultProvider( + config: Awaited> +): ProviderName { if (config.activeProvider === "openai" || config.activeProvider === "codex") { return config.activeProvider; } @@ -169,7 +187,7 @@ function defaultProvider(config: Awaited>): Pr } async function chooseProvider(current: ProviderName): Promise { - const options: Array> = [ + const options: SelectOption[] = [ { label: "OpenAI OAuth (via Codex CLI)", value: "codex" }, { label: "OpenAI API key", value: "openai" }, ]; @@ -181,18 +199,22 @@ async function chooseProvider(current: ProviderName): Promise { type OAuthLoginMethod = "browser" | "device"; async function chooseOAuthLoginMethod(): Promise { - const options: Array> = [ + const options: SelectOption[] = [ { label: "Browser login", value: "browser" }, { label: "Device login (code entry)", value: "device" }, ]; - return await selectWithArrows("Select OpenAI OAuth login method:", options, 0); + return await selectWithArrows( + "Select OpenAI OAuth login method:", + options, + 0 + ); } function printSourceInstructions(path: string) { printInfo( `Automatic ${commandText("source")} is not possible from a child CLI process. Run ${commandText( - `source ${path}`, - )} in your current shell.`, + `source ${path}` + )} in your current shell.` ); } @@ -218,7 +240,7 @@ async function runProviderSetup(provider: ProviderName): Promise { ...config, activeProvider: "codex", }); - printSuccess(`Saved active provider \"codex\" to ${path}`); + printSuccess(`Saved active provider "codex" to ${path}`); return 0; } @@ -239,10 +261,41 @@ async function runProviderSetup(provider: ProviderName): Promise { activeProvider: "openai", openaiApiKey: apiKey, }); - printSuccess(`Saved active provider \"openai\" to ${path}`); + printSuccess(`Saved active provider "openai" to ${path}`); return 0; } +async function maybeOfferShellInstall( + setupShell: SupportedShell, + offerShellInstall: boolean +): Promise { + if (!offerShellInstall) { + return; + } + + const shellInstalled = await isShellIntegrationInstalled(setupShell); + if (shellInstalled) { + return; + } + + const shouldInstallShell = await confirm( + `Install ${setupShell} shell integration + completions now?`, + true + ); + + if (!shouldInstallShell) { + printWarning("Skipped shell integration install."); + return; + } + + const installResult = await installShellIntegration(setupShell); + const installMessage = installResult.updated + ? `Installed tcomp shell integration in ${installResult.path}` + : `tcomp shell integration already installed in ${installResult.path}`; + printSuccess(installMessage); + printSourceInstructions(installResult.path); +} + async function runSetupFlow(options: { showWelcome: boolean; provider?: ProviderName; @@ -250,12 +303,16 @@ async function runSetupFlow(options: { offerShellInstall?: boolean; }): Promise { if (!canPromptInteractively()) { - printFailure(`Setup requires an interactive terminal. Run ${commandText("tcomp setup")} in a TTY.`); + printFailure( + `Setup requires an interactive terminal. Run ${commandText("tcomp setup")} in a TTY.` + ); return 1; } if (options.legacyAlias) { - printWarning(`\"${options.legacyAlias}\" has been replaced by ${commandText("tcomp setup")}.`); + printWarning( + `"${options.legacyAlias}" has been replaced by ${commandText("tcomp setup")}.` + ); } const requirementErrors = setupRequirementErrors(); @@ -268,36 +325,24 @@ async function runSetupFlow(options: { const setupShell = setupShellFromEnv(); if (!setupShell) { - printFailure("Could not detect a supported shell from SHELL. Expected zsh or bash."); + printFailure( + "Could not detect a supported shell from SHELL. Expected zsh or bash." + ); return 1; } if (options.showWelcome) { console.log(heading("tcomp setup")); - printInfo(`Welcome. Setup configures provider auth and optional ${setupShell} integration.`); - } - - if (options.offerShellInstall !== false) { - const shellInstalled = await isShellIntegrationInstalled(setupShell); - if (!shellInstalled) { - const shouldInstallShell = await confirm(`Install ${setupShell} shell integration + completions now?`, true); - - if (shouldInstallShell) { - const installResult = await installShellIntegration(setupShell); - if (installResult.updated) { - printSuccess(`Installed tcomp shell integration in ${installResult.path}`); - } else { - printSuccess(`tcomp shell integration already installed in ${installResult.path}`); - } - printSourceInstructions(installResult.path); - } else { - printWarning("Skipped shell integration install."); - } - } + printInfo( + `Welcome. Setup configures provider auth and optional ${setupShell} integration.` + ); } + await maybeOfferShellInstall(setupShell, options.offerShellInstall !== false); + const config = await loadUserConfig(); - const provider = options.provider ?? (await chooseProvider(defaultProvider(config))); + const provider = + options.provider ?? (await chooseProvider(defaultProvider(config))); const code = await runProviderSetup(provider); if (code === 0) { printSuccess(`Setup complete. Active provider: ${providerLabel(provider)}`); @@ -311,7 +356,10 @@ async function hasCompletedSetup(): Promise { return true; } - if (config.activeProvider === "openai" && Boolean(config.openaiApiKey?.trim())) { + if ( + config.activeProvider === "openai" && + Boolean(config.openaiApiKey?.trim()) + ) { return true; } @@ -323,7 +371,10 @@ async function ensureSetupBeforeSuggestion(): Promise { return; } - const code = await runSetupFlow({ showWelcome: true, offerShellInstall: true }); + const code = await runSetupFlow({ + showWelcome: true, + offerShellInstall: true, + }); if (code !== 0) { process.exit(code); } @@ -355,15 +406,21 @@ async function handleConfigCommand(args: ConfigModeArgs): Promise { console.log(heading("tcomp config")); console.log(`${label("Active provider:")} ${commandText(activeProvider)}`); console.log( - `${label("OpenAI OAuth:")} ${codexConfigured ? statusOk("configured") : statusWarn("not configured")}`, + `${label("OpenAI OAuth:")} ${codexConfigured ? statusOk("configured") : statusWarn("not configured")}` ); console.log( - `${label("OpenAI API key:")} ${openaiConfigured ? statusOk("configured") : statusWarn("not configured")}`, + `${label("OpenAI API key:")} ${openaiConfigured ? statusOk("configured") : statusWarn("not configured")}` ); console.log(""); - printInfo(`Run ${commandText("tcomp config codex")} to run OpenAI OAuth setup (browser or device login).`); - printInfo(`Run ${commandText("tcomp config openai")} to set/update your OpenAI API key.`); - printInfo(`Run ${commandText("tcomp use codex")} or ${commandText("tcomp use openai")} to switch providers.`); + printInfo( + `Run ${commandText("tcomp config codex")} to run OpenAI OAuth setup (browser or device login).` + ); + printInfo( + `Run ${commandText("tcomp config openai")} to set/update your OpenAI API key.` + ); + printInfo( + `Run ${commandText("tcomp use codex")} or ${commandText("tcomp use openai")} to switch providers.` + ); printInfo(`Run ${commandText("tcomp setup")} to run full onboarding again.`); return 0; } @@ -375,20 +432,24 @@ async function handleUseCommand(args: UseModeArgs): Promise { activeProvider: args.provider, }); - printSuccess(`Active provider set to ${providerLabel(args.provider)} in ${path}`); + printSuccess( + `Active provider set to ${providerLabel(args.provider)} in ${path}` + ); if (args.provider === "openai" && !config.openaiApiKey?.trim()) { printWarning( `OpenAI API key is not configured. Run ${commandText("tcomp config openai")} or ${commandText( - "tcomp setup", - )}.`, + "tcomp setup" + )}.` ); } if (args.provider === "codex") { const codexReady = await isCodexConfigured(); if (!codexReady) { - printWarning(`Codex auth not configured yet. Run ${commandText("tcomp config codex")} if needed.`); + printWarning( + `Codex auth not configured yet. Run ${commandText("tcomp config codex")} if needed.` + ); } } @@ -399,7 +460,10 @@ async function main() { const rawArgv = process.argv.slice(2); if (rawArgv.length === 0) { if (!(await hasCompletedSetup())) { - const code = await runSetupFlow({ showWelcome: true, offerShellInstall: true }); + const code = await runSetupFlow({ + showWelcome: true, + offerShellInstall: true, + }); process.exit(code); } @@ -407,7 +471,7 @@ async function main() { return; } - let args; + let args: ParsedArgs; try { args = parseArgs(rawArgv); } catch (error) { diff --git a/src/codex-auth.ts b/src/codex-auth.ts index 415b9a9..5ae9dcc 100644 --- a/src/codex-auth.ts +++ b/src/codex-auth.ts @@ -1,5 +1,5 @@ -import { readFile } from "node:fs/promises"; import { spawn } from "node:child_process"; +import { readFile } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; @@ -38,7 +38,7 @@ export async function loadCodexChatGPTAuth(): Promise { } catch (error) { if (isFileMissing(error)) { throw new Error( - `Codex auth not found at ${authPath}. Run "tcomp config codex" or "tcomp setup" first.`, + `Codex auth not found at ${authPath}. Run "tcomp config codex" or "tcomp setup" first.` ); } throw error; @@ -53,23 +53,31 @@ export async function loadCodexChatGPTAuth(): Promise { if (parsed.auth_mode !== "chatgpt") { throw new Error( - `Codex auth mode is not ChatGPT (found: ${String(parsed.auth_mode)}). Run "codex login" with ChatGPT auth.`, + `Codex auth mode is not ChatGPT (found: ${String(parsed.auth_mode)}). Run "codex login" with ChatGPT auth.` ); } - const accessToken = typeof parsed.tokens?.access_token === "string" ? parsed.tokens.access_token : ""; - const accountId = typeof parsed.tokens?.account_id === "string" ? parsed.tokens.account_id : undefined; + const accessToken = + typeof parsed.tokens?.access_token === "string" + ? parsed.tokens.access_token + : ""; + const accountId = + typeof parsed.tokens?.account_id === "string" + ? parsed.tokens.account_id + : undefined; if (!accessToken) { throw new Error( - `Codex auth file does not contain a ChatGPT access token. Run "tcomp config codex" or "tcomp setup".`, + `Codex auth file does not contain a ChatGPT access token. Run "tcomp config codex" or "tcomp setup".` ); } return { accessToken, accountId }; } -export async function ensureCodexChatGPTAuth(interactive: boolean): Promise { +export async function ensureCodexChatGPTAuth( + interactive: boolean +): Promise { try { return await loadCodexChatGPTAuth(); } catch (error) { @@ -90,16 +98,16 @@ export async function ensureCodexChatGPTAuth(interactive: boolean): Promise { - const args = - action === "login" - ? options.deviceAuth - ? ["login", "--device-auth"] - : ["login"] - : action === "status" - ? ["login", "status"] - : ["logout"]; + let args: string[]; + if (action === "login") { + args = options.deviceAuth ? ["login", "--device-auth"] : ["login"]; + } else if (action === "status") { + args = ["login", "status"]; + } else { + args = ["logout"]; + } return await new Promise((resolve, reject) => { const child = spawn("codex", args, { @@ -109,7 +117,9 @@ export async function runCodexCliAuthAction( child.on("error", (error) => { if ((error as NodeJS.ErrnoException).code === "ENOENT") { - reject(new Error('Could not find "codex" in PATH. Install Codex CLI first.')); + reject( + new Error('Could not find "codex" in PATH. Install Codex CLI first.') + ); return; } reject(error); @@ -117,7 +127,9 @@ export async function runCodexCliAuthAction( child.on("exit", (code, signal) => { if (signal) { - reject(new Error(`codex ${args.join(" ")} terminated by signal ${signal}`)); + reject( + new Error(`codex ${args.join(" ")} terminated by signal ${signal}`) + ); return; } resolve(code ?? 1); diff --git a/src/globals.d.ts b/src/globals.d.ts index 83f766c..55d5749 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,3 +1,2 @@ declare const BUILD_VERSION: string; declare const BUILD_TIME: string; - diff --git a/src/interactive.ts b/src/interactive.ts index 4fb524a..cefe72e 100644 --- a/src/interactive.ts +++ b/src/interactive.ts @@ -1,11 +1,11 @@ -import * as readline from "node:readline"; +import { createInterface, emitKeypressEvents, type Key } from "node:readline"; export function canPromptInteractively(): boolean { return Boolean(process.stdin.isTTY && process.stderr.isTTY); } export async function askLine(prompt: string): Promise { - const rl = readline.createInterface({ + const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: canPromptInteractively(), @@ -23,7 +23,7 @@ export async function askLine(prompt: string): Promise { export async function askChoice( prompt: string, options: string[], - defaultValue?: string, + defaultValue?: string ): Promise { const normalized = new Set(options); const suffix = defaultValue ? ` [${defaultValue}]` : ""; @@ -38,7 +38,10 @@ export async function askChoice( } } -export async function confirm(prompt: string, defaultYes = true): Promise { +export async function confirm( + prompt: string, + defaultYes = true +): Promise { const suffix = defaultYes ? " [Y/n]" : " [y/N]"; const raw = (await askLine(`${prompt}${suffix}: `)).trim().toLowerCase(); if (!raw) { @@ -55,17 +58,22 @@ export interface SelectOption { export async function selectWithArrows( prompt: string, options: SelectOption[], - defaultIndex = 0, + defaultIndex = 0 ): Promise { if (options.length === 0) { throw new Error("selectWithArrows requires at least one option"); } - if (!canPromptInteractively() || typeof process.stdin.setRawMode !== "function") { + if ( + !canPromptInteractively() || + typeof process.stdin.setRawMode !== "function" + ) { const labels = options.map((option) => option.label); - const defaultLabel = options[Math.max(0, Math.min(defaultIndex, options.length - 1))]?.label; + const defaultLabel = + options[Math.max(0, Math.min(defaultIndex, options.length - 1))]?.label; const selected = await askChoice(prompt, labels, defaultLabel); - const matched = options.find((option) => option.label === selected) ?? options[0]; + const matched = + options.find((option) => option.label === selected) ?? options[0]; return matched.value; } @@ -73,7 +81,7 @@ export async function selectWithArrows( const stdout = process.stdout; const initialIndex = Math.max(0, Math.min(defaultIndex, options.length - 1)); - readline.emitKeypressEvents(stdin); + emitKeypressEvents(stdin); return await new Promise((resolve, reject) => { let index = initialIndex; @@ -116,14 +124,16 @@ export async function selectWithArrows( const done = (value: T) => { clearRender(); - stdout.write(`${prompt} ${String( - options.find((option) => option.value === value)?.label ?? "", - )}\n`); + stdout.write( + `${prompt} ${String( + options.find((option) => option.value === value)?.label ?? "" + )}\n` + ); cleanup(); resolve(value); }; - const onKeyPress = (_: string, key: readline.Key) => { + const onKeyPress = (_: string, key: Key) => { if (key.name === "up") { index = (index - 1 + options.length) % options.length; render(); diff --git a/src/openai.ts b/src/openai.ts index 553e180..113a1a4 100644 --- a/src/openai.ts +++ b/src/openai.ts @@ -1,5 +1,5 @@ -import { buildUserPrompt, loadSystemPrompt } from "./prompt"; import { ensureCodexChatGPTAuth } from "./codex-auth"; +import { buildUserPrompt, loadSystemPrompt } from "./prompt"; import type { RiskLevel, RuntimeContext, Suggestion } from "./types"; import { loadUserConfig } from "./user-config"; @@ -13,20 +13,20 @@ interface ChatCompletionResponse { } interface ResponsesApiResponse { - output_text?: unknown; + error?: { message?: string }; output?: unknown; + output_text?: unknown; response?: ResponsesApiResponse; - error?: { message?: string }; } interface ResolvedOpenAIConfig { - provider: "openai"; apiKey: string; + provider: "openai"; } interface ResolvedCodexConfig { - provider: "codex"; accessToken: string; + provider: "codex"; } type ResolvedRuntimeConfig = ResolvedOpenAIConfig | ResolvedCodexConfig; @@ -38,6 +38,12 @@ const CODEX_MODEL = "gpt-5.3-codex"; const GENERAL_ASSISTANT_SYSTEM_PROMPT = "You are a concise, helpful assistant. Answer directly in plain text unless asked for another format."; +const CODE_FENCE_START_REGEX = /^```[a-zA-Z0-9_-]*\s*/; +const CODE_FENCE_END_REGEX = /\s*```$/; +const SSE_EVENT_LINE_REGEX = /(^|\n)\s*event:\s/m; +const SSE_DATA_LINE_REGEX = /(^|\n)\s*data:\s/m; +const SSE_CHUNK_SPLIT_REGEX = /\r?\n\r?\n/; +const SSE_LINE_SPLIT_REGEX = /\r?\n/; export async function resolveRuntimeConfig(): Promise { const stored = await loadUserConfig(); @@ -46,7 +52,9 @@ export async function resolveRuntimeConfig(): Promise { if (provider === "openai") { const apiKey = stored.openaiApiKey?.trim(); if (!apiKey) { - throw new Error('OpenAI API key is missing. Run "tcomp config openai" or "tcomp setup".'); + throw new Error( + 'OpenAI API key is missing. Run "tcomp config openai" or "tcomp setup".' + ); } return { @@ -66,7 +74,9 @@ export async function resolveRuntimeConfig(): Promise { throw new Error('Setup is required before using tcomp. Run "tcomp setup".'); } -function normalizeContent(content: string | Array<{ type?: string; text?: string }> | undefined): string { +function normalizeContent( + content: string | Array<{ type?: string; text?: string }> | undefined +): string { if (typeof content === "string") { return content; } @@ -84,8 +94,8 @@ function stripCodeFence(input: string): string { return trimmed; } - const withoutStart = trimmed.replace(/^```[a-zA-Z0-9_-]*\s*/, ""); - return withoutStart.replace(/\s*```$/, "").trim(); + const withoutStart = trimmed.replace(CODE_FENCE_START_REGEX, ""); + return withoutStart.replace(CODE_FENCE_END_REGEX, "").trim(); } function tryExtractJson(raw: string): unknown { @@ -125,7 +135,8 @@ function parseSuggestion(raw: string): Suggestion { const obj = parsed as Record; const command = typeof obj.command === "string" ? obj.command.trim() : ""; - const explanation = typeof obj.explanation === "string" ? obj.explanation.trim() : ""; + const explanation = + typeof obj.explanation === "string" ? obj.explanation.trim() : ""; const risk = asRiskLevel(obj.risk); const needsConfirmation = typeof obj.needsConfirmation === "boolean" @@ -144,7 +155,7 @@ async function requestChatCompletion( apiKey: string, systemPrompt: string, userPrompt: string, - includeResponseFormat: boolean, + includeResponseFormat: boolean ): Promise { const payload: Record = { model: OPENAI_MODEL, @@ -192,7 +203,7 @@ async function requestChatCompletion( async function requestCodexResponses( accessToken: string, systemPrompt: string, - userPrompt: string, + userPrompt: string ): Promise { const payload = { model: CODEX_MODEL, @@ -230,7 +241,7 @@ async function requestCodexResponses( const errorMessage = json?.error?.message ?? text; if (response.status === 401 || response.status === 403) { throw new Error( - `Codex auth failed (${response.status}). Run "tcomp config codex" or "tcomp setup", then try again.`, + `Codex auth failed (${response.status}). Run "tcomp config codex" or "tcomp setup", then try again.` ); } throw new Error(`Codex API error (${response.status}): ${errorMessage}`); @@ -239,8 +250,8 @@ async function requestCodexResponses( const contentType = response.headers.get("content-type") ?? ""; const looksLikeSse = contentType.includes("text/event-stream") || - /(^|\n)\s*event:\s/m.test(text) || - /(^|\n)\s*data:\s/m.test(text); + SSE_EVENT_LINE_REGEX.test(text) || + SSE_DATA_LINE_REGEX.test(text); const content = looksLikeSse ? extractTextFromSseStream(text) : extractResponsesOutputText(json ?? text); @@ -255,40 +266,13 @@ function extractTextFromSseStream(sse: string): string { const deltas: string[] = []; const fallbackPayloads: unknown[] = []; - const chunks = sse.split(/\r?\n\r?\n/); - for (const chunk of chunks) { - if (!chunk.trim()) { - continue; - } - - let eventName = ""; - const dataLines: string[] = []; - for (const line of chunk.split(/\r?\n/)) { - if (line.startsWith("event:")) { - eventName = line.slice("event:".length).trim(); - continue; - } - if (line.startsWith("data:")) { - dataLines.push(line.slice("data:".length).trimStart()); - } - } - - if (dataLines.length === 0) { - continue; - } - - const data = dataLines.join("\n"); - if (data === "[DONE]") { - continue; - } - - let parsed: unknown; - try { - parsed = JSON.parse(data); - } catch { + for (const chunk of sse.split(SSE_CHUNK_SPLIT_REGEX)) { + const parsedChunk = parseSseChunk(chunk); + if (!parsedChunk) { continue; } + const { eventName, parsed } = parsedChunk; const delta = extractResponsesDeltaText(parsed, eventName); if (delta) { deltas.push(delta); @@ -301,7 +285,7 @@ function extractTextFromSseStream(sse: string): string { } if (doneTexts.length > 0) { - return doneTexts[doneTexts.length - 1] ?? ""; + return doneTexts.at(-1) ?? ""; } if (deltas.length > 0) { @@ -318,37 +302,83 @@ function extractTextFromSseStream(sse: string): string { return ""; } +function parseSseChunk( + chunk: string +): { eventName: string; parsed: unknown } | null { + if (!chunk.trim()) { + return null; + } + + let eventName = ""; + const dataLines: string[] = []; + for (const line of chunk.split(SSE_LINE_SPLIT_REGEX)) { + if (line.startsWith("event:")) { + eventName = line.slice("event:".length).trim(); + continue; + } + if (line.startsWith("data:")) { + dataLines.push(line.slice("data:".length).trimStart()); + } + } + + if (dataLines.length === 0) { + return null; + } + + const data = dataLines.join("\n"); + if (data === "[DONE]") { + return null; + } + + try { + return { eventName, parsed: JSON.parse(data) }; + } catch { + return null; + } +} + function extractResponsesDeltaText(input: unknown, eventName: string): string { - if (!input || typeof input !== "object") { + const obj = asRecord(input); + if (!obj) { return ""; } - const obj = input as Record; if (typeof obj.delta === "string") { return obj.delta; } - if (typeof obj.text === "string" && eventName.includes("delta")) { + const isDeltaEvent = eventName.includes("delta"); + if (typeof obj.text === "string" && isDeltaEvent) { return obj.text; } - const content = obj.content; - if (Array.isArray(content)) { - let combined = ""; - for (const item of content) { - if (item && typeof item === "object") { - const rec = item as Record; - if (typeof rec.delta === "string") { - combined += rec.delta; - } else if (typeof rec.text === "string" && eventName.includes("delta")) { - combined += rec.text; - } - } - } - return combined; + return extractDeltaFromContent(obj.content, isDeltaEvent); +} + +function extractDeltaFromContent( + content: unknown, + isDeltaEvent: boolean +): string { + if (!Array.isArray(content)) { + return ""; } - return ""; + let combined = ""; + for (const item of content) { + const record = asRecord(item); + if (!record) { + continue; + } + + if (typeof record.delta === "string") { + combined += record.delta; + continue; + } + if (typeof record.text === "string" && isDeltaEvent) { + combined += record.text; + } + } + return combined; } function extractResponsesDoneText(input: unknown, eventName: string): string { @@ -357,7 +387,10 @@ function extractResponsesDoneText(input: unknown, eventName: string): string { } const obj = input as Record; - if (eventName === "response.output_text.done" && typeof obj.text === "string") { + if ( + eventName === "response.output_text.done" && + typeof obj.text === "string" + ) { return obj.text; } @@ -378,12 +411,14 @@ function extractResponsesOutputText(input: unknown): string { if (typeof input === "string") { return input; } - if (!input || typeof input !== "object") { + + const obj = asRecord(input) as + | (ResponsesApiResponse & Record) + | null; + if (!obj) { return ""; } - const obj = input as ResponsesApiResponse & Record; - if (typeof obj.output_text === "string") { return obj.output_text; } @@ -395,43 +430,53 @@ function extractResponsesOutputText(input: unknown): string { } } - if (!Array.isArray(obj.output)) { + return extractOutputTextParts(obj.output); +} + +function extractOutputTextParts(output: unknown): string { + if (!Array.isArray(output)) { return ""; } const parts: string[] = []; - for (const item of obj.output) { - if (!item || typeof item !== "object") { - continue; - } - const record = item as Record; - if (!Array.isArray(record.content)) { + for (const item of output) { + const record = asRecord(item); + if (!(record && Array.isArray(record.content))) { continue; } + for (const piece of record.content) { - if (!piece || typeof piece !== "object") { - continue; - } - const part = piece as Record; - if (typeof part.text === "string") { + const part = asRecord(piece); + if (typeof part?.text === "string") { parts.push(part.text); } } } + return parts.join(""); } +function asRecord(input: unknown): Record | null { + if (!input || typeof input !== "object") { + return null; + } + return input as Record; +} + function isResponseFormatUnsupported(error: unknown): boolean { if (!(error instanceof Error)) { return false; } const msg = error.message.toLowerCase(); - return msg.includes("response_format") && (msg.includes("unsupported") || msg.includes("unknown")); + return ( + msg.includes("response_format") && + (msg.includes("unsupported") || msg.includes("unknown")) + ); } export async function generateSuggestion( request: string, - context: RuntimeContext, + context: RuntimeContext ): Promise { const config = await resolveRuntimeConfig(); const [systemPrompt, userPrompt] = await Promise.all([ @@ -442,7 +487,11 @@ export async function generateSuggestion( let raw: string; if (config.provider === "codex") { - raw = await requestCodexResponses(config.accessToken, systemPrompt, userPrompt); + raw = await requestCodexResponses( + config.accessToken, + systemPrompt, + userPrompt + ); if (process.env.TCOMP_DEBUG_RAW === "1") { console.error("DEBUG raw codex response:"); console.error(raw); @@ -451,12 +500,22 @@ export async function generateSuggestion( } try { - raw = await requestChatCompletion(config.apiKey, systemPrompt, userPrompt, true); + raw = await requestChatCompletion( + config.apiKey, + systemPrompt, + userPrompt, + true + ); } catch (error) { if (!isResponseFormatUnsupported(error)) { throw error; } - raw = await requestChatCompletion(config.apiKey, systemPrompt, userPrompt, false); + raw = await requestChatCompletion( + config.apiKey, + systemPrompt, + userPrompt, + false + ); } return parseSuggestion(raw); @@ -464,7 +523,7 @@ export async function generateSuggestion( export async function generatePromptResponse( request: string, - context: RuntimeContext, + context: RuntimeContext ): Promise { const config = await resolveRuntimeConfig(); const userPrompt = `User prompt: ${request} @@ -476,8 +535,17 @@ Runtime context: const raw = config.provider === "codex" - ? await requestCodexResponses(config.accessToken, GENERAL_ASSISTANT_SYSTEM_PROMPT, userPrompt) - : await requestChatCompletion(config.apiKey, GENERAL_ASSISTANT_SYSTEM_PROMPT, userPrompt, false); + ? await requestCodexResponses( + config.accessToken, + GENERAL_ASSISTANT_SYSTEM_PROMPT, + userPrompt + ) + : await requestChatCompletion( + config.apiKey, + GENERAL_ASSISTANT_SYSTEM_PROMPT, + userPrompt, + false + ); return raw.trim(); } diff --git a/src/prompt.ts b/src/prompt.ts index 451d789..a3eb79d 100644 --- a/src/prompt.ts +++ b/src/prompt.ts @@ -12,7 +12,10 @@ export async function loadSystemPrompt(): Promise { return cachedSystemPrompt; } -export function buildUserPrompt(request: string, context: RuntimeContext): string { +export function buildUserPrompt( + request: string, + context: RuntimeContext +): string { return JSON.stringify( { task: request, @@ -27,7 +30,6 @@ export function buildUserPrompt(request: string, context: RuntimeContext): strin }, }, null, - 2, + 2 ); } - diff --git a/src/shell-install.ts b/src/shell-install.ts index 6516426..2ea5cfa 100644 --- a/src/shell-install.ts +++ b/src/shell-install.ts @@ -8,7 +8,9 @@ interface InstallResult { updated: boolean; } -export async function installShellIntegration(shell: SupportedShell): Promise { +export async function installShellIntegration( + shell: SupportedShell +): Promise { const rcPath = getRcPath(shell); const markerStart = "# >>> tcomp integration >>>"; const markerEnd = "# <<< tcomp integration <<<"; @@ -34,7 +36,9 @@ export async function installShellIntegration(shell: SupportedShell): Promise { +export async function isShellIntegrationInstalled( + shell: SupportedShell +): Promise { const rcPath = getRcPath(shell); const markerStart = "# >>> tcomp integration >>>"; const markerEnd = "# <<< tcomp integration <<<"; @@ -73,10 +77,17 @@ function getRcPath(shell: SupportedShell): string { return getZshRcPath(); case "bash": return getBashRcPath(); + default: + return exhaustiveShell(shell); } } -function upsertManagedBlock(existing: string, start: string, end: string, block: string): string { +function upsertManagedBlock( + existing: string, + start: string, + end: string, + block: string +): string { const startIndex = existing.indexOf(start); const endIndex = existing.indexOf(end); @@ -112,3 +123,7 @@ function resolveHomeDir(): string { } return homedir(); } + +function exhaustiveShell(value: never): never { + throw new Error(`Unsupported shell: ${value}`); +} diff --git a/src/shell.ts b/src/shell.ts index da83567..de53848 100644 --- a/src/shell.ts +++ b/src/shell.ts @@ -10,6 +10,8 @@ export function renderShellIntegration(shell: SupportedShell): string { return renderZshIntegration(); case "bash": return renderBashIntegration(); + default: + return exhaustiveShell(shell); } } @@ -242,3 +244,7 @@ tcomp() { } `; } + +function exhaustiveShell(value: never): never { + throw new Error(`Unsupported shell: ${value}`); +} diff --git a/src/types.ts b/src/types.ts index c2693f9..9845580 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,13 +4,13 @@ export type ProviderName = "codex" | "openai"; export interface Suggestion { command: string; explanation: string; - risk: RiskLevel; needsConfirmation: boolean; + risk: RiskLevel; } export interface RuntimeContext { cwd: string; - shell: string; - platform: NodeJS.Platform; homeDir: string; + platform: NodeJS.Platform; + shell: string; } diff --git a/src/user-config.ts b/src/user-config.ts index 4702632..af3c3ce 100644 --- a/src/user-config.ts +++ b/src/user-config.ts @@ -1,4 +1,4 @@ -import { mkdir, readFile, writeFile, chmod } from "node:fs/promises"; +import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import type { ProviderName } from "./types"; @@ -34,7 +34,9 @@ export async function loadUserConfig(): Promise { } } -export async function updateUserConfig(patch: Partial): Promise { +export async function updateUserConfig( + patch: Partial +): Promise { const current = await loadUserConfig(); return saveUserConfig({ ...current, ...patch }); } @@ -69,12 +71,12 @@ function parseUserConfig(raw: string): UserConfig { const obj = parsed as Record; - const openaiApiKey = - typeof obj.openaiApiKey === "string" - ? obj.openaiApiKey - : typeof obj.apiKey === "string" - ? obj.apiKey - : undefined; + let openaiApiKey: string | undefined; + if (typeof obj.openaiApiKey === "string") { + openaiApiKey = obj.openaiApiKey; + } else if (typeof obj.apiKey === "string") { + openaiApiKey = obj.apiKey; + } const activeProvider = normalizeProviderName(obj.activeProvider) ?? @@ -95,7 +97,9 @@ function normalizeProviderName(value: unknown): ProviderName | undefined { return undefined; } -function normalizeProviderFromAuthMethod(value: unknown): ProviderName | undefined { +function normalizeProviderFromAuthMethod( + value: unknown +): ProviderName | undefined { if (value === "codex-oauth") { return "codex"; } diff --git a/src/version.ts b/src/version.ts index 7e1b79e..4cfb9f0 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ export const VERSION = - typeof BUILD_VERSION === "string" ? BUILD_VERSION : "0.4.0-dev"; + typeof BUILD_VERSION === "string" ? BUILD_VERSION : "0.4.0-dev"; export const APP_BUILD_TIME = typeof BUILD_TIME === "string" ? BUILD_TIME : ""; diff --git a/test/args.test.ts b/test/args.test.ts index 7607f80..f22066e 100644 --- a/test/args.test.ts +++ b/test/args.test.ts @@ -10,7 +10,9 @@ function expectSuggest(args: ReturnType) { describe("parseArgs", () => { test("treats unquoted words as a single prompt", () => { - const parsed = expectSuggest(parseArgs(["hey", "this", "is", "a", "prompt"])); + const parsed = expectSuggest( + parseArgs(["hey", "this", "is", "a", "prompt"]) + ); expect(parsed.prompt).toBe("hey this is a prompt"); }); @@ -21,7 +23,9 @@ describe("parseArgs", () => { }); test("supports explain flag after prompt", () => { - const parsed = expectSuggest(parseArgs(["find", "big", "files", "--explain"])); + const parsed = expectSuggest( + parseArgs(["find", "big", "files", "--explain"]) + ); expect(parsed.explain).toBe(true); expect(parsed.prompt).toBe("find big files"); }); @@ -79,7 +83,9 @@ describe("parseArgs", () => { }); test("throws on unknown options", () => { - expect(() => parseArgs(["--does-not-exist", "hello"])).toThrow(ArgParseError); + expect(() => parseArgs(["--does-not-exist", "hello"])).toThrow( + ArgParseError + ); }); }); diff --git a/test/config-and-auth-paths.test.ts b/test/config-and-auth-paths.test.ts index 0c9f9c0..9bf6395 100644 --- a/test/config-and-auth-paths.test.ts +++ b/test/config-and-auth-paths.test.ts @@ -3,7 +3,11 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { getCodexAuthPath, loadCodexChatGPTAuth } from "../src/codex-auth"; -import { getUserConfigPath, loadUserConfig, saveUserConfig } from "../src/user-config"; +import { + getUserConfigPath, + loadUserConfig, + saveUserConfig, +} from "../src/user-config"; describe("user config and codex auth paths", () => { const originalXdg = process.env.XDG_CONFIG_HOME; @@ -18,12 +22,12 @@ describe("user config and codex auth paths", () => { afterEach(async () => { if (originalXdg === undefined) { - delete process.env.XDG_CONFIG_HOME; + Reflect.deleteProperty(process.env, "XDG_CONFIG_HOME"); } else { process.env.XDG_CONFIG_HOME = originalXdg; } if (originalCodexHome === undefined) { - delete process.env.CODEX_HOME; + Reflect.deleteProperty(process.env, "CODEX_HOME"); } else { process.env.CODEX_HOME = originalCodexHome; } @@ -38,7 +42,9 @@ describe("user config and codex auth paths", () => { }); expect(savedPath).toBe(getUserConfigPath()); - expect(savedPath).toContain(join("xdg", "terminal-complete", "config.json")); + expect(savedPath).toContain( + join("xdg", "terminal-complete", "config.json") + ); const loaded = await loadUserConfig(); expect(loaded.activeProvider).toBe("codex"); @@ -47,7 +53,9 @@ describe("user config and codex auth paths", () => { test("loadUserConfig migrates legacy openai fields", async () => { const configPath = getUserConfigPath(); - await mkdir(join(sandboxDir, "xdg", "terminal-complete"), { recursive: true }); + await mkdir(join(sandboxDir, "xdg", "terminal-complete"), { + recursive: true, + }); await writeFile( configPath, JSON.stringify( @@ -56,9 +64,9 @@ describe("user config and codex auth paths", () => { apiKey: "legacy-key", }, null, - 2, + 2 ), - "utf8", + "utf8" ); const loaded = await loadUserConfig(); @@ -68,7 +76,9 @@ describe("user config and codex auth paths", () => { test("loadUserConfig migrates authMethod field", async () => { const configPath = getUserConfigPath(); - await mkdir(join(sandboxDir, "xdg", "terminal-complete"), { recursive: true }); + await mkdir(join(sandboxDir, "xdg", "terminal-complete"), { + recursive: true, + }); await writeFile( configPath, JSON.stringify( @@ -76,9 +86,9 @@ describe("user config and codex auth paths", () => { authMethod: "codex-oauth", }, null, - 2, + 2 ), - "utf8", + "utf8" ); const loaded = await loadUserConfig(); @@ -87,7 +97,9 @@ describe("user config and codex auth paths", () => { test("loadUserConfig infers provider when only openaiApiKey is set", async () => { const configPath = getUserConfigPath(); - await mkdir(join(sandboxDir, "xdg", "terminal-complete"), { recursive: true }); + await mkdir(join(sandboxDir, "xdg", "terminal-complete"), { + recursive: true, + }); await writeFile( configPath, JSON.stringify( @@ -95,9 +107,9 @@ describe("user config and codex auth paths", () => { openaiApiKey: "key-only", }, null, - 2, + 2 ), - "utf8", + "utf8" ); const loaded = await loadUserConfig(); @@ -124,9 +136,9 @@ describe("user config and codex auth paths", () => { }, }, null, - 2, + 2 ), - "utf8", + "utf8" ); const auth = await loadCodexChatGPTAuth(); @@ -147,11 +159,13 @@ describe("user config and codex auth paths", () => { }, }, null, - 2, + 2 ), - "utf8", + "utf8" ); - await expect(loadCodexChatGPTAuth()).rejects.toThrow("Codex auth mode is not ChatGPT"); + await expect(loadCodexChatGPTAuth()).rejects.toThrow( + "Codex auth mode is not ChatGPT" + ); }); }); diff --git a/test/shell-install.test.ts b/test/shell-install.test.ts index 6684ec2..15bf82b 100644 --- a/test/shell-install.test.ts +++ b/test/shell-install.test.ts @@ -2,7 +2,10 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { installShellIntegration, isShellIntegrationInstalled } from "../src/shell-install"; +import { + installShellIntegration, + isShellIntegrationInstalled, +} from "../src/shell-install"; describe("installShellIntegration(zsh)", () => { const originalZdotdir = process.env.ZDOTDIR; @@ -15,7 +18,7 @@ describe("installShellIntegration(zsh)", () => { afterEach(async () => { if (originalZdotdir === undefined) { - delete process.env.ZDOTDIR; + Reflect.deleteProperty(process.env, "ZDOTDIR"); } else { process.env.ZDOTDIR = originalZdotdir; } @@ -47,20 +50,20 @@ describe("installShellIntegration(zsh)", () => { await writeFile( zshrcPath, [ - "export PATH=\"$HOME/bin:$PATH\"", + 'export PATH="$HOME/bin:$PATH"', "# >>> tcomp integration >>>", "echo legacy block", "# <<< tcomp integration <<<", "", ].join("\n"), - "utf8", + "utf8" ); const result = await installShellIntegration("zsh"); expect(result.updated).toBe(true); const content = await readFile(zshrcPath, "utf8"); - expect(content).toContain("export PATH=\"$HOME/bin:$PATH\""); + expect(content).toContain('export PATH="$HOME/bin:$PATH"'); expect(content).toContain("# tcomp zsh integration"); const startMatches = content.match(/# >>> tcomp integration >>>/g) ?? []; @@ -87,7 +90,7 @@ describe("installShellIntegration(bash)", () => { afterEach(async () => { if (originalHome === undefined) { - delete process.env.HOME; + Reflect.deleteProperty(process.env, "HOME"); } else { process.env.HOME = originalHome; } @@ -119,20 +122,20 @@ describe("installShellIntegration(bash)", () => { await writeFile( bashrcPath, [ - "export PATH=\"$HOME/bin:$PATH\"", + 'export PATH="$HOME/bin:$PATH"', "# >>> tcomp integration >>>", "echo legacy block", "# <<< tcomp integration <<<", "", ].join("\n"), - "utf8", + "utf8" ); const result = await installShellIntegration("bash"); expect(result.updated).toBe(true); const content = await readFile(bashrcPath, "utf8"); - expect(content).toContain("export PATH=\"$HOME/bin:$PATH\""); + expect(content).toContain('export PATH="$HOME/bin:$PATH"'); expect(content).toContain("# tcomp bash integration"); const startMatches = content.match(/# >>> tcomp integration >>>/g) ?? []; diff --git a/test/shell.test.ts b/test/shell.test.ts index 66f04b3..17bef6c 100644 --- a/test/shell.test.ts +++ b/test/shell.test.ts @@ -4,7 +4,9 @@ import { renderShellIntegration } from "../src/shell"; describe("renderShellIntegration(zsh)", () => { test("includes command passthrough for setup/config/use/help/version", () => { const script = renderShellIntegration("zsh"); - expect(script).toContain("setup|config|use|auth|init|help|version|suggest|-h|--help|-v|--version"); + expect(script).toContain( + "setup|config|use|auth|init|help|version|suggest|-h|--help|-v|--version" + ); }); test("prefills BUFFER without printing generated command", () => { @@ -18,7 +20,9 @@ describe("renderShellIntegration(zsh)", () => { const script = renderShellIntegration("zsh"); expect(script).toContain("compdef _tcomp_complete tcomp terminal-complete"); expect(script).toContain("setup:run interactive onboarding"); - expect(script).toContain("config:show current config or rerun provider setup"); + expect(script).toContain( + "config:show current config or rerun provider setup" + ); expect(script).toContain("use:set active provider"); }); }); @@ -26,18 +30,24 @@ describe("renderShellIntegration(zsh)", () => { describe("renderShellIntegration(bash)", () => { test("includes command passthrough for setup/config/use/help/version", () => { const script = renderShellIntegration("bash"); - expect(script).toContain("setup|config|use|auth|init|help|version|suggest|-h|--help|-v|--version"); + expect(script).toContain( + "setup|config|use|auth|init|help|version|suggest|-h|--help|-v|--version" + ); }); test("supports editable prompt fallback and history", () => { const script = renderShellIntegration("bash"); - expect(script).toContain('read -r -e -i "$_tcomp_cmd" -p "tcomp> " _tcomp_edit'); + expect(script).toContain( + 'read -r -e -i "$_tcomp_cmd" -p "tcomp> " _tcomp_edit' + ); expect(script).toContain('history -s "$_tcomp_cmd"'); }); test("registers completion for tcomp and terminal-complete", () => { const script = renderShellIntegration("bash"); - expect(script).toContain("complete -o default -F _tcomp_complete tcomp terminal-complete"); + expect(script).toContain( + "complete -o default -F _tcomp_complete tcomp terminal-complete" + ); expect(script).toContain('compgen -W "setup config use help version"'); expect(script).toContain('compgen -W "codex openai"'); }); diff --git a/tsconfig.json b/tsconfig.json index 7d97add..e1634e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,4 +10,3 @@ }, "include": ["src/**/*.ts", "build.ts"] } -