safeinstall-cli 0.1.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/output.js ADDED
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.printConfigInfo = printConfigInfo;
7
+ exports.formatCommand = formatCommand;
8
+ exports.printWarnings = printWarnings;
9
+ exports.writeCliResult = writeCliResult;
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ function printConfigInfo(configPath) {
12
+ if (configPath) {
13
+ console.error(`Using config: ${configPath}`);
14
+ }
15
+ else {
16
+ console.error("Using config: built-in defaults");
17
+ }
18
+ }
19
+ function formatCommand(command, args) {
20
+ return [node_path_1.default.basename(command), ...args].join(" ");
21
+ }
22
+ function printWarnings(evaluations) {
23
+ for (const evaluation of evaluations) {
24
+ for (const warning of evaluation.warnings) {
25
+ console.error(`Warning: ${warning}`);
26
+ }
27
+ }
28
+ }
29
+ function printReason(reason, indent = "") {
30
+ console.error(`${indent}${reason.message}`);
31
+ if (reason.suggestion) {
32
+ console.error(`${indent}Suggestion: ${reason.suggestion}`);
33
+ }
34
+ }
35
+ function serializeCliResult(result) {
36
+ const { details: _details, ...serialized } = result;
37
+ return serialized;
38
+ }
39
+ function writeCliResult(result, jsonMode) {
40
+ if (jsonMode) {
41
+ process.stdout.write(`${JSON.stringify(serializeCliResult(result), null, 2)}\n`);
42
+ return;
43
+ }
44
+ if (result.details?.suppressHumanOutput === true) {
45
+ return;
46
+ }
47
+ if (result.configLabel) {
48
+ console.error(`Using config: ${result.configLabel}`);
49
+ }
50
+ else if (result.configPath) {
51
+ printConfigInfo(result.configPath);
52
+ }
53
+ for (const warning of result.warnings) {
54
+ console.error(`Warning: ${warning}`);
55
+ }
56
+ if (result.mode === "init") {
57
+ if (result.decision === "allow") {
58
+ console.error(result.summary);
59
+ if (typeof result.details?.configPath === "string") {
60
+ console.error(`Created: ${result.details.configPath}`);
61
+ }
62
+ return;
63
+ }
64
+ for (const reason of result.reasons) {
65
+ printReason(reason);
66
+ }
67
+ return;
68
+ }
69
+ if (result.decision === "allow") {
70
+ console.error(result.summary);
71
+ return;
72
+ }
73
+ if (result.affectedPackages.length > 0) {
74
+ console.error(result.mode === "check" ? "Check blocked." : "Install blocked.");
75
+ for (const affectedPackage of result.affectedPackages) {
76
+ const packageLabel = affectedPackage.resolvedVersion
77
+ ? `${affectedPackage.name}@${affectedPackage.resolvedVersion}`
78
+ : affectedPackage.requested;
79
+ console.error(`- ${packageLabel}`);
80
+ for (const reason of affectedPackage.reasons) {
81
+ printReason(reason, " ");
82
+ }
83
+ }
84
+ return;
85
+ }
86
+ console.error(result.mode === "check" ? "Check blocked." : "Install blocked.");
87
+ for (const reason of result.reasons) {
88
+ console.error(`- ${reason.message}`);
89
+ if (reason.suggestion) {
90
+ console.error(` Suggestion: ${reason.suggestion}`);
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildPackageManagerCommand = buildPackageManagerCommand;
4
+ exports.runPackageManager = runPackageManager;
5
+ const node_child_process_1 = require("node:child_process");
6
+ const signals_1 = require("./signals");
7
+ const IGNORE_SCRIPTS_FLAG = {
8
+ npm: "--ignore-scripts",
9
+ pnpm: "--ignore-scripts",
10
+ bun: "--ignore-scripts"
11
+ };
12
+ function buildPackageManagerCommand(manager, managerArgs, command, forwardedArgs, config) {
13
+ const args = [...managerArgs, command, ...forwardedArgs];
14
+ const ignoreScriptsFlag = IGNORE_SCRIPTS_FLAG[manager];
15
+ if (config.packageManagerDefaults[manager].ignoreScripts &&
16
+ !args.includes(ignoreScriptsFlag) &&
17
+ !args.some((arg) => arg.startsWith(`${ignoreScriptsFlag}=`))) {
18
+ args.push(ignoreScriptsFlag);
19
+ }
20
+ return {
21
+ command: manager,
22
+ args
23
+ };
24
+ }
25
+ async function runPackageManager(options) {
26
+ (0, signals_1.throwIfAborted)(options.signal);
27
+ const built = buildPackageManagerCommand(options.manager, options.managerArgs, options.command, options.forwardedArgs, options.config);
28
+ return new Promise((resolve, reject) => {
29
+ let settled = false;
30
+ const child = (0, node_child_process_1.spawn)(built.command, built.args, {
31
+ stdio: options.stdio ?? "inherit",
32
+ cwd: options.cwd,
33
+ env: options.env ?? process.env
34
+ });
35
+ const onAbort = () => {
36
+ const shutdownError = (0, signals_1.getShutdownSignalError)(options.signal);
37
+ child.kill(shutdownError?.signalName ?? "SIGTERM");
38
+ };
39
+ options.signal?.addEventListener("abort", onAbort, { once: true });
40
+ const cleanup = () => {
41
+ options.signal?.removeEventListener("abort", onAbort);
42
+ };
43
+ const rejectOnce = (error) => {
44
+ if (settled) {
45
+ return;
46
+ }
47
+ settled = true;
48
+ cleanup();
49
+ reject(error);
50
+ };
51
+ const resolveOnce = (result) => {
52
+ if (settled) {
53
+ return;
54
+ }
55
+ settled = true;
56
+ cleanup();
57
+ resolve(result);
58
+ };
59
+ let stdout = "";
60
+ let stderr = "";
61
+ if (options.stdio === "pipe") {
62
+ child.stdout?.on("data", (chunk) => {
63
+ stdout += chunk.toString();
64
+ });
65
+ child.stderr?.on("data", (chunk) => {
66
+ stderr += chunk.toString();
67
+ });
68
+ }
69
+ child.on("error", (error) => {
70
+ const shutdownError = (0, signals_1.getShutdownSignalError)(options.signal);
71
+ if (shutdownError) {
72
+ rejectOnce(shutdownError);
73
+ return;
74
+ }
75
+ if (error.code === "ENOENT") {
76
+ rejectOnce(new Error(`Package manager "${options.manager}" was not found in PATH.`));
77
+ return;
78
+ }
79
+ rejectOnce(error);
80
+ });
81
+ child.on("exit", (code, signal) => {
82
+ const shutdownError = (0, signals_1.getShutdownSignalError)(options.signal);
83
+ if (shutdownError) {
84
+ rejectOnce(shutdownError);
85
+ return;
86
+ }
87
+ if (signal) {
88
+ rejectOnce(new Error(`Package manager exited with signal ${signal}.`));
89
+ return;
90
+ }
91
+ resolveOnce({
92
+ code: code ?? 1,
93
+ stdout: options.stdio === "pipe" ? stdout : undefined,
94
+ stderr: options.stdio === "pipe" ? stderr : undefined
95
+ });
96
+ });
97
+ });
98
+ }
package/dist/policy.js ADDED
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.evaluatePackage = evaluatePackage;
4
+ function isPackageAllowlisted(config, packageName) {
5
+ return config.allowedPackages.includes(packageName);
6
+ }
7
+ function isSourcePolicyRelevant(sourceType) {
8
+ return sourceType !== "workspace" && sourceType !== "file" && sourceType !== "directory";
9
+ }
10
+ function allowedLifecycleScripts(config, packageName) {
11
+ return config.allowedScripts[packageName] ?? [];
12
+ }
13
+ function hasUnallowedLifecycleScripts(config, packageName, scripts) {
14
+ const allowed = new Set(allowedLifecycleScripts(config, packageName));
15
+ return scripts.some((script) => !allowed.has(script));
16
+ }
17
+ function releaseAgeHours(now, publishedAt) {
18
+ return (now.getTime() - publishedAt.getTime()) / 1000 / 60 / 60;
19
+ }
20
+ function formatHours(value) {
21
+ return value.toFixed(1).replace(/\.0$/, "");
22
+ }
23
+ function evaluatePackage(input) {
24
+ const evaluation = {
25
+ requested: input.requested,
26
+ priorState: input.priorState,
27
+ resolvedRegistryPackage: input.resolvedRegistryPackage,
28
+ blockedReasons: [],
29
+ warnings: []
30
+ };
31
+ if (isPackageAllowlisted(input.config, input.requested.name)) {
32
+ evaluation.warnings.push(`Package ${input.requested.name} is allowlisted; policy checks were skipped.`);
33
+ return evaluation;
34
+ }
35
+ if (isSourcePolicyRelevant(input.requested.sourceType) &&
36
+ !input.config.allowedSources.includes(input.requested.sourceType)) {
37
+ evaluation.blockedReasons.push({
38
+ code: "untrusted-source",
39
+ message: `Blocked: untrusted source (${input.requested.sourceType}).`,
40
+ suggestion: "Use a registry release or allow this source intentionally."
41
+ });
42
+ }
43
+ const priorSourceType = input.priorState?.declaredSourceType;
44
+ if (priorSourceType === "registry" &&
45
+ (input.requested.sourceType === "git" ||
46
+ input.requested.sourceType === "url" ||
47
+ input.requested.sourceType === "tarball")) {
48
+ evaluation.blockedReasons.push({
49
+ code: "trust-level-dropped",
50
+ message: `Blocked: trust level dropped (${input.requested.name} changed from registry to ${input.requested.sourceType}).`,
51
+ suggestion: "Keep this package on a registry release or allow the source change intentionally."
52
+ });
53
+ }
54
+ if (!input.resolvedRegistryPackage) {
55
+ return evaluation;
56
+ }
57
+ const ageHours = releaseAgeHours(input.now, input.resolvedRegistryPackage.publishedAt);
58
+ if (ageHours < input.config.minimumReleaseAgeHours) {
59
+ evaluation.blockedReasons.push({
60
+ code: "release-too-new",
61
+ message: `Blocked: release too new (${input.requested.name}@${input.resolvedRegistryPackage.resolvedVersion} is ${formatHours(ageHours)} hours old; minimum is ${input.config.minimumReleaseAgeHours} hours).`,
62
+ suggestion: "Retry later or lower minimumReleaseAgeHours if this package is intentionally urgent."
63
+ });
64
+ }
65
+ if (input.resolvedRegistryPackage.lifecycleScripts.length > 0 &&
66
+ hasUnallowedLifecycleScripts(input.config, input.requested.name, input.resolvedRegistryPackage.lifecycleScripts)) {
67
+ evaluation.blockedReasons.push({
68
+ code: "install-script-present",
69
+ message: `Blocked: install script present (${input.requested.name}@${input.resolvedRegistryPackage.resolvedVersion} has ${input.resolvedRegistryPackage.lifecycleScripts.join(", ")}).`,
70
+ suggestion: "Allow this package explicitly in allowedScripts if you trust its install hooks."
71
+ });
72
+ }
73
+ const priorHadLifecycleScripts = (input.priorLifecycleScripts ?? []).length > 0;
74
+ const currentHasLifecycleScripts = input.resolvedRegistryPackage.lifecycleScripts.length > 0;
75
+ if (!priorHadLifecycleScripts && currentHasLifecycleScripts && input.priorState?.installedVersion) {
76
+ evaluation.blockedReasons.push({
77
+ code: "trust-level-dropped",
78
+ message: `Blocked: trust level dropped (${input.requested.name} introduces lifecycle scripts in ${input.resolvedRegistryPackage.resolvedVersion}; installed ${input.priorState.installedVersion} had none).`,
79
+ suggestion: "Review the new version carefully and allow the script only if you intend to trust it."
80
+ });
81
+ }
82
+ return evaluation;
83
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.fileExists = fileExists;
7
+ exports.findNearestUpward = findNearestUpward;
8
+ exports.findNearestPackageDir = findNearestPackageDir;
9
+ exports.resolveEffectiveCwd = resolveEffectiveCwd;
10
+ exports.hasAmbiguousWorkspaceFlags = hasAmbiguousWorkspaceFlags;
11
+ exports.resolveInvocationContext = resolveInvocationContext;
12
+ exports.relativeProjectKey = relativeProjectKey;
13
+ const promises_1 = require("node:fs/promises");
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ const CWD_FLAGS = new Set(["-C", "--dir", "--cwd", "--prefix"]);
16
+ const AMBIGUOUS_WORKSPACE_FLAGS = {
17
+ npm: new Set(["-w", "--workspace", "--workspaces"]),
18
+ pnpm: new Set(["-F", "--filter", "-r", "--recursive"]),
19
+ bun: new Set([])
20
+ };
21
+ async function fileExists(filePath) {
22
+ try {
23
+ await (0, promises_1.access)(filePath);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ async function findNearestUpward(startDir, fileName) {
31
+ let currentDir = node_path_1.default.resolve(startDir);
32
+ while (true) {
33
+ const candidate = node_path_1.default.join(currentDir, fileName);
34
+ if (await fileExists(candidate)) {
35
+ return candidate;
36
+ }
37
+ const parentDir = node_path_1.default.dirname(currentDir);
38
+ if (parentDir === currentDir) {
39
+ return undefined;
40
+ }
41
+ currentDir = parentDir;
42
+ }
43
+ }
44
+ async function findNearestPackageDir(startDir) {
45
+ const packageJsonPath = await findNearestUpward(startDir, "package.json");
46
+ return packageJsonPath ? node_path_1.default.dirname(packageJsonPath) : undefined;
47
+ }
48
+ function extractFlagValue(args, index) {
49
+ const token = args[index];
50
+ if (token.includes("=")) {
51
+ return token.slice(token.indexOf("=") + 1);
52
+ }
53
+ return args[index + 1];
54
+ }
55
+ function resolveEffectiveCwd(invokedCwd, args) {
56
+ let effectiveCwd = node_path_1.default.resolve(invokedCwd);
57
+ for (let index = 0; index < args.length; index += 1) {
58
+ const token = args[index];
59
+ const flagName = token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
60
+ if (!CWD_FLAGS.has(flagName)) {
61
+ continue;
62
+ }
63
+ const flagValue = extractFlagValue(args, index);
64
+ if (!flagValue) {
65
+ continue;
66
+ }
67
+ effectiveCwd = node_path_1.default.resolve(invokedCwd, flagValue);
68
+ if (!token.includes("=")) {
69
+ index += 1;
70
+ }
71
+ }
72
+ return effectiveCwd;
73
+ }
74
+ function hasAmbiguousWorkspaceFlags(manager, args) {
75
+ const flags = AMBIGUOUS_WORKSPACE_FLAGS[manager];
76
+ return args.some((token) => flags.has(token.includes("=") ? token.slice(0, token.indexOf("=")) : token));
77
+ }
78
+ async function resolveInvocationContext(invokedCwd, forwardedArgs) {
79
+ const effectiveCwd = resolveEffectiveCwd(invokedCwd, forwardedArgs);
80
+ const packageDir = await findNearestPackageDir(effectiveCwd);
81
+ return {
82
+ invokedCwd: node_path_1.default.resolve(invokedCwd),
83
+ effectiveCwd,
84
+ packageDir
85
+ };
86
+ }
87
+ function relativeProjectKey(rootDir, packageDir) {
88
+ const relativePath = node_path_1.default.relative(rootDir, packageDir);
89
+ return relativePath === "" ? "." : relativePath.split(node_path_1.default.sep).join("/");
90
+ }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadNpmProjectInstallTargets = loadNpmProjectInstallTargets;
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const project_state_1 = require("../project-state");
10
+ const project_discovery_1 = require("../project-discovery");
11
+ const shared_1 = require("./shared");
12
+ function readDirectManifestDependencies(manifestDependencies) {
13
+ return Object.entries(manifestDependencies).sort(([left], [right]) => left.localeCompare(right));
14
+ }
15
+ async function resolveNpmLockfilePath(effectiveCwd) {
16
+ let currentDir = node_path_1.default.resolve(effectiveCwd);
17
+ while (true) {
18
+ const packageLockPath = node_path_1.default.join(currentDir, "package-lock.json");
19
+ if (await (0, project_discovery_1.fileExists)(packageLockPath)) {
20
+ return packageLockPath;
21
+ }
22
+ const shrinkwrapPath = node_path_1.default.join(currentDir, "npm-shrinkwrap.json");
23
+ if (await (0, project_discovery_1.fileExists)(shrinkwrapPath)) {
24
+ return shrinkwrapPath;
25
+ }
26
+ const parentDir = node_path_1.default.dirname(currentDir);
27
+ if (parentDir === currentDir) {
28
+ return undefined;
29
+ }
30
+ currentDir = parentDir;
31
+ }
32
+ }
33
+ async function loadNpmProjectInstallTargets(effectiveCwd, packageDir) {
34
+ const lockfilePath = await resolveNpmLockfilePath(effectiveCwd);
35
+ if (!lockfilePath) {
36
+ return {
37
+ targets: [],
38
+ issues: ["Project install blocked: package-lock.json or npm-shrinkwrap.json is required for safeinstall npm install."]
39
+ };
40
+ }
41
+ const rootDir = node_path_1.default.dirname(lockfilePath);
42
+ const targetPackageDir = packageDir ?? rootDir;
43
+ const packageEntryKey = (0, project_discovery_1.relativeProjectKey)(rootDir, targetPackageDir) === "." ? "" : (0, project_discovery_1.relativeProjectKey)(rootDir, targetPackageDir);
44
+ const manifestDependencies = await (0, project_state_1.loadManifestDependencies)(targetPackageDir);
45
+ const directDependencies = readDirectManifestDependencies(manifestDependencies);
46
+ if (directDependencies.length === 0) {
47
+ return {
48
+ targets: [],
49
+ issues: [],
50
+ lockfilePath
51
+ };
52
+ }
53
+ const rawLockfile = await (0, promises_1.readFile)(lockfilePath, "utf8");
54
+ const lockfile = JSON.parse(rawLockfile);
55
+ const packageEntry = lockfile.packages?.[packageEntryKey];
56
+ if (!packageEntry) {
57
+ return {
58
+ targets: [],
59
+ issues: [
60
+ `Project install blocked: ${targetPackageDir} does not map to a package entry in ${node_path_1.default.basename(lockfilePath)}.`
61
+ ],
62
+ lockfilePath
63
+ };
64
+ }
65
+ const issues = [];
66
+ const targets = [];
67
+ for (const [name, manifestSpec] of directDependencies) {
68
+ const lockedSpec = packageEntry.dependencies?.[name] ??
69
+ packageEntry.devDependencies?.[name] ??
70
+ packageEntry.optionalDependencies?.[name];
71
+ if (lockedSpec && lockedSpec !== manifestSpec) {
72
+ issues.push(`Project install blocked: ${name} has specifier ${JSON.stringify(manifestSpec)} in package.json but ${JSON.stringify(lockedSpec)} in ${node_path_1.default.basename(lockfilePath)}.`);
73
+ continue;
74
+ }
75
+ const resolvedPackageEntry = lockfile.packages?.[`node_modules/${name}`] ??
76
+ lockfile.packages?.[`${packageEntryKey ? `${packageEntryKey}/` : ""}node_modules/${name}`] ??
77
+ lockfile.dependencies?.[name];
78
+ if (!resolvedPackageEntry) {
79
+ issues.push(`Project install blocked: ${name} is declared in package.json but missing from ${node_path_1.default.basename(lockfilePath)}.`);
80
+ continue;
81
+ }
82
+ const declaredSourceType = (0, project_state_1.classifyDeclaredSource)(manifestSpec);
83
+ const sourceType = (0, shared_1.classifyResolvedSource)(declaredSourceType, resolvedPackageEntry.resolved, Boolean(resolvedPackageEntry.integrity));
84
+ targets.push({
85
+ manifestSpec,
86
+ requested: sourceType === "registry" && resolvedPackageEntry.version
87
+ ? (0, shared_1.createRegistryRequestedPackage)(name, resolvedPackageEntry.version)
88
+ : (0, shared_1.createNonRegistryRequestedPackage)(name, manifestSpec, sourceType, resolvedPackageEntry.resolved ?? manifestSpec),
89
+ lockfilePath
90
+ });
91
+ }
92
+ return { targets, issues, lockfilePath };
93
+ }
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadPnpmProjectInstallTargets = loadPnpmProjectInstallTargets;
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const yaml_1 = require("yaml");
10
+ const project_state_1 = require("../project-state");
11
+ const project_discovery_1 = require("../project-discovery");
12
+ const shared_1 = require("./shared");
13
+ function readDirectManifestDependencies(manifestDependencies) {
14
+ return Object.entries(manifestDependencies).sort(([left], [right]) => left.localeCompare(right));
15
+ }
16
+ function normalizeImporterDependency(entry) {
17
+ if (!entry) {
18
+ return {};
19
+ }
20
+ if (typeof entry === "string") {
21
+ return { version: entry };
22
+ }
23
+ return {
24
+ specifier: entry.specifier,
25
+ version: entry.version
26
+ };
27
+ }
28
+ function getImporter(lockfile, importerKey) {
29
+ if (lockfile.importers?.[importerKey]) {
30
+ return lockfile.importers[importerKey];
31
+ }
32
+ if (importerKey === "." && (lockfile.dependencies ||
33
+ lockfile.devDependencies ||
34
+ lockfile.optionalDependencies ||
35
+ lockfile.specifiers)) {
36
+ const applySpecifiers = (entries) => {
37
+ if (!entries) {
38
+ return undefined;
39
+ }
40
+ return Object.fromEntries(Object.entries(entries).map(([name, entry]) => {
41
+ if (typeof entry === "string") {
42
+ return [name, { specifier: lockfile.specifiers?.[name], version: entry }];
43
+ }
44
+ return [name, { specifier: entry.specifier ?? lockfile.specifiers?.[name], version: entry.version }];
45
+ }));
46
+ };
47
+ return {
48
+ dependencies: applySpecifiers(lockfile.dependencies),
49
+ devDependencies: applySpecifiers(lockfile.devDependencies),
50
+ optionalDependencies: applySpecifiers(lockfile.optionalDependencies)
51
+ };
52
+ }
53
+ return undefined;
54
+ }
55
+ function findPackageEntry(packages, name, versionRef) {
56
+ if (!packages) {
57
+ return undefined;
58
+ }
59
+ const exactKey = `${name}@${versionRef}`;
60
+ if (packages[exactKey]) {
61
+ return packages[exactKey];
62
+ }
63
+ const legacyKey = `/${name}/${versionRef}`;
64
+ if (packages[legacyKey]) {
65
+ return packages[legacyKey];
66
+ }
67
+ const matchingKey = Object.keys(packages).find((entryKey) => entryKey === exactKey || entryKey.startsWith(`${exactKey}(`));
68
+ if (matchingKey) {
69
+ return packages[matchingKey];
70
+ }
71
+ const legacyMatchingKey = Object.keys(packages).find((entryKey) => entryKey === legacyKey || entryKey.startsWith(`${legacyKey}_`));
72
+ return legacyMatchingKey ? packages[legacyMatchingKey] : undefined;
73
+ }
74
+ async function loadPnpmProjectInstallTargets(effectiveCwd, packageDir) {
75
+ const lockfilePath = await (async () => {
76
+ let currentDir = node_path_1.default.resolve(effectiveCwd);
77
+ while (true) {
78
+ const candidate = node_path_1.default.join(currentDir, "pnpm-lock.yaml");
79
+ if (await (0, project_discovery_1.fileExists)(candidate)) {
80
+ return candidate;
81
+ }
82
+ const parentDir = node_path_1.default.dirname(currentDir);
83
+ if (parentDir === currentDir) {
84
+ return undefined;
85
+ }
86
+ currentDir = parentDir;
87
+ }
88
+ })();
89
+ if (!lockfilePath) {
90
+ return {
91
+ targets: [],
92
+ issues: ["Project install blocked: pnpm-lock.yaml is required for safeinstall pnpm install."]
93
+ };
94
+ }
95
+ const rootDir = node_path_1.default.dirname(lockfilePath);
96
+ const targetPackageDir = packageDir ?? rootDir;
97
+ const importerKey = (0, project_discovery_1.relativeProjectKey)(rootDir, targetPackageDir);
98
+ const manifestDependencies = await (0, project_state_1.loadManifestDependencies)(targetPackageDir);
99
+ const directDependencies = readDirectManifestDependencies(manifestDependencies);
100
+ if (directDependencies.length === 0) {
101
+ return {
102
+ targets: [],
103
+ issues: [],
104
+ lockfilePath
105
+ };
106
+ }
107
+ const rawLockfile = await (0, promises_1.readFile)(lockfilePath, "utf8");
108
+ const lockfile = (0, yaml_1.parse)(rawLockfile);
109
+ const importer = getImporter(lockfile, importerKey);
110
+ if (!importer) {
111
+ return {
112
+ targets: [],
113
+ issues: [
114
+ `Project install blocked: ${targetPackageDir} does not map to a pnpm importer in ${node_path_1.default.basename(lockfilePath)}.`
115
+ ],
116
+ lockfilePath
117
+ };
118
+ }
119
+ const importerDependencies = {
120
+ ...(importer.dependencies ?? {}),
121
+ ...(importer.devDependencies ?? {}),
122
+ ...(importer.optionalDependencies ?? {})
123
+ };
124
+ const issues = [];
125
+ const targets = [];
126
+ for (const [name, manifestSpec] of directDependencies) {
127
+ const importerDependency = normalizeImporterDependency(importerDependencies[name]);
128
+ if (!importerDependency.version) {
129
+ issues.push(`Project install blocked: ${name} is declared in package.json but missing from pnpm-lock.yaml.`);
130
+ continue;
131
+ }
132
+ if (importerDependency.specifier && importerDependency.specifier !== manifestSpec) {
133
+ issues.push(`Project install blocked: ${name} has specifier ${JSON.stringify(manifestSpec)} in package.json but ${JSON.stringify(importerDependency.specifier)} in pnpm-lock.yaml.`);
134
+ continue;
135
+ }
136
+ const packageEntry = findPackageEntry(lockfile.packages, name, importerDependency.version);
137
+ const declaredSourceType = (0, project_state_1.classifyDeclaredSource)(manifestSpec);
138
+ if (importerDependency.version.startsWith("link:")) {
139
+ targets.push({
140
+ manifestSpec,
141
+ requested: (0, shared_1.createNonRegistryRequestedPackage)(name, manifestSpec, declaredSourceType === "directory" ? "directory" : "workspace", importerDependency.version),
142
+ lockfilePath
143
+ });
144
+ continue;
145
+ }
146
+ if (importerDependency.version.startsWith("file:")) {
147
+ targets.push({
148
+ manifestSpec,
149
+ requested: (0, shared_1.createNonRegistryRequestedPackage)(name, manifestSpec, "file", importerDependency.version),
150
+ lockfilePath
151
+ });
152
+ continue;
153
+ }
154
+ const packageVersion = packageEntry?.version ?? (0, shared_1.extractSemverPrefix)(importerDependency.version);
155
+ const resolvedReference = packageEntry?.resolution?.tarball ?? importerDependency.version;
156
+ const sourceType = (0, shared_1.classifyResolvedSource)(declaredSourceType, resolvedReference, Boolean(packageEntry?.resolution?.integrity));
157
+ targets.push({
158
+ manifestSpec,
159
+ requested: sourceType === "registry" && packageVersion
160
+ ? (0, shared_1.createRegistryRequestedPackage)(name, packageVersion)
161
+ : (0, shared_1.createNonRegistryRequestedPackage)(name, importerDependency.specifier ?? manifestSpec, sourceType, resolvedReference),
162
+ lockfilePath
163
+ });
164
+ }
165
+ return { targets, issues, lockfilePath };
166
+ }