@actiondock/core 2.0.12 → 2.2.1

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 (192) hide show
  1. package/README.md +61 -30
  2. package/dist/app/app.d.ts +73 -0
  3. package/dist/app/app.js +700 -0
  4. package/dist/app/index.d.ts +2 -0
  5. package/dist/app/index.js +2 -0
  6. package/dist/app/types.d.ts +283 -0
  7. package/dist/app/types.js +1 -0
  8. package/dist/catalog/action-index.d.ts +1 -1
  9. package/dist/catalog/action-index.js +93 -0
  10. package/dist/catalog/action-resolver.d.ts +2 -2
  11. package/dist/catalog/action-resolver.js +95 -0
  12. package/dist/catalog/index.d.ts +5 -5
  13. package/dist/catalog/index.js +5 -0
  14. package/dist/catalog/location-registry.d.ts +5 -5
  15. package/dist/catalog/location-registry.js +138 -0
  16. package/dist/catalog/package-catalog.d.ts +2 -2
  17. package/dist/catalog/package-catalog.js +76 -0
  18. package/dist/catalog/types.d.ts +2 -2
  19. package/dist/catalog/types.js +1 -0
  20. package/dist/doctor/doctor.d.ts +2 -1
  21. package/dist/doctor/doctor.js +613 -0
  22. package/dist/doctor/index.d.ts +2 -2
  23. package/dist/doctor/index.js +2 -0
  24. package/dist/doctor/types.js +1 -0
  25. package/dist/errors.d.ts +122 -0
  26. package/dist/errors.js +123 -0
  27. package/dist/execution/index.d.ts +2 -2
  28. package/dist/execution/index.js +2 -0
  29. package/dist/execution/service.d.ts +46 -41
  30. package/dist/execution/service.js +633 -0
  31. package/dist/execution/types.d.ts +59 -2
  32. package/dist/execution/types.js +1 -0
  33. package/dist/export/index.d.ts +1 -1
  34. package/dist/export/index.js +1 -0
  35. package/dist/export/templates.d.ts +45 -7
  36. package/dist/export/templates.js +631 -0
  37. package/dist/filter/index.d.ts +1 -1
  38. package/dist/filter/index.js +1 -0
  39. package/dist/filter/intent.js +123 -0
  40. package/dist/host/host.d.ts +55 -0
  41. package/dist/host/host.js +740 -0
  42. package/dist/host/index.d.ts +2 -0
  43. package/dist/host/index.js +2 -0
  44. package/dist/host/types.d.ts +81 -0
  45. package/dist/host/types.js +1 -0
  46. package/dist/index.d.ts +21 -16
  47. package/dist/index.js +20 -8458
  48. package/dist/ipc/diagnostic.d.ts +65 -0
  49. package/dist/ipc/diagnostic.js +129 -0
  50. package/dist/ipc/host.d.ts +7 -0
  51. package/dist/ipc/host.js +93 -0
  52. package/dist/ipc/index.d.ts +4 -0
  53. package/dist/ipc/index.js +4 -0
  54. package/dist/ipc/target.d.ts +56 -0
  55. package/dist/ipc/target.js +297 -0
  56. package/dist/ipc/types.d.ts +42 -0
  57. package/dist/ipc/types.js +1 -0
  58. package/dist/platform/default.d.ts +32 -0
  59. package/dist/platform/default.js +40 -0
  60. package/dist/platform/index.d.ts +3 -0
  61. package/dist/platform/index.js +3 -0
  62. package/dist/platform/node-fs.d.ts +37 -0
  63. package/dist/platform/node-fs.js +81 -0
  64. package/dist/platform/types.d.ts +104 -0
  65. package/dist/platform/types.js +1 -0
  66. package/dist/profile/client.d.ts +58 -12
  67. package/dist/profile/client.js +467 -0
  68. package/dist/profile/index.d.ts +3 -3
  69. package/dist/profile/index.js +3 -0
  70. package/dist/profile/manager.d.ts +2 -2
  71. package/dist/profile/manager.js +294 -0
  72. package/dist/profile/types.d.ts +1 -1
  73. package/dist/profile/types.js +1 -0
  74. package/dist/project/closure.js +68 -0
  75. package/dist/project/digest.d.ts +29 -0
  76. package/dist/project/digest.js +324 -0
  77. package/dist/project/index.d.ts +10 -5
  78. package/dist/project/index.js +10 -0
  79. package/dist/project/init.d.ts +3 -4
  80. package/dist/project/init.js +175 -0
  81. package/dist/project/loader.d.ts +21 -27
  82. package/dist/project/loader.js +484 -0
  83. package/dist/project/lockfile.d.ts +55 -0
  84. package/dist/project/lockfile.js +175 -0
  85. package/dist/project/manifest.d.ts +6 -28
  86. package/dist/project/manifest.js +220 -0
  87. package/dist/project/resolver.d.ts +116 -0
  88. package/dist/project/resolver.js +376 -0
  89. package/dist/project/transactions.d.ts +103 -0
  90. package/dist/project/transactions.js +1203 -0
  91. package/dist/project/types-generator.d.ts +33 -0
  92. package/dist/project/types-generator.js +168 -0
  93. package/dist/project/types.d.ts +58 -82
  94. package/dist/project/types.js +1 -0
  95. package/dist/registry/index.d.ts +5 -2
  96. package/dist/registry/index.js +5 -0
  97. package/dist/registry/lock.d.ts +27 -0
  98. package/dist/registry/lock.js +93 -0
  99. package/dist/registry/registry.d.ts +32 -11
  100. package/dist/registry/registry.js +611 -0
  101. package/dist/registry/resolve.d.ts +62 -0
  102. package/dist/registry/resolve.js +195 -0
  103. package/dist/registry/scan.d.ts +38 -0
  104. package/dist/registry/scan.js +135 -0
  105. package/dist/registry/types.d.ts +1 -1
  106. package/dist/registry/types.js +1 -0
  107. package/dist/runtime/action-collection.d.ts +27 -0
  108. package/dist/runtime/action-collection.js +57 -0
  109. package/dist/runtime/clock.d.ts +0 -2
  110. package/dist/runtime/clock.js +17 -0
  111. package/dist/runtime/context.d.ts +16 -5
  112. package/dist/runtime/context.js +242 -0
  113. package/dist/runtime/env.d.ts +3 -1
  114. package/dist/runtime/env.js +159 -0
  115. package/dist/runtime/events.d.ts +46 -14
  116. package/dist/runtime/events.js +604 -0
  117. package/dist/runtime/execution-manager.d.ts +1 -1
  118. package/dist/runtime/execution-manager.js +66 -0
  119. package/dist/runtime/index.d.ts +10 -9
  120. package/dist/runtime/index.js +10 -0
  121. package/dist/runtime/module-loader.d.ts +0 -8
  122. package/dist/runtime/module-loader.js +16 -0
  123. package/dist/runtime/process.d.ts +2 -4
  124. package/dist/runtime/process.js +146 -0
  125. package/dist/runtime/runner.d.ts +117 -10
  126. package/dist/runtime/runner.js +984 -0
  127. package/dist/runtime/standalone.d.ts +68 -19
  128. package/dist/runtime/standalone.js +536 -0
  129. package/dist/schema/validator.js +122 -0
  130. package/dist/server/body.js +93 -0
  131. package/dist/server/index.d.ts +5 -6
  132. package/dist/server/index.js +5 -0
  133. package/dist/server/routes/actions.d.ts +2 -2
  134. package/dist/server/routes/actions.js +298 -0
  135. package/dist/server/routes/common.d.ts +28 -17
  136. package/dist/server/routes/common.js +104 -0
  137. package/dist/server/routes/config.d.ts +2 -1
  138. package/dist/server/routes/config.js +173 -0
  139. package/dist/server/routes/doctor.d.ts +2 -2
  140. package/dist/server/routes/doctor.js +63 -0
  141. package/dist/server/routes/health.d.ts +2 -2
  142. package/dist/server/routes/health.js +35 -0
  143. package/dist/server/routes/index.d.ts +9 -9
  144. package/dist/server/routes/index.js +9 -0
  145. package/dist/server/routes/info.d.ts +4 -2
  146. package/dist/server/routes/info.js +158 -0
  147. package/dist/server/routes/playbooks.d.ts +2 -2
  148. package/dist/server/routes/playbooks.js +134 -0
  149. package/dist/server/routes/runs.d.ts +2 -2
  150. package/dist/server/routes/runs.js +343 -0
  151. package/dist/server/routes/state.d.ts +2 -1
  152. package/dist/server/routes/state.js +169 -0
  153. package/dist/server/security.d.ts +11 -4
  154. package/dist/server/security.js +101 -0
  155. package/dist/server/server.d.ts +6 -6
  156. package/dist/server/server.js +355 -0
  157. package/dist/server/types.d.ts +27 -8
  158. package/dist/server/types.js +1 -0
  159. package/dist/storage/data-dir-lock.d.ts +119 -0
  160. package/dist/storage/data-dir-lock.js +1016 -0
  161. package/dist/storage/driver.d.ts +4 -10
  162. package/dist/storage/driver.js +59 -0
  163. package/dist/storage/index.d.ts +30 -10
  164. package/dist/storage/index.js +98 -0
  165. package/dist/storage/lazy.d.ts +8 -0
  166. package/dist/storage/lazy.js +44 -0
  167. package/dist/storage/mask.d.ts +12 -5
  168. package/dist/storage/mask.js +48 -0
  169. package/dist/storage/params.d.ts +13 -0
  170. package/dist/storage/params.js +32 -0
  171. package/dist/storage/sqlite.d.ts +19 -6
  172. package/dist/storage/sqlite.js +689 -0
  173. package/dist/storage/types.d.ts +57 -6
  174. package/dist/storage/types.js +14 -0
  175. package/dist/target/index.d.ts +4 -0
  176. package/dist/target/index.js +4 -0
  177. package/dist/target/local.d.ts +52 -0
  178. package/dist/target/local.js +343 -0
  179. package/dist/target/remote.d.ts +76 -0
  180. package/dist/target/remote.js +839 -0
  181. package/dist/target/target.d.ts +6 -0
  182. package/dist/target/target.js +35 -0
  183. package/dist/target/types.d.ts +246 -0
  184. package/dist/target/types.js +34 -0
  185. package/dist/utils/index.d.ts +22 -2
  186. package/dist/utils/index.js +180 -0
  187. package/dist/version.d.ts +1 -1
  188. package/dist/version.js +4 -0
  189. package/package.json +6 -7
  190. package/dist/build/index.d.ts +0 -1
  191. package/dist/build/templates.d.ts +0 -19
  192. package/dist/server/runtime-registry.d.ts +0 -78
