pleumrouter 0.5.37 → 0.5.40

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/README.md CHANGED
@@ -46,7 +46,7 @@ SSH/CI에서는 `pleum login --device`를 쓰세요.
46
46
  | `pleum top [--period daily\|weekly\|monthly]` | 라우터 전체 모델 리더보드 + 제공자 점유율(공개 통계) |
47
47
  | `pleum balance [--switch]` | 크레딧 잔액 조회 · 워크스페이스(개인/팀) 전환 |
48
48
  | `pleum usage [--by model\|provider\|api_key\|day\|tag\|user] [--days N]` | 내 지출 리포트(막대 차트) — OAuth 로그인 세션 필요 |
49
- | `pleum health` | 게이트웨이 연결·DB·Redis·환율·로그인 상태 점검(셀프호스트 진단용) |
49
+ | `pleum health` | 공개 게이트웨이 상태·로그인 상태 점검. DB·Redis·환율 상세는 관리자 전용 |
50
50
  | `pleum lang [code]` | 표시 언어 조회/변경 — `en`(기본)·`ko`·`ja`·`zh`·`es`. 인자 없이 TTY면 화살표로 선택, `PLEUM_LANG` 환경변수로도 덮어쓰기 가능 |
51
51
  | `pleum completion <zsh\|bash>` | 셸 자동완성 스크립트 출력 |
52
52
  | `pleum upgrade` | 최신 버전으로 재설치 |
package/bin/pleum.mjs CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ // #9: `pleum ... | head` 처럼 소비자가 먼저 닫히면 Node는 stdout 'error'(EPIPE)
3
+ // 를 unhandled로 크래시시킨다 — EPIPE만 정상 종료로 흡수한다(src/epipe.mjs).
4
+ import { installEpipeGuard } from "../src/epipe.mjs";
5
+ installEpipeGuard();
2
6
  import { readFileSync } from "node:fs";
3
7
  import { login, loginDevice, logout } from "../src/login.mjs";
4
8
  import { launch, launchHelp, recoverAgySettings } from "../src/launch.mjs";
@@ -10,7 +14,6 @@ import { usage } from "../src/usage.mjs";
10
14
  import { top } from "../src/top.mjs";
11
15
  import { health } from "../src/health.mjs";
12
16
  import { completion } from "../src/completion.mjs";
13
- import { upgrade } from "../src/upgrade.mjs";
14
17
  import { runUpdateCommand } from "../src/update.mjs";
15
18
  import { mainMenu, runAgentMenu } from "../src/menu.mjs";
16
19
  import { lang } from "../src/lang.mjs";
