aws-runtime-bridge 1.1.2 → 1.1.6
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/index.js +5 -0
- package/dist/services/auto-register.d.ts.map +1 -1
- package/dist/services/auto-register.js +40 -9
- package/dist/services/auto-register.test.d.ts +2 -0
- package/dist/services/auto-register.test.d.ts.map +1 -0
- package/dist/services/auto-register.test.js +154 -0
- package/dist/services/aws-client-agent-mcp.d.ts +6 -1
- package/dist/services/aws-client-agent-mcp.d.ts.map +1 -1
- package/dist/services/aws-client-agent-mcp.js +96 -20
- package/dist/services/aws-client-agent-mcp.test.js +74 -4
- package/dist/services/cli-commands.d.ts +20 -0
- package/dist/services/cli-commands.d.ts.map +1 -0
- package/dist/services/cli-commands.js +55 -0
- package/dist/services/cli-commands.test.d.ts +2 -0
- package/dist/services/cli-commands.test.d.ts.map +1 -0
- package/dist/services/cli-commands.test.js +92 -0
- package/dist/services/startup-config-wizard.d.ts +2 -1
- package/dist/services/startup-config-wizard.d.ts.map +1 -1
- package/dist/services/startup-config-wizard.js +69 -14
- package/dist/services/startup-config-wizard.test.js +83 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,6 +32,11 @@ import { logger } from "./utils/logger.js";
|
|
|
32
32
|
import { autoRegister, unregister, bridgeRestartCleanup, } from "./services/auto-register.js";
|
|
33
33
|
import { ensureAwsClientAgentMcpReleased } from "./services/aws-client-agent-mcp.js";
|
|
34
34
|
import { ensureStartupConfig } from "./services/startup-config-wizard.js";
|
|
35
|
+
import { handleCliCommand } from "./services/cli-commands.js";
|
|
36
|
+
const cliCommandResult = await handleCliCommand();
|
|
37
|
+
if (cliCommandResult.handled) {
|
|
38
|
+
process.exit(cliCommandResult.exitCode);
|
|
39
|
+
}
|
|
35
40
|
// 验证生产环境配置
|
|
36
41
|
await ensureStartupConfig();
|
|
37
42
|
validateProductionToken();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-register.d.ts","sourceRoot":"","sources":["../../src/services/auto-register.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"auto-register.d.ts","sourceRoot":"","sources":["../../src/services/auto-register.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA+CH;;GAEG;AACH,UAAU,kBAAkB;IAC1B,eAAe;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,oBAAoB;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW;IACX,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAwBD,wBAAgB,2BAA2B,IAAI,MAAM,EAAE,CAEtD;AA6DD;;GAEG;AACH,wBAAgB,6BAA6B,IAAI,MAAM,CAGtD;AAyJD;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,kBAAkB,CA2C/C;AAED,wBAAgB,WAAW,IAAI,kBAAkB,EAAE,CA0ClD;AA4OD;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,YAAY,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,GACzC,OAAO,CAAC,OAAO,CAAC,CAalB;AAyHD;;GAEG;AACH,wBAAsB,gCAAgC,IAAI,OAAO,CAAC;IAChE,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAqED;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAuBnD;AAED;;GAEG;AACH,wBAAgB,oBAAoB;gBAlzBtB,OAAO;iBACN,MAAM;mBACJ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;kBACvB,IAAI;YACV,MAAM;EAgzBf;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,GAAG,SAAS,CAElD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC;IACpD,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CA2ED"}
|
|
@@ -13,7 +13,7 @@ import path from "node:path";
|
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import { logger } from "../utils/logger.js";
|
|
15
15
|
import { schedulerBaseUrl, runtimeToken } from "../config.js";
|
|
16
|
-
import { getRuntimeAccessToken, getRuntimeBindingPublicState, saveRuntimeBinding, } from "./runtime-binding.js";
|
|
16
|
+
import { getRuntimeAccessToken, getRuntimeBindingPublicState, loadRuntimeBinding, saveRuntimeBinding, } from "./runtime-binding.js";
|
|
17
17
|
// 默认配置
|
|
18
18
|
const DEFAULT_CONFIG = {
|
|
19
19
|
enabled: false,
|
|
@@ -410,6 +410,34 @@ function resolveRuntimeBridgeBaseUrl(config) {
|
|
|
410
410
|
: getLocalIpAddress(config.serverUrl || schedulerBaseUrl);
|
|
411
411
|
return `http://${config.registerIp || config.virtualIp || localIp}:${port}`;
|
|
412
412
|
}
|
|
413
|
+
function loadPrimaryLifecycleConfig() {
|
|
414
|
+
return loadConfigs()[0] || loadConfig();
|
|
415
|
+
}
|
|
416
|
+
function resolveRegisteredSchedulerBaseUrl(instanceId) {
|
|
417
|
+
if (!instanceId) {
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
const registration = Object.entries(registrationState.registrations).find(([, registeredInstanceId]) => registeredInstanceId === instanceId);
|
|
421
|
+
return registration?.[0];
|
|
422
|
+
}
|
|
423
|
+
function resolveLifecycleSchedulerBaseUrl(config, instanceId) {
|
|
424
|
+
const registeredServerUrl = resolveRegisteredSchedulerBaseUrl(instanceId);
|
|
425
|
+
if (registeredServerUrl) {
|
|
426
|
+
return registeredServerUrl;
|
|
427
|
+
}
|
|
428
|
+
const configuredServerUrl = normalizeOptionalString(config.serverUrl);
|
|
429
|
+
if (configuredServerUrl &&
|
|
430
|
+
(configuredServerUrl !== schedulerBaseUrl ||
|
|
431
|
+
Boolean(process.env.AWS_RUNTIME_SCHEDULER_BASE_URL))) {
|
|
432
|
+
return configuredServerUrl;
|
|
433
|
+
}
|
|
434
|
+
const binding = loadRuntimeBinding();
|
|
435
|
+
const pairedSchedulerBaseUrl = normalizeOptionalString(binding.schedulerBaseUrl);
|
|
436
|
+
if (binding.status === "paired" && pairedSchedulerBaseUrl) {
|
|
437
|
+
return pairedSchedulerBaseUrl;
|
|
438
|
+
}
|
|
439
|
+
return configuredServerUrl || schedulerBaseUrl;
|
|
440
|
+
}
|
|
413
441
|
/**
|
|
414
442
|
* 执行注册请求
|
|
415
443
|
*/
|
|
@@ -445,7 +473,8 @@ async function doRegister(config) {
|
|
|
445
473
|
* 执行注销请求
|
|
446
474
|
*/
|
|
447
475
|
async function doUnregister(instanceId) {
|
|
448
|
-
const
|
|
476
|
+
const targetServerUrl = resolveLifecycleSchedulerBaseUrl(loadPrimaryLifecycleConfig(), instanceId);
|
|
477
|
+
const response = await axios.post(`${targetServerUrl}/api/instances/unregister`, { instanceId }, {
|
|
449
478
|
headers: {
|
|
450
479
|
"Content-Type": "application/json",
|
|
451
480
|
"X-Runtime-Token": runtimeToken,
|
|
@@ -565,7 +594,7 @@ async function autoRegisterSingle(config) {
|
|
|
565
594
|
* 注销实例
|
|
566
595
|
*/
|
|
567
596
|
export async function requestRuntimeAccessTokenRefresh() {
|
|
568
|
-
const config =
|
|
597
|
+
const config = loadPrimaryLifecycleConfig();
|
|
569
598
|
if (!config.userKey) {
|
|
570
599
|
return {
|
|
571
600
|
success: false,
|
|
@@ -573,11 +602,12 @@ export async function requestRuntimeAccessTokenRefresh() {
|
|
|
573
602
|
};
|
|
574
603
|
}
|
|
575
604
|
const state = getRuntimeBindingPublicState();
|
|
576
|
-
const
|
|
605
|
+
const targetServerUrl = resolveLifecycleSchedulerBaseUrl(config);
|
|
606
|
+
const localIp = getLocalIpAddress(targetServerUrl);
|
|
577
607
|
const runtimeBridgePort = process.env.AWS_RUNTIME_BRIDGE_PORT || 18081;
|
|
578
608
|
const runtimeBridgeBaseUrl = `http://${config.registerIp || config.virtualIp || localIp}:${runtimeBridgePort}`;
|
|
579
609
|
try {
|
|
580
|
-
const response = await axios.post(`${
|
|
610
|
+
const response = await axios.post(`${targetServerUrl}/api/instances/runtime-tokens/refresh`, {
|
|
581
611
|
tenantId: config.tenantId,
|
|
582
612
|
userKey: config.userKey,
|
|
583
613
|
instanceId: state.instanceId,
|
|
@@ -603,7 +633,7 @@ export async function requestRuntimeAccessTokenRefresh() {
|
|
|
603
633
|
accessToken: response.data.runtimeAccessToken,
|
|
604
634
|
instanceId: state.instanceId,
|
|
605
635
|
userId: response.data.userId || config.userKey,
|
|
606
|
-
schedulerBaseUrl,
|
|
636
|
+
schedulerBaseUrl: targetServerUrl,
|
|
607
637
|
});
|
|
608
638
|
return {
|
|
609
639
|
success: true,
|
|
@@ -674,8 +704,9 @@ export function getInstanceId() {
|
|
|
674
704
|
* @returns 清理结果
|
|
675
705
|
*/
|
|
676
706
|
export async function bridgeRestartCleanup() {
|
|
677
|
-
const config =
|
|
707
|
+
const config = loadPrimaryLifecycleConfig();
|
|
678
708
|
const instanceId = registrationState.instanceId;
|
|
709
|
+
const targetServerUrl = resolveLifecycleSchedulerBaseUrl(config, instanceId);
|
|
679
710
|
// 构建清理请求
|
|
680
711
|
const requestBody = {};
|
|
681
712
|
if (instanceId) {
|
|
@@ -683,7 +714,7 @@ export async function bridgeRestartCleanup() {
|
|
|
683
714
|
}
|
|
684
715
|
// 如果有配置,也传入 runtimeBridgeBaseUrl
|
|
685
716
|
if (config.enabled) {
|
|
686
|
-
const localIp = getLocalIpAddress();
|
|
717
|
+
const localIp = getLocalIpAddress(targetServerUrl);
|
|
687
718
|
const port = process.env.AWS_RUNTIME_BRIDGE_PORT || 18081;
|
|
688
719
|
requestBody.runtimeBridgeBaseUrl = `http://${config.registerIp || config.virtualIp || localIp}:${port}`;
|
|
689
720
|
}
|
|
@@ -695,7 +726,7 @@ export async function bridgeRestartCleanup() {
|
|
|
695
726
|
logger.info(`[AutoRegister] Bridge 重启清理:正在通知调度中心...`);
|
|
696
727
|
logger.info(`[AutoRegister] instanceId: ${requestBody.instanceId || "(无)"}`);
|
|
697
728
|
logger.info(`[AutoRegister] runtimeBridgeBaseUrl: ${requestBody.runtimeBridgeBaseUrl || "(无)"}`);
|
|
698
|
-
const response = await axios.post(`${
|
|
729
|
+
const response = await axios.post(`${targetServerUrl}/api/instances/bridge-restart-cleanup`, requestBody, {
|
|
699
730
|
headers: {
|
|
700
731
|
"Content-Type": "application/json",
|
|
701
732
|
"X-Runtime-Token": runtimeToken,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-register.test.d.ts","sourceRoot":"","sources":["../../src/services/auto-register.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
vi.mock("node:os", async () => {
|
|
6
|
+
const actual = await vi.importActual("node:os");
|
|
7
|
+
const mocked = {
|
|
8
|
+
...actual,
|
|
9
|
+
homedir: () => process.env.AWS_TEST_HOME || actual.homedir(),
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
...mocked,
|
|
13
|
+
default: mocked,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
vi.mock("axios", () => ({
|
|
17
|
+
default: {
|
|
18
|
+
post: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
const originalEnv = { ...process.env };
|
|
22
|
+
const tempRoots = [];
|
|
23
|
+
function createRuntimeHome() {
|
|
24
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "aws-auto-register-"));
|
|
25
|
+
tempRoots.push(root);
|
|
26
|
+
return root;
|
|
27
|
+
}
|
|
28
|
+
function writeAutoRegisterConfig(homeDir, targets) {
|
|
29
|
+
const configPath = path.join(homeDir, ".aws-bridge", "config.json");
|
|
30
|
+
const autoRegisterTargets = (typeof targets === "string"
|
|
31
|
+
? [{ serverUrl: targets }]
|
|
32
|
+
: targets).map((target, index) => ({
|
|
33
|
+
serverUrl: target.serverUrl,
|
|
34
|
+
instanceName: target.instanceName || `test-bridge-${index + 1}`,
|
|
35
|
+
userKey: target.userKey || `aws_live_test_key_${index + 1}`,
|
|
36
|
+
registerIp: target.registerIp || "127.0.0.1",
|
|
37
|
+
}));
|
|
38
|
+
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
39
|
+
writeFileSync(configPath, `${JSON.stringify({
|
|
40
|
+
connectionKey: "bridge-key",
|
|
41
|
+
autoRegisterTargets,
|
|
42
|
+
}, null, 2)}\n`, "utf-8");
|
|
43
|
+
}
|
|
44
|
+
function useRuntimeHome(runtimeHome) {
|
|
45
|
+
process.env.AWS_TEST_HOME = runtimeHome;
|
|
46
|
+
process.env.HOME = runtimeHome;
|
|
47
|
+
process.env.USERPROFILE = runtimeHome;
|
|
48
|
+
process.env.AWS_RUNTIME_HOME_DIR = runtimeHome;
|
|
49
|
+
process.env.AWS_RUNTIME_SCHEDULER_BASE_URL = "";
|
|
50
|
+
}
|
|
51
|
+
async function mockSuccessfulPost(response) {
|
|
52
|
+
const { default: axios } = await import("axios");
|
|
53
|
+
vi.mocked(axios.post).mockResolvedValue({ data: response });
|
|
54
|
+
return vi.mocked(axios.post);
|
|
55
|
+
}
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
process.env = { ...originalEnv };
|
|
58
|
+
vi.resetModules();
|
|
59
|
+
vi.clearAllMocks();
|
|
60
|
+
vi.restoreAllMocks();
|
|
61
|
+
for (const root of tempRoots.splice(0)) {
|
|
62
|
+
rmSync(root, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
describe("auto-register scheduler URL selection", () => {
|
|
66
|
+
it("uses configured serverUrl for lifecycle callbacks when env scheduler URL is absent", async () => {
|
|
67
|
+
const runtimeHome = createRuntimeHome();
|
|
68
|
+
useRuntimeHome(runtimeHome);
|
|
69
|
+
writeAutoRegisterConfig(runtimeHome, "http://127.0.0.1:7380");
|
|
70
|
+
const post = await mockSuccessfulPost({
|
|
71
|
+
success: true,
|
|
72
|
+
instanceId: "bridge-instance-1",
|
|
73
|
+
userId: "user-1",
|
|
74
|
+
message: "ok",
|
|
75
|
+
runtimeAccessToken: "runtime-token-123456",
|
|
76
|
+
});
|
|
77
|
+
const { autoRegister, bridgeRestartCleanup, requestRuntimeAccessTokenRefresh, unregister } = await import("./auto-register.js");
|
|
78
|
+
await expect(autoRegister()).resolves.toBe(true);
|
|
79
|
+
await expect(bridgeRestartCleanup()).resolves.toMatchObject({ success: true });
|
|
80
|
+
post.mockResolvedValueOnce({
|
|
81
|
+
data: {
|
|
82
|
+
success: true,
|
|
83
|
+
runtimeAccessToken: "refreshed-token-123456",
|
|
84
|
+
userId: "user-1",
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
await expect(requestRuntimeAccessTokenRefresh()).resolves.toMatchObject({
|
|
88
|
+
success: true,
|
|
89
|
+
runtimeAccessToken: "refreshed-token-123456",
|
|
90
|
+
});
|
|
91
|
+
post.mockResolvedValueOnce({ data: { success: true, message: "unregistered" } });
|
|
92
|
+
await expect(unregister()).resolves.toBe(true);
|
|
93
|
+
expect(post.mock.calls[0]?.[0]).toBe("http://127.0.0.1:7380/api/instances/register");
|
|
94
|
+
expect(post.mock.calls[1]?.[0]).toBe("http://127.0.0.1:7380/api/instances/bridge-restart-cleanup");
|
|
95
|
+
expect(post.mock.calls[2]?.[0]).toBe("http://127.0.0.1:7380/api/instances/runtime-tokens/refresh");
|
|
96
|
+
expect(post.mock.calls[3]?.[0]).toBe("http://127.0.0.1:7380/api/instances/unregister");
|
|
97
|
+
});
|
|
98
|
+
it("uses the registered target URL instead of the first configured target", async () => {
|
|
99
|
+
const runtimeHome = createRuntimeHome();
|
|
100
|
+
useRuntimeHome(runtimeHome);
|
|
101
|
+
writeAutoRegisterConfig(runtimeHome, [
|
|
102
|
+
{ serverUrl: "http://scheduler-a.local:7380", userKey: "aws_live_key_a" },
|
|
103
|
+
{ serverUrl: "http://scheduler-b.local:7380", userKey: "aws_live_key_b" },
|
|
104
|
+
]);
|
|
105
|
+
const post = await mockSuccessfulPost({ success: false, message: "unexpected" });
|
|
106
|
+
post.mockResolvedValueOnce({
|
|
107
|
+
data: {
|
|
108
|
+
success: true,
|
|
109
|
+
instanceId: "bridge-a",
|
|
110
|
+
userId: "user-a",
|
|
111
|
+
runtimeAccessToken: "runtime-token-a-123456",
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
post.mockResolvedValueOnce({
|
|
115
|
+
data: {
|
|
116
|
+
success: true,
|
|
117
|
+
instanceId: "bridge-b",
|
|
118
|
+
userId: "user-b",
|
|
119
|
+
runtimeAccessToken: "runtime-token-b-123456",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
post.mockResolvedValueOnce({ data: { success: true, message: "cleanup ok" } });
|
|
123
|
+
const { autoRegister, bridgeRestartCleanup } = await import("./auto-register.js");
|
|
124
|
+
await expect(autoRegister()).resolves.toBe(true);
|
|
125
|
+
await expect(bridgeRestartCleanup()).resolves.toMatchObject({ success: true });
|
|
126
|
+
expect(post.mock.calls[0]?.[0]).toBe("http://scheduler-a.local:7380/api/instances/register");
|
|
127
|
+
expect(post.mock.calls[1]?.[0]).toBe("http://scheduler-b.local:7380/api/instances/register");
|
|
128
|
+
expect(post.mock.calls[2]?.[0]).toBe("http://scheduler-b.local:7380/api/instances/bridge-restart-cleanup");
|
|
129
|
+
});
|
|
130
|
+
it("falls back to paired runtime binding scheduler URL when config has no serverUrl", async () => {
|
|
131
|
+
const runtimeHome = createRuntimeHome();
|
|
132
|
+
useRuntimeHome(runtimeHome);
|
|
133
|
+
const post = await mockSuccessfulPost({
|
|
134
|
+
success: true,
|
|
135
|
+
runtimeAccessToken: "binding-refresh-token-123456",
|
|
136
|
+
userId: "user-from-binding",
|
|
137
|
+
});
|
|
138
|
+
const { saveRuntimeBinding } = await import("./runtime-binding.js");
|
|
139
|
+
const { requestRuntimeAccessTokenRefresh } = await import("./auto-register.js");
|
|
140
|
+
saveRuntimeBinding({
|
|
141
|
+
accessToken: "existing-binding-token-123456",
|
|
142
|
+
instanceId: "bridge-from-binding",
|
|
143
|
+
userId: "user-from-binding",
|
|
144
|
+
schedulerBaseUrl: "http://paired-scheduler.local:7380",
|
|
145
|
+
});
|
|
146
|
+
process.env.AWS_AUTO_REGISTER = "true";
|
|
147
|
+
process.env.AWS_USER_KEY = "aws_live_env_key";
|
|
148
|
+
await expect(requestRuntimeAccessTokenRefresh()).resolves.toMatchObject({
|
|
149
|
+
success: true,
|
|
150
|
+
runtimeAccessToken: "binding-refresh-token-123456",
|
|
151
|
+
});
|
|
152
|
+
expect(post.mock.calls[0]?.[0]).toBe("http://paired-scheduler.local:7380/api/instances/runtime-tokens/refresh");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -14,11 +14,16 @@ export interface ResolveAwsClientAgentMcpCommandOptions {
|
|
|
14
14
|
exists?: (filePath: string) => boolean;
|
|
15
15
|
release?: () => string | null;
|
|
16
16
|
}
|
|
17
|
+
export interface ReleaseBundledAwsClientAgentMcpOptions {
|
|
18
|
+
packageRoot?: string;
|
|
19
|
+
releasedRoot?: string;
|
|
20
|
+
now?: () => string;
|
|
21
|
+
}
|
|
17
22
|
export interface AwsMcpPreparedInfo extends AwsMcpCommandSpec {
|
|
18
23
|
source: "override" | "bundled" | "global";
|
|
19
24
|
}
|
|
20
25
|
export declare function getReleasedMcpDistPath(): string;
|
|
21
|
-
export declare function releaseBundledAwsClientAgentMcp(): string | null;
|
|
26
|
+
export declare function releaseBundledAwsClientAgentMcp(options?: ReleaseBundledAwsClientAgentMcpOptions): string | null;
|
|
22
27
|
export declare function resolveAwsClientAgentMcpCommand(options?: ResolveAwsClientAgentMcpCommandOptions): AwsMcpPreparedInfo;
|
|
23
28
|
export declare function getAwsClientAgentMcpPreparedInfo(options?: ResolveAwsClientAgentMcpCommandOptions): AwsMcpPreparedInfo;
|
|
24
29
|
export declare function buildAwsMcpServerConfig(input: AwsMcpServerConfigInput): AwsMcpServerConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aws-client-agent-mcp.d.ts","sourceRoot":"","sources":["../../src/services/aws-client-agent-mcp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"aws-client-agent-mcp.d.ts","sourceRoot":"","sources":["../../src/services/aws-client-agent-mcp.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAuB7C,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,sCAAsC;IACrD,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,sCAAsC;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAaD,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;CAC3C;AAqBD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AA0FD,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,sCAA2C,GACnD,MAAM,GAAG,IAAI,CAgDf;AA2ED,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,sCAA2C,GACnD,kBAAkB,CAEpB;AAED,wBAAgB,gCAAgC,CAC9C,OAAO,GAAE,sCAA2C,GACnD,kBAAkB,CAsBpB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,uBAAuB,GAC7B,kBAAkB,CAwBpB;AAED,wBAAgB,+BAA+B,IAAI,IAAI,CAgBtD"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { getRuntimeHomeDir, port, schedulerBaseUrl } from "../config.js";
|
|
5
|
-
import { getRuntimeAccessToken, loadRuntimeBinding, } from "./runtime-binding.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { getRuntimeAccessToken, loadRuntimeBinding, } from "./runtime-binding.js";
|
|
7
8
|
export const AWS_MCP_SERVER_NAME = "aws-mcp";
|
|
8
9
|
const AWS_MCP_ALLOWED_ENV_KEYS = [
|
|
9
10
|
"AWS_INTERNAL_API_KEY",
|
|
@@ -30,8 +31,8 @@ function getPackageRoot() {
|
|
|
30
31
|
const currentFile = fileURLToPath(import.meta.url);
|
|
31
32
|
return path.resolve(path.dirname(currentFile), "..", "..");
|
|
32
33
|
}
|
|
33
|
-
function getBundledEntryPath() {
|
|
34
|
-
return path.join(
|
|
34
|
+
function getBundledEntryPath(packageRoot = getPackageRoot()) {
|
|
35
|
+
return path.join(packageRoot, "package", "aws-client-agent-mcp", "dist", "index.js");
|
|
35
36
|
}
|
|
36
37
|
function getReleasedMcpRoot() {
|
|
37
38
|
return path.join(getRuntimeHomeDir(), ".aws-bridge", "mcp");
|
|
@@ -39,24 +40,107 @@ function getReleasedMcpRoot() {
|
|
|
39
40
|
export function getReleasedMcpDistPath() {
|
|
40
41
|
return path.join(getReleasedMcpRoot(), "dist");
|
|
41
42
|
}
|
|
42
|
-
function
|
|
43
|
-
return path.join(
|
|
43
|
+
function getReleaseManifestPath(releasedRoot = getReleasedMcpRoot()) {
|
|
44
|
+
return path.join(releasedRoot, ".release.json");
|
|
45
|
+
}
|
|
46
|
+
function readPackageVersion(packageJsonPath) {
|
|
47
|
+
try {
|
|
48
|
+
const metadata = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
49
|
+
return metadata.version || "unknown";
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return "unknown";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getBridgeVersion(packageRoot) {
|
|
56
|
+
return readPackageVersion(path.join(packageRoot, "package.json"));
|
|
57
|
+
}
|
|
58
|
+
function getBundledMcpVersion(packageRoot) {
|
|
59
|
+
return readPackageVersion(path.join(packageRoot, "package", "aws-client-agent-mcp", "package.json"));
|
|
60
|
+
}
|
|
61
|
+
function listDistFiles(dirPath, rootPath = dirPath) {
|
|
62
|
+
return readdirSync(dirPath, { withFileTypes: true }).flatMap((entry) => {
|
|
63
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
64
|
+
if (entry.isDirectory()) {
|
|
65
|
+
return listDistFiles(entryPath, rootPath);
|
|
66
|
+
}
|
|
67
|
+
if (!entry.isFile()) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
return [path.relative(rootPath, entryPath).split(path.sep).join("/")];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function hashDistDirectory(distPath) {
|
|
74
|
+
const hash = createHash("sha256");
|
|
75
|
+
for (const relativePath of listDistFiles(distPath).sort()) {
|
|
76
|
+
const filePath = path.join(distPath, ...relativePath.split("/"));
|
|
77
|
+
const stat = statSync(filePath);
|
|
78
|
+
hash.update(relativePath);
|
|
79
|
+
hash.update("\0");
|
|
80
|
+
hash.update(String(stat.size));
|
|
81
|
+
hash.update("\0");
|
|
82
|
+
hash.update(readFileSync(filePath));
|
|
83
|
+
hash.update("\0");
|
|
84
|
+
}
|
|
85
|
+
return `sha256:${hash.digest("hex")}`;
|
|
44
86
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
87
|
+
function readReleaseManifest(releasedRoot) {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(readFileSync(getReleaseManifestPath(releasedRoot), "utf-8"));
|
|
90
|
+
if (typeof parsed.bridgeVersion === "string" &&
|
|
91
|
+
typeof parsed.mcpVersion === "string" &&
|
|
92
|
+
typeof parsed.distHash === "string") {
|
|
93
|
+
return {
|
|
94
|
+
bridgeVersion: parsed.bridgeVersion,
|
|
95
|
+
mcpVersion: parsed.mcpVersion,
|
|
96
|
+
distHash: parsed.distHash,
|
|
97
|
+
releasedAt: typeof parsed.releasedAt === "string" ? parsed.releasedAt : "",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function shouldRefreshReleasedMcp(releasedEntry, manifest, nextManifest) {
|
|
107
|
+
return (!existsSync(releasedEntry) ||
|
|
108
|
+
!manifest ||
|
|
109
|
+
manifest.bridgeVersion !== nextManifest.bridgeVersion ||
|
|
110
|
+
manifest.mcpVersion !== nextManifest.mcpVersion ||
|
|
111
|
+
manifest.distHash !== nextManifest.distHash);
|
|
112
|
+
}
|
|
113
|
+
export function releaseBundledAwsClientAgentMcp(options = {}) {
|
|
114
|
+
const packageRoot = options.packageRoot || getPackageRoot();
|
|
115
|
+
const releasedRoot = options.releasedRoot || getReleasedMcpRoot();
|
|
116
|
+
const bundledDist = path.dirname(getBundledEntryPath(packageRoot));
|
|
117
|
+
const releasedDist = path.join(releasedRoot, "dist");
|
|
48
118
|
const releasedEntry = path.join(releasedDist, "index.js");
|
|
49
119
|
if (!existsSync(path.join(bundledDist, "index.js"))) {
|
|
50
120
|
return null;
|
|
51
121
|
}
|
|
52
|
-
|
|
53
|
-
|
|
122
|
+
const nextManifest = {
|
|
123
|
+
bridgeVersion: getBridgeVersion(packageRoot),
|
|
124
|
+
mcpVersion: getBundledMcpVersion(packageRoot),
|
|
125
|
+
distHash: hashDistDirectory(bundledDist),
|
|
126
|
+
};
|
|
127
|
+
const currentManifest = readReleaseManifest(releasedRoot);
|
|
128
|
+
if (shouldRefreshReleasedMcp(releasedEntry, currentManifest, nextManifest)) {
|
|
129
|
+
rmSync(releasedDist, { recursive: true, force: true });
|
|
130
|
+
mkdirSync(releasedRoot, { recursive: true });
|
|
54
131
|
cpSync(bundledDist, releasedDist, {
|
|
55
132
|
errorOnExist: false,
|
|
56
133
|
force: true,
|
|
57
134
|
recursive: true,
|
|
58
135
|
});
|
|
59
|
-
|
|
136
|
+
writeFileSync(getReleaseManifestPath(releasedRoot), `${JSON.stringify({
|
|
137
|
+
...nextManifest,
|
|
138
|
+
releasedAt: options.now ? options.now() : new Date().toISOString(),
|
|
139
|
+
}, null, 2)}\n`, "utf-8");
|
|
140
|
+
logger.info(`[runtime-bridge] ${AWS_MCP_SERVER_NAME} MCP 已更新到: ${releasedDist}`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
logger.info(`[runtime-bridge] ${AWS_MCP_SERVER_NAME} MCP 已是最新: ${releasedDist}`);
|
|
60
144
|
}
|
|
61
145
|
return releasedEntry;
|
|
62
146
|
}
|
|
@@ -125,14 +209,6 @@ export function getAwsClientAgentMcpPreparedInfo(options = {}) {
|
|
|
125
209
|
};
|
|
126
210
|
}
|
|
127
211
|
const exists = options.exists || existsSync;
|
|
128
|
-
const releasedEntry = getReleasedEntryPath();
|
|
129
|
-
if (exists(releasedEntry)) {
|
|
130
|
-
return {
|
|
131
|
-
source: "bundled",
|
|
132
|
-
command: process.execPath,
|
|
133
|
-
args: [releasedEntry],
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
212
|
const release = options.release || releaseBundledAwsClientAgentMcp;
|
|
137
213
|
const released = release();
|
|
138
214
|
if (released && exists(released)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
@@ -8,6 +8,15 @@ afterEach(() => {
|
|
|
8
8
|
vi.resetModules();
|
|
9
9
|
vi.restoreAllMocks();
|
|
10
10
|
});
|
|
11
|
+
function createBundledMcpPackage(version = '1.0.0') {
|
|
12
|
+
const packageRoot = mkdtempSync(path.join(os.tmpdir(), 'aws-mcp-package-'));
|
|
13
|
+
const bundledDist = path.join(packageRoot, 'package', 'aws-client-agent-mcp', 'dist');
|
|
14
|
+
mkdirSync(bundledDist, { recursive: true });
|
|
15
|
+
writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ name: 'aws-runtime-bridge', version: '9.9.9' }), 'utf-8');
|
|
16
|
+
writeFileSync(path.join(packageRoot, 'package', 'aws-client-agent-mcp', 'package.json'), JSON.stringify({ name: 'aws-client-agent-mcp', version }), 'utf-8');
|
|
17
|
+
writeFileSync(path.join(bundledDist, 'index.js'), 'console.log("mcp v1");\n', 'utf-8');
|
|
18
|
+
return packageRoot;
|
|
19
|
+
}
|
|
11
20
|
describe('aws-client-agent-mcp service', () => {
|
|
12
21
|
it('prefers explicit command override from env', async () => {
|
|
13
22
|
process.env.AWS_CLIENT_AGENT_MCP_COMMAND = 'custom-aws-client-agent-mcp';
|
|
@@ -19,18 +28,79 @@ describe('aws-client-agent-mcp service', () => {
|
|
|
19
28
|
args: ['--stdio', '--trace'],
|
|
20
29
|
});
|
|
21
30
|
});
|
|
22
|
-
it('uses bundled dist entry
|
|
31
|
+
it('uses bundled dist entry after release check', async () => {
|
|
23
32
|
process.env.AWS_CLIENT_AGENT_MCP_COMMAND = '';
|
|
24
33
|
const mod = await import('./aws-client-agent-mcp.js');
|
|
25
|
-
const exists = vi.fn().mockReturnValue(true);
|
|
26
|
-
const resolved = mod.resolveAwsClientAgentMcpCommand({ exists });
|
|
27
34
|
const releasedEntry = path.join(os.homedir(), '.aws-bridge', 'mcp', 'dist', 'index.js');
|
|
35
|
+
const exists = vi.fn((filePath) => filePath === releasedEntry);
|
|
36
|
+
const release = vi.fn(() => releasedEntry);
|
|
37
|
+
const resolved = mod.resolveAwsClientAgentMcpCommand({ exists, release });
|
|
28
38
|
expect(resolved).toEqual({
|
|
29
39
|
source: 'bundled',
|
|
30
40
|
command: process.execPath,
|
|
31
41
|
args: [releasedEntry],
|
|
32
42
|
});
|
|
33
43
|
expect(exists).toHaveBeenCalled();
|
|
44
|
+
expect(release).toHaveBeenCalledOnce();
|
|
45
|
+
});
|
|
46
|
+
it('writes release manifest and skips copy when bundled mcp is unchanged', async () => {
|
|
47
|
+
process.env.AWS_CLIENT_AGENT_MCP_COMMAND = '';
|
|
48
|
+
const packageRoot = createBundledMcpPackage('1.0.0');
|
|
49
|
+
const releasedRoot = mkdtempSync(path.join(os.tmpdir(), 'aws-mcp-release-'));
|
|
50
|
+
try {
|
|
51
|
+
const mod = await import('./aws-client-agent-mcp.js');
|
|
52
|
+
const firstRelease = mod.releaseBundledAwsClientAgentMcp({
|
|
53
|
+
packageRoot,
|
|
54
|
+
releasedRoot,
|
|
55
|
+
now: () => '2026-05-04T00:00:00.000Z',
|
|
56
|
+
});
|
|
57
|
+
const releasedIndex = path.join(releasedRoot, 'dist', 'index.js');
|
|
58
|
+
const manifestPath = path.join(releasedRoot, '.release.json');
|
|
59
|
+
expect(firstRelease).toBe(releasedIndex);
|
|
60
|
+
expect(existsSync(releasedIndex)).toBe(true);
|
|
61
|
+
const firstManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
62
|
+
expect(firstManifest).toMatchObject({
|
|
63
|
+
bridgeVersion: '9.9.9',
|
|
64
|
+
mcpVersion: '1.0.0',
|
|
65
|
+
releasedAt: '2026-05-04T00:00:00.000Z',
|
|
66
|
+
});
|
|
67
|
+
expect(firstManifest.distHash).toMatch(/^sha256:/);
|
|
68
|
+
writeFileSync(releasedIndex, 'console.log("local marker");\n', 'utf-8');
|
|
69
|
+
mod.releaseBundledAwsClientAgentMcp({
|
|
70
|
+
packageRoot,
|
|
71
|
+
releasedRoot,
|
|
72
|
+
now: () => '2026-05-05T00:00:00.000Z',
|
|
73
|
+
});
|
|
74
|
+
expect(readFileSync(releasedIndex, 'utf-8')).toBe('console.log("local marker");\n');
|
|
75
|
+
expect(JSON.parse(readFileSync(manifestPath, 'utf-8')).releasedAt).toBe('2026-05-04T00:00:00.000Z');
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
rmSync(packageRoot, { recursive: true, force: true });
|
|
79
|
+
rmSync(releasedRoot, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
it('refreshes released mcp when bundled dist changes', async () => {
|
|
83
|
+
process.env.AWS_CLIENT_AGENT_MCP_COMMAND = '';
|
|
84
|
+
const packageRoot = createBundledMcpPackage('1.0.0');
|
|
85
|
+
const releasedRoot = mkdtempSync(path.join(os.tmpdir(), 'aws-mcp-release-'));
|
|
86
|
+
try {
|
|
87
|
+
const mod = await import('./aws-client-agent-mcp.js');
|
|
88
|
+
const bundledIndex = path.join(packageRoot, 'package', 'aws-client-agent-mcp', 'dist', 'index.js');
|
|
89
|
+
const releasedIndex = path.join(releasedRoot, 'dist', 'index.js');
|
|
90
|
+
mod.releaseBundledAwsClientAgentMcp({ packageRoot, releasedRoot });
|
|
91
|
+
writeFileSync(bundledIndex, 'console.log("mcp v2");\n', 'utf-8');
|
|
92
|
+
mod.releaseBundledAwsClientAgentMcp({
|
|
93
|
+
packageRoot,
|
|
94
|
+
releasedRoot,
|
|
95
|
+
now: () => '2026-05-05T00:00:00.000Z',
|
|
96
|
+
});
|
|
97
|
+
expect(readFileSync(releasedIndex, 'utf-8')).toBe('console.log("mcp v2");\n');
|
|
98
|
+
expect(JSON.parse(readFileSync(path.join(releasedRoot, '.release.json'), 'utf-8')).releasedAt).toBe('2026-05-05T00:00:00.000Z');
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
rmSync(packageRoot, { recursive: true, force: true });
|
|
102
|
+
rmSync(releasedRoot, { recursive: true, force: true });
|
|
103
|
+
}
|
|
34
104
|
});
|
|
35
105
|
it('releases bundled mcp into home directory when runtime dist is missing', async () => {
|
|
36
106
|
process.env.AWS_CLIENT_AGENT_MCP_COMMAND = '';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { StartupConfigWizardResult } from "./startup-config-wizard.js";
|
|
2
|
+
export interface CliCommandResult {
|
|
3
|
+
handled: boolean;
|
|
4
|
+
exitCode: number;
|
|
5
|
+
}
|
|
6
|
+
export interface CommandRunResult {
|
|
7
|
+
status: number | null;
|
|
8
|
+
error?: Error;
|
|
9
|
+
}
|
|
10
|
+
export interface CliCommandOptions {
|
|
11
|
+
argv?: string[];
|
|
12
|
+
packageRoot?: string;
|
|
13
|
+
stdout?: (message: string) => void;
|
|
14
|
+
stderr?: (message: string) => void;
|
|
15
|
+
runCommand?: (command: string, args: string[]) => CommandRunResult;
|
|
16
|
+
configure?: () => Promise<StartupConfigWizardResult>;
|
|
17
|
+
}
|
|
18
|
+
export declare function readPackageVersion(packageRoot?: string): string;
|
|
19
|
+
export declare function handleCliCommand(options?: CliCommandOptions): Promise<CliCommandResult>;
|
|
20
|
+
//# sourceMappingURL=cli-commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-commands.d.ts","sourceRoot":"","sources":["../../src/services/cli-commands.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAG5E,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,gBAAgB,CAAC;IACnE,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,yBAAyB,CAAC,CAAC;CACtD;AAYD,wBAAgB,kBAAkB,CAAC,WAAW,SAA0B,GAAG,MAAM,CAMhF;AAcD,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CAqC3B"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { configureStartupConfig } from "./startup-config-wizard.js";
|
|
6
|
+
function getDefaultPackageRoot() {
|
|
7
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
8
|
+
return path.resolve(path.dirname(currentFile), "..", "..");
|
|
9
|
+
}
|
|
10
|
+
export function readPackageVersion(packageRoot = getDefaultPackageRoot()) {
|
|
11
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
12
|
+
const metadata = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
13
|
+
return metadata.version || "unknown";
|
|
14
|
+
}
|
|
15
|
+
function defaultRunCommand(command, args) {
|
|
16
|
+
const result = spawnSync(command, args, {
|
|
17
|
+
shell: process.platform === "win32",
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
});
|
|
20
|
+
return { status: result.status, error: result.error };
|
|
21
|
+
}
|
|
22
|
+
function isVersionCommand(command) {
|
|
23
|
+
return command === "-v" || command === "--version" || command === "version";
|
|
24
|
+
}
|
|
25
|
+
export async function handleCliCommand(options = {}) {
|
|
26
|
+
const argv = options.argv || process.argv.slice(2);
|
|
27
|
+
const command = argv[0];
|
|
28
|
+
const stdout = options.stdout || console.log;
|
|
29
|
+
const stderr = options.stderr || console.error;
|
|
30
|
+
if (isVersionCommand(command)) {
|
|
31
|
+
stdout(`aws-runtime-bridge ${readPackageVersion(options.packageRoot)}`);
|
|
32
|
+
return { handled: true, exitCode: 0 };
|
|
33
|
+
}
|
|
34
|
+
if (command === "update") {
|
|
35
|
+
stdout("[runtime-bridge] 正在更新 aws-runtime-bridge 到最新版本...");
|
|
36
|
+
const runCommand = options.runCommand || defaultRunCommand;
|
|
37
|
+
const result = runCommand("npm", ["install", "-g", "aws-runtime-bridge@latest"]);
|
|
38
|
+
if (result.error) {
|
|
39
|
+
stderr(`[runtime-bridge] 更新失败: ${result.error.message}`);
|
|
40
|
+
return { handled: true, exitCode: 1 };
|
|
41
|
+
}
|
|
42
|
+
if (result.status !== 0) {
|
|
43
|
+
stderr(`[runtime-bridge] 更新失败,npm 退出码: ${result.status ?? "unknown"}`);
|
|
44
|
+
return { handled: true, exitCode: result.status ?? 1 };
|
|
45
|
+
}
|
|
46
|
+
stdout("[runtime-bridge] 更新完成。请重新运行 awsb -v 查看当前版本。");
|
|
47
|
+
return { handled: true, exitCode: 0 };
|
|
48
|
+
}
|
|
49
|
+
if (command === "config") {
|
|
50
|
+
const configure = options.configure || configureStartupConfig;
|
|
51
|
+
const result = await configure();
|
|
52
|
+
return { handled: true, exitCode: result === "configured" ? 0 : 1 };
|
|
53
|
+
}
|
|
54
|
+
return { handled: false, exitCode: 0 };
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-commands.test.d.ts","sourceRoot":"","sources":["../../src/services/cli-commands.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { handleCliCommand, readPackageVersion } from "./cli-commands.js";
|
|
6
|
+
const tempRoots = [];
|
|
7
|
+
function createPackageRoot(version = "9.8.7") {
|
|
8
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "aws-bridge-cli-"));
|
|
9
|
+
tempRoots.push(root);
|
|
10
|
+
mkdirSync(root, { recursive: true });
|
|
11
|
+
writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "aws-runtime-bridge", version }), "utf-8");
|
|
12
|
+
return root;
|
|
13
|
+
}
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
for (const root of tempRoots.splice(0)) {
|
|
16
|
+
rmSync(root, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
describe("cli commands", () => {
|
|
20
|
+
it("reads package version from package root", () => {
|
|
21
|
+
const packageRoot = createPackageRoot("1.2.3");
|
|
22
|
+
expect(readPackageVersion(packageRoot)).toBe("1.2.3");
|
|
23
|
+
});
|
|
24
|
+
it("prints version for -v", async () => {
|
|
25
|
+
const packageRoot = createPackageRoot("1.2.3");
|
|
26
|
+
const output = [];
|
|
27
|
+
const result = await handleCliCommand({
|
|
28
|
+
argv: ["-v"],
|
|
29
|
+
packageRoot,
|
|
30
|
+
stdout: (message) => output.push(message),
|
|
31
|
+
});
|
|
32
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
33
|
+
expect(output).toEqual(["aws-runtime-bridge 1.2.3"]);
|
|
34
|
+
});
|
|
35
|
+
it("prints version for version command", async () => {
|
|
36
|
+
const packageRoot = createPackageRoot("2.0.0");
|
|
37
|
+
const output = [];
|
|
38
|
+
const result = await handleCliCommand({
|
|
39
|
+
argv: ["version"],
|
|
40
|
+
packageRoot,
|
|
41
|
+
stdout: (message) => output.push(message),
|
|
42
|
+
});
|
|
43
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
44
|
+
expect(output).toEqual(["aws-runtime-bridge 2.0.0"]);
|
|
45
|
+
});
|
|
46
|
+
it("runs npm global update for update command", async () => {
|
|
47
|
+
const output = [];
|
|
48
|
+
const commands = [];
|
|
49
|
+
const result = await handleCliCommand({
|
|
50
|
+
argv: ["update"],
|
|
51
|
+
stdout: (message) => output.push(message),
|
|
52
|
+
runCommand: (command, args) => {
|
|
53
|
+
commands.push({ command, args });
|
|
54
|
+
return { status: 0 };
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
58
|
+
expect(commands).toEqual([
|
|
59
|
+
{ command: "npm", args: ["install", "-g", "aws-runtime-bridge@latest"] },
|
|
60
|
+
]);
|
|
61
|
+
expect(output.at(-1)).toBe("[runtime-bridge] 更新完成。请重新运行 awsb -v 查看当前版本。");
|
|
62
|
+
});
|
|
63
|
+
it("returns npm exit code when update fails", async () => {
|
|
64
|
+
const errors = [];
|
|
65
|
+
const result = await handleCliCommand({
|
|
66
|
+
argv: ["update"],
|
|
67
|
+
stdout: () => undefined,
|
|
68
|
+
stderr: (message) => errors.push(message),
|
|
69
|
+
runCommand: () => ({ status: 7 }),
|
|
70
|
+
});
|
|
71
|
+
expect(result).toEqual({ handled: true, exitCode: 7 });
|
|
72
|
+
expect(errors).toEqual(["[runtime-bridge] 更新失败,npm 退出码: 7"]);
|
|
73
|
+
});
|
|
74
|
+
it("runs interactive config command", async () => {
|
|
75
|
+
const result = await handleCliCommand({
|
|
76
|
+
argv: ["config"],
|
|
77
|
+
configure: async () => "configured",
|
|
78
|
+
});
|
|
79
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
80
|
+
});
|
|
81
|
+
it("returns non-zero when config command fails", async () => {
|
|
82
|
+
const result = await handleCliCommand({
|
|
83
|
+
argv: ["config"],
|
|
84
|
+
configure: async () => "failed",
|
|
85
|
+
});
|
|
86
|
+
expect(result).toEqual({ handled: true, exitCode: 1 });
|
|
87
|
+
});
|
|
88
|
+
it("does not handle normal startup arguments", async () => {
|
|
89
|
+
const result = await handleCliCommand({ argv: [] });
|
|
90
|
+
expect(result).toEqual({ handled: false, exitCode: 0 });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -9,7 +9,8 @@ interface EnsureStartupConfigOptions {
|
|
|
9
9
|
env?: NodeJS.ProcessEnv;
|
|
10
10
|
generateConnectionKey?: () => string;
|
|
11
11
|
}
|
|
12
|
-
export type StartupConfigWizardResult = "exists" | "created" | "skipped" | "non-interactive" | "failed";
|
|
12
|
+
export type StartupConfigWizardResult = "exists" | "created" | "configured" | "skipped" | "non-interactive" | "failed";
|
|
13
13
|
export declare function ensureStartupConfig(options?: EnsureStartupConfigOptions): Promise<StartupConfigWizardResult>;
|
|
14
|
+
export declare function configureStartupConfig(options?: EnsureStartupConfigOptions): Promise<StartupConfigWizardResult>;
|
|
14
15
|
export {};
|
|
15
16
|
//# sourceMappingURL=startup-config-wizard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"startup-config-wizard.d.ts","sourceRoot":"","sources":["../../src/services/startup-config-wizard.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"startup-config-wizard.d.ts","sourceRoot":"","sources":["../../src/services/startup-config-wizard.ts"],"names":[],"mappings":"AAuBA,UAAU,QAAQ;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,IAAI,IAAI,CAAC;CACf;AAED,UAAU,0BAA0B;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,MAAM,CAAC;CACtC;AAED,MAAM,MAAM,yBAAyB,GACjC,QAAQ,GACR,SAAS,GACT,YAAY,GACZ,SAAS,GACT,iBAAiB,GACjB,QAAQ,CAAC;AAqKb,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CAiDpC;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CA0BpC"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { createInterface } from "node:readline/promises";
|
|
6
5
|
import { stdin as input, stdout as output } from "node:process";
|
|
7
|
-
import {
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
8
7
|
import { logger } from "../utils/logger.js";
|
|
8
|
+
import { getAutoRegisterConfigFilePath } from "./auto-register.js";
|
|
9
9
|
function normalizeAnswer(answer) {
|
|
10
10
|
return answer.trim().toLowerCase();
|
|
11
11
|
}
|
|
@@ -19,6 +19,22 @@ function writeConfigFile(configPath, config) {
|
|
|
19
19
|
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
20
20
|
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
21
21
|
}
|
|
22
|
+
function readExistingConfig(configPath) {
|
|
23
|
+
if (!existsSync(configPath)) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
28
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
29
|
+
? parsed
|
|
30
|
+
: {};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const err = error;
|
|
34
|
+
logger.warn(`[runtime-bridge] 读取现有配置失败,将使用空默认值: ${err.message}`);
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
22
38
|
function generateDefaultConnectionKey() {
|
|
23
39
|
return `awsb_${randomBytes(24).toString("base64url")}`;
|
|
24
40
|
}
|
|
@@ -32,15 +48,20 @@ function writeSkippedConfig(configPath, generateConnectionKey) {
|
|
|
32
48
|
output.write(`${message}\n`);
|
|
33
49
|
}
|
|
34
50
|
async function askOptional(prompt, label, defaultValue) {
|
|
35
|
-
const answer = await prompt.question(`${label}
|
|
51
|
+
const answer = await prompt.question(`${label}(默认使用当前值 ${defaultValue},直接回车保留): `);
|
|
36
52
|
return answer.trim() || defaultValue;
|
|
37
53
|
}
|
|
38
|
-
async function askRequired(prompt, label) {
|
|
54
|
+
async function askRequired(prompt, label, currentValue) {
|
|
39
55
|
for (;;) {
|
|
40
|
-
const answer = (await prompt.question(
|
|
56
|
+
const answer = (await prompt.question(currentValue
|
|
57
|
+
? `${label}(默认使用当前值 ${currentValue},直接回车保留): `
|
|
58
|
+
: `${label}: `)).trim();
|
|
41
59
|
if (answer) {
|
|
42
60
|
return answer;
|
|
43
61
|
}
|
|
62
|
+
if (currentValue) {
|
|
63
|
+
return currentValue;
|
|
64
|
+
}
|
|
44
65
|
output.write(`${label} 不能为空,请重新输入。\n`);
|
|
45
66
|
}
|
|
46
67
|
}
|
|
@@ -56,19 +77,22 @@ async function askShouldConfigure(prompt) {
|
|
|
56
77
|
output.write("请输入 Y 继续配置,或输入 n 跳过。\n");
|
|
57
78
|
}
|
|
58
79
|
}
|
|
59
|
-
async function collectStartupConfig(prompt) {
|
|
80
|
+
async function collectStartupConfig(prompt, existingConfig = {}) {
|
|
81
|
+
const existingTarget = existingConfig.autoRegisterTargets?.[0] || {};
|
|
60
82
|
const defaultServerUrl = "http://127.0.0.1:8080";
|
|
61
83
|
const defaultInstanceName = os.hostname() || "AgentsWork Runtime Bridge";
|
|
62
84
|
const defaultRegisterIp = "127.0.0.1";
|
|
63
|
-
const connectionKey = await askRequired(prompt, "connectionKey(面板连接 Bridge 的密钥)");
|
|
64
|
-
const serverUrl = await askOptional(prompt, "serverUrl(调度中心地址)", defaultServerUrl);
|
|
65
|
-
const instanceName = await askOptional(prompt, "instanceName(实例名称)", defaultInstanceName);
|
|
66
|
-
const userKey = await askRequired(prompt, "userKey(用户 API Key)");
|
|
67
|
-
const registerIp = await askOptional(prompt, "registerIp(Bridge 对外访问 IP)", defaultRegisterIp);
|
|
85
|
+
const connectionKey = await askRequired(prompt, "connectionKey(面板连接 Bridge 的密钥)", existingConfig.connectionKey);
|
|
86
|
+
const serverUrl = await askOptional(prompt, "serverUrl(调度中心地址)", existingTarget.serverUrl || defaultServerUrl);
|
|
87
|
+
const instanceName = await askOptional(prompt, "instanceName(实例名称)", existingTarget.instanceName || defaultInstanceName);
|
|
88
|
+
const userKey = await askRequired(prompt, "userKey(用户 API Key)", existingTarget.userKey);
|
|
89
|
+
const registerIp = await askOptional(prompt, "registerIp(Bridge 对外访问 IP)", existingTarget.registerIp || defaultRegisterIp);
|
|
68
90
|
return {
|
|
91
|
+
...existingConfig,
|
|
69
92
|
connectionKey,
|
|
70
93
|
autoRegisterTargets: [
|
|
71
94
|
{
|
|
95
|
+
...existingTarget,
|
|
72
96
|
serverUrl,
|
|
73
97
|
instanceName,
|
|
74
98
|
userKey,
|
|
@@ -77,6 +101,12 @@ async function collectStartupConfig(prompt) {
|
|
|
77
101
|
],
|
|
78
102
|
};
|
|
79
103
|
}
|
|
104
|
+
async function createPrompt(options) {
|
|
105
|
+
return {
|
|
106
|
+
prompt: options.prompt || createInterface({ input, output, terminal: true }),
|
|
107
|
+
shouldClosePrompt: !options.prompt,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
80
110
|
export async function ensureStartupConfig(options = {}) {
|
|
81
111
|
const configPath = options.configPath || getAutoRegisterConfigFilePath();
|
|
82
112
|
const generateConnectionKey = options.generateConnectionKey || generateDefaultConnectionKey;
|
|
@@ -95,8 +125,7 @@ export async function ensureStartupConfig(options = {}) {
|
|
|
95
125
|
logger.info(`[runtime-bridge] 未检测到配置文件: ${configPath};当前为非交互环境,跳过首次配置引导`);
|
|
96
126
|
return "non-interactive";
|
|
97
127
|
}
|
|
98
|
-
const
|
|
99
|
-
const shouldClosePrompt = !options.prompt;
|
|
128
|
+
const { prompt, shouldClosePrompt } = await createPrompt(options);
|
|
100
129
|
try {
|
|
101
130
|
const shouldConfigure = await askShouldConfigure(prompt);
|
|
102
131
|
if (!shouldConfigure) {
|
|
@@ -120,3 +149,29 @@ export async function ensureStartupConfig(options = {}) {
|
|
|
120
149
|
}
|
|
121
150
|
}
|
|
122
151
|
}
|
|
152
|
+
export async function configureStartupConfig(options = {}) {
|
|
153
|
+
const configPath = options.configPath || getAutoRegisterConfigFilePath();
|
|
154
|
+
const interactive = options.interactive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
155
|
+
if (!interactive) {
|
|
156
|
+
logger.warn("[runtime-bridge] awsb config 需要交互式终端");
|
|
157
|
+
return "failed";
|
|
158
|
+
}
|
|
159
|
+
const { prompt, shouldClosePrompt } = await createPrompt(options);
|
|
160
|
+
try {
|
|
161
|
+
const existingConfig = readExistingConfig(configPath);
|
|
162
|
+
const config = await collectStartupConfig(prompt, existingConfig);
|
|
163
|
+
writeConfigFile(configPath, config);
|
|
164
|
+
logger.info(`[runtime-bridge] 已更新配置文件: ${configPath}`);
|
|
165
|
+
return "configured";
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const err = error;
|
|
169
|
+
logger.warn(`[runtime-bridge] 配置引导失败: ${err.message}`);
|
|
170
|
+
return "failed";
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
if (shouldClosePrompt) {
|
|
174
|
+
prompt.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -2,14 +2,16 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { ensureStartupConfig } from "./startup-config-wizard.js";
|
|
5
|
+
import { configureStartupConfig, ensureStartupConfig, } from "./startup-config-wizard.js";
|
|
6
6
|
class FakePrompt {
|
|
7
7
|
constructor(answers) {
|
|
8
8
|
this.answers = answers;
|
|
9
9
|
this.index = 0;
|
|
10
10
|
this.closed = false;
|
|
11
|
+
this.prompts = [];
|
|
11
12
|
}
|
|
12
|
-
async question() {
|
|
13
|
+
async question(prompt) {
|
|
14
|
+
this.prompts.push(prompt);
|
|
13
15
|
return this.answers[this.index++] ?? "";
|
|
14
16
|
}
|
|
15
17
|
close() {
|
|
@@ -100,4 +102,83 @@ describe("startup config wizard", () => {
|
|
|
100
102
|
connectionKey: "awsb_env_skip_key",
|
|
101
103
|
});
|
|
102
104
|
});
|
|
105
|
+
it("reconfigures existing config and keeps current values on empty input", async () => {
|
|
106
|
+
const configPath = createConfigPath();
|
|
107
|
+
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
108
|
+
writeFileSync(configPath, JSON.stringify({
|
|
109
|
+
connectionKey: "old-connection-key",
|
|
110
|
+
autoRegisterTargets: [
|
|
111
|
+
{
|
|
112
|
+
serverUrl: "http://old-server:7380",
|
|
113
|
+
instanceName: "old-instance",
|
|
114
|
+
userKey: "old-user-key",
|
|
115
|
+
registerIp: "10.0.0.8",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
}, null, 2), "utf-8");
|
|
119
|
+
const prompt = new FakePrompt(["", "", "", "", ""]);
|
|
120
|
+
const result = await configureStartupConfig({
|
|
121
|
+
configPath,
|
|
122
|
+
interactive: true,
|
|
123
|
+
prompt,
|
|
124
|
+
});
|
|
125
|
+
expect(result).toBe("configured");
|
|
126
|
+
expect(JSON.parse(readFileSync(configPath, "utf-8"))).toEqual({
|
|
127
|
+
connectionKey: "old-connection-key",
|
|
128
|
+
autoRegisterTargets: [
|
|
129
|
+
{
|
|
130
|
+
serverUrl: "http://old-server:7380",
|
|
131
|
+
instanceName: "old-instance",
|
|
132
|
+
userKey: "old-user-key",
|
|
133
|
+
registerIp: "10.0.0.8",
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
expect(prompt.prompts).toEqual([
|
|
138
|
+
"connectionKey(面板连接 Bridge 的密钥)(默认使用当前值 old-connection-key,直接回车保留): ",
|
|
139
|
+
"serverUrl(调度中心地址)(默认使用当前值 http://old-server:7380,直接回车保留): ",
|
|
140
|
+
"instanceName(实例名称)(默认使用当前值 old-instance,直接回车保留): ",
|
|
141
|
+
"userKey(用户 API Key)(默认使用当前值 old-user-key,直接回车保留): ",
|
|
142
|
+
"registerIp(Bridge 对外访问 IP)(默认使用当前值 10.0.0.8,直接回车保留): ",
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
it("reconfigures existing config with changed values", async () => {
|
|
146
|
+
const configPath = createConfigPath();
|
|
147
|
+
mkdirSync(path.dirname(configPath), { recursive: true });
|
|
148
|
+
writeFileSync(configPath, JSON.stringify({
|
|
149
|
+
connectionKey: "old-connection-key",
|
|
150
|
+
autoRegisterTargets: [
|
|
151
|
+
{
|
|
152
|
+
serverUrl: "http://old-server:7380",
|
|
153
|
+
instanceName: "old-instance",
|
|
154
|
+
userKey: "old-user-key",
|
|
155
|
+
registerIp: "10.0.0.8",
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
}), "utf-8");
|
|
159
|
+
const prompt = new FakePrompt([
|
|
160
|
+
"new-connection-key",
|
|
161
|
+
"http://new-server:7380",
|
|
162
|
+
"new-instance",
|
|
163
|
+
"new-user-key",
|
|
164
|
+
"10.0.0.9",
|
|
165
|
+
]);
|
|
166
|
+
const result = await configureStartupConfig({
|
|
167
|
+
configPath,
|
|
168
|
+
interactive: true,
|
|
169
|
+
prompt,
|
|
170
|
+
});
|
|
171
|
+
expect(result).toBe("configured");
|
|
172
|
+
expect(JSON.parse(readFileSync(configPath, "utf-8"))).toEqual({
|
|
173
|
+
connectionKey: "new-connection-key",
|
|
174
|
+
autoRegisterTargets: [
|
|
175
|
+
{
|
|
176
|
+
serverUrl: "http://new-server:7380",
|
|
177
|
+
instanceName: "new-instance",
|
|
178
|
+
userKey: "new-user-key",
|
|
179
|
+
registerIp: "10.0.0.9",
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
});
|
|
103
184
|
});
|