@actiondock/core 2.10.0 → 2.12.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 (252) hide show
  1. package/README.md +1 -2
  2. package/dist/catalog/action-catalog.js +3 -33
  3. package/dist/catalog/discovery.d.ts +0 -3
  4. package/dist/catalog/discovery.js +4 -4
  5. package/dist/catalog/graph.d.ts +0 -4
  6. package/dist/catalog/graph.js +8 -14
  7. package/dist/{profile/client-actions.d.ts → client/actions.d.ts} +1 -1
  8. package/dist/{profile/client-actions.js → client/actions.js} +2 -8
  9. package/dist/{profile/client-config.d.ts → client/config.d.ts} +1 -1
  10. package/dist/{profile/client-config.js → client/config.js} +1 -1
  11. package/dist/client/dispatcher.d.ts +4 -0
  12. package/dist/client/dispatcher.js +21 -0
  13. package/dist/client/health.d.ts +38 -0
  14. package/dist/{profile/client-health.js → client/health.js} +9 -6
  15. package/dist/client/index.d.ts +16 -0
  16. package/dist/client/index.js +20 -0
  17. package/dist/{profile/client-playbooks.d.ts → client/playbooks.d.ts} +1 -1
  18. package/dist/{profile/client-playbooks.js → client/playbooks.js} +1 -1
  19. package/dist/{profile/client-runs.d.ts → client/runs.d.ts} +4 -2
  20. package/dist/{profile/client-runs.js → client/runs.js} +7 -1
  21. package/dist/{profile/client-state.d.ts → client/state.d.ts} +1 -1
  22. package/dist/{profile/client-state.js → client/state.js} +1 -1
  23. package/dist/{profile/client-transport.d.ts → client/transport.d.ts} +29 -10
  24. package/dist/{profile/client-transport.js → client/transport.js} +125 -35
  25. package/dist/doctor/checks/index.d.ts +1 -1
  26. package/dist/doctor/checks/index.js +1 -1
  27. package/dist/doctor/checks/project.js +20 -88
  28. package/dist/doctor/checks/runtime.d.ts +0 -4
  29. package/dist/doctor/checks/runtime.js +0 -28
  30. package/dist/doctor/doctor.js +1 -2
  31. package/dist/errors.d.ts +53 -3
  32. package/dist/errors.js +53 -3
  33. package/dist/execution/service.d.ts +55 -37
  34. package/dist/execution/service.js +601 -212
  35. package/dist/execution/types.d.ts +24 -5
  36. package/dist/host/host.d.ts +42 -22
  37. package/dist/host/host.js +562 -149
  38. package/dist/host/routing.js +6 -6
  39. package/dist/host/types.d.ts +25 -13
  40. package/dist/index.d.ts +9 -4
  41. package/dist/index.js +5 -2
  42. package/dist/input/advice.d.ts +0 -44
  43. package/dist/input/advice.js +7 -141
  44. package/dist/input/flat-materializer.js +1 -1
  45. package/dist/input/flat-parser.js +1 -1
  46. package/dist/input/index.d.ts +0 -1
  47. package/dist/input/index.js +0 -1
  48. package/dist/input/input-resolver.js +1 -1
  49. package/dist/input/validation-mapper.d.ts +1 -1
  50. package/dist/ipc/diagnostic.d.ts +8 -21
  51. package/dist/ipc/diagnostic.js +24 -57
  52. package/dist/ipc/host.js +21 -7
  53. package/dist/ipc/service.d.ts +14 -0
  54. package/dist/ipc/service.js +132 -26
  55. package/dist/ipc/types.d.ts +0 -4
  56. package/dist/package/index.d.ts +4 -5
  57. package/dist/package/index.js +1 -2
  58. package/dist/package/runtime.d.ts +94 -23
  59. package/dist/package/runtime.js +628 -274
  60. package/dist/package/static-index.d.ts +3 -6
  61. package/dist/package/static-index.js +11 -40
  62. package/dist/package/types.d.ts +19 -4
  63. package/dist/platform/index.d.ts +2 -1
  64. package/dist/platform/index.js +2 -1
  65. package/dist/platform/loader-hook.d.ts +32 -0
  66. package/dist/platform/loader-hook.js +90 -0
  67. package/dist/{node → platform}/module-loader.d.ts +13 -0
  68. package/dist/{node → platform}/module-loader.js +51 -4
  69. package/dist/platform/node.d.ts +19 -8
  70. package/dist/platform/node.js +50 -20
  71. package/dist/platform/types.d.ts +7 -47
  72. package/dist/process/context-process.d.ts +3 -18
  73. package/dist/process/context-process.js +3 -44
  74. package/dist/process/control.d.ts +21 -0
  75. package/dist/process/control.js +34 -0
  76. package/dist/process/converters.d.ts +23 -0
  77. package/dist/process/converters.js +92 -0
  78. package/dist/process/cursor.js +2 -22
  79. package/dist/process/{diagnostics-sink.d.ts → diagnostics.d.ts} +0 -7
  80. package/dist/process/{diagnostics-sink.js → diagnostics.js} +6 -8
  81. package/dist/process/index.d.ts +1 -3
  82. package/dist/process/index.js +1 -3
  83. package/dist/process/input-dispatcher.d.ts +4 -13
  84. package/dist/process/input-dispatcher.js +48 -69
  85. package/dist/process/managed-record.d.ts +3 -31
  86. package/dist/process/metadata-store.d.ts +0 -41
  87. package/dist/process/metadata-store.js +2 -341
  88. package/dist/process/process-driver.js +2 -2
  89. package/dist/process/process-manager.d.ts +8 -50
  90. package/dist/process/process-manager.js +28 -417
  91. package/dist/process/quotas.d.ts +14 -0
  92. package/dist/process/quotas.js +29 -0
  93. package/dist/process/run-executor.js +0 -1
  94. package/dist/process/terminal-output-cache.d.ts +20 -34
  95. package/dist/process/terminal-output-cache.js +43 -64
  96. package/dist/profile/index.d.ts +1 -2
  97. package/dist/profile/index.js +1 -2
  98. package/dist/profile/manager.d.ts +2 -4
  99. package/dist/profile/manager.js +10 -40
  100. package/dist/profile/types.d.ts +1 -17
  101. package/dist/project/digest.d.ts +5 -1
  102. package/dist/project/digest.js +81 -219
  103. package/dist/project/index.d.ts +4 -5
  104. package/dist/project/index.js +4 -5
  105. package/dist/project/init.js +2 -2
  106. package/dist/project/loader.d.ts +3 -3
  107. package/dist/project/loader.js +72 -50
  108. package/dist/project/lockfile.js +6 -5
  109. package/dist/project/manifest.js +5 -4
  110. package/dist/project/transactions.d.ts +4 -42
  111. package/dist/project/transactions.js +83 -138
  112. package/dist/project/types-generator.js +2 -1
  113. package/dist/project/types.d.ts +2 -0
  114. package/dist/registry/index.d.ts +0 -1
  115. package/dist/registry/index.js +0 -1
  116. package/dist/registry/lock.d.ts +2 -11
  117. package/dist/registry/lock.js +14 -183
  118. package/dist/registry/registry.d.ts +1 -6
  119. package/dist/registry/registry.js +5 -17
  120. package/dist/runtime/action-registry.d.ts +1 -5
  121. package/dist/runtime/action-registry.js +0 -12
  122. package/dist/runtime/context.js +2 -2
  123. package/dist/runtime/events.d.ts +11 -50
  124. package/dist/runtime/events.js +88 -368
  125. package/dist/runtime/index.d.ts +0 -4
  126. package/dist/runtime/index.js +0 -4
  127. package/dist/runtime/run-persistence.d.ts +2 -0
  128. package/dist/runtime/run-persistence.js +2 -1
  129. package/dist/runtime/standalone-commands.d.ts +18 -0
  130. package/dist/runtime/standalone-commands.js +466 -0
  131. package/dist/runtime/standalone-help.d.ts +20 -0
  132. package/dist/runtime/standalone-help.js +72 -0
  133. package/dist/runtime/standalone.d.ts +1 -44
  134. package/dist/runtime/standalone.js +32 -573
  135. package/dist/schema/validator.d.ts +2 -2
  136. package/dist/schema/validator.js +2 -9
  137. package/dist/server/index.d.ts +0 -2
  138. package/dist/server/index.js +0 -2
  139. package/dist/server/routes/actions.js +1 -1
  140. package/dist/server/routes/info.js +1 -1
  141. package/dist/server/routes/playbooks.js +1 -1
  142. package/dist/server/routes/runs.js +123 -16
  143. package/dist/server/routes/state.js +43 -18
  144. package/dist/server/security.d.ts +1 -1
  145. package/dist/server/security.js +2 -2
  146. package/dist/server/server.js +3 -7
  147. package/dist/service/factory.js +4 -3
  148. package/dist/service/index.d.ts +1 -1
  149. package/dist/service/index.js +1 -1
  150. package/dist/service/remote-errors.d.ts +1 -1
  151. package/dist/service/remote-errors.js +6 -6
  152. package/dist/service/remote-polling.d.ts +3 -0
  153. package/dist/service/remote-polling.js +10 -5
  154. package/dist/service/remote.d.ts +5 -1
  155. package/dist/service/remote.js +108 -17
  156. package/dist/service/sse-stream.js +2 -2
  157. package/dist/service/types.d.ts +22 -19
  158. package/dist/service/types.js +1 -24
  159. package/dist/storage/clock.d.ts +1 -1
  160. package/dist/storage/clock.js +1 -1
  161. package/dist/storage/data-dir-lock.d.ts +10 -22
  162. package/dist/storage/data-dir-lock.js +28 -56
  163. package/dist/storage/file-lock.d.ts +64 -0
  164. package/dist/storage/file-lock.js +447 -0
  165. package/dist/storage/index.d.ts +2 -2
  166. package/dist/storage/index.js +5 -4
  167. package/dist/storage/run-liveness.d.ts +48 -0
  168. package/dist/storage/run-liveness.js +44 -0
  169. package/dist/storage/sqlite-config.d.ts +16 -0
  170. package/dist/storage/sqlite-config.js +59 -0
  171. package/dist/storage/sqlite-driver.d.ts +3 -1
  172. package/dist/storage/sqlite-driver.js +14 -4
  173. package/dist/storage/sqlite-events.d.ts +13 -0
  174. package/dist/storage/sqlite-events.js +48 -0
  175. package/dist/storage/sqlite-runs.d.ts +107 -0
  176. package/dist/storage/sqlite-runs.js +622 -0
  177. package/dist/storage/sqlite-schema.d.ts +5 -0
  178. package/dist/storage/sqlite-schema.js +143 -0
  179. package/dist/storage/sqlite-state.d.ts +33 -0
  180. package/dist/storage/sqlite-state.js +270 -0
  181. package/dist/storage/sqlite.d.ts +30 -57
  182. package/dist/storage/sqlite.js +84 -741
  183. package/dist/storage/types.d.ts +35 -2
  184. package/dist/utils/index.d.ts +18 -14
  185. package/dist/utils/index.js +37 -29
  186. package/dist/utils/semver.d.ts +18 -0
  187. package/dist/utils/semver.js +19 -0
  188. package/dist/utils/warning.d.ts +1 -0
  189. package/dist/utils/warning.js +52 -0
  190. package/dist/{json/value-validator.d.ts → value-validator.d.ts} +9 -0
  191. package/dist/value-validator.js +330 -0
  192. package/dist/version.d.ts +1 -1
  193. package/dist/version.js +1 -1
  194. package/package.json +10 -12
  195. package/dist/execution/index.d.ts +0 -2
  196. package/dist/execution/index.js +0 -2
  197. package/dist/filter/index.d.ts +0 -1
  198. package/dist/filter/index.js +0 -1
  199. package/dist/graph/index.d.ts +0 -4
  200. package/dist/graph/index.js +0 -4
  201. package/dist/input/metadata.d.ts +0 -99
  202. package/dist/input/metadata.js +0 -99
  203. package/dist/invocation/index.d.ts +0 -2
  204. package/dist/invocation/index.js +0 -2
  205. package/dist/ipc/index.d.ts +0 -4
  206. package/dist/ipc/index.js +0 -4
  207. package/dist/json/index.d.ts +0 -1
  208. package/dist/json/index.js +0 -1
  209. package/dist/json/value-validator.js +0 -330
  210. package/dist/node/index.d.ts +0 -1
  211. package/dist/node/index.js +0 -1
  212. package/dist/platform/node-fs.d.ts +0 -37
  213. package/dist/platform/node-fs.js +0 -81
  214. package/dist/process/control-arbiter.d.ts +0 -108
  215. package/dist/process/control-arbiter.js +0 -250
  216. package/dist/process/process-executor.d.ts +0 -47
  217. package/dist/process/process-executor.js +0 -296
  218. package/dist/profile/client-health.d.ts +0 -14
  219. package/dist/profile/client-query.d.ts +0 -30
  220. package/dist/profile/client-query.js +0 -69
  221. package/dist/profile/client.d.ts +0 -26
  222. package/dist/profile/client.js +0 -31
  223. package/dist/project/closure.d.ts +0 -25
  224. package/dist/project/closure.js +0 -74
  225. package/dist/registry/store.d.ts +0 -42
  226. package/dist/registry/store.js +0 -43
  227. package/dist/runtime/clock.d.ts +0 -7
  228. package/dist/runtime/clock.js +0 -7
  229. package/dist/runtime/module-loader.d.ts +0 -2
  230. package/dist/runtime/module-loader.js +0 -1
  231. package/dist/runtime/process.d.ts +0 -8
  232. package/dist/runtime/process.js +0 -1
  233. package/dist/runtime/runner.d.ts +0 -234
  234. package/dist/runtime/runner.js +0 -609
  235. package/dist/schema/index.d.ts +0 -1
  236. package/dist/schema/index.js +0 -1
  237. package/dist/server/dispatcher.d.ts +0 -25
  238. package/dist/server/dispatcher.js +0 -54
  239. package/dist/service/local.d.ts +0 -27
  240. package/dist/service/local.js +0 -141
  241. package/dist/storage/driver.d.ts +0 -10
  242. package/dist/storage/driver.js +0 -10
  243. package/dist/storage/lazy.d.ts +0 -8
  244. package/dist/storage/lazy.js +0 -45
  245. package/dist/storage/lock-core.d.ts +0 -158
  246. package/dist/storage/lock-core.js +0 -872
  247. package/dist/utils/net.d.ts +0 -10
  248. package/dist/utils/net.js +0 -16
  249. package/dist/utils/process.d.ts +0 -10
  250. package/dist/utils/process.js +0 -21
  251. /package/dist/{filter → utils}/intent.d.ts +0 -0
  252. /package/dist/{filter → utils}/intent.js +0 -0
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { computeManifestDigest, parseJsonWithoutDuplicates } from "./digest.js";
4
4
  import { MANIFEST_FILE_NAME } from "./manifest.js";
