aws-runtime-bridge 1.0.3 → 1.1.0

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.
Files changed (142) hide show
  1. package/dist/adapter/adapter.test.js +4 -4
  2. package/dist/adapter/types.d.ts.map +1 -1
  3. package/dist/adapter/types.js +0 -7
  4. package/dist/adapter/types.test.js +5 -53
  5. package/dist/middleware/auth.d.ts.map +1 -1
  6. package/dist/middleware/auth.js +4 -0
  7. package/dist/routes/instance.d.ts.map +1 -1
  8. package/dist/routes/instance.js +36 -0
  9. package/dist/routes/runtime-binding.d.ts.map +1 -1
  10. package/dist/routes/runtime-binding.js +45 -0
  11. package/dist/routes/sessions.js +1 -1
  12. package/dist/routes/terminal.d.ts.map +1 -1
  13. package/dist/routes/terminal.js +48 -14
  14. package/dist/routes/terminal.test.js +6 -2
  15. package/dist/services/agent-process-manager.js +4 -4
  16. package/dist/services/auto-register.d.ts +9 -0
  17. package/dist/services/auto-register.d.ts.map +1 -1
  18. package/dist/services/auto-register.js +190 -32
  19. package/dist/services/aws-client-agent-mcp.test.js +3 -0
  20. package/dist/services/mcp-launch-binding-queue.d.ts +36 -0
  21. package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -0
  22. package/dist/services/mcp-launch-binding-queue.js +92 -0
  23. package/dist/services/mcp-launch-binding-queue.test.d.ts +2 -0
  24. package/dist/services/mcp-launch-binding-queue.test.d.ts.map +1 -0
  25. package/dist/services/mcp-launch-binding-queue.test.js +107 -0
  26. package/dist/services/orphan-monitor.js +1 -1
  27. package/dist/services/process-detector.d.ts +1 -1
  28. package/dist/services/process-detector.d.ts.map +1 -1
  29. package/dist/services/process-detector.js +2 -11
  30. package/dist/services/process-registry.d.ts +1 -0
  31. package/dist/services/process-registry.d.ts.map +1 -1
  32. package/dist/services/process-registry.js +129 -108
  33. package/dist/services/terminal-persistence.d.ts.map +1 -1
  34. package/dist/services/terminal-persistence.js +47 -37
  35. package/dist/services/terminal-persistence.test.js +47 -1
  36. package/dist/utils/file-utils.d.ts +3 -0
  37. package/dist/utils/file-utils.d.ts.map +1 -1
  38. package/dist/utils/file-utils.js +32 -0
  39. package/package/aws-client-agent-mcp/README.md +288 -288
  40. package/package.json +76 -76
  41. package/dist/routes/aws-mcp.d.ts +0 -10
  42. package/dist/routes/aws-mcp.d.ts.map +0 -1
  43. package/dist/routes/aws-mcp.js +0 -74
  44. package/dist/routes/aws-mcp.test.d.ts +0 -2
  45. package/dist/routes/aws-mcp.test.d.ts.map +0 -1
  46. package/dist/routes/aws-mcp.test.js +0 -42
  47. package/dist/routes/memory.d.ts +0 -13
  48. package/dist/routes/memory.d.ts.map +0 -1
  49. package/dist/routes/memory.js +0 -429
  50. package/dist/services/aws-mcp-http.d.ts +0 -11
  51. package/dist/services/aws-mcp-http.d.ts.map +0 -1
  52. package/dist/services/aws-mcp-http.js +0 -225
  53. package/dist/services/aws-mcp-http.test.d.ts +0 -2
  54. package/dist/services/aws-mcp-http.test.d.ts.map +0 -1
  55. package/dist/services/aws-mcp-http.test.js +0 -27
  56. package/dist/services/easytier-manager.d.ts +0 -106
  57. package/dist/services/easytier-manager.d.ts.map +0 -1
  58. package/dist/services/easytier-manager.js +0 -331
  59. package/dist/services/easytier-manager.test.d.ts +0 -5
  60. package/dist/services/easytier-manager.test.d.ts.map +0 -1
  61. package/dist/services/easytier-manager.test.js +0 -98
  62. package/dist/services/memory-service.d.ts +0 -195
  63. package/dist/services/memory-service.d.ts.map +0 -1
  64. package/dist/services/memory-service.js +0 -650
  65. package/dist/services/session-lookup.d.ts +0 -20
  66. package/dist/services/session-lookup.d.ts.map +0 -1
  67. package/dist/services/session-lookup.js +0 -43
  68. package/dist/services/user-api-key-service.d.ts +0 -28
  69. package/dist/services/user-api-key-service.d.ts.map +0 -1
  70. package/dist/services/user-api-key-service.js +0 -75
  71. package/node_modules/@cc-switch/sdk/dist/adapters/common.d.ts +0 -38
  72. package/node_modules/@cc-switch/sdk/dist/adapters/common.d.ts.map +0 -1
  73. package/node_modules/@cc-switch/sdk/dist/adapters/common.js +0 -47
  74. package/node_modules/@cc-switch/sdk/dist/adapters/index.d.ts +0 -5
  75. package/node_modules/@cc-switch/sdk/dist/adapters/index.d.ts.map +0 -1
  76. package/node_modules/@cc-switch/sdk/dist/adapters/index.js +0 -28
  77. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claude.d.ts +0 -10
  78. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claude.d.ts.map +0 -1
  79. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claude.js +0 -39
  80. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claudecode.d.ts +0 -10
  81. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claudecode.d.ts.map +0 -1
  82. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claudecode.js +0 -40
  83. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.d.ts +0 -18
  84. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.d.ts.map +0 -1
  85. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.js +0 -63
  86. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.test.d.ts +0 -2
  87. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.test.d.ts.map +0 -1
  88. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.test.js +0 -86
  89. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-placeholder.d.ts +0 -9
  90. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-placeholder.d.ts.map +0 -1
  91. package/node_modules/@cc-switch/sdk/dist/adapters/mcp-placeholder.js +0 -14
  92. package/node_modules/@cc-switch/sdk/dist/adapters/skill-claude.d.ts +0 -10
  93. package/node_modules/@cc-switch/sdk/dist/adapters/skill-claude.d.ts.map +0 -1
  94. package/node_modules/@cc-switch/sdk/dist/adapters/skill-claude.js +0 -51
  95. package/node_modules/@cc-switch/sdk/dist/adapters/skill-claudecode.d.ts +0 -10
  96. package/node_modules/@cc-switch/sdk/dist/adapters/skill-claudecode.d.ts.map +0 -1
  97. package/node_modules/@cc-switch/sdk/dist/adapters/skill-claudecode.js +0 -51
  98. package/node_modules/@cc-switch/sdk/dist/adapters/skill-opencode.d.ts +0 -10
  99. package/node_modules/@cc-switch/sdk/dist/adapters/skill-opencode.d.ts.map +0 -1
  100. package/node_modules/@cc-switch/sdk/dist/adapters/skill-opencode.js +0 -51
  101. package/node_modules/@cc-switch/sdk/dist/adapters/skill-placeholder.d.ts +0 -9
  102. package/node_modules/@cc-switch/sdk/dist/adapters/skill-placeholder.d.ts.map +0 -1
  103. package/node_modules/@cc-switch/sdk/dist/adapters/skill-placeholder.js +0 -14
  104. package/node_modules/@cc-switch/sdk/dist/services/instance-service.d.ts +0 -78
  105. package/node_modules/@cc-switch/sdk/dist/services/instance-service.d.ts.map +0 -1
  106. package/node_modules/@cc-switch/sdk/dist/services/instance-service.js +0 -180
  107. package/package/cc-switch-sdk/dist/adapters/common.d.ts +0 -38
  108. package/package/cc-switch-sdk/dist/adapters/common.d.ts.map +0 -1
  109. package/package/cc-switch-sdk/dist/adapters/common.js +0 -47
  110. package/package/cc-switch-sdk/dist/adapters/index.d.ts +0 -5
  111. package/package/cc-switch-sdk/dist/adapters/index.d.ts.map +0 -1
  112. package/package/cc-switch-sdk/dist/adapters/index.js +0 -28
  113. package/package/cc-switch-sdk/dist/adapters/mcp-claude.d.ts +0 -10
  114. package/package/cc-switch-sdk/dist/adapters/mcp-claude.d.ts.map +0 -1
  115. package/package/cc-switch-sdk/dist/adapters/mcp-claude.js +0 -39
  116. package/package/cc-switch-sdk/dist/adapters/mcp-claudecode.d.ts +0 -10
  117. package/package/cc-switch-sdk/dist/adapters/mcp-claudecode.d.ts.map +0 -1
  118. package/package/cc-switch-sdk/dist/adapters/mcp-claudecode.js +0 -40
  119. package/package/cc-switch-sdk/dist/adapters/mcp-opencode.d.ts +0 -18
  120. package/package/cc-switch-sdk/dist/adapters/mcp-opencode.d.ts.map +0 -1
  121. package/package/cc-switch-sdk/dist/adapters/mcp-opencode.js +0 -63
  122. package/package/cc-switch-sdk/dist/adapters/mcp-opencode.test.d.ts +0 -2
  123. package/package/cc-switch-sdk/dist/adapters/mcp-opencode.test.d.ts.map +0 -1
  124. package/package/cc-switch-sdk/dist/adapters/mcp-opencode.test.js +0 -86
  125. package/package/cc-switch-sdk/dist/adapters/mcp-placeholder.d.ts +0 -9
  126. package/package/cc-switch-sdk/dist/adapters/mcp-placeholder.d.ts.map +0 -1
  127. package/package/cc-switch-sdk/dist/adapters/mcp-placeholder.js +0 -14
  128. package/package/cc-switch-sdk/dist/adapters/skill-claude.d.ts +0 -10
  129. package/package/cc-switch-sdk/dist/adapters/skill-claude.d.ts.map +0 -1
  130. package/package/cc-switch-sdk/dist/adapters/skill-claude.js +0 -51
  131. package/package/cc-switch-sdk/dist/adapters/skill-claudecode.d.ts +0 -10
  132. package/package/cc-switch-sdk/dist/adapters/skill-claudecode.d.ts.map +0 -1
  133. package/package/cc-switch-sdk/dist/adapters/skill-claudecode.js +0 -51
  134. package/package/cc-switch-sdk/dist/adapters/skill-opencode.d.ts +0 -10
  135. package/package/cc-switch-sdk/dist/adapters/skill-opencode.d.ts.map +0 -1
  136. package/package/cc-switch-sdk/dist/adapters/skill-opencode.js +0 -51
  137. package/package/cc-switch-sdk/dist/adapters/skill-placeholder.d.ts +0 -9
  138. package/package/cc-switch-sdk/dist/adapters/skill-placeholder.d.ts.map +0 -1
  139. package/package/cc-switch-sdk/dist/adapters/skill-placeholder.js +0 -14
  140. package/package/cc-switch-sdk/dist/services/instance-service.d.ts +0 -78
  141. package/package/cc-switch-sdk/dist/services/instance-service.d.ts.map +0 -1
  142. package/package/cc-switch-sdk/dist/services/instance-service.js +0 -180
