@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.
@@ -4,6 +4,7 @@ import type {
4
4
  ActionDefinition,
5
5
  ExecutionResult,
6
6
  JsonValue,
7
+ Logger,
7
8
  ProcessAPI,
8
9
  ProgressReporter,
9
10
  RuntimeError,
@@ -12,8 +13,7 @@ import type {
12
13
  import type { ProjectConfig } from "../project/types";
13
14
  import { validateSchema } from "../schema/validator";
14
15
  import type { RuntimeStorage, TerminalRunStatus } from "../storage/types";
15
- import { RuntimeConfig, RuntimeStateStore, StderrLogger } from "./context";
16
- import { getProcessExecutor } from "./process";
16
+ import { createActionContext, StderrLogger } from "./context";
17
17
 
18
18
  /**
19
19
  * ActionRunner 初始化配置选项。
@@ -37,6 +37,8 @@ export interface RunnerOptions {
37
37
  * 启动 Action 执行时的可选控制参数。
38
38
  */
39
39
  export interface ExecutionStartOptions {
40
+ /** 显式指定的运行 ID */
41
+ runId?: string;
40
42
  /** 根运行 ID */
41
43
  rootRunId?: string;
42
44
  /** 父级运行 ID(嵌套调用场景下建立调用链树) */
@@ -57,6 +59,8 @@ export interface ExecutionStartOptions {
57
59
  process?: ProcessAPI;
58
60
  /** 外部注入的进度报告器 */
59
61
  progress?: ProgressReporter;
62
+ /** 外部注入的日志记录器 */
63
+ logger?: Logger;
60
64
  }
61
65
 
62
66
  /**
@@ -134,7 +138,7 @@ export class ActionRunner {
134
138
  input: unknown = {},
135
139
  options: ExecutionStartOptions = {}
136
140
  ): ExecutionHandle {
137
- const runId = randomUUID();
141
+ const runId = options.runId || randomUUID();
138
142
  const startedAt = new Date().toISOString();
139
143
  const callStack = [...(options.callStack || [])];
140
144
 
@@ -243,26 +247,26 @@ export class ActionRunner {
243
247
  };
244
248
 
245
249
  // 5. 构建 ActionContext 运行时上下文
246
- const config = new RuntimeConfig(
247
- this.storage,
248
- this.configOverrides,
249
- this.projectConfig
250
- );
251
- const state = new RuntimeStateStore(this.storage);
252
- const log = new StderrLogger(action.id);
253
-
254
- const invoker = {
255
- invoke: async <I, O>(
256
- childAction: ActionDefinition<I, O>,
257
- childInput: I
258
- ): Promise<O> => {
250
+ const ctx = createActionContext({
251
+ storage: this.storage,
252
+ overrides: this.configOverrides,
253
+ projectConfig: this.projectConfig,
254
+ runId,
255
+ rootRunId: initialRun.rootRunId,
256
+ parentRunId: options.parentRunId,
257
+ signal: controller.signal,
258
+ process: options.process,
259
+ progress: options.progress,
260
+ logger: options.logger || new StderrLogger(action.id),
261
+ onActionInvoke: async (childAction, childInput, parentRunId) => {
259
262
  const childResult = await this.execute(childAction, childInput, {
260
263
  rootRunId: initialRun.rootRunId,
261
- parentRunId: runId,
264
+ parentRunId,
262
265
  callStack,
263
266
  signal: controller.signal,
264
267
  process: options.process,
265
268
  progress: options.progress,
269
+ logger: options.logger,
266
270
  });
267
271
  if (!childResult.ok) {
268
272
  const err = new Error(childResult.error.message);
@@ -270,26 +274,9 @@ export class ActionRunner {
270
274
  (err as any).details = childResult.error.details;
271
275
  throw err;
272
276
  }
273
- return childResult.data as O;
277
+ return childResult.data;
274
278
  },
275
- };
276
-
277
- const ctx: ActionContext = {
278
- config,
279
- state,
280
- actions: invoker,
281
- process: options.process || getProcessExecutor(),
282
- log,
283
- progress: options.progress || {
284
- report() {},
285
- },
286
- signal: controller.signal,
287
- run: {
288
- id: runId,
289
- rootId: initialRun.rootRunId,
290
- parentId: options.parentRunId,
291
- },
292
- };
279
+ });
293
280
 
294
281
  // 6. 执行 Action 业务逻辑并与取消/超时信号进行竞态
295
282
  const abortPromise = new Promise<never>((_, reject) => {
@@ -2,5 +2,6 @@ export * from "./types";
2
2
  export * from "./security";
3
3
  export * from "./body";
4
4
  export * from "./runtime-registry";
5
+ export * from "./routes";
5
6
  export * from "./server";
6
7
 
@@ -0,0 +1,262 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { filterByIntent } from "../../filter";
4
+ import { loadActions, loadProjectConfig } from "../../project/loader";
5
+ import { listLinkedPackages, resolveActionProject } from "../../registry/registry";
6
+ import { ActionRunner } from "../../runtime/runner";
7
+ import { InvalidJsonError, readJsonBody, RequestTooLargeError } from "../body";
8
+ import { type RouteContext, jsonResponse } from "./common";
9
+
10
+ /**
11
+ * 处理 Action 列表、详情查询及同步/异步运行接口。
12
+ */
13
+ export async function handleActionsRoutes(ctx: RouteContext): Promise<Response | null> {
14
+ const { req, url, pathname, corsHeaders, projectRoot, customHome, runtimeRegistry, options } = ctx;
15
+
16
+ // 1. Actions List: GET /api/v1/actions
17
+ if (pathname === "/api/v1/actions" && req.method === "GET") {
18
+ try {
19
+ const actionList: Array<{
20
+ id: string;
21
+ description: string;
22
+ packageId?: string;
23
+ }> = [];
24
+
25
+ if (projectRoot) {
26
+ const config = loadProjectConfig(projectRoot);
27
+ const actions = await loadActions(projectRoot, config.actionsDir);
28
+ for (const [id, a] of actions.entries()) {
29
+ actionList.push({
30
+ id,
31
+ description: a.description || "",
32
+ packageId: config.id,
33
+ });
34
+ }
35
+ }
36
+
37
+ const linked = listLinkedPackages(customHome);
38
+ for (const pkg of linked) {
39
+ if (projectRoot && pkg.path === projectRoot) continue;
40
+ if (!existsSync(pkg.path)) continue;
41
+ try {
42
+ const config = loadProjectConfig(pkg.path);
43
+ const actions = await loadActions(pkg.path, config.actionsDir);
44
+ for (const [id, a] of actions.entries()) {
45
+ actionList.push({
46
+ id,
47
+ description: a.description || "",
48
+ packageId: pkg.id,
49
+ });
50
+ }
51
+ } catch {
52
+ // 忽略故障包
53
+ }
54
+ }
55
+
56
+ const intent = url.searchParams.get("intent");
57
+ const targetPkg = url.searchParams.get("package");
58
+
59
+ let filtered = targetPkg
60
+ ? actionList.filter((a) => a.packageId === targetPkg)
61
+ : actionList;
62
+
63
+ if (intent) {
64
+ filtered = filterByIntent(
65
+ filtered,
66
+ intent,
67
+ [(a) => a.id, (a) => a.description, (a) => a.packageId],
68
+ false
69
+ );
70
+ }
71
+
72
+ return jsonResponse(filtered, 200, corsHeaders);
73
+ } catch (err: any) {
74
+ return jsonResponse(
75
+ {
76
+ ok: false,
77
+ error: { code: "ACTIONS_LIST_ERROR", message: err.message },
78
+ },
79
+ 500,
80
+ corsHeaders
81
+ );
82
+ }
83
+ }
84
+
85
+ // 2. Action Show: GET /api/v1/actions/:id
86
+ const actionShowMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)$/);
87
+ if (actionShowMatch && req.method === "GET") {
88
+ const actionId = decodeURIComponent(actionShowMatch[1]);
89
+ try {
90
+ const resolved = await resolveActionProject(
91
+ actionId,
92
+ projectRoot || process.cwd(),
93
+ customHome
94
+ );
95
+ const config = loadProjectConfig(resolved.projectRoot);
96
+ const actions = await loadActions(resolved.projectRoot, config.actionsDir);
97
+ const action = actions.get(resolved.actionId);
98
+ if (!action) {
99
+ return jsonResponse(
100
+ {
101
+ ok: false,
102
+ error: {
103
+ code: "ACTION_NOT_FOUND",
104
+ message: `Action '${resolved.actionId}' not found in package '${resolved.packageId}'`,
105
+ },
106
+ },
107
+ 404,
108
+ corsHeaders
109
+ );
110
+ }
111
+
112
+ return jsonResponse(
113
+ {
114
+ id: action.id,
115
+ packageId: resolved.packageId,
116
+ description: action.description || "",
117
+ inputSchema: action.inputSchema || null,
118
+ outputSchema: action.outputSchema || null,
119
+ },
120
+ 200,
121
+ corsHeaders
122
+ );
123
+ } catch (err: any) {
124
+ return jsonResponse(
125
+ {
126
+ ok: false,
127
+ error: { code: "ACTION_NOT_FOUND", message: err.message },
128
+ },
129
+ 404,
130
+ corsHeaders
131
+ );
132
+ }
133
+ }
134
+
135
+ // 3. Action Run: POST /api/v1/actions/:id/run
136
+ const actionRunMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)\/run$/);
137
+ if (actionRunMatch && req.method === "POST") {
138
+ const actionId = decodeURIComponent(actionRunMatch[1]);
139
+ let body: any = {};
140
+ try {
141
+ body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
142
+ } catch (err: any) {
143
+ if (err instanceof RequestTooLargeError) {
144
+ return jsonResponse(
145
+ {
146
+ ok: false,
147
+ runId: randomUUID(),
148
+ error: { code: "REQUEST_TOO_LARGE", message: err.message },
149
+ },
150
+ 413,
151
+ corsHeaders
152
+ );
153
+ }
154
+ if (err instanceof InvalidJsonError) {
155
+ return jsonResponse(
156
+ {
157
+ ok: false,
158
+ runId: randomUUID(),
159
+ error: { code: "INVALID_JSON", message: err.message },
160
+ },
161
+ 400,
162
+ corsHeaders
163
+ );
164
+ }
165
+ return jsonResponse(
166
+ {
167
+ ok: false,
168
+ runId: randomUUID(),
169
+ error: { code: "INVALID_JSON", message: `Failed to parse request body: ${err.message}` },
170
+ },
171
+ 400,
172
+ corsHeaders
173
+ );
174
+ }
175
+
176
+ try {
177
+ const resolved = await resolveActionProject(
178
+ actionId,
179
+ projectRoot || process.cwd(),
180
+ customHome
181
+ );
182
+ const config = loadProjectConfig(resolved.projectRoot);
183
+ const actions = await loadActions(resolved.projectRoot, config.actionsDir);
184
+ const storage = runtimeRegistry.getStorage(config.id, resolved.projectRoot);
185
+
186
+ const runner = new ActionRunner({
187
+ packageId: config.id,
188
+ storage,
189
+ projectConfig: config,
190
+ configOverrides: body?.config || {},
191
+ actions,
192
+ });
193
+
194
+ const isAsync = body?.execution?.mode === "async" || body?.async === true;
195
+ const timeoutMs =
196
+ typeof body?.execution?.timeoutMs === "number" && body.execution.timeoutMs > 0
197
+ ? body.execution.timeoutMs
198
+ : undefined;
199
+
200
+ if (isAsync) {
201
+ const handle = runner.start(resolved.actionId, body?.input || {}, {
202
+ timeoutMs,
203
+ });
204
+ runtimeRegistry.executionManager.register(handle);
205
+
206
+ // 监听结算以广播 SSE 完成事件
207
+ handle.result
208
+ .then((res) => {
209
+ runtimeRegistry.emit(handle.runId, { type: "finish", data: res });
210
+ })
211
+ .catch((err) => {
212
+ runtimeRegistry.emit(handle.runId, {
213
+ type: "finish",
214
+ data: {
215
+ ok: false,
216
+ error: {
217
+ code: "ACTION_EXECUTION_ERROR",
218
+ message: err?.message || String(err),
219
+ },
220
+ },
221
+ });
222
+ });
223
+
224
+ return jsonResponse(
225
+ {
226
+ ok: true,
227
+ runId: handle.runId,
228
+ status: "running",
229
+ streamUrl: `/api/v1/runs/${handle.runId}/stream`,
230
+ },
231
+ 202,
232
+ corsHeaders
233
+ );
234
+ }
235
+
236
+ // 同步执行模式
237
+ const handle = runner.start(resolved.actionId, body?.input || {}, {
238
+ signal: req.signal,
239
+ timeoutMs,
240
+ });
241
+ runtimeRegistry.executionManager.register(handle);
242
+ const result = await handle.result;
243
+
244
+ return jsonResponse(result, 200, corsHeaders);
245
+ } catch (err: any) {
246
+ return jsonResponse(
247
+ {
248
+ ok: false,
249
+ runId: randomUUID(),
250
+ error: {
251
+ code: "ACTION_EXECUTION_ERROR",
252
+ message: err.message || String(err),
253
+ },
254
+ },
255
+ 500,
256
+ corsHeaders
257
+ );
258
+ }
259
+ }
260
+
261
+ return null;
262
+ }
@@ -0,0 +1,126 @@
1
+ import { existsSync } from "node:fs";
2
+ import { loadProjectConfig } from "../../project/loader";
3
+ import { listLinkedPackages, resolvePackageRoot } from "../../registry/registry";
4
+ import type { RuntimeStorage } from "../../storage/types";
5
+ import type { RunRecord } from "@actiondock/sdk";
6
+ import type { ServerRuntimeRegistry } from "../runtime-registry";
7
+ import type { ServerOptions } from "../types";
8
+
9
+ /**
10
+ * 路由处理统一上下文对象。
11
+ */
12
+ export interface RouteContext {
13
+ req: Request;
14
+ url: URL;
15
+ pathname: string;
16
+ corsHeaders: Record<string, string>;
17
+ projectRoot: string | null;
18
+ customHome?: string;
19
+ runtimeRegistry: ServerRuntimeRegistry;
20
+ options: ServerOptions;
21
+ }
22
+
23
+ /**
24
+ * 辅助函数:构造带 CORS 头的标准 JSON HTTP 响应。
25
+ */
26
+ export function jsonResponse(
27
+ data: unknown,
28
+ status = 200,
29
+ corsHeaders: Record<string, string> = {}
30
+ ): Response {
31
+ return new Response(JSON.stringify(data, null, 2), {
32
+ status,
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ ...corsHeaders,
36
+ },
37
+ });
38
+ }
39
+
40
+ /**
41
+ * 依据 package 参数或项目上下文解析目标 Storage 实例与根目录。
42
+ */
43
+ export function resolveStorageForPackage(
44
+ packageIdOrPath: string | undefined,
45
+ runtimeRegistry: ServerRuntimeRegistry,
46
+ projectRoot?: string | null,
47
+ customHome?: string
48
+ ): { packageId: string; storage: RuntimeStorage; projectRoot?: string } {
49
+ if (packageIdOrPath) {
50
+ const root = resolvePackageRoot(packageIdOrPath, customHome);
51
+ if (root) {
52
+ const config = loadProjectConfig(root);
53
+ return {
54
+ packageId: config.id,
55
+ storage: runtimeRegistry.getStorage(config.id, root),
56
+ projectRoot: root,
57
+ };
58
+ }
59
+ return {
60
+ packageId: packageIdOrPath,
61
+ storage: runtimeRegistry.getStorage(packageIdOrPath),
62
+ };
63
+ }
64
+
65
+ if (projectRoot) {
66
+ const config = loadProjectConfig(projectRoot);
67
+ return {
68
+ packageId: config.id,
69
+ storage: runtimeRegistry.getStorage(config.id, projectRoot),
70
+ projectRoot,
71
+ };
72
+ }
73
+
74
+ const linked = listLinkedPackages(customHome);
75
+ if (linked.length > 0) {
76
+ const first = linked[0];
77
+ return {
78
+ packageId: first.id,
79
+ storage: runtimeRegistry.getStorage(first.id, first.path),
80
+ projectRoot: first.path,
81
+ };
82
+ }
83
+
84
+ return {
85
+ packageId: "default",
86
+ storage: runtimeRegistry.getStorage("default"),
87
+ };
88
+ }
89
+
90
+ /**
91
+ * 跨活跃连接与所有已知持久化存储全局检索指定 runId 的运行记录。
92
+ */
93
+ export function findRunAcrossStorages(
94
+ runId: string,
95
+ runtimeRegistry: ServerRuntimeRegistry,
96
+ projectRoot?: string | null,
97
+ customHome?: string
98
+ ): { storage: RuntimeStorage; run: RunRecord } | null {
99
+ const inMemory = runtimeRegistry.findRun(runId);
100
+ if (inMemory) return inMemory as { storage: RuntimeStorage; run: RunRecord };
101
+
102
+ if (projectRoot) {
103
+ try {
104
+ const config = loadProjectConfig(projectRoot);
105
+ const storage = runtimeRegistry.getStorage(config.id, projectRoot);
106
+ const run = storage.getRun(runId);
107
+ if (run) return { storage, run };
108
+ } catch {
109
+ // 忽略读取错误
110
+ }
111
+ }
112
+
113
+ try {
114
+ const linked = listLinkedPackages(customHome);
115
+ for (const pkg of linked) {
116
+ if (!existsSync(pkg.path)) continue;
117
+ const storage = runtimeRegistry.getStorage(pkg.id, pkg.path);
118
+ const run = storage.getRun(runId);
119
+ if (run) return { storage, run };
120
+ }
121
+ } catch {
122
+ // 忽略读取错误
123
+ }
124
+
125
+ return null;
126
+ }
@@ -0,0 +1,147 @@
1
+ import { loadProjectConfig } from "../../project/loader";
2
+ import { readJsonBody } from "../body";
3
+ import { type RouteContext, jsonResponse, resolveStorageForPackage } from "./common";
4
+
5
+ /**
6
+ * 处理配置元数据与当前值读取、更新及删除接口。
7
+ */
8
+ export async function handleConfigRoutes(ctx: RouteContext): Promise<Response | null> {
9
+ const { req, url, pathname, corsHeaders, projectRoot, customHome, runtimeRegistry, options } = ctx;
10
+
11
+ // 1. Config Env Check: GET /api/v1/config/env
12
+ if (pathname === "/api/v1/config/env" && req.method === "GET") {
13
+ try {
14
+ const pkgParam = url.searchParams.get("package") || undefined;
15
+ const { packageId, projectRoot: root } = resolveStorageForPackage(
16
+ pkgParam,
17
+ runtimeRegistry,
18
+ projectRoot,
19
+ customHome
20
+ );
21
+ if (!root) {
22
+ return jsonResponse({ ok: true, packageId, envChecks: [] }, 200, corsHeaders);
23
+ }
24
+ const cfg = loadProjectConfig(root);
25
+ const declared = cfg.config || {};
26
+ const envChecks: any[] = [];
27
+ for (const [k, def] of Object.entries(declared)) {
28
+ const envKeys = [
29
+ k,
30
+ `ACTIONDOCK_${k}`,
31
+ `${packageId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_${k}`,
32
+ ];
33
+ const foundEnv = envKeys.find((ek) => process.env[ek] !== undefined);
34
+ envChecks.push({
35
+ key: k,
36
+ required: def.default === undefined,
37
+ satisfied: Boolean(foundEnv || def.default !== undefined),
38
+ matchedEnv: foundEnv || null,
39
+ hasDefault: def.default !== undefined,
40
+ secret: Boolean(def.secret),
41
+ });
42
+ }
43
+ return jsonResponse({ ok: true, packageId, envChecks }, 200, corsHeaders);
44
+ } catch (err: any) {
45
+ return jsonResponse(
46
+ { ok: false, error: { code: "CONFIG_ENV_ERROR", message: err.message } },
47
+ 500,
48
+ corsHeaders
49
+ );
50
+ }
51
+ }
52
+
53
+ // 2. Config Query: GET /api/v1/config
54
+ if (pathname === "/api/v1/config" && req.method === "GET") {
55
+ try {
56
+ const pkgParam = url.searchParams.get("package") || undefined;
57
+ const { packageId, storage, projectRoot: root } = resolveStorageForPackage(
58
+ pkgParam,
59
+ runtimeRegistry,
60
+ projectRoot,
61
+ customHome
62
+ );
63
+ const stored = storage.listConfig();
64
+ let declared: Record<string, any> = {};
65
+ if (root) {
66
+ try {
67
+ const cfg = loadProjectConfig(root);
68
+ declared = cfg.config || {};
69
+ } catch {}
70
+ }
71
+ const maskedValues: Record<string, any> = {};
72
+ for (const [k, v] of Object.entries(stored)) {
73
+ if (declared[k]?.secret) {
74
+ maskedValues[k] = "******";
75
+ } else {
76
+ maskedValues[k] = v;
77
+ }
78
+ }
79
+ return jsonResponse(
80
+ { ok: true, packageId, declared, values: maskedValues },
81
+ 200,
82
+ corsHeaders
83
+ );
84
+ } catch (err: any) {
85
+ return jsonResponse(
86
+ { ok: false, error: { code: "CONFIG_LIST_ERROR", message: err.message } },
87
+ 500,
88
+ corsHeaders
89
+ );
90
+ }
91
+ }
92
+
93
+ // 3. Config Update: PUT / POST /api/v1/config
94
+ if (pathname === "/api/v1/config" && (req.method === "PUT" || req.method === "POST")) {
95
+ try {
96
+ const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
97
+ const pkgParam = url.searchParams.get("package") || body.package || undefined;
98
+ const { packageId, storage } = resolveStorageForPackage(
99
+ pkgParam,
100
+ runtimeRegistry,
101
+ projectRoot,
102
+ customHome
103
+ );
104
+ const key = body.key;
105
+ if (!key) {
106
+ return jsonResponse(
107
+ { ok: false, error: { code: "INVALID_ARGUMENT", message: "Config 'key' is required" } },
108
+ 400,
109
+ corsHeaders
110
+ );
111
+ }
112
+ storage.setConfig(key, body.value);
113
+ return jsonResponse({ ok: true, packageId, key, message: "updated" }, 200, corsHeaders);
114
+ } catch (err: any) {
115
+ return jsonResponse(
116
+ { ok: false, error: { code: "CONFIG_SET_ERROR", message: err.message } },
117
+ 500,
118
+ corsHeaders
119
+ );
120
+ }
121
+ }
122
+
123
+ // 4. Config Delete: DELETE /api/v1/config/:key
124
+ const configKeyMatch = pathname.match(/^\/api\/v1\/config\/([^/]+)$/);
125
+ if (configKeyMatch && req.method === "DELETE") {
126
+ try {
127
+ const key = decodeURIComponent(configKeyMatch[1]);
128
+ const pkgParam = url.searchParams.get("package") || undefined;
129
+ const { packageId, storage } = resolveStorageForPackage(
130
+ pkgParam,
131
+ runtimeRegistry,
132
+ projectRoot,
133
+ customHome
134
+ );
135
+ const deleted = storage.deleteConfig(key);
136
+ return jsonResponse({ ok: true, packageId, key, deleted }, 200, corsHeaders);
137
+ } catch (err: any) {
138
+ return jsonResponse(
139
+ { ok: false, error: { code: "CONFIG_DELETE_ERROR", message: err.message } },
140
+ 500,
141
+ corsHeaders
142
+ );
143
+ }
144
+ }
145
+
146
+ return null;
147
+ }
@@ -0,0 +1,29 @@
1
+ import { runDoctorChecks } from "../../doctor/doctor";
2
+ import { type RouteContext, jsonResponse } from "./common";
3
+
4
+ /**
5
+ * 处理环境与依赖诊断接口(GET /api/v1/doctor)。
6
+ */
7
+ export async function handleDoctorRoute(ctx: RouteContext): Promise<Response | null> {
8
+ const { req, url, pathname, corsHeaders, projectRoot, customHome } = ctx;
9
+
10
+ if (pathname !== "/api/v1/doctor" || req.method !== "GET") {
11
+ return null;
12
+ }
13
+
14
+ try {
15
+ const targetPkg = url.searchParams.get("package") || undefined;
16
+ const report = await runDoctorChecks({
17
+ cwd: projectRoot || process.cwd(),
18
+ packageIdOrPath: targetPkg,
19
+ customHome,
20
+ });
21
+ return jsonResponse({ ok: true, report }, 200, corsHeaders);
22
+ } catch (err: any) {
23
+ return jsonResponse(
24
+ { ok: false, error: { code: "DOCTOR_ERROR", message: err.message } },
25
+ 500,
26
+ corsHeaders
27
+ );
28
+ }
29
+ }