huaweicloud-devkit 1.1.1 → 1.1.2-next.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.
package/README.md CHANGED
@@ -249,10 +249,13 @@ npx --yes huaweicloud-devkit install --target all
249
249
  ### Update All Agents
250
250
 
251
251
  ```bash
252
- npx --yes huaweicloud-devkit update --target all
252
+ npx huaweicloud-devkit version
253
+ npx --yes huaweicloud-devkit@latest update --target all
253
254
  ```
254
255
 
255
- `update` is incremental — it refreshes installed files without touching your config.
256
+ `update` is incremental — it refreshes installed files without touching your
257
+ config. Always keep `@latest` so npm fetches the newest version instead of a
258
+ locally cached older one.
256
259
 
257
260
  ## What It Does
258
261
 
package/README.zh-CN.md CHANGED
@@ -249,10 +249,11 @@ npx --yes huaweicloud-devkit install --target all
249
249
  ### 更新所有 Agent
250
250
 
251
251
  ```bash
252
- npx --yes huaweicloud-devkit update --target all
252
+ npx huaweicloud-devkit version
253
+ npx --yes huaweicloud-devkit@latest update --target all
253
254
  ```
254
255
 
255
- `update` 是增量更新——只刷新已安装的文件,不动配置文件。
256
+ `update` 是增量更新——只刷新已安装的文件,不动配置文件。请务必保留 `@latest`,确保 npm 获取最新版本而非本地缓存的旧版本。
256
257
 
257
258
  ## 功能特性
258
259
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.1",
3
+ "version": "1.1.2-next.1",
4
4
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
5
5
  "type": "module",
