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 +5 -2
- package/README.zh-CN.md +3 -2
- package/package.json +1 -1
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +8 -0
- package/plugins/huaweicloud-core/src/mcp-server.mjs +24 -1
- package/plugins/huaweicloud-core/src/setup-cli.mjs +6 -13
- package/plugins/huaweicloud-core/src/tools.mjs +76 -0
- package/plugins/huaweicloud-core/src/update-check.mjs +383 -0
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
const
|
|
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ℹ️ 检测到新版本:${
|
|
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
|
+
}
|