5
+ import { ActionDockError, LOCKFILE_CORRUPTED, LOCKFILE_INVALID, LOCKFILE_READ_FAILED, UNSUPPORTED_LOCKFILE_VERSION, } from "../errors.js";
5
6
  export const LOCKFILE_NAME = "actiondock.lock.json";
6
7
  export const LOCKFILE_VERSION = 1;
7
8
  /**
@@ -18,23 +19,23 @@ export function loadLockfile(projectRoot) {
18
19
  raw = readFileSync(filePath, "utf-8");
19
20
  }
20
21
  catch (err) {
21
- throw new Error(`Failed to read lockfile at ${filePath}: ${err.message}`);
22
+ throw new ActionDockError(LOCKFILE_READ_FAILED, `Failed to read lockfile at ${filePath}: ${err.message}`);
22
23
  }
23
24
  let parsed;
24
25
  try {
25
26
  parsed = parseJsonWithoutDuplicates(raw);
26
27
  }
27
28
  catch (err) {
28
- throw new Error(`Corrupted or duplicate keys in lockfile at ${filePath}: ${err.message}`);
29
+ throw new ActionDockError(LOCKFILE_CORRUPTED, `Corrupted or duplicate keys in lockfile at ${filePath}: ${err.message}`);
29
30
  }
30
31
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
31
- throw new Error(`Invalid lockfile format in ${filePath}: expected a JSON object`);
32
+ throw new ActionDockError(LOCKFILE_INVALID, `Invalid lockfile format in ${filePath}: expected a JSON object`);
32
33
  }
33
34
  if (parsed.lockfileVersion !== LOCKFILE_VERSION) {
34
- throw new Error(`Unsupported lockfileVersion in ${filePath}: received '${parsed.lockfileVersion}', expected ${LOCKFILE_VERSION}`);
35
+ throw new ActionDockError(UNSUPPORTED_LOCKFILE_VERSION, `Unsupported lockfileVersion in ${filePath}: received '${parsed.lockfileVersion}', expected ${LOCKFILE_VERSION}`);
35
36
  }
36
37
  if (!parsed.packages || typeof parsed.packages !== "object" || Array.isArray(parsed.packages)) {
37
- throw new Error(`Invalid lockfile format in ${filePath}: 'packages' must be an object`);
38
+ throw new ActionDockError(LOCKFILE_INVALID, `Invalid lockfile format in ${filePath}: 'packages' must be an object`);
38
39
  }
39
40
  // 规范化并注入兼容字段
40
41
  for (const [key, item] of Object.entries(parsed.packages)) {
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
2
2
  import { basename, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { isPathOutsideBoundary, PACKAGE_ID_REGEX } from "../utils/index.js";
4
+ import { ActionDockError, MANIFEST_CORRUPTED, MANIFEST_INVALID, MANIFEST_READ_FAILED, UNSUPPORTED_MANIFEST_SCHEMA, } from "../errors.js";
4
5
  export const MANIFEST_FILE_NAME = "actiondock.json";
5
6
  export const ACTION_ID_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
6
7
  export const PLAYBOOK_ID_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
@@ -18,21 +19,21 @@ export function loadManifest(projectRoot) {
18
19
  raw = readFileSync(filePath, "utf-8");
19
20
  }
20
21
  catch (err) {
21
- throw new Error(`Failed to read manifest at ${filePath}: ${err.message}`);
22
+ throw new ActionDockError(MANIFEST_READ_FAILED, `Failed to read manifest at ${filePath}: ${err.message}`);
22
23
  }
23
24
  let parsed;
24
25
  try {
25
26
  parsed = JSON.parse(raw);
26
27
  }
27
28
  catch (err) {
28
- throw new Error(`Corrupted JSON in manifest at ${filePath}: ${err.message}`);
29
+ throw new ActionDockError(MANIFEST_CORRUPTED, `Corrupted JSON in manifest at ${filePath}: ${err.message}`);
29
30
  }
30
31
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
31
- throw new Error(`Invalid manifest format in ${filePath}: expected a JSON object`);
32
+ throw new ActionDockError(MANIFEST_INVALID, `Invalid manifest format in ${filePath}: expected a JSON object`);
32
33
  }
33
34
  if (parsed.schemaVersion !== undefined &&
34
35
  (typeof parsed.schemaVersion !== "number" || parsed.schemaVersion < 1 || parsed.schemaVersion > 2)) {
35
- throw new Error(`Unsupported manifest schemaVersion in ${filePath}: received '${parsed.schemaVersion}', expected 2`);
36
+ throw new ActionDockError(UNSUPPORTED_MANIFEST_SCHEMA, `Unsupported manifest schemaVersion in ${filePath}: received '${parsed.schemaVersion}', expected 2`);
36
37
  }
37
38
  if (!parsed.id) {
38
39
  parsed.id = basename(projectRoot);
@@ -20,43 +20,10 @@ export interface ProjectTransaction {
20
20
  }): Promise<void>;
21
21
  releaseLock(): void;
22
22
  }
23
- /**
24
- * 检查当前进程 PID 是否处于活跃运行状态。
25
- */
26
- export declare function isPidAlive(pid: number): boolean;
27
- /**
28
- * 从工程锁隔离或回滚临时目录名称中解析操作者 PID 与隔离生成时间戳。
29
- * 命名规范:*.(quarantine|rollback|release).<pid>.<timestamp>.<uuid> 或 *.orphan.<timestamp>.<uuid>
30
- */
31
- export declare function parseProjectQuarantineTimestamp(entryName: string): {
32
- operatorPid?: number;
33
- timestamp?: number;
34
- };
35
- export declare const parseQuarantineTimestamp: typeof parseProjectQuarantineTimestamp;
36
- /**
37
- * 安全清理陈旧接管守卫(reclaim guard)。
38
- * 采用原子重命名检疫并严格核对 guardToken 与持有者存活状态,确保仅删除目标陈旧 guard,严禁误删活跃守卫或并发新守卫。
39
- *
40
- * @param reclaimPath 目标守卫路径
41
- * @param expectedGuardToken 预期持有的守卫令牌(必填,拒绝未指定令牌的盲目清理)
42
- */
43
- export declare function safeRemoveStaleProjectReclaimGuard(reclaimPath: string, expectedGuardToken: string): void;
44
- /**
45
- * 安全回滚当前进程创建的工程修改锁。
46
- * 优先核对 lockToken,回退核对 sessionToken,防止误删接管者或并发新锁。
47
- */
48
- export declare function safeRollbackProjectLock(lockPath: string, expectedSessionToken: string, expectedLockToken?: string): void;
49
- /**
50
- * 清理过期的工程锁隔离目录(GC 回收机制)。
51
- * 实施分流存活校验,防止误删活跃持锁者与回滚中目录,同时防止孤儿目录无限泄漏,语义由目录锁内核统一承载。
52
- */
53
- export declare function cleanStaleProjectQuarantines(parentDir: string, basePrefix: string, maxAgeMs?: number, deadline?: number): void;
54
23
  /**
55
24
  * 安全释放工程主锁。
56
- * 仅当锁目录中持有当前 lockToken / sessionToken 时才删除,杜绝删除他人新锁。
57
- * 释放后同步触发一轮工程锁隔离目录 GC。
58
25
  */
