huaweicloud-devkit 1.1.5 → 1.1.6-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 +1 -1
- package/README.zh-CN.md +1 -1
- 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/huawei-deployment/SKILL.md +19 -14
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +119 -55
- package/plugins/huaweicloud-core/skills/huawei-sandbox/references/framework-commands.md +5 -5
- package/plugins/huaweicloud-core/src/mcp-protocol.mjs +20 -18
- package/plugins/huaweicloud-core/src/mcp-server.mjs +30 -2
- package/plugins/huaweicloud-core/src/officeace-paths.mjs +51 -0
- package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +43 -1
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +83 -30
- package/plugins/huaweicloud-core/src/setup-cli.mjs +88 -56
- package/plugins/huaweicloud-core/src/tools.mjs +43 -4
- package/plugins/huaweicloud-core/src/update-check.mjs +32 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Shared OfficeAce config-root resolution used by both the installer
|
|
2
|
+
// (setup-cli.mjs) and the MCP runtime (tools.mjs), so skills are installed
|
|
3
|
+
// to and read from the same location (#559).
|
|
4
|
+
//
|
|
5
|
+
// OfficeAce exposes no API for the plugin to query the current user or
|
|
6
|
+
// install directory, so the root is located by probing. The probe result is
|
|
7
|
+
// persisted in a marker file so a non-standard directory (entered via prompt,
|
|
8
|
+
// whose OFFICE_CLAW_CONFIG_ROOT env does not survive across processes) is
|
|
9
|
+
// still found by later uninstall/update runs.
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import { join, resolve, dirname } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
|
|
15
|
+
function markerPath() {
|
|
16
|
+
return join(process.env.HUAWEICLOUD_HOME || homedir(), '.config', 'huaweicloud', 'devkit-officeace-root.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sqliteSibling(dir) {
|
|
20
|
+
return join(resolve(dir, '..'), 'data', 'mcp-connectors.sqlite');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A directory is a usable OfficeAce config root when it exists and carries
|
|
24
|
+
// OfficeAce evidence — capabilities.json, the connector DB, or DevKit's own
|
|
25
|
+
// installed artifacts. Deliberately more lenient than "capabilities.json must
|
|
26
|
+
// exist" so uninstall/update still find the root after it was removed.
|
|
27
|
+
export function isUsableOfficeaceRoot(dir) {
|
|
28
|
+
if (!dir || !existsSync(dir)) return false;
|
|
29
|
+
return (
|
|
30
|
+
existsSync(join(dir, 'capabilities.json')) ||
|
|
31
|
+
existsSync(sqliteSibling(dir)) ||
|
|
32
|
+
existsSync(join(dir, 'skills')) ||
|
|
33
|
+
existsSync(join(dir, 'huaweicloud-plugins'))
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readOfficeaceRootMarker() {
|
|
38
|
+
try {
|
|
39
|
+
const marker = JSON.parse(readFileSync(markerPath(), 'utf8'));
|
|
40
|
+
return isUsableOfficeaceRoot(marker.root) ? marker.root : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function writeOfficeaceRootMarker(root) {
|
|
47
|
+
try {
|
|
48
|
+
mkdirSync(dirname(markerPath()), { recursive: true });
|
|
49
|
+
writeFileSync(markerPath(), JSON.stringify({ root, savedAt: new Date().toISOString() }, null, 2));
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
@@ -108,15 +108,57 @@ function evaluate(stage, inputs, options = {}) {
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function isNonEmptyString(value) {
|
|
112
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isNonEmptyContainer(value) {
|
|
116
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
117
|
+
if (value && typeof value === 'object') return Object.keys(value).length > 0;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// A risk check that could not inspect its input must never report "allow"
|
|
122
|
+
// (fail-open) — it reports deny with an `invalid` risk and a synthetic finding
|
|
123
|
+
// so the reason is visible through the tool response (#564).
|
|
124
|
+
function invalidRiskResult(stage, reason) {
|
|
125
|
+
return {
|
|
126
|
+
decision: 'deny',
|
|
127
|
+
risk: 'invalid',
|
|
128
|
+
findings: [
|
|
129
|
+
{
|
|
130
|
+
ruleId: 'invalid-input',
|
|
131
|
+
title: 'Invalid input for risk check',
|
|
132
|
+
category: 'invalid',
|
|
133
|
+
severity: 'deny',
|
|
134
|
+
message: reason,
|
|
135
|
+
remediation: 'Provide a valid, non-empty input before re-running the check.',
|
|
136
|
+
source: stage,
|
|
137
|
+
evidence: '',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
111
143
|
export function evaluateCommandRisk(command, options = {}) {
|
|
144
|
+
if (!isNonEmptyString(command)) {
|
|
145
|
+
return invalidRiskResult('command', 'command must be a non-empty string.');
|
|
146
|
+
}
|
|
112
147
|
return evaluate('command', { command }, options);
|
|
113
148
|
}
|
|
114
149
|
|
|
115
150
|
export function evaluateArtifacts(artifacts, options = {}) {
|
|
116
|
-
|
|
151
|
+
if (!Array.isArray(artifacts) || artifacts.length === 0) {
|
|
152
|
+
return invalidRiskResult('artifact', 'artifacts must be a non-empty array.');
|
|
153
|
+
}
|
|
154
|
+
return evaluate('artifact', artifacts, options);
|
|
117
155
|
}
|
|
118
156
|
|
|
119
157
|
export function evaluateDeployPlan(plan, options = {}) {
|
|
158
|
+
const valid = isNonEmptyString(plan) || (plan != null && typeof plan === 'object' && isNonEmptyContainer(plan));
|
|
159
|
+
if (!valid) {
|
|
160
|
+
return invalidRiskResult('deploy_plan', 'plan must be a non-empty object, array, or string.');
|
|
161
|
+
}
|
|
120
162
|
return evaluate('deploy_plan', { plan }, options);
|
|
121
163
|
}
|
|
122
164
|
|
|
@@ -22,9 +22,18 @@ import { trackSandboxConnect, trackSandboxDisconnect } from '../telemetry/teleme
|
|
|
22
22
|
|
|
23
23
|
const execFileAsync = promisify(execFile);
|
|
24
24
|
|
|
25
|
+
// Public tunnel URL domain for the DevBridge s2 gateway. The pre-migration Huawei Cloud
|
|
26
|
+
// bridge domain was retired in Sep 2026 and now serves a 「服务已迁移」 placeholder page with
|
|
27
|
+
// HTTP 200 — never construct tunnel URLs from it.
|
|
28
|
+
const DEVBRIDGE_TUNNEL_DOMAIN = 'devbridge-s2.hwtunnel.com';
|
|
29
|
+
// Migration placeholder page marker — a tunnel URL returning this body must be treated as unreachable.
|
|
30
|
+
const DEVBRIDGE_MIGRATION_MARKER = '服务已迁移';
|
|
31
|
+
|
|
25
32
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
26
33
|
export const WS_EXEC_INDEX_URL = pathToFileURL(join(__dirname, '..', 'ws-exec', 'index.js')).href;
|
|
27
34
|
|
|
35
|
+
export const TUNNEL_URL_PATTERN = /TUNNEL_URL:(https:\/\/[A-Za-z0-9_-]+-\d+\.devbridge-s2\.hwtunnel\.com)/;
|
|
36
|
+
|
|
28
37
|
let currentWorkspaceId = process.env.HW_WORKSPACE_ID || null;
|
|
29
38
|
|
|
30
39
|
function getCurrentWorkspaceId() {
|
|
@@ -180,6 +189,24 @@ export function splitBase64Chunks(base64, chunkSize = UPLOAD_CHUNK_SIZE) {
|
|
|
180
189
|
return chunks;
|
|
181
190
|
}
|
|
182
191
|
|
|
192
|
+
export function formatPortConflictWarning(basePort, targetPort) {
|
|
193
|
+
return targetPort !== basePort ? `Port ${basePort} is in use — auto-assigned port ${targetPort}` : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function formatPortDriftWarning(basePort, targetPort) {
|
|
197
|
+
if (targetPort === basePort) return undefined;
|
|
198
|
+
return `Port ${basePort} was occupied — nginx now listens on port ${targetPort}. Any DevBridge tunnel bound to port ${basePort} is detached: run "devbridge port create <tunnelId> -p ${targetPort} --protocol http -a" and restart "devbridge host" for the new port.`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function formatProxyPortWarning(basePort, targetPort) {
|
|
202
|
+
if (targetPort === basePort) return undefined;
|
|
203
|
+
return `Port ${basePort} is in use — the proxy template still listens on port ${basePort}: auto-increment does not apply to proxy configs, so nginx may fail to bind. Free the port or deploy a static/spa build instead.`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function buildExposeRemediation(port) {
|
|
207
|
+
return `In the sandbox: source /tmp/hw_creds.sh; source /tmp/hw_api_key 2>/dev/null; devbridge delete-all; devbridge create <name>; devbridge port create <tunnelId> -p ${port} --protocol http -a; nohup devbridge host <tunnelId> -p ${port} > /tmp/host.log 2>&1 & If deploy_nginx reported a different (auto-incremented) port in its "port" field, use THAT port instead of the one shown here. Full procedure in huawei-sandbox skill, Step 7 (Expose via DevBridge).`;
|
|
208
|
+
}
|
|
209
|
+
|
|
183
210
|
export async function uploadFileWithSession(workspaceId, localPath, remotePath, username = 'root', timeoutMs = 30000) {
|
|
184
211
|
if (!existsSync(localPath)) {
|
|
185
212
|
throw new Error(`sandbox upload: local file not found: ${localPath}`);
|
|
@@ -728,7 +755,6 @@ export async function deployNginx(
|
|
|
728
755
|
const basePort = nginxType === 'proxy' ? listenPort : port;
|
|
729
756
|
|
|
730
757
|
let targetPort = basePort;
|
|
731
|
-
let portWarning;
|
|
732
758
|
const maxPortAttempts = 10;
|
|
733
759
|
for (let offset = 0; offset < maxPortAttempts; offset += 1) {
|
|
734
760
|
targetPort = basePort + offset;
|
|
@@ -740,9 +766,6 @@ export async function deployNginx(
|
|
|
740
766
|
10000,
|
|
741
767
|
);
|
|
742
768
|
if (!String(portCheck.stdout || '').includes('IN_USE')) break;
|
|
743
|
-
if (offset === 0) {
|
|
744
|
-
portWarning = `Port ${basePort} is in use — auto-assigned port ${targetPort}`;
|
|
745
|
-
}
|
|
746
769
|
} catch {}
|
|
747
770
|
if (offset === maxPortAttempts - 1) {
|
|
748
771
|
throw new Error(
|
|
@@ -840,9 +863,11 @@ fi`;
|
|
|
840
863
|
|
|
841
864
|
let tunnelActive = false;
|
|
842
865
|
try {
|
|
866
|
+
// Version-aware: devbridge 0.1.x exposes JSON via `list -j`; 0.2.x removed -j and
|
|
867
|
+
// prints a table whose data rows start with the 8-char base32 tunnel ID.
|
|
843
868
|
const tunnelCheck = await execOneShot(
|
|
844
869
|
workspaceId,
|
|
845
|
-
'devbridge list -j 2>/dev/null | grep -q \'"tunnelId"\' && echo "ACTIVE" || echo "INACTIVE"',
|
|
870
|
+
'(devbridge list -j 2>/dev/null | grep -q \'"tunnelId"\' || devbridge list 2>/dev/null | grep -Eq \'^[a-z2-7]{8}[[:space:]]\') && echo "ACTIVE" || echo "INACTIVE"',
|
|
846
871
|
username,
|
|
847
872
|
10000,
|
|
848
873
|
);
|
|
@@ -860,9 +885,17 @@ fi`;
|
|
|
860
885
|
stdout: result.stdout,
|
|
861
886
|
nextStep: 'expose_via_devbridge',
|
|
862
887
|
warning:
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
888
|
+
[
|
|
889
|
+
!tunnelActive
|
|
890
|
+
? 'No active DevBridge tunnel — deployment is incomplete. Proceed to Step 7 to expose the app.'
|
|
891
|
+
: undefined,
|
|
892
|
+
nginxType === 'proxy'
|
|
893
|
+
? formatProxyPortWarning(basePort, targetPort)
|
|
894
|
+
: formatPortConflictWarning(basePort, targetPort),
|
|
895
|
+
tunnelActive && nginxType !== 'proxy' ? formatPortDriftWarning(basePort, targetPort) : undefined,
|
|
896
|
+
]
|
|
897
|
+
.filter(Boolean)
|
|
898
|
+
.join(' ') || undefined,
|
|
866
899
|
};
|
|
867
900
|
}
|
|
868
901
|
|
|
@@ -918,7 +951,13 @@ export async function deployCheck(
|
|
|
918
951
|
`fi`,
|
|
919
952
|
``,
|
|
920
953
|
`TOTAL=$((TOTAL+1))`,
|
|
954
|
+
`DB_TUNNEL_ACTIVE=0`,
|
|
921
955
|
`if devbridge list -j 2>/dev/null | grep -q '"tunnelId"'; then`,
|
|
956
|
+
` DB_TUNNEL_ACTIVE=1`,
|
|
957
|
+
`elif devbridge list 2>/dev/null | grep -Eq '^[a-z2-7]{8}[[:space:]]'; then`,
|
|
958
|
+
` DB_TUNNEL_ACTIVE=1`,
|
|
959
|
+
`fi`,
|
|
960
|
+
`if [ "$DB_TUNNEL_ACTIVE" = "1" ]; then`,
|
|
922
961
|
` echo "devbridge_tunnel:PASS"`,
|
|
923
962
|
` PASS=$((PASS+1))`,
|
|
924
963
|
`else`,
|
|
@@ -927,18 +966,29 @@ export async function deployCheck(
|
|
|
927
966
|
``,
|
|
928
967
|
`TOTAL=$((TOTAL+1))`,
|
|
929
968
|
`TUNNEL_ID=$(devbridge list -j 2>/dev/null | grep -oP '"tunnelId":\\s*"\\K[^"]+' | head -1)`,
|
|
930
|
-
`
|
|
931
|
-
`
|
|
932
|
-
`
|
|
933
|
-
`
|
|
934
|
-
`
|
|
935
|
-
`
|
|
936
|
-
`
|
|
937
|
-
`
|
|
938
|
-
`
|
|
969
|
+
`if [ -z "$TUNNEL_ID" ]; then`,
|
|
970
|
+
` TUNNEL_ID=$(devbridge list 2>/dev/null | grep -E '^[a-z2-7]{8}[[:space:]]' | awk '{print $1}' | head -1)`,
|
|
971
|
+
`fi`,
|
|
972
|
+
`TUNNEL_URL="https://\${TUNNEL_ID}-${port}.${DEVBRIDGE_TUNNEL_DOMAIN}"`,
|
|
973
|
+
`probe_tunnel() {`,
|
|
974
|
+
` local url="$1" code`,
|
|
975
|
+
` code=$(curl -s -o /tmp/.dc_tunnel_body -w "%{http_code}" --max-time 10 "$url" 2>/dev/null || echo "000")`,
|
|
976
|
+
` [ "$code" = "000" ] && return 1`,
|
|
977
|
+
` # A migrated gateway serves a placeholder page with HTTP 200 — treat it as unreachable.`,
|
|
978
|
+
` if grep -q "${DEVBRIDGE_MIGRATION_MARKER}" /tmp/.dc_tunnel_body 2>/dev/null; then return 1; fi`,
|
|
979
|
+
` rm -f /tmp/.dc_tunnel_body`,
|
|
980
|
+
` [ "$code" = "200" ] || [ "$code" = "304" ] || return 1`,
|
|
981
|
+
` return 0`,
|
|
982
|
+
`}`,
|
|
983
|
+
`if [ -n "$TUNNEL_ID" ] && probe_tunnel "$TUNNEL_URL"; then`,
|
|
984
|
+
` echo "tunnel_url_accessible:PASS ($TUNNEL_URL)"`,
|
|
985
|
+
` PASS=$((PASS+1))`,
|
|
986
|
+
`elif [ -n "$TUNNEL_ID" ]; then`,
|
|
987
|
+
` echo "tunnel_url_accessible:FAIL ($TUNNEL_URL -> unreachable, tunnel not found, or migration placeholder page)"`,
|
|
939
988
|
`else`,
|
|
940
|
-
` echo "tunnel_url_accessible:FAIL (no tunnel
|
|
989
|
+
` echo "tunnel_url_accessible:FAIL (no tunnel)"`,
|
|
941
990
|
`fi`,
|
|
991
|
+
`rm -f /tmp/.dc_tunnel_body 2>/dev/null || true`,
|
|
942
992
|
``,
|
|
943
993
|
`${`
|
|
944
994
|
TOTAL=$((TOTAL+1))
|
|
@@ -999,7 +1049,7 @@ fi
|
|
|
999
1049
|
if (m) checks[m[1]] = { status: m[2], detail: (m[3] || '').trim() };
|
|
1000
1050
|
}
|
|
1001
1051
|
const scoreMatch = cleanStdout.match(/SCORE:(\d+)\/(\d+)/);
|
|
1002
|
-
const tunnelMatch = cleanStdout.match(
|
|
1052
|
+
const tunnelMatch = cleanStdout.match(TUNNEL_URL_PATTERN);
|
|
1003
1053
|
const complete = /VERDICT:COMPLETE/.test(cleanStdout);
|
|
1004
1054
|
|
|
1005
1055
|
const missing = [];
|
|
@@ -1015,6 +1065,18 @@ fi
|
|
|
1015
1065
|
? 'Check output parsing failed — individual check results could not be extracted. See rawOutput for details.'
|
|
1016
1066
|
: undefined;
|
|
1017
1067
|
|
|
1068
|
+
const nextStepValue = !complete
|
|
1069
|
+
? missing.includes('devbridge_tunnel') || missing.includes('tunnel_url_accessible')
|
|
1070
|
+
? 'expose_via_devbridge'
|
|
1071
|
+
: missing.includes('nginx_serving')
|
|
1072
|
+
? 'configure_nginx'
|
|
1073
|
+
: missing.includes('qr_code')
|
|
1074
|
+
? 'generate_qr_code'
|
|
1075
|
+
: parseWarning
|
|
1076
|
+
? 'review_raw_output'
|
|
1077
|
+
: 'review_checks'
|
|
1078
|
+
: 'complete';
|
|
1079
|
+
|
|
1018
1080
|
return {
|
|
1019
1081
|
ok: true,
|
|
1020
1082
|
complete,
|
|
@@ -1025,17 +1087,8 @@ fi
|
|
|
1025
1087
|
missingSteps: missing.length > 0 ? missing.join(', ') : undefined,
|
|
1026
1088
|
parseWarning,
|
|
1027
1089
|
rawOutput: parseWarning ? stdout.trim() : undefined,
|
|
1028
|
-
nextStep:
|
|
1029
|
-
|
|
1030
|
-
? 'expose_via_devbridge'
|
|
1031
|
-
: missing.includes('nginx_serving')
|
|
1032
|
-
? 'configure_nginx'
|
|
1033
|
-
: missing.includes('qr_code')
|
|
1034
|
-
? 'generate_qr_code'
|
|
1035
|
-
: parseWarning
|
|
1036
|
-
? 'review_raw_output'
|
|
1037
|
-
: 'review_checks'
|
|
1038
|
-
: 'complete',
|
|
1090
|
+
nextStep: nextStepValue,
|
|
1091
|
+
remediation: nextStepValue === 'expose_via_devbridge' ? buildExposeRemediation(port) : undefined,
|
|
1039
1092
|
};
|
|
1040
1093
|
}
|
|
1041
1094
|
|
|
@@ -37,7 +37,8 @@ import {
|
|
|
37
37
|
} from './proxy/proxy-config.mjs';
|
|
38
38
|
import { removeKooCli, removeObsConfig } from './sandbox/uninstall-cleanup.mjs';
|
|
39
39
|
import { mergeCommandStyle, mergeArgsStyle, extractUserDelta, applyUserDelta } from './mcp-config-merge.mjs';
|
|
40
|
-
import { saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
|
|
40
|
+
import { readAgentDelta, saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
|
|
41
|
+
import { isUsableOfficeaceRoot, readOfficeaceRootMarker, writeOfficeaceRootMarker } from './officeace-paths.mjs';
|
|
41
42
|
import { queryDistTagsFetch, determineTarget, semverCompare } from './update-check.mjs';
|
|
42
43
|
import { getKooCliVersion, compareVersion, kooCliDownloadBase, KOO_CLI_BASE } from './koocli-version.mjs';
|
|
43
44
|
import { findHcloudBin, hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
|
|
@@ -185,11 +186,17 @@ function readOfficeaceRegistryInstallDir() {
|
|
|
185
186
|
|
|
186
187
|
function officeaceCapabilitiesDir() {
|
|
187
188
|
const configRoot = process.env.OFFICE_CLAW_CONFIG_ROOT;
|
|
188
|
-
if (
|
|
189
|
+
if (isUsableOfficeaceRoot(configRoot)) return configRoot;
|
|
190
|
+
const markerRoot = readOfficeaceRootMarker();
|
|
191
|
+
if (markerRoot) return markerRoot;
|
|
192
|
+
// Skip system probes (registry / Program Files) so automated environments can
|
|
193
|
+
// keep OfficeAce detection hermetic — those probes can leak a real OfficeAce
|
|
194
|
+
// install into a sandboxed/fake HOME (#654).
|
|
195
|
+
if (process.env.HUAWEICLOUD_DEVKIT_SKIP_OFFICEACE_DETECT === '1') return null;
|
|
189
196
|
const regDir = readOfficeaceRegistryInstallDir();
|
|
190
197
|
if (regDir) {
|
|
191
198
|
const dir = join(regDir, '.office-claw');
|
|
192
|
-
if (
|
|
199
|
+
if (isUsableOfficeaceRoot(dir)) return dir;
|
|
193
200
|
}
|
|
194
201
|
if (platform() === 'win32') {
|
|
195
202
|
const bases = [process.env.ProgramFiles, 'C:\\Program Files', 'D:\\Program Files'];
|
|
@@ -197,7 +204,7 @@ function officeaceCapabilitiesDir() {
|
|
|
197
204
|
for (const base of bases) {
|
|
198
205
|
if (!base) continue;
|
|
199
206
|
const dir = join(base, 'OfficeAce', '.office-claw');
|
|
200
|
-
if (
|
|
207
|
+
if (isUsableOfficeaceRoot(dir)) return dir;
|
|
201
208
|
}
|
|
202
209
|
}
|
|
203
210
|
return null;
|
|
@@ -242,7 +249,7 @@ function ensureOfficeaceMcpInSqlite() {
|
|
|
242
249
|
if (!existsSync(dbPath)) {
|
|
243
250
|
console.log(` \x1b[31mOfficeAce database not found: ${dbPath}\x1b[0m`);
|
|
244
251
|
console.log(` \x1b[33mPlease ensure OfficeAce is installed and has been launched at least once.\x1b[0m`);
|
|
245
|
-
return
|
|
252
|
+
return 'db-missing';
|
|
246
253
|
}
|
|
247
254
|
|
|
248
255
|
const mcpPath = join(officeacePluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
@@ -281,21 +288,27 @@ function ensureOfficeaceMcpInSqlite() {
|
|
|
281
288
|
if (existing.command === 'node' && existing.args_json === nextArgsJson && existing.env_json === nextEnvJson) {
|
|
282
289
|
console.log(` MCP config unchanged: ${dbPath}`);
|
|
283
290
|
db.close();
|
|
284
|
-
return
|
|
291
|
+
return 'ok';
|
|
285
292
|
}
|
|
286
293
|
db.prepare(
|
|
287
294
|
'UPDATE mcp_connectors SET command = ?, args_json = ?, env_json = ?, updated_at = ?, status = ?, enabled = 1 WHERE id = ?',
|
|
288
295
|
).run('node', nextArgsJson, nextEnvJson, now, 'disconnected', existing.id);
|
|
289
296
|
console.log(` MCP config merged (user fields preserved): ${dbPath}`);
|
|
290
297
|
} else {
|
|
291
|
-
|
|
298
|
+
// Restore user identity + args saved by a previous uninstall (#559).
|
|
299
|
+
// owner_user_id has no queryable source: we copy it from an existing
|
|
300
|
+
// row, and fall back to the value backed up before a prior uninstall so
|
|
301
|
+
// an emptied table does not make registration permanently impossible.
|
|
302
|
+
const officeaceDelta = readAgentDelta('officeace');
|
|
303
|
+
const ownerUserId = officeaceGetOwnerUserId() || (officeaceDelta && officeaceDelta.ownerUserId);
|
|
292
304
|
if (!ownerUserId) {
|
|
293
|
-
console.log(` \x1b[31mCannot determine owner_user_id
|
|
305
|
+
console.log(` \x1b[31mCannot determine owner_user_id for the huaweicloud-devkit connector.\x1b[0m`);
|
|
306
|
+
console.log(
|
|
307
|
+
` \x1b[33mThe OfficeAce connector database has no rows to copy the user id from and no prior registration is backed up. Open OfficeAce, add/enable any connector once, then re-run the install.\x1b[0m`,
|
|
308
|
+
);
|
|
294
309
|
db.close();
|
|
295
|
-
return
|
|
310
|
+
return 'owner-missing';
|
|
296
311
|
}
|
|
297
|
-
// Restore user args saved by a previous uninstall (issue #615).
|
|
298
|
-
const officeaceDelta = takeAgentDelta('officeace');
|
|
299
312
|
const insertArgsJson =
|
|
300
313
|
officeaceDelta && Array.isArray(officeaceDelta.argsExtra)
|
|
301
314
|
? JSON.stringify([mcpPath, ...officeaceDelta.argsExtra])
|
|
@@ -304,10 +317,12 @@ function ensureOfficeaceMcpInSqlite() {
|
|
|
304
317
|
`INSERT INTO mcp_connectors (id, owner_user_id, type, name, normalized_name, transport, timeout_ms, command, args_json, env_json, enabled, status, created_at, updated_at, version, seeded)
|
|
305
318
|
VALUES (?, ?, 'custom', 'huaweicloud-devkit', 'huaweicloud-devkit', 'stdio', 60000, 'node', ?, ?, 1, 'disconnected', ?, ?, 1, 0)`,
|
|
306
319
|
).run(randomUUID(), ownerUserId, insertArgsJson, envJson, now, now);
|
|
320
|
+
// Consume the backup only after a successful insert so a DB failure keeps it for the next retry.
|
|
321
|
+
takeAgentDelta('officeace');
|
|
307
322
|
console.log(` MCP config created: ${dbPath}`);
|
|
308
323
|
}
|
|
309
324
|
db.close();
|
|
310
|
-
return
|
|
325
|
+
return 'ok';
|
|
311
326
|
} catch (error) {
|
|
312
327
|
if (db) {
|
|
313
328
|
try {
|
|
@@ -315,7 +330,7 @@ function ensureOfficeaceMcpInSqlite() {
|
|
|
315
330
|
} catch {}
|
|
316
331
|
}
|
|
317
332
|
console.log(` \x1b[31mFailed to write MCP config: ${error.message}\x1b[0m`);
|
|
318
|
-
return
|
|
333
|
+
return 'error';
|
|
319
334
|
}
|
|
320
335
|
}
|
|
321
336
|
|
|
@@ -325,17 +340,28 @@ function removeOfficeaceMcpFromSqlite() {
|
|
|
325
340
|
let db;
|
|
326
341
|
try {
|
|
327
342
|
db = openOfficeaceDb();
|
|
328
|
-
// Back up user args before deleting the connector row
|
|
343
|
+
// Back up user identity + args before deleting the connector row:
|
|
344
|
+
// owner_user_id is copied from existing rows on reinstall, so an empty
|
|
345
|
+
// table after this delete would make registration impossible (#559).
|
|
329
346
|
try {
|
|
330
|
-
const row = db
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
347
|
+
const row = db
|
|
348
|
+
.prepare("SELECT owner_user_id, args_json FROM mcp_connectors WHERE name = 'huaweicloud-devkit'")
|
|
349
|
+
.get();
|
|
350
|
+
const delta = {};
|
|
351
|
+
if (row?.owner_user_id) delta.ownerUserId = row.owner_user_id;
|
|
352
|
+
try {
|
|
353
|
+
const parsed = row ? JSON.parse(row.args_json || '[]') : [];
|
|
354
|
+
if (Array.isArray(parsed) && parsed.length > 1) delta.argsExtra = parsed.slice(1);
|
|
355
|
+
} catch {}
|
|
356
|
+
if (Object.keys(delta).length > 0) saveAgentDelta('officeace', delta);
|
|
357
|
+
} catch {}
|
|
358
|
+
// mcp_connector_tools may not exist on minimal installs — deleting it must
|
|
359
|
+
// not abort deleting the connector row itself.
|
|
360
|
+
try {
|
|
361
|
+
db.prepare(
|
|
362
|
+
"DELETE FROM mcp_connector_tools WHERE connector_id IN (SELECT id FROM mcp_connectors WHERE name = 'huaweicloud-devkit')",
|
|
363
|
+
).run();
|
|
335
364
|
} catch {}
|
|
336
|
-
db.prepare(
|
|
337
|
-
"DELETE FROM mcp_connector_tools WHERE connector_id IN (SELECT id FROM mcp_connectors WHERE name = 'huaweicloud-devkit')",
|
|
338
|
-
).run();
|
|
339
365
|
const result2 = db.prepare("DELETE FROM mcp_connectors WHERE name = 'huaweicloud-devkit'").run();
|
|
340
366
|
if (result2.changes > 0) {
|
|
341
367
|
console.log(` MCP config removed: ${dbPath}`);
|
|
@@ -565,7 +591,10 @@ function installRuntimeDeps(pluginsDir) {
|
|
|
565
591
|
function removeIfExists(p) {
|
|
566
592
|
if (existsSync(p)) {
|
|
567
593
|
try {
|
|
568
|
-
|
|
594
|
+
// Retry transient EBUSY/EPERM/ENOTEMPTY (locked files on Windows) before
|
|
595
|
+
// giving up — uninstall should not leave a skill behind just because a
|
|
596
|
+
// handle was momentarily open (#556).
|
|
597
|
+
rmSync(p, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
569
598
|
return true;
|
|
570
599
|
} catch (error) {
|
|
571
600
|
console.log(` \x1b[33m[WARN]\x1b[0m Could not remove ${p}: ${error.message}`);
|
|
@@ -824,8 +853,7 @@ function uninstallOpenCode() {
|
|
|
824
853
|
if (existsSync(skills)) {
|
|
825
854
|
for (const entry of readdirSync(skills, { withFileTypes: true })) {
|
|
826
855
|
if (entry.name.startsWith('huawei')) {
|
|
827
|
-
removeIfExists(join(skills, entry.name))
|
|
828
|
-
removed++;
|
|
856
|
+
if (removeIfExists(join(skills, entry.name))) removed++;
|
|
829
857
|
}
|
|
830
858
|
}
|
|
831
859
|
console.log(` Removed ${removed} skills`);
|
|
@@ -836,8 +864,7 @@ function uninstallOpenCode() {
|
|
|
836
864
|
if (existsSync(commands)) {
|
|
837
865
|
for (const entry of readdirSync(commands, { withFileTypes: true })) {
|
|
838
866
|
if (entry.name.startsWith('huawei')) {
|
|
839
|
-
removeIfExists(join(commands, entry.name))
|
|
840
|
-
cmdRemoved++;
|
|
867
|
+
if (removeIfExists(join(commands, entry.name))) cmdRemoved++;
|
|
841
868
|
}
|
|
842
869
|
}
|
|
843
870
|
if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
|
|
@@ -863,8 +890,7 @@ function pruneStale(targetDir, sourceDir) {
|
|
|
863
890
|
for (const entry of readdirSync(targetDir, { withFileTypes: true })) {
|
|
864
891
|
if (!entry.name.startsWith('huawei')) continue;
|
|
865
892
|
if (!sourceNames.has(entry.name)) {
|
|
866
|
-
removeIfExists(join(targetDir, entry.name))
|
|
867
|
-
removed++;
|
|
893
|
+
if (removeIfExists(join(targetDir, entry.name))) removed++;
|
|
868
894
|
}
|
|
869
895
|
}
|
|
870
896
|
return removed;
|
|
@@ -1011,8 +1037,7 @@ function uninstallOpenClaw() {
|
|
|
1011
1037
|
if (existsSync(skillsDir)) {
|
|
1012
1038
|
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
1013
1039
|
if (entry.name.startsWith('huawei')) {
|
|
1014
|
-
removeIfExists(join(skillsDir, entry.name))
|
|
1015
|
-
removed++;
|
|
1040
|
+
if (removeIfExists(join(skillsDir, entry.name))) removed++;
|
|
1016
1041
|
}
|
|
1017
1042
|
}
|
|
1018
1043
|
console.log(` Removed ${removed} skills`);
|
|
@@ -1023,8 +1048,7 @@ function uninstallOpenClaw() {
|
|
|
1023
1048
|
if (existsSync(cmdDir)) {
|
|
1024
1049
|
for (const entry of readdirSync(cmdDir, { withFileTypes: true })) {
|
|
1025
1050
|
if (entry.name.startsWith('huawei')) {
|
|
1026
|
-
removeIfExists(join(cmdDir, entry.name))
|
|
1027
|
-
cmdRemoved++;
|
|
1051
|
+
if (removeIfExists(join(cmdDir, entry.name))) cmdRemoved++;
|
|
1028
1052
|
}
|
|
1029
1053
|
}
|
|
1030
1054
|
if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
|
|
@@ -1170,8 +1194,7 @@ function uninstallCodexDesktop() {
|
|
|
1170
1194
|
if (existsSync(skillsDir)) {
|
|
1171
1195
|
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
1172
1196
|
if (entry.name.startsWith('huawei')) {
|
|
1173
|
-
removeIfExists(join(skillsDir, entry.name))
|
|
1174
|
-
removed++;
|
|
1197
|
+
if (removeIfExists(join(skillsDir, entry.name))) removed++;
|
|
1175
1198
|
}
|
|
1176
1199
|
}
|
|
1177
1200
|
console.log(` Removed ${removed} skills`);
|
|
@@ -1182,8 +1205,7 @@ function uninstallCodexDesktop() {
|
|
|
1182
1205
|
if (existsSync(cmdDir)) {
|
|
1183
1206
|
for (const entry of readdirSync(cmdDir, { withFileTypes: true })) {
|
|
1184
1207
|
if (entry.name.startsWith('huawei')) {
|
|
1185
|
-
removeIfExists(join(cmdDir, entry.name))
|
|
1186
|
-
cmdRemoved++;
|
|
1208
|
+
if (removeIfExists(join(cmdDir, entry.name))) cmdRemoved++;
|
|
1187
1209
|
}
|
|
1188
1210
|
}
|
|
1189
1211
|
if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
|
|
@@ -1327,8 +1349,7 @@ function uninstallCodeArts() {
|
|
|
1327
1349
|
if (!existsSync(skillsDir)) continue;
|
|
1328
1350
|
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
1329
1351
|
if (entry.name.startsWith('huawei')) {
|
|
1330
|
-
removeIfExists(join(skillsDir, entry.name))
|
|
1331
|
-
removed++;
|
|
1352
|
+
if (removeIfExists(join(skillsDir, entry.name))) removed++;
|
|
1332
1353
|
}
|
|
1333
1354
|
}
|
|
1334
1355
|
}
|
|
@@ -1488,8 +1509,7 @@ function uninstallCodeArtsWork() {
|
|
|
1488
1509
|
let removed = 0;
|
|
1489
1510
|
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
1490
1511
|
if (entry.name.startsWith('huawei')) {
|
|
1491
|
-
removeIfExists(join(skillsDir, entry.name))
|
|
1492
|
-
removed++;
|
|
1512
|
+
if (removeIfExists(join(skillsDir, entry.name))) removed++;
|
|
1493
1513
|
}
|
|
1494
1514
|
}
|
|
1495
1515
|
if (removed > 0) console.log(` Removed ${removed} skills`);
|
|
@@ -1776,8 +1796,7 @@ function uninstallWorkBuddy() {
|
|
|
1776
1796
|
if (existsSync(skillsDir)) {
|
|
1777
1797
|
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
1778
1798
|
if (entry.name.startsWith('huawei')) {
|
|
1779
|
-
removeIfExists(join(skillsDir, entry.name))
|
|
1780
|
-
removed++;
|
|
1799
|
+
if (removeIfExists(join(skillsDir, entry.name))) removed++;
|
|
1781
1800
|
}
|
|
1782
1801
|
}
|
|
1783
1802
|
if (removed > 0) console.log(` Removed ${removed} skills`);
|
|
@@ -1813,9 +1832,10 @@ function uninstallWorkBuddy() {
|
|
|
1813
1832
|
if (settings.hooks?.PostToolUse) {
|
|
1814
1833
|
const before = settings.hooks.PostToolUse.length;
|
|
1815
1834
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((e) => e?.matcher !== '*');
|
|
1816
|
-
|
|
1835
|
+
const after = settings.hooks.PostToolUse.length;
|
|
1836
|
+
if (after === 0) delete settings.hooks.PostToolUse;
|
|
1817
1837
|
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
1818
|
-
if (before !==
|
|
1838
|
+
if (before !== after) {
|
|
1819
1839
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
1820
1840
|
console.log(` PostToolUse hook removed from ${settingsPath}`);
|
|
1821
1841
|
}
|
|
@@ -1966,8 +1986,7 @@ function uninstallAtomCode() {
|
|
|
1966
1986
|
if (existsSync(skillsDir)) {
|
|
1967
1987
|
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
1968
1988
|
if (entry.name.startsWith('huawei')) {
|
|
1969
|
-
removeIfExists(join(skillsDir, entry.name))
|
|
1970
|
-
removed++;
|
|
1989
|
+
if (removeIfExists(join(skillsDir, entry.name))) removed++;
|
|
1971
1990
|
}
|
|
1972
1991
|
}
|
|
1973
1992
|
if (removed > 0) console.log(` Removed ${removed} skills`);
|
|
@@ -2281,8 +2300,7 @@ function uninstallDsh() {
|
|
|
2281
2300
|
if (existsSync(skillsDir)) {
|
|
2282
2301
|
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
2283
2302
|
if (entry.name.startsWith('huawei')) {
|
|
2284
|
-
removeIfExists(join(skillsDir, entry.name))
|
|
2285
|
-
removed++;
|
|
2303
|
+
if (removeIfExists(join(skillsDir, entry.name))) removed++;
|
|
2286
2304
|
}
|
|
2287
2305
|
}
|
|
2288
2306
|
if (removed > 0) console.log(` Removed ${removed} skills`);
|
|
@@ -2377,8 +2395,17 @@ async function installOfficeAce() {
|
|
|
2377
2395
|
console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
|
|
2378
2396
|
|
|
2379
2397
|
installRuntimeDeps(pluginDest);
|
|
2380
|
-
ensureOfficeaceMcpInSqlite()
|
|
2398
|
+
if (ensureOfficeaceMcpInSqlite() === 'owner-missing') {
|
|
2399
|
+
throw new Error(
|
|
2400
|
+
'OfficeAce MCP connector registration failed (owner_user_id unavailable). ' +
|
|
2401
|
+
'Open OfficeAce, add/enable any connector once, then re-run install.',
|
|
2402
|
+
);
|
|
2403
|
+
}
|
|
2381
2404
|
registerOfficeaceSkillEntries();
|
|
2405
|
+
// Persist the resolved root so a later uninstall/update (new process, no
|
|
2406
|
+
// OFFICE_CLAW_CONFIG_ROOT env) still targets the same directory (#559).
|
|
2407
|
+
const resolvedRoot = officeaceCapabilitiesDir();
|
|
2408
|
+
if (resolvedRoot) writeOfficeaceRootMarker(resolvedRoot);
|
|
2382
2409
|
}
|
|
2383
2410
|
|
|
2384
2411
|
async function updateOfficeAce() {
|
|
@@ -2395,8 +2422,15 @@ async function updateOfficeAce() {
|
|
|
2395
2422
|
copyDir(safetyDir, join(pluginDest, 'safety'));
|
|
2396
2423
|
console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
|
|
2397
2424
|
installRuntimeDeps(pluginDest);
|
|
2398
|
-
ensureOfficeaceMcpInSqlite()
|
|
2425
|
+
if (ensureOfficeaceMcpInSqlite() === 'owner-missing') {
|
|
2426
|
+
console.log(
|
|
2427
|
+
` \x1b[31m[WARN]\x1b[0m OfficeAce MCP connector registration failed (owner_user_id unavailable). ` +
|
|
2428
|
+
`The connector may stay disconnected until you add/enable any connector once in OfficeAce and re-run update.`,
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2399
2431
|
registerOfficeaceSkillEntries();
|
|
2432
|
+
const resolvedRoot = officeaceCapabilitiesDir();
|
|
2433
|
+
if (resolvedRoot) writeOfficeaceRootMarker(resolvedRoot);
|
|
2400
2434
|
mkdirSync(pluginDest, { recursive: true });
|
|
2401
2435
|
writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
|
|
2402
2436
|
}
|
|
@@ -2407,8 +2441,7 @@ function uninstallOfficeAce() {
|
|
|
2407
2441
|
if (existsSync(skillsDir)) {
|
|
2408
2442
|
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
2409
2443
|
if (entry.name.startsWith('huawei')) {
|
|
2410
|
-
removeIfExists(join(skillsDir, entry.name))
|
|
2411
|
-
removed++;
|
|
2444
|
+
if (removeIfExists(join(skillsDir, entry.name))) removed++;
|
|
2412
2445
|
}
|
|
2413
2446
|
}
|
|
2414
2447
|
if (removed > 0) console.log(` Removed ${removed} skills`);
|
|
@@ -2991,8 +3024,7 @@ function uninstallHermes() {
|
|
|
2991
3024
|
if (existsSync(skillsDir)) {
|
|
2992
3025
|
for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
|
|
2993
3026
|
if (entry.name.startsWith('huawei')) {
|
|
2994
|
-
removeIfExists(join(skillsDir, entry.name))
|
|
2995
|
-
removed++;
|
|
3027
|
+
if (removeIfExists(join(skillsDir, entry.name))) removed++;
|
|
2996
3028
|
}
|
|
2997
3029
|
}
|
|
2998
3030
|
if (removed > 0) console.log(` Removed ${removed} skills`);
|
|
@@ -4133,7 +4165,7 @@ async function cmdDoctor() {
|
|
|
4133
4165
|
console.log('\x1b[33mFix failures above, then restart your session.\x1b[0m');
|
|
4134
4166
|
}
|
|
4135
4167
|
if (fail === 0 && mcpConfigured) {
|
|
4136
|
-
console.log('\n\x1b[32mAll checks passed.\x1b[0m
|
|
4168
|
+
console.log('\n\x1b[32mAll checks passed.\x1b[0m You can now describe your Huawei Cloud task');
|
|
4137
4169
|
}
|
|
4138
4170
|
|
|
4139
4171
|
// Detect "installed but not restarted" — check all supported agents
|