@@ -715,7 +715,7 @@ function getChildProcesses(pid) {
715
715
  * @param timeoutMs 超时时间(毫秒)
716
716
  * @returns 是否成功终止
717
717
  */
718
- export function waitForProcessExit(pid, timeoutMs = 5000) {
718
+ export async function waitForProcessExit(pid, timeoutMs = 5000) {
719
719
  const startTime = Date.now();
720
720
  while (Date.now() - startTime < timeoutMs) {
721
721
  if (!isProcessRunning(pid)) {
@@ -724,16 +724,7 @@ export function waitForProcessExit(pid, timeoutMs = 5000) {
724
724
  // 等待 100ms 后重试
725
725
  const delay = Math.min(100, timeoutMs - (Date.now() - startTime));
726
726
  if (delay > 0) {
727
- try {
728
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
729
- }
730
- catch {
731
- // SharedArrayBuffer 不可用时回退到 setTimeout
732
- const startWait = Date.now();
733
- while (Date.now() - startWait < delay) {
734
- // 忙等待
735
- }
736
- }
727
+ await new Promise(resolve => setTimeout(resolve, delay));
737
728
  }
738
729
  }
739
730
  return false;
@@ -72,6 +72,7 @@ export declare class ProcessRegistry {
72
72
  * 持久化注册表状态到文件
73
73
  */
74
74
  private persist;
75
+ private mutate;
75
76
  /**
76
77
  * 注册新进程
77
78
  *
@@ -1 +1 @@
1
- {"version":3,"file":"process-registry.d.ts","sourceRoot":"","sources":["../../src/services/process-registry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAAC;AAEvG;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyC;IACjE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkC;IAE/D;;OAEG;IACH,OAAO;IAEP;;;;OAIG;WACW,WAAW,IAAI,eAAe;IAO5C;;OAEG;YACW,OAAO;IAYrB;;;;;OAKG;IACU,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB9E;;;;;;OAMG;IACU,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;IAmBvF;;;;;OAKG;IACI,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAItD;;;;;OAKG;IACI,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS;IAQvD;;;;OAIG;IACI,MAAM,IAAI,aAAa,EAAE;IAIhC;;;;;OAKG;IACI,UAAU,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,EAAE;IAIvD;;;;;OAKG;IACI,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAI1C;;;;;OAKG;IACI,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI3C;;;;;OAKG;IACU,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc5D;;;;;OAKG;IACU,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa9D;;;;;OAKG;IACU,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IActD;;;;;OAKG;IACU,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa/D;;;;;;OAMG;IACU,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC;IAalF;;;;OAIG;IACU,oBAAoB,CAAC,QAAQ,EAAE,qBAAqB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAiCnF;;;;OAIG;IACI,WAAW,IAAI,aAAa,EAAE;IAIrC;;;;OAIG;IACI,QAAQ,IAAI,MAAM,CAAC,YAAY,GAAG,OAAO,EAAE,MAAM,CAAC;IAkBzD;;OAEG;IACU,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAOpC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,eAAe,CAEpD;AAED;;;;GAIG;AACH,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAU7E;AAED;;;;GAIG;AACH,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAK1F"}
1
+ {"version":3,"file":"process-registry.d.ts","sourceRoot":"","sources":["../../src/services/process-registry.ts"],"names":[],"mappings":"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"}
@@ -7,7 +7,7 @@ import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { promises as fs } from 'node:fs';
9
9
  import { createLogger } from '../utils/logger.js';
10
- import { ensureDir } from '../utils/file-utils.js';
10
+ import { atomicWriteJsonFile, withFileLock } from '../utils/file-utils.js';
11
11
  const log = createLogger('process-registry');
12
12
  /**
13
13
  * 获取持久化注册表文件路径
@@ -47,15 +47,17 @@ export class ProcessRegistry {
47
47
  async persist() {
48
48
  const filePath = getProcessRegistryFile();
49
49
  try {
50
- await ensureDir(path.dirname(filePath));
51
50
  const records = Array.from(this.records.values());
52
- await fs.writeFile(filePath, JSON.stringify(records, null, 2), 'utf-8');
51
+ await atomicWriteJsonFile(filePath, records);
53
52
  log.debug(`[persist] 已持久化 ${records.length} 条进程记录`);
54
53
  }
55
54
  catch (error) {
56
55
  log.error('[persist] 持久化失败:', error);
57
56
  }
58
57
  }
58
+ async mutate(operation) {
59
+ return withFileLock(getProcessRegistryFile(), async () => operation());
60
+ }
59
61
  /**
60
62
  * 注册新进程
61
63
  *
@@ -63,19 +65,21 @@ export class ProcessRegistry {
63
65
  * @returns 是否注册成功
64
66
  */
65
67
  async register(record) {
66
- const fullRecord = {
67
- ...record,
68
- createdAt: new Date().toISOString(),
69
- };
70
- // 如果已存在相同 agentId,先移除旧的映射
71
- const existing = this.records.get(record.agentId);
72
- if (existing) {
73
- this.pidToAgentId.delete(existing.pid);
74
- }
75
- this.records.set(record.agentId, fullRecord);
76
- this.pidToAgentId.set(record.pid, record.agentId);
77
- log.info(`[register] 注册进程: agentId=${record.agentId}, pid=${record.pid}, mode=${record.mode}, state=${record.state}`);
78
- await this.persist();
68
+ await this.mutate(async () => {
69
+ const fullRecord = {
70
+ ...record,
71
+ createdAt: new Date().toISOString(),
72
+ };
73
+ // 如果已存在相同 agentId,先移除旧的映射
74
+ const existing = this.records.get(record.agentId);
75
+ if (existing) {
76
+ this.pidToAgentId.delete(existing.pid);
77
+ }
78
+ this.records.set(record.agentId, fullRecord);
79
+ this.pidToAgentId.set(record.pid, record.agentId);
80
+ log.info(`[register] 注册进程: agentId=${record.agentId}, pid=${record.pid}, mode=${record.mode}, state=${record.state}`);
81
+ await this.persist();
82
+ });
79
83
  }
80
84
  /**
81
85
  * 更新进程记录
@@ -85,20 +89,22 @@ export class ProcessRegistry {
85
89
  * @returns 是否更新成功
86
90
  */
87
91
  async update(agentId, updates) {
88
- const record = this.records.get(agentId);
89
- if (!record) {
90
- log.warn(`[update] 未找到进程: agentId=${agentId}`);
91
- return false;
92
- }
93
- // 如果更新包含 pid,需要更新 pid 映射
94
- if (updates.pid !== undefined && updates.pid !== record.pid) {
95
- this.pidToAgentId.delete(record.pid);
96
- this.pidToAgentId.set(updates.pid, agentId);
97
- }
98
- Object.assign(record, updates);
99
- log.debug(`[update] 更新进程: agentId=${agentId}, updates=${JSON.stringify(updates)}`);
100
- await this.persist();
101
- return true;
92
+ return this.mutate(async () => {
93
+ const record = this.records.get(agentId);
94
+ if (!record) {
95
+ log.warn(`[update] 未找到进程: agentId=${agentId}`);
96
+ return false;
97
+ }
98
+ // 如果更新包含 pid,需要更新 pid 映射
99
+ if (updates.pid !== undefined && updates.pid !== record.pid) {
100
+ this.pidToAgentId.delete(record.pid);
101
+ this.pidToAgentId.set(updates.pid, agentId);
102
+ }
103
+ Object.assign(record, updates);
104
+ log.debug(`[update] 更新进程: agentId=${agentId}, updates=${JSON.stringify(updates)}`);
105
+ await this.persist();
106
+ return true;
107
+ });
102
108
  }
103
109
  /**
104
110
  * 通过 agentId 获取进程记录
@@ -164,16 +170,18 @@ export class ProcessRegistry {
164
170
  * @returns 是否标记成功
165
171
  */
166
172
  async markOrphaned(agentId) {
167
- const record = this.records.get(agentId);
168
- if (!record) {
169
- log.warn(`[markOrphaned] 未找到进程: agentId=${agentId}`);
170
- return false;
171
- }
172
- record.state = 'orphaned';
173
- record.orphanedAt = new Date().toISOString();
174
- log.info(`[markOrphaned] 标记进程为孤儿: agentId=${agentId}, orphanedAt=${record.orphanedAt}`);
175
- await this.persist();
176
- return true;
173
+ return this.mutate(async () => {
174
+ const record = this.records.get(agentId);
175
+ if (!record) {
176
+ log.warn(`[markOrphaned] 未找到进程: agentId=${agentId}`);
177
+ return false;
178
+ }
179
+ record.state = 'orphaned';
180
+ record.orphanedAt = new Date().toISOString();
181
+ log.info(`[markOrphaned] 标记进程为孤儿: agentId=${agentId}, orphanedAt=${record.orphanedAt}`);
182
+ await this.persist();
183
+ return true;
184
+ });
177
185
  }
178
186
  /**
179
187
  * 标记进程为终止状态
@@ -182,15 +190,17 @@ export class ProcessRegistry {
182
190
  * @returns 是否标记成功
183
191
  */
184
192
  async markTerminated(agentId) {
185
- const record = this.records.get(agentId);
186
- if (!record) {
187
- log.warn(`[markTerminated] 未找到进程: agentId=${agentId}`);
188
- return false;
189
- }
190
- record.state = 'terminated';
191
- log.info(`[markTerminated] 标记进程为终止: agentId=${agentId}`);
192
- await this.persist();
193
- return true;
193
+ return this.mutate(async () => {
194
+ const record = this.records.get(agentId);
195
+ if (!record) {
196
+ log.warn(`[markTerminated] 未找到进程: agentId=${agentId}`);
197
+ return false;
198
+ }
199
+ record.state = 'terminated';
200
+ log.info(`[markTerminated] 标记进程为终止: agentId=${agentId}`);
201
+ await this.persist();
202
+ return true;
203
+ });
194
204
  }
195
205
  /**
196
206
  * 移除进程记录
@@ -199,16 +209,18 @@ export class ProcessRegistry {
199
209
  * @returns 是否移除成功
200
210
  */
201
211
  async remove(agentId) {
202
- const record = this.records.get(agentId);
203
- if (!record) {
204
- log.warn(`[remove] 未找到进程: agentId=${agentId}`);
205
- return false;
206
- }
207
- this.records.delete(agentId);
208
- this.pidToAgentId.delete(record.pid);
209
- log.info(`[remove] 移除进程: agentId=${agentId}, pid=${record.pid}`);
210
- await this.persist();
211
- return true;
212
+ return this.mutate(async () => {
213
+ const record = this.records.get(agentId);
214
+ if (!record) {
215
+ log.warn(`[remove] 未找到进程: agentId=${agentId}`);
216
+ return false;
217
+ }
218
+ this.records.delete(agentId);
219
+ this.pidToAgentId.delete(record.pid);
220
+ log.info(`[remove] 移除进程: agentId=${agentId}, pid=${record.pid}`);
221
+ await this.persist();
222
+ return true;
223
+ });
212
224
  }
213
225
  /**
214
226
  * 更新进程心跳时间
@@ -217,15 +229,17 @@ export class ProcessRegistry {
217
229
  * @returns 是否更新成功
218
230
  */
219
231
  async updateHeartbeat(agentId) {
220
- const record = this.records.get(agentId);
221
- if (!record) {
222
- log.warn(`[updateHeartbeat] 未找到进程: agentId=${agentId}`);
223
- return false;
224
- }
225
- record.lastHeartbeat = new Date().toISOString();
226
- log.debug(`[updateHeartbeat] 更新心跳: agentId=${agentId}, lastHeartbeat=${record.lastHeartbeat}`);
227
- await this.persist();
228
- return true;
232
+ return this.mutate(async () => {
233
+ const record = this.records.get(agentId);
234
+ if (!record) {
235
+ log.warn(`[updateHeartbeat] 未找到进程: agentId=${agentId}`);
236
+ return false;
237
+ }
238
+ record.lastHeartbeat = new Date().toISOString();
239
+ log.debug(`[updateHeartbeat] 更新心跳: agentId=${agentId}, lastHeartbeat=${record.lastHeartbeat}`);
240
+ await this.persist();
241
+ return true;
242
+ });
229
243
  }
230
244
  /**
231
245
  * 更新进程健康状态
@@ -235,15 +249,17 @@ export class ProcessRegistry {
235
249
  * @returns 是否更新成功
236
250
  */
237
251
  async updateHealth(agentId, health) {
238
- const record = this.records.get(agentId);
239
- if (!record) {
240
- log.warn(`[updateHealth] 未找到进程: agentId=${agentId}`);
241
- return false;
242
- }
243
- record.healthStatus = health;
244
- log.debug(`[updateHealth] 更新健康状态: agentId=${agentId}, health=${JSON.stringify(health)}`);
245
- await this.persist();
246
- return true;
252
+ return this.mutate(async () => {
253
+ const record = this.records.get(agentId);
254
+ if (!record) {
255
+ log.warn(`[updateHealth] 未找到进程: agentId=${agentId}`);
256
+ return false;
257
+ }
258
+ record.healthStatus = health;
259
+ log.debug(`[updateHealth] 更新健康状态: agentId=${agentId}, health=${JSON.stringify(health)}`);
260
+ await this.persist();
261
+ return true;
262
+ });
247
263
  }
248
264
  /**
249
265
  * 从持久化会话重建注册表
@@ -251,30 +267,32 @@ export class ProcessRegistry {
251
267
  * @param sessions - 持久化会话数组
252
268
  */
253
269
  async rebuildFromPersisted(sessions) {
254
- log.info(`[rebuildFromPersisted] ${sessions.length} 个持久化会话重建注册表`);
255
- this.records.clear();
256
- this.pidToAgentId.clear();
257
- for (const session of sessions) {
258
- if (!session.pid) {
259
- log.warn(`[rebuildFromPersisted] 跳过无 PID 的会话: agentId=${session.agentId}`);
260
- continue;
270
+ await this.mutate(async () => {
271
+ log.info(`[rebuildFromPersisted] 从 ${sessions.length} 个持久化会话重建注册表`);
272
+ this.records.clear();
273
+ this.pidToAgentId.clear();
274
+ for (const session of sessions) {
275
+ if (!session.pid) {
276
+ log.warn(`[rebuildFromPersisted] 跳过无 PID 的会话: agentId=${session.agentId}`);
277
+ continue;
278
+ }
279
+ const record = {
280
+ agentId: session.agentId,
281
+ sessionId: session.sessionId,
282
+ pid: session.pid,
283
+ mode: session.mode || 'sdk',
284
+ state: 'unknown', // 重建时状态未知,需要外部检测
285
+ workspacePath: session.workspacePath || '',
286
+ command: session.command || '',
287
+ createdAt: new Date().toISOString(),
288
+ };
289
+ this.records.set(record.agentId, record);
290
+ this.pidToAgentId.set(record.pid, record.agentId);
291
+ log.debug(`[rebuildFromPersisted] 重建进程记录: agentId=${record.agentId}, pid=${record.pid}`);
261
292
  }
262
- const record = {
263
- agentId: session.agentId,
264
- sessionId: session.sessionId,
265
- pid: session.pid,
266
- mode: session.mode || 'sdk',
267
- state: 'unknown', // 重建时状态未知,需要外部检测
268
- workspacePath: session.workspacePath || '',
269
- command: session.command || '',
270
- createdAt: new Date().toISOString(),
271
- };
272
- this.records.set(record.agentId, record);
273
- this.pidToAgentId.set(record.pid, record.agentId);
274
- log.debug(`[rebuildFromPersisted] 重建进程记录: agentId=${record.agentId}, pid=${record.pid}`);
275
- }
276
- log.info(`[rebuildFromPersisted] 重建完成,共 ${this.records.size} 条记录`);
277
- await this.persist();
293
+ log.info(`[rebuildFromPersisted] 重建完成,共 ${this.records.size} 条记录`);
294
+ await this.persist();
295
+ });
278
296
  }
279
297
  /**
280
298
  * 导出所有进程记录用于持久化
@@ -308,11 +326,13 @@ export class ProcessRegistry {
308
326
  * 清空所有进程记录
309
327
  */
310
328
  async clear() {
311
- const count = this.records.size;
312
- this.records.clear();
313
- this.pidToAgentId.clear();
314
- log.info(`[clear] 清空所有进程记录: ${count} 条`);
315
- await this.persist();
329
+ await this.mutate(async () => {
330
+ const count = this.records.size;
331
+ this.records.clear();
332
+ this.pidToAgentId.clear();
333
+ log.info(`[clear] 清空所有进程记录: ${count} 条`);
334
+ await this.persist();
335
+ });
316
336
  }
317
337
  }
318
338
  ProcessRegistry.instance = null;
@@ -348,7 +368,8 @@ export async function loadPersistedProcessRegistry() {
348
368
  */
349
369
  export async function savePersistedProcessRegistry(records) {
350
370
  const filePath = getProcessRegistryFile();
351
- await ensureDir(path.dirname(filePath));
352
- await fs.writeFile(filePath, JSON.stringify(records, null, 2), 'utf-8');
353
- log.debug(`[savePersistedProcessRegistry] 保存了 ${records.length} 条进程记录`);
371
+ await withFileLock(filePath, async () => {
372
+ await atomicWriteJsonFile(filePath, records);
373
+ log.debug(`[savePersistedProcessRegistry] 保存了 ${records.length} 条进程记录`);
374
+ });
354
375
  }
@@ -1 +1 @@
1
- {"version":3,"file":"terminal-persistence.d.ts","sourceRoot":"","sources":["../../src/services/terminal-persistence.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAKpD;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAUzE;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAIvF;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CASrF;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB7E;AAED;;;;;GAKG;AACH,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAGvC;AAED;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBpF"}
1
+ {"version":3,"file":"terminal-persistence.d.ts","sourceRoot":"","sources":["../../src/services/terminal-persistence.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA6BpD;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAOzE;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvF;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrF;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB7E;AAED;;;;;GAKG;AACH,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAGvC;AAED;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBpF"}
@@ -6,9 +6,30 @@
6
6
  import path from 'node:path';
7
7
  import os from 'node:os';
8
8
  import { promises as fs } from 'node:fs';
9
- import { ensureDir } from '../utils/file-utils.js';
9
+ import { atomicWriteJsonFile, withFileLock } from '../utils/file-utils.js';
10
10
  import { createLogger } from '../utils/logger.js';
11
11
  const log = createLogger('terminal-persistence');
12
+ async function readAllPersistedSessions(filePath) {
13
+ try {
14
+ const raw = await fs.readFile(filePath, 'utf-8');
15
+ return JSON.parse(raw);
16
+ }
17
+ catch (error) {
18
+ const err = error;
19
+ if (err.code !== 'ENOENT') {
20
+ log.warn(`[readAllPersistedSessions] 读取持久化会话失败: ${filePath}, error=${err.message}`);
21
+ }
22
+ return [];
23
+ }
24
+ }
25
+ async function updatePersistedSessions(updater) {
26
+ const filePath = getPersistedSessionsFile();
27
+ await withFileLock(filePath, async () => {
28
+ const sessions = await readAllPersistedSessions(filePath);
29
+ const nextSessions = await updater(sessions);
30
+ await atomicWriteJsonFile(filePath, nextSessions);
31
+ });
32
+ }
12
33
  /**
13
34
  * 获取持久化会话文件路径
14
35
  *
@@ -24,15 +45,11 @@ export function getPersistedSessionsFile() {
24
45
  */
25
46
  export async function loadPersistedSessions() {
26
47
  const filePath = getPersistedSessionsFile();
27
- try {
28
- const raw = await fs.readFile(filePath, 'utf-8');
29
- const sessions = JSON.parse(raw);
48
+ return withFileLock(filePath, async () => {
49
+ const sessions = await readAllPersistedSessions(filePath);
30
50
  // 只返回状态为 running 的会话
31
51
  return sessions.filter(s => s.status === 'running');
32
- }
33
- catch {
34
- return [];
35
- }
52
+ });
36
53
  }
37
54
  /**
38
55
  * 保存持久化的终端会话列表
@@ -41,8 +58,9 @@ export async function loadPersistedSessions() {
41
58
  */
42
59
  export async function savePersistedSessions(sessions) {
43
60
  const filePath = getPersistedSessionsFile();
44
- await ensureDir(path.dirname(filePath));
45
- await fs.writeFile(filePath, JSON.stringify(sessions, null, 2), 'utf-8');
61
+ await withFileLock(filePath, async () => {
62
+ await atomicWriteJsonFile(filePath, sessions);
63
+ });
46
64
  }
47
65
  /**
48
66
  * 添加或更新持久化会话
@@ -50,15 +68,17 @@ export async function savePersistedSessions(sessions) {
50
68
  * @param session - 会话数据
51
69
  */
52
70
  export async function upsertPersistedSession(session) {
53
- const sessions = await loadPersistedSessions();
54
- const index = sessions.findIndex(s => s.sessionId === session.sessionId);
55
- if (index >= 0) {
56
- sessions[index] = session;
57
- }
58
- else {
59
- sessions.push(session);
60
- }
61
- await savePersistedSessions(sessions);
71
+ await updatePersistedSessions((sessions) => {
72
+ const runningSessions = sessions.filter(s => s.status === 'running');
73
+ const index = runningSessions.findIndex(s => s.sessionId === session.sessionId);
74
+ if (index >= 0) {
75
+ runningSessions[index] = session;
76
+ }
77
+ else {
78
+ runningSessions.push(session);
79
+ }
80
+ return runningSessions;
81
+ });
62
82
  }
63
83
  /**
64
84
  * 移除持久化会话(从完整文件中删除,包括 stopped 状态的)
@@ -67,9 +87,8 @@ export async function upsertPersistedSession(session) {
67
87
  */
68
88
  export async function removePersistedSession(sessionId) {
69
89
  const filePath = getPersistedSessionsFile();
70
- try {
71
- const raw = await fs.readFile(filePath, 'utf-8');
72
- const allSessions = JSON.parse(raw);
90
+ await withFileLock(filePath, async () => {
91
+ const allSessions = await readAllPersistedSessions(filePath);
73
92
  const beforeCount = allSessions.length;
74
93
  const filtered = allSessions.filter(s => s.sessionId !== sessionId);
75
94
  const afterCount = filtered.length;
@@ -79,12 +98,8 @@ export async function removePersistedSession(sessionId) {
79
98
  else {
80
99
  log.warn(`[removePersistedSession] 未找到要删除的会话: sessionId=${sessionId}, 当前会话数=${beforeCount}`);
81
100
  }
82
- await savePersistedSessions(filtered);
83
- }
84
- catch (error) {
85
- // 文件不存在,无需删除
86
- log.debug(`[removePersistedSession] 文件不存在或读取失败: ${filePath}`);
87
- }
101
+ await atomicWriteJsonFile(filePath, filtered);
102
+ });
88
103
  }
89
104
  /**
90
105
  * 根据 agentId 查找持久化会话
@@ -104,9 +119,8 @@ export async function findPersistedSessionByAgentId(agentId) {
104
119
  */
105
120
  export async function removePersistedSessionByAgentId(agentId) {
106
121
  const filePath = getPersistedSessionsFile();
107
- try {
108
- const raw = await fs.readFile(filePath, 'utf-8');
109
- const allSessions = JSON.parse(raw);
122
+ await withFileLock(filePath, async () => {
123
+ const allSessions = await readAllPersistedSessions(filePath);
110
124
  const beforeCount = allSessions.length;
111
125
  const filtered = allSessions.filter(s => s.agentId !== agentId);
112
126
  const afterCount = filtered.length;
@@ -116,10 +130,6 @@ export async function removePersistedSessionByAgentId(agentId) {
116
130
  else {
117
131
  log.debug(`[removePersistedSessionByAgentId] 未找到要删除的会话: agentId=${agentId}`);
118
132
  }
119
- await savePersistedSessions(filtered);
120
- }
121
- catch (error) {
122
- // 文件不存在,无需删除
123
- log.debug(`[removePersistedSessionByAgentId] 文件不存在或读取失败: ${filePath}`);
124
- }
133
+ await atomicWriteJsonFile(filePath, filtered);
134
+ });
125
135
  }
@@ -1,8 +1,33 @@
1
1
  /**
2
2
  * Terminal Persistence 服务单元测试
3
3
  */
4
- import { describe, it, expect } from 'vitest';
4
+ import path from 'node:path';
5
+ import { promises as fs } from 'node:fs';
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ const tmpRoot = path.join(process.cwd(), '.tmp-vitest-terminal-persistence');
8
+ vi.mock('node:os', () => ({
9
+ default: {
10
+ tmpdir: () => tmpRoot,
11
+ },
12
+ }));
13
+ const persistence = await import('./terminal-persistence.js');
14
+ function makeSession(sessionId, agentId) {
15
+ return {
16
+ sessionId,
17
+ agentId,
18
+ workspacePath: `/workspace/${agentId}`,
19
+ command: 'claude',
20
+ startedAt: new Date(0).toISOString(),
21
+ status: 'running',
22
+ };
23
+ }
5
24
  describe('terminal-persistence service', () => {
25
+ beforeEach(async () => {
26
+ await fs.rm(tmpRoot, { recursive: true, force: true });
27
+ });
28
+ afterEach(async () => {
29
+ await fs.rm(tmpRoot, { recursive: true, force: true });
30
+ });
6
31
  it('generates correct file path', () => {
7
32
  const getPersistedSessionsFile = (tmpdir) => {
8
33
  return `${tmpdir}/agentswork-runtime-bridge/sessions.json`;
@@ -85,4 +110,25 @@ describe('terminal-persistence service', () => {
85
110
  };
86
111
  expect(parseSessions('invalid json')).toEqual([]);
87
112
  });
113
+ it('serializes concurrent upserts without losing sessions', async () => {
114
+ await Promise.all([
115
+ persistence.upsertPersistedSession(makeSession('session-1', 'agent-1')),
116
+ persistence.upsertPersistedSession(makeSession('session-2', 'agent-2')),
117
+ persistence.upsertPersistedSession(makeSession('session-3', 'agent-3')),
118
+ ]);
119
+ const sessions = await persistence.loadPersistedSessions();
120
+ expect(sessions.map(session => session.sessionId).sort()).toEqual(['session-1', 'session-2', 'session-3']);
121
+ });
122
+ it('does not revive a removed session during concurrent writes', async () => {
123
+ await persistence.savePersistedSessions([
124
+ makeSession('session-1', 'agent-1'),
125
+ makeSession('session-2', 'agent-2'),
126
+ ]);
127
+ await Promise.all([
128
+ persistence.removePersistedSession('session-1'),
129
+ persistence.upsertPersistedSession(makeSession('session-3', 'agent-3')),
130
+ ]);
131
+ const sessions = await persistence.loadPersistedSessions();
132
+ expect(sessions.map(session => session.sessionId).sort()).toEqual(['session-2', 'session-3']);
133
+ });
88
134
  });
@@ -1,6 +1,9 @@
1
1
  import type { SkillPackage, DownloadResult, Skill } from '../types.js';
2
2
  import type { AppFlags, InstalledSkill, CcSwitchSdk } from '@cc-switch/sdk';
3
3
  export declare function ensureDir(targetDir: string): Promise<string>;
4
+ export declare function withFileLock<T>(targetPath: string, operation: () => Promise<T>): Promise<T>;
5
+ export declare function atomicWriteFile(targetPath: string, content: string): Promise<void>;
6
+ export declare function atomicWriteJsonFile(targetPath: string, value: unknown): Promise<void>;
4
7
  export declare function downloadSkillZip(skillPackage: SkillPackage, targetDir: string): Promise<DownloadResult | null>;
5
8
  export declare function runCommand(command: string, args: string[]): Promise<void>;
6
9
  export declare function escapePowerShellLiteral(text: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE5E,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,YAAY,EAC1B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA0ChC;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhG;AAED,wBAAsB,oCAAoC,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASlG;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,QAAQ,CAW1E;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAM1D;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhF;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,WAAW,EAChB,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAyBhC"}
1
+ {"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAI5E,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAqBjG;AAED,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWxF;AAED,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3F;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,YAAY,EAC1B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA0ChC;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhG;AAED,wBAAsB,oCAAoC,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASlG;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,QAAQ,CAW1E;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAM1D;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhF;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,WAAW,EAChB,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAyBhC"}
@@ -4,10 +4,42 @@ import { promises as fs } from 'node:fs';
4
4
  import { createHash, randomUUID } from 'node:crypto';
5
5
  import axios from 'axios';
6
6
  import { schedulerBaseUrl, runtimeToken } from '../config.js';
7
+ const fileLocks = new Map();
7
8
  export async function ensureDir(targetDir) {
8
9
  await fs.mkdir(targetDir, { recursive: true });
9
10
  return targetDir;
10
11
  }
12
+ export async function withFileLock(targetPath, operation) {
13
+ const lockKey = path.resolve(targetPath);
14
+ const previous = fileLocks.get(lockKey) ?? Promise.resolve();
15
+ let release;
16
+ const current = new Promise((resolve) => {
17
+ release = resolve;
18
+ });
19
+ const chained = previous.then(() => current);
20
+ fileLocks.set(lockKey, chained);
21
+ await previous;
22
+ try {
23
+ return await operation();
24
+ }
25
+ finally {
26
+ release();
27
+ if (fileLocks.get(lockKey) === chained) {
28
+ fileLocks.delete(lockKey);
29
+ }
30
+ }
31
+ }
32
+ export async function atomicWriteFile(targetPath, content) {
33
+ await ensureDir(path.dirname(targetPath));
34
+ const targetDir = path.dirname(targetPath);
35
+ const targetBase = path.basename(targetPath);
36
+ const tmpPath = path.join(targetDir, `.${targetBase}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
37
+ await fs.writeFile(tmpPath, content, 'utf-8');
38
+ await fs.rename(tmpPath, targetPath);
39
+ }
40
+ export async function atomicWriteJsonFile(targetPath, value) {
41
+ await atomicWriteFile(targetPath, `${JSON.stringify(value, null, 2)}\n`);
42
+ }
11
43
  export async function downloadSkillZip(skillPackage, targetDir) {
12
44
  if (!skillPackage || !skillPackage.downloadUrl) {
13
45
  return null;