aws-runtime-bridge 1.3.2 → 1.3.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/README.md +17 -1
- package/dist/adapter/CodexSdkAdapter.d.ts +6 -0
- package/dist/adapter/CodexSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/CodexSdkAdapter.js +41 -3
- package/dist/adapter/OpencodeSdkAdapter.d.ts +6 -0
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +44 -7
- package/dist/services/cli-commands.d.ts +7 -0
- package/dist/services/cli-commands.d.ts.map +1 -1
- package/dist/services/cli-commands.js +214 -4
- package/dist/services/cli-commands.test.js +140 -2
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ npm install -g .
|
|
|
23
23
|
|
|
24
24
|
## 启动
|
|
25
25
|
|
|
26
|
-
首次运行 `awsb` / `aws-bridge` 时,如果不存在 `~/.aws-bridge/config.json`,CLI 会进入交互式配置引导;也可以在提示中选择跳过。跳过时仍会创建配置文件并自动生成随机 `connectionKey`,终端会输出该密钥,请保存后在 server/面板连接此 Bridge 时使用。非交互环境(如 systemd、CI、Docker
|
|
26
|
+
首次运行 `awsb` / `aws-bridge` 时,如果不存在 `~/.aws-bridge/config.json`,CLI 会进入交互式配置引导;也可以在提示中选择跳过。跳过时仍会创建配置文件并自动生成随机 `connectionKey`,终端会输出该密钥,请保存后在 server/面板连接此 Bridge 时使用。非交互环境(如 systemd、CI、Docker 后台启动)不会阻塞等待输入,也会自动生成随机 `connectionKey` 并跳过引导。
|
|
27
27
|
|
|
28
28
|
引导会生成类似下面的配置:
|
|
29
29
|
|
|
@@ -63,6 +63,22 @@ AWS_CLIENT_AGENT_MCP_ARGS='[]' \
|
|
|
63
63
|
aws-bridge
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## systemd 服务管理
|
|
67
|
+
|
|
68
|
+
Linux systemd 环境中可使用 CLI 安装或卸载 `awsb.service`。安装命令会写入 systemd unit、执行 `systemctl daemon-reload`、启用开机自启,并在服务尚未运行时立即启动:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
sudo awsb service install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
卸载命令会在服务运行时先停止服务,禁用开机自启,删除 unit 文件,并重新执行 `systemctl daemon-reload`:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
sudo awsb service uninstall
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`service` 命令仅支持 Linux systemd;在其他平台会直接返回清晰错误。生成的 unit 会设置 `AWS_BRIDGE_SKIP_SETUP=true`,避免后台服务启动时阻塞在交互式配置引导。
|
|
81
|
+
|
|
66
82
|
## 关键环境变量
|
|
67
83
|
|
|
68
84
|
| 变量名 | 说明 | 默认值 |
|
|
@@ -10,6 +10,7 @@ export declare class CodexSdkAdapter extends EventEmitter implements BaseProvide
|
|
|
10
10
|
readonly providerId = "codex";
|
|
11
11
|
readonly displayName = "Codex";
|
|
12
12
|
private sessions;
|
|
13
|
+
private sdkModule?;
|
|
13
14
|
startSession(sessionId: string, config: AdapterSessionConfig): Promise<void>;
|
|
14
15
|
sendMessage(sessionId: string, message: string): Promise<void>;
|
|
15
16
|
sendConfirmation(_sessionId: string, _accept: boolean): Promise<void>;
|
|
@@ -27,6 +28,11 @@ export declare class CodexSdkAdapter extends EventEmitter implements BaseProvide
|
|
|
27
28
|
getSessionStatus(sessionId: string): SessionStatus | undefined;
|
|
28
29
|
getSessionPid(_sessionId: string): number | undefined;
|
|
29
30
|
cleanup(): void;
|
|
31
|
+
/**
|
|
32
|
+
* 按需加载 Codex SDK:只有启动 Codex provider 时才解析重依赖,
|
|
33
|
+
* 避免 bridge 基础安装默认拉取 @openai/codex 的多平台二进制包。
|
|
34
|
+
*/
|
|
35
|
+
private loadSdk;
|
|
30
36
|
private buildCodexOptions;
|
|
31
37
|
private warnUnsupportedMcpConfig;
|
|
32
38
|
private buildThreadOptions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodexSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/CodexSdkAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"CodexSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/CodexSdkAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgB3C,OAAO,KAAK,EAEV,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EAEnB,aAAa,EAEd,MAAM,YAAY,CAAC;AA0RpB,qBAAa,eAAgB,SAAQ,YAAa,YAAW,mBAAmB;IAC9E,QAAQ,CAAC,UAAU,WAAW;IAC9B,QAAQ,CAAC,WAAW,WAAW;IAE/B,OAAO,CAAC,QAAQ,CAAwC;IACxD,OAAO,CAAC,SAAS,CAAC,CAAiB;IAE7B,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmE5E,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC9D,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAKrE,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7F,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE;QAAE,gBAAgB,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAanG,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlD,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBlD,aAAa,CACjB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,EACzB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IA4BhB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,EAAE;IAIzD,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAItC,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAK3D,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAI9D,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIrD,OAAO,IAAI,IAAI;IAOf;;;OAGG;YACW,OAAO;IA6BrB,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,wBAAwB;IAYhC,OAAO,CAAC,kBAAkB;YAqCZ,OAAO;IAiCrB,OAAO,CAAC,iBAAiB;IAgCzB,OAAO,CAAC,gBAAgB;IA+ExB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,kBAAkB;IAe1B,OAAO,CAAC,kBAAkB;IAuD1B,OAAO,CAAC,YAAY;IA2BpB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,cAAc;YAcR,cAAc;IAuB5B,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,2BAA2B;CAOpC"}
|
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
* 将 Codex 事件流映射到 runtime-bridge 统一 ProviderEvent。
|
|
6
6
|
*/
|
|
7
7
|
import { EventEmitter } from 'node:events';
|
|
8
|
-
import { Codex, } from '@openai/codex-sdk';
|
|
9
8
|
import { v4 as uuidv4 } from 'uuid';
|
|
10
9
|
import { getToolActionInfo } from './types.js';
|
|
10
|
+
function hasErrorCode(error) {
|
|
11
|
+
return error instanceof Error && 'code' in error && typeof error.code === 'string';
|
|
12
|
+
}
|
|
13
|
+
function isMissingPackageError(error, packageName) {
|
|
14
|
+
return hasErrorCode(error) && error.code === 'ERR_MODULE_NOT_FOUND' && error.message.includes(packageName);
|
|
15
|
+
}
|
|
11
16
|
const CODEX_IDLE_THRESHOLD_MS = 1500;
|
|
12
17
|
function isRecord(value) {
|
|
13
18
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -243,7 +248,8 @@ export class CodexSdkAdapter extends EventEmitter {
|
|
|
243
248
|
totalUsage: { inputTokens: 0, outputTokens: 0 },
|
|
244
249
|
};
|
|
245
250
|
try {
|
|
246
|
-
const
|
|
251
|
+
const sdk = await this.loadSdk();
|
|
252
|
+
const codex = new sdk.Codex(this.buildCodexOptions(config));
|
|
247
253
|
this.warnUnsupportedMcpConfig(config);
|
|
248
254
|
const thread = codex.startThread(this.buildThreadOptions(config));
|
|
249
255
|
const entry = {
|
|
@@ -365,7 +371,8 @@ export class CodexSdkAdapter extends EventEmitter {
|
|
|
365
371
|
this.sessions.delete(sessionId);
|
|
366
372
|
}
|
|
367
373
|
async resumeSession(sessionId, providerSessionId, config) {
|
|
368
|
-
const
|
|
374
|
+
const sdk = await this.loadSdk();
|
|
375
|
+
const codex = new sdk.Codex(this.buildCodexOptions(config));
|
|
369
376
|
this.warnUnsupportedMcpConfig(config);
|
|
370
377
|
const thread = codex.resumeThread(providerSessionId, this.buildThreadOptions(config));
|
|
371
378
|
const adapterSession = {
|
|
@@ -411,6 +418,37 @@ export class CodexSdkAdapter extends EventEmitter {
|
|
|
411
418
|
}
|
|
412
419
|
this.sessions.clear();
|
|
413
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* 按需加载 Codex SDK:只有启动 Codex provider 时才解析重依赖,
|
|
423
|
+
* 避免 bridge 基础安装默认拉取 @openai/codex 的多平台二进制包。
|
|
424
|
+
*/
|
|
425
|
+
async loadSdk() {
|
|
426
|
+
if (!this.sdkModule) {
|
|
427
|
+
try {
|
|
428
|
+
this.sdkModule = await import('@openai/codex-sdk');
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
if (isMissingPackageError(error, '@openai/codex-sdk')) {
|
|
432
|
+
throw new Error([
|
|
433
|
+
'Codex SDK provider is not installed.',
|
|
434
|
+
'',
|
|
435
|
+
'aws-runtime-bridge keeps heavy provider SDKs optional to reduce global install size.',
|
|
436
|
+
'To use SDK mode with command "codex", install:',
|
|
437
|
+
'',
|
|
438
|
+
' npm install -g @openai/codex-sdk',
|
|
439
|
+
'',
|
|
440
|
+
'If aws-runtime-bridge is installed locally, install the SDK in the same project:',
|
|
441
|
+
'',
|
|
442
|
+
' npm install @openai/codex-sdk',
|
|
443
|
+
'',
|
|
444
|
+
`Original error: ${error instanceof Error ? error.message : String(error)}`,
|
|
445
|
+
].join('\n'));
|
|
446
|
+
}
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return this.sdkModule;
|
|
451
|
+
}
|
|
414
452
|
buildCodexOptions(config) {
|
|
415
453
|
const env = toEnvRecord(process.env, config.envOverrides);
|
|
416
454
|
const options = { env };
|
|
@@ -16,6 +16,7 @@ export declare class OpencodeSdkAdapter extends EventEmitter implements BaseProv
|
|
|
16
16
|
readonly providerId = "opencode";
|
|
17
17
|
readonly displayName = "OpenCode";
|
|
18
18
|
private sessions;
|
|
19
|
+
private sdkModule?;
|
|
19
20
|
startSession(sessionId: string, config: AdapterSessionConfig): Promise<void>;
|
|
20
21
|
sendMessage(sessionId: string, message: string): Promise<void>;
|
|
21
22
|
sendConfirmation(sessionId: string, accept: boolean): Promise<void>;
|
|
@@ -32,6 +33,11 @@ export declare class OpencodeSdkAdapter extends EventEmitter implements BaseProv
|
|
|
32
33
|
getSessionStatus(sessionId: string): SessionStatus | undefined;
|
|
33
34
|
getSessionPid(sessionId: string): number | undefined;
|
|
34
35
|
cleanup(): void;
|
|
36
|
+
/**
|
|
37
|
+
* 按需加载 OpenCode SDK:只有启动 OpenCode provider 时才解析依赖,
|
|
38
|
+
* 避免 bridge 基础安装默认携带所有可选 runtime SDK。
|
|
39
|
+
*/
|
|
40
|
+
private loadSdk;
|
|
35
41
|
private waitForServer;
|
|
36
42
|
private startSseLoop;
|
|
37
43
|
private runSseLoop;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OpencodeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/OpencodeSdkAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"OpencodeSdkAdapter.d.ts","sourceRoot":"","sources":["../../src/adapter/OpencodeSdkAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAM3C,OAAO,KAAK,EACV,mBAAmB,EACnB,oBAAoB,EAGpB,mBAAmB,EACnB,aAAa,EACd,MAAM,YAAY,CAAC;AAsGpB,qBAAa,kBAAmB,SAAQ,YAAa,YAAW,mBAAmB;IACjF,QAAQ,CAAC,UAAU,cAAc;IACjC,QAAQ,CAAC,WAAW,cAAc;IAElC,OAAO,CAAC,QAAQ,CAA2C;IAC3D,OAAO,CAAC,SAAS,CAAC,CAAoB;IAEhC,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgI5E,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwC9D,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBnE,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7F,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE;QAAE,gBAAgB,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAOnG,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAclD,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BxD,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,EAAE;IAIzD,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAItC,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI3D,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAI9D,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAOpD,OAAO,IAAI,IAAI;IAaf;;;OAGG;YACW,OAAO;YA6BP,aAAa;IAmB3B,OAAO,CAAC,YAAY;YAYN,UAAU;IAuBxB,OAAO,CAAC,mBAAmB;IA6H3B,OAAO,CAAC,gBAAgB;IA6GxB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,2BAA2B;CAOpC"}
|
|
@@ -10,14 +10,13 @@
|
|
|
10
10
|
*
|
|
11
11
|
* 参考:spectrai-community/src/main/adapter/OpenCodeSdkAdapter.ts
|
|
12
12
|
*/
|
|
13
|
-
import { spawn } from 'child_process';
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
14
|
import { EventEmitter } from 'node:events';
|
|
15
|
-
import * as fs from 'fs';
|
|
16
|
-
import * as net from 'net';
|
|
17
|
-
import * as os from 'os';
|
|
18
|
-
import * as path from 'path';
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as net from 'node:net';
|
|
17
|
+
import * as os from 'node:os';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
19
|
import { v4 as uuidv4 } from 'uuid';
|
|
20
|
-
import { createOpencodeClient, } from '@opencode-ai/sdk';
|
|
21
20
|
import { getToolActionInfo } from './types.js';
|
|
22
21
|
// ============ 可执行文件查找 ============
|
|
23
22
|
/**
|
|
@@ -77,6 +76,12 @@ async function findAvailablePort(startPort = 14096) {
|
|
|
77
76
|
}
|
|
78
77
|
throw new Error(`No available port found in range ${startPort}–${startPort + 99}`);
|
|
79
78
|
}
|
|
79
|
+
function hasErrorCode(error) {
|
|
80
|
+
return error instanceof Error && 'code' in error && typeof error.code === 'string';
|
|
81
|
+
}
|
|
82
|
+
function isMissingPackageError(error, packageName) {
|
|
83
|
+
return hasErrorCode(error) && error.code === 'ERR_MODULE_NOT_FOUND' && error.message.includes(packageName);
|
|
84
|
+
}
|
|
80
85
|
// ============ Adapter ============
|
|
81
86
|
export class OpencodeSdkAdapter extends EventEmitter {
|
|
82
87
|
constructor() {
|
|
@@ -151,7 +156,8 @@ export class OpencodeSdkAdapter extends EventEmitter {
|
|
|
151
156
|
session.sseActive = false;
|
|
152
157
|
});
|
|
153
158
|
// 步骤 3:创建 SDK 客户端
|
|
154
|
-
const
|
|
159
|
+
const sdk = await this.loadSdk();
|
|
160
|
+
const client = sdk.createOpencodeClient({
|
|
155
161
|
baseUrl: `http://127.0.0.1:${port}`,
|
|
156
162
|
});
|
|
157
163
|
session.client = client;
|
|
@@ -330,6 +336,37 @@ export class OpencodeSdkAdapter extends EventEmitter {
|
|
|
330
336
|
this.sessions.clear();
|
|
331
337
|
}
|
|
332
338
|
// ============ 私有方法 ============
|
|
339
|
+
/**
|
|
340
|
+
* 按需加载 OpenCode SDK:只有启动 OpenCode provider 时才解析依赖,
|
|
341
|
+
* 避免 bridge 基础安装默认携带所有可选 runtime SDK。
|
|
342
|
+
*/
|
|
343
|
+
async loadSdk() {
|
|
344
|
+
if (!this.sdkModule) {
|
|
345
|
+
try {
|
|
346
|
+
this.sdkModule = await import('@opencode-ai/sdk');
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
if (isMissingPackageError(error, '@opencode-ai/sdk')) {
|
|
350
|
+
throw new Error([
|
|
351
|
+
'OpenCode SDK provider is not installed.',
|
|
352
|
+
'',
|
|
353
|
+
'aws-runtime-bridge keeps Codex/OpenCode provider SDKs optional to reduce default install size.',
|
|
354
|
+
'To use SDK mode with command "opencode", install:',
|
|
355
|
+
'',
|
|
356
|
+
' npm install -g @opencode-ai/sdk',
|
|
357
|
+
'',
|
|
358
|
+
'If aws-runtime-bridge is installed locally, install the SDK in the same project:',
|
|
359
|
+
'',
|
|
360
|
+
' npm install @opencode-ai/sdk',
|
|
361
|
+
'',
|
|
362
|
+
`Original error: ${error instanceof Error ? error.message : String(error)}`,
|
|
363
|
+
].join('\n'));
|
|
364
|
+
}
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return this.sdkModule;
|
|
369
|
+
}
|
|
333
370
|
async waitForServer(client, directory, timeoutMs, port) {
|
|
334
371
|
const deadline = Date.now() + timeoutMs;
|
|
335
372
|
while (Date.now() < deadline) {
|
|
@@ -14,7 +14,14 @@ export interface CliCommandOptions {
|
|
|
14
14
|
stderr?: (message: string) => void;
|
|
15
15
|
runCommand?: (command: string, args: string[]) => CommandRunResult;
|
|
16
16
|
configure?: () => Promise<StartupConfigWizardResult>;
|
|
17
|
+
platform?: NodeJS.Platform;
|
|
18
|
+
serviceUnitPath?: string;
|
|
19
|
+
executablePath?: string;
|
|
17
20
|
}
|
|
18
21
|
export declare function readPackageVersion(packageRoot?: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* 生成 systemd unit 内容,主流程是用当前 Node 可执行文件启动当前 CLI 入口。
|
|
24
|
+
*/
|
|
25
|
+
export declare function createSystemdUnitContent(executablePath: string): string;
|
|
19
26
|
export declare function handleCliCommand(options?: CliCommandOptions): Promise<CliCommandResult>;
|
|
20
27
|
//# sourceMappingURL=cli-commands.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-commands.d.ts","sourceRoot":"","sources":["../../src/services/cli-commands.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli-commands.d.ts","sourceRoot":"","sources":["../../src/services/cli-commands.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAM5E,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;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAYD,wBAAgB,kBAAkB,CAAC,WAAW,SAA0B,GAAG,MAAM,CAMhF;AAgCD;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAsBvE;AAwOD,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CA+C3B"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { configureStartupConfig } from "./startup-config-wizard.js";
|
|
6
|
+
const SYSTEMD_SERVICE_NAME = "awsb.service";
|
|
7
|
+
const DEFAULT_SYSTEMD_UNIT_PATH = `/etc/systemd/system/${SYSTEMD_SERVICE_NAME}`;
|
|
6
8
|
function getDefaultPackageRoot() {
|
|
7
9
|
const currentFile = fileURLToPath(import.meta.url);
|
|
8
10
|
return path.resolve(path.dirname(currentFile), "..", "..");
|
|
@@ -19,14 +21,215 @@ function defaultRunCommand(command, args) {
|
|
|
19
21
|
});
|
|
20
22
|
return { status: result.status, error: result.error };
|
|
21
23
|
}
|
|
24
|
+
function defaultStdout(message) {
|
|
25
|
+
process.stdout.write(`${message}\n`);
|
|
26
|
+
}
|
|
27
|
+
function defaultStderr(message) {
|
|
28
|
+
process.stderr.write(`${message}\n`);
|
|
29
|
+
}
|
|
22
30
|
function isVersionCommand(command) {
|
|
23
31
|
return command === "-v" || command === "--version" || command === "version";
|
|
24
32
|
}
|
|
33
|
+
function quoteSystemdExecArg(value) {
|
|
34
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
35
|
+
}
|
|
36
|
+
function getDefaultExecutablePath() {
|
|
37
|
+
return process.argv[1]
|
|
38
|
+
? path.resolve(process.argv[1])
|
|
39
|
+
: fileURLToPath(import.meta.url);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 生成 systemd unit 内容,主流程是用当前 Node 可执行文件启动当前 CLI 入口。
|
|
43
|
+
*/
|
|
44
|
+
export function createSystemdUnitContent(executablePath) {
|
|
45
|
+
const execStart = [process.execPath, executablePath]
|
|
46
|
+
.map((part) => quoteSystemdExecArg(part))
|
|
47
|
+
.join(" ");
|
|
48
|
+
return [
|
|
49
|
+
"[Unit]",
|
|
50
|
+
"Description=AgentsWorkStudio Runtime Bridge",
|
|
51
|
+
"After=network-online.target",
|
|
52
|
+
"Wants=network-online.target",
|
|
53
|
+
"",
|
|
54
|
+
"[Service]",
|
|
55
|
+
"Type=simple",
|
|
56
|
+
"Environment=AWS_BRIDGE_SKIP_SETUP=true",
|
|
57
|
+
`ExecStart=${execStart}`,
|
|
58
|
+
"Restart=on-failure",
|
|
59
|
+
"RestartSec=5s",
|
|
60
|
+
"",
|
|
61
|
+
"[Install]",
|
|
62
|
+
"WantedBy=multi-user.target",
|
|
63
|
+
"",
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
function runSystemctl(runCommand, args) {
|
|
67
|
+
return runCommand("systemctl", args);
|
|
68
|
+
}
|
|
69
|
+
function systemctlFailureMessage(action, result) {
|
|
70
|
+
if (result.error) {
|
|
71
|
+
return `[runtime-bridge] ${action}失败: ${result.error.message}`;
|
|
72
|
+
}
|
|
73
|
+
return `[runtime-bridge] ${action}失败,systemctl 退出码: ${result.status ?? "unknown"}`;
|
|
74
|
+
}
|
|
75
|
+
function commandSucceeded(result) {
|
|
76
|
+
return !result.error && result.status === 0;
|
|
77
|
+
}
|
|
78
|
+
function ensureLinuxSystemd(platform, stderr) {
|
|
79
|
+
if (platform === "linux") {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
stderr("[runtime-bridge] service 命令仅支持 Linux systemd 环境。");
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
function printServiceUsage(stdout) {
|
|
86
|
+
stdout("Usage: awsb service <install|uninstall>");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 安装 systemd 服务,主干流程是写入 unit、重载 systemd、启用开机自启,并在未运行时启动服务。
|
|
90
|
+
*/
|
|
91
|
+
function installSystemdService(options) {
|
|
92
|
+
if (!ensureLinuxSystemd(options.platform, options.stderr)) {
|
|
93
|
+
return { handled: true, exitCode: 1 };
|
|
94
|
+
}
|
|
95
|
+
const unitContent = createSystemdUnitContent(options.executablePath);
|
|
96
|
+
try {
|
|
97
|
+
mkdirSync(path.dirname(options.serviceUnitPath), { recursive: true });
|
|
98
|
+
writeFileSync(options.serviceUnitPath, unitContent, {
|
|
99
|
+
encoding: "utf-8",
|
|
100
|
+
mode: 0o644,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
105
|
+
options.stderr(`[runtime-bridge] 写入 systemd unit 失败: ${err.message}`);
|
|
106
|
+
return { handled: true, exitCode: 1 };
|
|
107
|
+
}
|
|
108
|
+
const daemonReloadResult = runSystemctl(options.runCommand, ["daemon-reload"]);
|
|
109
|
+
if (!commandSucceeded(daemonReloadResult)) {
|
|
110
|
+
options.stderr(systemctlFailureMessage("重载 systemd", daemonReloadResult));
|
|
111
|
+
return { handled: true, exitCode: daemonReloadResult.status ?? 1 };
|
|
112
|
+
}
|
|
113
|
+
const enableResult = runSystemctl(options.runCommand, [
|
|
114
|
+
"enable",
|
|
115
|
+
SYSTEMD_SERVICE_NAME,
|
|
116
|
+
]);
|
|
117
|
+
if (!commandSucceeded(enableResult)) {
|
|
118
|
+
options.stderr(systemctlFailureMessage("启用 awsb 开机自启", enableResult));
|
|
119
|
+
return { handled: true, exitCode: enableResult.status ?? 1 };
|
|
120
|
+
}
|
|
121
|
+
const activeResult = runSystemctl(options.runCommand, [
|
|
122
|
+
"is-active",
|
|
123
|
+
"--quiet",
|
|
124
|
+
SYSTEMD_SERVICE_NAME,
|
|
125
|
+
]);
|
|
126
|
+
if (activeResult.error) {
|
|
127
|
+
options.stderr(systemctlFailureMessage("检查 awsb 服务状态", activeResult));
|
|
128
|
+
return { handled: true, exitCode: 1 };
|
|
129
|
+
}
|
|
130
|
+
if (activeResult.status !== 0) {
|
|
131
|
+
const startResult = runSystemctl(options.runCommand, [
|
|
132
|
+
"start",
|
|
133
|
+
SYSTEMD_SERVICE_NAME,
|
|
134
|
+
]);
|
|
135
|
+
if (!commandSucceeded(startResult)) {
|
|
136
|
+
options.stderr(systemctlFailureMessage("启动 awsb 服务", startResult));
|
|
137
|
+
return { handled: true, exitCode: startResult.status ?? 1 };
|
|
138
|
+
}
|
|
139
|
+
options.stdout("[runtime-bridge] awsb systemd 服务已安装、已启用开机自启并已启动。");
|
|
140
|
+
return { handled: true, exitCode: 0 };
|
|
141
|
+
}
|
|
142
|
+
options.stdout("[runtime-bridge] awsb systemd 服务已安装并已启用开机自启;服务已在运行。");
|
|
143
|
+
return { handled: true, exitCode: 0 };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 卸载 systemd 服务,主干流程是按需停止、禁用自启、删除 unit,并重载 systemd。
|
|
147
|
+
*/
|
|
148
|
+
function uninstallSystemdService(options) {
|
|
149
|
+
if (!ensureLinuxSystemd(options.platform, options.stderr)) {
|
|
150
|
+
return { handled: true, exitCode: 1 };
|
|
151
|
+
}
|
|
152
|
+
const unitExists = existsSync(options.serviceUnitPath);
|
|
153
|
+
const activeResult = runSystemctl(options.runCommand, [
|
|
154
|
+
"is-active",
|
|
155
|
+
"--quiet",
|
|
156
|
+
SYSTEMD_SERVICE_NAME,
|
|
157
|
+
]);
|
|
158
|
+
if (activeResult.error) {
|
|
159
|
+
options.stderr(systemctlFailureMessage("检查 awsb 服务状态", activeResult));
|
|
160
|
+
return { handled: true, exitCode: 1 };
|
|
161
|
+
}
|
|
162
|
+
if (activeResult.status === 0) {
|
|
163
|
+
const stopResult = runSystemctl(options.runCommand, [
|
|
164
|
+
"stop",
|
|
165
|
+
SYSTEMD_SERVICE_NAME,
|
|
166
|
+
]);
|
|
167
|
+
if (!commandSucceeded(stopResult)) {
|
|
168
|
+
options.stderr(systemctlFailureMessage("停止 awsb 服务", stopResult));
|
|
169
|
+
return { handled: true, exitCode: stopResult.status ?? 1 };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const disableResult = runSystemctl(options.runCommand, [
|
|
173
|
+
"disable",
|
|
174
|
+
SYSTEMD_SERVICE_NAME,
|
|
175
|
+
]);
|
|
176
|
+
if (disableResult.error || (disableResult.status !== 0 && unitExists)) {
|
|
177
|
+
options.stderr(systemctlFailureMessage("禁用 awsb 开机自启", disableResult));
|
|
178
|
+
return { handled: true, exitCode: disableResult.status ?? 1 };
|
|
179
|
+
}
|
|
180
|
+
if (unitExists) {
|
|
181
|
+
try {
|
|
182
|
+
unlinkSync(options.serviceUnitPath);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
186
|
+
options.stderr(`[runtime-bridge] 删除 systemd unit 失败: ${err.message}`);
|
|
187
|
+
return { handled: true, exitCode: 1 };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
options.stdout("[runtime-bridge] awsb systemd unit 不存在,跳过删除。");
|
|
192
|
+
}
|
|
193
|
+
const daemonReloadResult = runSystemctl(options.runCommand, ["daemon-reload"]);
|
|
194
|
+
if (!commandSucceeded(daemonReloadResult)) {
|
|
195
|
+
options.stderr(systemctlFailureMessage("重载 systemd", daemonReloadResult));
|
|
196
|
+
return { handled: true, exitCode: daemonReloadResult.status ?? 1 };
|
|
197
|
+
}
|
|
198
|
+
options.stdout("[runtime-bridge] awsb systemd 服务已卸载。");
|
|
199
|
+
return { handled: true, exitCode: 0 };
|
|
200
|
+
}
|
|
201
|
+
function handleServiceCommand(argv, options, stdout, stderr) {
|
|
202
|
+
const subcommand = argv[1];
|
|
203
|
+
if (!subcommand ||
|
|
204
|
+
subcommand === "-h" ||
|
|
205
|
+
subcommand === "--help" ||
|
|
206
|
+
subcommand === "help") {
|
|
207
|
+
printServiceUsage(stdout);
|
|
208
|
+
return { handled: true, exitCode: subcommand ? 0 : 1 };
|
|
209
|
+
}
|
|
210
|
+
const serviceOptions = {
|
|
211
|
+
stdout,
|
|
212
|
+
stderr,
|
|
213
|
+
runCommand: options.runCommand || defaultRunCommand,
|
|
214
|
+
platform: options.platform || process.platform,
|
|
215
|
+
serviceUnitPath: options.serviceUnitPath || DEFAULT_SYSTEMD_UNIT_PATH,
|
|
216
|
+
executablePath: options.executablePath || getDefaultExecutablePath(),
|
|
217
|
+
};
|
|
218
|
+
if (subcommand === "install") {
|
|
219
|
+
return installSystemdService(serviceOptions);
|
|
220
|
+
}
|
|
221
|
+
if (subcommand === "uninstall") {
|
|
222
|
+
return uninstallSystemdService(serviceOptions);
|
|
223
|
+
}
|
|
224
|
+
stderr(`[runtime-bridge] 未知 service 子命令: ${subcommand}`);
|
|
225
|
+
printServiceUsage(stdout);
|
|
226
|
+
return { handled: true, exitCode: 1 };
|
|
227
|
+
}
|
|
25
228
|
export async function handleCliCommand(options = {}) {
|
|
26
229
|
const argv = options.argv || process.argv.slice(2);
|
|
27
230
|
const command = argv[0];
|
|
28
|
-
const stdout = options.stdout ||
|
|
29
|
-
const stderr = options.stderr ||
|
|
231
|
+
const stdout = options.stdout || defaultStdout;
|
|
232
|
+
const stderr = options.stderr || defaultStderr;
|
|
30
233
|
if (isVersionCommand(command)) {
|
|
31
234
|
stdout(`aws-runtime-bridge ${readPackageVersion(options.packageRoot)}`);
|
|
32
235
|
return { handled: true, exitCode: 0 };
|
|
@@ -34,7 +237,11 @@ export async function handleCliCommand(options = {}) {
|
|
|
34
237
|
if (command === "update") {
|
|
35
238
|
stdout("[runtime-bridge] 正在更新 aws-runtime-bridge 到最新版本...");
|
|
36
239
|
const runCommand = options.runCommand || defaultRunCommand;
|
|
37
|
-
const result = runCommand("npm", [
|
|
240
|
+
const result = runCommand("npm", [
|
|
241
|
+
"install",
|
|
242
|
+
"-g",
|
|
243
|
+
"aws-runtime-bridge@latest",
|
|
244
|
+
]);
|
|
38
245
|
if (result.error) {
|
|
39
246
|
stderr(`[runtime-bridge] 更新失败: ${result.error.message}`);
|
|
40
247
|
return { handled: true, exitCode: 1 };
|
|
@@ -51,5 +258,8 @@ export async function handleCliCommand(options = {}) {
|
|
|
51
258
|
const result = await configure();
|
|
52
259
|
return { handled: true, exitCode: result === "configured" ? 0 : 1 };
|
|
53
260
|
}
|
|
261
|
+
if (command === "service") {
|
|
262
|
+
return handleServiceCommand(argv, options, stdout, stderr);
|
|
263
|
+
}
|
|
54
264
|
return { handled: false, exitCode: 0 };
|
|
55
265
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } 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 } from "vitest";
|
|
5
|
-
import { handleCliCommand, readPackageVersion } from "./cli-commands.js";
|
|
5
|
+
import { createSystemdUnitContent, handleCliCommand, readPackageVersion, } from "./cli-commands.js";
|
|
6
6
|
const tempRoots = [];
|
|
7
7
|
function createPackageRoot(version = "9.8.7") {
|
|
8
8
|
const root = mkdtempSync(path.join(os.tmpdir(), "aws-bridge-cli-"));
|
|
@@ -11,6 +11,14 @@ function createPackageRoot(version = "9.8.7") {
|
|
|
11
11
|
writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "aws-runtime-bridge", version }), "utf-8");
|
|
12
12
|
return root;
|
|
13
13
|
}
|
|
14
|
+
function createServiceUnitPath() {
|
|
15
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "aws-bridge-service-"));
|
|
16
|
+
tempRoots.push(root);
|
|
17
|
+
return path.join(root, "systemd", "awsb.service");
|
|
18
|
+
}
|
|
19
|
+
function quotedSystemdArg(value) {
|
|
20
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
21
|
+
}
|
|
14
22
|
afterEach(() => {
|
|
15
23
|
for (const root of tempRoots.splice(0)) {
|
|
16
24
|
rmSync(root, { recursive: true, force: true });
|
|
@@ -85,6 +93,136 @@ describe("cli commands", () => {
|
|
|
85
93
|
});
|
|
86
94
|
expect(result).toEqual({ handled: true, exitCode: 1 });
|
|
87
95
|
});
|
|
96
|
+
it("creates a systemd unit that starts the current executable with setup skipped", () => {
|
|
97
|
+
const executablePath = "/opt/aws-runtime-bridge/dist/index.js";
|
|
98
|
+
const unit = createSystemdUnitContent(executablePath);
|
|
99
|
+
expect(unit).toContain("Description=AgentsWorkStudio Runtime Bridge");
|
|
100
|
+
expect(unit).toContain("Environment=AWS_BRIDGE_SKIP_SETUP=true");
|
|
101
|
+
expect(unit).toContain(`ExecStart=${quotedSystemdArg(process.execPath)} ${quotedSystemdArg(executablePath)}`);
|
|
102
|
+
expect(unit).toContain("Restart=on-failure");
|
|
103
|
+
expect(unit).toContain("WantedBy=multi-user.target");
|
|
104
|
+
});
|
|
105
|
+
it("installs service, enables autostart, and starts when inactive", async () => {
|
|
106
|
+
const output = [];
|
|
107
|
+
const commands = [];
|
|
108
|
+
const serviceUnitPath = createServiceUnitPath();
|
|
109
|
+
const executablePath = "/usr/local/bin/awsb";
|
|
110
|
+
const result = await handleCliCommand({
|
|
111
|
+
argv: ["service", "install"],
|
|
112
|
+
platform: "linux",
|
|
113
|
+
serviceUnitPath,
|
|
114
|
+
executablePath,
|
|
115
|
+
stdout: (message) => output.push(message),
|
|
116
|
+
runCommand: (command, args) => {
|
|
117
|
+
commands.push({ command, args });
|
|
118
|
+
if (args[0] === "is-active") {
|
|
119
|
+
return { status: 3 };
|
|
120
|
+
}
|
|
121
|
+
return { status: 0 };
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
125
|
+
expect(readFileSync(serviceUnitPath, "utf-8")).toContain(`ExecStart=${quotedSystemdArg(process.execPath)} ${quotedSystemdArg(executablePath)}`);
|
|
126
|
+
expect(commands).toEqual([
|
|
127
|
+
{ command: "systemctl", args: ["daemon-reload"] },
|
|
128
|
+
{ command: "systemctl", args: ["enable", "awsb.service"] },
|
|
129
|
+
{ command: "systemctl", args: ["is-active", "--quiet", "awsb.service"] },
|
|
130
|
+
{ command: "systemctl", args: ["start", "awsb.service"] },
|
|
131
|
+
]);
|
|
132
|
+
expect(output.at(-1)).toBe("[runtime-bridge] awsb systemd 服务已安装、已启用开机自启并已启动。");
|
|
133
|
+
});
|
|
134
|
+
it("does not start service during install when it is already active", async () => {
|
|
135
|
+
const commands = [];
|
|
136
|
+
const serviceUnitPath = createServiceUnitPath();
|
|
137
|
+
const result = await handleCliCommand({
|
|
138
|
+
argv: ["service", "install"],
|
|
139
|
+
platform: "linux",
|
|
140
|
+
serviceUnitPath,
|
|
141
|
+
executablePath: "/usr/local/bin/awsb",
|
|
142
|
+
runCommand: (command, args) => {
|
|
143
|
+
commands.push({ command, args });
|
|
144
|
+
return { status: 0 };
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
148
|
+
expect(commands).toEqual([
|
|
149
|
+
{ command: "systemctl", args: ["daemon-reload"] },
|
|
150
|
+
{ command: "systemctl", args: ["enable", "awsb.service"] },
|
|
151
|
+
{ command: "systemctl", args: ["is-active", "--quiet", "awsb.service"] },
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
it("uninstalls service by stopping, disabling, removing unit, and reloading systemd", async () => {
|
|
155
|
+
const output = [];
|
|
156
|
+
const commands = [];
|
|
157
|
+
const serviceUnitPath = createServiceUnitPath();
|
|
158
|
+
mkdirSync(path.dirname(serviceUnitPath), { recursive: true });
|
|
159
|
+
writeFileSync(serviceUnitPath, "unit", "utf-8");
|
|
160
|
+
const result = await handleCliCommand({
|
|
161
|
+
argv: ["service", "uninstall"],
|
|
162
|
+
platform: "linux",
|
|
163
|
+
serviceUnitPath,
|
|
164
|
+
stdout: (message) => output.push(message),
|
|
165
|
+
runCommand: (command, args) => {
|
|
166
|
+
commands.push({ command, args });
|
|
167
|
+
return { status: 0 };
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
171
|
+
expect(existsSync(serviceUnitPath)).toBe(false);
|
|
172
|
+
expect(commands).toEqual([
|
|
173
|
+
{ command: "systemctl", args: ["is-active", "--quiet", "awsb.service"] },
|
|
174
|
+
{ command: "systemctl", args: ["stop", "awsb.service"] },
|
|
175
|
+
{ command: "systemctl", args: ["disable", "awsb.service"] },
|
|
176
|
+
{ command: "systemctl", args: ["daemon-reload"] },
|
|
177
|
+
]);
|
|
178
|
+
expect(output.at(-1)).toBe("[runtime-bridge] awsb systemd 服务已卸载。");
|
|
179
|
+
});
|
|
180
|
+
it("tolerates uninstall when service is inactive and unit is absent", async () => {
|
|
181
|
+
const output = [];
|
|
182
|
+
const commands = [];
|
|
183
|
+
const serviceUnitPath = createServiceUnitPath();
|
|
184
|
+
const result = await handleCliCommand({
|
|
185
|
+
argv: ["service", "uninstall"],
|
|
186
|
+
platform: "linux",
|
|
187
|
+
serviceUnitPath,
|
|
188
|
+
stdout: (message) => output.push(message),
|
|
189
|
+
runCommand: (command, args) => {
|
|
190
|
+
commands.push({ command, args });
|
|
191
|
+
if (args[0] === "is-active" || args[0] === "disable") {
|
|
192
|
+
return { status: 1 };
|
|
193
|
+
}
|
|
194
|
+
return { status: 0 };
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
198
|
+
expect(commands).toEqual([
|
|
199
|
+
{ command: "systemctl", args: ["is-active", "--quiet", "awsb.service"] },
|
|
200
|
+
{ command: "systemctl", args: ["disable", "awsb.service"] },
|
|
201
|
+
{ command: "systemctl", args: ["daemon-reload"] },
|
|
202
|
+
]);
|
|
203
|
+
expect(output).toContain("[runtime-bridge] awsb systemd unit 不存在,跳过删除。");
|
|
204
|
+
});
|
|
205
|
+
it("fails service commands clearly on non-linux platforms", async () => {
|
|
206
|
+
const errors = [];
|
|
207
|
+
const result = await handleCliCommand({
|
|
208
|
+
argv: ["service", "install"],
|
|
209
|
+
platform: "win32",
|
|
210
|
+
stderr: (message) => errors.push(message),
|
|
211
|
+
});
|
|
212
|
+
expect(result).toEqual({ handled: true, exitCode: 1 });
|
|
213
|
+
expect(errors).toEqual([
|
|
214
|
+
"[runtime-bridge] service 命令仅支持 Linux systemd 环境。",
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
it("prints service help for missing subcommand", async () => {
|
|
218
|
+
const output = [];
|
|
219
|
+
const result = await handleCliCommand({
|
|
220
|
+
argv: ["service"],
|
|
221
|
+
stdout: (message) => output.push(message),
|
|
222
|
+
});
|
|
223
|
+
expect(result).toEqual({ handled: true, exitCode: 1 });
|
|
224
|
+
expect(output).toEqual(["Usage: awsb service <install|uninstall>"]);
|
|
225
|
+
});
|
|
88
226
|
it("does not handle normal startup arguments", async () => {
|
|
89
227
|
const result = await handleCliCommand({ argv: [] });
|
|
90
228
|
expect(result).toEqual({ handled: false, exitCode: 0 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aws-runtime-bridge",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
4
4
|
"description": "AgentsWorkStudio runtime bridge service for machine-level agent runtime integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -49,8 +49,6 @@
|
|
|
49
49
|
"@anthropic-ai/claude-agent-sdk": "^0.2.87",
|
|
50
50
|
"@cc-switch/sdk": "file:package/cc-switch-sdk",
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
52
|
-
"@openai/codex-sdk": "^0.125.0",
|
|
53
|
-
"@opencode-ai/sdk": "^1.3.13",
|
|
54
52
|
"axios": "^1.7.9",
|
|
55
53
|
"cors": "^2.8.5",
|
|
56
54
|
"express": "^4.21.2",
|
|
@@ -60,8 +58,22 @@
|
|
|
60
58
|
"ws": "^8.14.2",
|
|
61
59
|
"yaml": "^2.3.4"
|
|
62
60
|
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@openai/codex-sdk": "^0.125.0",
|
|
63
|
+
"@opencode-ai/sdk": "^1.3.13"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"@openai/codex-sdk": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"@opencode-ai/sdk": {
|
|
70
|
+
"optional": true
|
|
71
|
+
}
|
|
72
|
+
},
|
|
63
73
|
"devDependencies": {
|
|
64
74
|
"@eslint/js": "^9.0.0",
|
|
75
|
+
"@openai/codex-sdk": "^0.125.0",
|
|
76
|
+
"@opencode-ai/sdk": "^1.3.13",
|
|
65
77
|
"@types/cors": "^2.8.19",
|
|
66
78
|
"@types/express": "^4.17.21",
|
|
67
79
|
"@types/js-yaml": "^4.0.9",
|