59
- export declare function safeReleaseProjectLock(lockPath: string, expectedSessionToken: string, expectedLockToken?: string): void;
26
+ export declare function safeReleaseProjectLock(lockPath: string, expectedSessionToken?: string, expectedLockToken?: string): void;
60
27
  /**
61
28
  * 检查项目修改锁 .actiondock/project.lock 是否存在且持有者 PID 处于存活状态。
62
29
  *
@@ -66,9 +33,7 @@ export declare function safeReleaseProjectLock(lockPath: string, expectedSession
66
33
  export declare function isProjectLockHeld(projectRoot: string, excludeSelf?: boolean): boolean;
67
34
  /**
68
35
  * 获取工程修改排他锁(.actiondock/project.lock)。
69
- * 引入所有竞争者均遵守的原子 reclaim guard 机制(project.lock.reclaim)。
70
- * 采用 mkdirSync 原子排他目录创建、renameSync 元数据写入、宽限期重试检测与基于 sessionToken 的释放函数。
71
- * 避免空文件窗口与 truncate。
36
+ * 基于统一轻量文件锁原语实现,非阻塞且支持原子抢占与崩溃恢复。
72
37
  *
73
38
  * @param projectRoot 项目根目录
74
39
  * @param options 锁配置参数
@@ -78,9 +43,9 @@ export declare function acquireProjectLock(projectRoot: string, options?: {
78
43
  acquireTimeoutMs?: number;
79
44
  }): () => void;
80
45
  /**
81
- * 依据恢复后的锁文件执行禁用安装脚本的冻结安装,使 node_modules 与声明重新一致。
46
+ * 依据恢复后的锁文件异步执行禁用安装脚本的冻结安装,使 node_modules 与声明重新一致。
82
47
  */
