@actiondock/core 2.0.9 → 2.0.11-beta.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.
- 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 +348 -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 +271 -34
- package/src/runtime/context.ts +8 -3
- 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 +193 -68
- 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,166 @@ 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
|
+
|
|
351
|
+
function projectHasActionSync(
|
|
352
|
+
projectRoot: string,
|
|
353
|
+
actionsDir: string | undefined,
|
|
354
|
+
actionId: string
|
|
355
|
+
): boolean {
|
|
356
|
+
const manifest = loadManifest(projectRoot);
|
|
357
|
+
if (manifest?.actions && actionId in manifest.actions) {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
const dir = join(projectRoot, actionsDir || "actions");
|
|
361
|
+
if (!existsSync(dir)) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
if (actionId.includes("..") || actionId.startsWith("/") || actionId.startsWith("\\")) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (existsSync(join(dir, `${actionId}.ts`)) || existsSync(join(dir, `${actionId}.js`))) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
const relFile = actionId.replace(/\./g, "/");
|
|
371
|
+
if (existsSync(join(dir, `${relFile}.ts`)) || existsSync(join(dir, `${relFile}.js`))) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function resolveActionProjectSync(
|
|
378
|
+
actionIdentifier: string,
|
|
379
|
+
cwd: string = process.cwd(),
|
|
380
|
+
customHome?: string
|
|
381
|
+
): ResolvedActionProject {
|
|
382
|
+
// 1. Check current directory / parent project
|
|
383
|
+
const currentRoot = findProjectRoot(cwd);
|
|
384
|
+
if (currentRoot) {
|
|
385
|
+
try {
|
|
386
|
+
const config = loadProjectConfig(currentRoot);
|
|
387
|
+
if (projectHasActionSync(currentRoot, config.actionsDir, actionIdentifier)) {
|
|
388
|
+
return {
|
|
389
|
+
projectRoot: currentRoot,
|
|
390
|
+
packageId: config.id,
|
|
391
|
+
actionId: actionIdentifier,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
// Ignore and proceed to registry lookup
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 2. Check if scoped format: <package-id>/<action-id> or <package-id>:<action-id>
|
|
400
|
+
let targetPackage: string | undefined;
|
|
401
|
+
let pureActionId = actionIdentifier;
|
|
402
|
+
|
|
403
|
+
if (actionIdentifier.includes("/")) {
|
|
404
|
+
const slashIdx = actionIdentifier.lastIndexOf("/");
|
|
405
|
+
targetPackage = actionIdentifier.slice(0, slashIdx);
|
|
406
|
+
pureActionId = actionIdentifier.slice(slashIdx + 1);
|
|
407
|
+
} else if (actionIdentifier.includes(":")) {
|
|
408
|
+
const colonIdx = actionIdentifier.lastIndexOf(":");
|
|
409
|
+
targetPackage = actionIdentifier.slice(0, colonIdx);
|
|
410
|
+
pureActionId = actionIdentifier.slice(colonIdx + 1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const linkedList = listLinkedPackages(customHome);
|
|
414
|
+
|
|
415
|
+
if (targetPackage) {
|
|
416
|
+
let targetRoot: string | undefined;
|
|
417
|
+
let targetPkgId = targetPackage;
|
|
418
|
+
|
|
419
|
+
const pkg = linkedList.find(
|
|
420
|
+
(p) => p.id === targetPackage || getPackageSlug(p.id) === targetPackage
|
|
421
|
+
);
|
|
422
|
+
if (pkg && existsSync(pkg.path)) {
|
|
423
|
+
targetRoot = pkg.path;
|
|
424
|
+
targetPkgId = pkg.id;
|
|
425
|
+
} else if (currentRoot) {
|
|
426
|
+
try {
|
|
427
|
+
const config = loadProjectConfig(currentRoot);
|
|
428
|
+
if (config.id === targetPackage || getPackageSlug(config.id) === targetPackage) {
|
|
429
|
+
targetRoot = currentRoot;
|
|
430
|
+
targetPkgId = config.id;
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
// Ignore
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!targetRoot || !existsSync(targetRoot)) {
|
|
438
|
+
throw new Error(
|
|
439
|
+
`Linked package '${targetPackage}' not found or path no longer exists (${pkg?.path || "unregistered"}). Run 'ad link' in the package directory.`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const config = loadProjectConfig(targetRoot);
|
|
444
|
+
if (!projectHasActionSync(targetRoot, config.actionsDir, pureActionId)) {
|
|
445
|
+
throw new Error(`Action '${pureActionId}' not found in package '${targetPkgId}' (${targetRoot})`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
projectRoot: targetRoot,
|
|
450
|
+
packageId: targetPkgId,
|
|
451
|
+
actionId: pureActionId,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 3. Search across all linked packages
|
|
456
|
+
const matches: Array<{ entry: LinkedPackageEntry; actionId: string }> = [];
|
|
457
|
+
|
|
458
|
+
for (const pkg of linkedList) {
|
|
459
|
+
if (!existsSync(pkg.path)) continue;
|
|
460
|
+
try {
|
|
461
|
+
const config = loadProjectConfig(pkg.path);
|
|
462
|
+
if (projectHasActionSync(pkg.path, config.actionsDir, actionIdentifier)) {
|
|
463
|
+
matches.push({ entry: pkg, actionId: actionIdentifier });
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// Ignore invalid linked package
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (matches.length === 1) {
|
|
471
|
+
return {
|
|
472
|
+
projectRoot: matches[0].entry.path,
|
|
473
|
+
packageId: matches[0].entry.id,
|
|
474
|
+
actionId: matches[0].actionId,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (matches.length > 1) {
|
|
479
|
+
const pkgList = matches.map((m) => `'${m.entry.id}'`).join(", ");
|
|
480
|
+
throw new Error(
|
|
481
|
+
`Action '${actionIdentifier}' is provided by multiple linked packages: ${pkgList}. Please specify using '<package-id>/${actionIdentifier}'.`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (currentRoot) {
|
|
486
|
+
throw new Error(`Action '${actionIdentifier}' not found in current project or any linked packages`);
|
|
487
|
+
} else {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`Action '${actionIdentifier}' not found. You are not in an ActionDock project, and no linked package provides '${actionIdentifier}'. Use 'ad link' to register your package.`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
333
494
|
export async function resolveActionProject(
|
|
334
495
|
actionIdentifier: string,
|
|
335
496
|
cwd: string = process.cwd(),
|
|
@@ -340,8 +501,7 @@ export async function resolveActionProject(
|
|
|
340
501
|
if (currentRoot) {
|
|
341
502
|
try {
|
|
342
503
|
const config = loadProjectConfig(currentRoot);
|
|
343
|
-
|
|
344
|
-
if (actions.has(actionIdentifier)) {
|
|
504
|
+
if (await projectHasAction(currentRoot, config.actionsDir, actionIdentifier)) {
|
|
345
505
|
return {
|
|
346
506
|
projectRoot: currentRoot,
|
|
347
507
|
packageId: config.id,
|
|
@@ -358,11 +518,11 @@ export async function resolveActionProject(
|
|
|
358
518
|
let pureActionId = actionIdentifier;
|
|
359
519
|
|
|
360
520
|
if (actionIdentifier.includes("/")) {
|
|
361
|
-
const slashIdx = actionIdentifier.
|
|
521
|
+
const slashIdx = actionIdentifier.lastIndexOf("/");
|
|
362
522
|
targetPackage = actionIdentifier.slice(0, slashIdx);
|
|
363
523
|
pureActionId = actionIdentifier.slice(slashIdx + 1);
|
|
364
524
|
} else if (actionIdentifier.includes(":")) {
|
|
365
|
-
const colonIdx = actionIdentifier.
|
|
525
|
+
const colonIdx = actionIdentifier.lastIndexOf(":");
|
|
366
526
|
targetPackage = actionIdentifier.slice(0, colonIdx);
|
|
367
527
|
pureActionId = actionIdentifier.slice(colonIdx + 1);
|
|
368
528
|
}
|
|
@@ -370,25 +530,41 @@ export async function resolveActionProject(
|
|
|
370
530
|
const linkedList = listLinkedPackages(customHome);
|
|
371
531
|
|
|
372
532
|
if (targetPackage) {
|
|
533
|
+
let targetRoot: string | undefined;
|
|
534
|
+
let targetPkgId = targetPackage;
|
|
535
|
+
|
|
373
536
|
const pkg = linkedList.find(
|
|
374
537
|
(p) => p.id === targetPackage || getPackageSlug(p.id) === targetPackage
|
|
375
538
|
);
|
|
539
|
+
if (pkg && existsSync(pkg.path)) {
|
|
540
|
+
targetRoot = pkg.path;
|
|
541
|
+
targetPkgId = pkg.id;
|
|
542
|
+
} else if (currentRoot) {
|
|
543
|
+
try {
|
|
544
|
+
const config = loadProjectConfig(currentRoot);
|
|
545
|
+
if (config.id === targetPackage || getPackageSlug(config.id) === targetPackage) {
|
|
546
|
+
targetRoot = currentRoot;
|
|
547
|
+
targetPkgId = config.id;
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
// Ignore
|
|
551
|
+
}
|
|
552
|
+
}
|
|
376
553
|
|
|
377
|
-
if (!
|
|
554
|
+
if (!targetRoot || !existsSync(targetRoot)) {
|
|
378
555
|
throw new Error(
|
|
379
556
|
`Linked package '${targetPackage}' not found or path no longer exists (${pkg?.path || "unregistered"}). Run 'ad link' in the package directory.`
|
|
380
557
|
);
|
|
381
558
|
}
|
|
382
559
|
|
|
383
|
-
const config = loadProjectConfig(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
throw new Error(`Action '${pureActionId}' not found in package '${pkg.id}' (${pkg.path})`);
|
|
560
|
+
const config = loadProjectConfig(targetRoot);
|
|
561
|
+
if (!(await projectHasAction(targetRoot, config.actionsDir, pureActionId))) {
|
|
562
|
+
throw new Error(`Action '${pureActionId}' not found in package '${targetPkgId}' (${targetRoot})`);
|
|
387
563
|
}
|
|
388
564
|
|
|
389
565
|
return {
|
|
390
|
-
projectRoot:
|
|
391
|
-
packageId:
|
|
566
|
+
projectRoot: targetRoot,
|
|
567
|
+
packageId: targetPkgId,
|
|
392
568
|
actionId: pureActionId,
|
|
393
569
|
};
|
|
394
570
|
}
|
|
@@ -400,8 +576,7 @@ export async function resolveActionProject(
|
|
|
400
576
|
if (!existsSync(pkg.path)) continue;
|
|
401
577
|
try {
|
|
402
578
|
const config = loadProjectConfig(pkg.path);
|
|
403
|
-
|
|
404
|
-
if (actions.has(actionIdentifier)) {
|
|
579
|
+
if (await projectHasAction(pkg.path, config.actionsDir, actionIdentifier)) {
|
|
405
580
|
matches.push({ entry: pkg, actionId: actionIdentifier });
|
|
406
581
|
}
|
|
407
582
|
} catch {
|
|
@@ -438,24 +613,69 @@ export function resolvePackageRoot(
|
|
|
438
613
|
cwd?: string,
|
|
439
614
|
customHome?: string
|
|
440
615
|
): string | null {
|
|
441
|
-
if (packageIdOrPath) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
616
|
+
if (!packageIdOrPath) {
|
|
617
|
+
return findProjectRoot(cwd);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const baseDir = cwd || process.cwd();
|
|
621
|
+
const resolvedPath = resolve(baseDir, packageIdOrPath);
|
|
622
|
+
|
|
623
|
+
// 1. Check if packageIdOrPath is an existing directory or file path on disk
|
|
624
|
+
if (existsSync(resolvedPath)) {
|
|
625
|
+
try {
|
|
626
|
+
const stat = statSync(resolvedPath);
|
|
627
|
+
const targetDir = stat.isDirectory() ? resolvedPath : dirname(resolvedPath);
|
|
628
|
+
if (existsSync(join(targetDir, "actiondock.json"))) {
|
|
629
|
+
return targetDir;
|
|
630
|
+
}
|
|
631
|
+
const parentRoot = findProjectRoot(targetDir);
|
|
632
|
+
if (parentRoot) {
|
|
633
|
+
return parentRoot;
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
// ignore
|
|
454
637
|
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// If it was explicitly a path (starts with . or / or ~ or contains / or \), and did not resolve above:
|
|
641
|
+
const isExplicitPath =
|
|
642
|
+
packageIdOrPath.startsWith(".") ||
|
|
643
|
+
packageIdOrPath.startsWith("/") ||
|
|
644
|
+
packageIdOrPath.startsWith("~") ||
|
|
645
|
+
packageIdOrPath.includes("/") ||
|
|
646
|
+
packageIdOrPath.includes("\\");
|
|
647
|
+
|
|
648
|
+
if (isExplicitPath) {
|
|
649
|
+
// An explicit path that does not exist or is not an ActionDock project must fail
|
|
455
650
|
return null;
|
|
456
651
|
}
|
|
457
652
|
|
|
458
|
-
|
|
653
|
+
// 2. Check linked packages in registry
|
|
654
|
+
const linkedList = listLinkedPackages(customHome);
|
|
655
|
+
const found = linkedList.find(
|
|
656
|
+
(p) =>
|
|
657
|
+
p.id === packageIdOrPath ||
|
|
658
|
+
getPackageSlug(p.id) === packageIdOrPath ||
|
|
659
|
+
p.path === resolvedPath
|
|
660
|
+
);
|
|
661
|
+
if (found) {
|
|
662
|
+
return found.path;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// 3. Check current project (from cwd)
|
|
666
|
+
const currentRoot = findProjectRoot(cwd);
|
|
667
|
+
if (currentRoot) {
|
|
668
|
+
try {
|
|
669
|
+
const config = loadProjectConfig(currentRoot);
|
|
670
|
+
if (config.id === packageIdOrPath || getPackageSlug(config.id) === packageIdOrPath) {
|
|
671
|
+
return currentRoot;
|
|
672
|
+
}
|
|
673
|
+
} catch {
|
|
674
|
+
// ignore broken config
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return null;
|
|
459
679
|
}
|
|
460
680
|
|
|
461
681
|
export function resolvePlaybookProject(
|
|
@@ -499,26 +719,43 @@ export function resolvePlaybookProject(
|
|
|
499
719
|
const linkedList = listLinkedPackages(customHome);
|
|
500
720
|
|
|
501
721
|
if (targetPackage) {
|
|
722
|
+
let targetRoot: string | undefined;
|
|
723
|
+
let targetPkgId = targetPackage;
|
|
724
|
+
|
|
502
725
|
const pkg = linkedList.find(
|
|
503
726
|
(p) => p.id === targetPackage || getPackageSlug(p.id) === targetPackage
|
|
504
727
|
);
|
|
728
|
+
if (pkg && existsSync(pkg.path)) {
|
|
729
|
+
targetRoot = pkg.path;
|
|
730
|
+
targetPkgId = pkg.id;
|
|
731
|
+
} else if (currentRoot) {
|
|
732
|
+
try {
|
|
733
|
+
const config = loadProjectConfig(currentRoot);
|
|
734
|
+
if (config.id === targetPackage || getPackageSlug(config.id) === targetPackage) {
|
|
735
|
+
targetRoot = currentRoot;
|
|
736
|
+
targetPkgId = config.id;
|
|
737
|
+
}
|
|
738
|
+
} catch {
|
|
739
|
+
// Ignore
|
|
740
|
+
}
|
|
741
|
+
}
|
|
505
742
|
|
|
506
|
-
if (!
|
|
743
|
+
if (!targetRoot || !existsSync(targetRoot)) {
|
|
507
744
|
throw new Error(
|
|
508
745
|
`Linked package '${targetPackage}' not found or path no longer exists (${pkg?.path || "unregistered"}). Run 'ad link' in the package directory.`
|
|
509
746
|
);
|
|
510
747
|
}
|
|
511
748
|
|
|
512
|
-
const config = loadProjectConfig(
|
|
513
|
-
const playbooks = loadPlaybooks(
|
|
749
|
+
const config = loadProjectConfig(targetRoot);
|
|
750
|
+
const playbooks = loadPlaybooks(targetRoot, config.playbooksDir);
|
|
514
751
|
const pb = playbooks.get(purePlaybookId);
|
|
515
752
|
if (!pb) {
|
|
516
|
-
throw new Error(`Playbook '${purePlaybookId}' not found in package '${
|
|
753
|
+
throw new Error(`Playbook '${purePlaybookId}' not found in package '${targetPkgId}' (${targetRoot})`);
|
|
517
754
|
}
|
|
518
755
|
|
|
519
756
|
return {
|
|
520
|
-
projectRoot:
|
|
521
|
-
packageId:
|
|
757
|
+
projectRoot: targetRoot,
|
|
758
|
+
packageId: targetPkgId,
|
|
522
759
|
playbookId: purePlaybookId,
|
|
523
760
|
playbook: pb,
|
|
524
761
|
};
|
package/src/runtime/context.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ActionContext,
|
|
4
4
|
ActionDefinition,
|
|
5
5
|
ActionInvoker,
|
|
6
|
+
ActionRef,
|
|
6
7
|
Config,
|
|
7
8
|
Logger,
|
|
8
9
|
ProcessAPI,
|
|
@@ -194,8 +195,9 @@ export interface ContextOptions {
|
|
|
194
195
|
signal?: AbortSignal;
|
|
195
196
|
process?: ProcessAPI;
|
|
196
197
|
progress?: ProgressReporter;
|
|
198
|
+
logger?: Logger;
|
|
197
199
|
onActionInvoke?: (
|
|
198
|
-
action: ActionDefinition,
|
|
200
|
+
action: ActionDefinition | ActionRef | string,
|
|
199
201
|
input: unknown,
|
|
200
202
|
parentRunId?: string
|
|
201
203
|
) => Promise<unknown>;
|
|
@@ -214,13 +216,16 @@ export function createActionContext(options: ContextOptions): ActionContext {
|
|
|
214
216
|
options.projectConfig
|
|
215
217
|
);
|
|
216
218
|
const state = new RuntimeStateStore(options.storage);
|
|
217
|
-
const log = new StderrLogger();
|
|
219
|
+
const log = options.logger || new StderrLogger();
|
|
218
220
|
const signal = options.signal ?? new AbortController().signal;
|
|
219
221
|
const currentRunId = options.runId || randomUUID();
|
|
220
222
|
const currentRootRunId = options.rootRunId || options.parentRunId || currentRunId;
|
|
221
223
|
|
|
222
224
|
const invoker: ActionInvoker = {
|
|
223
|
-
async invoke<I, O>(
|
|
225
|
+
async invoke<I, O>(
|
|
226
|
+
action: ActionDefinition<I, O> | ActionRef | string,
|
|
227
|
+
input?: I
|
|
228
|
+
): Promise<O> {
|
|
224
229
|
if (options.onActionInvoke) {
|
|
225
230
|
return (await options.onActionInvoke(
|
|
226
231
|
action as any,
|