@@ -0,0 +1,611 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from "node:fs";
2
+ import { readFile, rename, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { findProjectRoot, loadProjectConfig } from "../project/loader.js";
5
+ import { getActionDockHome, getPackageSlug } from "../utils/index.js";
6
+ import { withRegistryLock } from "./lock.js";
7
+ import { buildLinkedPackageEntry, discoverProjectConfigs, discoverProjectConfigsAsync, discoverProjects, pathExistsAsync, } from "./scan.js";
8
+ import { probeActionAsync, probeActionSync, probePlaybook, resolveEntityFlow, runFlowAsync, runFlowSync, } from "./resolve.js";
9
+ /**
10
+ * 空注册表初始结构。
11
+ */
12
+ function emptyRegistry() {
13
+ return { version: "2.0.0", packages: {}, workspaces: {} };
14
+ }
15
+ /**
16
+ * 注册表内容解析与 links 迁移(纯函数,同步与异步加载共享的唯一事实源)。
17
+ *
18
+ * @param raw 文件原始文本
19
+ * @param filePath 注册表文件路径(用于错误信息定位)
20
+ * @returns 解析后的注册表数据
21
+ * @throws JSON 非法或顶层非对象时抛出带恢复指引的错误(调用方负责留档原文件)
22
+ */
23
+ export function parseRegistryContent(raw, filePath) {
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(raw);
27
+ }
28
+ catch (err) {
29
+ throw new Error(`Registry file '${filePath}' is corrupted (invalid JSON: ${err.message}). ` +
30
+ `The original data has been preserved as '${filePath}.corrupt' and can be restored manually. ` +
31
+ `Fix or restore the file, or remove it to start with an empty registry.`);
32
+ }
33
+ if (!parsed || typeof parsed !== "object") {
34
+ throw new Error(`Registry file '${filePath}' is corrupted (top-level value is not an object). ` +
35
+ `The original data has been preserved as '${filePath}.corrupt' and can be restored manually. ` +
36
+ `Fix or restore the file, or remove it to start with an empty registry.`);
37
+ }
38
+ const packages = parsed.packages || {};
39
+ const workspaces = parsed.workspaces || {};
40
+ // 格式兼容与迁移:如果包含 schemaVersion: 1 的 links 但缺少 packages/workspaces
41
+ if (Array.isArray(parsed.links) && Object.keys(packages).length === 0 && Object.keys(workspaces).length === 0) {
42
+ for (const link of parsed.links) {
43
+ if (!link.path || !existsSync(link.path))
44
+ continue;
45
+ if (link.type === "workspace") {
46
+ workspaces[link.path] = {
47
+ path: resolve(link.path),
48
+ linkedAt: link.linkedAt || new Date().toISOString(),
49
+ };
50
+ }
51
+ else {
52
+ try {
53
+ const config = loadProjectConfig(link.path);
54
+ packages[config.id] = buildLinkedPackageEntry(config, resolve(link.path), link.linkedAt || new Date().toISOString());
55
+ }
56
+ catch {
57
+ // 迁移时项目已损坏或配置非法:跳过该条 link,避免迁移整体失败
58
+ }
59
+ }
60
+ }
61
+ }
62
+ return {
63
+ version: "2.0.0",
64
+ packages,
65
+ workspaces,
66
+ };
67
+ }
68
+ export function getRegistryFilePath(customHome) {
69
+ const baseDir = getActionDockHome(customHome);
70
+ return join(baseDir, ".actiondock", "registry.json");
71
+ }
72
+ /**
73
+ * 同步加载注册表(保留同步签名供 CLI 与诊断路径使用)。
74
+ *
75
+ * @throws 注册表文件损坏时抛出带恢复指引的错误,绝不静默返回空表,
76
+ * 否则下次 save 会用空数据覆盖原文件导致全部链接记录丢失
77
+ */
78
+ export function loadRegistry(customHome) {
79
+ const filePath = getRegistryFilePath(customHome);
80
+ if (!existsSync(filePath)) {
81
+ return emptyRegistry();
82
+ }
83
+ let raw;
84
+ try {
85
+ raw = readFileSync(filePath, "utf-8");
86
+ }
87
+ catch (err) {
88
+ if (err.code === "ENOENT") {
89
+ return emptyRegistry();
90
+ }
91
+ throw err;
92
+ }
93
+ try {
94
+ return parseRegistryContent(raw, filePath);
95
+ }
96
+ catch (err) {
97
+ archiveCorruptFile(filePath);
98
+ throw err;
99
+ }
100
+ }
101
+ /**
102
+ * 异步加载注册表(损坏语义与同步版一致:留档后抛错,不静默清空)。
103
+ */
104
+ export async function loadRegistryAsync(customHome) {
105
+ const filePath = getRegistryFilePath(customHome);
106
+ let raw;
107
+ try {
108
+ raw = await readFile(filePath, "utf-8");
109
+ }
110
+ catch (err) {
111
+ if (err.code === "ENOENT") {
112
+ return emptyRegistry();
113
+ }
114
+ throw err;
115
+ }
116
+ try {
117
+ return parseRegistryContent(raw, filePath);
118
+ }
119
+ catch (err) {
120
+ archiveCorruptFile(filePath);
121
+ throw err;
122
+ }
123
+ }
124
+ /**
125
+ * 将损坏的注册表文件原子留档为 `${filePath}.corrupt`,
126
+ * 后续 save 的临时文件 rename 不会覆盖留档(文件名不同)。
127
+ */
128
+ function archiveCorruptFile(filePath) {
129
+ try {
130
+ renameSync(filePath, `${filePath}.corrupt`);
131
+ }
132
+ catch {
133
+ // 留档失败(如文件被其他进程先行移动)时仅放弃留档:错误仍会向上抛出,
134
+ // 不会出现静默清空
135
+ }
136
+ }
137
+ /**
138
+ * 由结构化数据构造统一落盘载荷(schemaVersion 1 复合格式,保障双向兼容)。
139
+ */
140
+ function buildUnifiedPayload(data) {
141
+ const links = [];
142
+ if (data.workspaces) {
143
+ for (const [wsPath, ws] of Object.entries(data.workspaces)) {
144
+ links.push({
145
+ type: "workspace",
146
+ path: resolve(ws.path || wsPath),
147
+ linkedAt: ws.linkedAt || new Date().toISOString(),
148
+ depth: 3,
149
+ });
150
+ }
151
+ }
152
+ if (data.packages) {
153
+ for (const pkg of Object.values(data.packages)) {
154
+ if (!pkg.workspaceRoot && pkg.path) {
155
+ links.push({
156
+ type: "package",
157
+ path: resolve(pkg.path),
158
+ linkedAt: pkg.linkedAt || new Date().toISOString(),
159
+ });
160
+ }
161
+ }
162
+ }
163
+ return {
164
+ version: "2.0.0",
165
+ schemaVersion: 1,
166
+ packages: data.packages || {},
167
+ workspaces: data.workspaces || {},
168
+ links,
169
+ };
170
+ }
171
+ let tmpFileSeq = 0;
172
+ /**
173
+ * 原子写入注册表:临时文件写盘后 rename,持锁执行,避免并发撕裂。
174
+ */
175
+ export async function saveRegistry(data, customHome) {
176
+ const filePath = getRegistryFilePath(customHome);
177
+ ensureRegistryDir(filePath);
178
+ const unifiedData = buildUnifiedPayload(data);
179
+ const payload = JSON.stringify(unifiedData, null, 2) + "\n";
180
+ await withRegistryLock(filePath, async () => {
181
+ await writeRegistryPayloadLocked(filePath, payload);
182
+ });
183
+ }
184
+ /**
185
+ * 在已持有注册表锁的上下文中直接原子落盘(供复合读改写流程使用,
186
+ * 避免与 saveRegistry 的锁重入死等);调用方必须已持有对应锁。
187
+ */
188
+ export async function writeRegistryLocked(data, customHome) {
189
+ const filePath = getRegistryFilePath(customHome);
190
+ ensureRegistryDir(filePath);
191
+ const payload = JSON.stringify(buildUnifiedPayload(data), null, 2) + "\n";
192
+ await writeRegistryPayloadLocked(filePath, payload);
193
+ }
194
+ /**
195
+ * 原子落盘单次写入:临时文件写盘后 rename;文件名含进程号与自增序号,
196
+ * 确保同一进程内多次保存也不会冲突。
197
+ */
198
+ async function writeRegistryPayloadLocked(filePath, payload) {
199
+ const tempPath = `${filePath}.${process.pid}.${++tmpFileSeq}.tmp`;
200
+ await writeFile(tempPath, payload, "utf-8");
201
+ await rename(tempPath, filePath);
202
+ }
203
+ function ensureRegistryDir(filePath) {
204
+ const dir = dirname(filePath);
205
+ if (!existsSync(dir)) {
206
+ mkdirSync(dir, { recursive: true });
207
+ }
208
+ }
209
+ export async function linkPackage(targetPath = process.cwd(), customHome, options) {
210
+ const filePath = getRegistryFilePath(customHome);
211
+ ensureRegistryDir(filePath);
212
+ const absPath = resolve(targetPath);
213
+ const directHasConfig = existsSync(join(absPath, "actiondock.json"));
214
+ // 如果当前目录直接包含 actiondock.json 且未强制递归,按单包链接
215
+ if (directHasConfig && !options?.recursive) {
216
+ const config = loadProjectConfig(absPath);
217
+ return await withRegistryLock(filePath, async () => {
218
+ const registry = await loadRegistryAsync(customHome);
219
+ const entry = buildLinkedPackageEntry(config, absPath, new Date().toISOString());
220
+ registry.packages[config.id] = entry;
221
+ await writeRegistryLocked(registry, customHome);
222
+ return {
223
+ id: config.id,
224
+ name: entry.name,
225
+ version: entry.version,
226
+ path: absPath,
227
+ linkedAt: entry.linkedAt,
228
+ isWorkspace: false,
229
+ entries: [entry],
230
+ };
231
+ });
232
+ }
233
+ // 尝试扫描子目录发现多个 ActionDock 子项目(Workspace 模式);
234
+ // 门槛以「发现子项目根目录」为准而非「配置可加载」,即使全部子项目配置损坏
235
+ // 也仍注册 workspace(与旧版行为一致,避免边缘场景下静默落入父级回退分支)
236
+ const discoveredRoots = discoverProjects(absPath);
237
+ if (discoveredRoots.length > 0) {
238
+ return await withRegistryLock(filePath, async () => {
239
+ const registry = await loadRegistryAsync(customHome);
240
+ const now = new Date().toISOString();
241
+ const wsEntry = {
242
+ path: absPath,
243
+ linkedAt: now,
244
+ };
245
+ registry.workspaces = registry.workspaces || {};
246
+ registry.workspaces[absPath] = wsEntry;
247
+ const linkedEntries = [];
248
+ for (const root of discoveredRoots) {
249
+ try {
250
+ const config = loadProjectConfig(root);
251
+ const entry = buildLinkedPackageEntry(config, root, now, absPath);
252
+ registry.packages[config.id] = entry;
253
+ linkedEntries.push(entry);
254
+ }
255
+ catch {
256
+ // 子项目配置损坏:跳过该子项目条目,不影响 workspace 注册本身
257
+ }
258
+ }
259
+ await writeRegistryLocked(registry, customHome);
260
+ const wsName = basename(absPath);
261
+ return {
262
+ id: wsName,
263
+ name: wsName,
264
+ version: "2.0.0",
265
+ path: absPath,
266
+ linkedAt: now,
267
+ isWorkspace: true,
268
+ entries: linkedEntries,
269
+ workspace: wsEntry,
270
+ };
271
+ });
272
+ }
273
+ // 回退检查:如果在子目录执行(例如在 package 的 actions/ 目录下),查找父级项目根目录
274
+ const parentRoot = findProjectRoot(absPath);
275
+ if (parentRoot) {
276
+ const config = loadProjectConfig(parentRoot);
277
+ return await withRegistryLock(filePath, async () => {
278
+ const registry = await loadRegistryAsync(customHome);
279
+ const entry = buildLinkedPackageEntry(config, parentRoot, new Date().toISOString());
280
+ registry.packages[config.id] = entry;
281
+ await writeRegistryLocked(registry, customHome);
282
+ return {
283
+ id: config.id,
284
+ name: entry.name,
285
+ version: entry.version,
286
+ path: parentRoot,
287
+ linkedAt: entry.linkedAt,
288
+ isWorkspace: false,
289
+ entries: [entry],
290
+ };
291
+ });
292
+ }
293
+ throw new Error(`Cannot link: actiondock.json not found in '${absPath}' or its subdirectories`);
294
+ }
295
+ export async function unlinkPackage(identifier = process.cwd(), customHome) {
296
+ const filePath = getRegistryFilePath(customHome);
297
+ ensureRegistryDir(filePath);
298
+ return await withRegistryLock(filePath, async () => {
299
+ const registry = await loadRegistryAsync(customHome);
300
+ const absPath = resolve(identifier);
301
+ // 检查是否匹配 Workspace 绝对路径
302
+ if (registry.workspaces && registry.workspaces[absPath]) {
303
+ const removedWs = registry.workspaces[absPath];
304
+ delete registry.workspaces[absPath];
305
+ let removedCount = 0;
306
+ for (const [id, entry] of Object.entries(registry.packages)) {
307
+ if (entry.workspaceRoot === absPath || entry.path.startsWith(absPath)) {
308
+ delete registry.packages[id];
309
+ removedCount++;
310
+ }
311
+ }
312
+ await writeRegistryLocked(registry, customHome);
313
+ return {
314
+ type: "workspace",
315
+ id: basename(absPath),
316
+ path: absPath,
317
+ packagesCount: removedCount,
318
+ removedWorkspace: removedWs,
319
+ };
320
+ }
321
+ // 检查是否匹配 Workspace 目录别名
322
+ if (registry.workspaces) {
323
+ for (const [wsPath, wsEntry] of Object.entries(registry.workspaces)) {
324
+ if (basename(wsPath) === identifier) {
325
+ delete registry.workspaces[wsPath];
326
+ let removedCount = 0;
327
+ for (const [id, entry] of Object.entries(registry.packages)) {
328
+ if (entry.workspaceRoot === wsPath || entry.path.startsWith(wsPath)) {
329
+ delete registry.packages[id];
330
+ removedCount++;
331
+ }
332
+ }
333
+ await writeRegistryLocked(registry, customHome);
334
+ return {
335
+ type: "workspace",
336
+ id: basename(wsPath),
337
+ path: wsPath,
338
+ packagesCount: removedCount,
339
+ removedWorkspace: wsEntry,
340
+ };
341
+ }
342
+ }
343
+ }
344
+ // 检查是否直接匹配 Package ID
345
+ let targetKey;
346
+ if (registry.packages[identifier]) {
347
+ targetKey = identifier;
348
+ }
349
+ else {
350
+ // 匹配路径或短 slug
351
+ for (const [id, entry] of Object.entries(registry.packages)) {
352
+ if (entry.path === absPath ||
353
+ entry.id === identifier ||
354
+ getPackageSlug(entry.id) === identifier) {
355
+ targetKey = id;
356
+ break;
357
+ }
358
+ }
359
+ }
360
+ if (!targetKey) {
361
+ return null;
362
+ }
363
+ const removed = registry.packages[targetKey];
364
+ delete registry.packages[targetKey];
365
+ await writeRegistryLocked(registry, customHome);
366
+ return {
367
+ type: "package",
368
+ id: removed.id,
369
+ path: removed.path,
370
+ packagesCount: 1,
371
+ removedPackage: removed,
372
+ };
373
+ });
374
+ }
375
+ export function listLinkedPackages(customHome) {
376
+ const registry = loadRegistry(customHome);
377
+ const result = { ...registry.packages };
378
+ // 动态扫描已挂载的 Workspace 目录,确保新拉取/新建的子包即时感知
379
+ if (registry.workspaces) {
380
+ for (const ws of Object.values(registry.workspaces)) {
381
+ if (!existsSync(ws.path))
382
+ continue;
383
+ for (const { root, config } of discoverProjectConfigs(ws.path)) {
384
+ if (!result[config.id] || result[config.id].workspaceRoot === ws.path) {
385
+ result[config.id] = buildLinkedPackageEntry(config, root, ws.linkedAt, ws.path);
386
+ }
387
+ }
388
+ }
389
+ }
390
+ return Object.values(result);
391
+ }
392
+ export async function listLinkedPackagesAsync(customHome) {
393
+ const registry = await loadRegistryAsync(customHome);
394
+ const result = { ...registry.packages };
395
+ if (registry.workspaces) {
396
+ for (const ws of Object.values(registry.workspaces)) {
397
+ if (!(await pathExistsAsync(ws.path)))
398
+ continue;
399
+ for (const { root, config } of await discoverProjectConfigsAsync(ws.path)) {
400
+ if (!result[config.id] || result[config.id].workspaceRoot === ws.path) {
401
+ result[config.id] = buildLinkedPackageEntry(config, root, ws.linkedAt, ws.path);
402
+ }
403
+ }
404
+ }
405
+ }
406
+ return Object.values(result);
407
+ }
408
+ export function listLinkedWorkspaces(customHome) {
409
+ const registry = loadRegistry(customHome);
410
+ return Object.values(registry.workspaces || {});
411
+ }
412
+ export function resolveActionProjectSync(actionIdentifier, cwd = process.cwd(), customHome) {
413
+ return runFlowSync(resolveEntityFlow({
414
+ identifier: actionIdentifier,
415
+ cwd,
416
+ entityNoun: "Action",
417
+ listLinkedPackages: () => listLinkedPackages(customHome),
418
+ probe: (root, config, id) => probeActionSync(root, config, id),
419
+ buildResult: (projectRoot, packageId, actionId) => ({ projectRoot, packageId, actionId }),
420
+ }));
421
+ }
422
+ export async function resolveActionProject(actionIdentifier, cwd = process.cwd(), customHome) {
423
+ return runFlowAsync(resolveEntityFlow({
424
+ identifier: actionIdentifier,
425
+ cwd,
426
+ entityNoun: "Action",
427
+ listLinkedPackages: () => listLinkedPackagesAsync(customHome),
428
+ probe: (root, config, id) => probeActionAsync(root, config, id),
429
+ buildResult: (projectRoot, packageId, actionId) => ({ projectRoot, packageId, actionId }),
430
+ }));
431
+ }
432
+ export function resolvePlaybookProject(playbookIdentifier, cwd = process.cwd(), customHome) {
433
+ return runFlowSync(resolveEntityFlow({
434
+ identifier: playbookIdentifier,
435
+ cwd,
436
+ entityNoun: "Playbook",
437
+ listLinkedPackages: () => listLinkedPackages(customHome),
438
+ probe: (root, config, id) => probePlaybook(root, config, id),
439
+ buildResult: (projectRoot, packageId, playbookId, playbook) => ({
440
+ projectRoot,
441
+ packageId,
442
+ playbookId,
443
+ playbook,
444
+ }),
445
+ }));
446
+ }
447
+ export function resolvePackageRoot(packageIdOrPath, cwd, customHome) {
448
+ if (!packageIdOrPath) {
449
+ return findProjectRoot(cwd);
450
+ }
451
+ const baseDir = cwd || process.cwd();
452
+ const resolvedPath = resolve(baseDir, packageIdOrPath);
453
+ // 检查 packageIdOrPath 是否为磁盘上现存的目录或文件路径
454
+ if (existsSync(resolvedPath)) {
455
+ try {
456
+ const info = statSync(resolvedPath);
457
+ const targetDir = info.isDirectory() ? resolvedPath : dirname(resolvedPath);
458
+ if (existsSync(join(targetDir, "actiondock.json"))) {
459
+ return targetDir;
460
+ }
461
+ const parentRoot = findProjectRoot(targetDir);
462
+ if (parentRoot) {
463
+ return parentRoot;
464
+ }
465
+ }
466
+ catch {
467
+ // stat 失败(如并发删除):忽略并继续走标识符解析路径
468
+ }
469
+ }
470
+ // 显式路径形态但未解析成功的场景:
471
+ // scoped package 标识符(如 @team/tools)虽含 '/' 但属于包 ID 而非文件路径
472
+ const isScopedPackage = packageIdOrPath.startsWith("@");
473
+ const isExplicitPath = !isScopedPackage &&
474
+ (packageIdOrPath.startsWith(".") ||
475
+ packageIdOrPath.startsWith("/") ||
476
+ packageIdOrPath.startsWith("~") ||
477
+ packageIdOrPath.includes("/") ||
478
+ packageIdOrPath.includes("\\"));
479
+ if (isExplicitPath) {
480
+ // 显式路径不存在或不是 ActionDock 项目时必须失败,禁止静默回退
481
+ return null;
482
+ }
483
+ // 检查当前项目(自 cwd 向上探测)
484
+ const currentRoot = findProjectRoot(cwd);
485
+ if (currentRoot) {
486
+ try {
487
+ const config = loadProjectConfig(currentRoot);
488
+ if (config.id === packageIdOrPath || getPackageSlug(config.id) === packageIdOrPath) {
489
+ return currentRoot;
490
+ }
491
+ }
492
+ catch {
493
+ // 当前项目配置损坏:忽略,继续注册表匹配
494
+ }
495
+ }
496
+ // 检查注册表中的链接包
497
+ const linkedList = listLinkedPackages(customHome);
498
+ const found = linkedList.find((p) => p.id === packageIdOrPath ||
499
+ getPackageSlug(p.id) === packageIdOrPath ||
500
+ p.path === resolvedPath);
501
+ if (found) {
502
+ return found.path;
503
+ }
504
+ return null;
505
+ }
506
+ export function getRegistryStatus(customHome) {
507
+ const registry = loadRegistry(customHome);
508
+ const workspaces = [];
509
+ const packages = [];
510
+ let staleCount = 0;
511
+ const seenPackageIds = new Set();
512
+ // 处理 Workspace:目录存活则动态扫描子项目,缺失则标记 stale
513
+ if (registry.workspaces) {
514
+ for (const wsPath of Object.keys(registry.workspaces)) {
515
+ if (!existsSync(wsPath)) {
516
+ staleCount++;
517
+ workspaces.push({
518
+ type: "workspace",
519
+ id: basename(wsPath),
520
+ path: wsPath,
521
+ status: "stale",
522
+ packagesCount: 0,
523
+ children: [],
524
+ });
525
+ continue;
526
+ }
527
+ const children = [];
528
+ for (const { root, config } of discoverProjectConfigs(wsPath)) {
529
+ seenPackageIds.add(config.id);
530
+ children.push({
531
+ id: config.id,
532
+ name: config.name || config.id,
533
+ version: config.version || "0.0.0",
534
+ path: root,
535
+ status: "active",
536
+ });
537
+ }
538
+ workspaces.push({
539
+ type: "workspace",
540
+ id: basename(wsPath),
541
+ path: wsPath,
542
+ status: "active",
543
+ packagesCount: children.length,
544
+ children,
545
+ });
546
+ }
547
+ }
548
+ // 处理独立包(不属于任何存活 Workspace 且未被动态扫描覆盖)
549
+ for (const [pkgId, pkgEntry] of Object.entries(registry.packages)) {
550
+ if (pkgEntry.workspaceRoot && registry.workspaces && registry.workspaces[pkgEntry.workspaceRoot]) {
551
+ continue;
552
+ }
553
+ if (seenPackageIds.has(pkgId)) {
554
+ continue;
555
+ }
556
+ const isPkgActive = existsSync(pkgEntry.path);
557
+ if (!isPkgActive) {
558
+ staleCount++;
559
+ }
560
+ packages.push({
561
+ type: "package",
562
+ id: pkgEntry.id,
563
+ name: pkgEntry.name,
564
+ version: pkgEntry.version,
565
+ path: pkgEntry.path,
566
+ status: isPkgActive ? "active" : "stale",
567
+ });
568
+ }
569
+ const totalPackagesCount = workspaces.reduce((acc, ws) => acc + (ws.children?.length || 0), 0) +
570
+ packages.filter((p) => p.status === "active").length;
571
+ return {
572
+ workspaces,
573
+ packages,
574
+ staleCount,
575
+ totalPackagesCount,
576
+ };
577
+ }
578
+ export async function pruneRegistry(customHome) {
579
+ const filePath = getRegistryFilePath(customHome);
580
+ ensureRegistryDir(filePath);
581
+ return await withRegistryLock(filePath, async () => {
582
+ const registry = await loadRegistryAsync(customHome);
583
+ const prunedWorkspaces = [];
584
+ const prunedPackages = [];
585
+ // 清理目录已缺失的 Workspace 链接
586
+ if (registry.workspaces) {
587
+ for (const [wsPath, wsEntry] of Object.entries(registry.workspaces)) {
588
+ if (!existsSync(wsPath)) {
589
+ prunedWorkspaces.push(wsEntry);
590
+ delete registry.workspaces[wsPath];
591
+ }
592
+ }
593
+ }
594
+ // 清理路径已缺失的 Package 链接
595
+ for (const [pkgId, pkgEntry] of Object.entries(registry.packages)) {
596
+ if (!existsSync(pkgEntry.path)) {
597
+ prunedPackages.push(pkgEntry);
598
+ delete registry.packages[pkgId];
599
+ }
600
+ }
601
+ if (prunedWorkspaces.length > 0 || prunedPackages.length > 0) {
602
+ await writeRegistryLocked(registry, customHome);
603
+ }
604
+ return {
605
+ prunedPackages,
606
+ prunedWorkspaces,
607
+ };
608
+ });
609
+ }
610
+ // 重新导出扫描辅助,保持既有从 registry 模块的导入路径可用
611
+ export { discoverProjects, IGNORED_SCAN_DIRS } from "./scan.js";
@@ -0,0 +1,62 @@
1
+ import type { ActionDockManifest, PlaybookDefinition } from "../project/types.js";
2
+ import type { LinkedPackageEntry } from "./types.js";
3
+ /**
4
+ * 流程步骤的惰性执行体:同步驱动器直接调用,异步驱动器 await,
5
+ * 执行中抛出的异常由驱动器通过 it.throw 回灌生成器的 try/catch。
6
+ */
7
+ type FlowThunk = () => any;
8
+ /**
9
+ * 实体解析探针:命中返回实体值(Action 场景恒为 true),未命中返回 undefined。
10
+ */
11
+ type EntityProbe<V> = (projectRoot: string, config: ActionDockManifest, entityId: string) => V;
12
+ /**
13
+ * 由命中结果构造最终解析产物。
14
+ */
15
+ type EntityResultBuilder<V, R> = (projectRoot: string, packageId: string, entityId: string, value: V) => R;
16
+ interface ResolveEntityFlowOptions<V, R> {
17
+ /** 实体完整标识符(可能是裸 ID 或 <pkg>/<id>、<pkg>:<id> 形式) */
18
+ identifier: string;
19
+ /** 起始工作目录 */
20
+ cwd: string;
21
+ /** 实体名词(Action 或 Playbook),仅用于错误文案 */
22
+ entityNoun: string;
23
+ /** 获取链接包列表(同步或异步由注入方决定,流程通过 thunk 统一驱动) */
24
+ listLinkedPackages: () => LinkedPackageEntry[];
25
+ /** 实体探针 */
26
+ probe: EntityProbe<V>;
27
+ /** 解析结果构造器 */
28
+ buildResult: EntityResultBuilder<V, R>;
29
+ }
30
+ /**
31
+ * 实体解析唯一流程(生成器表达):
32
+ * 当前项目探测 → scoped 标识符拆分 → 注册表精确匹配 → 遍历链接包 →
33
+ * 单命中返回、多命中报错、零命中报错。
34
+ *
35
+ * 仅将可能异步的操作(探针、链接列表获取)以 thunk 形式 yield,
36
+ * 由 runFlowSync / runFlowAsync 双驱动器分别以同步调用或 await 方式执行。
37
+ */
38
+ export declare function resolveEntityFlow<V, R>(opts: ResolveEntityFlowOptions<V, R>): Generator<FlowThunk, R, any>;
39
+ /**
40
+ * 同步驱动器:逐个调用 thunk 并把结果回灌生成器,
41
+ * thunk 抛出的异常经 it.throw 回灌生成器内部的 try/catch 处理。
42
+ * 同步流程中误注入异步 thunk 时立即抛错,避免 Promise 被当作命中值静默错判。
43
+ */
44
+ export declare function runFlowSync<R>(flow: Generator<FlowThunk, R, any>): R;
45
+ /**
46
+ * 异步驱动器:await 每个 thunk 后回灌结果,拒绝原因经 it.throw 回灌。
47
+ */
48
+ export declare function runFlowAsync<R>(flow: Generator<FlowThunk, R, any>): Promise<R>;
49
+ /**
50
+ * Action 探针(同步):manifest 声明优先,其次探测源码文件是否落盘。
51
+ */
52
+ export declare function probeActionSync(projectRoot: string, config: ActionDockManifest, actionId: string): true | undefined;
53
+ /**
54
+ * Action 探针(异步):manifest 声明优先,其次加载源码模块;
55
+ * 模块加载失败时回退同步探测,保证与同步路径判定一致。
56
+ */
57
+ export declare function probeActionAsync(projectRoot: string, config: ActionDockManifest, actionId: string): Promise<true | undefined>;
58
+ /**
59
+ * Playbook 探针:以 actiondock.json 的 playbooks 声明为唯一事实源,命中返回规程定义。
60
+ */
61
+ export declare function probePlaybook(projectRoot: string, config: ActionDockManifest, playbookId: string): PlaybookDefinition | undefined;
62
+ export {};