@cc-x/cc-x 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/menus.js CHANGED
@@ -10,6 +10,7 @@ import { launchSession } from '../actions.js';
10
10
  import { isOfficial, reconcileCurrent, saveStore } from '../config/store.js';
11
11
  import { setDefault } from '../env/default.js';
12
12
  import { getLang, providerDisplayName, setLang, T } from '../i18n/index.js';
13
+ import { banner as updateBanner, maybeRefresh, MODE_NOTIFY, upgradeCommand } from '../update/update.js';
13
14
  import { padDisplay } from '../utils/display.js';
14
15
  import { editForm } from './edit.js';
15
16
  import { noteSuffix, stateLabel } from './format.js';
@@ -23,14 +24,27 @@ async function readLine(prompt) {
23
24
  /** 一级 · 主菜单。布局:[profiles…] '' 新增 语言 '' 退出。 */
24
25
  export async function openMenu(paths, store, scope, version, catalog) {
25
26
  let sel = 0;
27
+ let refreshed = false;
26
28
  for (;;) {
27
29
  const n = store.providers.length;
30
+ // 更新检查(仅 notify 模式):首轮触发一次后台刷新;横幅永远读缓存(瞬时、不阻塞)。
31
+ let notice;
32
+ if (store.update === MODE_NOTIFY) {
33
+ if (!refreshed) {
34
+ maybeRefresh(paths.dir);
35
+ refreshed = true;
36
+ }
37
+ const latest = updateBanner(paths.dir, version);
38
+ if (latest)
39
+ notice = T('menu.updateAvailable', latest, upgradeCommand());
40
+ }
41
+ const updLabel = store.update === MODE_NOTIFY ? T('menu.updateNotify') : T('menu.updateOff');
28
42
  const buildItems = () => {
29
43
  const labels = store.providers.map((p) => {
30
44
  const dft = p.name === store.current ? T('menu.default') : '';
31
45
  return `${padDisplay(providerDisplayName(p), 16)}${padDisplay(dft, 8)}[${stateLabel(p)}]${noteSuffix(p)}`;
32
46
  });
33
- return [...labels, '', T('menu.newProfile'), T('menu.language'), '', T('menu.exit')];
47
+ return [...labels, '', T('menu.newProfile'), T('menu.language'), updLabel, '', T('menu.exit')];
34
48
  };
35
49
  const onMove = (from, to) => {
36
50
  const ps = store.providers;
@@ -45,6 +59,7 @@ export async function openMenu(paths, store, scope, version, catalog) {
45
59
  };
46
60
  sel = await selectMenu({
47
61
  title: T('menu.mainTitle', version),
62
+ ...(notice ? { notice } : {}),
48
63
  items: buildItems(),
49
64
  colors: { [n + 1]: 'yellow' },
50
65
  start: sel,
@@ -53,7 +68,7 @@ export async function openMenu(paths, store, scope, version, catalog) {
53
68
  hint: T('menu.mainHint'),
54
69
  noNumber: true,
55
70
  });
56
- if (sel < 0 || sel === n + 4)
71
+ if (sel < 0 || sel === n + 5)
57
72
  return; // 退出 / Esc / q
58
73
  if (sel === n + 1) {
59
74
  // 新增配置
@@ -71,6 +86,14 @@ export async function openMenu(paths, store, scope, version, catalog) {
71
86
  store.lang = next;
72
87
  saveStore(paths, store);
73
88
  }
89
+ else if (sel === n + 3) {
90
+ // 更新检查开关:关闭 <-> 提醒(关闭=删字段,与 Go 的 omitempty 对齐)
91
+ if (store.update === MODE_NOTIFY)
92
+ delete store.update;
93
+ else
94
+ store.update = MODE_NOTIFY;
95
+ saveStore(paths, store);
96
+ }
74
97
  else if (sel < n) {
75
98
  const target = store.providers[sel];
76
99
  if (target)
package/dist/ui/select.js CHANGED
@@ -41,6 +41,9 @@ export async function selectMenu(opts) {
41
41
  if (opts.title) {
42
42
  lines.push(` ${paint(opts.title, 'cyan')}`, '');
43
43
  }
44
+ if (opts.notice) {
45
+ lines.push(` ${paint(opts.notice, 'yellow')}`, '');
46
+ }
44
47
  if (opts.status) {
45
48
  lines.push(` ${paint(opts.status, 'green')}`, '');
46
49
  }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * 「检查新版本」的轻量实现,对齐 Go 版 internal/update。
3
+ *
4
+ * 不走 GitHub API(仅用 releases/latest 的 302 重定向抠版本号,无速率限制),结果缓存在
5
+ * ~/.cc-mini/update-check.json,每 24h 才真去网络。显示永远读缓存(瞬时、不阻塞),过期时后台
6
+ * 异步刷新——新版本「下次打开」才提示。离线/失败一律静默。只写工具自己的 ~/.cc-mini/(铁律)。
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ export const MODE_OFF = '';
11
+ export const MODE_NOTIFY = 'notify';
12
+ const LATEST_URL = 'https://github.com/becomeless/cc-x/releases/latest';
13
+ const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
14
+ const HTTP_TIMEOUT_MS = 2000;
15
+ const CACHE_FILE = 'update-check.json';
16
+ const UPGRADE_CMD = 'npm i -g @cc-x/cc-x@latest';
17
+ const TAG_RE = /\/tag\/v?(\d+\.\d+\.\d+)/;
18
+ /** 读缓存:若已知有比 current 更新的版本,返回最新版本号;否则 undefined。不联网。 */
19
+ export function banner(storeDir, current) {
20
+ const c = readCache(storeDir);
21
+ if (!c || !c.latest)
22
+ return undefined;
23
+ return isNewer(c.latest, current) ? c.latest : undefined;
24
+ }
25
+ /** 缓存过期(或不存在)时后台异步联网刷新一次;不阻塞调用方。 */
26
+ export function maybeRefresh(storeDir) {
27
+ const c = readCache(storeDir);
28
+ if (c && Date.now() - c.checkedAt * 1000 < CACHE_MAX_AGE_MS)
29
+ return; // 仍新鲜
30
+ void refresh(storeDir); // fire-and-forget
31
+ }
32
+ async function refresh(storeDir) {
33
+ const latest = await fetchLatest();
34
+ if (!latest)
35
+ return; // 失败静默;不动缓存
36
+ writeCache(storeDir, { checkedAt: Math.floor(Date.now() / 1000), latest });
37
+ }
38
+ /** 当前版本(npm 版)的升级命令。 */
39
+ export function upgradeCommand() {
40
+ return UPGRADE_CMD;
41
+ }
42
+ function cachePath(storeDir) {
43
+ return join(storeDir, CACHE_FILE);
44
+ }
45
+ function readCache(storeDir) {
46
+ try {
47
+ const c = JSON.parse(readFileSync(cachePath(storeDir), 'utf-8'));
48
+ if (typeof c.checkedAt === 'number' && typeof c.latest === 'string')
49
+ return c;
50
+ }
51
+ catch {
52
+ /* 无缓存 / 损坏 -> 当作没有 */
53
+ }
54
+ return undefined;
55
+ }
56
+ /** 原子写(temp + rename),避免被进程退出打断写出半截文件。 */
57
+ function writeCache(storeDir, c) {
58
+ try {
59
+ if (!existsSync(storeDir))
60
+ mkdirSync(storeDir, { recursive: true });
61
+ const tmp = `${cachePath(storeDir)}.tmp`;
62
+ writeFileSync(tmp, JSON.stringify(c), 'utf-8');
63
+ renameSync(tmp, cachePath(storeDir));
64
+ }
65
+ catch {
66
+ /* 写不了就算了 */
67
+ }
68
+ }
69
+ /** 用 releases/latest 的 302 重定向抠最新版本号;redirect:'manual' = 不跟随 = 不走 GitHub API。 */
70
+ async function fetchLatest() {
71
+ try {
72
+ const resp = await fetch(LATEST_URL, {
73
+ redirect: 'manual',
74
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
75
+ });
76
+ const loc = resp.headers.get('location');
77
+ if (!loc)
78
+ return undefined;
79
+ const m = TAG_RE.exec(loc);
80
+ return m ? m[1] : undefined;
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ }
86
+ /** latest 是否严格新于 current("a.b.c",忽略前导 v 与后缀)。解析失败一律 false(不误报)。 */
87
+ export function isNewer(latest, current) {
88
+ const lp = parseSemver(latest);
89
+ const cp = parseSemver(current);
90
+ if (!lp || !cp)
91
+ return false;
92
+ const [la, lb, lc] = lp;
93
+ const [ca, cb, cc] = cp;
94
+ if (la !== ca)
95
+ return la > ca;
96
+ if (lb !== cb)
97
+ return lb > cb;
98
+ return lc > cc;
99
+ }
100
+ function parseSemver(s) {
101
+ let v = s.trim().replace(/^v/, '');
102
+ const cut = v.search(/[-+]/);
103
+ if (cut >= 0)
104
+ v = v.slice(0, cut);
105
+ const parts = v.split('.');
106
+ if (parts.length !== 3)
107
+ return undefined;
108
+ const nums = parts.map((p) => Number.parseInt(p, 10));
109
+ if (nums.some((n) => Number.isNaN(n)))
110
+ return undefined;
111
+ return [nums[0], nums[1], nums[2]];
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cc-x/cc-x",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Claude Code API 切换器(命令 xx):在官方账号与第三方 Anthropic 兼容 API 间切换,纯环境变量、不碰 Claude Code 配置文件。",
5
5
  "keywords": [
6
6
  "claude",