@actiondock/core 2.20.0 → 2.21.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.
Files changed (67) hide show
  1. package/dist/errors.d.ts +11 -1
  2. package/dist/errors.js +23 -0
  3. package/dist/execution/service.js +36 -5
  4. package/dist/host/host.d.ts +11 -0
  5. package/dist/host/host.js +258 -39
  6. package/dist/host/types.d.ts +10 -1
  7. package/dist/index.d.ts +8 -1
  8. package/dist/index.js +5 -1
  9. package/dist/invocation/types.d.ts +31 -0
  10. package/dist/invocation/types.js +10 -1
  11. package/dist/package/index.d.ts +1 -1
  12. package/dist/package/runtime.js +18 -4
  13. package/dist/platform/node.d.ts +6 -0
  14. package/dist/platform/node.js +59 -16
  15. package/dist/platform/types.d.ts +11 -0
  16. package/dist/process/command-executor.d.ts +35 -0
  17. package/dist/process/command-executor.js +326 -0
  18. package/dist/process/index.d.ts +1 -0
  19. package/dist/process/index.js +1 -0
  20. package/dist/process/process-driver.d.ts +19 -6
  21. package/dist/process/process-driver.js +223 -54
  22. package/dist/process/run-executor.js +101 -47
  23. package/dist/project/dependencies.d.ts +92 -0
  24. package/dist/project/dependencies.js +543 -0
  25. package/dist/project/index.d.ts +3 -0
  26. package/dist/project/index.js +3 -0
  27. package/dist/project/init.js +2 -2
  28. package/dist/project/loader.js +15 -30
  29. package/dist/project/package-manager.d.ts +40 -0
  30. package/dist/project/package-manager.js +224 -0
  31. package/dist/project/preparation.d.ts +8 -0
  32. package/dist/project/preparation.js +130 -0
  33. package/dist/project/transactions.d.ts +5 -1
  34. package/dist/project/transactions.js +35 -75
  35. package/dist/runtime/context.d.ts +21 -2
  36. package/dist/runtime/context.js +42 -17
  37. package/dist/runtime/run-persistence.d.ts +1 -1
  38. package/dist/runtime/run-persistence.js +4 -3
  39. package/dist/security/config-cipher.d.ts +54 -0
  40. package/dist/security/config-cipher.js +190 -0
  41. package/dist/security/config-key.d.ts +35 -0
  42. package/dist/security/config-key.js +469 -0
  43. package/dist/server/routes/config.js +20 -1
  44. package/dist/service/factory.js +3 -0
  45. package/dist/service/types.d.ts +8 -0
  46. package/dist/storage/config-value.d.ts +33 -0
  47. package/dist/storage/config-value.js +102 -0
  48. package/dist/storage/file-lock.js +35 -21
  49. package/dist/storage/index.d.ts +24 -2
  50. package/dist/storage/index.js +65 -2
  51. package/dist/storage/migrations/index.d.ts +40 -0
  52. package/dist/storage/migrations/index.js +96 -0
  53. package/dist/storage/migrations/v2-to-v3.d.ts +16 -0
  54. package/dist/storage/migrations/v2-to-v3.js +116 -0
  55. package/dist/storage/sqlite-config.d.ts +62 -3
  56. package/dist/storage/sqlite-config.js +163 -9
  57. package/dist/storage/sqlite-driver.d.ts +5 -2
  58. package/dist/storage/sqlite-driver.js +24 -3
  59. package/dist/storage/sqlite-schema.d.ts +20 -2
  60. package/dist/storage/sqlite-schema.js +129 -132
  61. package/dist/storage/sqlite.d.ts +7 -2
  62. package/dist/storage/sqlite.js +64 -13
  63. package/dist/storage/types.d.ts +72 -4
  64. package/dist/storage/types.js +7 -2
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.js +1 -1
  67. package/package.json +2 -2
