pleumrouter 0.5.30 → 0.5.32
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/bin/pleum.mjs +10 -3
- package/package.json +1 -1
- package/src/ansi.mjs +4 -0
- package/src/completion.mjs +1 -1
- package/src/config.mjs +13 -0
- package/src/interactive.mjs +35 -9
- package/src/launch.mjs +24 -5
- package/src/locales/es.mjs +54 -2
- package/src/locales/ja.mjs +54 -2
- package/src/locales/ko.mjs +54 -2
- package/src/locales/zh.mjs +54 -2
- package/src/menu.mjs +315 -35
- package/src/models.mjs +191 -5
- package/src/recipes.mjs +96 -65
- package/src/settings.mjs +34 -8
- package/src/update.mjs +157 -0
- package/src/upgrade.mjs +8 -14
package/src/recipes.mjs
CHANGED
|
@@ -7,12 +7,12 @@ export const CORE_MODELS = [
|
|
|
7
7
|
"claude-sonnet-5",
|
|
8
8
|
"claude-opus-5-5",
|
|
9
9
|
"gpt-6-astra",
|
|
10
|
-
"
|
|
10
|
+
"gpt-6-sol",
|
|
11
|
+
"gpt-6-luna",
|
|
11
12
|
"gemini-3.8-flash",
|
|
13
|
+
"qwen3.8-max",
|
|
12
14
|
"glm-5.3-flash",
|
|
13
15
|
"kimi-k3",
|
|
14
|
-
"gpt-6-sol",
|
|
15
|
-
"gpt-6-luna",
|
|
16
16
|
];
|
|
17
17
|
|
|
18
18
|
// 도구별 설정의 진실 원천. 출처: frontend .../docs/cookbook/{codex,claude-code,...} + 에이전트별 공식 문서.
|
|
@@ -42,14 +42,29 @@ export const RECIPES = {
|
|
|
42
42
|
},
|
|
43
43
|
// 함정: ANTHROPIC_BASE_URL은 반드시 루트(/v1 없이) — CLI가 /v1/messages를 스스로 덧붙인다.
|
|
44
44
|
// /v1을 넣으면 /v1/v1/messages가 되어 실패.
|
|
45
|
-
env: (key) =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
env: (key, m) => {
|
|
46
|
+
const isObj = m && typeof m === "object";
|
|
47
|
+
const sonnet = (isObj ? m.sonnet : m) || "claude-sonnet-5";
|
|
48
|
+
const opus = (isObj ? m.opus : null) || "claude-opus-5-5";
|
|
49
|
+
const haiku = (isObj ? m.haiku : null) || DEFAULT_MODEL;
|
|
50
|
+
const toAnthropic = (x) => (x.startsWith("anthropic/") ? x : `anthropic/${x}`);
|
|
51
|
+
return {
|
|
52
|
+
ANTHROPIC_BASE_URL: ROOT,
|
|
53
|
+
ANTHROPIC_API_KEY: key,
|
|
54
|
+
ANTHROPIC_AUTH_TOKEN: key,
|
|
55
|
+
// /model 이 GET {ANTHROPIC_BASE_URL}/v1/models 를 치게 한다. 기본값은 꺼져 있다.
|
|
56
|
+
CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
|
|
57
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: toAnthropic(sonnet),
|
|
58
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: toAnthropic(opus),
|
|
59
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: toAnthropic(haiku),
|
|
60
|
+
ANTHROPIC_SMALL_MODEL: toAnthropic(haiku),
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
args: (m) => {
|
|
64
|
+
const isObj = m && typeof m === "object";
|
|
65
|
+
const main = (isObj ? m.sonnet : m) || DEFAULT_MODEL;
|
|
66
|
+
return ["--model", main.startsWith("anthropic/") ? main : `anthropic/${main}`];
|
|
67
|
+
},
|
|
53
68
|
},
|
|
54
69
|
codex: {
|
|
55
70
|
label: "Codex CLI",
|
|
@@ -61,7 +76,67 @@ export const RECIPES = {
|
|
|
61
76
|
referer: "https://github.com/openai/codex",
|
|
62
77
|
},
|
|
63
78
|
env: (key) => ({ PLEUM_API_KEY: key }),
|
|
64
|
-
args: () =>
|
|
79
|
+
args: (m) => {
|
|
80
|
+
const isObj = m && typeof m === "object";
|
|
81
|
+
const effort = isObj ? m.effort : null;
|
|
82
|
+
return effort ? ["-c", `model_reasoning_effort="${effort}"`] : [];
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
agy: {
|
|
86
|
+
label: "Antigravity CLI",
|
|
87
|
+
bin: "agy",
|
|
88
|
+
type: "agy-settings",
|
|
89
|
+
install: "https://antigravity.google/cli/install.sh",
|
|
90
|
+
note: "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. Routes any coding model via PleumRouter.",
|
|
91
|
+
attribution: {
|
|
92
|
+
title: "Antigravity CLI",
|
|
93
|
+
referer: "https://www.antigravity.google/docs/cli/install",
|
|
94
|
+
},
|
|
95
|
+
env: (key) => ({
|
|
96
|
+
GOOGLE_GEMINI_BASE_URL: ROOT,
|
|
97
|
+
GEMINI_API_KEY: key,
|
|
98
|
+
GOOGLE_API_KEY: key,
|
|
99
|
+
}),
|
|
100
|
+
args: (m) => {
|
|
101
|
+
const isObj = m && typeof m === "object";
|
|
102
|
+
const model = (isObj ? m.model : m) || DEFAULT_MODEL;
|
|
103
|
+
const effort = isObj ? m.effort : null;
|
|
104
|
+
const res = ["--model", model];
|
|
105
|
+
if (effort) res.push("--effort", effort);
|
|
106
|
+
return res;
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
opencode: {
|
|
110
|
+
label: "opencode",
|
|
111
|
+
bin: "opencode",
|
|
112
|
+
type: "env",
|
|
113
|
+
install: "npm i -g opencode-ai@latest",
|
|
114
|
+
// opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만 읽는다 — env var는 없다.
|
|
115
|
+
// 대신 OPENCODE_CONFIG_CONTENT(파일 대신 JSON 문자열을 그대로 읽는 env var)로 파일 없이 주입한다.
|
|
116
|
+
note: "Generates a temporary PleumRouter provider config for this run only — your own opencode.json is untouched.",
|
|
117
|
+
attribution: {
|
|
118
|
+
title: "opencode",
|
|
119
|
+
referer: "https://opencode.ai",
|
|
120
|
+
},
|
|
121
|
+
env: (key, model) => {
|
|
122
|
+
const active = model || DEFAULT_MODEL;
|
|
123
|
+
const models = Object.fromEntries(CORE_MODELS.map((id) => [id, {}]));
|
|
124
|
+
models[active] = {};
|
|
125
|
+
return {
|
|
126
|
+
PLEUM_API_KEY: key,
|
|
127
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
128
|
+
provider: {
|
|
129
|
+
pleum: {
|
|
130
|
+
npm: "@ai-sdk/openai-compatible",
|
|
131
|
+
name: "PleumRouter",
|
|
132
|
+
options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
|
|
133
|
+
models,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
args: (m) => ["--model", `pleum/${m || DEFAULT_MODEL}`],
|
|
65
140
|
},
|
|
66
141
|
aider: {
|
|
67
142
|
label: "Aider",
|
|
@@ -101,38 +176,6 @@ export const RECIPES = {
|
|
|
101
176
|
env: (key) => ({ OPENAI_API_BASE: V1, OPENAI_API_KEY: key }),
|
|
102
177
|
args: () => [],
|
|
103
178
|
},
|
|
104
|
-
opencode: {
|
|
105
|
-
label: "opencode",
|
|
106
|
-
bin: "opencode",
|
|
107
|
-
type: "env",
|
|
108
|
-
install: "npm i -g opencode-ai@latest",
|
|
109
|
-
// opencode는 커스텀 엔드포인트를 opencode.json provider 블록으로만 읽는다 — env var는 없다.
|
|
110
|
-
// 대신 OPENCODE_CONFIG_CONTENT(파일 대신 JSON 문자열을 그대로 읽는 env var)로 파일 없이 주입한다.
|
|
111
|
-
note: "Generates a temporary PleumRouter provider config for this run only — your own opencode.json is untouched.",
|
|
112
|
-
attribution: {
|
|
113
|
-
title: "opencode",
|
|
114
|
-
referer: "https://opencode.ai",
|
|
115
|
-
},
|
|
116
|
-
env: (key, model) => {
|
|
117
|
-
const active = model || DEFAULT_MODEL;
|
|
118
|
-
const models = Object.fromEntries(CORE_MODELS.map((id) => [id, {}]));
|
|
119
|
-
models[active] = {};
|
|
120
|
-
return {
|
|
121
|
-
PLEUM_API_KEY: key,
|
|
122
|
-
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
123
|
-
provider: {
|
|
124
|
-
pleum: {
|
|
125
|
-
npm: "@ai-sdk/openai-compatible",
|
|
126
|
-
name: "PleumRouter",
|
|
127
|
-
options: { baseURL: V1, apiKey: "{env:PLEUM_API_KEY}" },
|
|
128
|
-
models,
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
}),
|
|
132
|
-
};
|
|
133
|
-
},
|
|
134
|
-
args: (m) => ["--model", `pleum/${m || DEFAULT_MODEL}`],
|
|
135
|
-
},
|
|
136
179
|
omp: {
|
|
137
180
|
label: "omp (Oh My Pi)",
|
|
138
181
|
bin: "omp",
|
|
@@ -211,23 +254,6 @@ export const RECIPES = {
|
|
|
211
254
|
}),
|
|
212
255
|
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("google/") ? (m || DEFAULT_MODEL) : `google/${m || DEFAULT_MODEL}`],
|
|
213
256
|
},
|
|
214
|
-
agy: {
|
|
215
|
-
label: "Antigravity CLI",
|
|
216
|
-
bin: "agy",
|
|
217
|
-
type: "agy-settings",
|
|
218
|
-
install: "https://antigravity.google/cli/install.sh",
|
|
219
|
-
note: "Writes modelProvider=gemini into ~/.gemini/antigravity-cli/settings.json. GEMINI_API_KEY alone is ignored. Gemini-family models only.",
|
|
220
|
-
attribution: {
|
|
221
|
-
title: "Antigravity CLI",
|
|
222
|
-
referer: "https://www.antigravity.google/docs/cli/install",
|
|
223
|
-
},
|
|
224
|
-
env: (key) => ({
|
|
225
|
-
GOOGLE_GEMINI_BASE_URL: ROOT,
|
|
226
|
-
GEMINI_API_KEY: key,
|
|
227
|
-
GOOGLE_API_KEY: key,
|
|
228
|
-
}),
|
|
229
|
-
args: (m) => ["--model", (m || DEFAULT_MODEL).startsWith("google/") ? (m || DEFAULT_MODEL) : `google/${m || DEFAULT_MODEL}`],
|
|
230
|
-
},
|
|
231
257
|
pi: {
|
|
232
258
|
label: "Pi",
|
|
233
259
|
bin: "pi",
|
|
@@ -376,12 +402,15 @@ export function tomlBasicString(value) {
|
|
|
376
402
|
return `"${escaped}"`;
|
|
377
403
|
}
|
|
378
404
|
|
|
379
|
-
export function codexToml(model) {
|
|
380
|
-
const
|
|
405
|
+
export function codexToml(model, effort) {
|
|
406
|
+
const isObj = model && typeof model === "object";
|
|
407
|
+
const m = (isObj ? model.model : model) || DEFAULT_MODEL;
|
|
408
|
+
const eff = (isObj ? model.effort : effort) || null;
|
|
409
|
+
const effortLine = eff ? `model_reasoning_effort = ${tomlBasicString(eff)}\n` : "";
|
|
381
410
|
return `# Generated by pleum CLI. Isolated CODEX_HOME — your ~/.codex is untouched.
|
|
382
411
|
model = ${tomlBasicString(m)}
|
|
383
412
|
model_provider = "pleum"
|
|
384
|
-
|
|
413
|
+
${effortLine}
|
|
385
414
|
[model_providers.pleum]
|
|
386
415
|
name = "PleumRouter"
|
|
387
416
|
base_url = ${tomlBasicString(V1)}
|
|
@@ -399,7 +428,9 @@ export function mergeAgySettings(existingJson, model) {
|
|
|
399
428
|
throw new Error("agy settings.json must be a JSON object");
|
|
400
429
|
}
|
|
401
430
|
}
|
|
402
|
-
|
|
431
|
+
const isObj = model && typeof model === "object";
|
|
432
|
+
const m = (isObj ? model.model : model) || DEFAULT_MODEL;
|
|
433
|
+
return { ...obj, modelProvider: "gemini", model: m };
|
|
403
434
|
}
|
|
404
435
|
|
|
405
436
|
export function piModelsJson(model) {
|
package/src/settings.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { style, sanitizeTerminalText } from "./ansi.mjs";
|
|
10
10
|
import { select, confirm } from "./interactive.mjs";
|
|
11
|
-
import { readConfig, writeConfig, isLoggedIn, getActiveContext } from "./config.mjs";
|
|
11
|
+
import { readConfig, writeConfig, isLoggedIn, getActiveContext, isAutoUpdateEnabled, setAutoUpdateEnabled } from "./config.mjs";
|
|
12
12
|
import { getLocale, LANGS, t } from "./i18n.mjs";
|
|
13
13
|
import { RECIPES } from "./recipes.mjs";
|
|
14
14
|
import { fetchModels, matchModels, formatModelLabel } from "./models.mjs";
|
|
@@ -18,12 +18,25 @@ import { completion } from "./completion.mjs";
|
|
|
18
18
|
import { lang } from "./lang.mjs";
|
|
19
19
|
import { switchCreditWorkspace } from "./balance.mjs";
|
|
20
20
|
import { runList, saveModel } from "./menu.mjs";
|
|
21
|
+
import { checkLatestVersion, isNewerVersion, runUpdateCommand } from "./update.mjs";
|
|
21
22
|
|
|
22
23
|
// Settings 서브메뉴 항목(순수 함수 — 테스트 대상).
|
|
23
|
-
export function buildSettingsItems({
|
|
24
|
+
export function buildSettingsItems({
|
|
25
|
+
lang,
|
|
26
|
+
loggedIn,
|
|
27
|
+
keyPrefix,
|
|
28
|
+
version,
|
|
29
|
+
activeContext = { type: "personal" },
|
|
30
|
+
autoUpdate = true,
|
|
31
|
+
latestVersion = null,
|
|
32
|
+
}) {
|
|
24
33
|
const contextDesc = activeContext?.type === "team" && activeContext.org_name
|
|
25
34
|
? `${t("Team")}: ${activeContext.org_name}`
|
|
26
35
|
: t("Personal Credit");
|
|
36
|
+
const updateDesc = latestVersion && isNewerVersion(version, latestVersion)
|
|
37
|
+
? `${t("Current version: {version}", { version })} → v${latestVersion} (${t("Update available")})`
|
|
38
|
+
: t("Current version: {version}", { version });
|
|
39
|
+
|
|
27
40
|
return [
|
|
28
41
|
{ id: "settings:lang", title: t("Language"), desc: `${t("current")}: ${LANGS[lang] ?? lang}` },
|
|
29
42
|
{ id: "settings:models", title: t("Default models"), desc: t("Set the default model used per agent") },
|
|
@@ -38,7 +51,12 @@ export function buildSettingsItems({ lang, loggedIn, keyPrefix, version, activeC
|
|
|
38
51
|
: `${t("Logged in")} (${t("key")} ${keyPrefix}…)`
|
|
39
52
|
: t("Not logged in"),
|
|
40
53
|
},
|
|
41
|
-
{
|
|
54
|
+
{
|
|
55
|
+
id: "settings:auto_update",
|
|
56
|
+
title: t("Auto update"),
|
|
57
|
+
desc: autoUpdate ? t("Enabled (updates automatically on start)") : t("Disabled"),
|
|
58
|
+
},
|
|
59
|
+
{ id: "settings:update", title: t("Update CLI"), desc: updateDesc },
|
|
42
60
|
{ id: "settings:completion", title: t("Shell completion"), desc: t("Print a zsh/bash completion script") },
|
|
43
61
|
];
|
|
44
62
|
}
|
|
@@ -73,12 +91,18 @@ function clearSavedModel(tool) {
|
|
|
73
91
|
|
|
74
92
|
async function settingsSelect() {
|
|
75
93
|
const cfg = readConfig();
|
|
94
|
+
let updateInfo = null;
|
|
95
|
+
try {
|
|
96
|
+
updateInfo = await checkLatestVersion();
|
|
97
|
+
} catch {}
|
|
76
98
|
const items = buildSettingsItems({
|
|
77
99
|
lang: getLocale(),
|
|
78
100
|
loggedIn: isLoggedIn(cfg),
|
|
79
101
|
keyPrefix: cfg?.access_token ? "oauth" : cfg?.key ? cfg.key.slice(0, 8) : null,
|
|
80
102
|
version: readVersion(),
|
|
81
103
|
activeContext: getActiveContext(cfg),
|
|
104
|
+
autoUpdate: isAutoUpdateEnabled(cfg),
|
|
105
|
+
latestVersion: updateInfo?.latestVersion,
|
|
82
106
|
});
|
|
83
107
|
return runList(items, {
|
|
84
108
|
header: style.bold(style.plum(t("Settings"))),
|
|
@@ -164,12 +188,14 @@ export async function runSettings() {
|
|
|
164
188
|
case "settings:account":
|
|
165
189
|
await runAccount();
|
|
166
190
|
continue;
|
|
191
|
+
case "settings:auto_update": {
|
|
192
|
+
const next = !isAutoUpdateEnabled();
|
|
193
|
+
setAutoUpdateEnabled(next);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
167
196
|
case "settings:update":
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// mainMenu()의 chat 케이스처럼 즉시 루프를 벗어난다.
|
|
171
|
-
console.log(t("Reinstalling — pleum will exit when finished. Run `pleum` again afterward."));
|
|
172
|
-
upgrade();
|
|
197
|
+
console.log(t("Updating pleumrouter CLI..."));
|
|
198
|
+
await runUpdateCommand([], { exit: false });
|
|
173
199
|
return "quit";
|
|
174
200
|
case "settings:completion":
|
|
175
201
|
await runCompletion();
|
package/src/update.mjs
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { t } from "./i18n.mjs";
|
|
6
|
+
|
|
7
|
+
const REGISTRY_URL = "https://registry.npmjs.org/pleumrouter/latest";
|
|
8
|
+
const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1시간 캐시
|
|
9
|
+
|
|
10
|
+
export function readCliVersion() {
|
|
11
|
+
try {
|
|
12
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
13
|
+
return pkg.version;
|
|
14
|
+
} catch {
|
|
15
|
+
return "0.0.0";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseSemver(v) {
|
|
20
|
+
if (!v) return [0, 0, 0];
|
|
21
|
+
const clean = String(v).trim().replace(/^v/, "");
|
|
22
|
+
const parts = clean.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
|
|
23
|
+
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isNewerVersion(current, latest) {
|
|
27
|
+
const [cMaj, cMin, cPat] = parseSemver(current);
|
|
28
|
+
const [lMaj, lMin, lPat] = parseSemver(latest);
|
|
29
|
+
if (lMaj !== cMaj) return lMaj > cMaj;
|
|
30
|
+
if (lMin !== cMin) return lMin > cMin;
|
|
31
|
+
return lPat > cPat;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getUpdateCachePath() {
|
|
35
|
+
return join(homedir(), ".pleum", "update-cache.json");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readUpdateCache(cachePath = getUpdateCachePath()) {
|
|
39
|
+
try {
|
|
40
|
+
if (!existsSync(cachePath)) return null;
|
|
41
|
+
return JSON.parse(readFileSync(cachePath, "utf8"));
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function writeUpdateCache(latestVersion, cachePath = getUpdateCachePath()) {
|
|
48
|
+
try {
|
|
49
|
+
const dir = join(homedir(), ".pleum");
|
|
50
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
51
|
+
writeFileSync(
|
|
52
|
+
cachePath,
|
|
53
|
+
JSON.stringify({ last_check: Date.now(), latest_version: latestVersion }, null, 2),
|
|
54
|
+
{ mode: 0o600 },
|
|
55
|
+
);
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function fetchLatestVersionFromRegistry({ timeoutMs = 2000, fetchFn = globalThis.fetch } = {}) {
|
|
60
|
+
try {
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
63
|
+
const res = await fetchFn(REGISTRY_URL, {
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
headers: { Accept: "application/json" },
|
|
66
|
+
});
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
if (!res.ok) return null;
|
|
69
|
+
const data = await res.json();
|
|
70
|
+
return data?.version ?? null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function checkLatestVersion({
|
|
77
|
+
force = false,
|
|
78
|
+
timeoutMs = 2000,
|
|
79
|
+
currentVersion = null,
|
|
80
|
+
cachePath = getUpdateCachePath(),
|
|
81
|
+
fetchFn = globalThis.fetch,
|
|
82
|
+
} = {}) {
|
|
83
|
+
const cur = currentVersion || readCliVersion();
|
|
84
|
+
const cached = readUpdateCache(cachePath);
|
|
85
|
+
|
|
86
|
+
if (!force && cached?.latest_version && typeof cached.last_check === "number") {
|
|
87
|
+
if (Date.now() - cached.last_check < CHECK_INTERVAL_MS) {
|
|
88
|
+
return {
|
|
89
|
+
currentVersion: cur,
|
|
90
|
+
latestVersion: cached.latest_version,
|
|
91
|
+
isOutdated: isNewerVersion(cur, cached.latest_version),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const remote = await fetchLatestVersionFromRegistry({ timeoutMs, fetchFn });
|
|
97
|
+
if (remote) {
|
|
98
|
+
writeUpdateCache(remote, cachePath);
|
|
99
|
+
return {
|
|
100
|
+
currentVersion: cur,
|
|
101
|
+
latestVersion: remote,
|
|
102
|
+
isOutdated: isNewerVersion(cur, remote),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 네트워크 실패 시 기존 캐시가 있으면 재활용, 없으면 현재 버전으로 안전 폴백
|
|
107
|
+
const fallbackLatest = cached?.latest_version || cur;
|
|
108
|
+
return {
|
|
109
|
+
currentVersion: cur,
|
|
110
|
+
latestVersion: fallbackLatest,
|
|
111
|
+
isOutdated: isNewerVersion(cur, fallbackLatest),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function performUpdate({ bin = null } = {}) {
|
|
116
|
+
const npmCmd = bin || (process.platform === "win32" ? "npm.cmd" : "npm");
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const child = spawn(npmCmd, ["install", "-g", "pleumrouter@latest"], {
|
|
119
|
+
stdio: "inherit",
|
|
120
|
+
shell: false,
|
|
121
|
+
});
|
|
122
|
+
child.on("error", (e) => {
|
|
123
|
+
console.error(`✗ ${e.message}`);
|
|
124
|
+
resolve(1);
|
|
125
|
+
});
|
|
126
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function runUpdateCommand(args = [], { exit = true } = {}) {
|
|
131
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
132
|
+
const cur = readCliVersion();
|
|
133
|
+
|
|
134
|
+
if (force) {
|
|
135
|
+
console.log(`▶ ${t("Updating pleumrouter (force reinstall)...")}\n`);
|
|
136
|
+
const code = await performUpdate();
|
|
137
|
+
if (exit) process.exit(code);
|
|
138
|
+
return code;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { latestVersion, isOutdated } = await checkLatestVersion({ force: true });
|
|
142
|
+
|
|
143
|
+
if (!isOutdated) {
|
|
144
|
+
console.log(`✓ ${t("Already using the latest version of pleumrouter (v{version}).", { version: cur })}`);
|
|
145
|
+
console.log(t("Use --force (-f) to reinstall anyway."));
|
|
146
|
+
if (exit) process.exit(0);
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(`▶ ${t("Updating pleumrouter from v{from} to v{to}...", { from: cur, to: latestVersion })}\n`);
|
|
151
|
+
const code = await performUpdate();
|
|
152
|
+
if (code === 0) {
|
|
153
|
+
console.log(`\n✓ ${t("Update completed! Please run `pleum` again to use the new version.")}`);
|
|
154
|
+
}
|
|
155
|
+
if (exit) process.exit(code);
|
|
156
|
+
return code;
|
|
157
|
+
}
|
package/src/upgrade.mjs
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { performUpdate, runUpdateCommand } from "./update.mjs";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
shell: false,
|
|
10
|
-
});
|
|
11
|
-
child.on("error", (e) => {
|
|
12
|
-
console.error(`✗ ${e.message}`);
|
|
13
|
-
process.exit(1);
|
|
14
|
-
});
|
|
15
|
-
child.on("exit", (code) => process.exit(code ?? 0));
|
|
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);
|
|
16
9
|
}
|
|
10
|
+
|