@nazozokc/envcheck
Advanced tools
+5
-2
@@ -5,3 +5,3 @@ { | ||
| "type": "module", | ||
| "version": "1.0.0", | ||
| "version": "1.0.1", | ||
| "scripts": { | ||
@@ -22,3 +22,6 @@ "build": "bun build src/index.ts --outdir dist --target bun", | ||
| "execa": "^9.6.1" | ||
| } | ||
| }, | ||
| "files": [ | ||
| "dist/" | ||
| ] | ||
| } |
-166
| # AGENTS.md — envcheck | ||
| ## プロジェクト概要 | ||
| `envcheck` は、システムに特定のパッケージ(Node.js, npm, git など)が | ||
| 各種パッケージマネージャー経由でインストールされているかを確認する CLI ツール。 | ||
| - **言語**: TypeScript | ||
| - **ランタイム**: Bun | ||
| - **出力形式**: シンプルな OK / NG 表示 | ||
| --- | ||
| ## ディレクトリ構成 | ||
| ``` | ||
| envcheck/ | ||
| ├── src/ | ||
| │ ├── index.ts # エントリポイント・CLI引数パース | ||
| │ ├── checkers/ # パッケージマネージャーごとのチェッカー | ||
| │ │ ├── index.ts # チェッカーの共通インターフェース定義・エクスポート | ||
| │ │ ├── nix.ts | ||
| │ │ ├── pacman.ts | ||
| │ │ ├── apt.ts | ||
| │ │ ├── scoop.ts | ||
| │ │ ├── brew.ts | ||
| │ │ └── ... # 追加マネージャーはここに追加 | ||
| │ └── utils.ts # 共通ユーティリティ | ||
| ├── AGENTS.md | ||
| ├── package.json | ||
| ├── tsconfig.json | ||
| └── README.md | ||
| ``` | ||
| --- | ||
| ## 技術スタック・バージョン制約 | ||
| - **Bun**: 最新安定版を使う | ||
| - **TypeScript**: strict モード必須(`tsconfig.json` の `strict: true`) | ||
| - **依存ライブラリ**: 最小限に抑える。Commander.js / Consola / Clack は使ってよい | ||
| - `bun run` でビルド・実行すること。`npm run` は使わない | ||
| --- | ||
| ## チェッカーの実装ルール | ||
| ### 共通インターフェース | ||
| `src/checkers/index.ts` に以下を定義し、全チェッカーはこれに従う: | ||
| ```typescript | ||
| export interface Checker { | ||
| /** パッケージマネージャー名 (例: "nix", "apt") */ | ||
| name: string; | ||
| /** パッケージマネージャーが利用可能か確認する */ | ||
| isAvailable(): Promise<boolean>; | ||
| /** 指定パッケージがインストール済みか確認する */ | ||
| check(pkg: string): Promise<boolean>; | ||
| } | ||
| ``` | ||
| ### 新しいチェッカーを追加するとき | ||
| 1. `src/checkers/{name}.ts` を作成し `Checker` を実装する | ||
| 2. `src/checkers/index.ts` に export を追加する | ||
| 3. `src/index.ts` のチェッカー一覧に登録する | ||
| 4. README.md の対応マネージャー一覧を更新する | ||
| ### 対応パッケージマネージャー(実装対象) | ||
| | マネージャー | 確認コマンド例 | | ||
| |-------------|---------------| | ||
| | nix | `nix-env -q` / `nix profile list` | | ||
| | pacman | `pacman -Q {pkg}` | | ||
| | apt | `dpkg -l {pkg}` | | ||
| | scoop | `scoop list {pkg}` | | ||
| | homebrew | `brew list {pkg}` | | ||
| | winget | `winget list {pkg}` | | ||
| | dnf | `dnf list installed {pkg}` | | ||
| | zypper | `zypper se --installed-only {pkg}` | | ||
| | apk | `apk info {pkg}` | | ||
| | cargo | `cargo install --list` | | ||
| | pip | `pip show {pkg}` | | ||
| 追加は自由。上記が基本対象。 | ||
| --- | ||
| ## CLI 仕様 | ||
| ### 基本構文 | ||
| ``` | ||
| envcheck <manager> <package> [package...] | ||
| ``` | ||
| ### 使用例 | ||
| ```sh | ||
| # nix に nodejs と npm が入っているか確認 | ||
| envcheck nix nodejs npm | ||
| # apt に git が入っているか確認 | ||
| envcheck apt git | ||
| ``` | ||
| ### 出力フォーマット | ||
| ``` | ||
| [nix] nodejs ... OK | ||
| [nix] npm ... NG | ||
| ``` | ||
| - OK は緑、NG は赤で表示する(ターミナルカラー) | ||
| - 終了コード: 全て OK なら `0`、1 つでも NG なら `1` | ||
| ### エラーケース | ||
| - 指定したマネージャーが存在しない(未対応)→ エラーメッセージを出して終了コード `2` | ||
| - マネージャーがシステムに入っていない → 「{manager} is not available on this system」と表示して終了コード `2` | ||
| --- | ||
| ## コーディング規約 | ||
| - `async/await` を使う。コールバックは使わない | ||
| - エラーは絶対に握りつぶさない。`try/catch` で明示的に処理する | ||
| - 型は明示的に書く。`any` は禁止 | ||
| - `console.log` は使わない。Consola か Clack を使う | ||
| - 外部コマンドの実行は `Bun.spawnSync` か `execa` を使う | ||
| --- | ||
| ## テスト | ||
| - テストファイルは `src/__tests__/` に置く | ||
| - `bun test` で実行できるようにする | ||
| - 各チェッカーの `isAvailable()` と `check()` には必ずユニットテストを書く | ||
| - 外部コマンドはモックする(実際のシステムに依存しないこと) | ||
| --- | ||
| ## コミット規約 | ||
| Conventional Commits に従う: | ||
| ``` | ||
| feat: add homebrew checker | ||
| fix: fix pacman check command for meta packages | ||
| refactor: extract common spawn logic to utils | ||
| test: add unit tests for apt checker | ||
| docs: update supported managers in README | ||
| ``` | ||
| 1 チェッカー追加 = 1 コミットを基本とする。 | ||
| --- | ||
| ## やってはいけないこと | ||
| - `any` 型の使用 | ||
| - `console.log` の直接使用 | ||
| - チェッカーのインターフェースを変更する場合は、全チェッカーへの影響を必ず確認してから変更する | ||
| - ルートに直接ファイルを増やさない(`src/` 配下に置く) | ||
| - `npm install` / `npm run` の使用(Bun を使う) |
-106
| Default to using Bun instead of Node.js. | ||
| - Use `bun <file>` instead of `node <file>` or `ts-node <file>` | ||
| - Use `bun test` instead of `jest` or `vitest` | ||
| - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild` | ||
| - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` | ||
| - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>` | ||
| - Use `bunx <package> <command>` instead of `npx <package> <command>` | ||
| - Bun automatically loads .env, so don't use dotenv. | ||
| ## APIs | ||
| - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`. | ||
| - `bun:sqlite` for SQLite. Don't use `better-sqlite3`. | ||
| - `Bun.redis` for Redis. Don't use `ioredis`. | ||
| - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`. | ||
| - `WebSocket` is built-in. Don't use `ws`. | ||
| - Prefer `Bun.file` over `node:fs`'s readFile/writeFile | ||
| - Bun.$`ls` instead of execa. | ||
| ## Testing | ||
| Use `bun test` to run tests. | ||
| ```ts#index.test.ts | ||
| import { test, expect } from "bun:test"; | ||
| test("hello world", () => { | ||
| expect(1).toBe(1); | ||
| }); | ||
| ``` | ||
| ## Frontend | ||
| Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind. | ||
| Server: | ||
| ```ts#index.ts | ||
| import index from "./index.html" | ||
| Bun.serve({ | ||
| routes: { | ||
| "/": index, | ||
| "/api/users/:id": { | ||
| GET: (req) => { | ||
| return new Response(JSON.stringify({ id: req.params.id })); | ||
| }, | ||
| }, | ||
| }, | ||
| // optional websocket support | ||
| websocket: { | ||
| open: (ws) => { | ||
| ws.send("Hello, world!"); | ||
| }, | ||
| message: (ws, message) => { | ||
| ws.send(message); | ||
| }, | ||
| close: (ws) => { | ||
| // handle close | ||
| } | ||
| }, | ||
| development: { | ||
| hmr: true, | ||
| console: true, | ||
| } | ||
| }) | ||
| ``` | ||
| HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle. | ||
| ```html#index.html | ||
| <html> | ||
| <body> | ||
| <h1>Hello, world!</h1> | ||
| <script type="module" src="./frontend.tsx"></script> | ||
| </body> | ||
| </html> | ||
| ``` | ||
| With the following `frontend.tsx`: | ||
| ```tsx#frontend.tsx | ||
| import React from "react"; | ||
| import { createRoot } from "react-dom/client"; | ||
| // import .css files directly and it works | ||
| import './index.css'; | ||
| const root = createRoot(document.body); | ||
| export default function Frontend() { | ||
| return <h1>Hello, world!</h1>; | ||
| } | ||
| root.render(<Frontend />); | ||
| ``` | ||
| Then, run index.ts | ||
| ```sh | ||
| bun --hot ./index.ts | ||
| ``` | ||
| For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. |
| import { describe, it, expect, vi, beforeEach, afterEach } from 'bun:test'; | ||
| import { nixChecker } from '../checkers/nix'; | ||
| import { aptChecker } from '../checkers/apt'; | ||
| import { pacmanChecker } from '../checkers/pacman'; | ||
| import { dnfChecker } from '../checkers/dnf'; | ||
| import { zypperChecker } from '../checkers/zypper'; | ||
| import { apkChecker } from '../checkers/apk'; | ||
| import { cargoChecker } from '../checkers/cargo'; | ||
| import { pipChecker } from '../checkers/pip'; | ||
| import { scoopChecker } from '../checkers/scoop'; | ||
| import { wingetChecker } from '../checkers/winget'; | ||
| import { brewChecker } from '../checkers/brew'; | ||
| // Mock execa globally | ||
| vi.mock('execa', async () => { | ||
| const actual = await vi.importActual('execa'); | ||
| return { | ||
| ...actual, | ||
| default: vi.fn(), | ||
| }; | ||
| }); | ||
| describe('Checker Interface - Basic Structure', () => { | ||
| const checkers = [ | ||
| nixChecker, | ||
| aptChecker, | ||
| pacmanChecker, | ||
| dnfChecker, | ||
| zypperChecker, | ||
| apkChecker, | ||
| cargoChecker, | ||
| pipChecker, | ||
| scoopChecker, | ||
| wingetChecker, | ||
| brewChecker, | ||
| ]; | ||
| checkers.forEach(checker => { | ||
| describe(`${checker.name}Checker`, () => { | ||
| it('should have a name property', () => { | ||
| expect(checker.name).toBeDefined(); | ||
| expect(typeof checker.name).toBe('string'); | ||
| }); | ||
| it('should have isAvailable method', () => { | ||
| expect(typeof checker.isAvailable).toBe('function'); | ||
| }); | ||
| it('should have check method', () => { | ||
| expect(typeof checker.check).toBe('function'); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| describe('Checker Functionality', () => { | ||
| // Get the mocked execa | ||
| const mockExeca = require('execa').default; | ||
| beforeEach(() => { | ||
| vi.resetAllMocks(); | ||
| }); | ||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
| }); | ||
| describe('Simple Checkers (apt, pacman, dnf, zypper, apk, cargo, pip)', () => { | ||
| const simpleCheckers = [ | ||
| { checker: aptChecker, command: ['dpkg'] }, | ||
| { checker: pacmanChecker, command: ['pacman'] }, | ||
| { checker: dnfChecker, command: ['dnf'] }, | ||
| { checker: zypperChecker, command: ['zypper'] }, | ||
| { checker: apkChecker, command: ['apk'] }, | ||
| { checker: cargoChecker, command: ['cargo', 'install', '--list'] }, | ||
| { checker: pipChecker, command: ['pip', 'show'] }, | ||
| ]; | ||
| simpleCheckers.forEach(({ checker, command }) => { | ||
| describe(`${checker.name}Checker`, () => { | ||
| it('should return true for isAvailable when command succeeds', async () => { | ||
| mockExeca.mockResolvedValueOnce({}); | ||
| const result = await checker.isAvailable(); | ||
| expect(result).toBe(true); | ||
| expect(mockExeca).toHaveBeenCalledWith(command[0], [...command.slice(1), '--version']); | ||
| }); | ||
| it('should return false for isAvailable when command fails', async () => { | ||
| mockExeca.mockRejectedValueOnce(new Error('command not found')); | ||
| const result = await checker.isAvailable(); | ||
| expect(result).toBe(false); | ||
| }); | ||
| it('should return true for check when package check succeeds', async () => { | ||
| mockExeca.mockResolvedValueOnce({}); | ||
| const result = await checker.check('testpkg'); | ||
| expect(result).toBe(true); | ||
| expect(mockExeca).toHaveBeenCalledWith(command[0], [...command.slice(1), 'testpkg']); | ||
| }); | ||
| it('should return false for check when package check fails', async () => { | ||
| mockExeca.mockRejectedValueOnce(new Error('package not found')); | ||
| const result = await checker.check('testpkg'); | ||
| expect(result).toBe(false); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| describe('List Checkers (brew)', () => { | ||
| describe('brewChecker', () => { | ||
| it('should return true for isAvailable when brew command succeeds', async () => { | ||
| mockExeca.mockResolvedValueOnce({}); | ||
| const result = await brewChecker.isAvailable(); | ||
| expect(result).toBe(true); | ||
| expect(mockExeca).toHaveBeenCalledWith('brew', ['--version']); | ||
| }); | ||
| it('should return false for isAvailable when brew command fails', async () => { | ||
| mockExeca.mockRejectedValueOnce(new Error('command not found')); | ||
| const result = await brewChecker.isAvailable(); | ||
| expect(result).toBe(false); | ||
| }); | ||
| it('should return true for check when package is in list', async () => { | ||
| mockExeca.mockResolvedValueOnce({ stdout: 'git\nnode\npython\n' }); | ||
| const result = await brewChecker.check('node'); | ||
| expect(result).toBe(true); | ||
| expect(mockExeca).toHaveBeenCalledWith('brew', ['list']); | ||
| }); | ||
| it('should return false for check when package is not in list', async () => { | ||
| mockExeca.mockResolvedValueOnce({ stdout: 'git\npython\n' }); | ||
| const result = await brewChecker.check('node'); | ||
| expect(result).toBe(false); | ||
| }); | ||
| }); | ||
| }); | ||
| describe('Special Checkers (nix, scoop, winget)', () => { | ||
| describe('nixChecker', () => { | ||
| it('should return true for isAvailable when nix command succeeds', async () => { | ||
| mockExeca.mockResolvedValueOnce({}); | ||
| const result = await nixChecker.isAvailable(); | ||
| expect(result).toBe(true); | ||
| expect(mockExeca).toHaveBeenCalledWith('nix', ['--version']); | ||
| }); | ||
| it('should return false for isAvailable when nix command fails', async () => { | ||
| mockExeca.mockRejectedValueOnce(new Error('command not found')); | ||
| const result = await nixChecker.isAvailable(); | ||
| expect(result).toBe(false); | ||
| }); | ||
| it('should return true for check when package is found via nix-env', async () => { | ||
| mockExeca.mockResolvedValueOnce({}); | ||
| const result = await nixChecker.check('nodejs'); | ||
| expect(result).toBe(true); | ||
| expect(mockExeca).toHaveBeenCalledWith('nix-env', ['-q', 'nodejs']); | ||
| }); | ||
| it('should return true for check when package is found via nix profile list', async () => { | ||
| mockExeca | ||
| .mockRejectedValueOnce(new Error('package not found')) // nix-env fails | ||
| .mockResolvedValueOnce({ stdout: 'nodejs\nnpm\n' }); // nix profile list succeeds | ||
| const result = await nixChecker.check('nodejs'); | ||
| expect(result).toBe(true); | ||
| }); | ||
| it('should return false for check when package not found in either method', async () => { | ||
| mockExeca | ||
| .mockRejectedValueOnce(new Error('package not found')) // nix-env fails | ||
| .mockRejectedValueOnce(new Error('command failed')); // nix profile list fails | ||
| const result = await nixChecker.check('nonexistent'); | ||
| expect(result).toBe(false); | ||
| }); | ||
| }); | ||
| describe('scoopChecker', () => { | ||
| it('should return true for isAvailable when scoop command succeeds', async () => { | ||
| mockExeca.mockResolvedValueOnce({}); | ||
| const result = await scoopChecker.isAvailable(); | ||
| expect(result).toBe(true); | ||
| expect(mockExeca).toHaveBeenCalledWith('scoop', ['--version']); | ||
| }); | ||
| it('should return false for isAvailable when scoop command fails', async () => { | ||
| mockExeca.mockRejectedValueOnce(new Error('command not found')); | ||
| const result = await scoopChecker.isAvailable(); | ||
| expect(result).toBe(false); | ||
| }); | ||
| it('should return true for check when package is found in scoop list', async () => { | ||
| mockExeca.mockResolvedValueOnce({ stdout: 'git 2.40.0\nnode 18.17.0\n' }); | ||
| const result = await scoopChecker.check('node'); | ||
| expect(result).toBe(true); | ||
| expect(mockExeca).toHaveBeenCalledWith('scoop', ['list']); | ||
| }); | ||
| it('should return false for check when package is not found in scoop list', async () => { | ||
| mockExeca.mockResolvedValueOnce({ stdout: 'git 2.40.0\npython 3.11.0\n' }); | ||
| const result = await scoopChecker.check('node'); | ||
| expect(result).toBe(false); | ||
| }); | ||
| }); | ||
| describe('wingetChecker', () => { | ||
| it('should return true for isAvailable when winget command succeeds', async () => { | ||
| mockExeca.mockResolvedValueOnce({}); | ||
| const result = await wingetChecker.isAvailable(); | ||
| expect(result).toBe(true); | ||
| expect(mockExeca).toHaveBeenCalledWith('winget', ['--version']); | ||
| }); | ||
| it('should return false for isAvailable when winget command fails', async () => { | ||
| mockExeca.mockRejectedValueOnce(new Error('command not found')); | ||
| const result = await wingetChecker.isAvailable(); | ||
| expect(result).toBe(false); | ||
| }); | ||
| it('should return true for check when package is found in winget list', async () => { | ||
| mockExeca.mockResolvedValueOnce({ stdout: 'Git.Git 2.40.0\nNodejs.NodeJS 18.17.0\n' }); | ||
| const result = await wingetChecker.check('Nodejs.NodeJS'); | ||
| expect(result).toBe(true); | ||
| expect(mockExeca).toHaveBeenCalledWith('winget', ['list']); | ||
| }); | ||
| it('should return false for check when package is not found in winget list', async () => { | ||
| mockExeca.mockResolvedValueOnce({ stdout: 'Git.Git 2.40.0\nPython.Python 3.11.0\n' }); | ||
| const result = await wingetChecker.check('Nodejs.NodeJS'); | ||
| expect(result).toBe(false); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
| import { describe, it, expect } from 'bun:test'; | ||
| describe('CLI Index File', () => { | ||
| it('should exist', () => { | ||
| expect(true).toBe(true); | ||
| }); | ||
| }); |
| import { execa } from 'execa'; | ||
| import { createSimpleChecker } from './baseChecker'; | ||
| export const apkChecker = createSimpleChecker('apk', ['apk']); |
| import { execa } from 'execa'; | ||
| import { createSimpleChecker } from './baseChecker'; | ||
| export const aptChecker = createSimpleChecker('apt', ['dpkg']); |
| import { ExecaError, ExecaReturnValue } from 'execa'; | ||
| /** | ||
| * Base checker utility to reduce code duplication for checkers that: | ||
| * - Check availability by running a version-like command | ||
| * - Check package by running a command that takes the package as an argument and exits with 0 if installed | ||
| */ | ||
| export const createSimpleChecker = (name: string, baseCommand: string[]) => { | ||
| return { | ||
| name, | ||
| async isAvailable(): Promise<boolean> { | ||
| try { | ||
| // Try to get version to check availability | ||
| const versionCmd = [...baseCommand.slice(0, 1), '--version']; | ||
| await execa(versionCmd[0], versionCmd.slice(1)); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }, | ||
| async check(pkg: string): Promise<boolean> { | ||
| try { | ||
| await execa(baseCommand[0], [...baseCommand.slice(1), pkg]); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| }; | ||
| }; | ||
| /** | ||
| * Base checker utility for checkers that: | ||
| * - Check availability by running a version-like command | ||
| * - Check package by running a command that outputs a list of installed packages and checking if the package is in that list | ||
| */ | ||
| export const createListChecker = (name: string, listCommand: string[]) => { | ||
| return { | ||
| name, | ||
| async isAvailable(): Promise<boolean> { | ||
| try { | ||
| // Try to get version to check availability | ||
| const versionCmd = [...listCommand.slice(0, 1), '--version']; | ||
| await execa(versionCmd[0], versionCmd.slice(1)); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }, | ||
| async check(pkg: string): Promise<boolean> { | ||
| try { | ||
| const { stdout } = await execa(listCommand[0], listCommand.slice(1)); | ||
| const lines = stdout.trim().split('\n').map(line => line.trim()); | ||
| return lines.includes(pkg); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| }; | ||
| }; | ||
| // Import execa here to avoid circular dependencies | ||
| import { execa } from 'execa'; |
| import { execa } from 'execa'; | ||
| import { createListChecker } from './baseChecker'; | ||
| export const brewChecker = createListChecker('brew', ['brew', 'list']); |
| import { execa } from 'execa'; | ||
| import { createSimpleChecker } from './baseChecker'; | ||
| export const cargoChecker = createSimpleChecker('cargo', ['cargo', 'install', '--list']); |
| import { execa } from 'execa'; | ||
| import { createSimpleChecker } from './baseChecker'; | ||
| export const dnfChecker = createSimpleChecker('dnf', ['dnf']); |
| export interface Checker { | ||
| /** パッケージマネージャー名 (例: "nix", "apt") */ | ||
| name: string; | ||
| /** パッケージマネージャーが利用可能か確認する */ | ||
| isAvailable(): Promise<boolean>; | ||
| /** 指定パッケージがインストール済みか確認する */ | ||
| check(pkg: string): Promise<boolean>; | ||
| } |
| import { execa } from 'execa'; | ||
| export const nixChecker = { | ||
| name: 'nix', | ||
| async isAvailable(): Promise<boolean> { | ||
| try { | ||
| await execa('nix', ['--version']); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }, | ||
| async check(pkg: string): Promise<boolean> { | ||
| try { | ||
| // Try nix-env -q first | ||
| await execa('nix-env', ['-q', pkg]); | ||
| return true; | ||
| } catch { | ||
| try { | ||
| // Fallback to nix profile list | ||
| const { stdout } = await execa('nix', ['profile', 'list']); | ||
| return stdout.includes(pkg); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| }; |
| import { execa } from 'execa'; | ||
| import { createSimpleChecker } from './baseChecker'; | ||
| export const pacmanChecker = createSimpleChecker('pacman', ['pacman']); |
| import { execa } from 'execa'; | ||
| import { createSimpleChecker } from './baseChecker'; | ||
| export const pipChecker = createSimpleChecker('pip', ['pip', 'show']); |
| import { execa } from 'execa'; | ||
| export const scoopChecker = { | ||
| name: 'scoop', | ||
| async isAvailable(): Promise<boolean> { | ||
| try { | ||
| await execa('scoop', ['--version']); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }, | ||
| async check(pkg: string): Promise<boolean> { | ||
| try { | ||
| const { stdout } = await execa('scoop', ['list']); | ||
| // Scoop list output format: name version | ||
| // We'll check if the package name appears in the list (first column) | ||
| const lines = stdout.split('\n'); | ||
| for (const line of lines) { | ||
| if (line.trim() && line.split(/\s+/)[0] === pkg) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| }; |
| import { execa } from 'execa'; | ||
| export const wingetChecker = { | ||
| name: 'winget', | ||
| async isAvailable(): Promise<boolean> { | ||
| try { | ||
| await execa('winget', ['--version']); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }, | ||
| async check(pkg: string): Promise<boolean> { | ||
| try { | ||
| const { stdout } = await execa('winget', ['list']); | ||
| // Winget list output format: Id Version Available Source | ||
| // We'll check if the package Id appears in the list | ||
| const lines = stdout.split('\n'); | ||
| for (const line of lines) { | ||
| if (line.trim() && line.split(/\s+/)[0] === pkg) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| }; |
| import { execa } from 'execa'; | ||
| import { createSimpleChecker } from './baseChecker'; | ||
| export const zypperChecker = createSimpleChecker('zypper', ['zypper']); |
-77
| #!/usr/bin/env node | ||
| import { Command } from "commander"; | ||
| import { consola } from "consola"; | ||
| import { nixChecker } from "./checkers/nix.js"; | ||
| import { aptChecker } from "./checkers/apt.js"; | ||
| import { pacmanChecker } from "./checkers/pacman.js"; | ||
| import { scoopChecker } from "./checkers/scoop.js"; | ||
| import { brewChecker } from "./checkers/brew.js"; | ||
| import { wingetChecker } from "./checkers/winget.js"; | ||
| import { dnfChecker } from "./checkers/dnf.js"; | ||
| import { zypperChecker } from "./checkers/zypper.js"; | ||
| import { apkChecker } from "./checkers/apk.js"; | ||
| import { cargoChecker } from "./checkers/cargo.js"; | ||
| import { pipChecker } from "./checkers/pip.js"; | ||
| // Map of checker names to checker instances | ||
| const checkers: Record<string, any> = { | ||
| nix: nixChecker, | ||
| apt: aptChecker, | ||
| pacman: pacmanChecker, | ||
| scoop: scoopChecker, | ||
| brew: brewChecker, | ||
| winget: wingetChecker, | ||
| dnf: dnfChecker, | ||
| zypper: zypperChecker, | ||
| apk: apkChecker, | ||
| cargo: cargoChecker, | ||
| pip: pipChecker, | ||
| }; | ||
| const program = new Command(); | ||
| program | ||
| .name("envcheck") | ||
| .description( | ||
| "CLI tool to check if packages are installed via various package managers", | ||
| ) | ||
| .argument( | ||
| "<manager>", | ||
| "package manager to use (nix, apt, pacman, scoop, brew, winget, dnf, zypper, apk, cargo, pip)", | ||
| ) | ||
| .argument("<packages...>", "packages to check") | ||
| .action(async (manager, packages) => { | ||
| const checker = checkers[manager]; | ||
| if (!checker) { | ||
| consola.error(`Unsupported package manager: ${manager}`); | ||
| consola.info(`Supported managers: ${Object.keys(checkers).join(", ")}`); | ||
| process.exit(2); | ||
| } | ||
| if (!(await checker.isAvailable())) { | ||
| consola.error(`${manager} is not available on this system`); | ||
| process.exit(2); | ||
| } | ||
| let allOk = true; | ||
| for (const pkg of packages) { | ||
| const isInstalled = await checker.check(pkg); | ||
| const status = isInstalled ? "OK" : "NG"; | ||
| const color = isInstalled ? "green" : "red"; | ||
| // Pad package name for alignment (max 20 chars) | ||
| const paddedPkg = pkg.padEnd(20); | ||
| consola[color](`[${manager}] ${paddedPkg} ... ${status}`); | ||
| if (!isInstalled) { | ||
| allOk = false; | ||
| } | ||
| } | ||
| process.exit(allOk ? 0 : 1); | ||
| }); | ||
| program.parse(); | ||
| { | ||
| "compilerOptions": { | ||
| // Environment setup & latest features | ||
| "lib": ["ESNext"], | ||
| "target": "ESNext", | ||
| "module": "Preserve", | ||
| "moduleDetection": "force", | ||
| "jsx": "react-jsx", | ||
| "allowJs": true, | ||
| // Bundler mode | ||
| "moduleResolution": "bundler", | ||
| "allowImportingTsExtensions": true, | ||
| "verbatimModuleSyntax": true, | ||
| "noEmit": false, | ||
| // Best practices | ||
| "strict": true, | ||
| "skipLibCheck": true, | ||
| "noFallthroughCasesInSwitch": true, | ||
| "noUncheckedIndexedAccess": true, | ||
| "noImplicitOverride": true, | ||
| // Some stricter flags (disabled by default) | ||
| "noUnusedLocals": false, | ||
| "noUnusedParameters": false, | ||
| "noPropertyAccessFromIndexSignature": false | ||
| } | ||
| } |
425505
-5.63%3
-86.36%11200
-4.06%