@yhotamos/enja-cli 1.0.0 → 1.0.1

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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js CHANGED
@@ -1,67 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync } from "fs";
3
- import { Command } from 'commander';
4
- import { translate } from "./commands/translate.js";
5
- import { history } from "./commands/history.js";
6
- import { config } from "./commands/config.js";
7
- const pkgJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
8
- const program = new Command();
9
- program
10
- .name('enja')
11
- .usage('[arguments] [options]')
12
- .description(`Description: ${pkgJson.description}`)
13
- .version(pkgJson.version, '-v, --version', 'output the current version');
14
- // 翻訳コマンド (デフォルト)
15
- program
16
- .argument('[text]', 'テキストを翻訳する')
17
- .option('-f, --file <path>', 'ファイルを翻訳する')
18
- .option('-o, --output <path>', 'ファイルに出力する (デフォルト: 標準出力)')
19
- .option('-s, --strip-html', 'HTMLタグを除去してから翻訳する')
20
- .option('-N, --no-cache', 'キャッシュを使用せずに再翻訳する')
21
- .option('-F, --flip', '翻訳方向を逆にする (デフォルト: 英語→日本語)')
22
- .option('--endpoint <url>', 'カスタム翻訳エンドポイントを指定')
23
- .option('--api-key <key>', 'API キーを指定')
24
- .option('--provider <name>', '翻訳プロバイダーを指定 (gas, custom)')
25
- .showHelpAfterError()
26
- .addHelpText('after', `\nExamples:
27
- $ enja "Hello, world!" # 引数で渡された文字列を翻訳
28
- $ git --help | enja # パイプ(標準入力)で渡されたテキストを翻訳
29
- $ enja -f input.txt # ファイルからテキストを読み込んで翻訳
30
- $ enja -f input.txt -o output.txt # ファイルから読み込み,翻訳結果をファイルに保存
31
- $ cat README.md | enja -o japanese.md # パイプとファイル出力の組み合わせ
32
- $ curl -s https://example.com | enja -s # HTMLタグを除去して翻訳
33
- $ enja "Hello" --endpoint https://api.example.com/translate --api-key YOUR_KEY # カスタムエンドポイント`)
34
- .addHelpText('afterAll', `\nEnja CLI v${pkgJson.version}`)
35
- .addHelpText('afterAll', 'Copyright (c) 2025 yhotta240')
36
- .addHelpText('afterAll', 'GitHub: https://github.com/yhotamos/enja-cli')
37
- .action(translate);
38
- // 履歴コマンド
39
- program
40
- .command('history')
41
- .description('翻訳履歴を表示する')
42
- .argument('[id]', 'ID で履歴を表示する(完全 ID または短縮 ID)')
43
- .option('-d, --detail', '詳細表示')
44
- .option('-n, --number <number>', '表示件数 (デフォルト: 10)', '10')
45
- .option('--delete <id>', '特定の履歴を削除する')
46
- .option('--clear', '履歴をクリア')
47
- .action(history);
48
- // 設定コマンド
49
- program
50
- .command('config')
51
- .description('設定を管理する')
52
- .argument('[key]', '設定キー (endpoint, api-key, provider)')
53
- .argument('[value]', '設定値')
54
- .option('-l, --list', '設定を一覧表示')
55
- .option('--unset <key>', '設定を削除(デフォルトに戻す)')
56
- .option('--reset', 'すべての設定をリセット')
57
- .addHelpText('after', `\nExamples:
58
- $ enja config # すべての設定を表示
59
- $ enja config --list # すべての設定を表示
60
- $ enja config endpoint # endpoint の値を表示
61
- $ enja config endpoint <URL> # endpoint を設定
62
- $ enja config api-key <KEY> # API キーを設定
63
- $ enja config provider gas # プロバイダーを設定
64
- $ enja config --unset api-key # API キーを削除
65
- $ enja config --reset # すべての設定をリセット`)
66
- .action(config);
67
- program.parse();
2
+ import*as c from'fs';import {readFileSync,promises}from'fs';import {Command}from'commander';import Y from'ora';import*as d from'path';import*as C from'os';import {randomUUID,createHash}from'crypto';import l from'kleur';var x=class{apiUrl;apiKey;constructor(t,r){this.apiUrl=t,this.apiKey=r;}async translate(t,r,o){try{let i={"Content-Type":"application/json"};this.apiKey&&(i.Authorization=`Bearer ${this.apiKey}`);let n=await fetch(this.apiUrl,{method:"POST",headers:i,body:JSON.stringify({text:t,sourceLang:r,targetLang:o})});if(!n.ok)throw new Error(`HTTP ${n.status} ${n.statusText}`);let s=await n.json();if(s.code!==200||!s.translatedText)throw new Error(`${s.error||"\u7FFB\u8A33\u306B\u5931\u6557\u3057\u307E\u3057\u305F"}`);return {text:s.translatedText,detectedSourceLang:s.detectedSourceLang}}catch(i){throw i}}};function m(){if(process.platform==="win32"){let r=process.env.APPDATA||d.join(C.homedir(),"AppData","Roaming");if(!r)throw new Error("APPDATA \u74B0\u5883\u5909\u6570\u304C\u8A2D\u5B9A\u3055\u308C\u3066\u304A\u3089\u305A\uFF0C\u4EE3\u66FF\u30D1\u30B9\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F");return d.join(r,"enja-cli")}let t=C.homedir();return d.join(t,".config","enja-cli")}function A(){return d.join(m(),"history.json")}function L(){return d.join(m(),"config.json")}var h={provider:"gas",endpoint:"https://script.google.com/macros/s/AKfycbxOSbKD0aBTaQqIzHv00BMzp6WwrtWHBU3gJY0vhB2HblgUO-cgesfT1l-rrfttnWZzew/exec",apiKey:void 0},y=class{filePath;constructor(){this.filePath=L();}ensureConfigDir(){let t=m();c.existsSync(t)||c.mkdirSync(t,{recursive:true});}async readConfig(){try{if(!c.existsSync(this.filePath))return {...h};let t=c.readFileSync(this.filePath,"utf-8"),r=JSON.parse(t);return {...h,...r}}catch{return console.warn("\u8A2D\u5B9A\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF0E\u898F\u5B9A\u5024\u3092\u4F7F\u7528\u3057\u307E\u3059"),{...h}}}async writeConfig(t){try{this.ensureConfigDir(),c.writeFileSync(this.filePath,JSON.stringify(t,null,2),"utf-8");}catch{throw new Error("\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u306E\u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F")}}async get(){return await this.readConfig()}async set(t,r){let o=await this.readConfig();switch(t){case "endpoint":o.endpoint=r;break;case "api-key":o.apiKey=r;break;case "provider":if(r!=="gas"&&r!=="custom")throw new Error(`\u7121\u52B9\u306A\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC (${r}): gas \u307E\u305F\u306F custom \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044`);o.provider=r;break;default:throw new Error(`\u7121\u52B9\u306A\u8A2D\u5B9A\u30AD\u30FC (${t})`)}await this.writeConfig(o);}async unset(t){let r=await this.readConfig();switch(t){case "endpoint":r.endpoint=h.endpoint;break;case "api-key":r.apiKey=void 0;break;case "provider":r.provider=h.provider;break;default:throw new Error(`\u7121\u52B9\u306A\u8A2D\u5B9A\u30AD\u30FC (${t})`)}await this.writeConfig(r);}async reset(){await this.writeConfig({...h});}};var J="https://script.google.com/macros/s/AKfycbxOSbKD0aBTaQqIzHv00BMzp6WwrtWHBU3gJY0vhB2HblgUO-cgesfT1l-rrfttnWZzew/exec";async function K(e){let r=await new y().get(),o=e?.endpoint||r.endpoint||J,i=e?.apiKey||r.apiKey||void 0,n=e?.provider||r.provider||"gas";return {endpoint:o,provider:n,apiKey:i}}async function k(e){let t=await K(e);switch(t.provider){case "gas":case "custom":return new x(t.endpoint,t.apiKey);default:throw new Error(`\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u306A\u3044\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC (${t.provider})`)}}var B=100,w=class{filePath;constructor(){this.filePath=A();}ensureConfigDir(){let t=m();c.existsSync(t)||c.mkdirSync(t,{recursive:true});}async readHistory(){try{let t=await promises.readFile(this.filePath,"utf-8");return JSON.parse(t)}catch(t){return t&&t.code==="ENOENT"?[]:(console.warn("\u5C65\u6B74\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF0E\u7A7A\u914D\u5217\u3092\u8FD4\u3057\u307E\u3059"),[])}}async writeHistory(t){try{this.ensureConfigDir();let r=`${this.filePath}.tmp`,o=JSON.stringify(t,null,2);await promises.writeFile(r,o,"utf-8"),await promises.rename(r,this.filePath);}catch{throw new Error("\u5C65\u6B74\u30D5\u30A1\u30A4\u30EB\u306E\u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F")}}async add(t){let r=await this.readHistory(),o={...t,id:randomUUID(),timestamp:new Date().toISOString()};r.unshift(o),r.length>B&&r.splice(B),await this.writeHistory(r);}async getAll(){return await this.readHistory()}async getRecent(t){return (await this.readHistory()).slice(0,t)}async deleteById(t){let r=await this.readHistory(),o=r.filter(i=>i.id!==t);return o.length===r.length?false:(await this.writeHistory(o),true)}async clear(){await this.writeHistory([]);}async findById(t){return (await this.readHistory()).find(o=>o.id===t)||null}async findByShortId(t){return (await this.readHistory()).filter(o=>o.id.startsWith(t))}async findByHash(t,r,o){return (await this.readHistory()).find(n=>n.sourceHash===t&&n.sourceLang===r&&n.targetLang===o)||null}};function F(e){return createHash("sha256").update(e).digest("hex")}async function R(e,t){try{if(!e&&!t.file&&!process.stdin.isTTY){let r=await _();await j(r,t,"stdin");return}if(t.file){if(!c.existsSync(t.file))throw new Error(`\u30D5\u30A1\u30A4\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (${t.file})`);let r=c.readFileSync(t.file,"utf-8");await j(r,t,"file");return}if(e){await j(e,t,"arg");return}console.error("error: \u5165\u529B\u304C\u63D0\u4F9B\u3055\u308C\u3066\u3044\u307E\u305B\u3093"),console.error("\u4F7F\u3044\u65B9: enja <\u30C6\u30AD\u30B9\u30C8> \u307E\u305F\u306F enja -f <\u30D5\u30A1\u30A4\u30EB> \u307E\u305F\u306F \u30D1\u30A4\u30D7\u5165\u529B"),process.exit(1);}catch(r){console.error(r instanceof Error?`error: ${r.message}`:r),process.exit(1);}}async function j(e,t,r){if(!e||e.trim().length===0)throw new Error("\u7FFB\u8A33\u3059\u308B\u30C6\u30AD\u30B9\u30C8\u304C\u7A7A\u3067\u3059");let o=e;if(t.stripHtml&&(o=G(e),!o||o.trim().length===0))throw new Error("HTML\u30BF\u30B0\u3092\u9664\u53BB\u3057\u305F\u7D50\u679C\uFF0C\u7FFB\u8A33\u3059\u308B\u30C6\u30AD\u30B9\u30C8\u304C\u7A7A\u306B\u306A\u308A\u307E\u3057\u305F");let i=await k(t),n=new w,s=t.flip?"ja":"en",a=t.flip?"en":"ja",p=F(o),g=await n.findByHash(p,s,a);if(g&&t.cache!==false){console.log(`${l.green("\u2714")} \u30AD\u30E3\u30C3\u30B7\u30E5\u304B\u3089\u7FFB\u8A33\u7D50\u679C\u3092\u53D6\u5F97\u3057\u307E\u3057\u305F`);let H=g.translatedText;if(t.output)try{c.writeFileSync(t.output,H,"utf-8"),console.log(`${l.green("\u2714")} ${t.output} \u306B\u7FFB\u8A33\u7D50\u679C\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F`);}catch{throw new Error(`\u30D5\u30A1\u30A4\u30EB\u3078\u306E\u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F (${t.output})`)}else console.log(H);return}let u=`(${s} \u2192 ${a})`,O=Y(`\u7FFB\u8A33\u4E2D... ${u}`).start();try{let T=(await i.translate(o,s,a)).text;if(O.succeed(`\u7FFB\u8A33\u5B8C\u4E86 ${u}`),await n.add({sourceText:o,translatedText:T,sourceLang:s,targetLang:a,textLength:o.length,sourceHash:p,options:{stripHtml:t.stripHtml,file:t.file,inputMethod:r}}),t.output)try{c.writeFileSync(t.output,T,"utf-8"),console.log(`${l.green("\u2714")} ${t.output} \u306B\u7FFB\u8A33\u7D50\u679C\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F`);}catch{throw new Error(`\u30D5\u30A1\u30A4\u30EB\u3078\u306E\u66F8\u304D\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F (${t.output})`)}else console.log(T);}catch(H){throw O.fail(`\u7FFB\u8A33\u5931\u6557 ${u}`),H}}function _(){return new Promise((e,t)=>{let r="";process.stdin.setEncoding("utf-8"),process.stdin.on("data",o=>{r+=o;}),process.stdin.on("end",()=>{e(r);}),process.stdin.on("error",o=>{t(o);});})}function G(e){return e.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,"").replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi,"").replace(/<[^>]+>/g,"").replace(/&nbsp;/g," ").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&amp;/g,"&").replace(/&quot;/g,'"').replace(/&#39;/g,"'").replace(/\n\s*\n/g,`
3
+ `).trim()}function E(e,t=false){return e.length===0?"\u5C65\u6B74\u306F\u3042\u308A\u307E\u305B\u3093":t?Q(e):q(e)}function q(e){let t=[];return t.push(`\u5168 ${e.length} \u4EF6\u306E\u5C65\u6B74
4
+ `),e.forEach((r,o)=>{let i=new Date(r.timestamp).toLocaleString("ja-JP"),n=r.sourceText.length>30?r.sourceText.replace(/[\r\n]+/g," ").substring(0,30)+"...":r.sourceText;t.push(`${l.cyan("["+(o+1)+"]")} ${r.id.substring(0,8)} | ${i}`),t.push(` ${r.sourceLang} \u2192 ${r.targetLang} | ${n}`),t.push("");}),t.join(`
5
+ `)}function Q(e){let t=[];return t.push(`\u5168 ${e.length} \u4EF6\u306E\u5C65\u6B74\u306E\u8A73\u7D30
6
+ `),e.forEach((r,o)=>{o>0&&t.push("\u2500".repeat(60));let i=new Date(r.timestamp).toLocaleString("ja-JP");if(t.push(`${l.cyan("ID:")} ${r.id}`),t.push(`${l.cyan("Date:")} ${i}`),t.push(`${l.cyan("Direction:")} ${r.sourceLang} \u2192 ${r.targetLang}`),t.push(`${l.cyan("Length:")} ${r.textLength} characters`),r.options){let n=[];r.options.inputMethod&&n.push(`input=${r.options.inputMethod}`),r.options.stripHtml&&n.push("stripHtml=true"),r.options.file&&n.push(`file=${r.options.file}`),n.length>0&&t.push(`${l.cyan("Options:")} ${n.join(", ")}`);}t.push(""),t.push(`${l.cyan("Input:")}`),t.push(r.sourceText),t.push(""),t.push(`${l.cyan("Output:")}`),t.push(r.translatedText),t.push("");}),t.join(`
7
+ `)}async function U(e,t){try{let r=new w;if(e){let s=e.trim();if(!s)throw new Error("\u7A7A\u306EID\u304C\u6307\u5B9A\u3055\u308C\u307E\u3057\u305F");if(s.length>=36){let g=await r.findById(s);if(!g)throw new Error(`\u6307\u5B9A\u3055\u308C\u305FID\u306E\u5C65\u6B74\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (${e})`);console.log(E([g],t.detail));return}if(s.length<8)throw new Error(`\u77ED\u7E2EID\u306F\u5C11\u306A\u304F\u3068\u30828\u6587\u5B57\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044 (${s.length})`);let a=await r.findByShortId(s);if(a.length===0)throw new Error(`\u6307\u5B9A\u3055\u308C\u305FID\u306E\u5C65\u6B74\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (${e})`);let p=E(a,t.detail);console.log(p);return}if(t.delete){let s=t.delete.trim();if(!s)throw new Error("\u7A7A\u306EID\u304C\u6307\u5B9A\u3055\u308C\u307E\u3057\u305F");if(s.length>=36){if(await r.deleteById(s))console.log(`${l.green("\u2714")} \u5C65\u6B74ID ${s} \u3092\u524A\u9664\u3057\u307E\u3057\u305F`);else throw new Error(`\u6307\u5B9A\u3055\u308C\u305FID\u306E\u5C65\u6B74\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (${s})`);return}if(s.length<8)throw new Error(`\u77ED\u7E2EID\u3067\u524A\u9664\u3059\u308B\u5834\u5408\u306F\u5C11\u306A\u304F\u3068\u30828\u6587\u5B57\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044 (${s.length})`);let a=await r.findByShortId(s);if(a.length===0)throw new Error(`\u6307\u5B9A\u3055\u308C\u305FID\u306E\u5C65\u6B74\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093 (${s})`);if(a.length>1){console.error("error: \u6307\u5B9A\u3055\u308C\u305F\u77ED\u7E2EID\u306F\u8907\u6570\u306E\u5C65\u6B74\u306B\u4E00\u81F4\u3057\u307E\u3057\u305F\uFF0E\u5B8C\u5168\u306AID\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF0E");let u=E(a,!1);console.error(u),process.exit(1);}let p=a[0].id;if(await r.deleteById(p))console.log(`${l.green("\u2714")} \u5C65\u6B74ID ${p} \u3092\u524A\u9664\u3057\u307E\u3057\u305F`);else throw new Error(`\u524A\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F (${p})`);return}if(t.clear){await r.clear(),console.log(`${l.green("\u2714")} \u5C65\u6B74\u3092\u30AF\u30EA\u30A2\u3057\u307E\u3057\u305F`);return}let o=Number(t.number)||10,i=await r.getRecent(o),n=E(i,t.detail);console.log(n);}catch(r){console.error(r instanceof Error?`error: ${r.message}`:r),process.exit(1);}}async function N(e,t,r){let o=new y;try{if(r?.reset){await o.reset(),console.log(`${l.green("\u2714")} \u8A2D\u5B9A\u3092\u30EA\u30BB\u30C3\u30C8\u3057\u307E\u3057\u305F`);return}if(r?.unset){await o.unset(r.unset),console.log(`${l.green("\u2714")} ${r.unset} \u3092\u30EA\u30BB\u30C3\u30C8\u3057\u307E\u3057\u305F`);return}if(e&&t){await o.set(e,t),console.log(`${l.green("\u2714")} ${e} \u3092\u8A2D\u5B9A\u3057\u307E\u3057\u305F`);return}if(e&&!t){let n=await o.get();if(e==="endpoint")console.log(n.endpoint);else if(e==="api-key")console.log(n.apiKey?M(n.apiKey):"(not set)");else if(e==="provider")console.log(n.provider);else throw new Error(`\u7121\u52B9\u306A\u8A2D\u5B9A\u30AD\u30FC (${e})`);return}let i=await o.get();console.log(`${l.blue("provider:")} ${i.provider}`),console.log(`${l.blue("endpoint:")} ${i.endpoint}`),console.log(`${l.blue("apiKey:")} ${i.apiKey?M(i.apiKey):"(not set)"}`);}catch(i){console.error(i instanceof Error?`error: ${i.message}`:i),process.exit(1);}}function M(e){if(e.length<=8)return "*".repeat(e.length);let t=4,r=e.slice(0,t),o=e.slice(-t),i="*".repeat(e.length-t*2);return `${r}${i}${o}`}var b=JSON.parse(readFileSync(new URL("../package.json",import.meta.url),"utf-8")),P=new Command;P.name("enja").usage("[arguments] [options]").description(`Description: ${b.description}`).version(b.version,"-v, --version","output the current version");P.argument("[text]","\u30C6\u30AD\u30B9\u30C8\u3092\u7FFB\u8A33\u3059\u308B").option("-f, --file <path>","\u30D5\u30A1\u30A4\u30EB\u3092\u7FFB\u8A33\u3059\u308B").option("-o, --output <path>","\u30D5\u30A1\u30A4\u30EB\u306B\u51FA\u529B\u3059\u308B (\u30C7\u30D5\u30A9\u30EB\u30C8: \u6A19\u6E96\u51FA\u529B)").option("-s, --strip-html","HTML\u30BF\u30B0\u3092\u9664\u53BB\u3057\u3066\u304B\u3089\u7FFB\u8A33\u3059\u308B").option("-N, --no-cache","\u30AD\u30E3\u30C3\u30B7\u30E5\u3092\u4F7F\u7528\u305B\u305A\u306B\u518D\u7FFB\u8A33\u3059\u308B").option("-F, --flip","\u7FFB\u8A33\u65B9\u5411\u3092\u9006\u306B\u3059\u308B (\u30C7\u30D5\u30A9\u30EB\u30C8: \u82F1\u8A9E\u2192\u65E5\u672C\u8A9E)").option("--endpoint <url>","\u30AB\u30B9\u30BF\u30E0\u7FFB\u8A33\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u3092\u6307\u5B9A").option("--api-key <key>","API \u30AD\u30FC\u3092\u6307\u5B9A").option("--provider <name>","\u7FFB\u8A33\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3092\u6307\u5B9A (gas, custom)").showHelpAfterError().addHelpText("after",`
8
+ Examples:
9
+ $ enja "Hello, world!" # \u5F15\u6570\u3067\u6E21\u3055\u308C\u305F\u6587\u5B57\u5217\u3092\u7FFB\u8A33
10
+ $ git --help | enja # \u30D1\u30A4\u30D7(\u6A19\u6E96\u5165\u529B)\u3067\u6E21\u3055\u308C\u305F\u30C6\u30AD\u30B9\u30C8\u3092\u7FFB\u8A33
11
+ $ enja -f input.txt # \u30D5\u30A1\u30A4\u30EB\u304B\u3089\u30C6\u30AD\u30B9\u30C8\u3092\u8AAD\u307F\u8FBC\u3093\u3067\u7FFB\u8A33
12
+ $ enja -f input.txt -o output.txt # \u30D5\u30A1\u30A4\u30EB\u304B\u3089\u8AAD\u307F\u8FBC\u307F\uFF0C\u7FFB\u8A33\u7D50\u679C\u3092\u30D5\u30A1\u30A4\u30EB\u306B\u4FDD\u5B58
13
+ $ cat README.md | enja -o japanese.md # \u30D1\u30A4\u30D7\u3068\u30D5\u30A1\u30A4\u30EB\u51FA\u529B\u306E\u7D44\u307F\u5408\u308F\u305B
14
+ $ curl -s https://example.com | enja -s # HTML\u30BF\u30B0\u3092\u9664\u53BB\u3057\u3066\u7FFB\u8A33
15
+ $ enja "Hello" --endpoint https://api.example.com/translate --api-key YOUR_KEY # \u30AB\u30B9\u30BF\u30E0\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8`).addHelpText("afterAll",`
16
+ Enja CLI v${b.version}`).addHelpText("afterAll","Copyright (c) 2025 yhotta240").addHelpText("afterAll","GitHub: https://github.com/yhotamos/enja-cli").action(R);P.command("history").description("\u7FFB\u8A33\u5C65\u6B74\u3092\u8868\u793A\u3059\u308B").argument("[id]","ID \u3067\u5C65\u6B74\u3092\u8868\u793A\u3059\u308B\uFF08\u5B8C\u5168 ID \u307E\u305F\u306F\u77ED\u7E2E ID\uFF09").option("-d, --detail","\u8A73\u7D30\u8868\u793A").option("-n, --number <number>","\u8868\u793A\u4EF6\u6570 (\u30C7\u30D5\u30A9\u30EB\u30C8: 10)","10").option("--delete <id>","\u7279\u5B9A\u306E\u5C65\u6B74\u3092\u524A\u9664\u3059\u308B").option("--clear","\u5C65\u6B74\u3092\u30AF\u30EA\u30A2").action(U);P.command("config").description("\u8A2D\u5B9A\u3092\u7BA1\u7406\u3059\u308B").argument("[key]","\u8A2D\u5B9A\u30AD\u30FC (endpoint, api-key, provider)").argument("[value]","\u8A2D\u5B9A\u5024").option("-l, --list","\u8A2D\u5B9A\u3092\u4E00\u89A7\u8868\u793A").option("--unset <key>","\u8A2D\u5B9A\u3092\u524A\u9664\uFF08\u30C7\u30D5\u30A9\u30EB\u30C8\u306B\u623B\u3059\uFF09").option("--reset","\u3059\u3079\u3066\u306E\u8A2D\u5B9A\u3092\u30EA\u30BB\u30C3\u30C8").addHelpText("after",`
17
+ Examples:
18
+ $ enja config # \u3059\u3079\u3066\u306E\u8A2D\u5B9A\u3092\u8868\u793A
19
+ $ enja config --list # \u3059\u3079\u3066\u306E\u8A2D\u5B9A\u3092\u8868\u793A
20
+ $ enja config endpoint # endpoint \u306E\u5024\u3092\u8868\u793A
21
+ $ enja config endpoint <URL> # endpoint \u3092\u8A2D\u5B9A
22
+ $ enja config api-key <KEY> # API \u30AD\u30FC\u3092\u8A2D\u5B9A
23
+ $ enja config provider gas # \u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3092\u8A2D\u5B9A
24
+ $ enja config --unset api-key # API \u30AD\u30FC\u3092\u524A\u9664
25
+ $ enja config --reset # \u3059\u3079\u3066\u306E\u8A2D\u5B9A\u3092\u30EA\u30BB\u30C3\u30C8`).action(N);P.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yhotamos/enja-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "英語を日本語に翻訳するシンプルなコマンドラインツール",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -11,8 +11,30 @@
11
11
  "enja": "dist/index.js"
12
12
  },