@@ -55,7 +58,7 @@ function buildHelp() {
55
58
  const examples = [
56
59
  ["pleum wire", null],
57
60
  ["pleum launch claude --model claude-sonnet-5", null],
58
- ["pleum launch codex --model gpt-5.3-codex -- --full-auto", null],
61
+ ["pleum launch codex --model gpt-6-sol -- --full-auto", null],
59
62
  ["pleum claude", null],
60
63
  ["pleum codex", null],
61
64
  ["pleum agy", null],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pleumrouter",
3
- "version": "0.5.37",
3
+ "version": "0.5.40",
4
4
  "description": "Official PleumRouter terminal CLI — launch any coding agent against PleumRouter.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/config.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
- import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync, renameSync } from "node:fs";
4
4
  import { t } from "./i18n.mjs";
5
5
 
6
6
  // 공개 게이트웨이 루트(canonical API). /v1/* 를 Next.js Route Handler 가 프록시하는
@@ -35,7 +35,16 @@ export function readConfig() {
35
35
 
36
36
  export function writeConfig(cfg) {
37
37
  mkdirSync(DIR, { recursive: true, mode: 0o700 });
38
- writeFileSync(FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
38
+ // 기존 파일 퍼미션과 무관하게 항상 0600이 되게 원자적으로 쓴다(#41과 동일 계약).
39
+ const tmp = `${FILE}.pleum-tmp-${process.pid}`;
40
+ writeFileSync(tmp, JSON.stringify(cfg, null, 2), { mode: 0o600 });
41
+ try {
42
+ chmodSync(tmp, 0o600);
43
+ renameSync(tmp, FILE);
44
+ } catch (e) {
45
+ try { unlinkSync(tmp); } catch {}
46
+ throw e;
47
+ }
39
48
  }
40
49
 
41
50
  // 로그인 저장 — 기존 설정(lang·launch_models·last_selection)을 보존한 채 자격증명만 병합.
@@ -241,16 +250,4 @@ export async function authFetch(path, init = {}) {
241
250
  });
242
251
  }
243
252
 
244
- /** 자동 업데이트 활성화 여부 (기본값: true). */
245
- export function isAutoUpdateEnabled(cfg = readConfig()) {
246
- return cfg?.auto_update !== false;
247
- }
248
-
249
- /** 자동 업데이트 설정 저장. */
250
- export function setAutoUpdateEnabled(enabled) {
251
- const cfg = readConfig() ?? {};
252
- cfg.auto_update = Boolean(enabled);
253
- writeConfig(cfg);
254
- }
255
-
256
253
 
package/src/epipe.mjs ADDED
@@ -0,0 +1,10 @@
1
+ // #9: `pleum ... | head` 등 조기 종료 파이프에서 stdout/stdstderr 'error'(EPIPE)
2
+ // 가 unhandled 크래시를 일으키던 것을 흡수한다. EPIPE만 정상 exit(0)으로 끝내고
3
+ // 그 외 오류는 그대로 전파된다(실패로 취급).
4
+ export function installEpipeGuard(streams = [process.stdout, process.stderr], exit = (n) => process.exit(n)) {
5
+ for (const stream of streams) {
6
+ stream?.on?.("error", (e) => {
7
+ if (e?.code === "EPIPE") exit(0);
8
+ });
9
+ }
10
+ }
@@ -0,0 +1,65 @@
1
+ import { spawn } from "node:child_process";
2
+ import { resolveLaunchCommand } from "./launch.mjs";
3
+ import { t } from "./i18n.mjs";
4
+
5
+ // 설치 명령 실행기(#40·#10) — RECIPES.install 문자열을 셸 없이 안전하게 실행한다.
6
+ //
7
+ // 지원 형태(정의된 단계만 실행):
8
+ // - 공백 구분 argv 명령 1개 이상("npm i -g x")
9
+ // - "&&" 로 이어진 순차 단계("python -m pip install aider-install && aider-install")
10
+ // 미지원(셸 문법 — shell:true 없이는 실행 불가, 주입 표면을 만들지 않는다):
11
+ // - 파이프("|")·리다이렉트(<>)·치환($`;) 등 → {manual: true} 로 수동 안내.
12
+ // Windows .cmd/.bat spawn(EINVAL)은 resolveLaunchCommand 가 node 진입점으로
13
+ // 우회해 예방한다(#10).
14
+
15
+ const SHELL_METACHARS = /[|<>;$`' "\\]/;
16
+
17
+ export function parseInstallCommand(install) {
18
+ const raw = String(install ?? "").trim();
19
+ if (!raw) return { manual: true, reason: "empty" };
20
+ // URL 단독(antigravity/goose) — 실행할 명령이 아니라 문서 링크.
21
+ if (/^https?:\/\//.test(raw) && !/\s/.test(raw)) return { manual: true, reason: "url" };
22
+ const steps = raw.split("&&").map((part) => part.trim().split(/\s+/).filter(Boolean));
23
+ if (steps.some((args) => args.length === 0)) return { manual: true, reason: "empty-step" };
24
+ if (steps.some((args) => args.some((tok) => SHELL_METACHARS.test(tok)))) {
25
+ return { manual: true, reason: "shell-syntax" };
26
+ }
27
+ return { steps };
28
+ }
29
+
30
+ /**
31
+ * 설치 명령 실행. {code, manual} 반환 — manual이면 호출자가 수동 안내를 출력한다.
32
+ * spawnImpl/resolve는 테스트 주입용.
33
+ */
34
+ export async function runInstallCommand(install, {
35
+ spawnImpl = spawn,
36
+ resolve = resolveLaunchCommand,
37
+ } = {}) {
38
+ const parsed = parseInstallCommand(install);
39
+ if (parsed.manual) return { manual: true, reason: parsed.reason, code: null };
40
+
41
+ for (const args of parsed.steps) {
42
+ const [bin, ...rest] = args;
43
+ const code = await new Promise((res) => {
44
+ let command;
45
+ try {
46
+ command = resolve(bin, rest);
47
+ } catch (e) {
48
+ console.error(`✗ ${e.message}`);
49
+ res(1);
50
+ return;
51
+ }
52
+ const child = spawnImpl(command.command, command.args, { stdio: "inherit", shell: false });
53
+ child.on("exit", (n) => res(n ?? 1));
54
+ child.on("error", () => res(1));
55
+ });
56
+ if (code !== 0) return { manual: false, code };
57
+ }
58
+ return { manual: false, code: 0 };
59
+ }
60
+
61
+ /** 수동 설치 안내 출력 — 미지원 셸 문법을 절대 임의 실행하지 않는다. */
62
+ export function printManualInstall(install) {
63
+ console.log(t("This install command needs a shell pipe; run it manually:"));
64
+ console.log(` $ ${install}`);
65
+ }
package/src/launch.mjs CHANGED
@@ -54,6 +54,14 @@ function nodeEntryFromWindowsShim(script, { read = readFileSync, exists = exists
54
54
  return exists(entry) ? entry : null;
55
55
  }
56
56
 
57
+ // POSIX 관례: 시그널 종료 exit code = 128 + 신호 번호(SIGINT=130, SIGTERM=143).
58
+ const SIGNAL_NUMBERS = { HUP: 1, INT: 2, QUIT: 3, ABRT: 6, KILL: 9, TERM: 15 };
59
+ export function signalExitCode(signal) {
60
+ if (!signal) return 0;
61
+ const num = SIGNAL_NUMBERS[signal.replace(/^SIG/, "").toUpperCase()];
62
+ return 128 + (num ?? 0);
63
+ }
64
+
57
65
  // 플랫폼·filesystem 주입으로 Windows 경로 정책을 Linux/macOS에서도 회귀 테스트한다.
58
66
  export function resolveLaunchCommand(bin, args, {
59
67
  platform = process.platform,
@@ -112,7 +120,8 @@ export function listAgyModelIds({ run = execFileSync } = {}) {
112
120
  const settingsDir = join(home, ".gemini", "antigravity-cli");
113
121
  mkdirSync(settingsDir, { recursive: true, mode: 0o700 });
114
122
  writeFileSync(join(settingsDir, "settings.json"), '{"modelProvider":"gemini"}', { mode: 0o600 });
115
- const output = run("agy", ["models"], {
123
+ const agyCmd = resolveLaunchCommand("agy", ["models"]);
124
+ const output = run(agyCmd.command, agyCmd.args, {
116
125
  encoding: "utf8",
117
126
  timeout: 15000,
118
127
  env: {
@@ -150,7 +159,8 @@ export function supportsAgyModel(model, ids) {
150
159
 
151
160
  function opencodeStateDir({ run = execFileSync } = {}) {
152
161
  try {
153
- const paths = run("opencode", ["debug", "paths"], { encoding: "utf8", timeout: 5000 });
162
+ const ocCmd = resolveLaunchCommand("opencode", ["debug", "paths"]);
163
+ const paths = run(ocCmd.command, ocCmd.args, { encoding: "utf8", timeout: 5000 });
154
164
  const state = /^state\s+(.+)$/m.exec(String(paths))?.[1]?.trim();
155
165
  if (state) return state;
156
166
  } catch {}
@@ -344,11 +354,13 @@ export async function launch(tool, model, passthrough, {
344
354
  if (exit) process.exit(1);
345
355
  resolve(1);
346
356
  });
347
- child.on("exit", (code) => {
357
+ child.on("exit", (code, signal) => {
348
358
  cleanup();
349
359
  removeListeners();
350
- if (exit) process.exit(code ?? 0);
351
- resolve(code ?? 0);
360
+ // 시그널 종료는 code가 null — exit 0은 "정상 완료"로 거짓 보고한다(#116).
361
+ const effective = code ?? signalExitCode(signal);
362
+ if (exit) process.exit(effective);
363
+ resolve(effective);
352
364
  });
353
365
  });
354
366
  }
@@ -9,7 +9,6 @@ export const ES = {
9
9
  "/help for commands · /exit to quit · wrap multi-line input in \"\"\" · Ctrl+C to stop generation": "/help para comandos · /exit para salir · envuelve la entrada multilínea en \"\"\" · Ctrl+C para detener la generación",
10
10
  "1) Add to {file}:": "1) Añade a {file}:",
11
11
  "2) Export the key and run:": "2) Exporta la clave y ejecuta:",
12
- "Account": "Cuenta",
13
12
  "All models — price and context included": "Todos los modelos — precio y contexto incluidos",
14
13
  "Anthropic's coding agent — supports subagents": "Agente de codificación de Anthropic — admite subagentes",
15
14
  "Attaching and uploading {n} explicit local image(s).": "Adjuntando y subiendo {n} imagen(es) local(es) seleccionada(s) explícitamente.",
@@ -31,8 +30,6 @@ export const ES = {
31
30
  "Local code execution & pair programmer": "Ejecución de código local y programador en pareja",
32
31
  "Available: {list}": "Disponibles: {list}",
33
32
  "Balance": "Saldo",
34
- "Balance is below the auto-charge threshold ({threshold} ₩).": "El saldo está por debajo del umbral de recarga automática ({threshold} ₩).",
35
- "Balance:": "Saldo:",
36
33
  "Block's local-first agent": "Agente local-first de Block",
37
34
  "Chat with a model": "Chatear con un modelo",
38
35
  "Check gateway connectivity and login status": "Verificar la conectividad del gateway y el estado de la sesión",
@@ -49,12 +46,9 @@ export const ES = {
49
46
  "Current system prompt:": "System prompt actual:",
50
47
  "Current version: {version}": "Versión actual: {version}",
51
48
  "Default models": "Modelos predeterminados",
52
- "Delete the saved key": "Eliminar la clave guardada",
53
49
  "Error:": "Error:",
54
- "Example: pleum launch claude --model claude-sonnet-4": "Ejemplo: pleum launch claude --model claude-sonnet-4",
55
50
  "Examples:": "Ejemplos:",
56
51
  "FX rate USD/KRW {rate}": "Tipo de cambio USD/KRW {rate}",
57
- "Failed to check balance (HTTP {status})": "No se pudo verificar el saldo (HTTP {status})",
58
52
  "Failed to fetch leaderboard ({status})": "No se pudo obtener el ranking ({status})",
59
53
  "Failed to fetch model detail ({status})": "No se pudo obtener el detalle del modelo ({status})",
60
54
  "Failed to fetch model list ({status})": "No se pudo obtener la lista de modelos ({status})",
@@ -87,24 +81,18 @@ export const ES = {
87
81
  "Gateway": "Gateway",
88
82
  "Gateway connection failed: {error}": "Error de conexión al gateway: {error}",
89
83
  "No response within {s}s": "Sin respuesta en {s} s",
90
- "Gateway connectivity · login status": "Conectividad del gateway · estado de la sesión",
91
84
  "Goodbye.": "Adiós.",
92
85
  "Goose sets its model in its own config — use the openai/<id> format in `goose configure`.": "Goose define su modelo en su propia configuración — usa el formato openai/<id> en `goose configure`.",
93
- "Health check": "Verificación de estado",
94
86
  "Install canceled — manual install: {install}": "Instalación cancelada — instalación manual: {install}",
95
87
  "Install failed (exit {code})": "Error de instalación (código de salida {code})",
96
88
  "Language": "Idioma",
97
89
  "Language set to {name}": "Idioma configurado a {name}",
98
90
  "Language, default models, account, updates": "Idioma, modelos predeterminados, cuenta, actualizaciones",
99
- "Leaderboard": "Ranking",
100
91
  "Log in": "Iniciar sesión",
101
92
  "Log in at the URL above, then enter the code (within {min} min).": "Inicia sesión en la URL de arriba y luego introduce el código (dentro de {min} min).",
102
93
  "Log in from your browser. If it doesn't open automatically, open this URL:": "Inicia sesión desde tu navegador. Si no se abre automáticamente, abre esta URL:",
103
- "Log in via browser + auto-save the key": "Iniciar sesión por navegador y guardar la clave automáticamente",
104
- "Log in via browser and save the API key": "Iniciar sesión por navegador y guardar la clave de API",
105
94
  "Log out? [Y/n] ": "¿Cerrar sesión? [Y/n] ",
106
95
  "Logged in": "Sesión iniciada",
107
- "Logged in. Saved the key to ~/.pleum/config.json.": "Sesión iniciada. Clave guardada en ~/.pleum/config.json.",
108
96
  "Logged in. Saved the session to ~/.pleum/config.json.": "Sesión iniciada. Sesión guardada en ~/.pleum/config.json.",
109
97
  "Legacy API key login is no longer supported. Run:\n pleum login": "El inicio de sesión con clave API heredada ya no es compatible. Ejecuta:\n pleum login",
110
98
  "Session expired. Run:\n pleum login": "La sesión ha caducado. Ejecuta:\n pleum login",
@@ -123,7 +111,6 @@ export const ES = {
123
111
  "Model list": "Lista de modelos",
124
112
  "Model list (price, context included)": "Lista de modelos (precio, contexto incluido)",
125
113
  "Model not found: {id} (list: pleum models)": "Modelo no encontrado: {id} (lista: pleum models)",
126
- "Model rankings · provider share": "Ranking de modelos · cuota por proveedor",
127
114
  "No data": "Sin datos",
128
115
  "No matches": "Sin coincidencias",
129
116
  "No matches — showing the full list.": "Sin coincidencias — mostrando la lista completa.",
@@ -145,8 +132,6 @@ export const ES = {
145
132
  "Print a zsh/bash completion script": "Imprimir un script de autocompletado de zsh/bash",
146
133
  "Print the version": "Imprimir la versión",
147
134
  "Provider share": "Cuota por proveedor",
148
- "Reinstall the latest version": "Reinstalar la última versión",
149
- "Reinstalling — pleum will exit when finished. Run `pleum` again afterward.": "Reinstalando — pleum se cerrará al terminar. Ejecuta pleum de nuevo después.",
150
135
  "Run a coding agent against PleumRouter": "Ejecutar un agente de codificación contra PleumRouter",
151
136
  "Run pleum login again to get a new key (recent keys include it by default).": "Ejecuta pleum login de nuevo para obtener una nueva clave (las claves recientes la incluyen por defecto).",
152
137
  "Running {label}: {cmd}": "Ejecutando {label}: {cmd}",
@@ -162,9 +147,7 @@ export const ES = {
162
147
  "Shell completion": "Autocompletado de shell",
163
148
  "Show/change display language (en|ko|ja|zh|es)": "Mostrar/cambiar el idioma de visualización (en|ko|ja|zh|es)",
164
149
  "Specify a model: pleum run <model> [\"prompt\"] (model selection isn't available on non-interactive stdin)": "Especifica un modelo: pleum run <model> [\"prompt\"] (la selección de modelo no está disponible en stdin no interactivo)",
165
- "Spend over the last 30 days": "Gasto de los últimos 30 días",
166
150
  "Start an interactive chat": "Iniciar un chat interactivo",
167
- "State mismatch or missing key": "Discrepancia de estado o clave faltante",
168
151
  "Stats {state}": "Estadísticas {state}",
169
152
  "Summarize in one line: ...": "Resume en una línea: ...",
170
153
  "Switched model →": "Modelo cambiado →",
@@ -172,7 +155,6 @@ export const ES = {
172
155
  "Terminal chat (REPL) / one-shot / pipe": "Chat de terminal (REPL) / un solo disparo / pipe",
173
156
  "Terminal pair programming": "Programación en pareja por terminal",
174
157
  "The code has expired.": "El código ha expirado.",
175
- "The server may not support API-key balance checks yet, or the key may have expired — try pleum login again.": "Es posible que el servidor aún no admita la verificación de saldo por clave de API, o que la clave haya expirado — intenta pleum login de nuevo.",
176
158
  "This API key doesn't have usage:read permission.": "Esta clave de API no tiene el permiso usage:read.",
177
159
  "Timed out: the code has expired.": "Tiempo de espera agotado: el código ha expirado.",
178
160
  "Total": "Total",
@@ -204,7 +186,6 @@ export const ES = {
204
186
  "Unsupported tool: {tool}": "Herramienta no compatible: {tool}",
205
187
  "Update CLI": "Actualizar la CLI",
206
188
  "Usage": "Uso",
207
- "Usage report": "Informe de uso",
208
189
  "Usage: pleum completion <zsh|bash>": "Uso: pleum completion <zsh|bash>",
209
190
  "Usage: pleum launch <tool> [--model id] [-- tool args]\n": "Uso: pleum launch <tool> [--model id] [-- tool args]\n",
210
191
  "Usage: pleum models <model-id> --info": "Uso: pleum models <model-id> --info",
@@ -244,7 +225,6 @@ export const ES = {
244
225
  "price = per 1M tokens (display rate; actual billing may vary slightly with the intraday rate)": "precio = por 1M tokens (tarifa de referencia; la facturación real puede variar ligeramente según la tarifa intradía)",
245
226
  "price/1M": "precio/1M",
246
227
  "select a model": "selecciona un modelo",
247
- "supported": "compatible",
248
228
  "switch: /model <id> · list: /models": "cambiar: /model <id> · lista: /models",
249
229
  "tokens": "tokens",
250
230
  "total": "total",
@@ -260,8 +240,6 @@ export const ES = {
260
240
  "{n} providers configured": "{n} proveedores configurados",
261
241
  "… {n} more — narrow with a search term": "… {n} más — acota con un término de búsqueda",
262
242
  "… {n} more — type to narrow down": "… {n} más — escribe para acotar",
263
- "↑/↓ move • enter run • → set model • esc quit": "↑/↓ mover • enter ejecutar • → definir modelo • esc salir",
264
- "↑/↓ move • enter run • → set model • esc back": "↑/↓ mover • enter ejecutar • → definir modelo • esc volver",
265
243
  "↑/↓ move • enter select • esc back": "↑/↓ mover • enter seleccionar • esc volver",
266
244
  "↑/↓ move • enter set model • esc back": "↑/↓ mover • enter definir modelo • esc volver",
267
245
  "⏹ aborted": "⏹ abortado",
@@ -287,48 +265,20 @@ export const ES = {
287
265
  "by user": "por usuario",
288
266
  "Failed to fetch balance ({status})": "Error al obtener el saldo ({status})",
289
267
 
268
+ "This install command needs a shell pipe; run it manually:": "Este comando de instalación requiere una tubería de shell; ejecútalo manualmente:",
269
+
290
270
  // Update & Auto-update
291
271
  "Update CLI to the latest version": "Actualizar CLI a la última versión",
292
272
  "Alias for pleum update --force": "Alias de pleum update --force",
293
- "Auto update": "Actualización automática",
294
- "Enabled (updates automatically on start)": "Activada (comprueba y actualiza al inicio)",
295
- "Disabled": "Desactivada",
296
273
  "Update available": "Actualización disponible",
297
- "Update available: v{current} → v{latest}": "Actualización disponible: v{current} → v{latest}",
298
- "Update available: v{current} → v{latest}. Run 'pleum update' to update.": "Actualización disponible: v{current} → v{latest}. Ejecute 'pleum update' para actualizar.",
299
- "Auto-updating PleumRouter CLI...": "Actualizando automáticamente PleumRouter CLI...",
300
274
  "Updating pleumrouter CLI...": "Actualizando PleumRouter CLI...",
301
275
  "Updating pleumrouter (force reinstall)...": "Actualizando PleumRouter (reinstalación forzada)...",
302
276
  "Already using the latest version of pleumrouter (v{version}).": "Ya está utilizando la última versión de PleumRouter (v{version}).",
303
277
  "Use --force (-f) to reinstall anyway.": "Utilice --force (-f) para reinstalar de todos modos.",
304
278
  "Updating pleumrouter from v{from} to v{to}...": "Actualizando PleumRouter de v{from} a v{to}...",
305
279
  "Update completed! Please run `pleum` again to use the new version.": "¡Actualización completada! Ejecute `pleum` de nuevo para usar la nueva versión.",
306
- "Update completed! Please run `pleum` again.": "¡Actualización completada! Ejecute `pleum` de nuevo.",
307
-
308
280
  // Claude Code Tier Mapping
309
- "Sonnet model": "Modelo Sonnet",
310
- "Opus model": "Modelo Opus",
311
- "Haiku model": "Modelo Haiku",
312
- "Use existing model configuration?": "¿Continuar con la configuración de modelos existente?",
313
- "Keep existing configuration": "Continuar con la configuración existente",
314
- "Reconfigure tiers": "Reconfigurar modelos",
315
- "Choose Sonnet, Opus, and Haiku models again": "Volver a elegir los modelos Sonnet, Opus y Haiku",
316
-
317
281
  // Codex & agy Effort
318
- "Select reasoning effort": "Seleccionar esfuerzo de razonamiento",
319
- "Medium (Recommended)": "Medio (Recomendado)",
320
- "Balanced reasoning speed and depth": "Velocidad y profundidad de razonamiento equilibradas",
321
- "High (Deep reasoning)": "Alto (Razonamiento profundo)",
322
- "Maximum reasoning depth for complex tasks": "Máxima profundidad de razonamiento para tareas complejas",
323
- "Low (Fast response)": "Bajo (Respuesta rápida)",
324
- "Lightweight reasoning for quick answers": "Razonamiento ligero para respuestas rápidas",
325
- "High (Recommended)": "Alto (Recomendado)",
326
- "High reasoning effort for standard tasks": "Alto esfuerzo de razonamiento para tareas estándar",
327
- "Max (Maximum reasoning)": "Máximo (Razonamiento extremo)",
328
- "Full reasoning effort for difficult problems": "Esfuerzo de razonamiento completo para problemas difíciles",
329
- "Medium (Standard)": "Medio (Estándar)",
330
- "Balanced reasoning effort": "Esfuerzo de razonamiento equilibrado",
331
- "Low reasoning effort for rapid iteration": "Bajo esfuerzo de razonamiento para iteraciones rápidas",
332
282
  // Agent launch modes
333
283
  "Antigravity custom supports only verified Gemini models in API-key mode.": "Antigravity custom solo admite modelos Gemini verificados en modo de clave API.",
334
284
  "Anthropic official models": "Continuar con modelos oficiales de Anthropic",
@@ -9,7 +9,6 @@ export const JA = {
9
9
  "/help for commands · /exit to quit · wrap multi-line input in \"\"\" · Ctrl+C to stop generation": "/help でコマンド一覧 · /exit で終了 · 複数行入力は \"\"\" で囲む · Ctrl+C で生成停止",
10
10
  "1) Add to {file}:": "1) {file} に追加:",
11
11
  "2) Export the key and run:": "2) キーをエクスポートして実行:",
12
- "Account": "アカウント",
13
12
  "All models — price and context included": "全モデル — 価格・コンテキスト込み",
14
13
  "Anthropic's coding agent — supports subagents": "Anthropic のコーディングエージェント — サブエージェント対応",
15
14
  "Attaching and uploading {n} explicit local image(s).": "明示的に選択したローカル画像 {n} 件を添付・アップロード中。",
@@ -31,8 +30,6 @@ export const JA = {
31
30
  "Local code execution & pair programmer": "ローカルコード実行&ペアプログラマー",
32
31
  "Available: {list}": "利用可能: {list}",
33
32
  "Balance": "残高",
34
- "Balance is below the auto-charge threshold ({threshold} ₩).": "残高が自動チャージのしきい値 ({threshold} ₩) を下回っています。",
35
- "Balance:": "残高:",
36
33
  "Block's local-first agent": "Block のローカルファーストエージェント",
37
34
  "Chat with a model": "モデルとチャット",
38
35
  "Check gateway connectivity and login status": "ゲートウェイ接続とログイン状態を確認",
@@ -49,12 +46,9 @@ export const JA = {
49
46
  "Current system prompt:": "現在のシステムプロンプト:",
50
47
  "Current version: {version}": "現在のバージョン: {version}",
51
48
  "Default models": "デフォルトモデル",
52
- "Delete the saved key": "保存済みキーを削除",
53
49
  "Error:": "エラー:",
54
- "Example: pleum launch claude --model claude-sonnet-4": "例: pleum launch claude --model claude-sonnet-4",
55
50
  "Examples:": "例:",
56
51
  "FX rate USD/KRW {rate}": "為替レート USD/KRW {rate}",
57
- "Failed to check balance (HTTP {status})": "残高確認に失敗しました (HTTP {status})",
58
52
  "Failed to fetch leaderboard ({status})": "リーダーボードの取得に失敗しました ({status})",
59
53
  "Failed to fetch model detail ({status})": "モデル詳細の取得に失敗しました ({status})",
60
54
  "Failed to fetch model list ({status})": "モデル一覧の取得に失敗しました ({status})",
@@ -88,24 +82,18 @@ export const JA = {
88
82
  "Gateway": "ゲートウェイ",
89
83
  "Gateway connection failed: {error}": "ゲートウェイ接続に失敗しました: {error}",
90
84
  "No response within {s}s": "{s}秒以内に応答がありません",
91
- "Gateway connectivity · login status": "ゲートウェイ接続 · ログイン状態",
92
85
  "Goodbye.": "さようなら。",
93
86
  "Goose sets its model in its own config — use the openai/<id> format in `goose configure`.": "Goose は自身の設定でモデルを指定します — `goose configure` で openai/<id> 形式を使用してください。",
94
- "Health check": "ヘルスチェック",
95
87
  "Install canceled — manual install: {install}": "インストールを中止しました — 手動インストール: {install}",
96
88
  "Install failed (exit {code})": "インストールに失敗しました (終了コード {code})",
97
89
  "Language": "言語",
98
90
  "Language set to {name}": "言語を {name} に設定しました",
99
91
  "Language, default models, account, updates": "言語・デフォルトモデル・アカウント・アップデート",
100
- "Leaderboard": "リーダーボード",
101
92
  "Log in": "ログイン",
102
93
  "Log in at the URL above, then enter the code (within {min} min).": "上記URLでログイン後、コードを入力してください ({min} 分以内)。",
103
94
  "Log in from your browser. If it doesn't open automatically, open this URL:": "ブラウザからログインしてください。自動的に開かない場合は、次のURLを開いてください:",
104
- "Log in via browser + auto-save the key": "ブラウザ経由でログイン + キーを自動保存",
105
- "Log in via browser and save the API key": "ブラウザ経由でログインしAPIキーを保存",
106
95
  "Log out? [Y/n] ": "ログアウトしますか? [Y/n] ",
107
96
  "Logged in": "ログイン済み",
108
- "Logged in. Saved the key to ~/.pleum/config.json.": "ログインしました。キーを ~/.pleum/config.json に保存しました。",
109
97
  "Logged in. Saved the session to ~/.pleum/config.json.": "ログインしました。セッションを ~/.pleum/config.json に保存しました。",
110
98
  "Legacy API key login is no longer supported. Run:\n pleum login": "レガシー API キーログインはサポートされなくなりました。実行してください:\n pleum login",
111
99
  "Session expired. Run:\n pleum login": "セッションの有効期限が切れました。実行してください:\n pleum login",
@@ -125,7 +113,6 @@ export const JA = {
125
113
  "Model list": "モデル一覧",
126
114
  "Model list (price, context included)": "モデル一覧 (価格・コンテキスト込み)",
127
115
  "Model not found: {id} (list: pleum models)": "モデルが見つかりません: {id} (一覧: pleum models)",
128
- "Model rankings · provider share": "モデルランキング · プロバイダーシェア",
129
116
  "No data": "データなし",
130
117
  "No matches": "一致なし",
131
118
  "No matches — showing the full list.": "一致なし — 全リストを表示します。",
@@ -149,10 +136,8 @@ export const JA = {
149
136
  "Print a zsh/bash completion script": "zsh/bash補完スクリプトを出力",
150
137
  "Print the version": "バージョンを表示",
151
138
  "Provider share": "プロバイダーシェア",
152
- "Reinstall the latest version": "最新バージョンを再インストール",
153
139
  "Removes the PleumRouter provider from Desktop config files": "Desktop 設定ファイルから PleumRouter プロバイダーを削除",
154
140
  "Revoked desktop API key {id}.": "デスクトップ API キー {id} を失効しました。",
155
- "Reinstalling — pleum will exit when finished. Run `pleum` again afterward.": "再インストール中です — 完了するとpleumは終了します。その後もう一度pleumを実行してください。",
156
141
  "Run a coding agent against PleumRouter": "PleumRouter に対してコーディングエージェントを実行",
157
142
  "Run pleum login again to get a new key (recent keys include it by default).": "新しいキーを取得するには pleum login を再実行してください (最近のキーはデフォルトで含まれます)。",
158
143
  "Running {label}: {cmd}": "{label} を実行中: {cmd}",
@@ -168,9 +153,7 @@ export const JA = {
168
153
  "Shell completion": "シェル補完",
169
154
  "Show/change display language (en|ko|ja|zh|es)": "表示言語を表示/変更 (en|ko|ja|zh|es)",
170
155
  "Specify a model: pleum run <model> [\"prompt\"] (model selection isn't available on non-interactive stdin)": "モデルを指定してください: pleum run <model> [\"prompt\"] (非対話的な標準入力ではモデル選択は使用できません)",
171
- "Spend over the last 30 days": "過去30日間の支出",
172
156
  "Start an interactive chat": "対話型チャットを開始",
173
- "State mismatch or missing key": "state不一致またはキー欠落",
174
157
  "Stats {state}": "統計 {state}",
175
158
  "Summarize in one line: ...": "一行で要約: ...",
176
159
  "Switched model →": "モデルを切り替えました →",
@@ -178,7 +161,6 @@ export const JA = {
178
161
  "Terminal chat (REPL) / one-shot / pipe": "ターミナルチャット (REPL) / ワンショット / パイプ",
179
162
  "Terminal pair programming": "ターミナルペアプログラミング",
180
163
  "The code has expired.": "コードの有効期限が切れました。",
181
- "The server may not support API-key balance checks yet, or the key may have expired — try pleum login again.": "サーバーがAPIキーの残高確認に未対応か、キーの有効期限が切れている可能性があります — pleum login を再度試してください。",
182
164
  "This API key doesn't have usage:read permission.": "このAPIキーには usage:read 権限がありません。",
183
165
  "Timed out: the code has expired.": "タイムアウト: コードの有効期限が切れました。",
184
166
  "Total": "合計",
@@ -202,7 +184,6 @@ export const JA = {
202
184
  "Unsupported tool: {tool}. Use claude or codex.": "サポートされていないツールです: {tool}。claude または codex を指定してください。",
203
185
  "Update CLI": "CLIを更新",
204
186
  "Usage": "使用状況",
205
- "Usage report": "使用状況レポート",
206
187
  "Usage: pleum completion <zsh|bash>": "使い方: pleum completion <zsh|bash>",
207
188
  "Usage: pleum launch <tool> [--model id] [-- tool args]\n": "使い方: pleum launch <tool> [--model id] [-- tool args]\n",
208
189
  "Usage: pleum models <model-id> --info": "使い方: pleum models <model-id> --info",
@@ -244,7 +225,6 @@ export const JA = {
244
225
  "price = per 1M tokens (display rate; actual billing may vary slightly with the intraday rate)": "価格 = 100万トークンあたり (表示レート; 実際の請求は日中レートによりわずかに異なる場合があります)",
245
226
  "price/1M": "価格/100万",
246
227
  "select a model": "モデルを選択",
247
- "supported": "サポート対象",
248
228
  "switch: /model <id> · list: /models": "切替: /model <id> · 一覧: /models",
249
229
  "tokens": "トークン",
250
230
  "total": "合計",
@@ -260,8 +240,6 @@ export const JA = {
260
240
  "{n} providers configured": "{n} 件のプロバイダーを設定済み",
261
241
  "… {n} more — narrow with a search term": "… 他{n}件 — 検索語で絞り込んでください",
262
242
  "… {n} more — type to narrow down": "… 他{n}件 — 入力して絞り込む",
263
- "↑/↓ move • enter run • → set model • esc quit": "↑/↓ 移動 • enter 実行 • → モデル設定 • esc 終了",
264
- "↑/↓ move • enter run • → set model • esc back": "↑/↓ 移動 • enter 実行 • → モデル設定 • esc 戻る",
265
243
  "↑/↓ move • enter select • esc back": "↑/↓ 移動 • enter 選択 • esc 戻る",
266
244
  "↑/↓ move • enter set model • esc back": "↑/↓ 移動 • enter モデル設定 • esc 戻る",
267
245
  "⏹ aborted": "⏹ 中断しました",
@@ -287,48 +265,20 @@ export const JA = {
287
265
  "by user": "ユーザー別",
288
266
  "Failed to fetch balance ({status})": "残高の取得に失敗しました ({status})",
289
267
 
268
+ "This install command needs a shell pipe; run it manually:": "シェルパイプが必要なインストールコマンドです。手動で実行してください:",
269
+
290
270
  // Update & Auto-update
291
271
  "Update CLI to the latest version": "CLIを最新バージョンにアップデート",
292
272
  "Alias for pleum update --force": "pleum update --force のエイリアス",
293
- "Auto update": "自動アップデート",
294
- "Enabled (updates automatically on start)": "有効 (起動時に自動確認・更新)",
295
- "Disabled": "無効",
296
273
  "Update available": "更新可能",
297
- "Update available: v{current} → v{latest}": "アップデートがあります: v{current} → v{latest}",
298
- "Update available: v{current} → v{latest}. Run 'pleum update' to update.": "アップデートがあります: v{current} → v{latest}。「pleum update」を実行して更新してください。",
299
- "Auto-updating PleumRouter CLI...": "PleumRouter CLI を自動アップデート中...",
300
274
  "Updating pleumrouter CLI...": "PleumRouter CLI をアップデート中...",
301
275
  "Updating pleumrouter (force reinstall)...": "PleumRouter をアップデート中 (強制再インストール)...",
302
276
  "Already using the latest version of pleumrouter (v{version}).": "すでに最新バージョンの PleumRouter を使用しています (v{version})。",
303
277
  "Use --force (-f) to reinstall anyway.": "強制再インストールするには --force (-f) を使用してください。",
304
278
  "Updating pleumrouter from v{from} to v{to}...": "PleumRouter を v{from} から v{to} へアップデート中...",
305
279
  "Update completed! Please run `pleum` again to use the new version.": "アップデート完了!新しいバージョンを使うには `pleum` を再起動してください。",
306
- "Update completed! Please run `pleum` again.": "アップデート完了!`pleum` を再起動してください。",
307
-
308
280
  // Claude Code Tier Mapping
309
- "Sonnet model": "Sonnet モデル",
310
- "Opus model": "Opus モデル",
311
- "Haiku model": "Haiku モデル",
312
- "Use existing model configuration?": "既存のモデル設定で続行しますか?",
313
- "Keep existing configuration": "既存の設定のまま続行",
314
- "Reconfigure tiers": "モデルを再設定",
315
- "Choose Sonnet, Opus, and Haiku models again": "Sonnet、Opus、Haiku モデルを再選択",
316
-
317
281
  // Codex & agy Effort
318
- "Select reasoning effort": "推論強度 (effort) の選択",
319
- "Medium (Recommended)": "中 (推奨)",
320
- "Balanced reasoning speed and depth": "推論速度と深さのバランス",
321
- "High (Deep reasoning)": "高 (深層推論)",
322
- "Maximum reasoning depth for complex tasks": "複雑なタスク向けの最大の推論深度",
323
- "Low (Fast response)": "低 (高速応答)",
324
- "Lightweight reasoning for quick answers": "素早い応答のための軽量推論",
325
- "High (Recommended)": "高 (推奨)",
326
- "High reasoning effort for standard tasks": "標準タスク向けの高い推論強度",
327
- "Max (Maximum reasoning)": "最大 (極限推論)",
328
- "Full reasoning effort for difficult problems": "難関タスク向けの最大の推論強度",
329
- "Medium (Standard)": "中 (標準)",
330
- "Balanced reasoning effort": "バランスの取れた標準的な推論強度",
331
- "Low reasoning effort for rapid iteration": "迅速なイテレーション向けの低い推論強度",
332
282
  // Agent launch modes
333
283
  "Antigravity custom supports only verified Gemini models in API-key mode.": "Antigravity custom は API キーモードで検証済みの Gemini モデルのみ対応します。",
334
284
  "Anthropic official models": "Anthropic 公式モデルで続行",
@@ -10,7 +10,6 @@ export const KO = {
10
10
  "✓ installed": "✓ 설치됨",
11
11
  "✗ not installed —": "✗ 미설치 —",
12
12
  "Manual setup (needs a provider definition file):": "설정 안내(provider 정의 파일 필요):",
13
- "Example: pleum launch claude --model claude-sonnet-4": "예: pleum launch claude --model claude-sonnet-4",
14
13
  "Unsupported tool: {tool}": "지원하지 않는 도구: {tool}",
15
14
  "Manual setup:": "설정 안내:",
16
15
  "Running {label}: {cmd}": "{label} 실행: {cmd}",
@@ -21,11 +20,9 @@ export const KO = {
21
20
  "2) Export the key and run:": "2) 키 export 후 실행:",
22
21
 
23
22
  // login.mjs
24
- "State mismatch or missing key": "state 불일치 또는 키 누락",
25
23
  "Log in from your browser. If it doesn't open automatically, open this URL:":
26
24
  "브라우저에서 로그인하세요. 자동으로 안 열리면 아래 URL을 여세요:",
27
25
  "Login timed out (2 minutes)": "로그인 시간 초과(2분)",
28
- "Logged in. Saved the key to ~/.pleum/config.json.": "로그인 완료. 키를 ~/.pleum/config.json 에 저장했습니다.",
29
26
  "Logged in. Saved the session to ~/.pleum/config.json.": "로그인 완료. 세션을 ~/.pleum/config.json 에 저장했습니다.",
30
27
  "Legacy API key login is no longer supported. Run:\n pleum login": "레거시 API 키 로그인은 더 이상 지원되지 않습니다. 실행하세요:\n pleum login",
31
28
  "Session expired. Run:\n pleum login": "세션이 만료되었습니다. 실행하세요:\n pleum login",
@@ -85,7 +82,6 @@ export const KO = {
85
82
  "out": "출력",
86
83
  "(incl. {pct}% markup)": "(마크업 {pct}% 포함)",
87
84
  "forced (can't be turned off)": "강제(끌 수 없음)",
88
- "supported": "지원",
89
85
  "usage": "사용량",
90
86
  "weekly": "주간",
91
87
  "total": "누적",
@@ -118,12 +114,6 @@ export const KO = {
118
114
  "Failed to fetch usage ({status})": "사용량 조회 실패 ({status})",
119
115
 
120
116
  // balance.mjs
121
- "Failed to check balance (HTTP {status})": "잔액 조회 실패 (HTTP {status})",
122
- "The server may not support API-key balance checks yet, or the key may have expired — try pleum login again.":
123
- "서버가 아직 API 키 잔액 조회를 지원하지 않거나 키가 만료됐을 수 있습니다 — pleum login 재시도.",
124
- "Balance:": "잔액:",
125
- "Balance is below the auto-charge threshold ({threshold} ₩).": "잔액이 자동충전 임계값({threshold} ₩) 아래입니다.",
126
-
127
117
  // health.mjs
128
118
  "Gateway connection failed: {error}": "게이트웨이 연결 실패: {error}",
129
119
  "No response within {s}s": "{s}초 안에 응답 없음",
@@ -170,7 +160,6 @@ export const KO = {
170
160
  "Terminal AI coding engine for complex tasks": "복잡한 작업을 위한 터미널 AI 코딩 엔진",
171
161
  "Local code execution & pair programmer": "로컬 코드 실행 및 페어 프로그래머",
172
162
  "Log in": "로그인",
173
- "Log in via browser and save the API key": "브라우저로 로그인하고 API 키를 저장합니다",
174
163
  "Chat with a model": "모델과 채팅",
175
164
  "Start an interactive chat": "대화형 채팅을 시작합니다",
176
165
  "model": "모델",
@@ -185,14 +174,8 @@ export const KO = {
185
174
  "Manual install:": "수동 설치:",
186
175
  "Model list": "모델 목록",
187
176
  "All models — price and context included": "전체 모델 — 가격·컨텍스트 포함",
188
- "Leaderboard": "리더보드",
189
- "Model rankings · provider share": "모델 랭킹·제공자 점유율",
190
- "Usage report": "지출 리포트",
191
- "Spend over the last 30 days": "최근 30일 내 지출",
192
177
  "Balance": "잔액",
193
178
  "Check your credit balance": "크레딧 잔액 조회",
194
- "Health check": "상태 점검",
195
- "Gateway connectivity · login status": "게이트웨이 연결·로그인 상태",
196
179
  "Failed to fetch model list: {error}": "모델 목록 조회 실패: {error}",
197
180
  "select a model": "모델 선택",
198
181
  "{label} needs manual installation:": "{label}는 수동 설치가 필요합니다:",
@@ -200,9 +183,6 @@ export const KO = {
200
183
  "Install canceled — manual install: {install}": "설치 취소 — 수동 설치: {install}",
201
184
  "Install failed (exit {code})": "설치 실패 (exit {code})",
202
185
  "{label} installed": "{label} 설치 완료",
203
- "↑/↓ move • enter run • → set model • esc quit": "↑/↓ 이동 • enter 실행 • → 모델 설정 • esc 종료",
204
- "↑/↓ move • enter run • → set model • esc back": "↑/↓ 이동 • enter 실행 • → 모델 설정 • esc 뒤로",
205
-
206
186
  // run.mjs
207
187
  [`Specify a model: pleum run <model> ["prompt"] (model selection isn't available on non-interactive stdin)`]:
208
188
  `모델을 지정하세요: pleum run <model> ["프롬프트"] (비대화형 stdin에선 모델 선택 불가)`,
@@ -260,9 +240,7 @@ export const KO = {
260
240
 
261
241
  // bin/pleum.mjs
262
242
  "(no args) interactive launcher menu — keyboard only (TTY only)": "(인자 없음) 인터랙티브 런처 메뉴 — 키보드 전용 (TTY 전용)",
263
- "Log in via browser + auto-save the key": "브라우저로 로그인 + 키 자동 저장",
264
243
  "--device: browser-less environments (SSH/CI)": "--device: 브라우저 없는 환경(SSH·CI)",
265
- "Delete the saved key": "저장된 키 삭제",
266
244
  "Run a coding agent against PleumRouter": "코딩 에이전트를 PleumRouter로 실행",
267
245
  "omit the tool to see the supported list and install status": "도구 생략 시 지원 목록·설치 여부 표시",
268
246
  "Terminal chat (REPL) / one-shot / pipe": "터미널 채팅(REPL) / 1회 실행 / 파이프",
@@ -274,7 +252,6 @@ export const KO = {
274
252
  "Check gateway connectivity and login status": "게이트웨이 연결·로그인 상태 점검",
275
253
  "Show/change display language (en|ko|ja|zh|es)": "표시 언어 조회/변경 (en|ko|ja|zh|es)",
276
254
  "Print a shell completion script": "셸 자동완성 스크립트 출력",
277
- "Reinstall the latest version": "최신 버전으로 재설치",
278
255
  "Print the version": "버전 출력",
279
256
  "interactive": "대화형",
280
257
  "Summarize in one line: ...": "한 줄로 요약: ...",
@@ -318,7 +295,6 @@ export const KO = {
318
295
  "Language": "언어",
319
296
  "Default models": "기본 모델",
320
297
  "Set the default model used per agent": "에이전트별 기본 모델 설정",
321
- "Account": "계정",
322
298
  "Update CLI": "CLI 업데이트",
323
299
  "Current version: {version}": "현재 버전: {version}",
324
300
  "Shell completion": "셸 자동완성",
@@ -327,8 +303,6 @@ export const KO = {
327
303
  "Use default (clear)": "기본값 사용(해제)",
328
304
  "Log out? [Y/n] ": "로그아웃할까요? [Y/n] ",
329
305
  "Choose a shell": "셸 선택",
330
- "Reinstalling — pleum will exit when finished. Run `pleum` again afterward.":
331
- "재설치 중입니다 — 완료되면 pleum이 종료됩니다. 이후 다시 pleum을 실행하세요.",
332
306
  "↑/↓ move • enter select • esc back": "↑/↓ 이동 • enter 선택 • esc 뒤로",
333
307
  "↑/↓ move • enter set model • esc back": "↑/↓ 이동 • enter 모델 설정 • esc 뒤로",
334
308
  "Log out": "로그아웃",
@@ -350,48 +324,20 @@ export const KO = {
350
324
  "by user": "사용자별",
351
325
  "Failed to fetch balance ({status})": "잔액 조회 실패 ({status})",
352
326
 
327
+ "This install command needs a shell pipe; run it manually:": "셸 파이프가 필요한 설치 명령입니다 — 직접 실행해 주세요:",
328
+
353
329
  // Update & Auto-update
354
330
  "Update CLI to the latest version": "CLI를 최신 버전으로 업데이트",
355
331
  "Alias for pleum update --force": "pleum update --force의 별칭",
356
- "Auto update": "자동 업데이트",
357
- "Enabled (updates automatically on start)": "켜짐 (시작 시 자동 확인 및 업데이트)",
358
- "Disabled": "꺼짐",
359
332
  "Update available": "업데이트 가능",
360
- "Update available: v{current} → v{latest}": "업데이트 가능: v{current} → v{latest}",
361
- "Update available: v{current} → v{latest}. Run 'pleum update' to update.": "업데이트 가능: v{current} → v{latest}. 'pleum update'를 실행하여 업데이트하세요.",
362
- "Auto-updating PleumRouter CLI...": "PleumRouter CLI 자동 업데이트 중...",
363
333
  "Updating pleumrouter CLI...": "PleumRouter CLI 업데이트 중...",
364
334
  "Updating pleumrouter (force reinstall)...": "PleumRouter 업데이트 중 (강제 재설치)...",
365
335
  "Already using the latest version of pleumrouter (v{version}).": "이미 최신 버전의 PleumRouter를 사용 중입니다 (v{version}).",
366
336
  "Use --force (-f) to reinstall anyway.": "재설치하려면 --force (-f) 옵션을 사용하세요.",
367
337
  "Updating pleumrouter from v{from} to v{to}...": "PleumRouter를 v{from}에서 v{to}로 업데이트 중...",
368
338
  "Update completed! Please run `pleum` again to use the new version.": "업데이트 완료! 새 버전을 사용하려면 `pleum`을 다시 실행하세요.",
369
- "Update completed! Please run `pleum` again.": "업데이트 완료! `pleum`을 다시 실행하세요.",
370
-
371
339
  // Claude Code Tier Mapping
372
- "Sonnet model": "Sonnet 모델",
373
- "Opus model": "Opus 모델",
374
- "Haiku model": "Haiku 모델",
375
- "Use existing model configuration?": "기존 모델 설정대로 진행하시겠습니까?",
376
- "Keep existing configuration": "기존 설정대로 진행",
377
- "Reconfigure tiers": "모델 다시 설정",
378
- "Choose Sonnet, Opus, and Haiku models again": "Sonnet, Opus, Haiku 모델 새로 지정",
379
-
380
340
  // Codex & agy Effort
381
- "Select reasoning effort": "추론 강도(effort) 선택",
382
- "Medium (Recommended)": "보통 (추천)",
383
- "Balanced reasoning speed and depth": "추론 속도와 깊이의 균형",
384
- "High (Deep reasoning)": "높음 (심층 추론)",
385
- "Maximum reasoning depth for complex tasks": "복잡한 작업을 위한 최대 추론 깊이",
386
- "Low (Fast response)": "낮음 (빠른 응답)",
387
- "Lightweight reasoning for quick answers": "빠른 응답을 위한 가벼운 추론",
388
- "High (Recommended)": "높음 (추천)",
389
- "High reasoning effort for standard tasks": "일반 작업을 위한 높은 추론 강도",
390
- "Max (Maximum reasoning)": "최대 (한계 추론)",
391
- "Full reasoning effort for difficult problems": "고난도 문제를 위한 최대 추론 강도",
392
- "Medium (Standard)": "보통 (표준)",
393
- "Balanced reasoning effort": "균형 잡힌 표준 추론 강도",
394
- "Low reasoning effort for rapid iteration": "빠른 반복 작업을 위한 낮은 추론 강도",
395
341
  // Agent launch modes
396
342
  "Antigravity custom supports only verified Gemini models in API-key mode.": "Antigravity custom은 API 키 모드에서 검증된 Gemini 모델만 지원합니다.",
397
343
  "Anthropic official models": "Anthropic 공식 모델로 진행",
@@ -9,7 +9,6 @@ export const ZH = {
9
9
  "/help for commands · /exit to quit · wrap multi-line input in \"\"\" · Ctrl+C to stop generation": "/help 查看命令 · /exit 退出 · 多行输入用 \"\"\" 包裹 · Ctrl+C 停止生成",
10
10
  "1) Add to {file}:": "1) 添加到 {file}:",
11
11
  "2) Export the key and run:": "2) 导出密钥并运行:",
12
- "Account": "账户",
13
12
  "All models — price and context included": "所有模型 — 含价格与上下文",
14
13
  "Anthropic's coding agent — supports subagents": "Anthropic 的编程代理 — 支持子代理",
15
14
  "Attaching and uploading {n} explicit local image(s).": "正在附加并上传明确选择的 {n} 张本地图片。",
@@ -31,8 +30,6 @@ export const ZH = {
31
30
  "Local code execution & pair programmer": "本地代码执行与结对编程器",
32
31
  "Available: {list}": "可用:{list}",
33
32
  "Balance": "余额",
34
- "Balance is below the auto-charge threshold ({threshold} ₩).": "余额低于自动充值阈值({threshold} ₩)。",
35
- "Balance:": "余额:",
36
33
  "Block's local-first agent": "Block 的本地优先代理",
37
34
  "Chat with a model": "与模型对话",
38
35
  "Check gateway connectivity and login status": "检查网关连通性与登录状态",
@@ -49,12 +46,9 @@ export const ZH = {
49
46
  "Current system prompt:": "当前系统提示词:",
50
47
  "Current version: {version}": "当前版本:{version}",
51
48
  "Default models": "默认模型",
52
- "Delete the saved key": "删除已保存的密钥",
53
49
  "Error:": "错误:",
54
- "Example: pleum launch claude --model claude-sonnet-4": "示例:pleum launch claude --model claude-sonnet-4",
55
50
  "Examples:": "示例:",
56
51
  "FX rate USD/KRW {rate}": "汇率 USD/KRW {rate}",
57
- "Failed to check balance (HTTP {status})": "查询余额失败(HTTP {status})",
58
52
  "Failed to fetch leaderboard ({status})": "获取排行榜失败({status})",
59
53
  "Failed to fetch model detail ({status})": "获取模型详情失败({status})",
60
54
  "Failed to fetch model list ({status})": "获取模型列表失败({status})",
@@ -87,24 +81,18 @@ export const ZH = {
87
81
  "Gateway": "网关",
88
82
  "Gateway connection failed: {error}": "网关连接失败:{error}",
89
83
  "No response within {s}s": "{s} 秒内无响应",
90
- "Gateway connectivity · login status": "网关连通性 · 登录状态",
91
84
  "Goodbye.": "再见。",
92
85
  "Goose sets its model in its own config — use the openai/<id> format in `goose configure`.": "Goose 在自己的配置中设置模型 — 在 `goose configure` 中使用 openai/<id> 格式。",
93
- "Health check": "健康检查",
94
86
  "Install canceled — manual install: {install}": "安装已取消 — 手动安装:{install}",
95
87
  "Install failed (exit {code})": "安装失败(退出码 {code})",
96
88
  "Language": "语言",
97
89
  "Language set to {name}": "语言已设置为 {name}",
98
90
  "Language, default models, account, updates": "语言·默认模型·账户·更新",
99
- "Leaderboard": "排行榜",
100
91
  "Log in": "登录",
101
92
  "Log in at the URL above, then enter the code (within {min} min).": "在上方 URL 登录,然后输入代码({min} 分钟内有效)。",
102
93
  "Log in from your browser. If it doesn't open automatically, open this URL:": "在浏览器中登录。如果没有自动打开,请手动打开此 URL:",
103
- "Log in via browser + auto-save the key": "通过浏览器登录 + 自动保存密钥",
104
- "Log in via browser and save the API key": "通过浏览器登录并保存 API 密钥",
105
94
  "Log out? [Y/n] ": "要登出吗? [Y/n] ",
106
95
  "Logged in": "已登录",
107
- "Logged in. Saved the key to ~/.pleum/config.json.": "已登录。密钥已保存至 ~/.pleum/config.json。",
108
96
  "Logged in. Saved the session to ~/.pleum/config.json.": "已登录。会话已保存至 ~/.pleum/config.json。",
109
97
  "Legacy API key login is no longer supported. Run:\n pleum login": "不再支持旧版 API 密钥登录。请运行:\n pleum login",
110
98
  "Session expired. Run:\n pleum login": "会话已过期。请运行:\n pleum login",
@@ -123,7 +111,6 @@ export const ZH = {
123
111
  "Model list": "模型列表",
124
112
  "Model list (price, context included)": "模型列表(含价格与上下文)",
125
113
  "Model not found: {id} (list: pleum models)": "未找到模型:{id} (列表:pleum models)",
126
- "Model rankings · provider share": "模型排名 · 提供商份额",
127
114
  "No data": "暂无数据",
128
115
  "No matches": "无匹配项",
129
116
  "No matches — showing the full list.": "无匹配项 — 显示完整列表。",
@@ -145,8 +132,6 @@ export const ZH = {
145
132
  "Print a zsh/bash completion script": "输出 zsh/bash 自动补全脚本",
146
133
  "Print the version": "输出版本号",
147
134
  "Provider share": "提供商份额",
148
- "Reinstall the latest version": "重新安装最新版本",
149
- "Reinstalling — pleum will exit when finished. Run `pleum` again afterward.": "正在重新安装 — 完成后 pleum 将退出。之后请再次运行 pleum。",
150
135
  "Run a coding agent against PleumRouter": "通过 PleumRouter 运行编程代理",
151
136
  "Run pleum login again to get a new key (recent keys include it by default).": "再次运行 pleum login 获取新密钥(近期签发的密钥默认已包含该权限)。",
152
137
  "Running {label}: {cmd}": "正在运行 {label}:{cmd}",
@@ -162,9 +147,7 @@ export const ZH = {
162
147
  "Shell completion": "Shell 自动补全",
163
148
  "Show/change display language (en|ko|ja|zh|es)": "显示/更改界面语言(en|ko|ja|zh|es)",
164
149
  "Specify a model: pleum run <model> [\"prompt\"] (model selection isn't available on non-interactive stdin)": "请指定模型:pleum run <model> [\"prompt\"] (非交互式 stdin 下无法选择模型)",
165
- "Spend over the last 30 days": "最近 30 天的花费",
166
150
  "Start an interactive chat": "开始交互式对话",
167
- "State mismatch or missing key": "状态不匹配或缺少密钥",
168
151
  "Stats {state}": "统计:{state}",
169
152
  "Summarize in one line: ...": "用一句话总结:...",
170
153
  "Switched model →": "已切换模型 →",
@@ -172,7 +155,6 @@ export const ZH = {
172
155
  "Terminal chat (REPL) / one-shot / pipe": "终端对话(REPL)/ 单次 / 管道",
173
156
  "Terminal pair programming": "终端结对编程",
174
157
  "The code has expired.": "代码已过期。",
175
- "The server may not support API-key balance checks yet, or the key may have expired — try pleum login again.": "服务器可能尚不支持 API 密钥余额查询,或密钥已过期 — 请重新运行 pleum login。",
176
158
  "This API key doesn't have usage:read permission.": "此 API 密钥没有 usage:read 权限。",
177
159
  "Timed out: the code has expired.": "已超时:代码已过期。",
178
160
  "Total": "总计",
@@ -204,7 +186,6 @@ export const ZH = {
204
186
  "Unsupported tool: {tool}": "不支持的工具:{tool}",
205
187
  "Update CLI": "更新 CLI",
206
188
  "Usage": "用量",
207
- "Usage report": "用量报告",
208
189
  "Usage: pleum completion <zsh|bash>": "用法:pleum completion <zsh|bash>",
209
190
  "Usage: pleum launch <tool> [--model id] [-- tool args]\n": "用法:pleum launch <tool> [--model id] [-- tool args]\n",
210
191
  "Usage: pleum models <model-id> --info": "用法:pleum models <model-id> --info",
@@ -244,7 +225,6 @@ export const ZH = {
244
225
  "price = per 1M tokens (display rate; actual billing may vary slightly with the intraday rate)": "价格 = 每 1M tokens(显示汇率;实际计费可能随盘中汇率略有浮动)",
245
226
  "price/1M": "价格/1M",
246
227
  "select a model": "选择模型",
247
- "supported": "支持",
248
228
  "switch: /model <id> · list: /models": "切换:/model <id> · 列表:/models",
249
229
  "tokens": "tokens",
250
230
  "total": "总计",
@@ -260,8 +240,6 @@ export const ZH = {
260
240
  "{n} providers configured": "已配置 {n} 个提供商",
261
241
  "… {n} more — narrow with a search term": "… 还有 {n} 项 — 输入关键词缩小范围",
262
242
  "… {n} more — type to narrow down": "… 还有 {n} 项 — 输入以缩小范围",
263
- "↑/↓ move • enter run • → set model • esc quit": "↑/↓ 移动 • enter 运行 • → 设置模型 • esc 退出",
264
- "↑/↓ move • enter run • → set model • esc back": "↑/↓ 移动 • enter 运行 • → 设置模型 • esc 返回",
265
243
  "↑/↓ move • enter select • esc back": "↑/↓ 移动 • enter 选择 • esc 返回",
266
244
  "↑/↓ move • enter set model • esc back": "↑/↓ 移动 • enter 设置模型 • esc 返回",
267
245
  "⏹ aborted": "⏹ 已中止",
@@ -287,48 +265,20 @@ export const ZH = {
287
265
  "by user": "按用户",
288
266
  "Failed to fetch balance ({status})": "获取余额失败 ({status})",
289
267
 
268
+ "This install command needs a shell pipe; run it manually:": "此安装命令需要 shell 管道,请手动运行:",
269
+
290
270
  // Update & Auto-update
291
271
  "Update CLI to the latest version": "将 CLI 更新到最新版本",
292
272
  "Alias for pleum update --force": "pleum update --force 的别名",
293
- "Auto update": "自动更新",
294
- "Enabled (updates automatically on start)": "已启用 (启动时自动检查并更新)",
295
- "Disabled": "已禁用",
296
273
  "Update available": "有可用更新",
297
- "Update available: v{current} → v{latest}": "有可用更新: v{current} → v{latest}",
298
- "Update available: v{current} → v{latest}. Run 'pleum update' to update.": "有可用更新: v{current} → v{latest}。运行 'pleum update' 进行更新。",
299
- "Auto-updating PleumRouter CLI...": "正在自动更新 PleumRouter CLI...",
300
274
  "Updating pleumrouter CLI...": "正在更新 PleumRouter CLI...",
301
275
  "Updating pleumrouter (force reinstall)...": "正在更新 PleumRouter (强制重新安装)...",
302
276
  "Already using the latest version of pleumrouter (v{version}).": "已在使用最新版本的 PleumRouter (v{version})。",
303
277
  "Use --force (-f) to reinstall anyway.": "如需强制重新安装,请使用 --force (-f)。",
304
278
  "Updating pleumrouter from v{from} to v{to}...": "正在将 PleumRouter 从 v{from} 更新到 v{to}...",
305
279
  "Update completed! Please run `pleum` again to use the new version.": "更新完成!请重新运行 `pleum` 以使用新版本。",
306
- "Update completed! Please run `pleum` again.": "更新完成!请重新运行 `pleum`。",
307
-
308
280
  // Claude Code Tier Mapping
309
- "Sonnet model": "Sonnet 模型",
310
- "Opus model": "Opus 模型",
311
- "Haiku model": "Haiku 模型",
312
- "Use existing model configuration?": "使用现有的模型配置继续吗?",
313
- "Keep existing configuration": "保持现有配置继续",
314
- "Reconfigure tiers": "重新配置模型",
315
- "Choose Sonnet, Opus, and Haiku models again": "重新选择 Sonnet、Opus 和 Haiku 模型",
316
-
317
281
  // Codex & agy Effort
318
- "Select reasoning effort": "选择推理强度 (effort)",
319
- "Medium (Recommended)": "中 (推荐)",
320
- "Balanced reasoning speed and depth": "推理速度与深度的平衡",
321
- "High (Deep reasoning)": "高 (深度推理)",
322
- "Maximum reasoning depth for complex tasks": "适合复杂任务的最大推理深度",
323
- "Low (Fast response)": "低 (快速响应)",
324
- "Lightweight reasoning for quick answers": "快速解答的轻量推理",
325
- "High (Recommended)": "高 (推荐)",
326
- "High reasoning effort for standard tasks": "标准任务的高推理强度",
327
- "Max (Maximum reasoning)": "最大 (极值推理)",
328
- "Full reasoning effort for difficult problems": "困难问题的全力推理强度",
329
- "Medium (Standard)": "中 (标准)",
330
- "Balanced reasoning effort": "平衡的标准推理强度",
331
- "Low reasoning effort for rapid iteration": "快速迭代的低推理强度",
332
282
  // Agent launch modes
333
283
  "Antigravity custom supports only verified Gemini models in API-key mode.": "Antigravity 自选模型在 API 密钥模式下仅支持已验证的 Gemini 模型。",
334
284
  "Anthropic official models": "使用 Anthropic 官方模型",
package/src/login.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer } from "node:http";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { spawn } from "node:child_process";
4
- import { ROOT, SITE_ROOT, V1, readConfig, writeConfig, mergeLoginConfig, clearCredentials, setActiveContext } from "./config.mjs";
4
+ import { ROOT, SITE_ROOT, V1, readConfig, writeConfig, mergeLoginConfig, clearCredentials, setActiveContext, refreshAccessToken, accessTokenIsStale } from "./config.mjs";
5
5
  import { t } from "./i18n.mjs";
6
6
  import { sanitizeTerminalText } from "./ansi.mjs";
7
7
 
@@ -99,14 +99,17 @@ export async function login() {
99
99
  payload.refresh_token;
100
100
  res.writeHead(ok ? 200 : 400, { "Content-Type": "application/json" });
101
101
  res.end(JSON.stringify({ ok: Boolean(ok) }));
102
+ if (!ok) {
103
+ // 오청 1회가 전체 플로우를 죽이지 않는다(#116) — 브라우저의 사족
104
+ // 요청(favicon 등)이 오면 400로 답하고 다음(정상) 콜백을 계속 기다린다.
105
+ return;
106
+ }
102
107
  server.close();
103
- if (ok) {
104
- resolve({
105
- access_token: payload.access_token,
106
- refresh_token: payload.refresh_token,
107
- expires_in: payload.expires_in,
108
- });
109
- } else reject(new Error(t("State mismatch or missing key")));
108
+ resolve({
109
+ access_token: payload.access_token,
110
+ refresh_token: payload.refresh_token,
111
+ expires_in: payload.expires_in,
112
+ });
110
113
  });
111
114
  });
112
115
 
@@ -186,10 +189,19 @@ export async function logout() {
186
189
  const cfg = readConfig();
187
190
  if (cfg?.access_token) {
188
191
  // best-effort — 서버가 죽어 있어도 로컬 자격증명은 반드시 지운다.
189
- await fetch(`${V1}/auth/logout`, {
190
- method: "POST",
191
- headers: { Authorization: `Bearer ${cfg.access_token}` },
192
- }).catch(() => {});
192
+ // #153: access_token이 만료된 채 두면 폐기가 401로 조용히 생략된다 —
193
+ // requireAuth/authFetch는 미로그인 시 process.exit하므로 로그아웃 경로에
194
+ // 못 쓴다. 순수 refresh 갱신 후 유효한 토큰으로 폐기한다(실패는 무시).
195
+ let token = cfg.access_token;
196
+ if (accessTokenIsStale(token, 0)) {
197
+ token = (await refreshAccessToken(cfg).catch(() => null)) || token;
198
+ }
199
+ if (token) {
200
+ await fetch(`${V1}/auth/logout`, {
201
+ method: "POST",
202
+ headers: { Authorization: `Bearer ${token}` },
203
+ }).catch(() => {});
204
+ }
193
205
  }
194
206
  if (cfg) writeConfig(clearCredentials(cfg));
195
207
  }
package/src/menu.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // bare `pleum` 인라인 런처. 첫 화면의 에이전트 실행 → 도구 → 실행 모드를 공유한다.
2
2
 
3
3
  import { readFileSync } from "node:fs";
4
- import { spawn } from "node:child_process";
4
+
5
5
  import { createInterface } from "node:readline";
6
6
  import { style, displayWidth, terminalWidth, terminalHeight, hyperlink, sanitizeTerminalText } from "./ansi.mjs";
7
7
  import { keyFeeder, select } from "./interactive.mjs";
@@ -14,6 +14,7 @@ import { login } from "./login.mjs";
14
14
  import { balance } from "./balance.mjs";
15
15
  import { runSettings } from "./settings.mjs";
16
16
  import { t } from "./i18n.mjs";
17
+ import { parseInstallCommand, runInstallCommand, printManualInstall } from "./install.mjs";
17
18
 
18
19
  // 값은 t() 키(영어 원문)만 담는다 — 여기서 t()를 호출하면 모듈 로드 시점(import 1회)에
19
20
  // 번역이 확정돼, 같은 프로세스 안에서 언어를 바꿔도(Settings) 이 설명들만 예전 언어로 남는다.
@@ -40,7 +41,9 @@ const AGENT_DESC = {
40
41
  interpreter: "Local code execution & pair programmer",
41
42
  };
42
43
 
43
- const autoInstallable = (r) => !/^https?:/.test(r.install); // URL이면 수동 설치 안내만
44
+ // 실행 가능한(셸 문법 없는 argv 단계) 설치만 자동 설치 — URL·파이프·리다이렉트는
45
+ // 수동 안내한다(#40: naive split이 "|"·"&&" 를 인자로 넘겨 전부 실패하던 결함).
46
+ const autoInstallable = (r) => !parseInstallCommand(r.install).manual;
44
47
 
45
48
  // 최상위 메뉴 항목(순수 함수 — 테스트 대상). 로그인은 첫 에이전트 실행 시 시작한다.
46
49
  export function buildItems({ savedModels = {}, activeContext = { type: "personal" } }) {
@@ -212,15 +215,15 @@ async function ensureInstalled(r) {
212
215
  return false;
213
216
  }
214
217
  console.log(style.dim(`$ ${r.install}`));
215
- const code = await new Promise((res) => {
216
- const [cmd, ...args] = r.install.trim().split(/\s+/);
217
- const bin = process.platform === "win32" && (cmd === "npm" || cmd === "npx" || cmd === "yarn" || cmd === "pnpm") ? `${cmd}.cmd` : cmd;
218
- const c = spawn(bin, args, { stdio: "inherit" });
219
- c.on("exit", (n) => res(n ?? 1));
220
- c.on("error", () => res(1));
221
- });
222
- if (code !== 0) {
223
- console.log(`${style.red("✗")} ${t("Install failed (exit {code})", { code })}`);
218
+ // 공용 실행기: "&&" 순차 단계 지원, Windows .cmd는 resolveLaunchCommand 우회,
219
+ // 셸 문법(파이프 등)은 절대 임의 실행하지 않고 수동 안내로 명시한다.
220
+ const result = await runInstallCommand(r.install);
221
+ if (result.manual) {
222
+ printManualInstall(r.install);
223
+ return false;
224
+ }
225
+ if (result.code !== 0) {
226
+ console.log(`${style.red("✗")} ${t("Install failed (exit {code})", { code: result.code })}`);
224
227
  return false;
225
228
  }
226
229
  console.log(`${style.green("✓")} ${t("{label} installed", { label: r.label })}`);
package/src/models.mjs CHANGED
@@ -181,7 +181,7 @@ export async function listModels(rest) {
181
181
  // ── 모델 상세(ollama show 대응) — GET /v1/models/{id} ────────────────────
182
182
  // id는 provider/model 형식이라 슬래시를 그대로 둔다(백엔드 :path 컨버터).
183
183
  export async function fetchModelDetail(id) {
184
- const res = await fetch(`${V1}/models/${id}`);
184
+ const res = await fetch(`${V1}/models/${encodeURIComponent(id)}`);
185
185
  if (res.status === 404) throw new Error(t("Model not found: {id} (list: pleum models)", { id }));
186
186
  if (!res.ok) throw new Error(t("Failed to fetch model detail ({status})", { status: res.status }));
187
187
  return res.json();
package/src/settings.mjs CHANGED
@@ -13,7 +13,6 @@ import { getLocale, LANGS, t } from "./i18n.mjs";
13
13
  import { RECIPES } from "./recipes.mjs";
14
14
  import { fetchModels, matchModels, formatModelLabel } from "./models.mjs";
15
15
  import { login, logout } from "./login.mjs";
16
- import { upgrade } from "./upgrade.mjs";
17
16
  import { completion } from "./completion.mjs";
18
17
  import { lang } from "./lang.mjs";
19
18
  import { switchCreditWorkspace } from "./balance.mjs";
package/src/update.mjs CHANGED
@@ -3,6 +3,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { t } from "./i18n.mjs";
6
+ import { resolveLaunchCommand } from "./launch.mjs";
6
7
 
7
8
  const REGISTRY_URL = "https://registry.npmjs.org/pleumrouter/latest";
8
9
  const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1시간 캐시
@@ -112,10 +113,14 @@ export async function checkLatestVersion({
112
113
  };
113
114
  }
114
115
 
115
- export function performUpdate({ bin = null } = {}) {
116
- const npmCmd = bin || (process.platform === "win32" ? "npm.cmd" : "npm");
116
+ export function performUpdate({ bin = null, resolve = resolveLaunchCommand, spawn: spawnImpl = spawn } = {}) {
117
+ // Windows에서 npm.cmd 를 shell 없이 spawn 하면 EINVAL(Node 18.20+) — 공용
118
+ // 경로로 node 진입점을 찾아 우회한다(#10). 비윈도는 명령을 그대로 둔다.
119
+ const target = bin
120
+ ? { command: bin, args: ["install", "-g", "pleumrouter@latest"] }
121
+ : resolve("npm", ["install", "-g", "pleumrouter@latest"]);
117
122
  return new Promise((resolve) => {
118
- const child = spawn(npmCmd, ["install", "-g", "pleumrouter@latest"], {
123
+ const child = spawnImpl(target.command, target.args, {
119
124
  stdio: "inherit",
120
125
  shell: false,
121
126
  });
package/src/wire.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { dirname, join } from "node:path";
3
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { ROOT, V1, authFetch } from "./config.mjs";
5
5
  import { tomlBasicString } from "./recipes.mjs";
6
6
  import { t } from "./i18n.mjs";
@@ -40,9 +40,19 @@ function readText(path) {
40
40
  }
41
41
  }
42
42
 
43
- function writeSecretFile(path, contents, mode = 0o600) {
43
+ export function writeSecretFile(path, contents, mode = 0o600) {
44
44
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
45
- writeFileSync(path, contents, { mode });
45
+ // 기존 파일이 0644 등으로 있으면 writeFileSync의 mode는 무시된다(#41) —
46
+ // tmp(강제 chmod) + rename 원자적 교체로 결과물이 항상 mode가 되게 한다.
47
+ const tmp = `${path}.pleum-tmp-${process.pid}`;
48
+ writeFileSync(tmp, contents, { mode });
49
+ try {
50
+ chmodSync(tmp, mode);
51
+ renameSync(tmp, path);
52
+ } catch (e) {
53
+ try { unlinkSync(tmp); } catch {} // 키 평문 잔여를 남기지 않는다
54
+ throw e;
55
+ }
46
56
  }
47
57
 
48
58
  function backupOnce(src, bak) {
package/src/upgrade.mjs DELETED
@@ -1,10 +0,0 @@
1
- import { performUpdate, runUpdateCommand } from "./update.mjs";
2
-
3
- export { performUpdate, runUpdateCommand };
4
-
5
- // 최신 버전으로 전역 재설치. 하위 호환을 위해 유지하며 process.exit() 호출 규약을 보존한다.
6
- export async function upgrade() {
7
- const code = await runUpdateCommand(["--force"], { exit: false });
8
- process.exit(code ?? 0);
9
- }
10
-