@@ -0,0 +1,92 @@
1
+ import type { ProcessDriver } from "../process/driver.js";
2
+ export interface EnsureProjectDependenciesOptions {
3
+ /** 是否允许自动安装(默认依据 process.env.ACTIONDOCK_AUTO_INSTALL !== "false") */
4
+ install?: boolean;
5
+ /** 取消信号 */
6
+ signal?: AbortSignal;
7
+ /** 进度输出回调,默认输出至 process.stderr */
8
+ onProgress?: (message: string) => void;
9
+ /** 自定义安装超时(毫秒),默认 5 分钟 (300,000 ms) */
10
+ timeoutMs?: number;
11
+ /** 可选注入的底层进程驱动(用于确定性测试) */
12
+ processDriver?: ProcessDriver;
13
+ }
14
+ export interface EnsureProjectDependenciesResult {
15
+ /** 是否触发并完成了安装 */
16
+ installed: boolean;
17
+ /** 实际安装根目录 */
18
+ installRoot?: string;
19
+ /** 使用的包管理器(若进行了安装) */
20
+ packageManager?: "npm" | "bun";
21
+ }
22
+ /**
23
+ * 解析并确定目标工程的实际安装根目录。
24
+ *
25
+ * 归属规则:
26
+ * - 位于 node_modules 目录内的包(包括经符号链接指向物理 node_modules 的情形),直接跳过(不在其内部再执行安装);
27
+ * - 无 package.json 的工程,直接跳过(保留无外部依赖合法工程的运行路径);
28
+ * - 工作区子包:校验上级 package.json.workspaces 归属,确属工作区子包时使用工作区根目录;
29
+ * - 仅接受实际 workspace 声明归属,不因祖先存在锁文件误认工作区根;
30
+ * - 校验 package.json 及 workspaces 声明类型,严禁吞没 JSON 解析与 I/O 异常;
31
+ * - 独立包:使用自身 package.json 所在目录作为安装根。
32
+ */
33
+ export declare function resolveInstallRoot(packageRoot: string): {
34
+ installRoot?: string;
35
+ isWorkspace: boolean;
36
+ skip: boolean;
37
+ reason?: string;
38
+ workspacePatterns?: string[];
39
+ };
40
+ /**
41
+ * 检查当前包声明的必需直接生产依赖是否已在 node_modules 中物化。
42
+ *
43
+ * 判定设计契约:
44
+ * - 仅检查 package.json 中的 dependencies 生产依赖;
45
+ * - 校验 dependencies 与 optionalDependencies 的记录对象结构,杜绝数组或无效数据;
46
+ * - 使用 Object.hasOwn 判定可选依赖排除,防止原型属性污染及误判;
47
+ * - optionalDependencies 缺失不视为必须安装;同名条目在 optionalDependencies 中声明时覆盖 dependencies;
48
+ * - 使用 node:module.findPackageJSON 仅探测依赖包元数据是否存在,不代表完整依赖加载健康;
49
+ * - 仅模块未找到(ERR_MODULE_NOT_FOUND)归类为 missing,读取权限与损坏配置等错误必须向外显式抛出;
50
+ * - 无需检查全部传递依赖。
51
+ */
52
+ export declare function checkDependenciesMaterialized(packageRoot: string): {
53
+ ready: boolean;
54
+ missing: string[];
55
+ requiredCount: number;
56
+ };
57
+ export interface PackageManagerPlan {
58
+ /** 决策选定的包管理器 */
59
+ manager: "npm" | "bun";
60
+ /** 安装根是否存在该管理器对应的锁文件 */
61
+ hasLock: boolean;
62
+ /** 该管理器在当前锁状态下应执行的安装参数 */
63
+ args: string[];
64
+ }
65
+ /**
66
+ * 解析安装根的锁文件、packageManager 声明与环境变量,产出确定性的包管理器安装决策。
67
+ *
68
+ * 决策契约(优先级从高到低):
69
+ * - 环境变量 ACTIONDOCK_INSTALLER 显式指定 > package.json packageManager 声明 > 锁文件归属 > 默认 npm;
70
+ * - 多锁文件冲突、声明与环境变量互相冲突、声明或环境变量为不支持的工具时,一律显式拒绝;
71
+ * - 检测到 pnpm 或 yarn 工程时引导手动准备,不自动安装;
72
+ * - 有锁走冻结安装(npm ci / bun --frozen-lockfile),无锁走普通生产安装。
73
+ *
74
+ * @param installRoot 安装根目录
75
+ * @param env 环境变量视图(默认 process.env,测试可注入确定性环境)
76
+ */
77
+ export declare function resolvePackageManagerPlan(installRoot: string, env?: Record<string, string | undefined>): PackageManagerPlan;
78
+ /**
79
+ * 确保项目所需生产依赖处于就绪状态。
80
+ *
81
+ * 核心设计原则:
82
+ * - 仅在所需必需生产依赖未物化、且安装根及工作区未探测到已有依赖目录时自动触发首次依赖安装;
83
+ * - 已有 node_modules 但依赖缺失或损坏时不自动重建,抛出 DEPENDENCIES_NOT_READY 并提供修复指引;
84
+ * - 明确禁止安装时(--no-install 或 ACTIONDOCK_AUTO_INSTALL=false),抛出 DEPENDENCIES_NOT_READY;
85
+ * - 存在未恢复的悬空事务时,抛出 PROJECT_RECOVERY_REQUIRED;
86
+ * - 取消信号生效时抛出 ACTION_CANCELLED;
87
+ * - 受工程排他锁保护,并发冲突时抛出 PROJECT_BUSY。
88
+ *
89
+ * @param packageRoot 目标包根目录
90
+ * @param options 执行选项
91
+ */
92
+ export declare function ensureProjectDependencies(requestedPackageRoot: string, options?: EnsureProjectDependenciesOptions): Promise<EnsureProjectDependenciesResult>;
@@ -0,0 +1,543 @@
1
+ import { existsSync, globSync, readFileSync, realpathSync } from "node:fs";
2
+ import { findPackageJSON } from "node:module";
3
+ import { dirname, join, matchesGlob, relative, resolve } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { ActionDockError, ACTION_CANCELLED, DEPENDENCIES_NOT_READY, DEPENDENCY_INSTALL_FAILED, PROJECT_RECOVERY_REQUIRED, } from "../errors.js";
6
+ import { runPackageManager, } from "./package-manager.js";
7
+ import { acquireProjectLock, hasPendingTransactions } from "./transactions.js";
8
+ function resolvePackageDirectory(packageRoot) {
9
+ const path = resolve(packageRoot);
10
+ try {
11
+ return realpathSync(path);
12
+ }
13
+ catch (error) {
14
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Cannot resolve project directory '${path}': ${error?.message || String(error)}`, { path, cause: error });
15
+ }
16
+ }
17
+ function readWorkspacePatterns(pkg, path) {
18
+ if (pkg.workspaces === undefined)
19
+ return [];
20
+ const patterns = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
21
+ if (!Array.isArray(patterns) || patterns.some((pattern) => typeof pattern !== "string" || pattern.length === 0)) {
22
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Invalid workspaces field in '${path}': expected an array of non-empty patterns or an object with a packages array.`, { path });
23
+ }
24
+ return patterns;
25
+ }
26
+ /**
27
+ * 校验指定相对路径是否匹配 npm workspaces 通配符规则。
28
+ *
29
+ * 契约:
30
+ * - 支持正向规则与负向排除规则(以 ! 开头);
31
+ * - 路径与规则统一规范化(去除前导 ./ 与末尾 /);
32
+ * - 正向规则匹配且未被负向排除时判定为匹配。
33
+ */
34
+ function matchesWorkspacePatterns(patterns, relPath) {
35
+ const normRel = relPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
36
+ let matched = false;
37
+ for (const raw of patterns) {
38
+ if (typeof raw !== "string")
39
+ continue;
40
+ let p = raw.replace(/\\/g, "/").trim();
41
+ if (p.startsWith("./"))
42
+ p = p.slice(2);
43
+ if (p.endsWith("/"))
44
+ p = p.slice(0, -1);
45
+ const isNeg = p.startsWith("!");
46
+ const cleanPattern = isNeg ? p.slice(1).replace(/^\.\//, "").replace(/\/+$/, "") : p;
47
+ if (isNeg) {
48
+ if (matchesGlob(normRel, cleanPattern)) {
49
+ return false;
50
+ }
51
+ }
52
+ else {
53
+ if (matchesGlob(normRel, cleanPattern)) {
54
+ matched = true;
55
+ }
56
+ }
57
+ }
58
+ return matched;
59
+ }
60
+ /**
61
+ * 检查目标工程链、安装根及声明工作区成员中是否存在已有的 node_modules 目录。
62
+ *
63
+ * 契约:
64
+ * - 若安装根或从 packageRoot 到 installRoot 路径链上已存在 node_modules,返回其完整路径;
65
+ * - 若为工作区,检索已声明工作区成员目录下是否已存在 node_modules,避免冷安装(如 npm ci)破坏性清除子包已有依赖;
66
+ * - 若不存在任何已有依赖目录,返回 null。
67
+ */
68
+ function findExistingNodeModules(installRoot, packageRoot, isWorkspace, patterns = []) {
69
+ const rootNm = join(installRoot, "node_modules");
70
+ if (existsSync(rootNm)) {
71
+ return rootNm;
72
+ }
73
+ const rootAbs = resolve(installRoot);
74
+ let curr = resolve(packageRoot);
75
+ while (curr.startsWith(rootAbs)) {
76
+ const nm = join(curr, "node_modules");
77
+ if (existsSync(nm)) {
78
+ return nm;
79
+ }
80
+ if (curr === rootAbs)
81
+ break;
82
+ const parent = dirname(curr);
83
+ if (parent === curr)
84
+ break;
85
+ curr = parent;
86
+ }
87
+ if (isWorkspace && patterns.length > 0) {
88
+ for (const raw of patterns) {
89
+ if (typeof raw !== "string")
90
+ continue;
91
+ let p = raw.replace(/\\/g, "/").trim();
92
+ if (p.startsWith("./"))
93
+ p = p.slice(2);
94
+ if (p.endsWith("/"))
95
+ p = p.slice(0, -1);
96
+ if (p.startsWith("!"))
97
+ continue;
98
+ try {
99
+ const globPattern = `${p}/node_modules`;
100
+ const matches = globSync(globPattern, { cwd: installRoot });
101
+ for (const match of matches) {
102
+ const matchPath = join(installRoot, match);
103
+ const memberRel = dirname(match).replace(/\\/g, "/");
104
+ if (matchesWorkspacePatterns(patterns, memberRel) && existsSync(matchPath)) {
105
+ return matchPath;
106
+ }
107
+ }
108
+ }
109
+ catch (error) {
110
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Cannot inspect workspace dependency directories in '${installRoot}': ${error?.message || String(error)}`, { installRoot, pattern: p, cause: error });
111
+ }
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ /**
117
+ * 解析并确定目标工程的实际安装根目录。
118
+ *
119
+ * 归属规则:
120
+ * - 位于 node_modules 目录内的包(包括经符号链接指向物理 node_modules 的情形),直接跳过(不在其内部再执行安装);
121
+ * - 无 package.json 的工程,直接跳过(保留无外部依赖合法工程的运行路径);
122
+ * - 工作区子包:校验上级 package.json.workspaces 归属,确属工作区子包时使用工作区根目录;
123
+ * - 仅接受实际 workspace 声明归属,不因祖先存在锁文件误认工作区根;
124
+ * - 校验 package.json 及 workspaces 声明类型,严禁吞没 JSON 解析与 I/O 异常;
125
+ * - 独立包:使用自身 package.json 所在目录作为安装根。
126
+ */
127
+ export function resolveInstallRoot(packageRoot) {
128
+ const requestedRoot = resolve(packageRoot);
129
+ const absRoot = resolvePackageDirectory(requestedRoot);
130
+ const isInsideNodeModules = (p) => {
131
+ const parts = p.replace(/\\/g, "/").split("/");
132
+ return parts.includes("node_modules");
133
+ };
134
+ if (isInsideNodeModules(requestedRoot) || isInsideNodeModules(absRoot)) {
135
+ return { isWorkspace: false, skip: true, reason: "Inside node_modules" };
136
+ }
137
+ const pkgJsonPath = join(absRoot, "package.json");
138
+ if (!existsSync(pkgJsonPath)) {
139
+ return { isWorkspace: false, skip: true, reason: "No package.json" };
140
+ }
141
+ let pkgJson;
142
+ try {
143
+ const raw = readFileSync(pkgJsonPath, "utf-8");
144
+ pkgJson = JSON.parse(raw);
145
+ }
146
+ catch (err) {
147
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Failed to read or parse package.json at '${pkgJsonPath}': ${err?.message || String(err)}`, { path: pkgJsonPath, cause: err });
148
+ }
149
+ if (!pkgJson || typeof pkgJson !== "object" || Array.isArray(pkgJson)) {
150
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Invalid package.json at '${pkgJsonPath}': root must be a JSON object.`, { path: pkgJsonPath });
151
+ }
152
+ const ownPatterns = readWorkspacePatterns(pkgJson, pkgJsonPath);
153
+ let current = dirname(absRoot);
154
+ while (true) {
155
+ const candidatePkgJson = join(current, "package.json");
156
+ if (existsSync(candidatePkgJson)) {
157
+ let candidateParsed;
158
+ try {
159
+ const raw = readFileSync(candidatePkgJson, "utf-8");
160
+ candidateParsed = JSON.parse(raw);
161
+ }
162
+ catch (err) {
163
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Failed to read or parse workspace package.json at '${candidatePkgJson}': ${err?.message || String(err)}`, { path: candidatePkgJson, cause: err });
164
+ }
165
+ if (!candidateParsed ||
166
+ typeof candidateParsed !== "object" ||
167
+ Array.isArray(candidateParsed)) {
168
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Invalid package.json at '${candidatePkgJson}': root must be a JSON object.`, { path: candidatePkgJson });
169
+ }
170
+ const patterns = readWorkspacePatterns(candidateParsed, candidatePkgJson);
171
+ if (patterns.length > 0 && matchesWorkspacePatterns(patterns, relative(current, absRoot))) {
172
+ return {
173
+ installRoot: current,
174
+ isWorkspace: true,
175
+ skip: false,
176
+ workspacePatterns: patterns,
177
+ };
178
+ }
179
+ }
180
+ const parent = dirname(current);
181
+ if (parent === current) {
182
+ break;
183
+ }
184
+ current = parent;
185
+ }
186
+ return { installRoot: absRoot, isWorkspace: ownPatterns.length > 0, skip: false, workspacePatterns: ownPatterns };
187
+ }
188
+ /**
189
+ * 检查当前包声明的必需直接生产依赖是否已在 node_modules 中物化。
190
+ *
191
+ * 判定设计契约:
192
+ * - 仅检查 package.json 中的 dependencies 生产依赖;
193
+ * - 校验 dependencies 与 optionalDependencies 的记录对象结构,杜绝数组或无效数据;
194
+ * - 使用 Object.hasOwn 判定可选依赖排除,防止原型属性污染及误判;
195
+ * - optionalDependencies 缺失不视为必须安装;同名条目在 optionalDependencies 中声明时覆盖 dependencies;
196
+ * - 使用 node:module.findPackageJSON 仅探测依赖包元数据是否存在,不代表完整依赖加载健康;
197
+ * - 仅模块未找到(ERR_MODULE_NOT_FOUND)归类为 missing,读取权限与损坏配置等错误必须向外显式抛出;
198
+ * - 无需检查全部传递依赖。
199
+ */
200
+ export function checkDependenciesMaterialized(packageRoot) {
201
+ const absRoot = resolvePackageDirectory(packageRoot);
202
+ const pkgJsonPath = join(absRoot, "package.json");
203
+ if (!existsSync(pkgJsonPath)) {
204
+ return { ready: true, missing: [], requiredCount: 0 };
205
+ }
206
+ let pkgJson;
207
+ try {
208
+ const raw = readFileSync(pkgJsonPath, "utf-8");
209
+ pkgJson = JSON.parse(raw);
210
+ }
211
+ catch (err) {
212
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Failed to read or parse package.json at '${pkgJsonPath}': ${err?.message || String(err)}`, { path: pkgJsonPath, cause: err });
213
+ }
214
+ if (!pkgJson || typeof pkgJson !== "object" || Array.isArray(pkgJson)) {
215
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Invalid package.json at '${pkgJsonPath}': root must be a JSON object.`, { path: pkgJsonPath });
216
+ }
217
+ if (pkgJson.dependencies !== undefined) {
218
+ if (typeof pkgJson.dependencies !== "object" ||
219
+ pkgJson.dependencies === null ||
220
+ Array.isArray(pkgJson.dependencies)) {
221
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Invalid 'dependencies' field in '${pkgJsonPath}': expected an object record.`, { path: pkgJsonPath });
222
+ }
223
+ }
224
+ if (pkgJson.optionalDependencies !== undefined) {
225
+ if (typeof pkgJson.optionalDependencies !== "object" ||
226
+ pkgJson.optionalDependencies === null ||
227
+ Array.isArray(pkgJson.optionalDependencies)) {
228
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Invalid 'optionalDependencies' field in '${pkgJsonPath}': expected an object record.`, { path: pkgJsonPath });
229
+ }
230
+ }
231
+ const dependencies = pkgJson.dependencies ?? {};
232
+ const optionalDependencies = pkgJson.optionalDependencies ?? {};
233
+ for (const [field, entries] of [["dependencies", dependencies], ["optionalDependencies", optionalDependencies]]) {
234
+ for (const [name, specifier] of Object.entries(entries)) {
235
+ if (typeof specifier !== "string" || specifier.length === 0) {
236
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Invalid '${field}.${name}' in '${pkgJsonPath}': dependency specifier must be a non-empty string.`, { path: pkgJsonPath, field, dependency: name });
237
+ }
238
+ }
239
+ }
240
+ const requiredDeps = Object.keys(dependencies).filter((dep) => !Object.hasOwn(optionalDependencies, dep));
241
+ if (requiredDeps.length === 0) {
242
+ return { ready: true, missing: [], requiredCount: 0 };
243
+ }
244
+ const missing = [];
245
+ const baseHref = pathToFileURL(pkgJsonPath).href;
246
+ for (const dep of requiredDeps) {
247
+ try {
248
+ const found = findPackageJSON(dep, baseHref);
249
+ if (!found) {
250
+ missing.push(dep);
251
+ }
252
+ }
253
+ catch (err) {
254
+ if (err?.code === "ERR_MODULE_NOT_FOUND" || err?.code === "MODULE_NOT_FOUND") {
255
+ missing.push(dep);
256
+ }
257
+ else {
258
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Failed to resolve dependency '${dep}' from '${pkgJsonPath}': ${err?.message || String(err)}`, { dependency: dep, path: pkgJsonPath, cause: err });
259
+ }
260
+ }
261
+ }
262
+ return {
263
+ ready: missing.length === 0,
264
+ missing,
265
+ requiredCount: requiredDeps.length,
266
+ };
267
+ }
268
+ /**
269
+ * 解析安装根的锁文件、packageManager 声明与环境变量,产出确定性的包管理器安装决策。
270
+ *
271
+ * 决策契约(优先级从高到低):
272
+ * - 环境变量 ACTIONDOCK_INSTALLER 显式指定 > package.json packageManager 声明 > 锁文件归属 > 默认 npm;
273
+ * - 多锁文件冲突、声明与环境变量互相冲突、声明或环境变量为不支持的工具时,一律显式拒绝;
274
+ * - 检测到 pnpm 或 yarn 工程时引导手动准备,不自动安装;
275
+ * - 有锁走冻结安装(npm ci / bun --frozen-lockfile),无锁走普通生产安装。
276
+ *
277
+ * @param installRoot 安装根目录
278
+ * @param env 环境变量视图(默认 process.env,测试可注入确定性环境)
279
+ */
280
+ export function resolvePackageManagerPlan(installRoot, env = process.env) {
281
+ const hasPnpmLock = existsSync(join(installRoot, "pnpm-lock.yaml"));
282
+ const hasYarnLock = existsSync(join(installRoot, "yarn.lock"));
283
+ const hasNpmLock = existsSync(join(installRoot, "package-lock.json")) ||
284
+ existsSync(join(installRoot, "npm-shrinkwrap.json"));
285
+ const hasBunLock = existsSync(join(installRoot, "bun.lock")) || existsSync(join(installRoot, "bun.lockb"));
286
+ const lockTypes = [];
287
+ if (hasNpmLock)
288
+ lockTypes.push("npm");
289
+ if (hasBunLock)
290
+ lockTypes.push("bun");
291
+ if (hasPnpmLock)
292
+ lockTypes.push("pnpm");
293
+ if (hasYarnLock)
294
+ lockTypes.push("yarn");
295
+ // 检测多锁文件冲突(即便设置 ACTIONDOCK_INSTALLER 也严禁忽略明确锁文件冲突)
296
+ if (lockTypes.length > 1) {
297
+ throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Conflicting lockfiles found in '${installRoot}' (${lockTypes.join(", ")} lockfiles exist). Please clean up conflicting lockfiles.`, { stage: "planner", installRoot, conflictingManagers: lockTypes });
298
+ }
299
+ // 识别 package.json.packageManager
300
+ let declaredPm;
301
+ const rootPkgJsonPath = join(installRoot, "package.json");
302
+ if (existsSync(rootPkgJsonPath)) {
303
+ try {
304
+ const rootPkg = JSON.parse(readFileSync(rootPkgJsonPath, "utf-8"));
305
+ if (typeof rootPkg?.packageManager === "string") {
306
+ const match = rootPkg.packageManager.trim().match(/^([a-z0-9_-]+)/i);
307
+ if (match) {
308
+ declaredPm = match[1].toLowerCase();
309
+ }
310
+ }
311
+ }
312
+ catch (error) {
313
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Cannot read package manager declaration at '${rootPkgJsonPath}': ${error?.message || String(error)}`, { path: rootPkgJsonPath, cause: error });
314
+ }
315
+ }
316
+ if (declaredPm === "pnpm" || (hasPnpmLock && lockTypes.length === 1)) {
317
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Detected pnpm project (${hasPnpmLock ? "pnpm-lock.yaml found" : `packageManager: '${declaredPm}'`}) in '${installRoot}'. Automatic installation only supports npm and bun. Please install dependencies manually using pnpm.`, { stage: "planner", installRoot, manager: "pnpm" });
318
+ }
319
+ if (declaredPm === "yarn" || (hasYarnLock && lockTypes.length === 1)) {
320
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Detected yarn project (${hasYarnLock ? "yarn.lock found" : `packageManager: '${declaredPm}'`}) in '${installRoot}'. Automatic installation only supports npm and bun. Please install dependencies manually using yarn.`, { stage: "planner", installRoot, manager: "yarn" });
321
+ }
322
+ if (declaredPm && declaredPm !== "npm" && declaredPm !== "bun") {
323
+ throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Unsupported packageManager '${declaredPm}' declared in '${rootPkgJsonPath}'. Automatic installation only supports npm and bun.`, { stage: "planner", installRoot, manager: declaredPm });
324
+ }
325
+ if (declaredPm && lockTypes.length === 1) {
326
+ const lockPm = lockTypes[0];
327
+ if (declaredPm !== lockPm) {
328
+ throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Declared packageManager '${declaredPm}' conflicts with existing ${lockPm} lockfile in '${installRoot}'.`, { stage: "planner", installRoot, declaredManager: declaredPm, lockManager: lockPm });
329
+ }
330
+ }
331
+ const envInstaller = env.ACTIONDOCK_INSTALLER?.trim();
332
+ if (envInstaller) {
333
+ if (envInstaller !== "npm" && envInstaller !== "bun") {
334
+ throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Unsupported ACTIONDOCK_INSTALLER '${envInstaller}'. Allowed values: npm, bun.`);
335
+ }
336
+ if (lockTypes.length === 1 && envInstaller !== lockTypes[0]) {
337
+ throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Explicit ACTIONDOCK_INSTALLER '${envInstaller}' conflicts with existing ${lockTypes[0]} lockfile in '${installRoot}'.`, { stage: "planner", installRoot, envInstaller, lockManager: lockTypes[0] });
338
+ }
339
+ if (declaredPm && envInstaller !== declaredPm) {
340
+ throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Explicit ACTIONDOCK_INSTALLER '${envInstaller}' conflicts with declared packageManager '${declaredPm}' in '${installRoot}'.`, { stage: "planner", installRoot, envInstaller, declaredManager: declaredPm });
341
+ }
342
+ }
343
+ let manager;
344
+ let hasLock = false;
345
+ if (envInstaller) {
346
+ manager = envInstaller;
347
+ hasLock = manager === "npm" ? hasNpmLock : hasBunLock;
348
+ }
349
+ else if (declaredPm) {
350
+ manager = declaredPm;
351
+ hasLock = manager === "npm" ? hasNpmLock : hasBunLock;
352
+ }
353
+ else if (hasBunLock) {
354
+ manager = "bun";
355
+ hasLock = true;
356
+ }
357
+ else if (hasNpmLock) {
358
+ manager = "npm";
359
+ hasLock = true;
360
+ }
361
+ else {
362
+ manager = "npm";
363
+ hasLock = false;
364
+ }
365
+ let args;
366
+ if (manager === "npm") {
367
+ if (hasLock) {
368
+ args = ["ci", "--omit=dev", "--ignore-scripts"];
369
+ }
370
+ else {
371
+ args = ["install", "--omit=dev", "--ignore-scripts"];
372
+ }
373
+ }
374
+ else {
375
+ if (hasLock) {
376
+ args = ["install", "--production", "--frozen-lockfile", "--ignore-scripts"];
377
+ }
378
+ else {
379
+ args = ["install", "--production", "--ignore-scripts"];
380
+ }
381
+ }
382
+ return { manager, hasLock, args };
383
+ }
384
+ /**
385
+ * 确保项目所需生产依赖处于就绪状态。
386
+ *
387
+ * 核心设计原则:
388
+ * - 仅在所需必需生产依赖未物化、且安装根及工作区未探测到已有依赖目录时自动触发首次依赖安装;
389
+ * - 已有 node_modules 但依赖缺失或损坏时不自动重建,抛出 DEPENDENCIES_NOT_READY 并提供修复指引;
390
+ * - 明确禁止安装时(--no-install 或 ACTIONDOCK_AUTO_INSTALL=false),抛出 DEPENDENCIES_NOT_READY;
391
+ * - 存在未恢复的悬空事务时,抛出 PROJECT_RECOVERY_REQUIRED;
392
+ * - 取消信号生效时抛出 ACTION_CANCELLED;
393
+ * - 受工程排他锁保护,并发冲突时抛出 PROJECT_BUSY。
394
+ *
395
+ * @param packageRoot 目标包根目录
396
+ * @param options 执行选项
397
+ */
398
+ export async function ensureProjectDependencies(requestedPackageRoot, options = {}) {
399
+ const rootResolution = resolveInstallRoot(requestedPackageRoot);
400
+ if (rootResolution.skip || !rootResolution.installRoot) {
401
+ return { installed: false };
402
+ }
403
+ const packageRoot = resolvePackageDirectory(requestedPackageRoot);
404
+ const installRoot = rootResolution.installRoot;
405
+ // 1. 快速检查:若必需生产依赖已物化,直接返回无需持锁
406
+ const initialStatus = checkDependenciesMaterialized(packageRoot);
407
+ if (initialStatus.ready) {
408
+ return { installed: false, installRoot };
409
+ }
410
+ // 2. 依赖未就绪场景判定
411
+ const existingNm = findExistingNodeModules(installRoot, packageRoot, rootResolution.isWorkspace, rootResolution.workspacePatterns);
412
+ const allowInstall = options.install !== false && process.env.ACTIONDOCK_AUTO_INSTALL !== "false";
413
+ if (existingNm) {
414
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Dependencies in '${packageRoot}' are not ready. Missing direct production dependencies: ${initialStatus.missing.join(", ")}. Existing node_modules found in '${existingNm}', but required packages are missing or damaged. Please run 'npm install' or 'bun install' manually in '${installRoot}'.`, {
415
+ stage: "existing-node-modules-check",
416
+ installRoot,
417
+ packageRoot,
418
+ existingNodeModules: existingNm,
419
+ missingDependencies: initialStatus.missing,
420
+ hint: `Run 'npm install' or 'bun install' manually in '${installRoot}'.`,
421
+ });
422
+ }
423
+ if (!allowInstall) {
424
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Dependencies in '${packageRoot}' are not ready. Missing direct production dependencies: ${initialStatus.missing.join(", ")}. Automatic installation is disabled (--no-install or ACTIONDOCK_AUTO_INSTALL=false). Please run 'npm install' or 'bun install' in '${installRoot}'.`, {
425
+ stage: "first-install-check",
426
+ installRoot,
427
+ packageRoot,
428
+ missingDependencies: initialStatus.missing,
429
+ hint: `Run 'npm install' or 'bun install' in '${installRoot}'.`,
430
+ });
431
+ }
432
+ // 3. 检查悬空事务与提前取消
433
+ if (options.signal?.aborted) {
434
+ throw new ActionDockError(ACTION_CANCELLED, "Dependency installation was cancelled before starting.");
435
+ }
436
+ if (hasPendingTransactions(installRoot)) {
437
+ throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Project at '${installRoot}' has pending transactions requiring recovery before installing dependencies.`);
438
+ }
439
+ // 4. 获取工程排他锁并在锁内复核状态
440
+ const releaseLock = acquireProjectLock(installRoot);
441
+ try {
442
+ if (options.signal?.aborted) {
443
+ throw new ActionDockError(ACTION_CANCELLED, "Dependency installation was cancelled before starting.");
444
+ }
445
+ if (hasPendingTransactions(installRoot)) {
446
+ throw new ActionDockError(PROJECT_RECOVERY_REQUIRED, `PROJECT_RECOVERY_REQUIRED: Project at '${installRoot}' has pending transactions requiring recovery before installing dependencies.`);
447
+ }
448
+ const lockedStatus = checkDependenciesMaterialized(packageRoot);
449
+ if (lockedStatus.ready) {
450
+ return { installed: false, installRoot };
451
+ }
452
+ const lockedExistingNm = findExistingNodeModules(installRoot, packageRoot, rootResolution.isWorkspace, rootResolution.workspacePatterns);
453
+ if (lockedExistingNm) {
454
+ throw new ActionDockError(DEPENDENCIES_NOT_READY, `Dependencies in '${packageRoot}' are not ready. Existing node_modules found in '${lockedExistingNm}' after lock acquisition, but required packages are missing. Run 'npm install' or 'bun install' manually in '${installRoot}'.`, {
455
+ stage: "locked-node-modules-check",
456
+ installRoot,
457
+ packageRoot,
458
+ existingNodeModules: lockedExistingNm,
459
+ missingDependencies: lockedStatus.missing,
460
+ hint: `Run 'npm install' or 'bun install' manually in '${installRoot}'.`,
461
+ });
462
+ }
463
+ // 5. 包管理器决策与安装参数规划
464
+ const plan = resolvePackageManagerPlan(installRoot);
465
+ const pm = plan.manager;
466
+ const args = plan.args;
467
+ const reportProgress = (msg) => {
468
+ try {
469
+ if (options.onProgress) {
470
+ options.onProgress(msg);
471
+ }
472
+ else {
473
+ process.stderr.write(msg);
474
+ }
475
+ }
476
+ catch (error) {
477
+ throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Cannot report dependency installation progress: ${error?.message || String(error)}`, { stage: "progress", installRoot, manager: pm, cause: error });
478
+ }
479
+ };
480
+ reportProgress(`[actiondock] Installing dependencies in ${installRoot} (${pm} ${args.join(" ")})...\n`);
481
+ const timeoutMs = options.timeoutMs ?? 300_000;
482
+ const env = {
483
+ ...process.env,
484
+ npm_config_allow_scripts: "",
485
+ NPM_CONFIG_ALLOW_SCRIPTS: "",
486
+ npm_config_ignore_scripts: "true",
487
+ NPM_CONFIG_IGNORE_SCRIPTS: "true",
488
+ };
489
+ let procResult;
490
+ try {
491
+ procResult = await runPackageManager(pm, args, {
492
+ cwd: installRoot,
493
+ env,
494
+ timeoutMs,
495
+ signal: options.signal,
496
+ driver: options.processDriver,
497
+ onOutput: (_stream, text) => {
498
+ reportProgress(text);
499
+ },
500
+ });
501
+ }
502
+ catch (err) {
503
+ if (err?.code === "PROCESS_CANCELLED" || err?.name === "AbortError" || options.signal?.aborted) {
504
+ throw new ActionDockError(ACTION_CANCELLED, `Dependency installation in '${installRoot}' was cancelled by signal.`, { stage: "install", manager: pm, cwd: installRoot, cause: err });
505
+ }
506
+ if (err?.code === "PROCESS_TIMEOUT") {
507
+ throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Dependency installation timed out after ${timeoutMs}ms in '${installRoot}'.`, { stage: "install", manager: pm, cwd: installRoot, timeoutMs, cause: err });
508
+ }
509
+ if (err instanceof ActionDockError && err.code === DEPENDENCY_INSTALL_FAILED) {
510
+ throw err;
511
+ }
512
+ throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Dependency installation failed in '${installRoot}': ${err?.message || String(err)}`, { stage: "install", manager: pm, cwd: installRoot, cause: err });
513
+ }
514
+ if (procResult.status !== 0) {
515
+ if (procResult.status === 130 || procResult.signal === "SIGINT" || options.signal?.aborted) {
516
+ throw new ActionDockError(ACTION_CANCELLED, `Dependency installation in '${installRoot}' was cancelled by signal.`, { stage: "install", manager: pm, cwd: installRoot, status: procResult.status, signal: procResult.signal });
517
+ }
518
+ const errMsg = (procResult.stderr || procResult.stdout || `Exit code ${procResult.status}`).trim();
519
+ throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Dependency installation failed in '${installRoot}':\n${errMsg}`, {
520
+ stage: "install",
521
+ installRoot,
522
+ manager: pm,
523
+ status: procResult.status,
524
+ stderr: procResult.stderr,
525
+ stdout: procResult.stdout,
526
+ });
527
+ }
528
+ // 6. 安装后检验:必需直接生产依赖元数据已物化
529
+ const postStatus = checkDependenciesMaterialized(packageRoot);
530
+ if (!postStatus.ready) {
531
+ throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Dependencies in '${packageRoot}' are still not resolvable after installation: ${postStatus.missing.join(", ")}`, {
532
+ stage: "post-install-verify",
533
+ installRoot,
534
+ packageRoot,
535
+ missingDependencies: postStatus.missing,
536
+ });
537
+ }
538
+ return { installed: true, installRoot, packageManager: pm };
539
+ }
540
+ finally {
541
+ releaseLock();
542
+ }
543
+ }
@@ -1,5 +1,8 @@
1
1
  export { loadActionBindings, validateActionBindings, resolveActionBinding, hasRemoteActionBinding, validateCanonicalActionRef, validateBindingName, type ActionBindings, type ActionBindingTarget, } from "../invocation/bindings.js";
