@actiondock/core 2.0.9 → 2.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/build/index.ts +0 -1
- package/src/build/templates.ts +1 -1
- package/src/catalog/types.ts +1 -9
- package/src/doctor/doctor.ts +99 -38
- package/src/execution/service.ts +47 -7
- package/src/export/index.ts +0 -1
- package/src/export/templates.ts +221 -59
- package/src/index.ts +1 -0
- package/src/project/init.ts +3 -2
- package/src/project/loader.ts +46 -7
- package/src/project/manifest.ts +205 -2
- package/src/project/types.ts +49 -0
- package/src/registry/registry.ts +126 -32
- package/src/runtime/context.ts +2 -1
- package/src/runtime/index.ts +1 -0
- package/src/runtime/module-loader.ts +67 -0
- package/src/runtime/process.ts +12 -11
- package/src/runtime/runner.ts +23 -36
- package/src/server/index.ts +1 -0
- package/src/server/routes/actions.ts +262 -0
- package/src/server/routes/common.ts +126 -0
- package/src/server/routes/config.ts +147 -0
- package/src/server/routes/doctor.ts +29 -0
- package/src/server/routes/health.ts +40 -0
- package/src/server/routes/index.ts +9 -0
- package/src/server/routes/info.ts +221 -0
- package/src/server/routes/playbooks.ts +109 -0
- package/src/server/routes/runs.ts +301 -0
- package/src/server/routes/state.ts +133 -0
- package/src/server/server.ts +82 -1323
- package/src/storage/index.ts +20 -4
- package/src/storage/sqlite.ts +60 -31
- package/src/storage/types.ts +8 -0
- package/src/utils/index.ts +2 -0
- package/src/build/builder.ts +0 -218
- package/src/export/skill.ts +0 -350
package/src/project/manifest.ts
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { loadActionFileMap, loadProjectConfig } from "./loader";
|
|
4
|
+
import type {
|
|
5
|
+
ActionDockManifest,
|
|
6
|
+
ActionManifestEntry,
|
|
7
|
+
ManifestSyncChange,
|
|
8
|
+
ManifestSyncResult,
|
|
9
|
+
SyncManifestOptions,
|
|
10
|
+
} from "./types";
|
|
4
11
|
|
|
5
12
|
export const MANIFEST_FILE_NAME = "actiondock.manifest.json";
|
|
6
13
|
|
|
14
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
15
|
+
if (a === b) return true;
|
|
16
|
+
if (a == null || b == null) return a === b;
|
|
17
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
18
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
19
|
+
if (Array.isArray(a)) {
|
|
20
|
+
const arrB = b as unknown[];
|
|
21
|
+
if (a.length !== arrB.length) return false;
|
|
22
|
+
for (let i = 0; i < a.length; i++) {
|
|
23
|
+
if (!deepEqual(a[i], arrB[i])) return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const keysA = Object.keys(a as Record<string, unknown>);
|
|
28
|
+
const keysB = Object.keys(b as Record<string, unknown>);
|
|
29
|
+
if (keysA.length !== keysB.length) return false;
|
|
30
|
+
for (const k of keysA) {
|
|
31
|
+
if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
|
|
32
|
+
if (!deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
7
37
|
/**
|
|
8
38
|
* 读取并解析项目的声明式清单文件。
|
|
9
39
|
* 若文件不存在则返回 null。
|
|
@@ -94,3 +124,176 @@ export function createManifestEntry(options: {
|
|
|
94
124
|
annotations: options.annotations,
|
|
95
125
|
};
|
|
96
126
|
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 依据动作源码目录中的 Action 定义,增量同步或校验 actiondock.manifest.json 文件。
|
|
130
|
+
*
|
|
131
|
+
* @param projectRoot 项目根目录绝对路径
|
|
132
|
+
* @param options 同步选项(包括 actionsDir、check、prune、autoInstall)
|
|
133
|
+
* @returns 同步变更结果报告
|
|
134
|
+
*/
|
|
135
|
+
export async function syncManifest(
|
|
136
|
+
projectRoot: string,
|
|
137
|
+
options: SyncManifestOptions = {}
|
|
138
|
+
): Promise<ManifestSyncResult> {
|
|
139
|
+
const manifestPath = join(projectRoot, MANIFEST_FILE_NAME);
|
|
140
|
+
|
|
141
|
+
// 1. 确定 actions 目录
|
|
142
|
+
let actionsDir = options.actionsDir;
|
|
143
|
+
if (!actionsDir) {
|
|
144
|
+
try {
|
|
145
|
+
const config = loadProjectConfig(projectRoot);
|
|
146
|
+
actionsDir = config.actionsDir || "actions";
|
|
147
|
+
} catch {
|
|
148
|
+
actionsDir = "actions";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 2. 加载现有清单,不存在则初始化基准结构
|
|
153
|
+
const existingManifest = loadManifest(projectRoot);
|
|
154
|
+
const manifest: ActionDockManifest = existingManifest
|
|
155
|
+
? {
|
|
156
|
+
schemaVersion: existingManifest.schemaVersion || 1,
|
|
157
|
+
actions: { ...existingManifest.actions },
|
|
158
|
+
assets: existingManifest.assets ? [...existingManifest.assets] : [],
|
|
159
|
+
}
|
|
160
|
+
: {
|
|
161
|
+
schemaVersion: 1,
|
|
162
|
+
actions: {},
|
|
163
|
+
assets: [],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// 3. 动态加载所有 Action 源码定义
|
|
167
|
+
const actionFileMap = await loadActionFileMap(projectRoot, actionsDir, {
|
|
168
|
+
autoInstall: options.autoInstall !== false,
|
|
169
|
+
strict: true,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const changes: ManifestSyncChange[] = [];
|
|
173
|
+
const added: string[] = [];
|
|
174
|
+
const updated: string[] = [];
|
|
175
|
+
const removed: string[] = [];
|
|
176
|
+
const unchanged: string[] = [];
|
|
177
|
+
|
|
178
|
+
const scannedActionIds = new Set<string>();
|
|
179
|
+
|
|
180
|
+
// 4. 比对源码中的每个 Action 与现有清单项
|
|
181
|
+
for (const [actionId, fileEntry] of actionFileMap.entries()) {
|
|
182
|
+
scannedActionIds.add(actionId);
|
|
183
|
+
const relEntry = relative(projectRoot, fileEntry.filePath).replace(/\\/g, "/");
|
|
184
|
+
const act = fileEntry.action;
|
|
185
|
+
|
|
186
|
+
const newManifestEntry: ActionManifestEntry = {
|
|
187
|
+
entry: relEntry,
|
|
188
|
+
description: act.description ?? "",
|
|
189
|
+
inputSchema: act.inputSchema ?? {},
|
|
190
|
+
outputSchema: act.outputSchema ?? {},
|
|
191
|
+
uses: Array.isArray(act.uses) ? [...act.uses] : [],
|
|
192
|
+
tags: Array.isArray(act.tags) ? [...act.tags] : [],
|
|
193
|
+
};
|
|
194
|
+
if (act.annotations && typeof act.annotations === "object") {
|
|
195
|
+
newManifestEntry.annotations = act.annotations;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const oldManifestEntry = manifest.actions[actionId];
|
|
199
|
+
|
|
200
|
+
if (!oldManifestEntry) {
|
|
201
|
+
changes.push({
|
|
202
|
+
actionId,
|
|
203
|
+
type: "added",
|
|
204
|
+
entry: relEntry,
|
|
205
|
+
});
|
|
206
|
+
added.push(actionId);
|
|
207
|
+
if (!options.check) {
|
|
208
|
+
manifest.actions[actionId] = newManifestEntry;
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
const changedFields: string[] = [];
|
|
212
|
+
|
|
213
|
+
if (oldManifestEntry.entry !== newManifestEntry.entry) {
|
|
214
|
+
changedFields.push("entry");
|
|
215
|
+
}
|
|
216
|
+
if ((oldManifestEntry.description ?? "") !== (newManifestEntry.description ?? "")) {
|
|
217
|
+
changedFields.push("description");
|
|
218
|
+
}
|
|
219
|
+
if (!deepEqual(oldManifestEntry.inputSchema ?? {}, newManifestEntry.inputSchema ?? {})) {
|
|
220
|
+
changedFields.push("inputSchema");
|
|
221
|
+
}
|
|
222
|
+
if (!deepEqual(oldManifestEntry.outputSchema ?? {}, newManifestEntry.outputSchema ?? {})) {
|
|
223
|
+
changedFields.push("outputSchema");
|
|
224
|
+
}
|
|
225
|
+
if (!deepEqual(oldManifestEntry.uses ?? [], newManifestEntry.uses ?? [])) {
|
|
226
|
+
changedFields.push("uses");
|
|
227
|
+
}
|
|
228
|
+
if (!deepEqual(oldManifestEntry.tags ?? [], newManifestEntry.tags ?? [])) {
|
|
229
|
+
changedFields.push("tags");
|
|
230
|
+
}
|
|
231
|
+
if (!deepEqual(oldManifestEntry.annotations ?? {}, newManifestEntry.annotations ?? {})) {
|
|
232
|
+
changedFields.push("annotations");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (changedFields.length > 0) {
|
|
236
|
+
changes.push({
|
|
237
|
+
actionId,
|
|
238
|
+
type: "updated",
|
|
239
|
+
entry: relEntry,
|
|
240
|
+
changedFields,
|
|
241
|
+
});
|
|
242
|
+
updated.push(actionId);
|
|
243
|
+
if (!options.check) {
|
|
244
|
+
manifest.actions[actionId] = newManifestEntry;
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
changes.push({
|
|
248
|
+
actionId,
|
|
249
|
+
type: "unchanged",
|
|
250
|
+
entry: relEntry,
|
|
251
|
+
});
|
|
252
|
+
unchanged.push(actionId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 5. 检查清单中存在但源码中已不存在的废弃动作
|
|
258
|
+
for (const [actionId, item] of Object.entries(manifest.actions)) {
|
|
259
|
+
if (!scannedActionIds.has(actionId)) {
|
|
260
|
+
changes.push({
|
|
261
|
+
actionId,
|
|
262
|
+
type: "removed",
|
|
263
|
+
entry: item.entry,
|
|
264
|
+
});
|
|
265
|
+
removed.push(actionId);
|
|
266
|
+
if (!options.check && options.prune !== false) {
|
|
267
|
+
delete manifest.actions[actionId];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const inSync = added.length === 0 && updated.length === 0 && removed.length === 0;
|
|
273
|
+
|
|
274
|
+
// 6. 如果存在变更且非仅检查模式,保存清单文件
|
|
275
|
+
if (!options.check && !inSync) {
|
|
276
|
+
saveManifest(projectRoot, manifest);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
inSync,
|
|
281
|
+
manifestPath,
|
|
282
|
+
changes,
|
|
283
|
+
added,
|
|
284
|
+
updated,
|
|
285
|
+
removed,
|
|
286
|
+
unchanged,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 校验当前 actiondock.manifest.json 是否与动作源码定义保持一致(只读检查)。
|
|
292
|
+
*/
|
|
293
|
+
export async function checkManifestSync(
|
|
294
|
+
projectRoot: string,
|
|
295
|
+
options: Omit<SyncManifestOptions, "check"> = {}
|
|
296
|
+
): Promise<ManifestSyncResult> {
|
|
297
|
+
return syncManifest(projectRoot, { ...options, check: true });
|
|
298
|
+
}
|
|
299
|
+
|
package/src/project/types.ts
CHANGED
|
@@ -90,3 +90,52 @@ export interface ActionDockManifest {
|
|
|
90
90
|
actions: Record<string, ActionManifestEntry>;
|
|
91
91
|
assets?: string[];
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 清单同步操作选项。
|
|
96
|
+
*/
|
|
97
|
+
export interface SyncManifestOptions {
|
|
98
|
+
/** 自定义 actions 源码目录(默认为工程配置中的 actionsDir 或 "actions") */
|
|
99
|
+
actionsDir?: string;
|
|
100
|
+
/** 是否仅检查同步状态而不保存写入文件(默认 false) */
|
|
101
|
+
check?: boolean;
|
|
102
|
+
/** 是否自动移除源码中已不存在的 Action(默认 true) */
|
|
103
|
+
prune?: boolean;
|
|
104
|
+
/** 缺失依赖时是否自动执行安装(默认 true) */
|
|
105
|
+
autoInstall?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 单个 Action 的清单同步变更项。
|
|
110
|
+
*/
|
|
111
|
+
export interface ManifestSyncChange {
|
|
112
|
+
/** Action 标识符 */
|
|
113
|
+
actionId: string;
|
|
114
|
+
/** 变更类型:新增、更新、删除或未变 */
|
|
115
|
+
type: "added" | "updated" | "removed" | "unchanged";
|
|
116
|
+
/** 源码入口文件相对路径 */
|
|
117
|
+
entry?: string;
|
|
118
|
+
/** 发生变更的字段列表(例如 ["description", "inputSchema"]) */
|
|
119
|
+
changedFields?: string[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 清单同步执行结果。
|
|
124
|
+
*/
|
|
125
|
+
export interface ManifestSyncResult {
|
|
126
|
+
/** 清单当前是否已与源码完全一致 */
|
|
127
|
+
inSync: boolean;
|
|
128
|
+
/** 清单文件的物理绝对路径 */
|
|
129
|
+
manifestPath: string;
|
|
130
|
+
/** 所有 Action 的变更详情列表 */
|
|
131
|
+
changes: ManifestSyncChange[];
|
|
132
|
+
/** 新增的 Action 标识列表 */
|
|
133
|
+
added: string[];
|
|
134
|
+
/** 更新的 Action 标识列表 */
|
|
135
|
+
updated: string[];
|
|
136
|
+
/** 移除的 Action 标识列表 */
|
|
137
|
+
removed: string[];
|
|
138
|
+
/** 未改变的 Action 标识列表 */
|
|
139
|
+
unchanged: string[];
|
|
140
|
+
}
|
|
141
|
+
|
package/src/registry/registry.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { findProjectRoot, loadActions, loadPlaybooks, loadProjectConfig } from "../project/loader";
|
|
4
|
+
import { loadManifest } from "../project/manifest";
|
|
4
5
|
import { getActionDockHome, getPackageSlug } from "../utils";
|
|
5
6
|
import type {
|
|
6
7
|
GlobalRegistryData,
|
|
@@ -330,6 +331,23 @@ export function listLinkedWorkspaces(customHome?: string): LinkedWorkspaceEntry[
|
|
|
330
331
|
return Object.values(registry.workspaces || {});
|
|
331
332
|
}
|
|
332
333
|
|
|
334
|
+
async function projectHasAction(
|
|
335
|
+
projectRoot: string,
|
|
336
|
+
actionsDir: string | undefined,
|
|
337
|
+
actionId: string
|
|
338
|
+
): Promise<boolean> {
|
|
339
|
+
const manifest = loadManifest(projectRoot);
|
|
340
|
+
if (manifest?.actions && actionId in manifest.actions) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const actions = await loadActions(projectRoot, actionsDir, { autoInstall: false });
|
|
345
|
+
return actions.has(actionId);
|
|
346
|
+
} catch {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
333
351
|
export async function resolveActionProject(
|
|
334
352
|
actionIdentifier: string,
|
|
335
353
|
cwd: string = process.cwd(),
|
|
@@ -340,8 +358,7 @@ export async function resolveActionProject(
|
|
|
340
358
|
if (currentRoot) {
|
|
341
359
|
try {
|
|
342
360
|
const config = loadProjectConfig(currentRoot);
|
|
343
|
-
|
|
344
|
-
if (actions.has(actionIdentifier)) {
|
|
361
|
+
if (await projectHasAction(currentRoot, config.actionsDir, actionIdentifier)) {
|
|
345
362
|
return {
|
|
346
363
|
projectRoot: currentRoot,
|
|
347
364
|
packageId: config.id,
|
|
@@ -370,25 +387,41 @@ export async function resolveActionProject(
|
|
|
370
387
|
const linkedList = listLinkedPackages(customHome);
|
|
371
388
|
|
|
372
389
|
if (targetPackage) {
|
|
390
|
+
let targetRoot: string | undefined;
|
|
391
|
+
let targetPkgId = targetPackage;
|
|
392
|
+
|
|
373
393
|
const pkg = linkedList.find(
|
|
374
394
|
(p) => p.id === targetPackage || getPackageSlug(p.id) === targetPackage
|
|
375
395
|
);
|
|
396
|
+
if (pkg && existsSync(pkg.path)) {
|
|
397
|
+
targetRoot = pkg.path;
|
|
398
|
+
targetPkgId = pkg.id;
|
|
399
|
+
} else if (currentRoot) {
|
|
400
|
+
try {
|
|
401
|
+
const config = loadProjectConfig(currentRoot);
|
|
402
|
+
if (config.id === targetPackage || getPackageSlug(config.id) === targetPackage) {
|
|
403
|
+
targetRoot = currentRoot;
|
|
404
|
+
targetPkgId = config.id;
|
|
405
|
+
}
|
|
406
|
+
} catch {
|
|
407
|
+
// Ignore
|
|
408
|
+
}
|
|
409
|
+
}
|
|
376
410
|
|
|
377
|
-
if (!
|
|
411
|
+
if (!targetRoot || !existsSync(targetRoot)) {
|
|
378
412
|
throw new Error(
|
|
379
413
|
`Linked package '${targetPackage}' not found or path no longer exists (${pkg?.path || "unregistered"}). Run 'ad link' in the package directory.`
|
|
380
414
|
);
|
|
381
415
|
}
|
|
382
416
|
|
|
383
|
-
const config = loadProjectConfig(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
throw new Error(`Action '${pureActionId}' not found in package '${pkg.id}' (${pkg.path})`);
|
|
417
|
+
const config = loadProjectConfig(targetRoot);
|
|
418
|
+
if (!(await projectHasAction(targetRoot, config.actionsDir, pureActionId))) {
|
|
419
|
+
throw new Error(`Action '${pureActionId}' not found in package '${targetPkgId}' (${targetRoot})`);
|
|
387
420
|
}
|
|
388
421
|
|
|
389
422
|
return {
|
|
390
|
-
projectRoot:
|
|
391
|
-
packageId:
|
|
423
|
+
projectRoot: targetRoot,
|
|
424
|
+
packageId: targetPkgId,
|
|
392
425
|
actionId: pureActionId,
|
|
393
426
|
};
|
|
394
427
|
}
|
|
@@ -400,8 +433,7 @@ export async function resolveActionProject(
|
|
|
400
433
|
if (!existsSync(pkg.path)) continue;
|
|
401
434
|
try {
|
|
402
435
|
const config = loadProjectConfig(pkg.path);
|
|
403
|
-
|
|
404
|
-
if (actions.has(actionIdentifier)) {
|
|
436
|
+
if (await projectHasAction(pkg.path, config.actionsDir, actionIdentifier)) {
|
|
405
437
|
matches.push({ entry: pkg, actionId: actionIdentifier });
|
|
406
438
|
}
|
|
407
439
|
} catch {
|
|
@@ -438,24 +470,69 @@ export function resolvePackageRoot(
|
|
|
438
470
|
cwd?: string,
|
|
439
471
|
customHome?: string
|
|
440
472
|
): string | null {
|
|
441
|
-
if (packageIdOrPath) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
473
|
+
if (!packageIdOrPath) {
|
|
474
|
+
return findProjectRoot(cwd);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const baseDir = cwd || process.cwd();
|
|
478
|
+
const resolvedPath = resolve(baseDir, packageIdOrPath);
|
|
479
|
+
|
|
480
|
+
// 1. Check if packageIdOrPath is an existing directory or file path on disk
|
|
481
|
+
if (existsSync(resolvedPath)) {
|
|
482
|
+
try {
|
|
483
|
+
const stat = statSync(resolvedPath);
|
|
484
|
+
const targetDir = stat.isDirectory() ? resolvedPath : dirname(resolvedPath);
|
|
485
|
+
if (existsSync(join(targetDir, "actiondock.json"))) {
|
|
486
|
+
return targetDir;
|
|
487
|
+
}
|
|
488
|
+
const parentRoot = findProjectRoot(targetDir);
|
|
489
|
+
if (parentRoot) {
|
|
490
|
+
return parentRoot;
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
493
|
+
// ignore
|
|
454
494
|
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// If it was explicitly a path (starts with . or / or ~ or contains / or \), and did not resolve above:
|
|
498
|
+
const isExplicitPath =
|
|
499
|
+
packageIdOrPath.startsWith(".") ||
|
|
500
|
+
packageIdOrPath.startsWith("/") ||
|
|
501
|
+
packageIdOrPath.startsWith("~") ||
|
|
502
|
+
packageIdOrPath.includes("/") ||
|
|
503
|
+
packageIdOrPath.includes("\\");
|
|
504
|
+
|
|
505
|
+
if (isExplicitPath) {
|
|
506
|
+
// An explicit path that does not exist or is not an ActionDock project must fail
|
|
455
507
|
return null;
|
|
456
508
|
}
|
|
457
509
|
|
|
458
|
-
|
|
510
|
+
// 2. Check linked packages in registry
|
|
511
|
+
const linkedList = listLinkedPackages(customHome);
|
|
512
|
+
const found = linkedList.find(
|
|
513
|
+
(p) =>
|
|
514
|
+
p.id === packageIdOrPath ||
|
|
515
|
+
getPackageSlug(p.id) === packageIdOrPath ||
|
|
516
|
+
p.path === resolvedPath
|
|
517
|
+
);
|
|
518
|
+
if (found) {
|
|
519
|
+
return found.path;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// 3. Check current project (from cwd)
|
|
523
|
+
const currentRoot = findProjectRoot(cwd);
|
|
524
|
+
if (currentRoot) {
|
|
525
|
+
try {
|
|
526
|
+
const config = loadProjectConfig(currentRoot);
|
|
527
|
+
if (config.id === packageIdOrPath || getPackageSlug(config.id) === packageIdOrPath) {
|
|
528
|
+
return currentRoot;
|
|
529
|
+
}
|
|
530
|
+
} catch {
|
|
531
|
+
// ignore broken config
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return null;
|
|
459
536
|
}
|
|
460
537
|
|
|
461
538
|
export function resolvePlaybookProject(
|
|
@@ -499,26 +576,43 @@ export function resolvePlaybookProject(
|
|
|
499
576
|
const linkedList = listLinkedPackages(customHome);
|
|
500
577
|
|
|
501
578
|
if (targetPackage) {
|
|
579
|
+
let targetRoot: string | undefined;
|
|
580
|
+
let targetPkgId = targetPackage;
|
|
581
|
+
|
|
502
582
|
const pkg = linkedList.find(
|
|
503
583
|
(p) => p.id === targetPackage || getPackageSlug(p.id) === targetPackage
|
|
504
584
|
);
|
|
585
|
+
if (pkg && existsSync(pkg.path)) {
|
|
586
|
+
targetRoot = pkg.path;
|
|
587
|
+
targetPkgId = pkg.id;
|
|
588
|
+
} else if (currentRoot) {
|
|
589
|
+
try {
|
|
590
|
+
const config = loadProjectConfig(currentRoot);
|
|
591
|
+
if (config.id === targetPackage || getPackageSlug(config.id) === targetPackage) {
|
|
592
|
+
targetRoot = currentRoot;
|
|
593
|
+
targetPkgId = config.id;
|
|
594
|
+
}
|
|
595
|
+
} catch {
|
|
596
|
+
// Ignore
|
|
597
|
+
}
|
|
598
|
+
}
|
|
505
599
|
|
|
506
|
-
if (!
|
|
600
|
+
if (!targetRoot || !existsSync(targetRoot)) {
|
|
507
601
|
throw new Error(
|
|
508
602
|
`Linked package '${targetPackage}' not found or path no longer exists (${pkg?.path || "unregistered"}). Run 'ad link' in the package directory.`
|
|
509
603
|
);
|
|
510
604
|
}
|
|
511
605
|
|
|
512
|
-
const config = loadProjectConfig(
|
|
513
|
-
const playbooks = loadPlaybooks(
|
|
606
|
+
const config = loadProjectConfig(targetRoot);
|
|
607
|
+
const playbooks = loadPlaybooks(targetRoot, config.playbooksDir);
|
|
514
608
|
const pb = playbooks.get(purePlaybookId);
|
|
515
609
|
if (!pb) {
|
|
516
|
-
throw new Error(`Playbook '${purePlaybookId}' not found in package '${
|
|
610
|
+
throw new Error(`Playbook '${purePlaybookId}' not found in package '${targetPkgId}' (${targetRoot})`);
|
|
517
611
|
}
|
|
518
612
|
|
|
519
613
|
return {
|
|
520
|
-
projectRoot:
|
|
521
|
-
packageId:
|
|
614
|
+
projectRoot: targetRoot,
|
|
615
|
+
packageId: targetPkgId,
|
|
522
616
|
playbookId: purePlaybookId,
|
|
523
617
|
playbook: pb,
|
|
524
618
|
};
|
package/src/runtime/context.ts
CHANGED
|
@@ -194,6 +194,7 @@ export interface ContextOptions {
|
|
|
194
194
|
signal?: AbortSignal;
|
|
195
195
|
process?: ProcessAPI;
|
|
196
196
|
progress?: ProgressReporter;
|
|
197
|
+
logger?: Logger;
|
|
197
198
|
onActionInvoke?: (
|
|
198
199
|
action: ActionDefinition,
|
|
199
200
|
input: unknown,
|
|
@@ -214,7 +215,7 @@ export function createActionContext(options: ContextOptions): ActionContext {
|
|
|
214
215
|
options.projectConfig
|
|
215
216
|
);
|
|
216
217
|
const state = new RuntimeStateStore(options.storage);
|
|
217
|
-
const log = new StderrLogger();
|
|
218
|
+
const log = options.logger || new StderrLogger();
|
|
218
219
|
const signal = options.signal ?? new AbortController().signal;
|
|
219
220
|
const currentRunId = options.runId || randomUUID();
|
|
220
221
|
const currentRootRunId = options.rootRunId || options.parentRunId || currentRunId;
|
package/src/runtime/index.ts
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 统一源码模块加载器接口。
|
|
5
|
+
* 解耦 Action 与各类扩展模块的具体加载机制(如 ECMAScript 原生 import、tsx 动态转译加载等)。
|
|
6
|
+
*/
|
|
7
|
+
export interface ModuleLoader {
|
|
8
|
+
/**
|
|
9
|
+
* 解析模块标识符为绝对路径或完整 URL。
|
|
10
|
+
*
|
|
11
|
+
* @param specifier 模块规范说明符或物理路径
|
|
12
|
+
* @param parentPath 发起解析的父级文件或目录路径
|
|
13
|
+
*/
|
|
14
|
+
resolve?(specifier: string, parentPath?: string): string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 动态加载模块并返回命名空间全量导出对象。
|
|
18
|
+
*
|
|
19
|
+
* @param specifier 模块规范说明符或物理路径
|
|
20
|
+
* @param parentPath 发起加载的父级文件或目录路径
|
|
21
|
+
*/
|
|
22
|
+
load<T = any>(specifier: string, parentPath?: string): Promise<T>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 加载模块并解包其默认导出(default 或 action 属性)。
|
|
26
|
+
*
|
|
27
|
+
* @param specifier 模块规范说明符或物理路径
|
|
28
|
+
* @param parentPath 发起加载的父级文件或目录路径
|
|
29
|
+
*/
|
|
30
|
+
loadDefault?<T = any>(specifier: string, parentPath?: string): Promise<T>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 基于标准 ECMAScript 动态 import 的默认模块加载器。
|
|
35
|
+
*/
|
|
36
|
+
export class DefaultModuleLoader implements ModuleLoader {
|
|
37
|
+
async load<T = any>(specifier: string, _parentPath?: string): Promise<T> {
|
|
38
|
+
const importSpecifier = specifier.startsWith("file://")
|
|
39
|
+
? specifier
|
|
40
|
+
: pathToFileURL(specifier).href;
|
|
41
|
+
return (await import(importSpecifier)) as T;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async loadDefault<T = any>(specifier: string, parentPath?: string): Promise<T> {
|
|
45
|
+
const mod = await this.load<any>(specifier, parentPath);
|
|
46
|
+
return (mod?.default !== undefined ? mod.default : mod?.action !== undefined ? mod.action : mod) as T;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let globalModuleLoader: ModuleLoader | undefined;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 注册全局模块加载器实现。
|
|
54
|
+
*/
|
|
55
|
+
export function setModuleLoader(loader: ModuleLoader): void {
|
|
56
|
+
globalModuleLoader = loader;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 获取当前全局模块加载器,若未显式注册则回退使用 DefaultModuleLoader。
|
|
61
|
+
*/
|
|
62
|
+
export function getModuleLoader(): ModuleLoader {
|
|
63
|
+
if (!globalModuleLoader) {
|
|
64
|
+
globalModuleLoader = new DefaultModuleLoader();
|
|
65
|
+
}
|
|
66
|
+
return globalModuleLoader;
|
|
67
|
+
}
|
package/src/runtime/process.ts
CHANGED
|
@@ -200,23 +200,24 @@ export class DefaultProcessExecutor implements ProcessExecutor {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
try {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
const res: DetachedProcessResult = {
|
|
204
|
+
ok: true,
|
|
205
|
+
pid: child.pid,
|
|
206
|
+
ready: true,
|
|
207
|
+
durationMs: Date.now() - startTime,
|
|
208
|
+
};
|
|
209
|
+
const isReady = await options.probe(res as any);
|
|
207
210
|
if (isReady) {
|
|
208
|
-
return
|
|
209
|
-
ok: true,
|
|
210
|
-
pid: child.pid,
|
|
211
|
-
ready: true,
|
|
212
|
-
durationMs: Date.now() - startTime,
|
|
213
|
-
};
|
|
211
|
+
return res;
|
|
214
212
|
}
|
|
215
213
|
} catch {
|
|
216
214
|
// 探测失败继续轮询
|
|
217
215
|
}
|
|
218
216
|
|
|
219
|
-
|
|
217
|
+
const remaining = deadline - Date.now();
|
|
218
|
+
if (remaining <= 0) break;
|
|
219
|
+
const sleepTime = Math.min(probeInterval, remaining);
|
|
220
|
+
await new Promise((r) => setTimeout(r, sleepTime));
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
return {
|