bizrouter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/spawn.js ADDED
@@ -0,0 +1,92 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { accessSync, constants, existsSync } from 'node:fs';
3
+ import { delimiter, join } from 'node:path';
4
+ import { c, CliError, print } from './ui.js';
5
+ const INSTALL_HINTS = {
6
+ claude: 'npm install -g @anthropic-ai/claude-code (또는 https://code.claude.com/docs/en/quickstart)',
7
+ codex: 'npm install -g @openai/codex (또는 brew install codex)',
8
+ opencode: 'npm install -g opencode-ai (또는 brew install opencode)',
9
+ hermes: 'curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash',
10
+ };
11
+ export function whichBinary(bin) {
12
+ const paths = (process.env.PATH ?? '').split(delimiter).filter(Boolean);
13
+ const exts = process.platform === 'win32' ? ['.cmd', '.exe', '.bat', ''] : [''];
14
+ for (const dir of paths) {
15
+ for (const ext of exts) {
16
+ const candidate = join(dir, bin + ext);
17
+ if (!existsSync(candidate))
18
+ continue;
19
+ try {
20
+ accessSync(candidate, constants.X_OK);
21
+ return candidate;
22
+ }
23
+ catch {
24
+ // not executable, keep looking
25
+ }
26
+ }
27
+ }
28
+ return undefined;
29
+ }
30
+ export function binaryVersion(bin) {
31
+ const path = whichBinary(bin);
32
+ if (!path)
33
+ return undefined;
34
+ const result = spawnSync(path, ['--version'], { encoding: 'utf8', timeout: 8000 });
35
+ const text = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim().split('\n')[0];
36
+ return text || '(버전 확인 불가)';
37
+ }
38
+ const SECRET_ENV = new Set(['BIZROUTER_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CUSTOM_API_KEY', 'OPENAI_API_KEY']);
39
+ export function describePlan(plan, maskValue) {
40
+ const lines = [];
41
+ for (const [k, v] of Object.entries(plan.env)) {
42
+ const shown = SECRET_ENV.has(k) ? maskValue(v) : k === 'OPENCODE_CONFIG_CONTENT' ? `${v.slice(0, 80)}…` : v;
43
+ lines.push(` ${c.cyan(k)}=${shown === '' ? '""' : shown}`);
44
+ }
45
+ const argText = plan.args.map((a) => (/[\s"']/.test(a) ? JSON.stringify(a) : a)).join(' ');
46
+ lines.push(` ${c.bold(plan.bin)} ${argText}`);
47
+ return lines.join('\n');
48
+ }
49
+ export async function runPlan(plan, options) {
50
+ if (options.dryRun) {
51
+ print(describePlan(plan, options.maskValue));
52
+ plan.cleanup?.();
53
+ return 0;
54
+ }
55
+ const binPath = whichBinary(plan.bin);
56
+ if (!binPath) {
57
+ plan.cleanup?.();
58
+ throw new CliError(`\`${plan.bin}\` 실행 파일을 PATH 에서 찾을 수 없습니다.`, {
59
+ hint: INSTALL_HINTS[plan.bin] ? `먼저 설치하세요: ${INSTALL_HINTS[plan.bin]}` : undefined,
60
+ });
61
+ }
62
+ return new Promise((resolve, reject) => {
63
+ const child = spawn(binPath, plan.args, {
64
+ stdio: 'inherit',
65
+ env: { ...process.env, ...plan.env },
66
+ });
67
+ const forward = (signal) => () => {
68
+ if (!child.killed)
69
+ child.kill(signal);
70
+ };
71
+ const onInt = forward('SIGINT');
72
+ const onTerm = forward('SIGTERM');
73
+ process.on('SIGINT', onInt);
74
+ process.on('SIGTERM', onTerm);
75
+ const done = () => {
76
+ process.off('SIGINT', onInt);
77
+ process.off('SIGTERM', onTerm);
78
+ plan.cleanup?.();
79
+ };
80
+ child.on('error', (error) => {
81
+ done();
82
+ reject(new CliError(`\`${plan.bin}\` 실행에 실패했습니다: ${error.message}`));
83
+ });
84
+ child.on('exit', (code, signal) => {
85
+ done();
86
+ if (signal)
87
+ resolve(128 + (signal === 'SIGINT' ? 2 : signal === 'SIGTERM' ? 15 : 1));
88
+ else
89
+ resolve(code ?? 0);
90
+ });
91
+ });
92
+ }
package/dist/ui.js ADDED
@@ -0,0 +1,67 @@
1
+ // Terminal output helpers. Korean is the product language of the BizRouter
2
+ // console, so every user-facing string here is Korean (합니다체).
3
+ const useColor = (() => {
4
+ if (process.env.NO_COLOR !== undefined)
5
+ return false;
6
+ if (process.env.FORCE_COLOR !== undefined)
7
+ return true;
8
+ return Boolean(process.stderr.isTTY);
9
+ })();
10
+ function paint(code, text) {
11
+ return useColor ? `[${code}m${text}` : text;
12
+ }
13
+ export const c = {
14
+ bold: (t) => paint('1', t),
15
+ dim: (t) => paint('2', t),
16
+ red: (t) => paint('31', t),
17
+ green: (t) => paint('32', t),
18
+ yellow: (t) => paint('33', t),
19
+ cyan: (t) => paint('36', t),
20
+ violet: (t) => paint('35', t),
21
+ };
22
+ const TAG = c.violet('[bizrouter]');
23
+ export function info(message) {
24
+ process.stderr.write(`${TAG} ${message}\n`);
25
+ }
26
+ export function ok(message) {
27
+ process.stderr.write(`${TAG} ${c.green('✔')} ${message}\n`);
28
+ }
29
+ export function warn(message) {
30
+ process.stderr.write(`${TAG} ${c.yellow('⚠')} ${message}\n`);
31
+ }
32
+ export function fail(message) {
33
+ process.stderr.write(`${TAG} ${c.red('✖')} ${message}\n`);
34
+ }
35
+ export function print(message = '') {
36
+ process.stdout.write(`${message}\n`);
37
+ }
38
+ /** Show only the key prefix and the last four characters. */
39
+ export function maskKey(key) {
40
+ if (key.length <= 14)
41
+ return `${key.slice(0, 6)}…`;
42
+ return `${key.slice(0, 10)}…${key.slice(-4)}`;
43
+ }
44
+ export class CliError extends Error {
45
+ exitCode;
46
+ hint;
47
+ constructor(message, options = {}) {
48
+ super(message);
49
+ this.name = 'CliError';
50
+ this.exitCode = options.exitCode ?? 1;
51
+ this.hint = options.hint;
52
+ }
53
+ }
54
+ export function formatKrw(value) {
55
+ if (!Number.isFinite(value))
56
+ return '-';
57
+ if (value >= 100)
58
+ return `${Math.round(value).toLocaleString('ko-KR')}원`;
59
+ return `${value.toLocaleString('ko-KR', { maximumFractionDigits: 1 })}원`;
60
+ }
61
+ export function padEnd(text, width) {
62
+ // East Asian wide characters take two columns.
63
+ let columns = 0;
64
+ for (const ch of text)
65
+ columns += /[ᄀ-ᇿ -鿿가-힯＀-￯]/.test(ch) ? 2 : 1;
66
+ return text + ' '.repeat(Math.max(0, width - columns));
67
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "bizrouter",
3
+ "version": "0.1.0",
4
+ "description": "BizRouter CLI - run Claude Code, Codex, OpenCode, and Hermes through BizRouter with one command",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "bin": {
8
+ "bizrouter": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/**/*",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18.17"
16
+ },
17
+ "scripts": {
18
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/stamp-version.mjs && chmod +x dist/index.js",
19
+ "typecheck": "tsc --noEmit",
20
+ "test": "bun test",
21
+ "prepublishOnly": "bun run typecheck && bun test && bun run build"
22
+ },
23
+ "keywords": [
24
+ "bizrouter",
25
+ "llm-gateway",
26
+ "claude-code",
27
+ "codex",
28
+ "opencode",
29
+ "hermes"
30
+ ],
31
+ "homepage": "https://bizrouter.ai/docs/cli",
32
+ "devDependencies": {
33
+ "@types/bun": "1.2.20",
34
+ "@types/node": "24.2.0",
35
+ "typescript": "5.9.3"
36
+ }
37
+ }