@actiondock/core 2.19.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.
- package/dist/client/config.d.ts +17 -14
- package/dist/client/config.js +112 -14
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/errors.d.ts +13 -1
- package/dist/errors.js +25 -0
- package/dist/execution/service.js +44 -10
- package/dist/execution/types.d.ts +3 -3
- package/dist/host/host.d.ts +20 -1
- package/dist/host/host.js +430 -66
- package/dist/host/types.d.ts +13 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +5 -1
- package/dist/input/config-override.d.ts +5 -0
- package/dist/input/config-override.js +23 -0
- package/dist/input/input-resolver.d.ts +24 -7
- package/dist/input/input-resolver.js +73 -5
- package/dist/invocation/bindings.d.ts +53 -0
- package/dist/invocation/bindings.js +248 -0
- package/dist/invocation/policy.d.ts +5 -1
- package/dist/invocation/policy.js +16 -9
- package/dist/invocation/remote.d.ts +40 -0
- package/dist/invocation/remote.js +489 -0
- package/dist/invocation/types.d.ts +33 -0
- package/dist/invocation/types.js +11 -1
- package/dist/package/index.d.ts +2 -2
- package/dist/package/runtime.js +48 -15
- package/dist/platform/node.d.ts +6 -0
- package/dist/platform/node.js +59 -16
- package/dist/platform/types.d.ts +59 -1
- package/dist/process/command-executor.d.ts +35 -0
- package/dist/process/command-executor.js +326 -0
- package/dist/process/index.d.ts +1 -0
- package/dist/process/index.js +1 -0
- package/dist/process/process-driver.d.ts +19 -6
- package/dist/process/process-driver.js +223 -54
- package/dist/process/run-executor.js +101 -47
- package/dist/project/config-declarations.d.ts +61 -0
- package/dist/project/config-declarations.js +297 -0
- package/dist/project/dependencies.d.ts +92 -0
- package/dist/project/dependencies.js +543 -0
- package/dist/project/index.d.ts +8 -2
- package/dist/project/index.js +8 -2
- package/dist/project/init.js +2 -2
- package/dist/project/loader.js +19 -30
- package/dist/project/manifest.js +10 -0
- package/dist/project/package-manager.d.ts +40 -0
- package/dist/project/package-manager.js +224 -0
- package/dist/project/preparation.d.ts +8 -0
- package/dist/project/preparation.js +130 -0
- package/dist/project/transactions.d.ts +5 -1
- package/dist/project/transactions.js +35 -75
- package/dist/project/types.d.ts +30 -0
- package/dist/runtime/context.d.ts +27 -5
- package/dist/runtime/context.js +98 -26
- package/dist/runtime/env.js +15 -1
- package/dist/runtime/run-persistence.d.ts +1 -1
- package/dist/runtime/run-persistence.js +4 -3
- package/dist/runtime/standalone-commands.d.ts +2 -2
- package/dist/runtime/standalone-commands.js +113 -47
- package/dist/runtime/standalone-help.d.ts +1 -0
- package/dist/runtime/standalone-help.js +15 -9
- package/dist/runtime/standalone.js +2 -2
- package/dist/security/config-cipher.d.ts +54 -0
- package/dist/security/config-cipher.js +190 -0
- package/dist/security/config-key.d.ts +35 -0
- package/dist/security/config-key.js +469 -0
- package/dist/server/policy.d.ts +24 -107
- package/dist/server/policy.js +134 -291
- package/dist/server/routes/actions.js +10 -8
- package/dist/server/routes/common.d.ts +5 -20
- package/dist/server/routes/common.js +22 -58
- package/dist/server/routes/config.js +84 -7
- package/dist/server/routes/doctor.d.ts +1 -0
- package/dist/server/routes/doctor.js +4 -3
- package/dist/server/routes/info.js +100 -13
- package/dist/server/routes/playbooks.js +1 -3
- package/dist/server/routes/runs.js +96 -18
- package/dist/server/server.js +25 -53
- package/dist/server/types.d.ts +6 -6
- package/dist/service/factory.js +4 -0
- package/dist/service/remote.js +14 -29
- package/dist/service/types.d.ts +28 -5
- package/dist/service/types.js +8 -0
- package/dist/storage/config-value.d.ts +33 -0
- package/dist/storage/config-value.js +102 -0
- package/dist/storage/file-lock.js +35 -21
- package/dist/storage/index.d.ts +24 -2
- package/dist/storage/index.js +65 -2
- package/dist/storage/mask.d.ts +33 -6
- package/dist/storage/mask.js +298 -16
- package/dist/storage/migrations/index.d.ts +40 -0
- package/dist/storage/migrations/index.js +96 -0
- package/dist/storage/migrations/v2-to-v3.d.ts +16 -0
- package/dist/storage/migrations/v2-to-v3.js +116 -0
- package/dist/storage/sqlite-config.d.ts +62 -3
- package/dist/storage/sqlite-config.js +163 -9
- package/dist/storage/sqlite-driver.d.ts +5 -2
- package/dist/storage/sqlite-driver.js +24 -3
- package/dist/storage/sqlite-schema.d.ts +20 -2
- package/dist/storage/sqlite-schema.js +129 -132
- package/dist/storage/sqlite.d.ts +7 -2
- package/dist/storage/sqlite.js +64 -13
- package/dist/storage/types.d.ts +72 -4
- package/dist/storage/types.js +7 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/project/loader.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
3
|
import { NodeModuleLoader, unwrapDefaultExport } from "../platform/module-loader.js";
|
|
5
4
|
import { ActionDockError, ACTION_LOAD_FAILED, describeActionLoadFailure, INVALID_ACTION_ID, INVALID_JSON, INVALID_PACKAGE_ID, INVALID_PLAYBOOK_ID, MANIFEST_LOAD_FAILED, PLAYBOOK_NOT_FOUND, PROJECT_CONFIG_NOT_FOUND, } from "../errors.js";
|
|
6
5
|
import { loadManifest, ACTION_ID_REGEX, PLAYBOOK_ID_REGEX } from "./manifest.js";
|
|
6
|
+
import { assertValidConfigDeclarations } from "./config-declarations.js";
|
|
7
|
+
import { runPackageManager } from "./package-manager.js";
|
|
7
8
|
/**
|
|
8
9
|
* 从指定目录开始向上逐级递归查找包含 `actiondock.json` 的项目根目录。
|
|
9
10
|
*
|
|
@@ -68,6 +69,9 @@ export function loadProjectConfig(projectRoot) {
|
|
|
68
69
|
if (parsed.playbooksDir) {
|
|
69
70
|
assertWithinProjectRoot(projectRoot, parsed.playbooksDir, "playbooksDir");
|
|
70
71
|
}
|
|
72
|
+
if (parsed.config !== undefined) {
|
|
73
|
+
assertValidConfigDeclarations(parsed.config);
|
|
74
|
+
}
|
|
71
75
|
return parsed;
|
|
72
76
|
}
|
|
73
77
|
catch (err) {
|
|
@@ -81,36 +85,21 @@ export const ALLOWED_INSTALLERS = new Set(["npm", "bun"]);
|
|
|
81
85
|
/**
|
|
82
86
|
* 异步探测包管理器可用性(通过执行 --version 并检查退出码)。
|
|
83
87
|
*
|
|
84
|
-
*
|
|
85
|
-
* -
|
|
86
|
-
* -
|
|
87
|
-
* -
|
|
88
|
+
* 运维规范:
|
|
89
|
+
* - 复用统一包管理器启动解析与驱动底座,以 shell: false 执行;
|
|
90
|
+
* - 短超时限制,避免探测阻滞事件循环;
|
|
91
|
+
* - 仅在未明确指定包管理器时进行回退探测,选择后的执行失败不盲目换工具。
|
|
88
92
|
*/
|
|
89
|
-
function probePackageManager(command) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
settled = true;
|
|
100
|
-
resolve(false);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
proc.on("close", (code) => {
|
|
104
|
-
if (!settled) {
|
|
105
|
-
settled = true;
|
|
106
|
-
resolve(code === 0);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
resolve(false);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
93
|
+
async function probePackageManager(command) {
|
|
94
|
+
try {
|
|
95
|
+
const result = await runPackageManager(command, ["--version"], {
|
|
96
|
+
timeoutMs: 3000,
|
|
97
|
+
});
|
|
98
|
+
return result.status === 0;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
114
103
|
}
|
|
115
104
|
/**
|
|
116
105
|
* 探测宿主系统中可用的包管理工具。
|
package/dist/project/manifest.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { assertValidConfigDeclarations, validateConfigDeclarations } from "./config-declarations.js";
|
|
3
4
|
import { isPathOutsideBoundary, PACKAGE_ID_REGEX } from "../utils/index.js";
|
|
4
5
|
import { ActionDockError, MANIFEST_CORRUPTED, MANIFEST_INVALID, MANIFEST_READ_FAILED, UNSUPPORTED_MANIFEST_SCHEMA, } from "../errors.js";
|
|
5
6
|
export const MANIFEST_FILE_NAME = "actiondock.json";
|
|
@@ -38,6 +39,9 @@ export function loadManifest(projectRoot) {
|
|
|
38
39
|
if (!parsed.id) {
|
|
39
40
|
parsed.id = basename(projectRoot);
|
|
40
41
|
}
|
|
42
|
+
if (parsed.config !== undefined) {
|
|
43
|
+
assertValidConfigDeclarations(parsed.config);
|
|
44
|
+
}
|
|
41
45
|
return parsed;
|
|
42
46
|
}
|
|
43
47
|
/**
|
|
@@ -65,6 +69,12 @@ export function validateManifest(manifest, options) {
|
|
|
65
69
|
errors.push(`Manifest 'id' is invalid: '${m.id}' (must match ${PACKAGE_ID_REGEX})`);
|
|
66
70
|
}
|
|
67
71
|
}
|
|
72
|
+
if (m.config !== undefined) {
|
|
73
|
+
const configErrors = validateConfigDeclarations(m.config);
|
|
74
|
+
for (const err of configErrors) {
|
|
75
|
+
errors.push(err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
68
78
|
if (m.actions !== undefined) {
|
|
69
79
|
if (!m.actions || typeof m.actions !== "object" || Array.isArray(m.actions)) {
|
|
70
80
|
errors.push("Manifest 'actions' must be an object");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type CommandExecutionResult, type ExecuteCommandOptions } from "../process/command-executor.js";
|
|
2
|
+
export type { CommandExecutionResult };
|
|
3
|
+
export interface ResolvePackageManagerOptions {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
env?: Record<string, string | undefined>;
|
|
6
|
+
platform?: NodeJS.Platform;
|
|
7
|
+
execPath?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PackageManagerInvocation {
|
|
10
|
+
executable: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface RunPackageManagerOptions extends ExecuteCommandOptions {
|
|
14
|
+
platform?: NodeJS.Platform;
|
|
15
|
+
execPath?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 解析包管理器在目标平台上的确定性启动规范。
|
|
19
|
+
*
|
|
20
|
+
* 核心设计契约:
|
|
21
|
+
* - 保持参数边界与数组结构(保留 argv 边界,路径中的空格、引号、&、% 与 ! 原样保留);
|
|
22
|
+
* - shell: false 语义,杜绝命令注入与 Node.js DEP0190 警告;
|
|
23
|
+
* - POSIX 平台保留正常 PATH 命令执行,兼容已有 fake npm 脚本;
|
|
24
|
+
* - Windows 平台查找遵循 PATH 目录顺序及 PATHEXT 后缀顺序(默认 .COM;.EXE;.BAT;.CMD);
|
|
25
|
+
* - PATH 所选工具为唯一事实源,显式传入 env 必须被尊重,绝不从 process.env 漏回其它 PATH;
|
|
26
|
+
* - Windows 平台原生程序(.exe, .com)直接保留该可执行文件与原数组 args,无需绕过原生版本管理器;
|
|
27
|
+
* - Windows 平台 Bun 仅支持原生程序,遇到 cmd/bat 包装脚本明确报错,无工具时严禁退回名称;
|
|
28
|
+
* - Windows 平台 npm.cmd/bat:优先其真实目录布局定位标准 npm-cli.js,由 execPath (Node) 无 shell 启动;
|
|
29
|
+
* - 仅支持标准可定位 CLI 布局,非标准 wrapper 显式报错,无有效 PATH 工具显式报错。
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolvePackageManagerInvocation(manager: "npm" | "bun", args: readonly string[], options?: ResolvePackageManagerOptions): PackageManagerInvocation;
|
|
32
|
+
/**
|
|
33
|
+
* 统一执行包管理器命令。
|
|
34
|
+
*
|
|
35
|
+
* 运维规范:
|
|
36
|
+
* - 结合 resolvePackageManagerInvocation 完成无 shell 真实命令定位;
|
|
37
|
+
* - 通过统一 ProcessDriver 底座执行命令,保留取消、超时、输出流监听与进程树收敛;
|
|
38
|
+
* - 内部可信工具执行保持标准非阻塞异步。
|
|
39
|
+
*/
|
|
40
|
+
export declare function runPackageManager(manager: "npm" | "bun", args: readonly string[], options?: RunPackageManagerOptions): Promise<CommandExecutionResult>;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { ActionDockError, DEPENDENCY_INSTALL_FAILED, } from "../errors.js";
|
|
4
|
+
import { executeCommand, } from "../process/command-executor.js";
|
|
5
|
+
function isFile(filePath) {
|
|
6
|
+
try {
|
|
7
|
+
return statSync(filePath).isFile();
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Failed to inspect file at '${filePath}': ${error?.message ?? String(error)}`, { stage: "resolve", path: filePath, cause: error });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 规范化 Windows 环境变量字典。
|
|
18
|
+
*
|
|
19
|
+
* 核心契约:
|
|
20
|
+
* - 保持与 Node.js Windows child_process 相同的字典序排序与大小写去重语义;
|
|
21
|
+
* - 先由字典序首位键决定该大小写分组的最终获胜键;
|
|
22
|
+
* - 若获胜键的值为 undefined,则该变量视为未设置(同组后续键不可顶替);
|
|
23
|
+
* - 仅保留值非 undefined 的有效键值对,杜绝后续执行器剔除 undefined 时残留失效同名键。
|
|
24
|
+
*/
|
|
25
|
+
function normalizeWindowsEnvironment(env) {
|
|
26
|
+
const keys = [];
|
|
27
|
+
for (const key in env) {
|
|
28
|
+
keys.push(key);
|
|
29
|
+
}
|
|
30
|
+
keys.sort();
|
|
31
|
+
const seenUpper = new Set();
|
|
32
|
+
const normalized = {};
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const upper = key.toUpperCase();
|
|
35
|
+
if (seenUpper.has(upper)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
seenUpper.add(upper);
|
|
39
|
+
const value = env[key];
|
|
40
|
+
if (value !== undefined) {
|
|
41
|
+
normalized[key] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
function getNormalizedEnvValue(env, targetKey) {
|
|
47
|
+
const targetUpper = targetKey.toUpperCase();
|
|
48
|
+
for (const [key, value] of Object.entries(env)) {
|
|
49
|
+
if (key.toUpperCase() === targetUpper) {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function resolvePathExt(rawPathExt) {
|
|
56
|
+
if (!rawPathExt || rawPathExt.trim() === "") {
|
|
57
|
+
return [".COM", ".EXE", ".BAT", ".CMD"];
|
|
58
|
+
}
|
|
59
|
+
const list = rawPathExt
|
|
60
|
+
.split(";")
|
|
61
|
+
.map((e) => e.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((e) => (e.startsWith(".") ? e : `.${e}`));
|
|
64
|
+
return list.length > 0 ? list : [".COM", ".EXE", ".BAT", ".CMD"];
|
|
65
|
+
}
|
|
66
|
+
function findMatchingExecutable(dir, manager, ext) {
|
|
67
|
+
// Windows 文件系统大小写不敏感:优先尝试小写拼写,使返回路径与磁盘上常见的小写文件名一致;
|
|
68
|
+
// 再回退 PATHEXT 原样与全大写拼写,仅影响返回路径的外现大小写,不改变命中顺序。
|
|
69
|
+
const lowerCandidate = join(dir, `${manager}${ext.toLowerCase()}`);
|
|
70
|
+
if (isFile(lowerCandidate)) {
|
|
71
|
+
return lowerCandidate;
|
|
72
|
+
}
|
|
73
|
+
const directCandidate = join(dir, `${manager}${ext}`);
|
|
74
|
+
if (directCandidate !== lowerCandidate && isFile(directCandidate)) {
|
|
75
|
+
return directCandidate;
|
|
76
|
+
}
|
|
77
|
+
const upperCandidate = join(dir, `${manager}${ext.toUpperCase()}`);
|
|
78
|
+
if (upperCandidate !== directCandidate &&
|
|
79
|
+
upperCandidate !== lowerCandidate &&
|
|
80
|
+
isFile(upperCandidate)) {
|
|
81
|
+
return upperCandidate;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function findNpmCliInDirectory(targetDir) {
|
|
86
|
+
const candidates = [
|
|
87
|
+
join(targetDir, "node_modules", "npm", "bin", "npm-cli.js"),
|
|
88
|
+
join(targetDir, "..", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
89
|
+
join(targetDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
90
|
+
join(targetDir, "..", "npm", "bin", "npm-cli.js"),
|
|
91
|
+
join(targetDir, "npm", "bin", "npm-cli.js"),
|
|
92
|
+
];
|
|
93
|
+
for (const candidate of candidates) {
|
|
94
|
+
if (isFile(candidate)) {
|
|
95
|
+
return candidate;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 解析包管理器在目标平台上的确定性启动规范。
|
|
102
|
+
*
|
|
103
|
+
* 核心设计契约:
|
|
104
|
+
* - 保持参数边界与数组结构(保留 argv 边界,路径中的空格、引号、&、% 与 ! 原样保留);
|
|
105
|
+
* - shell: false 语义,杜绝命令注入与 Node.js DEP0190 警告;
|
|
106
|
+
* - POSIX 平台保留正常 PATH 命令执行,兼容已有 fake npm 脚本;
|
|
107
|
+
* - Windows 平台查找遵循 PATH 目录顺序及 PATHEXT 后缀顺序(默认 .COM;.EXE;.BAT;.CMD);
|
|
108
|
+
* - PATH 所选工具为唯一事实源,显式传入 env 必须被尊重,绝不从 process.env 漏回其它 PATH;
|
|
109
|
+
* - Windows 平台原生程序(.exe, .com)直接保留该可执行文件与原数组 args,无需绕过原生版本管理器;
|
|
110
|
+
* - Windows 平台 Bun 仅支持原生程序,遇到 cmd/bat 包装脚本明确报错,无工具时严禁退回名称;
|
|
111
|
+
* - Windows 平台 npm.cmd/bat:优先其真实目录布局定位标准 npm-cli.js,由 execPath (Node) 无 shell 启动;
|
|
112
|
+
* - 仅支持标准可定位 CLI 布局,非标准 wrapper 显式报错,无有效 PATH 工具显式报错。
|
|
113
|
+
*/
|
|
114
|
+
export function resolvePackageManagerInvocation(manager, args, options) {
|
|
115
|
+
const platform = options?.platform ?? process.platform;
|
|
116
|
+
const execPath = options?.execPath ?? process.execPath;
|
|
117
|
+
if (platform !== "win32") {
|
|
118
|
+
return {
|
|
119
|
+
executable: manager,
|
|
120
|
+
args: [...args],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const envSource = options?.env !== undefined ? options.env : process.env;
|
|
124
|
+
const effectiveEnv = normalizeWindowsEnvironment(envSource);
|
|
125
|
+
const rawPath = getNormalizedEnvValue(effectiveEnv, "PATH") ?? "";
|
|
126
|
+
const rawPathExt = getNormalizedEnvValue(effectiveEnv, "PATHEXT");
|
|
127
|
+
const pathExtList = resolvePathExt(rawPathExt);
|
|
128
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
129
|
+
const pathDirs = [];
|
|
130
|
+
for (const rawEntry of rawPath.split(";")) {
|
|
131
|
+
let entry = rawEntry.trim();
|
|
132
|
+
if (!entry)
|
|
133
|
+
continue;
|
|
134
|
+
if (entry.startsWith('"') && entry.endsWith('"') && entry.length >= 2) {
|
|
135
|
+
entry = entry.slice(1, -1).trim();
|
|
136
|
+
}
|
|
137
|
+
if (!entry)
|
|
138
|
+
continue;
|
|
139
|
+
pathDirs.push(isAbsolute(entry) ? entry : resolve(cwd, entry));
|
|
140
|
+
}
|
|
141
|
+
let selectedPath = null;
|
|
142
|
+
for (const dir of pathDirs) {
|
|
143
|
+
for (const ext of pathExtList) {
|
|
144
|
+
const match = findMatchingExecutable(dir, manager, ext);
|
|
145
|
+
if (match) {
|
|
146
|
+
selectedPath = match;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (selectedPath) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!selectedPath) {
|
|
155
|
+
throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Cannot locate ${manager} on Windows: '${manager}' was not found in PATH (${rawPath || "empty"}).`, { stage: "resolve", manager, path: rawPath });
|
|
156
|
+
}
|
|
157
|
+
const selectedExt = extname(selectedPath).toLowerCase();
|
|
158
|
+
const isNative = selectedExt === ".exe" || selectedExt === ".com";
|
|
159
|
+
if (isNative) {
|
|
160
|
+
return {
|
|
161
|
+
executable: selectedPath,
|
|
162
|
+
args: [...args],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (manager === "bun") {
|
|
166
|
+
throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Unsupported bun wrapper at '${selectedPath}': only native executable (.exe, .com) is supported on Windows. Non-standard wrappers cannot be executed without shell.`, { stage: "resolve", manager: "bun", path: selectedPath });
|
|
167
|
+
}
|
|
168
|
+
// Windows npm 非原生分支仅支持 .cmd 与 .bat
|
|
169
|
+
if (selectedExt !== ".cmd" && selectedExt !== ".bat") {
|
|
170
|
+
throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Unsupported npm wrapper extension '${selectedExt}' at '${selectedPath}': only native binaries (.exe, .com) or standard Windows batch scripts (.cmd, .bat) are supported.`, { stage: "resolve", manager: "npm", path: selectedPath });
|
|
171
|
+
}
|
|
172
|
+
// Windows npm .cmd / .bat wrapper 解析
|
|
173
|
+
let realWrapperPath;
|
|
174
|
+
try {
|
|
175
|
+
realWrapperPath = realpathSync(selectedPath);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Failed to resolve realpath for npm wrapper at '${selectedPath}': ${error?.message ?? String(error)}`, { stage: "resolve", manager: "npm", path: selectedPath, cause: error });
|
|
179
|
+
}
|
|
180
|
+
let foundCli = null;
|
|
181
|
+
if (basename(realWrapperPath).toLowerCase() === "npm-cli.js" && isFile(realWrapperPath)) {
|
|
182
|
+
foundCli = realWrapperPath;
|
|
183
|
+
}
|
|
184
|
+
if (!foundCli) {
|
|
185
|
+
const realDir = dirname(realWrapperPath);
|
|
186
|
+
foundCli = findNpmCliInDirectory(realDir);
|
|
187
|
+
}
|
|
188
|
+
if (!foundCli) {
|
|
189
|
+
throw new ActionDockError(DEPENDENCY_INSTALL_FAILED, `Unsupported npm wrapper at '${selectedPath}': cannot locate npm-cli.js in standard installation layout. Non-standard npm wrappers cannot be executed without shell.`, { stage: "resolve", manager: "npm", path: selectedPath });
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
executable: execPath,
|
|
193
|
+
args: [foundCli, ...args],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 统一执行包管理器命令。
|
|
198
|
+
*
|
|
199
|
+
* 运维规范:
|
|
200
|
+
* - 结合 resolvePackageManagerInvocation 完成无 shell 真实命令定位;
|
|
201
|
+
* - 通过统一 ProcessDriver 底座执行命令,保留取消、超时、输出流监听与进程树收敛;
|
|
202
|
+
* - 内部可信工具执行保持标准非阻塞异步。
|
|
203
|
+
*/
|
|
204
|
+
export async function runPackageManager(manager, args, options) {
|
|
205
|
+
const platform = options?.platform ?? process.platform;
|
|
206
|
+
const normalizedEnv = platform === "win32" && options?.env !== undefined
|
|
207
|
+
? normalizeWindowsEnvironment(options.env)
|
|
208
|
+
: options?.env;
|
|
209
|
+
const invocation = resolvePackageManagerInvocation(manager, args, {
|
|
210
|
+
cwd: options?.cwd,
|
|
211
|
+
env: normalizedEnv,
|
|
212
|
+
platform: options?.platform,
|
|
213
|
+
execPath: options?.execPath,
|
|
214
|
+
});
|
|
215
|
+
return executeCommand(invocation.executable, invocation.args, {
|
|
216
|
+
cwd: options?.cwd,
|
|
217
|
+
env: normalizedEnv,
|
|
218
|
+
signal: options?.signal,
|
|
219
|
+
timeoutMs: options?.timeoutMs,
|
|
220
|
+
maxOutputBytes: options?.maxOutputBytes,
|
|
221
|
+
onOutput: options?.onOutput,
|
|
222
|
+
driver: options?.driver,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LocalPackagePreparer } from "../invocation/types.js";
|
|
2
|
+
import { type EnsureProjectDependenciesOptions } from "./dependencies.js";
|
|
3
|
+
/**
|
|
4
|
+
* 创建单个宿主使用的本地包准备器。
|
|
5
|
+
* 包实例独立缓存就绪状态;同一根调用的同一安装目录共享在途任务。
|
|
6
|
+
* 共享成功后,各包仍须通过自己的依赖检查;失败不重试、不缓存。
|
|
7
|
+
*/
|
|
8
|
+
export declare function createLocalPackagePreparer(options?: Omit<EnsureProjectDependenciesOptions, "signal">): LocalPackagePreparer;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { ActionDockError, ACTION_CANCELLED, DEPENDENCIES_NOT_READY } from "../errors.js";
|
|
4
|
+
import { ensureProjectDependencies, resolveInstallRoot, } from "./dependencies.js";
|
|
5
|
+
function cancellationError(reason) {
|
|
6
|
+
return new ActionDockError(ACTION_CANCELLED, reason instanceof Error ? reason.message : String(reason ?? "Package preparation was cancelled"), { cause: reason });
|
|
7
|
+
}
|
|
8
|
+
function isCancellation(error) {
|
|
9
|
+
return (error instanceof ActionDockError && error.code === ACTION_CANCELLED) ||
|
|
10
|
+
(error instanceof Error && error.name === "AbortError");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 创建单个宿主使用的本地包准备器。
|
|
14
|
+
* 包实例独立缓存就绪状态;同一根调用的同一安装目录共享在途任务。
|
|
15
|
+
* 共享成功后,各包仍须通过自己的依赖检查;失败不重试、不缓存。
|
|
16
|
+
*/
|
|
17
|
+
export function createLocalPackagePreparer(options = {}) {
|
|
18
|
+
const readyCache = new Set();
|
|
19
|
+
const canonicalRootCache = new Map();
|
|
20
|
+
const inFlightTasks = new Map();
|
|
21
|
+
function getPackageInstanceKey(target) {
|
|
22
|
+
let canonicalRoot = canonicalRootCache.get(target.packageRoot);
|
|
23
|
+
if (!canonicalRoot) {
|
|
24
|
+
const path = resolve(target.packageRoot);
|
|
25
|
+
try {
|
|
26
|
+
canonicalRoot = realpathSync(path);
|
|
27
|
+
canonicalRootCache.set(target.packageRoot, canonicalRoot);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new ActionDockError(DEPENDENCIES_NOT_READY, `Cannot resolve project directory '${path}': ${error?.message || String(error)}`, { path, cause: error });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return JSON.stringify([
|
|
34
|
+
target.identity.id,
|
|
35
|
+
target.identity.instanceId,
|
|
36
|
+
target.identity.generation,
|
|
37
|
+
canonicalRoot,
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
async function waitForTask(task, signal) {
|
|
41
|
+
let released = false;
|
|
42
|
+
const releaseWaiter = () => {
|
|
43
|
+
if (!released) {
|
|
44
|
+
released = true;
|
|
45
|
+
task.waiters--;
|
|
46
|
+
}
|
|
47
|
+
return task.waiters;
|
|
48
|
+
};
|
|
49
|
+
const stopIfUnused = async () => {
|
|
50
|
+
if (releaseWaiter() !== 0 || task.settled)
|
|
51
|
+
return;
|
|
52
|
+
task.controller.abort(signal.reason);
|
|
53
|
+
try {
|
|
54
|
+
await task.promise;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (!isCancellation(error))
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
if (signal.aborted) {
|
|
62
|
+
await stopIfUnused();
|
|
63
|
+
throw cancellationError(signal.reason);
|
|
64
|
+
}
|
|
65
|
+
let onAbort;
|
|
66
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
67
|
+
onAbort = () => {
|
|
68
|
+
void stopIfUnused().then(() => reject(cancellationError(signal.reason)), reject);
|
|
69
|
+
};
|
|
70
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
71
|
+
if (signal.aborted)
|
|
72
|
+
onAbort();
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
await Promise.race([task.promise, aborted]);
|
|
76
|
+
if (signal.aborted)
|
|
77
|
+
throw cancellationError(signal.reason);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
releaseWaiter();
|
|
81
|
+
signal.removeEventListener("abort", onAbort);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return async function prepareLocalPackage(target, context) {
|
|
85
|
+
if (context.signal.aborted)
|
|
86
|
+
throw cancellationError(context.signal.reason);
|
|
87
|
+
if (!target.packageRoot)
|
|
88
|
+
return;
|
|
89
|
+
const instanceKey = getPackageInstanceKey(target);
|
|
90
|
+
if (readyCache.has(instanceKey))
|
|
91
|
+
return;
|
|
92
|
+
const root = resolveInstallRoot(target.packageRoot);
|
|
93
|
+
if (root.skip || !root.installRoot) {
|
|
94
|
+
if (context.signal.aborted)
|
|
95
|
+
throw cancellationError(context.signal.reason);
|
|
96
|
+
readyCache.add(instanceKey);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const flightKey = JSON.stringify([context.rootRunId, root.installRoot]);
|
|
100
|
+
while (!readyCache.has(instanceKey)) {
|
|
101
|
+
if (context.signal.aborted)
|
|
102
|
+
throw cancellationError(context.signal.reason);
|
|
103
|
+
const existing = inFlightTasks.get(flightKey);
|
|
104
|
+
if (existing) {
|
|
105
|
+
existing.waiters++;
|
|
106
|
+
await waitForTask(existing, context.signal);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
let task;
|
|
111
|
+
// 先登记任务与等待者,再开始底层检查,兼容同步完成的测试驱动。
|
|
112
|
+
const promise = Promise.resolve().then(async () => {
|
|
113
|
+
try {
|
|
114
|
+
if (controller.signal.aborted)
|
|
115
|
+
throw cancellationError(controller.signal.reason);
|
|
116
|
+
await ensureProjectDependencies(target.packageRoot, { ...options, signal: controller.signal });
|
|
117
|
+
readyCache.add(instanceKey);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
task.settled = true;
|
|
121
|
+
if (inFlightTasks.get(flightKey) === task)
|
|
122
|
+
inFlightTasks.delete(flightKey);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
task = { controller, promise, waiters: 1, settled: false };
|
|
126
|
+
inFlightTasks.set(flightKey, task);
|
|
127
|
+
await waitForTask(task, context.signal);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -45,7 +45,9 @@ export declare function acquireProjectLock(projectRoot: string, options?: {
|
|
|
45
45
|
/**
|
|
46
46
|
* 依据恢复后的锁文件异步执行禁用安装脚本的冻结安装,使 node_modules 与声明重新一致。
|
|
47
47
|
*/
|
|
48
|
-
export declare function runFrozenInstall(projectRoot: string
|
|
48
|
+
export declare function runFrozenInstall(projectRoot: string, options?: {
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
} | AbortSignal): Promise<void>;
|
|
49
51
|
/**
|
|
50
52
|
* 检查项目是否存在未完成提交的悬空事务。
|
|
51
53
|
*/
|
|
@@ -60,4 +62,6 @@ export declare function beginTransaction(projectRoot: string, description?: stri
|
|
|
60
62
|
*/
|
|
61
63
|
export declare function recoverPendingTransactions(projectRoot: string, options?: {
|
|
62
64
|
frozenInstall?: boolean;
|
|
65
|
+
allowInstall?: boolean;
|
|
66
|
+
signal?: AbortSignal;
|
|
63
67
|
}): Promise<string[]>;
|