@rynx-ai/daemon 0.1.10 → 0.1.11-beta.2
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/bundled-plugins/.rynx-plugin/marketplace.json +10 -0
- package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +37 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.d.ts +48 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +54 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.d.ts +85 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-mirror.js +333 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.d.ts +109 -0
- package/bundled-plugins/plugins/lark/dist/cli-mirror/rollout-watcher.js +347 -0
- package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +95 -0
- package/bundled-plugins/plugins/lark/dist/host-adapters.js +504 -0
- package/bundled-plugins/plugins/lark/dist/index.d.ts +7 -0
- package/bundled-plugins/plugins/lark/dist/index.js +7 -0
- package/bundled-plugins/plugins/lark/dist/lark-card-stream.d.ts +363 -0
- package/bundled-plugins/plugins/lark/dist/lark-card-stream.js +1335 -0
- package/bundled-plugins/plugins/lark/dist/lark-config-store.d.ts +30 -0
- package/bundled-plugins/plugins/lark/dist/lark-config-store.js +135 -0
- package/bundled-plugins/plugins/lark/dist/lark-content.d.ts +30 -0
- package/bundled-plugins/plugins/lark/dist/lark-content.js +138 -0
- package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.d.ts +65 -0
- package/bundled-plugins/plugins/lark/dist/lark-conversation-directory.js +106 -0
- package/bundled-plugins/plugins/lark/dist/lark-diff-card.d.ts +25 -0
- package/bundled-plugins/plugins/lark/dist/lark-diff-card.js +58 -0
- package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.d.ts +16 -0
- package/bundled-plugins/plugins/lark/dist/lark-fork-transcript.js +141 -0
- package/bundled-plugins/plugins/lark/dist/lark-resume-card.d.ts +31 -0
- package/bundled-plugins/plugins/lark/dist/lark-resume-card.js +105 -0
- package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.d.ts +18 -0
- package/bundled-plugins/plugins/lark/dist/lark-runtime-state-store.js +95 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/client.d.ts +8 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/client.js +32 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/index.d.ts +1 -0
- package/bundled-plugins/plugins/lark/dist/lark-sdk/index.js +1 -0
- package/bundled-plugins/plugins/lark/dist/lark-sender.d.ts +114 -0
- package/bundled-plugins/plugins/lark/dist/lark-sender.js +323 -0
- package/bundled-plugins/plugins/lark/dist/lark-session-store.d.ts +13 -0
- package/bundled-plugins/plugins/lark/dist/lark-session-store.js +14 -0
- package/bundled-plugins/plugins/lark/dist/lark-settings-card.d.ts +55 -0
- package/bundled-plugins/plugins/lark/dist/lark-settings-card.js +158 -0
- package/bundled-plugins/plugins/lark/dist/lark-setup.d.ts +39 -0
- package/bundled-plugins/plugins/lark/dist/lark-setup.js +103 -0
- package/bundled-plugins/plugins/lark/dist/lark.d.ts +507 -0
- package/bundled-plugins/plugins/lark/dist/lark.js +1977 -0
- package/bundled-plugins/plugins/lark/dist/runtime.d.ts +13 -0
- package/bundled-plugins/plugins/lark/dist/runtime.js +131316 -0
- package/bundled-plugins/plugins/lark/dist/settings.d.ts +7 -0
- package/bundled-plugins/plugins/lark/dist/settings.js +126958 -0
- package/bundled-plugins/plugins/lark/package.json +57 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +1 -0
- package/bundled-plugins/plugins/lark/ui/assets/index-D9N-AkLX.js +201 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/bundled-plugins/plugins/lark/ui/index.html +13 -0
- package/dist/bundled-plugins.d.ts +15 -0
- package/dist/bundled-plugins.js +134 -0
- package/dist/cli.js +26 -208
- package/dist/control-deps.js +2 -79
- package/dist/daemon-server.d.ts +4 -1
- package/dist/daemon-server.js +167 -57
- package/dist/db.js +145 -124
- package/dist/index-daemon.js +25 -4
- package/dist/migrations/cleanup-legacy-sessions.d.ts +1 -1
- package/dist/migrations/cleanup-legacy-sessions.js +33 -15
- package/dist/official-plugins.d.ts +2 -0
- package/dist/official-plugins.js +2 -0
- package/dist/plugin-cli.d.ts +1 -3
- package/dist/plugin-cli.js +56 -45
- package/dist/plugin-host-leases.d.ts +4 -0
- package/dist/plugin-host-leases.js +7 -0
- package/dist/plugin-host-rpc.d.ts +42 -4
- package/dist/plugin-host-rpc.js +144 -34
- package/dist/plugin-installer.d.ts +24 -55
- package/dist/plugin-installer.js +202 -415
- package/dist/plugin-management-service.d.ts +30 -23
- package/dist/plugin-management-service.js +290 -105
- package/dist/plugin-marketplace.d.ts +35 -0
- package/dist/plugin-marketplace.js +299 -0
- package/dist/plugin-package.d.ts +22 -24
- package/dist/plugin-package.js +520 -571
- package/dist/plugin-skill-context.d.ts +4 -0
- package/dist/plugin-skill-context.js +35 -0
- package/dist/plugin-store.d.ts +36 -37
- package/dist/plugin-store.js +140 -92
- package/dist/plugin-supervisor.d.ts +45 -125
- package/dist/plugin-supervisor.js +516 -1053
- package/dist/pm2.js +15 -10
- package/dist/runtime-project-store.d.ts +9 -0
- package/dist/runtime-project-store.js +83 -0
- package/dist/session-fallback-workspace.d.ts +22 -0
- package/dist/session-fallback-workspace.js +120 -0
- package/dist/session-fork-store.d.ts +41 -0
- package/dist/session-fork-store.js +288 -0
- package/dist/session-launch-operations.d.ts +14 -2
- package/dist/session-launch-operations.js +34 -2
- package/dist/session-meta-store.d.ts +7 -9
- package/dist/session-meta-store.js +28 -49
- package/dist/setup.js +26 -14
- package/package.json +14 -11
- package/dist/channel-store.d.ts +0 -22
- package/dist/channel-store.js +0 -65
- package/dist/instance-store.d.ts +0 -38
- package/dist/instance-store.js +0 -85
- package/dist/plugin-install-restart.d.ts +0 -13
- package/dist/plugin-install-restart.js +0 -24
- package/dist/plugin-runtime-control.d.ts +0 -2
- package/dist/plugin-runtime-control.js +0 -37
- package/dist/registry.d.ts +0 -10
- package/dist/registry.js +0 -52
package/dist/plugin-package.js
CHANGED
|
@@ -1,277 +1,230 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Language-neutral plugin package materialization and static inspection.
|
|
3
3
|
*
|
|
4
|
-
* This module
|
|
5
|
-
* copied/downloaded with lifecycle scripts disabled, then only package.json and
|
|
6
|
-
* the referenced JSON manifest are parsed.
|
|
4
|
+
* This module never imports plugin code and never invokes a package manager.
|
|
7
5
|
*/
|
|
8
6
|
import { createHash } from "node:crypto";
|
|
9
7
|
import { spawn } from "node:child_process";
|
|
10
|
-
import { constants as fsConstants,
|
|
11
|
-
import { lstat as lstatAsync, open as openFile, opendir
|
|
8
|
+
import { constants as fsConstants, cpSync, existsSync, lstatSync, opendirSync, readFileSync, realpathSync, rmSync, statSync, } from "node:fs";
|
|
9
|
+
import { lstat as lstatAsync, open as openFile, opendir } from "node:fs/promises";
|
|
12
10
|
import { homedir } from "node:os";
|
|
13
|
-
import {
|
|
11
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
12
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { create as createTar } from "tar";
|
|
16
13
|
import { parsePluginManifest, } from "@rynx-ai/plugin-sdk";
|
|
17
14
|
const MAX_METADATA_BYTES = 1024 * 1024;
|
|
18
15
|
const PACKAGE_HASH_STREAM_BYTES = 256 * 1024;
|
|
19
16
|
const DEFAULT_COMMAND_ABORT_GRACE_MS = 2_000;
|
|
20
17
|
export const DEFAULT_PLUGIN_PACKAGE_INSPECTION_LIMITS = Object.freeze({
|
|
21
|
-
maxFileBytes:
|
|
22
|
-
maxTotalBytes:
|
|
23
|
-
maxEntries:
|
|
18
|
+
maxFileBytes: 1024 * 1024 * 1024,
|
|
19
|
+
maxTotalBytes: 1024 * 1024 * 1024,
|
|
20
|
+
maxEntries: 25_000,
|
|
24
21
|
maxDepth: 64,
|
|
25
22
|
});
|
|
26
|
-
/** Classify provenance without fetching or executing anything. */
|
|
27
23
|
export function detectPluginSource(spec) {
|
|
28
|
-
|
|
29
|
-
return "local";
|
|
30
|
-
if (/^(?:git\+|git:|git@|ssh:|github:|gitlab:|bitbucket:)/i.test(spec) ||
|
|
31
|
-
/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#.*)?$/.test(spec)) {
|
|
32
|
-
return "git";
|
|
33
|
-
}
|
|
34
|
-
if (/^https?:\/\//i.test(spec))
|
|
35
|
-
return spec.includes(".git") ? "git" : "url";
|
|
36
|
-
return "npm";
|
|
24
|
+
return looksLocal(spec) ? "local" : "git";
|
|
37
25
|
}
|
|
38
|
-
/** Stable provenance for local sources; remote specs are retained verbatim. */
|
|
39
26
|
export function normalizePluginSpec(spec, source) {
|
|
40
|
-
|
|
27
|
+
const parsed = parsePluginSourceSpec(spec, source);
|
|
28
|
+
return parsed.type === "local"
|
|
29
|
+
? `${parsed.normalizedLocation}${parsed.subdir === "." ? "" : `#path=${parsed.subdir}`}`
|
|
30
|
+
: `${parsed.normalizedLocation}${parsed.subdir === "." ? "" : `#path=${parsed.subdir}`}${parsed.requested.type === "git" && parsed.requested.ref
|
|
31
|
+
? `${parsed.subdir === "." ? "#" : "&"}ref=${encodeURIComponent(parsed.requested.ref)}`
|
|
32
|
+
: ""}`;
|
|
41
33
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* with runtime dependencies present, while lifecycle scripts remain disabled.
|
|
49
|
-
*/
|
|
50
|
-
export const materializePluginPackage = async ({ spec, source, destination, onProgress, signal, }) => {
|
|
34
|
+
/** CLI source syntax: local paths, Git URLs, github:owner/repo, or owner/repo. */
|
|
35
|
+
export function parsePluginSourceSpec(spec, forced) {
|
|
36
|
+
const trimmed = spec.trim();
|
|
37
|
+
if (!trimmed)
|
|
38
|
+
throw new Error("plugin source must not be empty");
|
|
39
|
+
const source = forced ?? detectPluginSource(trimmed);
|
|
51
40
|
if (source === "local") {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (isWithin(sourceReal, resolve(destination))) {
|
|
76
|
-
throw new Error("plugin staging directory must not be inside the local source");
|
|
77
|
-
}
|
|
78
|
-
if (!hasProductionDependencies(sourceReal)) {
|
|
79
|
-
copyPackageTree(sourceReal, destination, true);
|
|
80
|
-
onProgress(`copied self-contained local plugin package from ${sourceReal}`);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const workspaceRoot = findPnpmWorkspaceRoot(sourceReal);
|
|
84
|
-
if (workspaceRoot) {
|
|
85
|
-
const stagedWorkspace = await stageLocalWorkspaceClosure(sourceReal, workspaceRoot, join(resolve(destination, ".."), "workspace-packages"));
|
|
86
|
-
onProgress(`materializing local workspace plugin with ${stagedWorkspace.packageCount - 1} workspace dependency/dependencies`);
|
|
87
|
-
await materializeWithNpm({
|
|
88
|
-
spec: stagedWorkspace.pluginArchive,
|
|
89
|
-
prefix: join(resolve(destination, ".."), "local-prefix"),
|
|
90
|
-
destination,
|
|
91
|
-
onProgress,
|
|
92
|
-
signal,
|
|
93
|
-
label: "local workspace",
|
|
94
|
-
});
|
|
95
|
-
// The rewritten file: specs are a staging-only resolution aid. Keep the
|
|
96
|
-
// installed plugin's own package metadata identical to its source.
|
|
97
|
-
copyBoundedMetadataFile(join(sourceReal, "package.json"), join(destination, "package.json"), "package.json");
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const localArchiveRoot = join(resolve(destination, ".."), "local-package");
|
|
101
|
-
const localPackage = join(localArchiveRoot, "package");
|
|
102
|
-
rmSync(localArchiveRoot, { recursive: true, force: true });
|
|
103
|
-
copyPackageTree(sourceReal, localPackage, true);
|
|
104
|
-
const localArchive = join(localArchiveRoot, "package.tgz");
|
|
105
|
-
await createPackageArchive(localPackage, localArchive);
|
|
106
|
-
await materializeWithNpm({
|
|
107
|
-
spec: localArchive,
|
|
108
|
-
prefix: join(resolve(destination, ".."), "local-prefix"),
|
|
109
|
-
destination,
|
|
110
|
-
onProgress,
|
|
111
|
-
signal,
|
|
112
|
-
label: "local",
|
|
113
|
-
});
|
|
41
|
+
const { location, subdir } = splitSourceFragment(trimmed);
|
|
42
|
+
const localPath = toLocalDir(location);
|
|
43
|
+
const root = realpathSync(localPath);
|
|
44
|
+
return {
|
|
45
|
+
type: "local",
|
|
46
|
+
requested: { type: "local", path: root, ...(subdir === "." ? {} : { subdir }) },
|
|
47
|
+
normalizedLocation: root,
|
|
48
|
+
subdir,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const { url, ref, subdir } = parseGitSpec(trimmed);
|
|
52
|
+
const normalizedUrl = normalizeGitUrl(url);
|
|
53
|
+
return {
|
|
54
|
+
type: "git",
|
|
55
|
+
requested: {
|
|
56
|
+
type: "git",
|
|
57
|
+
url: normalizedUrl,
|
|
58
|
+
...(subdir === "." ? {} : { subdir }),
|
|
59
|
+
...(ref ? { ref } : {}),
|
|
60
|
+
},
|
|
61
|
+
normalizedLocation: normalizedUrl,
|
|
62
|
+
subdir,
|
|
63
|
+
};
|
|
114
64
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"--ignore-scripts",
|
|
121
|
-
"--pack-destination",
|
|
122
|
-
archiveRoot,
|
|
123
|
-
"--",
|
|
124
|
-
spec,
|
|
125
|
-
], onProgress, signal);
|
|
126
|
-
const archives = readdirSync(archiveRoot)
|
|
127
|
-
.filter((name) => name.endsWith(".tgz"))
|
|
128
|
-
.map((name) => join(archiveRoot, name));
|
|
129
|
-
if (archives.length !== 1 || !statSync(archives[0]).isFile()) {
|
|
130
|
-
throw new Error("package source did not produce exactly one npm archive");
|
|
131
|
-
}
|
|
132
|
-
return archives[0];
|
|
65
|
+
export function pluginSourceFingerprint(source) {
|
|
66
|
+
const input = source.type === "git"
|
|
67
|
+
? `git\0${source.normalizedLocation}\0${source.subdir}`
|
|
68
|
+
: `local\0${source.normalizedLocation}\0${source.subdir}`;
|
|
69
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 32);
|
|
133
70
|
}
|
|
134
|
-
async
|
|
135
|
-
|
|
71
|
+
export const materializePluginPackage = async ({ spec, source, destination, onProgress, signal, }) => {
|
|
72
|
+
signal?.throwIfAborted();
|
|
73
|
+
const normalized = parsePluginSourceSpec(spec, source);
|
|
136
74
|
rmSync(destination, { recursive: true, force: true });
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
75
|
+
if (normalized.type === "local") {
|
|
76
|
+
const selected = selectSubdirectory(normalized.normalizedLocation, normalized.subdir);
|
|
77
|
+
copyClosedSource(selected, destination, signal);
|
|
78
|
+
onProgress(`copied local plugin snapshot from ${selected}`);
|
|
79
|
+
return {
|
|
80
|
+
requestedSource: normalized.requested,
|
|
81
|
+
resolvedRevision: "local",
|
|
82
|
+
sourceFingerprint: pluginSourceFingerprint(normalized),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const checkout = join(resolve(destination, ".."), "git-checkout");
|
|
86
|
+
rmSync(checkout, { recursive: true, force: true });
|
|
87
|
+
const request = normalized.requested;
|
|
88
|
+
await runPluginPackageCommand("git", ["clone", "--no-checkout", "--no-recurse-submodules", "--filter=blob:none", "--", request.url, checkout], onProgress, { signal });
|
|
89
|
+
const revision = await capturePluginPackageCommand("git", [
|
|
90
|
+
"-C",
|
|
91
|
+
checkout,
|
|
92
|
+
"rev-parse",
|
|
93
|
+
"--verify",
|
|
94
|
+
`${request.ref ?? request.commit ?? "HEAD"}^{commit}`,
|
|
95
|
+
], { signal });
|
|
96
|
+
await validateGitPackageTree(checkout, revision, normalized.subdir, signal);
|
|
97
|
+
if (normalized.subdir !== ".") {
|
|
98
|
+
await runPluginPackageCommand("git", ["-C", checkout, "sparse-checkout", "init", "--no-cone"], onProgress, { signal });
|
|
99
|
+
await runPluginPackageCommand("git", ["-C", checkout, "sparse-checkout", "set", "--no-cone", "--", normalized.subdir], onProgress, { signal });
|
|
100
|
+
}
|
|
101
|
+
await runPluginPackageCommand("git", ["-C", checkout, "checkout", "--detach", revision], onProgress, { signal });
|
|
102
|
+
const selected = selectSubdirectory(checkout, normalized.subdir);
|
|
103
|
+
copyClosedSource(selected, destination, signal);
|
|
104
|
+
onProgress(`checked out plugin source at ${revision}`);
|
|
105
|
+
return {
|
|
106
|
+
requestedSource: normalized.requested,
|
|
107
|
+
resolvedRevision: revision,
|
|
108
|
+
sourceFingerprint: pluginSourceFingerprint(normalized),
|
|
109
|
+
};
|
|
110
|
+
};
|
|
141
111
|
export function parseGitSpec(spec) {
|
|
142
112
|
let value = spec.trim();
|
|
143
|
-
let shorthand = false;
|
|
144
113
|
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#.*)?$/.test(value)) {
|
|
145
114
|
value = `https://github.com/${value}`;
|
|
146
|
-
shorthand = true;
|
|
147
115
|
}
|
|
148
116
|
else if (value.startsWith("github:")) {
|
|
149
117
|
value = `https://github.com/${value.slice("github:".length)}`;
|
|
150
|
-
shorthand = true;
|
|
151
|
-
}
|
|
152
|
-
else if (value.startsWith("gitlab:")) {
|
|
153
|
-
value = `https://gitlab.com/${value.slice("gitlab:".length)}`;
|
|
154
|
-
shorthand = true;
|
|
155
|
-
}
|
|
156
|
-
else if (value.startsWith("bitbucket:")) {
|
|
157
|
-
value = `https://bitbucket.org/${value.slice("bitbucket:".length)}`;
|
|
158
|
-
shorthand = true;
|
|
159
118
|
}
|
|
160
119
|
if (value.startsWith("git+"))
|
|
161
120
|
value = value.slice(4);
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
const url = hash >= 0 ? value.slice(0, hash) : value;
|
|
165
|
-
if (!url || url.startsWith("-") || /[\r\n\0]/.test(url)) {
|
|
121
|
+
const { location, ref, subdir } = splitSourceFragment(value);
|
|
122
|
+
if (!location || location.startsWith("-") || /[\r\n\0]/.test(location)) {
|
|
166
123
|
throw new Error("git plugin source has an invalid URL");
|
|
167
124
|
}
|
|
168
125
|
if (ref && (ref.startsWith("-") || /[\r\n\0]/.test(ref))) {
|
|
169
126
|
throw new Error("git plugin source has an invalid ref");
|
|
170
127
|
}
|
|
171
|
-
|
|
172
|
-
return { url: normalizedUrl, ...(ref ? { ref } : {}) };
|
|
128
|
+
return { url: location, ...(ref ? { ref } : {}), subdir };
|
|
173
129
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
130
|
+
export function normalizeGitUrl(value) {
|
|
131
|
+
if (/^git@/i.test(value)) {
|
|
132
|
+
if (/[\r\n\0?#]/.test(value))
|
|
133
|
+
throw new Error("git source URL is invalid");
|
|
134
|
+
const match = /^git@([^/:]+):(.+)$/.exec(value);
|
|
135
|
+
if (!match || !match[2] || match[2].startsWith("/")) {
|
|
136
|
+
throw new Error("git source URL is invalid");
|
|
137
|
+
}
|
|
138
|
+
const repositoryPath = match[2].replace(/\/+$/, "").replace(/\.git$/, "");
|
|
139
|
+
if (!repositoryPath)
|
|
140
|
+
throw new Error("git source URL is invalid");
|
|
141
|
+
return `git@${match[1].toLowerCase()}:${repositoryPath}`;
|
|
142
|
+
}
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = new URL(value);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
throw new Error("git source URL is invalid");
|
|
149
|
+
}
|
|
150
|
+
if ((parsed.protocol !== "https:" &&
|
|
151
|
+
parsed.protocol !== "http:" &&
|
|
152
|
+
parsed.protocol !== "ssh:") ||
|
|
153
|
+
(parsed.protocol !== "ssh:" && parsed.username) ||
|
|
154
|
+
parsed.password ||
|
|
155
|
+
parsed.search ||
|
|
156
|
+
parsed.hash) {
|
|
157
|
+
throw new Error("git source URL must be credential-free HTTP(S) without query or fragment");
|
|
158
|
+
}
|
|
159
|
+
parsed.protocol = parsed.protocol.toLowerCase();
|
|
160
|
+
parsed.hostname = parsed.hostname.toLowerCase();
|
|
161
|
+
if ((parsed.protocol === "https:" && parsed.port === "443") ||
|
|
162
|
+
(parsed.protocol === "http:" && parsed.port === "80") ||
|
|
163
|
+
(parsed.protocol === "ssh:" && parsed.port === "22")) {
|
|
164
|
+
parsed.port = "";
|
|
165
|
+
}
|
|
166
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
167
|
+
return parsed.toString().replace(/\/$/, "");
|
|
204
168
|
}
|
|
205
|
-
/** Parse and validate static metadata, validate all executable entry paths, and hash the tree. */
|
|
206
169
|
export async function inspectPluginPackage(packageDir, options = {}) {
|
|
207
|
-
|
|
170
|
+
options.signal?.throwIfAborted();
|
|
208
171
|
const packageRoot = realpathSync(packageDir);
|
|
209
|
-
const integrity = await
|
|
210
|
-
|
|
211
|
-
const packageJsonPath = requireContainedFile(packageRoot, join(packageRoot, "package.json"), "package.json", "file");
|
|
212
|
-
const packageJson = readJsonObject(packageJsonPath, "package.json");
|
|
213
|
-
if (!isRecord(packageJson.rynx) || typeof packageJson.rynx.manifest !== "string") {
|
|
214
|
-
throw new Error("package.json must declare `rynx.manifest` as a static JSON path");
|
|
215
|
-
}
|
|
216
|
-
const manifestPath = resolvePackageEntry(packageRoot, packageJson.rynx.manifest, "rynx.manifest");
|
|
172
|
+
const integrity = await validatePluginPackageSnapshot(packageRoot, options);
|
|
173
|
+
const manifestPath = requireContainedPath(packageRoot, join(packageRoot, ".rynx-plugin", "plugin.json"), ".rynx-plugin/plugin.json", "file");
|
|
217
174
|
const manifest = parsePluginManifest(readJsonObject(manifestPath, "plugin manifest"));
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
...(version ? { version } : {}),
|
|
229
|
-
integrity,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
/** Run package/dependency build scripts only after explicit user approval. */
|
|
233
|
-
export async function rebuildPluginPackage(packageDir, onProgress, signal) {
|
|
234
|
-
await runNpm([
|
|
235
|
-
"rebuild",
|
|
236
|
-
"--prefix",
|
|
237
|
-
realpathSync(packageDir),
|
|
238
|
-
"--foreground-scripts",
|
|
239
|
-
"--no-audit",
|
|
240
|
-
"--no-fund",
|
|
241
|
-
], onProgress, signal);
|
|
242
|
-
}
|
|
243
|
-
function looksLocal(spec) {
|
|
244
|
-
if (spec.startsWith("file:"))
|
|
245
|
-
return true;
|
|
246
|
-
if (spec === "~" || spec.startsWith("~/"))
|
|
247
|
-
return true;
|
|
248
|
-
if (spec.startsWith("./") || spec.startsWith("../") || isAbsolute(spec))
|
|
249
|
-
return true;
|
|
250
|
-
try {
|
|
251
|
-
return existsSync(spec) && statSync(spec).isDirectory();
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
254
|
-
return false;
|
|
175
|
+
if (manifest.runtime) {
|
|
176
|
+
if ("entry" in manifest.runtime) {
|
|
177
|
+
resolvePackagePath(packageRoot, manifest.runtime.entry, "runtime.entry", "file");
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const command = resolvePackagePath(packageRoot, manifest.runtime.command, "runtime.command", "file");
|
|
181
|
+
if (process.platform !== "win32" && (statSync(command).mode & 0o111) === 0) {
|
|
182
|
+
throw new Error("runtime.command must be executable");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
255
185
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (spec.startsWith("file:")) {
|
|
260
|
-
try {
|
|
261
|
-
path = fileURLToPath(spec);
|
|
186
|
+
if (manifest.settings?.background) {
|
|
187
|
+
if ("entry" in manifest.settings.background) {
|
|
188
|
+
resolvePackagePath(packageRoot, manifest.settings.background.entry, "settings.background.entry", "file");
|
|
262
189
|
}
|
|
263
|
-
|
|
264
|
-
|
|
190
|
+
else {
|
|
191
|
+
const command = resolvePackagePath(packageRoot, manifest.settings.background.command, "settings.background.command", "file");
|
|
192
|
+
if (process.platform !== "win32" && (statSync(command).mode & 0o111) === 0) {
|
|
193
|
+
throw new Error("settings.background.command must be executable");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (manifest.commands) {
|
|
198
|
+
const command = resolvePackagePath(packageRoot, manifest.commands.command, "commands.command", "file");
|
|
199
|
+
if (process.platform !== "win32" && (statSync(command).mode & 0o111) === 0) {
|
|
200
|
+
throw new Error("commands.command must be executable");
|
|
265
201
|
}
|
|
266
202
|
}
|
|
267
|
-
|
|
268
|
-
|
|
203
|
+
if (manifest.skills) {
|
|
204
|
+
resolvePackagePath(packageRoot, manifest.skills, "skills", "directory");
|
|
269
205
|
}
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
206
|
+
if (manifest.settings) {
|
|
207
|
+
resolvePackagePath(packageRoot, manifest.settings.ui.entry, "settings.ui.entry", "file");
|
|
208
|
+
}
|
|
209
|
+
return { manifest, version: manifest.version, integrity };
|
|
210
|
+
}
|
|
211
|
+
/** Validate and hash a closed package/catalog snapshot without executing it. */
|
|
212
|
+
export async function validatePluginPackageSnapshot(packageDir, options = {}) {
|
|
213
|
+
options.signal?.throwIfAborted();
|
|
214
|
+
return await hashPackageTree(realpathSync(packageDir), normalizedInspectionLimits(options.limits), options.signal);
|
|
215
|
+
}
|
|
216
|
+
export function resolvePackagePath(packageRoot, entry, label, kind) {
|
|
217
|
+
if (!entry.startsWith("./") || entry.includes("\\") || entry.includes("\0")) {
|
|
218
|
+
throw new Error(`${label} must be a package-root-relative POSIX path starting with ./`);
|
|
219
|
+
}
|
|
220
|
+
return requireContainedPath(packageRoot, resolve(packageRoot, entry), label, kind);
|
|
221
|
+
}
|
|
222
|
+
function selectSubdirectory(root, subdir) {
|
|
223
|
+
const candidate = subdir === "." ? root : resolve(root, subdir);
|
|
224
|
+
return requireContainedPath(root, candidate, "plugin source subdirectory", "directory");
|
|
273
225
|
}
|
|
274
|
-
function
|
|
226
|
+
function copyClosedSource(source, destination, signal) {
|
|
227
|
+
assertClosedSourceTree(source, DEFAULT_PLUGIN_PACKAGE_INSPECTION_LIMITS, signal);
|
|
275
228
|
cpSync(source, destination, {
|
|
276
229
|
recursive: true,
|
|
277
230
|
errorOnExist: true,
|
|
@@ -284,132 +237,68 @@ function copyPackageTree(source, destination, excludeNodeModules) {
|
|
|
284
237
|
const segments = rel.split(sep);
|
|
285
238
|
if (segments.includes(".git"))
|
|
286
239
|
return false;
|
|
287
|
-
|
|
288
|
-
// materialization prefix. Plugin entries resolve tools by package path,
|
|
289
|
-
// so these non-runtime shims must not escape into the immutable closure.
|
|
290
|
-
if (segments.includes(".bin"))
|
|
291
|
-
return false;
|
|
292
|
-
return !excludeNodeModules || !segments.includes("node_modules");
|
|
240
|
+
return !(segments.includes("node_modules") && lstatSync(entry).isSymbolicLink());
|
|
293
241
|
},
|
|
294
242
|
});
|
|
295
243
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
current = parent;
|
|
244
|
+
/**
|
|
245
|
+
* Check the source before copying. `cp` may expand hardlinks into independent
|
|
246
|
+
* files, so validating only the copied snapshot would lose that provenance.
|
|
247
|
+
*/
|
|
248
|
+
function assertClosedSourceTree(root, limits, signal) {
|
|
249
|
+
const rootInfo = lstatSync(root);
|
|
250
|
+
if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) {
|
|
251
|
+
throw new Error("plugin source must be a regular directory");
|
|
305
252
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const packageJson = readJsonObject(join(sourceReal, "package.json"), "package.json");
|
|
315
|
-
const name = packageJson.name;
|
|
316
|
-
if (typeof name !== "string" || !isSafeNpmPackageName(name)) {
|
|
317
|
-
throw new Error(`local workspace package has an invalid name: ${String(name)}`);
|
|
253
|
+
let entries = 0;
|
|
254
|
+
let totalBytes = 0;
|
|
255
|
+
visit(root, "", 0);
|
|
256
|
+
function visit(current, rel, depth) {
|
|
257
|
+
signal?.throwIfAborted();
|
|
258
|
+
entries += 1;
|
|
259
|
+
if (entries > limits.maxEntries) {
|
|
260
|
+
throw new Error(`plugin package exceeds ${limits.maxEntries} entries`);
|
|
318
261
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (existing.source !== sourceReal) {
|
|
322
|
-
throw new Error(`local workspace contains duplicate package name: ${name}`);
|
|
323
|
-
}
|
|
324
|
-
return;
|
|
262
|
+
if (depth > limits.maxDepth) {
|
|
263
|
+
throw new Error(`plugin package exceeds maximum depth ${limits.maxDepth}`);
|
|
325
264
|
}
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
throw new Error(`local workspace dependency is not installed: ${name} -> ${dependencyName}`);
|
|
341
|
-
}
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
const dependencyReal = realpathSync(dependencyPath);
|
|
345
|
-
if (!isWithin(workspaceRoot, dependencyReal) ||
|
|
346
|
-
(workspaceNodeModules && isWithin(workspaceNodeModules, dependencyReal))) {
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
const dependencyPackage = readJsonObject(join(dependencyReal, "package.json"), `${dependencyName} package.json`);
|
|
350
|
-
if (dependencyPackage.name !== dependencyName) {
|
|
351
|
-
throw new Error(`local workspace dependency identity mismatch: ${dependencyName}`);
|
|
265
|
+
const info = lstatSync(current);
|
|
266
|
+
const normalized = rel.split(sep).join("/");
|
|
267
|
+
if (info.isSymbolicLink() && rel.split(sep).includes("node_modules"))
|
|
268
|
+
return;
|
|
269
|
+
if (info.isDirectory()) {
|
|
270
|
+
const directory = opendirSync(current);
|
|
271
|
+
try {
|
|
272
|
+
for (;;) {
|
|
273
|
+
const entry = directory.readSync();
|
|
274
|
+
if (!entry)
|
|
275
|
+
break;
|
|
276
|
+
if (entry.name === ".git")
|
|
277
|
+
continue;
|
|
278
|
+
visit(join(current, entry.name), join(rel, entry.name), depth + 1);
|
|
352
279
|
}
|
|
353
|
-
visit(dependencyReal);
|
|
354
280
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
rmSync(stagingRoot, { recursive: true, force: true });
|
|
358
|
-
mkdirSync(stagingRoot, { recursive: true, mode: 0o700 });
|
|
359
|
-
visit(pluginRoot);
|
|
360
|
-
for (const entry of packages.values())
|
|
361
|
-
copyPackageTree(entry.source, entry.staged, true);
|
|
362
|
-
for (const entry of packages.values()) {
|
|
363
|
-
const file = join(entry.staged, "package.json");
|
|
364
|
-
const packageJson = readJsonObject(file, "staged package.json");
|
|
365
|
-
for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
|
|
366
|
-
const dependencies = packageJson[field];
|
|
367
|
-
if (!isRecord(dependencies))
|
|
368
|
-
continue;
|
|
369
|
-
for (const dependencyName of Object.keys(dependencies)) {
|
|
370
|
-
const target = packages.get(dependencyName);
|
|
371
|
-
if (target)
|
|
372
|
-
dependencies[dependencyName] = `file:${target.archive}`;
|
|
281
|
+
finally {
|
|
282
|
+
directory.closeSync();
|
|
373
283
|
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
287
|
+
throw new Error(`plugin package accepts regular files and directories only: ${normalized}`);
|
|
288
|
+
}
|
|
289
|
+
if (info.nlink !== 1) {
|
|
290
|
+
throw new Error(`plugin package contains a hardlinked file: ${normalized}`);
|
|
291
|
+
}
|
|
292
|
+
if (info.size > limits.maxFileBytes) {
|
|
293
|
+
throw new Error(`plugin package file exceeds ${limits.maxFileBytes} bytes: ${normalized}`);
|
|
294
|
+
}
|
|
295
|
+
totalBytes += info.size;
|
|
296
|
+
if (totalBytes > limits.maxTotalBytes) {
|
|
297
|
+
throw new Error(`plugin package exceeds ${limits.maxTotalBytes} total bytes`);
|
|
374
298
|
}
|
|
375
|
-
writeFileSync(file, `${JSON.stringify(packageJson, null, 2)}\n`, { mode: 0o600 });
|
|
376
|
-
}
|
|
377
|
-
await Promise.all([...packages.values()].map((entry) => createPackageArchive(entry.staged, entry.archive)));
|
|
378
|
-
const pluginName = readJsonObject(join(pluginRoot, "package.json"), "package.json").name;
|
|
379
|
-
const plugin = typeof pluginName === "string" ? packages.get(pluginName) : undefined;
|
|
380
|
-
if (!plugin)
|
|
381
|
-
throw new Error("local workspace plugin was not staged");
|
|
382
|
-
return { pluginArchive: plugin.archive, packageCount: packages.size };
|
|
383
|
-
}
|
|
384
|
-
async function createPackageArchive(packageRoot, archive) {
|
|
385
|
-
mkdirSync(dirname(archive), { recursive: true, mode: 0o700 });
|
|
386
|
-
await createTar({
|
|
387
|
-
cwd: packageRoot,
|
|
388
|
-
file: archive,
|
|
389
|
-
gzip: true,
|
|
390
|
-
portable: true,
|
|
391
|
-
prefix: "package",
|
|
392
|
-
noMtime: true,
|
|
393
|
-
}, readdirSync(packageRoot));
|
|
394
|
-
}
|
|
395
|
-
function hasProductionDependencies(packageRoot) {
|
|
396
|
-
const packageJson = readJsonObject(join(packageRoot, "package.json"), "package.json");
|
|
397
|
-
return ["dependencies", "optionalDependencies", "peerDependencies"].some((key) => {
|
|
398
|
-
const value = packageJson[key];
|
|
399
|
-
return isRecord(value) && Object.keys(value).length > 0;
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
function resolvePackageEntry(packageRoot, entry, label) {
|
|
403
|
-
if (!entry.startsWith("./") || entry.includes("\\") || entry.includes("\0")) {
|
|
404
|
-
throw new Error(`${label} must be a package-root-relative POSIX path starting with ./`);
|
|
405
|
-
}
|
|
406
|
-
const segments = entry.slice(2).split("/");
|
|
407
|
-
if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
408
|
-
throw new Error(`${label} must not contain empty, . or .. segments`);
|
|
409
299
|
}
|
|
410
|
-
return requireContainedFile(packageRoot, resolve(packageRoot, entry), label, "file");
|
|
411
300
|
}
|
|
412
|
-
function
|
|
301
|
+
function requireContainedPath(root, candidate, label, kind) {
|
|
413
302
|
let rootReal;
|
|
414
303
|
let candidateReal;
|
|
415
304
|
try {
|
|
@@ -422,9 +311,9 @@ function requireContainedFile(root, candidate, label, kind) {
|
|
|
422
311
|
if (!isWithin(rootReal, candidateReal)) {
|
|
423
312
|
throw new Error(`${label} resolves outside the plugin package`);
|
|
424
313
|
}
|
|
425
|
-
const
|
|
426
|
-
if (kind === "file" ? !
|
|
427
|
-
throw new Error(`${label} must be a ${kind}`);
|
|
314
|
+
const info = lstatSync(candidateReal);
|
|
315
|
+
if (info.isSymbolicLink() || (kind === "file" ? !info.isFile() : !info.isDirectory())) {
|
|
316
|
+
throw new Error(`${label} must be a regular ${kind}`);
|
|
428
317
|
}
|
|
429
318
|
return candidateReal;
|
|
430
319
|
}
|
|
@@ -432,40 +321,78 @@ function isWithin(root, candidate) {
|
|
|
432
321
|
const rel = relative(root, candidate);
|
|
433
322
|
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
|
|
434
323
|
}
|
|
435
|
-
function
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
324
|
+
function splitSourceFragment(spec) {
|
|
325
|
+
const hash = spec.indexOf("#");
|
|
326
|
+
const location = hash < 0 ? spec : spec.slice(0, hash);
|
|
327
|
+
const fragment = hash < 0 ? "" : spec.slice(hash + 1);
|
|
328
|
+
let ref;
|
|
329
|
+
let subdir = ".";
|
|
330
|
+
if (fragment && !fragment.includes("=")) {
|
|
331
|
+
ref = decodeURIComponent(fragment);
|
|
332
|
+
}
|
|
333
|
+
else if (fragment) {
|
|
334
|
+
const params = new URLSearchParams(fragment);
|
|
335
|
+
const keys = [...params.keys()];
|
|
336
|
+
if (keys.some((key) => key !== "ref" && key !== "path")) {
|
|
337
|
+
throw new Error("plugin source fragment supports only ref and path");
|
|
338
|
+
}
|
|
339
|
+
ref = params.get("ref") || undefined;
|
|
340
|
+
subdir = normalizeSubdir(params.get("path") || ".");
|
|
447
341
|
}
|
|
342
|
+
return { location, ...(ref ? { ref } : {}), subdir };
|
|
343
|
+
}
|
|
344
|
+
function normalizeSubdir(value) {
|
|
345
|
+
if (value === "." || value === "")
|
|
346
|
+
return ".";
|
|
347
|
+
if (value.startsWith("/") ||
|
|
348
|
+
value.includes("\\") ||
|
|
349
|
+
value.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
|
|
350
|
+
throw new Error("plugin source subdirectory must be a normalized relative POSIX path");
|
|
351
|
+
}
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
function looksLocal(spec) {
|
|
355
|
+
const location = spec.split("#", 1)[0];
|
|
356
|
+
if (location.startsWith("file:") || location === "~" || location.startsWith("~/"))
|
|
357
|
+
return true;
|
|
358
|
+
if (location.startsWith("./") || location.startsWith("../") || isAbsolute(location))
|
|
359
|
+
return true;
|
|
448
360
|
try {
|
|
449
|
-
|
|
450
|
-
if (!isRecord(parsed))
|
|
451
|
-
throw new Error("must contain a JSON object");
|
|
452
|
-
return parsed;
|
|
361
|
+
return existsSync(location) && statSync(location).isDirectory();
|
|
453
362
|
}
|
|
454
|
-
catch
|
|
455
|
-
|
|
363
|
+
catch {
|
|
364
|
+
return false;
|
|
456
365
|
}
|
|
457
366
|
}
|
|
458
|
-
function
|
|
367
|
+
function toLocalDir(spec) {
|
|
368
|
+
let value = spec;
|
|
369
|
+
if (value.startsWith("file:")) {
|
|
370
|
+
try {
|
|
371
|
+
value = fileURLToPath(value);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
value = value.slice("file:".length);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (value === "~" || value.startsWith("~/"))
|
|
378
|
+
value = join(homedir(), value.slice(1));
|
|
379
|
+
return resolve(value);
|
|
380
|
+
}
|
|
381
|
+
function readJsonObject(path, label) {
|
|
459
382
|
try {
|
|
460
|
-
const
|
|
461
|
-
if (!
|
|
462
|
-
throw new Error("not a file");
|
|
463
|
-
if (
|
|
383
|
+
const info = lstatSync(path);
|
|
384
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
385
|
+
throw new Error("not a regular file");
|
|
386
|
+
if (info.size > MAX_METADATA_BYTES)
|
|
464
387
|
throw new Error("metadata is too large");
|
|
465
|
-
|
|
388
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
389
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
390
|
+
throw new Error("must contain a JSON object");
|
|
391
|
+
}
|
|
392
|
+
return parsed;
|
|
466
393
|
}
|
|
467
394
|
catch (error) {
|
|
468
|
-
throw new Error(`
|
|
395
|
+
throw new Error(`invalid ${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
469
396
|
}
|
|
470
397
|
}
|
|
471
398
|
async function hashPackageTree(root, limits, signal) {
|
|
@@ -476,99 +403,60 @@ async function hashPackageTree(root, limits, signal) {
|
|
|
476
403
|
await visit(root, "", 0);
|
|
477
404
|
return `sha256-${hash.digest("base64")}`;
|
|
478
405
|
async function visit(path, rel, depth) {
|
|
479
|
-
|
|
406
|
+
signal?.throwIfAborted();
|
|
480
407
|
entryCount += 1;
|
|
481
|
-
if (entryCount > limits.maxEntries)
|
|
408
|
+
if (entryCount > limits.maxEntries)
|
|
482
409
|
throw new Error(`plugin package exceeds ${limits.maxEntries} entries`);
|
|
483
|
-
|
|
484
|
-
if (depth > limits.maxDepth) {
|
|
410
|
+
if (depth > limits.maxDepth)
|
|
485
411
|
throw new Error(`plugin package exceeds maximum depth ${limits.maxDepth}`);
|
|
486
|
-
|
|
487
|
-
const stat = await lstatAsync(path);
|
|
412
|
+
const info = await lstatAsync(path);
|
|
488
413
|
const normalized = rel.split(sep).join("/");
|
|
489
|
-
if (
|
|
414
|
+
if (info.isDirectory()) {
|
|
490
415
|
hash.update(`d\0${normalized}\0`);
|
|
491
416
|
const names = [];
|
|
492
417
|
const directory = await opendir(path);
|
|
493
|
-
for await (const entry of directory)
|
|
494
|
-
throwIfInspectionAborted(signal);
|
|
418
|
+
for await (const entry of directory)
|
|
495
419
|
names.push(entry.name);
|
|
496
|
-
|
|
497
|
-
throw new Error(`plugin package exceeds ${limits.maxEntries} entries`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
for (const name of names.sort()) {
|
|
420
|
+
for (const name of names.sort())
|
|
501
421
|
await visit(join(path, name), join(rel, name), depth + 1);
|
|
502
|
-
}
|
|
503
422
|
return;
|
|
504
423
|
}
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
throw new Error(`plugin package file exceeds ${limits.maxFileBytes} bytes: ${normalized}`);
|
|
508
|
-
}
|
|
509
|
-
if (totalBytes + stat.size > limits.maxTotalBytes) {
|
|
510
|
-
throw new Error(`plugin package exceeds ${limits.maxTotalBytes} total bytes`);
|
|
511
|
-
}
|
|
512
|
-
totalBytes += stat.size;
|
|
513
|
-
hash.update(`f\0${normalized}\0${stat.size}\0`);
|
|
514
|
-
const noFollow = process.platform === "win32" ? 0 : fsConstants.O_NOFOLLOW;
|
|
515
|
-
const file = await openFile(path, fsConstants.O_RDONLY | noFollow);
|
|
516
|
-
try {
|
|
517
|
-
const before = await file.stat();
|
|
518
|
-
if (!before.isFile() || before.size !== stat.size) {
|
|
519
|
-
throw new Error(`plugin package file changed during inspection: ${normalized}`);
|
|
520
|
-
}
|
|
521
|
-
let streamedBytes = 0;
|
|
522
|
-
const stream = file.createReadStream({
|
|
523
|
-
autoClose: false,
|
|
524
|
-
highWaterMark: PACKAGE_HASH_STREAM_BYTES,
|
|
525
|
-
...(signal ? { signal } : {}),
|
|
526
|
-
});
|
|
527
|
-
for await (const chunk of stream) {
|
|
528
|
-
throwIfInspectionAborted(signal);
|
|
529
|
-
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
530
|
-
streamedBytes += bytes.byteLength;
|
|
531
|
-
if (streamedBytes > limits.maxFileBytes ||
|
|
532
|
-
totalBytes - stat.size + streamedBytes > limits.maxTotalBytes) {
|
|
533
|
-
throw new Error(`plugin package file changed during inspection: ${normalized}`);
|
|
534
|
-
}
|
|
535
|
-
hash.update(bytes);
|
|
536
|
-
}
|
|
537
|
-
const after = await file.stat();
|
|
538
|
-
if (streamedBytes !== before.size ||
|
|
539
|
-
after.size !== before.size ||
|
|
540
|
-
after.mtimeMs !== before.mtimeMs) {
|
|
541
|
-
throw new Error(`plugin package file changed during inspection: ${normalized}`);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
finally {
|
|
545
|
-
await file.close();
|
|
546
|
-
}
|
|
547
|
-
return;
|
|
424
|
+
if (!info.isFile()) {
|
|
425
|
+
throw new Error(`plugin package accepts regular files and directories only: ${normalized}`);
|
|
548
426
|
}
|
|
549
|
-
if (
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
427
|
+
if (info.nlink !== 1)
|
|
428
|
+
throw new Error(`plugin package contains a hardlinked file: ${normalized}`);
|
|
429
|
+
if (info.size > limits.maxFileBytes) {
|
|
430
|
+
throw new Error(`plugin package file exceeds ${limits.maxFileBytes} bytes: ${normalized}`);
|
|
431
|
+
}
|
|
432
|
+
totalBytes += info.size;
|
|
433
|
+
if (totalBytes > limits.maxTotalBytes) {
|
|
434
|
+
throw new Error(`plugin package exceeds ${limits.maxTotalBytes} total bytes`);
|
|
435
|
+
}
|
|
436
|
+
hash.update(`f\0${normalized}\0${info.mode & 0o777}\0${info.size}\0`);
|
|
437
|
+
const noFollow = process.platform === "win32" ? 0 : fsConstants.O_NOFOLLOW;
|
|
438
|
+
const file = await openFile(path, fsConstants.O_RDONLY | noFollow);
|
|
439
|
+
try {
|
|
440
|
+
let streamedBytes = 0;
|
|
441
|
+
for await (const chunk of file.createReadStream({
|
|
442
|
+
autoClose: false,
|
|
443
|
+
highWaterMark: PACKAGE_HASH_STREAM_BYTES,
|
|
444
|
+
...(signal ? { signal } : {}),
|
|
445
|
+
})) {
|
|
446
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
447
|
+
streamedBytes += bytes.byteLength;
|
|
448
|
+
hash.update(bytes);
|
|
559
449
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
450
|
+
if (streamedBytes !== info.size)
|
|
451
|
+
throw new Error(`plugin package changed during inspection: ${normalized}`);
|
|
452
|
+
}
|
|
453
|
+
finally {
|
|
454
|
+
await file.close();
|
|
563
455
|
}
|
|
564
|
-
throw new Error(`unsupported package entry: ${normalized}`);
|
|
565
456
|
}
|
|
566
457
|
}
|
|
567
458
|
function normalizedInspectionLimits(overrides) {
|
|
568
|
-
const limits = {
|
|
569
|
-
...DEFAULT_PLUGIN_PACKAGE_INSPECTION_LIMITS,
|
|
570
|
-
...overrides,
|
|
571
|
-
};
|
|
459
|
+
const limits = { ...DEFAULT_PLUGIN_PACKAGE_INSPECTION_LIMITS, ...overrides };
|
|
572
460
|
for (const [name, value] of Object.entries(limits)) {
|
|
573
461
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
574
462
|
throw new Error(`plugin package inspection ${name} must be a positive safe integer`);
|
|
@@ -576,157 +464,218 @@ function normalizedInspectionLimits(overrides) {
|
|
|
576
464
|
}
|
|
577
465
|
return limits;
|
|
578
466
|
}
|
|
579
|
-
function
|
|
580
|
-
if (signal?.aborted) {
|
|
581
|
-
throw new Error("plugin package inspection aborted", { cause: signal.reason });
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
function isSafeNpmPackageName(name) {
|
|
585
|
-
return /^(?:@[A-Za-z0-9][A-Za-z0-9._-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name);
|
|
586
|
-
}
|
|
587
|
-
function runNpm(args, onProgress, signal) {
|
|
588
|
-
return runPluginPackageCommand("npm", args, onProgress, { signal });
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Run one package tool without a shell and tear down its full POSIX process
|
|
592
|
-
* group on cancellation. Windows cannot signal process groups through Node,
|
|
593
|
-
* so it safely degrades to TERM→KILL on the direct child.
|
|
594
|
-
*/
|
|
595
|
-
export function runPluginPackageCommand(command, args, onProgress, options = {}) {
|
|
467
|
+
function validateGitPackageTree(checkout, revision, subdir, signal) {
|
|
596
468
|
return new Promise((resolveDone, reject) => {
|
|
597
|
-
const { signal, cwd } = options;
|
|
598
|
-
const abortGraceMs = options.abortGraceMs ?? DEFAULT_COMMAND_ABORT_GRACE_MS;
|
|
599
|
-
if (!Number.isSafeInteger(abortGraceMs) || abortGraceMs <= 0 || abortGraceMs > 60_000) {
|
|
600
|
-
reject(new Error("plugin command abort grace must be 1-60000ms"));
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
469
|
if (signal?.aborted) {
|
|
604
470
|
reject(new Error("plugin installation aborted"));
|
|
605
471
|
return;
|
|
606
472
|
}
|
|
607
|
-
const
|
|
608
|
-
const child = spawn(
|
|
609
|
-
|
|
473
|
+
const limits = DEFAULT_PLUGIN_PACKAGE_INSPECTION_LIMITS;
|
|
474
|
+
const child = spawn("git", [
|
|
475
|
+
"-C",
|
|
476
|
+
checkout,
|
|
477
|
+
"ls-tree",
|
|
478
|
+
"-rlz",
|
|
479
|
+
"-r",
|
|
480
|
+
revision,
|
|
481
|
+
"--",
|
|
482
|
+
subdir,
|
|
483
|
+
], {
|
|
610
484
|
stdio: ["ignore", "pipe", "pipe"],
|
|
611
|
-
detached:
|
|
485
|
+
detached: process.platform !== "win32",
|
|
612
486
|
});
|
|
613
|
-
let
|
|
614
|
-
let
|
|
615
|
-
let
|
|
616
|
-
let
|
|
617
|
-
let
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
if (settled)
|
|
621
|
-
return;
|
|
622
|
-
settled = true;
|
|
623
|
-
signal?.removeEventListener("abort", abort);
|
|
624
|
-
if (escalationTimer)
|
|
625
|
-
clearTimeout(escalationTimer);
|
|
626
|
-
if (hardStopTimer)
|
|
627
|
-
clearTimeout(hardStopTimer);
|
|
628
|
-
if (groupPollTimer)
|
|
629
|
-
clearInterval(groupPollTimer);
|
|
630
|
-
callback();
|
|
631
|
-
};
|
|
632
|
-
const rejectAborted = () => settle(() => reject(new Error("plugin installation aborted", {
|
|
633
|
-
cause: signal?.reason,
|
|
634
|
-
})));
|
|
635
|
-
const processGroupAlive = () => {
|
|
636
|
-
if (!useProcessGroup || child.pid === undefined)
|
|
637
|
-
return !childClosed;
|
|
487
|
+
let pending = Buffer.alloc(0);
|
|
488
|
+
let stderr = "";
|
|
489
|
+
let failure;
|
|
490
|
+
let entries = 1;
|
|
491
|
+
let totalBytes = 0;
|
|
492
|
+
const directories = new Set([""]);
|
|
493
|
+
const terminate = () => {
|
|
638
494
|
try {
|
|
639
|
-
process.
|
|
640
|
-
|
|
495
|
+
if (process.platform !== "win32" && child.pid)
|
|
496
|
+
process.kill(-child.pid, "SIGKILL");
|
|
497
|
+
else
|
|
498
|
+
child.kill("SIGKILL");
|
|
641
499
|
}
|
|
642
|
-
catch
|
|
643
|
-
|
|
500
|
+
catch {
|
|
501
|
+
// The command has already exited.
|
|
644
502
|
}
|
|
645
503
|
};
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
504
|
+
const fail = (error) => {
|
|
505
|
+
failure ??= error;
|
|
506
|
+
terminate();
|
|
507
|
+
};
|
|
508
|
+
const inspectRecord = (line) => {
|
|
509
|
+
if (failure)
|
|
510
|
+
return;
|
|
511
|
+
const tab = line.indexOf(0x09);
|
|
512
|
+
if (tab < 0) {
|
|
513
|
+
fail(new Error("git ls-tree returned an invalid package entry"));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const metadata = line.subarray(0, tab).toString("utf8");
|
|
517
|
+
const pathBytes = line.subarray(tab + 1);
|
|
518
|
+
const path = pathBytes.toString("utf8");
|
|
519
|
+
const match = /^([0-7]{6}) ([a-z]+) [0-9a-f]{40,64} ([0-9]+|-)$/.exec(metadata);
|
|
520
|
+
if (!match || !path || !Buffer.from(path, "utf8").equals(pathBytes)) {
|
|
521
|
+
fail(new Error("git ls-tree returned an invalid package entry"));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const prefix = subdir === "." ? "" : `${subdir}/`;
|
|
525
|
+
const relativePath = prefix && path.startsWith(prefix) ? path.slice(prefix.length) : path;
|
|
526
|
+
const segments = relativePath.split("/");
|
|
527
|
+
if (match[1] === "120000" && segments.includes("node_modules"))
|
|
528
|
+
return;
|
|
529
|
+
if (match[1] !== "100644" && match[1] !== "100755") {
|
|
530
|
+
fail(new Error(`plugin package contains an unsupported Git entry: ${path}`));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (match[2] !== "blob" || match[3] === "-") {
|
|
534
|
+
fail(new Error(`plugin package contains a Git submodule or special entry: ${path}`));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (!relativePath || segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
538
|
+
fail(new Error(`git package contains an invalid path: ${path}`));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (segments.length > limits.maxDepth) {
|
|
542
|
+
fail(new Error(`plugin package exceeds maximum depth ${limits.maxDepth}`));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
let parent = "";
|
|
546
|
+
for (const segment of segments.slice(0, -1)) {
|
|
547
|
+
parent = parent ? `${parent}/${segment}` : segment;
|
|
548
|
+
if (!directories.has(parent)) {
|
|
549
|
+
directories.add(parent);
|
|
550
|
+
entries += 1;
|
|
662
551
|
}
|
|
663
552
|
}
|
|
664
|
-
|
|
665
|
-
|
|
553
|
+
entries += 1;
|
|
554
|
+
if (entries > limits.maxEntries) {
|
|
555
|
+
fail(new Error(`plugin package exceeds ${limits.maxEntries} entries`));
|
|
556
|
+
return;
|
|
666
557
|
}
|
|
667
|
-
|
|
668
|
-
|
|
558
|
+
const size = Number(match[3]);
|
|
559
|
+
if (!Number.isSafeInteger(size) || size > limits.maxFileBytes) {
|
|
560
|
+
fail(new Error(`plugin package file exceeds ${limits.maxFileBytes} bytes: ${path}`));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
totalBytes += size;
|
|
564
|
+
if (totalBytes > limits.maxTotalBytes) {
|
|
565
|
+
fail(new Error(`plugin package exceeds ${limits.maxTotalBytes} total bytes`));
|
|
669
566
|
}
|
|
670
567
|
};
|
|
671
|
-
const
|
|
672
|
-
|
|
568
|
+
const abort = () => fail(new Error("plugin installation aborted"));
|
|
569
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
570
|
+
child.stdout?.on("data", (chunk) => {
|
|
571
|
+
if (failure)
|
|
673
572
|
return;
|
|
674
|
-
|
|
675
|
-
|
|
573
|
+
pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
|
|
574
|
+
for (;;) {
|
|
575
|
+
const separator = pending.indexOf(0);
|
|
576
|
+
if (separator < 0)
|
|
577
|
+
break;
|
|
578
|
+
inspectRecord(pending.subarray(0, separator));
|
|
579
|
+
pending = pending.subarray(separator + 1);
|
|
580
|
+
}
|
|
581
|
+
if (pending.length > MAX_METADATA_BYTES) {
|
|
582
|
+
fail(new Error("git package path exceeds 1 MiB"));
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
child.stderr?.on("data", (chunk) => {
|
|
586
|
+
if (stderr.length < 64 * 1024)
|
|
587
|
+
stderr += chunk.toString("utf8");
|
|
588
|
+
});
|
|
589
|
+
child.once("error", (error) => fail(error));
|
|
590
|
+
child.once("close", (code) => {
|
|
591
|
+
signal?.removeEventListener("abort", abort);
|
|
592
|
+
if (!failure && pending.length > 0) {
|
|
593
|
+
failure = new Error("git ls-tree returned a truncated package entry");
|
|
594
|
+
}
|
|
595
|
+
if (failure)
|
|
596
|
+
reject(failure);
|
|
597
|
+
else if (code !== 0) {
|
|
598
|
+
reject(new Error(`git ls-tree exited with code ${code ?? "null"}${stderr ? `: ${stderr.trim()}` : ""}`));
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
resolveDone();
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
export function runPluginPackageCommand(command, args, onProgress, options = {}) {
|
|
607
|
+
return runCommand(command, args, onProgress, options).then(() => undefined);
|
|
608
|
+
}
|
|
609
|
+
async function capturePluginPackageCommand(command, args, options) {
|
|
610
|
+
let output = "";
|
|
611
|
+
await runCommand(command, args, (line) => {
|
|
612
|
+
output += `${line}\n`;
|
|
613
|
+
}, options);
|
|
614
|
+
const value = output.trim();
|
|
615
|
+
if (!value)
|
|
616
|
+
throw new Error(`${command} produced no output`);
|
|
617
|
+
return value;
|
|
618
|
+
}
|
|
619
|
+
function runCommand(command, args, onProgress, options) {
|
|
620
|
+
return new Promise((resolveDone, reject) => {
|
|
621
|
+
const graceMs = options.abortGraceMs ?? DEFAULT_COMMAND_ABORT_GRACE_MS;
|
|
622
|
+
if (options.signal?.aborted) {
|
|
623
|
+
reject(new Error("plugin installation aborted"));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const child = spawn(command, args, {
|
|
627
|
+
cwd: options.cwd,
|
|
628
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
629
|
+
detached: process.platform !== "win32",
|
|
630
|
+
});
|
|
631
|
+
let finished = false;
|
|
632
|
+
const finish = (callback) => {
|
|
633
|
+
if (finished)
|
|
634
|
+
return;
|
|
635
|
+
finished = true;
|
|
636
|
+
options.signal?.removeEventListener("abort", abort);
|
|
637
|
+
callback();
|
|
676
638
|
};
|
|
677
639
|
const abort = () => {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
|
|
640
|
+
try {
|
|
641
|
+
if (process.platform !== "win32" && child.pid)
|
|
642
|
+
process.kill(-child.pid, "SIGTERM");
|
|
643
|
+
else
|
|
644
|
+
child.kill("SIGTERM");
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
// Child already exited.
|
|
648
|
+
}
|
|
649
|
+
const timer = setTimeout(() => {
|
|
650
|
+
try {
|
|
651
|
+
if (process.platform !== "win32" && child.pid)
|
|
652
|
+
process.kill(-child.pid, "SIGKILL");
|
|
653
|
+
else
|
|
654
|
+
child.kill("SIGKILL");
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
// Child already exited.
|
|
658
|
+
}
|
|
659
|
+
}, graceMs);
|
|
660
|
+
timer.unref?.();
|
|
689
661
|
};
|
|
690
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const onData = (buffer) => {
|
|
694
|
-
for (const line of buffer.toString().split(/\r?\n/)) {
|
|
662
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
663
|
+
const forward = (chunk) => {
|
|
664
|
+
for (const line of chunk.toString("utf8").split(/\r?\n/)) {
|
|
695
665
|
if (line.trim())
|
|
696
666
|
onProgress(line);
|
|
697
667
|
}
|
|
698
668
|
};
|
|
699
|
-
child.stdout?.on("data",
|
|
700
|
-
child.stderr?.on("data",
|
|
701
|
-
child.
|
|
702
|
-
|
|
703
|
-
|
|
669
|
+
child.stdout?.on("data", forward);
|
|
670
|
+
child.stderr?.on("data", forward);
|
|
671
|
+
child.once("error", (error) => finish(() => reject(error)));
|
|
672
|
+
child.once("close", (code) => finish(() => {
|
|
673
|
+
if (options.signal?.aborted)
|
|
674
|
+
reject(new Error("plugin installation aborted"));
|
|
675
|
+
else if (code === 0)
|
|
676
|
+
resolveDone();
|
|
704
677
|
else
|
|
705
|
-
|
|
706
|
-
});
|
|
707
|
-
child.on("close", (code) => {
|
|
708
|
-
childClosed = true;
|
|
709
|
-
if (aborting || signal?.aborted) {
|
|
710
|
-
if (!aborting)
|
|
711
|
-
abort();
|
|
712
|
-
if (!useProcessGroup)
|
|
713
|
-
rejectAborted();
|
|
714
|
-
else
|
|
715
|
-
finishWhenStopped();
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
settle(() => {
|
|
719
|
-
if (code === 0)
|
|
720
|
-
resolveDone();
|
|
721
|
-
else
|
|
722
|
-
reject(new Error(`${command} exited with code ${code ?? "null"}`));
|
|
723
|
-
});
|
|
724
|
-
});
|
|
678
|
+
reject(new Error(`${command} exited with code ${code ?? "null"}`));
|
|
679
|
+
}));
|
|
725
680
|
});
|
|
726
681
|
}
|
|
727
|
-
function isRecord(value) {
|
|
728
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
729
|
-
}
|
|
730
|
-
function messageOf(error) {
|
|
731
|
-
return error instanceof Error ? error.message : String(error);
|
|
732
|
-
}
|