2
2
  export * from "./digest.js";
3
+ export * from "./dependencies.js";
4
+ export * from "./preparation.js";
5
+ export * from "./package-manager.js";
3
6
  export * from "./init.js";
4
7
  export * from "./loader.js";
5
8
  export * from "./lockfile.js";
@@ -1,5 +1,8 @@
1
1
  export { loadActionBindings, validateActionBindings, resolveActionBinding, hasRemoteActionBinding, validateCanonicalActionRef, validateBindingName, } from "../invocation/bindings.js";
2
2
  export * from "./digest.js";
3
+ export * from "./dependencies.js";
4
+ export * from "./preparation.js";
5
+ export * from "./package-manager.js";
3
6
  export * from "./init.js";
4
7
  export * from "./loader.js";
5
8
  export * from "./lockfile.js";
@@ -86,10 +86,10 @@ export function initProject(targetDir, options = {}) {
86
86
  node: ">=24.12.0",
87
87
  },
88
88
  dependencies: {
89
- "@actiondock/sdk": "^2.20.0",
89
+ "@actiondock/sdk": "^2.21.0",
90
90
  },
91
91
  devDependencies: {
92
- "@actiondock/testing": "^2.20.0",
92
+ "@actiondock/testing": "^2.21.0",
93
93
  "@types/node": "^22.13.0",
94
94
  "tsx": "^4.19.0",
95
95
  "typescript": "^5.7.0",