🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@nazozokc/envcheck

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nazozokc/envcheck - npm Package Compare versions

Comparing version
1.0.0
to
1.0.1
+5
-2
package.json

@@ -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/"
]
}
# 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 を使う)
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']);
#!/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
}
}