@actiondock/core 2.0.8 → 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.
@@ -1,9 +1,39 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import type { ActionDockManifest, ActionManifestEntry } from "./types";
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
+
@@ -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
+
@@ -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
- const actions = await loadActions(currentRoot, config.actionsDir);
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 (!pkg || !existsSync(pkg.path)) {
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(pkg.path);
384
- const actions = await loadActions(pkg.path, config.actionsDir);
385
- if (!actions.has(pureActionId)) {
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: pkg.path,
391
- packageId: pkg.id,
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
- const actions = await loadActions(pkg.path, config.actionsDir);
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
- const directRoot = findProjectRoot(packageIdOrPath);
443
- if (directRoot) return directRoot;
444
-
445
- const linkedList = listLinkedPackages(customHome);
446
- const found = linkedList.find(
447
- (p) =>
448
- p.id === packageIdOrPath ||
449
- getPackageSlug(p.id) === packageIdOrPath ||
450
- p.path === resolve(packageIdOrPath)
451
- );
452
- if (found) {
453
- return found.path;
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
- return findProjectRoot(cwd);
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 (!pkg || !existsSync(pkg.path)) {
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(pkg.path);
513
- const playbooks = loadPlaybooks(pkg.path, config.playbooksDir);
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 '${pkg.id}' (${pkg.path})`);
610
+ throw new Error(`Playbook '${purePlaybookId}' not found in package '${targetPkgId}' (${targetRoot})`);
517
611
  }
518
612
 
519
613
  return {
520
- projectRoot: pkg.path,
521
- packageId: pkg.id,
614
+ projectRoot: targetRoot,
615
+ packageId: targetPkgId,
522
616
  playbookId: purePlaybookId,
523
617
  playbook: pb,
524
618
  };
@@ -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;
@@ -6,4 +6,5 @@ export * from "./env";
6
6
  export * from "./clock";
7
7
  export * from "./process";
8
8
  export * from "./events";
9
+ export * from "./module-loader";
9
10
 
@@ -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
+ }
@@ -200,23 +200,24 @@ export class DefaultProcessExecutor implements ProcessExecutor {
200
200
  }
201
201
 
202
202
  try {
203
- const checkRes = await this.exec(options.command, ["--version"], {
204
- timeoutMs: 1000,
205
- });
206
- const isReady = await options.probe(checkRes);
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
- await new Promise((r) => setTimeout(r, probeInterval));
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 {