13
13
  "scripts": {
14
- "build": "tsc",
15
- "watch": "tsc --watch"
14
+ "build": "tsup --no-sourcemap",
15
+ "build:dev": "tsup",
16
+ "watch": "tsup --watch"
17
+ },
18
+ "tsup": {
19
+ "entry": [
20
+ "src/index.ts"
21
+ ],
22
+ "format": [
23
+ "esm"
24
+ ],
25
+ "bundle": true,
26
+ "minify": true,
27
+ "treeshake": true,
28
+ "external": [
29
+ "commander",
30
+ "kleur",
31
+ "ora"
32
+ ],
33
+ "dts": true,
34
+ "outDir": "dist",
35
+ "clean": true,
36
+ "sourcemap": true,
37
+ "target": "node18"
16
38
  },
17
39
  "keywords": [
18
40
  "translation",
@@ -44,6 +66,7 @@
44
66
  },
45
67
  "devDependencies": {
46
68
  "@types/node": "^24.10.1",
69
+ "tsup": "^8.5.1",
47
70
  "tsx": "^4.20.6",
48
71
  "typescript": "^5.9.3"
49
72
  }
@@ -1,69 +0,0 @@
1
- import { ConfigStorage } from '../services/config/storage.js';
2
- import kleur from 'kleur';
3
- /** 設定コマンドの実行 */
4
- export async function config(key, value, options) {
5
- const storage = new ConfigStorage();
6
- try {
7
- // --reset: すべての設定をリセット
8
- if (options?.reset) {
9
- await storage.reset();
10
- console.log(`${kleur.green('✔')} 設定をリセットしました`);
11
- return;
12
- }
13
- // --unset: 指定したキーを削除(デフォルトに戻す)
14
- if (options?.unset) {
15
- await storage.unset(options.unset);
16
- console.log(`${kleur.green('✔')} ${options.unset} をリセットしました`);
17
- return;
18
- }
19
- // key と value が指定された場合: 設定を保存
20
- if (key && value) {
21
- await storage.set(key, value);
22
- console.log(`${kleur.green('✔')} ${key} を設定しました`);
23
- return;
24
- }
25
- // key のみ指定された場合: その設定値を表示
26
- if (key && !value) {
27
- const config = await storage.get();
28
- if (key === 'endpoint') {
29
- console.log(config.endpoint);
30
- }
31
- else if (key === 'api-key') {
32
- console.log(config.apiKey ? maskApiKey(config.apiKey) : '(not set)');
33
- }
34
- else if (key === 'provider') {
35
- console.log(config.provider);
36
- }
37
- else {
38
- console.error(`error: 無効な設定キー (${key})`);
39
- process.exit(1);
40
- }
41
- return;
42
- }
43
- // --list または引数なし: すべての設定を表示
44
- const config = await storage.get();
45
- console.log(`${kleur.blue('provider:')} ${config.provider}`);
46
- console.log(`${kleur.blue('endpoint:')} ${config.endpoint}`);
47
- console.log(`${kleur.blue('apiKey:')} ${config.apiKey ? maskApiKey(config.apiKey) : '(not set)'}`);
48
- }
49
- catch (error) {
50
- if (error instanceof Error) {
51
- console.error(error.message);
52
- }
53
- else {
54
- console.error(error);
55
- }
56
- process.exit(1);
57
- }
58
- }
59
- // APIキーのマスキング表示
60
- function maskApiKey(apiKey) {
61
- if (apiKey.length <= 8) {
62
- return '*'.repeat(apiKey.length);
63
- }
64
- const visible = 4;
65
- const start = apiKey.slice(0, visible);
66
- const end = apiKey.slice(-visible);
67
- const masked = '*'.repeat(apiKey.length - visible * 2);
68
- return `${start}${masked}${end}`;
69
- }
@@ -1,96 +0,0 @@
1
- import { HistoryStorage } from '../services/history/storage.js';
2
- import { formatHistory } from '../services/history/formatter.js';
3
- import kleur from 'kleur';
4
- /** 履歴コマンドの実行 */
5
- export async function history(id, options) {
6
- try {
7
- const storage = new HistoryStorage();
8
- // 特定IDの履歴表示
9
- if (id) {
10
- const trimmed = id.trim();
11
- if (!trimmed) {
12
- throw new Error(`空のIDが指定されました`);
13
- }
14
- // 完全IDらしければ完全一致で検索
15
- if (trimmed.length >= 36) {
16
- const entry = await storage.findById(trimmed);
17
- if (!entry) {
18
- throw new Error(`指定されたIDの履歴が見つかりません (${id})`);
19
- }
20
- console.log(formatHistory([entry], options.detail));
21
- return;
22
- }
23
- // 短縮IDは最低 8 文字を要求
24
- if (trimmed.length < 8) {
25
- throw new Error(`短縮IDは少なくとも8文字を指定してください (${trimmed.length})`);
26
- }
27
- // 短縮IDで先頭一致検索
28
- const matches = await storage.findByShortId(trimmed);
29
- if (matches.length === 0) {
30
- throw new Error(`指定されたIDの履歴が見つかりません (${id})`);
31
- }
32
- // 複数マッチしても表示する
33
- const output = formatHistory(matches, options.detail);
34
- console.log(output);
35
- return;
36
- }
37
- // 履歴削除
38
- if (options.delete) {
39
- const delId = options.delete.trim();
40
- if (!delId) {
41
- throw new Error(`空のIDが指定されました`);
42
- }
43
- // 完全IDらしければ直接削除を試みる
44
- if (delId.length >= 36) {
45
- const deleted = await storage.deleteById(delId);
46
- if (deleted) {
47
- console.log(`${kleur.green('✔')} 履歴ID ${delId} を削除しました`);
48
- }
49
- else {
50
- throw new Error(`指定されたIDの履歴が見つかりません (${delId})`);
51
- }
52
- return;
53
- }
54
- // 短縮IDは最低 8 文字を要求
55
- if (delId.length < 8) {
56
- throw new Error(`短縮IDで削除する場合は少なくとも8文字を指定してください (${delId.length})`);
57
- }
58
- // 短縮IDで先頭一致検索(複数ヒットする可能性あり)
59
- const matches = await storage.findByShortId(delId);
60
- if (matches.length === 0) {
61
- throw new Error(`指定されたIDの履歴が見つかりません (${delId})`);
62
- }
63
- if (matches.length > 1) {
64
- console.error('error: 指定された短縮IDは複数の履歴に一致しました.完全なIDを指定してください.');
65
- const output = formatHistory(matches, false);
66
- console.error(output);
67
- process.exit(1);
68
- }
69
- // 単一マッチなら削除
70
- const targetId = matches[0].id;
71
- const deleted = await storage.deleteById(targetId);
72
- if (deleted) {
73
- console.log(`${kleur.green('✔')} 履歴ID ${targetId} を削除しました`);
74
- }
75
- else {
76
- throw new Error(`削除に失敗しました (${targetId})`);
77
- }
78
- return;
79
- }
80
- // 履歴クリア
81
- if (options.clear) {
82
- await storage.clear();
83
- console.log(`${kleur.green('✔')} 履歴をクリアしました`);
84
- return;
85
- }
86
- // 履歴表示
87
- const limit = Number(options.number) || 10;
88
- const entries = await storage.getRecent(limit);
89
- const output = formatHistory(entries, options.detail);
90
- console.log(output);
91
- }
92
- catch (error) {
93
- console.error(error instanceof Error ? `error: ${error.message}` : error);
94
- process.exit(1);
95
- }
96
- }
@@ -1,144 +0,0 @@
1
- import * as fs from 'fs';
2
- import ora from 'ora';
3
- import { createTranslator } from '../services/translator/factory.js';
4
- import { HistoryStorage } from '../services/history/storage.js';
5
- import { hashText } from '../utils/hash.js';
6
- import kleur from 'kleur';
7
- export async function translate(text, options) {
8
- try {
9
- // 標準入力からの読み込み処理
10
- if (!text && !options.file && !process.stdin.isTTY) {
11
- const stdin = await readStdin();
12
- await processTranslation(stdin, options, 'stdin');
13
- return;
14
- }
15
- // ファイルからの読み込み処理
16
- if (options.file) {
17
- if (!fs.existsSync(options.file)) {
18
- throw new Error(`error: ファイルが見つかりません (${options.file})`);
19
- }
20
- const fileContent = fs.readFileSync(options.file, 'utf-8');
21
- await processTranslation(fileContent, options, 'file');
22
- return;
23
- }
24
- // 引数で渡されたテキストの処理
25
- if (text) {
26
- await processTranslation(text, options, 'arg');
27
- return;
28
- }
29
- console.error('error: 入力が提供されていません');
30
- console.error('使い方: enja <テキスト> または enja -f <ファイル> または パイプ入力');
31
- process.exit(1);
32
- }
33
- catch (error) {
34
- console.error(error instanceof Error ? error.message : error);
35
- process.exit(1);
36
- }
37
- }
38
- async function processTranslation(text, options, inputMethod) {
39
- if (!text || text.trim().length === 0) {
40
- throw new Error('error: 翻訳するテキストが空です');
41
- }
42
- // HTMLタグ除去
43
- let processedText = text;
44
- if (options.stripHtml) {
45
- processedText = stripHtmlTags(text);
46
- if (!processedText || processedText.trim().length === 0) {
47
- throw new Error('error: HTMLタグを除去した結果、翻訳するテキストが空になりました');
48
- }
49
- }
50
- // 翻訳サービスの初期化
51
- const translator = await createTranslator(options);
52
- const historyStorage = new HistoryStorage();
53
- // 翻訳処理
54
- const sourceLang = options.flip ? 'ja' : 'en';
55
- const targetLang = options.flip ? 'en' : 'ja';
56
- // キャッシュチェック
57
- const textHash = hashText(processedText);
58
- const cachedEntry = await historyStorage.findByHash(textHash, sourceLang, targetLang);
59
- if (cachedEntry && options.cache !== false) {
60
- console.log(`${kleur.green('✔')} キャッシュから翻訳結果を取得しました`);
61
- const translated = cachedEntry.translatedText;
62
- // 出力処理
63
- if (options.output) {
64
- try {
65
- fs.writeFileSync(options.output, translated, 'utf-8');
66
- console.log(`${kleur.green('✔')} ${options.output} に翻訳結果を保存しました`);
67
- }
68
- catch (error) {
69
- throw new Error(`error: ファイルへの書き込みに失敗しました (${options.output})`);
70
- }
71
- }
72
- else {
73
- console.log(translated);
74
- }
75
- return;
76
- }
77
- const dir = `(${sourceLang} → ${targetLang})`;
78
- const spinner = ora(`翻訳中... ${dir}`).start();
79
- try {
80
- const result = await translator.translate(processedText, sourceLang, targetLang);
81
- const translated = result.text;
82
- spinner.succeed(`翻訳完了 ${dir}`);
83
- // 履歴に保存
84
- await historyStorage.add({
85
- sourceText: processedText,
86
- translatedText: translated,
87
- sourceLang,
88
- targetLang,
89
- textLength: processedText.length,
90
- sourceHash: textHash,
91
- options: {
92
- stripHtml: options.stripHtml,
93
- file: options.file,
94
- inputMethod,
95
- },
96
- });
97
- // 出力処理
98
- if (options.output) {
99
- try {
100
- fs.writeFileSync(options.output, translated, 'utf-8');
101
- console.log(`${kleur.green('✔')} ${options.output} に翻訳結果を保存しました`);
102
- }
103
- catch (error) {
104
- throw new Error(`error: ファイルへの書き込みに失敗しました (${options.output})`);
105
- }
106
- }
107
- else {
108
- console.log(translated);
109
- }
110
- }
111
- catch (error) {
112
- spinner.fail(`翻訳失敗 ${dir}`);
113
- throw error;
114
- }
115
- }
116
- function readStdin() {
117
- return new Promise((resolve, reject) => {
118
- let data = '';
119
- process.stdin.setEncoding('utf-8');
120
- process.stdin.on('data', (chunk) => {
121
- data += chunk;
122
- });
123
- process.stdin.on('end', () => {
124
- resolve(data);
125
- });
126
- process.stdin.on('error', (error) => {
127
- reject(error);
128
- });
129
- });
130
- }
131
- function stripHtmlTags(html) {
132
- return html
133
- .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
134
- .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
135
- .replace(/<[^>]+>/g, '')
136
- .replace(/&nbsp;/g, ' ')
137
- .replace(/&lt;/g, '<')
138
- .replace(/&gt;/g, '>')
139
- .replace(/&amp;/g, '&')
140
- .replace(/&quot;/g, '"')
141
- .replace(/&#39;/g, "'")
142
- .replace(/\n\s*\n/g, '\n')
143
- .trim();
144
- }
@@ -1,22 +0,0 @@
1
- import { ConfigStorage } from '../services/config/storage.js';
2
- const DEFAULT_GAS_API_URL = "https://script.google.com/macros/s/AKfycbxOSbKD0aBTaQqIzHv00BMzp6WwrtWHBU3gJY0vhB2HblgUO-cgesfT1l-rrfttnWZzew/exec";
3
- export async function getConfig(options) {
4
- // 設定ファイルから読み込み
5
- const storage = new ConfigStorage();
6
- const fileConfig = await storage.get();
7
- // 優先順位: コマンドラインオプション > 設定ファイル > デフォルト値
8
- const endpoint = options?.endpoint ||
9
- fileConfig.endpoint ||
10
- DEFAULT_GAS_API_URL;
11
- const apiKey = options?.apiKey ||
12
- fileConfig.apiKey ||
13
- undefined;
14
- const provider = options?.provider ||
15
- fileConfig.provider ||
16
- 'gas';
17
- return {
18
- endpoint,
19
- provider,
20
- apiKey,
21
- };
22
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,89 +0,0 @@
1
- import * as fs from 'fs';
2
- import { getConfigFilePath, getConfigDir } from '../../utils/paths.js';
3
- const DEFAULT_CONFIG = {
4
- provider: 'gas',
5
- endpoint: 'https://script.google.com/macros/s/AKfycbxOSbKD0aBTaQqIzHv00BMzp6WwrtWHBU3gJY0vhB2HblgUO-cgesfT1l-rrfttnWZzew/exec',
6
- apiKey: undefined,
7
- };
8
- /** 設定の永続化を管理するクラス */
9
- export class ConfigStorage {
10
- filePath;
11
- constructor() {
12
- this.filePath = getConfigFilePath();
13
- }
14
- ensureConfigDir() {
15
- const configDir = getConfigDir();
16
- if (!fs.existsSync(configDir)) {
17
- fs.mkdirSync(configDir, { recursive: true });
18
- }
19
- }
20
- async readConfig() {
21
- try {
22
- if (!fs.existsSync(this.filePath)) {
23
- return { ...DEFAULT_CONFIG };
24
- }
25
- const data = fs.readFileSync(this.filePath, 'utf-8');
26
- const config = JSON.parse(data);
27
- return { ...DEFAULT_CONFIG, ...config };
28
- }
29
- catch (error) {
30
- return { ...DEFAULT_CONFIG };
31
- }
32
- }
33
- async writeConfig(config) {
34
- try {
35
- this.ensureConfigDir();
36
- fs.writeFileSync(this.filePath, JSON.stringify(config, null, 2), 'utf-8');
37
- }
38
- catch (error) {
39
- throw new Error(`error: 設定ファイルの書き込みに失敗しました`);
40
- }
41
- }
42
- /** 設定を取得 */
43
- async get() {
44
- return await this.readConfig();
45
- }
46
- /** 設定を保存 */
47
- async set(key, value) {
48
- const config = await this.readConfig();
49
- switch (key) {
50
- case 'endpoint':
51
- config.endpoint = value;
52
- break;
53
- case 'api-key':
54
- config.apiKey = value;
55
- break;
56
- case 'provider':
57
- if (value !== 'gas' && value !== 'custom') {
58
- throw new Error(`error: 無効なプロバイダー (${value}): gas または custom を指定してください`);
59
- }
60
- config.provider = value;
61
- break;
62
- default:
63
- throw new Error(`error: 無効な設定キー (${key})`);
64
- }
65
- await this.writeConfig(config);
66
- }
67
- /** 指定したキーを削除(デフォルトに戻す) */
68
- async unset(key) {
69
- const config = await this.readConfig();
70
- switch (key) {
71
- case 'endpoint':
72
- config.endpoint = DEFAULT_CONFIG.endpoint;
73
- break;
74
- case 'api-key':
75
- config.apiKey = undefined;
76
- break;
77
- case 'provider':
78
- config.provider = DEFAULT_CONFIG.provider;
79
- break;
80
- default:
81
- throw new Error(`error: 無効な設定キー (${key})`);
82
- }
83
- await this.writeConfig(config);
84
- }
85
- /** 設定をリセット */
86
- async reset() {
87
- await this.writeConfig({ ...DEFAULT_CONFIG });
88
- }
89
- }
@@ -1,61 +0,0 @@
1
- import kleur from 'kleur';
2
- /** 履歴エントリをフォーマットして文字列として返す */
3
- export function formatHistory(entries, detailed = false) {
4
- if (entries.length === 0) {
5
- return '履歴はありません';
6
- }
7
- if (!detailed) {
8
- return formatSimple(entries);
9
- }
10
- return formatDetailed(entries);
11
- }
12
- /** 簡易フォーマット */
13
- function formatSimple(entries) {
14
- const lines = [];
15
- lines.push(`全 ${entries.length} 件の履歴\n`);
16
- entries.forEach((entry, index) => {
17
- const date = new Date(entry.timestamp).toLocaleString('ja-JP');
18
- const preview = entry.sourceText.length > 30
19
- ? entry.sourceText.replace(/[\r\n]+/g, ' ').substring(0, 30) + '...'
20
- : entry.sourceText;
21
- lines.push(`${kleur.cyan('[' + (index + 1) + ']')} ${entry.id.substring(0, 8)} | ${date}`);
22
- lines.push(` ${entry.sourceLang} → ${entry.targetLang} | ${preview}`);
23
- lines.push('');
24
- });
25
- return lines.join('\n');
26
- }
27
- /** 詳細フォーマット */
28
- function formatDetailed(entries) {
29
- const lines = [];
30
- lines.push(`全 ${entries.length} 件の履歴の詳細\n`);
31
- entries.forEach((entry, index) => {
32
- if (index > 0) {
33
- lines.push('─'.repeat(60));
34
- }
35
- const date = new Date(entry.timestamp).toLocaleString('ja-JP');
36
- lines.push(`${kleur.cyan('ID:')} ${entry.id}`);
37
- lines.push(`${kleur.cyan('Date:')} ${date}`);
38
- lines.push(`${kleur.cyan('Direction:')} ${entry.sourceLang} → ${entry.targetLang}`);
39
- lines.push(`${kleur.cyan('Length:')} ${entry.textLength} characters`);
40
- if (entry.options) {
41
- const opts = [];
42
- if (entry.options.inputMethod)
43
- opts.push(`input=${entry.options.inputMethod}`);
44
- if (entry.options.stripHtml)
45
- opts.push('stripHtml=true');
46
- if (entry.options.file)
47
- opts.push(`file=${entry.options.file}`);
48
- if (opts.length > 0) {
49
- lines.push(`${kleur.cyan('Options:')} ${opts.join(', ')}`);
50
- }
51
- }
52
- lines.push('');
53
- lines.push(`${kleur.cyan('Input:')}`);
54
- lines.push(entry.sourceText);
55
- lines.push('');
56
- lines.push(`${kleur.cyan('Output:')}`);
57
- lines.push(entry.translatedText);
58
- lines.push('');
59
- });
60
- return lines.join('\n');
61
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,104 +0,0 @@
1
- import * as fs from 'fs';
2
- import { promises as fsp } from 'fs';
3
- import { randomUUID } from 'crypto';
4
- import { getHistoryFilePath, getConfigDir } from '../../utils/paths.js';
5
- const MAX_HISTORY_ENTRIES = 100;
6
- /** 履歴管理のためのストレージクラス */
7
- export class HistoryStorage {
8
- filePath;
9
- constructor() {
10
- this.filePath = getHistoryFilePath();
11
- }
12
- /** 設定ディレクトリが存在しない場合は作成 */
13
- ensureConfigDir() {
14
- const configDir = getConfigDir();
15
- if (!fs.existsSync(configDir)) {
16
- fs.mkdirSync(configDir, { recursive: true });
17
- }
18
- }
19
- /** 履歴ファイルを読み込む */
20
- async readHistory() {
21
- try {
22
- // ファイルが存在しない場合は空配列を返す
23
- await fsp.access(this.filePath).catch(() => { throw new Error('NO_FILE'); });
24
- const data = await fsp.readFile(this.filePath, 'utf-8');
25
- return JSON.parse(data);
26
- }
27
- catch (error) {
28
- // 読み取りや JSON パースに失敗した場合は安全に空配列を返す
29
- return [];
30
- }
31
- }
32
- /** 履歴ファイルに書き込む */
33
- async writeHistory(entries) {
34
- try {
35
- this.ensureConfigDir();
36
- // 一時ファイルに書き込み、リネームで原子性を確保
37
- const tmpPath = `${this.filePath}.tmp`;
38
- const data = JSON.stringify(entries, null, 2);
39
- await fsp.writeFile(tmpPath, data, 'utf-8');
40
- await fsp.rename(tmpPath, this.filePath);
41
- }
42
- catch (error) {
43
- throw new Error(`error: 履歴ファイルの書き込みに失敗しました`);
44
- }
45
- }
46
- /** 履歴にエントリを追加 */
47
- async add(entry) {
48
- const entries = await this.readHistory();
49
- const newEntry = {
50
- ...entry,
51
- id: randomUUID(),
52
- timestamp: new Date().toISOString(),
53
- };
54
- entries.unshift(newEntry);
55
- // 最大エントリ数を超えた場合は古いエントリを削除
56
- if (entries.length > MAX_HISTORY_ENTRIES) {
57
- entries.splice(MAX_HISTORY_ENTRIES);
58
- }
59
- await this.writeHistory(entries);
60
- }
61
- /** すべての履歴を取得 */
62
- async getAll() {
63
- return await this.readHistory();
64
- }
65
- /** 最近の履歴を取得 */
66
- async getRecent(limit) {
67
- const entries = await this.readHistory();
68
- return entries.slice(0, limit);
69
- }
70
- /** 履歴を削除 */
71
- async deleteById(id) {
72
- const entries = await this.readHistory();
73
- const filteredEntries = entries.filter(entry => entry.id !== id);
74
- // 変更がなければ書き込みを行わない
75
- if (filteredEntries.length === entries.length)
76
- return false;
77
- await this.writeHistory(filteredEntries);
78
- return true;
79
- }
80
- /** 履歴をクリア */
81
- async clear() {
82
- await this.writeHistory([]);
83
- }
84
- /** IDで履歴を検索 */
85
- async findById(id) {
86
- const entries = await this.readHistory();
87
- return entries.find(entry => entry.id === id) || null;
88
- }
89
- /**
90
- * 短縮IDで履歴を検索(先頭一致)
91
- * 複数マッチする可能性があるため配列を返す
92
- */
93
- async findByShortId(id) {
94
- const entries = await this.readHistory();
95
- return entries.filter(entry => entry.id.startsWith(id));
96
- }
97
- /** ハッシュと翻訳方向で履歴を検索 */
98
- async findByHash(hash, sourceLang, targetLang) {
99
- const entries = await this.readHistory();
100
- return entries.find(entry => entry.sourceHash === hash &&
101
- entry.sourceLang === sourceLang &&
102
- entry.targetLang === targetLang) || null;
103
- }
104
- }
@@ -1,25 +0,0 @@
1
- import * as deepl from 'deepl-node';
2
- export class DeepLTranslator {
3
- translator;
4
- constructor(apiKey) {
5
- this.translator = new deepl.Translator(apiKey);
6
- }
7
- async translate(text, sourceLang, targetLang) {
8
- try {
9
- const result = await this.translator.translateText(text, sourceLang, targetLang);
10
- return {
11
- text: result.text,
12
- detectedSourceLang: result.detectedSourceLang,
13
- };
14
- }
15
- catch (error) {
16
- if (error instanceof Error) {
17
- throw new Error(`DeepL translation failed: ${error.message}`);
18
- }
19
- throw error;
20
- }
21
- }
22
- async checkUsage() {
23
- return await this.translator.getUsage();
24
- }
25
- }
@@ -1,12 +0,0 @@
1
- import { GASTranslator } from './gas.js';
2
- import { getConfig } from '../../config/index.js';
3
- export async function createTranslator(options) {
4
- const config = await getConfig(options);
5
- switch (config.provider) {
6
- case 'gas':
7
- case 'custom':
8
- return new GASTranslator(config.endpoint, config.apiKey);
9
- default:
10
- throw new Error(`error: サポートされていないプロバイダー (${config.provider})`);
11
- }
12
- }
@@ -1,44 +0,0 @@
1
- export class GASTranslator {
2
- apiUrl;
3
- apiKey;
4
- constructor(apiUrl, apiKey) {
5
- this.apiUrl = apiUrl;
6
- this.apiKey = apiKey;
7
- }
8
- async translate(text, sourceLang, targetLang) {
9
- try {
10
- const headers = {
11
- 'Content-Type': 'application/json',
12
- };
13
- if (this.apiKey) {
14
- headers['Authorization'] = `Bearer ${this.apiKey}`;
15
- }
16
- const response = await fetch(this.apiUrl, {
17
- method: 'POST',
18
- headers,
19
- body: JSON.stringify({
20
- text,
21
- sourceLang,
22
- targetLang,
23
- }),
24
- });
25
- if (!response.ok) {
26
- throw new Error(`error: HTTP ${response.status} ${response.statusText}`);
27
- }
28
- const data = await response.json();
29
- if (data.code !== 200 || !data.translatedText) {
30
- throw new Error(`error: ${data.error || '翻訳に失敗しました'}`);
31
- }
32
- return {
33
- text: data.translatedText,
34
- detectedSourceLang: data.detectedSourceLang,
35
- };
36
- }
37
- catch (error) {
38
- if (error instanceof Error) {
39
- throw error;
40
- }
41
- throw new Error(`error: 翻訳に失敗しました`);
42
- }
43
- }
44
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1,5 +0,0 @@
1
- import { createHash } from 'crypto';
2
- /** テキストの SHA-256 ハッシュを計算して返す */
3
- export function hashText(text) {
4
- return createHash('sha256').update(text).digest('hex');
5
- }
@@ -1,23 +0,0 @@
1
- import * as path from 'path';
2
- import * as os from 'os';
3
- /** OS の設定ディレクトリのパスを取得 */
4
- export function getConfigDir() {
5
- const platform = process.platform;
6
- if (platform === 'win32') {
7
- const appData = process.env.APPDATA;
8
- if (!appData) {
9
- throw new Error('error: APPDATA 環境変数が設定されていません');
10
- }
11
- return path.join(appData, 'enja-cli');
12
- }
13
- const homeDir = os.homedir();
14
- return path.join(homeDir, '.config', 'enja-cli');
15
- }
16
- /** 履歴ファイルのパスを取得 */
17
- export function getHistoryFilePath() {
18
- return path.join(getConfigDir(), 'history.json');
19
- }
20
- /** 設定ファイルのパスを取得 */
21
- export function getConfigFilePath() {
22
- return path.join(getConfigDir(), 'config.json');
23
- }