83
- export declare function runFrozenInstall(projectRoot: string): void;
48
+ export declare function runFrozenInstall(projectRoot: string): Promise<void>;
84
49
  /**
85
50
  * 检查项目是否存在未完成提交的悬空事务。
86
51
  */
@@ -92,9 +57,6 @@ export declare function hasPendingTransactions(projectRoot: string): boolean;
92
57
  export declare function beginTransaction(projectRoot: string, description?: string): Promise<ProjectTransaction>;
93
58
  /**
94
59
  * 依据事务日志恢复所有待恢复的悬空事务快照,并在成功后执行冻结安装。
95
- * 进入恢复前通过 acquireProjectLock 原子获取排他锁,持有锁执行恢复,完成后通过释放函数安全释放锁。
96
- * 若获取锁失败(已有活跃进程正在执行)则安全退出并返回空数组,杜绝 TOCTOU 竞争。
97
- * 若冻结安装失败则抛出 PROJECT_RECOVERY_REQUIRED 并阻止 Host 启动。
98
60
  */
99
61
  export declare function recoverPendingTransactions(projectRoot: string, options?: {
100
62
  frozenInstall?: boolean;
@@ -1,8 +1,9 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { acquireDirectoryLock, checkReclaimGuard, cleanStaleQuarantines, isProcessAlive, parseQuarantineTimestamp as parseQuarantineTimestampCore, readLockWithGracePeriod, safeReleaseLock as coreSafeReleaseLock, safeRemoveStaleReclaimGuard, safeRollbackLock, } from "../storage/lock-core.js";
5
+ import { acquireFileLockSync, isLockHeld, safeReleaseFileLockSync, } from "../storage/file-lock.js";
6
+ import { isProcessAlive } from "../utils/index.js";
6
7
  import { ActionDockError, PROJECT_BUSY, PROJECT_RECOVERY_REQUIRED } from "../errors.js";
7
8
  import { LOCKFILE_NAME } from "./lockfile.js";
8
9
  import { MANIFEST_FILE_NAME } from "./manifest.js";
@@ -18,51 +19,11 @@ export const SNAPSHOT_TRACKED_FILES = [
18
19
  "yarn.lock",
19
20
  ];
20
21
  const LOCK_DIR_NAME = "project.lock";
21
- /**
22
- * 检查当前进程 PID 是否处于活跃运行状态。
23
- */
24
- export function isPidAlive(pid) {
25
- return isProcessAlive(pid);
26
- }
27
- /**
28
- * 从工程锁隔离或回滚临时目录名称中解析操作者 PID 与隔离生成时间戳。
29
- * 命名规范:*.(quarantine|rollback|release).<pid>.<timestamp>.<uuid> 或 *.orphan.<timestamp>.<uuid>
30
- */
31
- export function parseProjectQuarantineTimestamp(entryName) {
32
- return parseQuarantineTimestampCore(entryName);
33
- }
34
- export const parseQuarantineTimestamp = parseProjectQuarantineTimestamp;
35
- /**
36
- * 安全清理陈旧接管守卫(reclaim guard)。
37
- * 采用原子重命名检疫并严格核对 guardToken 与持有者存活状态,确保仅删除目标陈旧 guard,严禁误删活跃守卫或并发新守卫。
38
- *
39
- * @param reclaimPath 目标守卫路径
40
- * @param expectedGuardToken 预期持有的守卫令牌(必填,拒绝未指定令牌的盲目清理)
41
- */
42
- export function safeRemoveStaleProjectReclaimGuard(reclaimPath, expectedGuardToken) {
43
- safeRemoveStaleReclaimGuard(reclaimPath, expectedGuardToken);
44
- }
45
- /**
46
- * 安全回滚当前进程创建的工程修改锁。
47
- * 优先核对 lockToken,回退核对 sessionToken,防止误删接管者或并发新锁。
48
- */
49
- export function safeRollbackProjectLock(lockPath, expectedSessionToken, expectedLockToken) {
50
- safeRollbackLock(lockPath, expectedSessionToken, expectedLockToken);
51
- }
52
- /**
53
- * 清理过期的工程锁隔离目录(GC 回收机制)。
54
- * 实施分流存活校验,防止误删活跃持锁者与回滚中目录,同时防止孤儿目录无限泄漏,语义由目录锁内核统一承载。
55
- */
56
- export function cleanStaleProjectQuarantines(parentDir, basePrefix, maxAgeMs = 10000, deadline) {
57
- cleanStaleQuarantines(parentDir, basePrefix, maxAgeMs, deadline);
58
- }
59
22
  /**
60
23
  * 安全释放工程主锁。
61
- * 仅当锁目录中持有当前 lockToken / sessionToken 时才删除,杜绝删除他人新锁。
62
- * 释放后同步触发一轮工程锁隔离目录 GC。
63
24
  */
64
25
  export function safeReleaseProjectLock(lockPath, expectedSessionToken, expectedLockToken) {
65
- coreSafeReleaseLock(lockPath, expectedSessionToken, expectedLockToken, { gcAfterRelease: true });
26
+ safeReleaseFileLockSync(lockPath, expectedLockToken, expectedSessionToken);
66
27
  }
67
28
  /**
68
29
  * 检查项目修改锁 .actiondock/project.lock 是否存在且持有者 PID 处于存活状态。
@@ -72,49 +33,16 @@ export function safeReleaseProjectLock(lockPath, expectedSessionToken, expectedL
72
33
  */
73
34
  export function isProjectLockHeld(projectRoot, excludeSelf = true) {
74
35
  const lockPath = join(projectRoot, ".actiondock", LOCK_DIR_NAME);
75
- const reclaimPath = `${lockPath}.reclaim`;
76
- if (existsSync(reclaimPath)) {
77
- const reclaimState = checkReclaimGuard(reclaimPath, 1000);
78
- if (reclaimState.active) {
79
- if (!excludeSelf || reclaimState.holderPid !== process.pid) {
80
- return true;
81
- }
82
- }
83
- }
84
- if (!existsSync(lockPath)) {
85
- return false;
86
- }
87
- try {
88
- const lockState = readLockWithGracePeriod(lockPath, 3000);
89
- if (!lockState.exists) {
90
- return false;
91
- }
92
- if (lockState.inGracePeriod) {
93
- return true;
94
- }
95
- if (lockState.info && typeof lockState.info.pid === "number") {
96
- if (excludeSelf && lockState.info.pid === process.pid) {
97
- return false;
98
- }
99
- return isProcessAlive(lockState.info.pid);
100
- }
101
- }
102
- catch {
103
- // 忽略读取解析异常
104
- }
105
- return false;
36
+ return isLockHeld(lockPath, excludeSelf);
106
37
  }
107
38
  /**
108
39
  * 获取工程修改排他锁(.actiondock/project.lock)。
109
- * 引入所有竞争者均遵守的原子 reclaim guard 机制(project.lock.reclaim)。
110
- * 采用 mkdirSync 原子排他目录创建、renameSync 元数据写入、宽限期重试检测与基于 sessionToken 的释放函数。
111
- * 避免空文件窗口与 truncate。
40
+ * 基于统一轻量文件锁原语实现,非阻塞且支持原子抢占与崩溃恢复。
112
41
  *
113
42
  * @param projectRoot 项目根目录
114
43
  * @param options 锁配置参数
115
44
  */
116
45
  export function acquireProjectLock(projectRoot, options = {}) {
117
- const timeoutMs = options.acquireTimeoutMs ?? 5000;
118
46
  const metaDir = join(projectRoot, ".actiondock");
119
47
  if (!existsSync(metaDir)) {
120
48
  mkdirSync(metaDir, { recursive: true });
@@ -122,55 +50,84 @@ export function acquireProjectLock(projectRoot, options = {}) {
122
50
  const lockPath = join(metaDir, LOCK_DIR_NAME);
123
51
  const sessionToken = options.sessionToken || randomUUID();
124
52
  const lockToken = randomUUID();
125
- const currentPid = process.pid;
126
53
  const lockData = {
127
- pid: currentPid,
54
+ pid: process.pid,
128
55
  sessionToken,
129
56
  lockToken,
130
57
  createdAt: Date.now(),
131
58
  };
132
- const content = JSON.stringify(lockData, null, 2);
133
- acquireDirectoryLock({
134
- lockPath,
135
- parentDir: metaDir,
136
- basePrefix: LOCK_DIR_NAME,
137
- metadataContent: content,
138
- sessionToken,
139
- lockToken,
140
- acquireTimeoutMs: options.acquireTimeoutMs,
141
- createLockError(message) {
142
- return new ActionDockError(PROJECT_BUSY, message);
143
- },
144
- messages: {
145
- timeoutWaitingReclaimGuard: (holderPid) => `PROJECT_BUSY: Timeout waiting for active reclaim guard (PID ${holderPid ?? "unknown"}) on project '${projectRoot}' after ${timeoutMs}ms`,
146
- timeoutAcquireBlockedByGuard: (holderPid) => `PROJECT_BUSY: Timeout acquiring project lock on '${projectRoot}' due to active reclaim guard (PID ${holderPid}) after ${timeoutMs}ms`,
147
- timeoutAcquire: () => `PROJECT_BUSY: Timeout acquiring project lock on '${projectRoot}' after ${timeoutMs}ms`,
148
- timeoutGracePeriod: () => `PROJECT_BUSY: Timeout waiting for project lock grace period on '${projectRoot}' after ${timeoutMs}ms`,
149
- timeoutGuardContention: () => `PROJECT_BUSY: Timeout contending for project reclaim guard on '${projectRoot}' after ${timeoutMs}ms`,
150
- timeoutReclaimStale: () => `PROJECT_BUSY: Timeout reclaiming stale project lock on '${projectRoot}' after ${timeoutMs}ms`,
151
- timeoutCreate: () => `PROJECT_BUSY: Timeout creating project lock on '${projectRoot}' after ${timeoutMs}ms`,
59
+ const handle = acquireFileLockSync(lockPath, {
60
+ metadata: lockData,
61
+ createLockError() {
62
+ return new ActionDockError(PROJECT_BUSY, `PROJECT_BUSY: Project modification lock is held by another process. Another command is running in ${projectRoot}.`);
152
63
  },
153
- assertStaleHolderReclaimable(info) {
64
+ assertHolderReclaimable(info) {
154
65
  const holderPid = info.pid;
155
- if (isProcessAlive(holderPid)) {
66
+ if (holderPid && isProcessAlive(holderPid)) {
156
67
  throw new ActionDockError(PROJECT_BUSY, `PROJECT_BUSY: Project modification lock is held by PID ${holderPid}. Another command is running in ${projectRoot}.`);
157
68
  }
158
69
  },
159
- onAcquired: () => true,
160
70
  });
161
71
  let released = false;
162
72
  return () => {
163
73
  if (released)
164
74
  return;
165
75
  released = true;
166
- safeReleaseProjectLock(lockPath, sessionToken, lockToken);
167
- cleanStaleQuarantines(metaDir, LOCK_DIR_NAME);
76
+ handle.release();
168
77
  };
169
78
  }
170
79
  /**
171
- * 依据恢复后的锁文件执行禁用安装脚本的冻结安装,使 node_modules 与声明重新一致。
80
+ * 异步执行子进程并收集退出状态与输出流。
81
+ *
82
+ * 异步化取舍说明:
83
+ * - 避免在事务回滚与恢复链路中执行同步阻塞调用导致事件循环停顿;
84
+ * - Windows 兼容性保留:npm 在 Windows 上是 .cmd 批处理脚本,启用 shell 以防直接调用抛错;
85
+ * - 命令与参数由系统内部锁文件逻辑决定,不接受外部未受控入参。
86
+ */
87
+ function execSubprocessAsync(command, args, options) {
88
+ return new Promise((resolve, reject) => {
89
+ try {
90
+ const proc = spawn(command, args, {
91
+ cwd: options?.cwd,
92
+ stdio: "pipe",
93
+ shell: process.platform === "win32",
94
+ env: options?.env,
95
+ });
96
+ let stdout = "";
97
+ let stderr = "";
98
+ if (proc.stdout) {
99
+ proc.stdout.on("data", (chunk) => {
100
+ stdout += chunk.toString();
101
+ });
102
+ }
103
+ if (proc.stderr) {
104
+ proc.stderr.on("data", (chunk) => {
105
+ stderr += chunk.toString();
106
+ });
107
+ }
108
+ let settled = false;
109
+ proc.on("error", (err) => {
110
+ if (!settled) {
111
+ settled = true;
112
+ reject(err);
113
+ }
114
+ });
115
+ proc.on("close", (status) => {
116
+ if (!settled) {
117
+ settled = true;
118
+ resolve({ status, stdout, stderr });
119
+ }
120
+ });
121
+ }
122
+ catch (err) {
123
+ reject(err);
124
+ }
125
+ });
126
+ }
127
+ /**
128
+ * 依据恢复后的锁文件异步执行禁用安装脚本的冻结安装,使 node_modules 与声明重新一致。
172
129
  */
173
- export function runFrozenInstall(projectRoot) {
130
+ export async function runFrozenInstall(projectRoot) {
174
131
  if (process.env.ACTIONDOCK_AUTO_INSTALL === "false") {
175
132
  return;
176
133
  }
@@ -191,12 +148,7 @@ export function runFrozenInstall(projectRoot) {
191
148
  args = ["ci", "--ignore-scripts"];
192
149
  }
193
150
  try {
194
- const check = spawnSync(cmd, ["--version"], {
195
- stdio: "pipe",
196
- // Windows 兼容:npm 为 .cmd 脚本,无 shell 直接 spawn 必然 ENOENT,
197
- // 会误判 npm 缺失而把冻结安装降级为普通 install
198
- shell: process.platform === "win32",
199
- });
151
+ const check = await execSubprocessAsync(cmd, ["--version"]);
200
152
  if (check.status !== 0) {
201
153
  cmd = "npm";
202
154
  args = ["install", "--ignore-scripts"];
@@ -206,20 +158,23 @@ export function runFrozenInstall(projectRoot) {
206
158
  cmd = "npm";
207
159
  args = ["install", "--ignore-scripts"];
208
160
  }
209
- const proc = spawnSync(cmd, args, {
210
- cwd: projectRoot,
211
- stdio: "pipe",
212
- shell: process.platform === "win32",
213
- env: {
214
- ...process.env,
215
- // 项目级安装禁用 npm 的 allow-scripts 白名单机制,避免用户全局 .npmrc
216
- // 的 allow-scripts 约束触发 EALLOWSCRIPTS 导致恢复失败(本安装已带 --ignore-scripts)
217
- npm_config_allow_scripts: "",
218
- NPM_CONFIG_ALLOW_SCRIPTS: "",
219
- },
220
- });
161
+ let proc;
162
+ try {
163
+ proc = await execSubprocessAsync(cmd, args, {
164
+ cwd: projectRoot,
165
+ env: {
166
+ ...process.env,
167
+ npm_config_allow_scripts: "",
168
+ NPM_CONFIG_ALLOW_SCRIPTS: "",
169
+ },
170
+ });
171
+ }
172
+ catch (err) {
173
+ const errorMsg = err?.message || "Unknown error";
174
+ throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Frozen install failed during recovery in ${projectRoot}: ${errorMsg}`);
175
+ }
221
176
  if (proc.status !== 0) {
222
- const errorMsg = proc.stderr?.toString() || proc.stdout?.toString() || "Unknown error";
177
+ const errorMsg = proc.stderr || proc.stdout || "Unknown error";
223
178
  throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Frozen install failed during recovery in ${projectRoot}: ${errorMsg}`);
224
179
  }
225
180
  }
@@ -291,7 +246,6 @@ export async function beginTransaction(projectRoot, description) {
291
246
  meta.status = "committed";
292
247
  try {
293
248
  writeFileSync(join(txDir, "transaction.json"), JSON.stringify(meta, null, 2) + "\n", "utf-8");
294
- // 成功提交后清理事务目录
295
249
  rmSync(txDir, { recursive: true, force: true });
296
250
  }
297
251
  finally {
@@ -303,7 +257,6 @@ export async function beginTransaction(projectRoot, description) {
303
257
  return;
304
258
  isFinalized = true;
305
259
  try {
306
- // 逐个恢复快照文件
307
260
  for (const rec of fileRecords) {
308
261
  const targetPath = join(projectRoot, rec.name);
309
262
  const snapPath = join(snapshotDir, rec.name);
@@ -313,15 +266,13 @@ export async function beginTransaction(projectRoot, description) {
313
266
  }
314
267
  }
315
268
  else {
316
- // 原先不存在的文件若在事务中被创建,予以删除
317
269
  if (existsSync(targetPath)) {
318
270
  unlinkSync(targetPath);
319
271
  }
320
272
  }
321
273
  }
322
- // 执行冻结安装使 node_modules 与恢复后的声明重新一致
323
274
  if (options?.frozenInstall !== false) {
324
- runFrozenInstall(projectRoot);
275
+ await runFrozenInstall(projectRoot);
325
276
  }
326
277
  meta.status = "rolled_back";
327
278
  writeFileSync(join(txDir, "transaction.json"), JSON.stringify(meta, null, 2) + "\n", "utf-8");
@@ -347,9 +298,6 @@ export async function beginTransaction(projectRoot, description) {
347
298
  }
348
299
  /**
349
300
  * 依据事务日志恢复所有待恢复的悬空事务快照,并在成功后执行冻结安装。
350
- * 进入恢复前通过 acquireProjectLock 原子获取排他锁,持有锁执行恢复,完成后通过释放函数安全释放锁。
351
- * 若获取锁失败(已有活跃进程正在执行)则安全退出并返回空数组,杜绝 TOCTOU 竞争。
352
- * 若冻结安装失败则抛出 PROJECT_RECOVERY_REQUIRED 并阻止 Host 启动。
353
301
  */
354
302
  export async function recoverPendingTransactions(projectRoot, options) {
355
303
  const txBaseDir = join(projectRoot, ".actiondock", "transactions");
@@ -362,10 +310,8 @@ export async function recoverPendingTransactions(projectRoot, options) {
362
310
  }
363
311
  catch (err) {
364
312
  if (err?.code === "PROJECT_BUSY") {
365
- // 仅当项目锁被其他活跃进程占用时,安全退出并返回空数组
366
313
  return [];
367
314
  }
368
- // 底层系统异常(如 EACCES、ENOSPC、ENOENT 等),向外抛出,严禁静默吞掉
369
315
  throw err;
370
316
  }
371
317
  try {
@@ -385,7 +331,6 @@ export async function recoverPendingTransactions(projectRoot, options) {
385
331
  }
386
332
  if (meta.status === "pending") {
387
333
  const snapshotDir = join(txDir, "snapshot");
388
- // 逐个恢复快照文件
389
334
  for (const rec of meta.files) {
390
335
  const targetPath = join(projectRoot, rec.name);
391
336
  const snapPath = join(snapshotDir, rec.name);
@@ -401,7 +346,7 @@ export async function recoverPendingTransactions(projectRoot, options) {
401
346
  }
402
347
  }
403
348
  if (options?.frozenInstall !== false) {
404
- runFrozenInstall(projectRoot);
349
+ await runFrozenInstall(projectRoot);
405
350
  }
406
351
  meta.status = "rolled_back";
407
352
  try {
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { computeManifestDigest } from "./digest.js";
4
4
  import { loadManifest } from "./manifest.js";
5
+ import { ActionDockError, MANIFEST_NOT_FOUND } from "../errors.js";
5
6
  export const GENERATED_TYPES_REL_PATH = join(".actiondock", "generated", "actions.d.ts");
6
7
  export const GENERATED_TYPES_OUTDATED_CODE = "GENERATED_TYPES_OUTDATED";
7
8
  /**
@@ -118,7 +119,7 @@ export type TypedActionDefinition<T extends keyof ActionSchemaMap> = ActionDefin
118
119
  export function writeActionTypes(projectRoot, manifest) {
119
120
  const targetManifest = manifest || loadManifest(projectRoot);
120
121
  if (!targetManifest) {
121
- throw new Error(`actiondock.json not found in ${projectRoot}`);
122
+ throw new ActionDockError(MANIFEST_NOT_FOUND, `actiondock.json not found in ${projectRoot}`);
122
123
  }
123
124
  const { code, digest } = generateActionTypes(targetManifest);
124
125
  const fullPath = join(projectRoot, GENERATED_TYPES_REL_PATH);
@@ -46,6 +46,8 @@ export interface ActionManifestEntry {
46
46
  export interface PlaybookManifestEntry {
47
47
  /** Playbook Markdown 入口文件相对路径(如 "playbooks/greet-user.md") */
48
48
  entry: string;
49
+ /** Playbook 展示名称,缺省回落文件名派生 */
50
+ name?: string;
49
51
  /** Playbook 任务描述 */
50
52
  description?: string;
51
53
  /** 该 Playbook 所依赖/调用的 Action ID 列表 */
@@ -2,6 +2,5 @@ export * from "./lock.js";
2
2
  export type { RegistryLockOptions } from "./lock.js";
3
3
  export * from "./registry.js";
4
4
  export * from "./scan.js";
5
- export * from "./store.js";
6
5
  export * from "./types.js";
7
6
  export { getActionDockHome, getPackageSlug, } from "../utils/index.js";
@@ -1,6 +1,5 @@
1
1
  export * from "./lock.js";
2
2
  export * from "./registry.js";
3
3
  export * from "./scan.js";
4
- export * from "./store.js";
5
4
  export * from "./types.js";
6
5
  export { getActionDockHome, getPackageSlug, } from "../utils/index.js";
@@ -14,8 +14,7 @@ export declare const REGISTRY_LOCK_ACQUIRE_TIMEOUT_MS = 15000;
14
14
  export declare const REGISTRY_LOCK_RETRY_DELAY_MS = 25;
15
15
  /**
16
16
  * 持锁期间的心跳续期间隔。持有者定期 touch 锁目录刷新 mtime,
17
- * 使长耗时持锁操作(如 linkPackage 的深度扫描)不会仅因耗时超过
18
- * stale 阈值而被等待方误判为残留锁抢占。
17
+ * 使长耗时持锁操作不会仅因耗时超过 stale 阈值而被等待方误判为残留锁抢占。
19
18
  */
20
19
  export declare const REGISTRY_LOCK_HEARTBEAT_MS = 3000;
21
20
  /**
@@ -29,15 +28,7 @@ export interface RegistryLockOptions {
29
28
  }
30
29
  /**
31
30
  * 异步注册表锁(防止多进程并发读写导致 registry.json 损坏)。
32
- *
33
- * 获取方式:fs/promises mkdir 的原子性保证同一时刻仅一个进程能创建锁目录,
34
- * 创建成功后立即写入含 pid 与随机 token 的 metadata.json。
35
- * 冲突处理:EEXIST 时按保守语义判定残留(见 isStaleLock):mtime 超龄且
36
- * pid 已死才抢占——抢占仍通过原子 rename 到隔离名实现,rename 仅有一个
37
- * 进程能成功,失败方回到等待重试,消除「检查后删除再重建」的竞态窗口;
38
- * 未过期的锁通过 sleep 让出事件循环后重试。
39
- * 心跳续期:持锁期间以固定间隔 touch 锁目录刷新 mtime,使 linkPackage 等
40
- * 长耗时持锁操作不会仅因耗时被误判残留;fn 完成后停止心跳并删除锁目录。
31
+ * 基于统一轻量文件锁原语实现。
41
32
  *
42
33
  * @param filePath 注册表文件路径,锁目录为其同级 `${filePath}.lock`
43
34
  * @param fn 持锁期间执行的操作(同步或异步)