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 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;AA8CH;;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;AA8LD;;;;;;;;;;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,CAoED;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CAuBnD;AAED;;GAEG;AACH,wBAAgB,oBAAoB;gBAnwBtB,OAAO;iBACN,MAAM;mBACJ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;kBACvB,IAAI;YACV,MAAM;EAiwBf;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,CA0ED"}
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 response = await axios.post(`${schedulerBaseUrl}/api/instances/unregister`, { instanceId }, {
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 = loadConfig();
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 localIp = getLocalIpAddress();
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(`${schedulerBaseUrl}/api/instances/runtime-tokens/refresh`, {
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 = loadConfig();
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(`${schedulerBaseUrl}/api/instances/bridge-restart-cleanup`, requestBody, {
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=auto-register.test.d.ts.map
@@ -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":"AAWA,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,kBAAmB,SAAQ,iBAAiB;IAC3D,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;CAC3C;AAqBD,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAMD,wBAAgB,+BAA+B,IAAI,MAAM,GAAG,IAAI,CAsB/D;AA2ED,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,sCAA2C,GACnD,kBAAkB,CAEpB;AAED,wBAAgB,gCAAgC,CAC9C,OAAO,GAAE,sCAA2C,GACnD,kBAAkB,CA+BpB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,uBAAuB,GAC7B,kBAAkB,CAwBpB;AAED,wBAAgB,+BAA+B,IAAI,IAAI,CAgBtD"}
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 { cpSync, existsSync, mkdirSync } from "node:fs";
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(getPackageRoot(), "package", "aws-client-agent-mcp", "dist", "index.js");
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 getReleasedEntryPath() {
43
- return path.join(getReleasedMcpDistPath(), "index.js");
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
- export function releaseBundledAwsClientAgentMcp() {
46
- const bundledDist = path.dirname(getBundledEntryPath());
47
- const releasedDist = getReleasedMcpDistPath();
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
- if (!existsSync(releasedEntry)) {
53
- mkdirSync(releasedDist, { recursive: true });
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
- logger.info(`[runtime-bridge] ${AWS_MCP_SERVER_NAME} MCP 已释放到: ${releasedDist}`);
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 when present', async () => {
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli-commands.test.d.ts.map
@@ -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":"AAqBA,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,SAAS,GACT,iBAAiB,GACjB,QAAQ,CAAC;AAiGb,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CAmDpC"}
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 { getAutoRegisterConfigFilePath } from "./auto-register.js";
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} (${defaultValue}): `);
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(`${label}: `)).trim();
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 prompt = options.prompt || createInterface({ input, output, terminal: true });
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-runtime-bridge",
3
- "version": "1.1.2",
3
+ "version": "1.1.6",
4
4
  "description": "AgentsWorkStudio runtime bridge service for machine-level agent runtime integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",