6
6
  "files": [
@@ -17,7 +17,7 @@
17
17
  "mcpServers": "./.mcp.json",
18
18
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
19
19
  "skills": "./skills/",
20
- "version": "1.1.1",
20
+ "version": "1.1.2-next.1",
21
21
  "author": {
22
22
  "name": "HuaweiCloud Mate",
23
23
  "url": "https://github.com/huaweicloud"
@@ -20,7 +20,7 @@
20
20
  "mcpServers": "./.mcp.json",
21
21
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
22
22
  "skills": "./skills/",
23
- "version": "1.1.1",
23
+ "version": "1.1.2-next.1",
24
24
  "author": {
25
25
  "name": "HuaweiCloud Mate",
26
26
  "url": "https://github.com/huaweicloud"
@@ -17,7 +17,7 @@
17
17
  "mcpServers": "./.mcp.json",
18
18
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
19
19
  "skills": "./skills/",
20
- "version": "1.1.1",
20
+ "version": "1.1.2-next.1",
21
21
  "author": {
22
22
  "name": "HuaweiCloud Mate",
23
23
  "url": "https://github.com/huaweicloud"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.1",
3
+ "version": "1.1.2-next.1",
4
4
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
5
5
  "author": {
6
6
  "name": "HuaweiCloud Mate",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.1",
3
+ "version": "1.1.2-next.1",
4
4
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
5
5
  "author": {
6
6
  "name": "HuaweiCloud Mate",
@@ -2,7 +2,7 @@
2
2
  "name": "huaweicloud-devkit",
3
3
  "id": "huaweicloud-devkit",
4
4
  "displayName": "HuaweiCloud DevKit",
5
- "version": "1.1.1",
5
+ "version": "1.1.2-next.1",
6
6
  "family": "bundle-plugin",
7
7
  "bundleFormat": "codex",
8
8
  "description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
@@ -10,6 +10,14 @@ version: 1
10
10
 
11
11
  Huawei Cloud comprises 200+ services. This skill is the entry point for ANY action or question related to Huawei Cloud. It matches a workload to the right service, then hands off to a service-specific skill for operational questions.
12
12
 
13
+ ## 会话启动
14
+
15
+ 首次操作前必须先调用 `huaweicloud_check_update` 检查插件版本:
16
+
17
+ 1. 如果 `updateAvailable == true` 且 `dismissed == false`,先询问用户是否升级
18
+ 2. 用户同意 → 调用 `huaweicloud_upgrade`,完成后提示重启会话
19
+ 3. 用户拒绝 → 调用 `huaweicloud_check_update` 传 `dismiss: true`
20
+
13
21
  ## How This Skill Works
14
22
 
15
23
  1. Match user intent against the Sub-skill registry below
@@ -6,6 +6,7 @@ import { platform } from 'node:os';
6
6
  import { fileURLToPath } from 'node:url';
7
7
 
8
8
  import { TOOL_DEFINITIONS, callTool } from './tools.mjs';
9
+ import { getCachedUpdateInfo, readInstalledVersion, peekCachedUpdateInfo, applyUpdateHint } from './update-check.mjs';
9
10
  import { initTelemetry } from './telemetry/telemetry.mjs';
10
11
  import { detectAgent } from './telemetry/agent-detect.mjs';
11
12
 
@@ -73,6 +74,27 @@ let useContentLengthFraming = true;
73
74
  // shutdown signal — exit cleanly so the host does not see CLOSE_TIMEOUT.
74
75
  const { harness } = detectAgent();
75
76
  const NEEDS_KEEPALIVE = harness === 'hermes' && platform() === 'win32';
77
+
78
+ // 版本升级检测预热:异步、非阻塞;失败静默(离线/超时不影响会话)。
79
+ process.nextTick(() => {
80
+ getCachedUpdateInfo(readInstalledVersion() || '0.0.0').catch(() => {});
81
+ });
82
+
83
+ // 会话内首个非 check/upgrade 工具调用附加 _updateInfo,只消费一次。
84
+ let hintConsumed = false;
85
+ function decorateResult(name, result) {
86
+ if (hintConsumed) return result;
87
+ try {
88
+ const hint = peekCachedUpdateInfo();
89
+ if (!hint) return result;
90
+ const decorated = applyUpdateHint(result, name, hint);
91
+ if (decorated !== result) hintConsumed = true;
92
+ return decorated;
93
+ } catch {
94
+ return result; // 兜底装饰失败绝不影响工具调用
95
+ }
96
+ }
97
+
76
98
  let keepAlive = null;
77
99
  function onStdinClose() {
78
100
  if (keepAlive) return;
@@ -191,11 +213,12 @@ async function dispatch(method, params) {
191
213
 
192
214
  if (method === 'tools/call') {
193
215
  const result = await callTool(params.name, params.arguments || {});
216
+ const decorated = decorateResult(params.name, result);
194
217
  return {
195
218
  content: [
196
219
  {
197
220
  type: 'text',
198
- text: JSON.stringify(result, null, 2),
221
+ text: JSON.stringify(decorated, null, 2),
199
222
  },
200
223
  ],
201
224
  isError: false,
@@ -35,6 +35,7 @@ import {
35
35
  getProxySettings,
36
36
  } from './proxy/proxy-config.mjs';
37
37
  import { removeKooCli, removeObsConfig } from './sandbox/uninstall-cleanup.mjs';
38
+ import { queryDistTagsSync, determineTarget, semverCompare } from './update-check.mjs';
38
39
 
39
40
  const { DatabaseSync } = createRequire(import.meta.url)('node:sqlite');
40
41
 
@@ -3056,20 +3057,12 @@ function parseTarget() {
3056
3057
 
3057
3058
  function checkForUpdate() {
3058
3059
  if (pkgVersion === '0.0.0') return;
3059
- const tag = /-next\.\d+/.test(pkgVersion) ? 'next' : 'latest';
3060
- let latest = null;
3061
- try {
3062
- const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
3063
- const r = spawnSync(npmBin, ['view', `huaweicloud-devkit@${tag}`, 'version'], {
3064
- encoding: 'utf8',
3065
- timeout: 5000,
3066
- windowsHide: true,
3067
- });
3068
- if (r.status === 0) latest = (r.stdout || '').trim().split('\n').pop().trim();
3069
- } catch {}
3070
- if (latest && latest !== pkgVersion) {
3060
+ const distTags = queryDistTagsSync({ timeoutMs: 5000 });
3061
+ const target = determineTarget(pkgVersion, distTags ?? {});
3062
+ if (target && semverCompare(target, pkgVersion) > 0) {
3063
+ const tag = distTags.next && semverCompare(target, distTags.next) === 0 ? 'next' : 'latest';
3071
3064
  console.log(
3072
- `\n\x1b[33mℹ️ 检测到新版本:${latest}(当前 ${pkgVersion},${tag} 频道)。建议运行 \`npx huaweicloud-devkit@${tag} update\` 更新。\x1b[0m`,
3065
+ `\n\x1b[33mℹ️ 检测到新版本:${target}(当前 ${pkgVersion},${tag} 频道)。建议运行 \`npx --yes huaweicloud-devkit@${tag} update\` 更新。\x1b[0m`,
3073
3066
  );
3074
3067
  }
3075
3068
  }
@@ -46,6 +46,17 @@ import {
46
46
  } from './auth/credentials.mjs';
47
47
  import { trackToolInvoke, trackSkillRetrieve } from './telemetry/telemetry.mjs';
48
48
  import { fingerprint, runHcloudConfigure, resolveManagedProfile } from './auth/reconcile.mjs';
49
+ import {
50
+ getCachedUpdateInfo,
51
+ getUpdateDistTags,
52
+ invalidateUpdateCache,
53
+ judgeUpdate,
54
+ determineTarget,
55
+ readInstalledVersion,
56
+ writeSkipState,
57
+ resolveSkipFilePath,
58
+ upgradePackage,
59
+ } from './update-check.mjs';
49
60
 
50
61
  const __dirname = dirname(fileURLToPath(import.meta.url));
51
62
  const SKILLS_ROOT_DEV = join(__dirname, '..', 'skills');
@@ -795,6 +806,36 @@ export const TOOL_DEFINITIONS = [
795
806
  },
796
807
  },
797
808
  },
809
+ {
810
+ name: 'huaweicloud_check_update',
811
+ description:
812
+ '检查 huaweicloud-devkit 插件是否有新版本。结果含 currentVersion / latestStable / latestNext / targetVersion / updateAvailable / dismissExpiresAt / result。支持 dismiss:true 记录用户拒绝(3 天冷却,新版本会重新提醒)。',
813
+ inputSchema: {
814
+ type: 'object',
815
+ properties: {
816
+ dismiss: { type: 'boolean', description: '用户拒绝升级时传 true,记录冷却状态。' },
817
+ dismissVersion: {
818
+ type: 'string',
819
+ description: '与 dismiss:true 搭配,用户拒绝的版本号。缺省时用当前检测到的 targetVersion。',
820
+ },
821
+ },
822
+ },
823
+ },
824
+ {
825
+ name: 'huaweicloud_upgrade',
826
+ description:
827
+ '升级 huaweicloud-devkit 到最新版本。执行前必须先征得用户同意。version 仅支持 "latest"。完成后需重启会话生效。',
828
+ inputSchema: {
829
+ type: 'object',
830
+ properties: {
831
+ version: { type: 'string', description: '仅支持 "latest"(默认)。' },
832
+ target: {
833
+ type: 'string',
834
+ description: 'agent 目标(opencode/codex/codearts/.../all)。缺省时用 all(仅更新已安装的)。',
835
+ },
836
+ },
837
+ },
838
+ },
798
839
  ];
799
840
 
800
841
  function toolInvokeValue(name, args) {
@@ -1284,11 +1325,46 @@ export async function callTool(name, args = {}) {
1284
1325
  return await hdkitVoucherStatus(args.domain_id);
1285
1326
  case 'huaweicloud_voucher_claim':
1286
1327
  return await hdkitVoucherClaim(args.domain_id);
1328
+ case 'huaweicloud_check_update':
1329
+ return await handleCheckUpdate(args);
1330
+ case 'huaweicloud_upgrade':
1331
+ return await handleUpgrade(args);
1287
1332
  default:
1288
1333
  throw new Error(`Unknown tool: ${name}`);
1289
1334
  }
1290
1335
  }
1291
1336
 
1337
+ async function handleCheckUpdate(args = {}) {
1338
+ const current = readInstalledVersion() || '0.0.0';
1339
+ if (args.dismiss === true) {
1340
+ const distTags = await getUpdateDistTags(current);
1341
+ const target = determineTarget(current, distTags);
1342
+ const dismissedVersion =
1343
+ typeof args.dismissVersion === 'string' && args.dismissVersion ? args.dismissVersion : target || current;
1344
+ const state = writeSkipState(resolveSkipFilePath(), dismissedVersion);
1345
+ invalidateUpdateCache();
1346
+ return judgeUpdate(current, distTags, state);
1347
+ }
1348
+ return getCachedUpdateInfo(current);
1349
+ }
1350
+
1351
+ async function handleUpgrade(args = {}) {
1352
+ const target = typeof args.target === 'string' && args.target ? args.target : 'all';
1353
+ const version = typeof args.version === 'string' && args.version ? args.version : 'latest';
1354
+ const current = readInstalledVersion() || '0.0.0';
1355
+ const info = await getCachedUpdateInfo(current);
1356
+ if (info && info.result === 'up_to_date') {
1357
+ return {
1358
+ success: false,
1359
+ requiresRestart: false,
1360
+ message: '已是最新版本,无需升级。',
1361
+ currentVersion: info.currentVersion,
1362
+ targetVersion: info.targetVersion,
1363
+ };
1364
+ }
1365
+ return upgradePackage({ target, version });
1366
+ }
1367
+
1292
1368
  function hookResult(result) {
1293
1369
  return {
1294
1370
  ok: result.decision !== 'deny',
@@ -0,0 +1,383 @@
1
+ // Version-update detection & auto-upgrade for huaweicloud-devkit (session-level).
2
+ // Spec: docs/superpowers/specs/2026-09-07-version-upgrade-design.md (internal, not committed).
3
+ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, rmSync } from 'node:fs';
4
+ import { spawn, spawnSync } from 'node:child_process';
5
+ import { join, dirname, resolve } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const IS_WINDOWS = process.platform === 'win32';
10
+ const NPM_BIN = IS_WINDOWS ? 'npm.cmd' : 'npm';
11
+ const NPX_BIN = IS_WINDOWS ? 'npx.cmd' : 'npx';
12
+ const TTL_MS = 60 * 60 * 1000;
13
+ const FAIL_THROTTLE_MS = 5 * 60 * 1000;
14
+ const COOLDOWN_DAYS = 3;
15
+
16
+ export function semverParse(input) {
17
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(String(input).trim());
18
+ if (!m) return null;
19
+ const pre = m[4] ? m[4].split('.') : null;
20
+ return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre, raw: String(input).trim() };
21
+ }
22
+
23
+ function comparePre(a, b) {
24
+ if (a === null && b === null) return 0;
25
+ if (a === null) return 1; // 无 prerelease(正式版)更大
26
+ if (b === null) return -1;
27
+ const length = Math.max(a.length, b.length);
28
+ for (let i = 0; i < length; i++) {
29
+ const x = a[i];
30
+ const y = b[i];
31
+ if (x === undefined) return -1;
32
+ if (y === undefined) return 1;
33
+ const xNum = /^\d+$/.test(x);
34
+ const yNum = /^\d+$/.test(y);
35
+ if (xNum && yNum) {
36
+ const dx = Number(x);
37
+ const dy = Number(y);
38
+ if (dx !== dy) return dx < dy ? -1 : 1;
39
+ } else if (xNum) {
40
+ return -1; // 数字标识符 < 字母标识符
41
+ } else if (yNum) {
42
+ return 1;
43
+ } else if (x !== y) {
44
+ return x < y ? -1 : 1;
45
+ }
46
+ }
47
+ return 0;
48
+ }
49
+
50
+ export function semverCompare(a, b) {
51
+ const A = semverParse(a);
52
+ const B = semverParse(b);
53
+ if (!A || !B) {
54
+ const sa = String(a);
55
+ const sb = String(b);
56
+ return sa === sb ? 0 : sa < sb ? -1 : 1;
57
+ }
58
+ if (A.major !== B.major) return A.major < B.major ? -1 : 1;
59
+ if (A.minor !== B.minor) return A.minor < B.minor ? -1 : 1;
60
+ if (A.patch !== B.patch) return A.patch < B.patch ? -1 : 1;
61
+ return comparePre(A.pre, B.pre);
62
+ }
63
+
64
+ export function hasPrerelease(v) {
65
+ const parsed = semverParse(v);
66
+ return Boolean(parsed && parsed.pre);
67
+ }
68
+
69
+ export function determineTarget(current, distTags = {}) {
70
+ const isPre = hasPrerelease(current);
71
+ const candidates = [];
72
+ if (distTags.latest) candidates.push(distTags.latest);
73
+ if (isPre && distTags.next) candidates.push(distTags.next);
74
+ if (!candidates.length) return null;
75
+ return candidates.slice().sort(semverCompare).pop();
76
+ }
77
+
78
+ export function parseDistTagsOutput(stdout) {
79
+ try {
80
+ const text = String(stdout || '').trim();
81
+ if (!text) return null; // 空输出视为 npm view 失败
82
+ const parsed = JSON.parse(text);
83
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
84
+ return {
85
+ latest: typeof parsed.latest === 'string' ? parsed.latest : null,
86
+ next: typeof parsed.next === 'string' ? parsed.next : null,
87
+ };
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ function buildResult(current, distTags, extra = {}) {
94
+ const result = {
95
+ currentVersion: current,
96
+ latestStable: distTags?.latest ?? null,
97
+ latestNext: distTags?.next ?? null,
98
+ targetVersion: extra.target ?? null,
99
+ updateAvailable: extra.result === 'update_available',
100
+ dismissed: extra.result === 'dismissed',
101
+ dismissExpiresAt: extra.dismissExpiresAt ?? null,
102
+ result: extra.result ?? 'up_to_date',
103
+ };
104
+ if (extra.note) result.note = extra.note;
105
+ return result;
106
+ }
107
+
108
+ export function judgeUpdate(current, distTags, skipState, now = Date.now()) {
109
+ if (process.env.HUAWEICLOUD_DEVKIT_SKIP_UPDATE === '1') {
110
+ return buildResult(current, distTags ?? null);
111
+ }
112
+ if (!distTags) {
113
+ return buildResult(current, null, { result: 'check_failed', note: '检测失败,不影响使用' });
114
+ }
115
+ const target = determineTarget(current, distTags);
116
+ if (!target || semverCompare(target, current) <= 0) {
117
+ return buildResult(current, distTags, { result: 'up_to_date', target: target ?? null });
118
+ }
119
+ const expiresAt = skipState?.expireAt ? new Date(skipState.expireAt).getTime() : 0;
120
+ const inCooldown =
121
+ Boolean(skipState) && now < expiresAt && semverCompare(target, String(skipState.dismissedVersion)) <= 0;
122
+ if (inCooldown) {
123
+ return buildResult(current, distTags, { result: 'dismissed', target, dismissExpiresAt: skipState.expireAt });
124
+ }
125
+ return buildResult(current, distTags, { result: 'update_available', target });
126
+ }
127
+
128
+ function selfDir() {
129
+ return dirname(fileURLToPath(import.meta.url));
130
+ }
131
+
132
+ export function readInstalledVersion() {
133
+ const pluginRoot = resolve(selfDir(), '..');
134
+ const packageRoot = resolve(pluginRoot, '..', '..');
135
+ for (const base of [pluginRoot, packageRoot]) {
136
+ try {
137
+ const version = JSON.parse(readFileSync(join(base, 'package.json'), 'utf8')).version;
138
+ if (typeof version === 'string' && version) return version;
139
+ } catch {}
140
+ }
141
+ return null;
142
+ }
143
+
144
+ export function skipFilePath() {
145
+ return join(resolve(selfDir(), '..'), '.update-skip.json');
146
+ }
147
+
148
+ export function fallbackSkipFilePath() {
149
+ const base = process.env.HUAWEICLOUD_HOME || homedir();
150
+ return join(base, '.config', 'huaweicloud', 'devkit-skip.json');
151
+ }
152
+
153
+ // A1 定稿: 标准 agent 用插件目录副本(有 package.json); codex 等无副本时回退共享文件
154
+ export function resolveSkipFilePath() {
155
+ try {
156
+ if (existsSync(join(dirname(skipFilePath()), 'package.json'))) return skipFilePath();
157
+ } catch {}
158
+ return fallbackSkipFilePath();
159
+ }
160
+
161
+ export function readSkipState(file) {
162
+ if (!existsSync(file)) return null;
163
+ try {
164
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
165
+ if (!parsed || typeof parsed.dismissedVersion !== 'string' || !parsed.dismissedAt || !parsed.expireAt) return null;
166
+ return parsed;
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+
172
+ export function writeSkipState(file, dismissedVersion, { at = Date.now(), days = COOLDOWN_DAYS } = {}) {
173
+ const state = {
174
+ dismissedVersion: String(dismissedVersion),
175
+ dismissedAt: new Date(at).toISOString(),
176
+ expireAt: new Date(at + days * 24 * 60 * 60 * 1000).toISOString(),
177
+ };
178
+ mkdirSync(dirname(file), { recursive: true });
179
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
180
+ writeFileSync(tmp, JSON.stringify(state, null, 2), 'utf8');
181
+ try {
182
+ renameSync(tmp, file);
183
+ } catch (error) {
184
+ rmSync(tmp, { force: true });
185
+ throw error;
186
+ }
187
+ return state;
188
+ }
189
+
190
+ export function queryDistTagsSync({ timeoutMs = 5000, cwd } = {}) {
191
+ try {
192
+ const result = spawnSync(NPM_BIN, ['view', 'huaweicloud-devkit', 'dist-tags', '--json'], {
193
+ encoding: 'utf8',
194
+ timeout: timeoutMs,
195
+ windowsHide: true,
196
+ cwd,
197
+ });
198
+ if (result.status !== 0) return null;
199
+ return parseDistTagsOutput(result.stdout);
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ export function queryDistTags({ timeoutMs = 5000, cwd } = {}) {
206
+ return new Promise((resolve) => {
207
+ let child;
208
+ try {
209
+ child = spawn(NPM_BIN, ['view', 'huaweicloud-devkit', 'dist-tags', '--json'], {
210
+ windowsHide: true,
211
+ cwd,
212
+ });
213
+ } catch {
214
+ resolve(null);
215
+ return;
216
+ }
217
+ const timer = setTimeout(() => {
218
+ try {
219
+ child.kill();
220
+ } catch {}
221
+ resolve(null);
222
+ }, timeoutMs);
223
+ let stdout = '';
224
+ child.stdout.on('data', (d) => {
225
+ stdout += String(d);
226
+ });
227
+ child.on('error', () => {
228
+ clearTimeout(timer);
229
+ resolve(null);
230
+ });
231
+ child.on('close', (code) => {
232
+ clearTimeout(timer);
233
+ if (code === 0) resolve(parseDistTagsOutput(stdout));
234
+ else resolve(null);
235
+ });
236
+ });
237
+ }
238
+
239
+ let cachedDistTags = null;
240
+ let cachedAt = 0;
241
+ let failedAt = 0;
242
+ let inflightQuery = null;
243
+ let lastHint = null;
244
+
245
+ export function invalidateUpdateCache() {
246
+ cachedDistTags = null;
247
+ cachedAt = 0;
248
+ failedAt = 0;
249
+ inflightQuery = null;
250
+ lastHint = null;
251
+ }
252
+
253
+ function cacheValid() {
254
+ return Boolean(cachedDistTags) && Date.now() - cachedAt <= TTL_MS;
255
+ }
256
+
257
+ export async function getCachedUpdateInfo(current, { doQuery = queryDistTags } = {}) {
258
+ if (process.env.HUAWEICLOUD_DEVKIT_SKIP_UPDATE === '1') {
259
+ lastHint = judgeUpdate(current, null);
260
+ return lastHint;
261
+ }
262
+ const skipState = readSkipState(resolveSkipFilePath());
263
+ if (!cacheValid()) {
264
+ if (!cachedDistTags && Date.now() - failedAt < FAIL_THROTTLE_MS) {
265
+ lastHint = judgeUpdate(current, null, skipState);
266
+ return lastHint;
267
+ }
268
+ if (!inflightQuery) {
269
+ inflightQuery = doQuery()
270
+ .then((distTags) => {
271
+ if (distTags) {
272
+ cachedDistTags = distTags;
273
+ cachedAt = Date.now();
274
+ } else {
275
+ failedAt = Date.now();
276
+ }
277
+ return distTags;
278
+ })
279
+ .finally(() => {
280
+ inflightQuery = null;
281
+ });
282
+ }
283
+ const distTags = await inflightQuery;
284
+ lastHint = judgeUpdate(current, distTags, skipState);
285
+ return lastHint;
286
+ }
287
+ lastHint = judgeUpdate(current, cachedDistTags, skipState);
288
+ return lastHint;
289
+ }
290
+
291
+ export function peekCachedUpdateInfo() {
292
+ return lastHint && lastHint.updateAvailable && lastHint.targetVersion ? lastHint : null;
293
+ }
294
+
295
+ export async function getUpdateDistTags(current) {
296
+ const result = await getCachedUpdateInfo(current);
297
+ if (!result) return null;
298
+ return { latest: result.latestStable ?? null, next: result.latestNext ?? null };
299
+ }
300
+
301
+ export function applyUpdateHint(result, name, hint) {
302
+ if (!hint || !hint.updateAvailable || !hint.targetVersion) return result;
303
+ if (name === 'huaweicloud_check_update' || name === 'huaweicloud_upgrade') return result;
304
+ return {
305
+ ...result,
306
+ _updateInfo: { currentVersion: hint.currentVersion, latestVersion: hint.targetVersion },
307
+ };
308
+ }
309
+
310
+ function defaultSpawn(command, args, options) {
311
+ return spawnSync(command, args, options);
312
+ }
313
+
314
+ function restartMessage(target) {
315
+ if (target === 'officeace') {
316
+ return '升级完成,请打开连接器 → 我的连接器 → huaweicloud-devkit → 重新连接后使用新版本。';
317
+ }
318
+ return '升级完成,请重启当前会话使新版本生效。';
319
+ }
320
+
321
+ export async function upgradePackage({ target = 'all', version = 'latest' } = {}, options = {}) {
322
+ if (version !== 'latest') {
323
+ return { success: false, error: 'version 参数仅支持 latest。目标版本由插件自动判定。' };
324
+ }
325
+ const { doQuery = queryDistTags, spawnFn = defaultSpawn } = options;
326
+ const previousVersion = readInstalledVersion();
327
+ let distTags;
328
+ try {
329
+ distTags = await doQuery();
330
+ } catch (error) {
331
+ return {
332
+ success: false,
333
+ error: error?.message || '无法确认最新版本(registry 查询失败)。',
334
+ manual: `npx --yes huaweicloud-devkit@latest update --target ${target}`,
335
+ };
336
+ }
337
+ const targetVersion = determineTarget(previousVersion, distTags ?? {});
338
+ if (distTags === null) {
339
+ return {
340
+ success: false,
341
+ error: '无法确认最新版本(registry 查询失败)。',
342
+ manual: `npx --yes huaweicloud-devkit@latest update --target ${target}`,
343
+ };
344
+ }
345
+ if (!targetVersion) {
346
+ return { success: false, message: '已是最新版本,无需升级。', requiresRestart: false };
347
+ }
348
+ const isNextTarget = Boolean(distTags.next && semverCompare(targetVersion, distTags.next) === 0);
349
+ const tag = isNextTarget ? 'next' : 'latest';
350
+ const command = ['--yes', `huaweicloud-devkit@${tag}`, 'update', '--target', String(target)];
351
+ let execResult;
352
+ try {
353
+ execResult = spawnFn(NPX_BIN, command, { encoding: 'utf8', timeout: 300000, windowsHide: true });
354
+ } catch (error) {
355
+ return {
356
+ success: false,
357
+ error: error?.message || '升级命令执行失败。',
358
+ manual: `npx --yes huaweicloud-devkit@${tag} update --target ${target}`,
359
+ };
360
+ }
361
+ if (execResult.status === 0) {
362
+ invalidateUpdateCache();
363
+ return {
364
+ success: true,
365
+ previousVersion,
366
+ installedVersion: targetVersion,
367
+ requiresRestart: true,
368
+ message: restartMessage(target),
369
+ };
370
+ }
371
+ const errObj = execResult.error;
372
+ const stderr = String(execResult.stderr || '').trim();
373
+ const reason = errObj?.message
374
+ ? errObj.message
375
+ : stderr
376
+ ? stderr.split(/\r?\n/).filter(Boolean).slice(-2).join(' ')
377
+ : `exit ${execResult.status}`;
378
+ return {
379
+ success: false,
380
+ error: reason,
381
+ manual: `npx --yes huaweicloud-devkit@${tag} update --target ${target}`,
382
+ };
383
+ }