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/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/SUPPORT.md +42 -0
- package/dist/async.js +26 -0
- package/dist/check-flow.js +160 -0
- package/dist/cli-options.js +15 -0
- package/dist/cli.js +97 -0
- package/dist/config.js +129 -0
- package/dist/disk-cache.js +67 -0
- package/dist/evaluations.js +27 -0
- package/dist/init-flow.js +55 -0
- package/dist/install-flow.js +274 -0
- package/dist/output.js +93 -0
- package/dist/package-managers.js +98 -0
- package/dist/policy.js +83 -0
- package/dist/project-discovery.js +90 -0
- package/dist/project-installs/npm.js +93 -0
- package/dist/project-installs/pnpm.js +166 -0
- package/dist/project-installs/shared.js +62 -0
- package/dist/project-installs/types.js +2 -0
- package/dist/project-installs.js +60 -0
- package/dist/project-state.js +101 -0
- package/dist/registry.js +239 -0
- package/dist/signals.js +55 -0
- package/dist/specs.js +185 -0
- package/dist/types.js +2 -0
- package/package.json +60 -0
- package/safeinstall.config.example.json +21 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const cli_options_1 = require("./cli-options");
|
|
5
|
+
const check_flow_1 = require("./check-flow");
|
|
6
|
+
const init_flow_1 = require("./init-flow");
|
|
7
|
+
const install_flow_1 = require("./install-flow");
|
|
8
|
+
const output_1 = require("./output");
|
|
9
|
+
const signals_1 = require("./signals");
|
|
10
|
+
const PACKAGE_VERSION = String(require("../package.json").version ?? "0.0.0");
|
|
11
|
+
function isHelpRequest(argv) {
|
|
12
|
+
return argv[0] === "--help" || argv[0] === "-h";
|
|
13
|
+
}
|
|
14
|
+
function isVersionRequest(argv) {
|
|
15
|
+
return argv[0] === "--version" || argv[0] === "-v";
|
|
16
|
+
}
|
|
17
|
+
function printHelp() {
|
|
18
|
+
process.stdout.write([
|
|
19
|
+
"Usage:",
|
|
20
|
+
" safeinstall <npm|pnpm|bun> <install-command> [...args]",
|
|
21
|
+
" safeinstall check",
|
|
22
|
+
" safeinstall init [--force]",
|
|
23
|
+
"",
|
|
24
|
+
"Global options:",
|
|
25
|
+
" --json Emit machine-readable JSON output.",
|
|
26
|
+
" --help, -h Show this help text.",
|
|
27
|
+
" --version, -v Show the current SafeInstall version."
|
|
28
|
+
].join("\n") + "\n");
|
|
29
|
+
}
|
|
30
|
+
async function main() {
|
|
31
|
+
const [, , ...rawArgv] = process.argv;
|
|
32
|
+
const { args: argv, json } = (0, cli_options_1.parseCliOptions)(rawArgv);
|
|
33
|
+
const shutdown = (0, signals_1.createShutdownController)();
|
|
34
|
+
try {
|
|
35
|
+
if (isHelpRequest(argv)) {
|
|
36
|
+
printHelp();
|
|
37
|
+
process.exitCode = 0;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (isVersionRequest(argv)) {
|
|
41
|
+
process.stdout.write(`${PACKAGE_VERSION}\n`);
|
|
42
|
+
process.exitCode = 0;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
let result;
|
|
46
|
+
if (argv[0] === "check") {
|
|
47
|
+
result = await (0, check_flow_1.runCheckFlow)(process.cwd(), argv, {
|
|
48
|
+
signal: shutdown.signal
|
|
49
|
+
});
|
|
50
|
+
(0, output_1.writeCliResult)(result, json);
|
|
51
|
+
process.exitCode = result.exitCode;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (argv[0] === "init") {
|
|
55
|
+
result = await (0, init_flow_1.runInitFlow)(process.cwd(), argv, {
|
|
56
|
+
force: argv.includes("--force")
|
|
57
|
+
});
|
|
58
|
+
(0, output_1.writeCliResult)(result, json);
|
|
59
|
+
process.exitCode = result.exitCode;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
result = await (0, install_flow_1.runInstallFlow)(process.cwd(), argv, {
|
|
63
|
+
jsonMode: json,
|
|
64
|
+
signal: shutdown.signal
|
|
65
|
+
});
|
|
66
|
+
(0, output_1.writeCliResult)(result, json);
|
|
67
|
+
process.exitCode = result.exitCode;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const interrupted = error instanceof signals_1.ShutdownSignalError;
|
|
71
|
+
const result = {
|
|
72
|
+
mode: argv[0] === "check" ? "check" : argv[0] === "init" ? "init" : "install",
|
|
73
|
+
decision: "error",
|
|
74
|
+
exitCode: interrupted ? (0, signals_1.signalExitCode)(error.signalName) : 1,
|
|
75
|
+
exitCodeMeaning: interrupted
|
|
76
|
+
? `SafeInstall was interrupted by ${error.signalName}.`
|
|
77
|
+
: "SafeInstall failed before it could complete the command.",
|
|
78
|
+
command: argv,
|
|
79
|
+
commandString: (0, output_1.formatCommand)("safeinstall", argv),
|
|
80
|
+
reasons: [
|
|
81
|
+
{
|
|
82
|
+
code: interrupted ? "interrupted" : "runtime-error",
|
|
83
|
+
message: error instanceof Error ? error.message : String(error)
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
summary: interrupted ? "SafeInstall interrupted." : "SafeInstall failed.",
|
|
87
|
+
warnings: [],
|
|
88
|
+
affectedPackages: []
|
|
89
|
+
};
|
|
90
|
+
(0, output_1.writeCliResult)(result, json);
|
|
91
|
+
process.exitCode = result.exitCode;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
shutdown.dispose();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
void main();
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
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.DEFAULT_REGISTRY_URL = exports.CONFIG_FILE_NAME = void 0;
|
|
7
|
+
exports.createDefaultConfig = createDefaultConfig;
|
|
8
|
+
exports.getConfigPath = getConfigPath;
|
|
9
|
+
exports.serializeConfig = serializeConfig;
|
|
10
|
+
exports.loadConfig = loadConfig;
|
|
11
|
+
const promises_1 = require("node:fs/promises");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const project_discovery_1 = require("./project-discovery");
|
|
14
|
+
exports.CONFIG_FILE_NAME = "safeinstall.config.json";
|
|
15
|
+
exports.DEFAULT_REGISTRY_URL = "https://registry.npmjs.org";
|
|
16
|
+
function normalizeRegistryUrl(input) {
|
|
17
|
+
let parsedUrl;
|
|
18
|
+
try {
|
|
19
|
+
parsedUrl = new URL(input);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error("Config error: registryUrl must be a valid http or https URL.");
|
|
23
|
+
}
|
|
24
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
25
|
+
throw new Error("Config error: registryUrl must use http or https.");
|
|
26
|
+
}
|
|
27
|
+
const normalizedPathname = parsedUrl.pathname.replace(/\/+$/, "");
|
|
28
|
+
parsedUrl.pathname = normalizedPathname === "" ? "/" : normalizedPathname;
|
|
29
|
+
return parsedUrl.toString().replace(/\/$/, "");
|
|
30
|
+
}
|
|
31
|
+
function createDefaultConfig() {
|
|
32
|
+
return {
|
|
33
|
+
minimumReleaseAgeHours: 72,
|
|
34
|
+
registryUrl: exports.DEFAULT_REGISTRY_URL,
|
|
35
|
+
allowedScripts: {},
|
|
36
|
+
allowedSources: ["registry", "workspace", "file", "directory"],
|
|
37
|
+
allowedPackages: [],
|
|
38
|
+
ciMode: process.env.CI === "true",
|
|
39
|
+
packageManagerDefaults: {
|
|
40
|
+
npm: { ignoreScripts: true },
|
|
41
|
+
pnpm: { ignoreScripts: true },
|
|
42
|
+
bun: { ignoreScripts: true }
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function isPackageManagerName(value) {
|
|
47
|
+
return value === "npm" || value === "pnpm" || value === "bun";
|
|
48
|
+
}
|
|
49
|
+
function mergeConfig(input) {
|
|
50
|
+
const defaultConfig = createDefaultConfig();
|
|
51
|
+
const merged = {
|
|
52
|
+
...defaultConfig,
|
|
53
|
+
...input,
|
|
54
|
+
allowedScripts: {
|
|
55
|
+
...defaultConfig.allowedScripts,
|
|
56
|
+
...(input.allowedScripts ?? {})
|
|
57
|
+
},
|
|
58
|
+
packageManagerDefaults: {
|
|
59
|
+
npm: {
|
|
60
|
+
...defaultConfig.packageManagerDefaults.npm,
|
|
61
|
+
...(input.packageManagerDefaults?.npm ?? {})
|
|
62
|
+
},
|
|
63
|
+
pnpm: {
|
|
64
|
+
...defaultConfig.packageManagerDefaults.pnpm,
|
|
65
|
+
...(input.packageManagerDefaults?.pnpm ?? {})
|
|
66
|
+
},
|
|
67
|
+
bun: {
|
|
68
|
+
...defaultConfig.packageManagerDefaults.bun,
|
|
69
|
+
...(input.packageManagerDefaults?.bun ?? {})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
if (!Number.isFinite(merged.minimumReleaseAgeHours) || merged.minimumReleaseAgeHours < 0) {
|
|
74
|
+
throw new Error("Config error: minimumReleaseAgeHours must be a non-negative number.");
|
|
75
|
+
}
|
|
76
|
+
merged.registryUrl = normalizeRegistryUrl(merged.registryUrl);
|
|
77
|
+
for (const source of merged.allowedSources) {
|
|
78
|
+
if (source !== "registry" &&
|
|
79
|
+
source !== "git" &&
|
|
80
|
+
source !== "tarball" &&
|
|
81
|
+
source !== "url" &&
|
|
82
|
+
source !== "file" &&
|
|
83
|
+
source !== "directory" &&
|
|
84
|
+
source !== "workspace" &&
|
|
85
|
+
source !== "unknown") {
|
|
86
|
+
throw new Error(`Config error: unsupported source type "${source}".`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const [packageName, scripts] of Object.entries(merged.allowedScripts)) {
|
|
90
|
+
if (!Array.isArray(scripts)) {
|
|
91
|
+
throw new Error(`Config error: allowedScripts.${packageName} must be an array.`);
|
|
92
|
+
}
|
|
93
|
+
for (const script of scripts) {
|
|
94
|
+
if (script !== "preinstall" && script !== "install" && script !== "postinstall") {
|
|
95
|
+
throw new Error(`Config error: allowedScripts.${packageName} contains unsupported script "${script}".`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const [manager, defaults] of Object.entries(merged.packageManagerDefaults)) {
|
|
100
|
+
if (!isPackageManagerName(manager)) {
|
|
101
|
+
throw new Error(`Config error: unsupported package manager "${manager}".`);
|
|
102
|
+
}
|
|
103
|
+
if (typeof defaults.ignoreScripts !== "boolean") {
|
|
104
|
+
throw new Error(`Config error: packageManagerDefaults.${manager}.ignoreScripts must be a boolean.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return merged;
|
|
108
|
+
}
|
|
109
|
+
async function findConfigFile(startDir) {
|
|
110
|
+
return (0, project_discovery_1.findNearestUpward)(node_path_1.default.resolve(startDir), exports.CONFIG_FILE_NAME);
|
|
111
|
+
}
|
|
112
|
+
function getConfigPath(cwd) {
|
|
113
|
+
return node_path_1.default.join(cwd, exports.CONFIG_FILE_NAME);
|
|
114
|
+
}
|
|
115
|
+
function serializeConfig(config) {
|
|
116
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
117
|
+
}
|
|
118
|
+
async function loadConfig(startDir) {
|
|
119
|
+
const configPath = await findConfigFile(startDir);
|
|
120
|
+
if (!configPath) {
|
|
121
|
+
return { config: createDefaultConfig() };
|
|
122
|
+
}
|
|
123
|
+
const rawText = await (0, promises_1.readFile)(configPath, "utf8");
|
|
124
|
+
const parsed = JSON.parse(rawText);
|
|
125
|
+
return {
|
|
126
|
+
config: mergeConfig(parsed),
|
|
127
|
+
path: configPath
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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.DiskCache = void 0;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const promises_1 = require("node:fs/promises");
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
function defaultCacheDir() {
|
|
12
|
+
if (process.env.SAFEINSTALL_CACHE_DIR) {
|
|
13
|
+
return process.env.SAFEINSTALL_CACHE_DIR;
|
|
14
|
+
}
|
|
15
|
+
if (process.env.XDG_CACHE_HOME) {
|
|
16
|
+
return node_path_1.default.join(process.env.XDG_CACHE_HOME, "safeinstall");
|
|
17
|
+
}
|
|
18
|
+
if (process.platform === "darwin") {
|
|
19
|
+
return node_path_1.default.join(node_os_1.default.homedir(), "Library", "Caches", "SafeInstall");
|
|
20
|
+
}
|
|
21
|
+
return node_path_1.default.join(node_os_1.default.homedir(), ".cache", "safeinstall");
|
|
22
|
+
}
|
|
23
|
+
function hashKey(key) {
|
|
24
|
+
return (0, node_crypto_1.createHash)("sha256").update(key).digest("hex");
|
|
25
|
+
}
|
|
26
|
+
class DiskCache {
|
|
27
|
+
cacheDir;
|
|
28
|
+
ttlMs;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.cacheDir = options.cacheDir ?? defaultCacheDir();
|
|
31
|
+
this.ttlMs = options.ttlMs;
|
|
32
|
+
}
|
|
33
|
+
async getJson(namespace, key) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = await (0, promises_1.readFile)(this.cacheFilePath(namespace, key), "utf8");
|
|
36
|
+
const entry = JSON.parse(raw);
|
|
37
|
+
if (typeof entry.cachedAt !== "number") {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
if (Date.now() - entry.cachedAt > this.ttlMs) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return entry.value;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async setJson(namespace, key, value) {
|
|
50
|
+
try {
|
|
51
|
+
const namespaceDir = node_path_1.default.join(this.cacheDir, namespace);
|
|
52
|
+
await (0, promises_1.mkdir)(namespaceDir, { recursive: true });
|
|
53
|
+
const payload = {
|
|
54
|
+
cachedAt: Date.now(),
|
|
55
|
+
value
|
|
56
|
+
};
|
|
57
|
+
await (0, promises_1.writeFile)(this.cacheFilePath(namespace, key), `${JSON.stringify(payload)}\n`, "utf8");
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Cache failures must never block installs or checks.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
cacheFilePath(namespace, key) {
|
|
64
|
+
return node_path_1.default.join(this.cacheDir, namespace, `${hashKey(key)}.json`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.DiskCache = DiskCache;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.evaluateRequestedPackages = evaluateRequestedPackages;
|
|
4
|
+
const async_1 = require("./async");
|
|
5
|
+
const project_state_1 = require("./project-state");
|
|
6
|
+
const policy_1 = require("./policy");
|
|
7
|
+
const REGISTRY_EVALUATION_CONCURRENCY = 8;
|
|
8
|
+
async function evaluateRequestedPackages(projectDir, requestedPackages, registryClient, config) {
|
|
9
|
+
const now = new Date();
|
|
10
|
+
return (0, async_1.mapConcurrent)(requestedPackages, REGISTRY_EVALUATION_CONCURRENCY, async (requested) => {
|
|
11
|
+
const priorState = await (0, project_state_1.loadProjectDependencyState)(projectDir, requested.name);
|
|
12
|
+
const priorLifecycleScripts = priorState?.installedVersion && requested.sourceType === "registry"
|
|
13
|
+
? await registryClient.getLifecycleScripts(requested.name, priorState.installedVersion)
|
|
14
|
+
: [];
|
|
15
|
+
const resolvedRegistryPackage = requested.sourceType === "registry"
|
|
16
|
+
? await registryClient.resolvePackage(requested)
|
|
17
|
+
: undefined;
|
|
18
|
+
return (0, policy_1.evaluatePackage)({
|
|
19
|
+
config,
|
|
20
|
+
requested,
|
|
21
|
+
now,
|
|
22
|
+
priorState,
|
|
23
|
+
resolvedRegistryPackage,
|
|
24
|
+
priorLifecycleScripts
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runInitFlow = runInitFlow;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const output_1 = require("./output");
|
|
7
|
+
const project_discovery_1 = require("./project-discovery");
|
|
8
|
+
async function runInitFlow(cwd, argv, options) {
|
|
9
|
+
const configPath = (0, config_1.getConfigPath)(cwd);
|
|
10
|
+
const alreadyExists = await (0, project_discovery_1.fileExists)(configPath);
|
|
11
|
+
if (alreadyExists && !options.force) {
|
|
12
|
+
return {
|
|
13
|
+
mode: "init",
|
|
14
|
+
decision: "error",
|
|
15
|
+
exitCode: 1,
|
|
16
|
+
exitCodeMeaning: "SafeInstall could not create the config file.",
|
|
17
|
+
command: argv,
|
|
18
|
+
commandString: (0, output_1.formatCommand)("safeinstall", argv),
|
|
19
|
+
reasons: [
|
|
20
|
+
{
|
|
21
|
+
code: "config-exists",
|
|
22
|
+
message: `Config already exists at ${configPath}.`,
|
|
23
|
+
suggestion: "Re-run with --force to overwrite it intentionally."
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
summary: "Init failed: config already exists.",
|
|
27
|
+
warnings: [],
|
|
28
|
+
affectedPackages: [],
|
|
29
|
+
details: {
|
|
30
|
+
configPath,
|
|
31
|
+
overwritten: false
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const config = (0, config_1.createDefaultConfig)();
|
|
36
|
+
await (0, promises_1.writeFile)(configPath, (0, config_1.serializeConfig)(config), "utf8");
|
|
37
|
+
return {
|
|
38
|
+
mode: "init",
|
|
39
|
+
decision: "allow",
|
|
40
|
+
exitCode: 0,
|
|
41
|
+
exitCodeMeaning: "Config created successfully.",
|
|
42
|
+
command: argv,
|
|
43
|
+
commandString: (0, output_1.formatCommand)("safeinstall", argv),
|
|
44
|
+
reasons: [],
|
|
45
|
+
summary: alreadyExists ? "Starter config overwritten." : "Starter config created.",
|
|
46
|
+
warnings: [
|
|
47
|
+
"Edit allowedScripts, allowedSources, or allowedPackages only when you intend to trust that exception."
|
|
48
|
+
],
|
|
49
|
+
affectedPackages: [],
|
|
50
|
+
details: {
|
|
51
|
+
configPath,
|
|
52
|
+
overwritten: alreadyExists
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runInstallFlow = runInstallFlow;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const evaluations_1 = require("./evaluations");
|
|
6
|
+
const output_1 = require("./output");
|
|
7
|
+
const package_managers_1 = require("./package-managers");
|
|
8
|
+
const project_state_1 = require("./project-state");
|
|
9
|
+
const project_discovery_1 = require("./project-discovery");
|
|
10
|
+
const project_installs_1 = require("./project-installs");
|
|
11
|
+
const registry_1 = require("./registry");
|
|
12
|
+
const signals_1 = require("./signals");
|
|
13
|
+
const specs_1 = require("./specs");
|
|
14
|
+
function configLabel(configPath) {
|
|
15
|
+
return configPath ?? "built-in defaults";
|
|
16
|
+
}
|
|
17
|
+
function createProjectIssueReason(message) {
|
|
18
|
+
if (message.includes("declares") && message.includes("as packageManager")) {
|
|
19
|
+
return {
|
|
20
|
+
code: "package-manager-mismatch",
|
|
21
|
+
message,
|
|
22
|
+
suggestion: "Run SafeInstall with the declared package manager or update package.json intentionally."
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (message.includes("required for safeinstall")) {
|
|
26
|
+
return {
|
|
27
|
+
code: "lockfile-required",
|
|
28
|
+
message,
|
|
29
|
+
suggestion: "Create or refresh the lockfile before retrying the project install."
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (message.includes("missing from pnpm-lock.yaml") || message.includes("missing from package-lock.json")) {
|
|
33
|
+
return {
|
|
34
|
+
code: "lockfile-missing-entry",
|
|
35
|
+
message,
|
|
36
|
+
suggestion: "Refresh the lockfile so it contains every direct dependency declared in package.json."
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (message.includes("specifier")) {
|
|
40
|
+
return {
|
|
41
|
+
code: "lockfile-specifier-mismatch",
|
|
42
|
+
message,
|
|
43
|
+
suggestion: "Regenerate the lockfile so package.json and the lockfile agree before installing."
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
code: "project-install-blocked",
|
|
48
|
+
message,
|
|
49
|
+
suggestion: "Fix the lockfile inconsistency before retrying."
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function collectTargetPackages(packageDir, effectiveCwd, argv) {
|
|
53
|
+
const plan = (0, specs_1.buildInstallPlan)(argv);
|
|
54
|
+
if (!plan.projectInstall) {
|
|
55
|
+
return { issues: [], plan };
|
|
56
|
+
}
|
|
57
|
+
const lockfileResult = await (0, project_installs_1.loadProjectInstallTargetsForManager)(effectiveCwd, packageDir, plan.manager);
|
|
58
|
+
if (lockfileResult) {
|
|
59
|
+
if (lockfileResult.issues.length > 0) {
|
|
60
|
+
return {
|
|
61
|
+
issues: lockfileResult.issues.map(createProjectIssueReason),
|
|
62
|
+
plan
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
issues: [],
|
|
67
|
+
plan: {
|
|
68
|
+
...plan,
|
|
69
|
+
packages: lockfileResult.targets.map((target) => target.requested)
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const manifestDependencies = await (0, project_state_1.loadManifestDependencies)(packageDir);
|
|
74
|
+
const packages = Object.entries(manifestDependencies).map(([name, spec]) => (0, specs_1.parseManifestDependency)(name, spec));
|
|
75
|
+
return {
|
|
76
|
+
issues: [],
|
|
77
|
+
plan: {
|
|
78
|
+
...plan,
|
|
79
|
+
packages
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function createAffectedPackage(evaluation) {
|
|
84
|
+
return {
|
|
85
|
+
name: evaluation.requested.name,
|
|
86
|
+
requested: evaluation.requested.raw,
|
|
87
|
+
sourceType: evaluation.requested.sourceType,
|
|
88
|
+
resolvedVersion: evaluation.resolvedRegistryPackage?.resolvedVersion,
|
|
89
|
+
reasons: evaluation.blockedReasons,
|
|
90
|
+
warnings: evaluation.warnings
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function runInstallFlow(cwd, argv, options) {
|
|
94
|
+
(0, signals_1.throwIfAborted)(options.signal);
|
|
95
|
+
const rawPlan = (0, specs_1.buildInstallPlan)(argv);
|
|
96
|
+
const invocation = await (0, project_discovery_1.resolveInvocationContext)(cwd, [...rawPlan.managerArgs, ...rawPlan.forwardedArgs]);
|
|
97
|
+
const { config, path } = await (0, config_1.loadConfig)(invocation.effectiveCwd);
|
|
98
|
+
const commandString = (0, output_1.formatCommand)("safeinstall", argv);
|
|
99
|
+
if ((0, project_discovery_1.hasAmbiguousWorkspaceFlags)(rawPlan.manager, [...rawPlan.managerArgs, ...rawPlan.forwardedArgs])) {
|
|
100
|
+
return {
|
|
101
|
+
mode: "install",
|
|
102
|
+
decision: "block",
|
|
103
|
+
exitCode: 2,
|
|
104
|
+
exitCodeMeaning: "Install was blocked because the target workspace is ambiguous.",
|
|
105
|
+
command: argv,
|
|
106
|
+
commandString,
|
|
107
|
+
configPath: path,
|
|
108
|
+
configLabel: configLabel(path),
|
|
109
|
+
packageManager: rawPlan.manager,
|
|
110
|
+
reasons: [
|
|
111
|
+
{
|
|
112
|
+
code: "ambiguous-workspace-target",
|
|
113
|
+
message: "Install blocked: workspace-targeting flags are ambiguous for SafeInstall in this command.",
|
|
114
|
+
suggestion: "Run SafeInstall from the target package directory or use -C/--prefix to point at one package."
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
summary: "Install blocked.",
|
|
118
|
+
warnings: [],
|
|
119
|
+
affectedPackages: [],
|
|
120
|
+
execution: {
|
|
121
|
+
ranPackageManager: false
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (!invocation.packageDir && rawPlan.projectInstall) {
|
|
126
|
+
return {
|
|
127
|
+
mode: "install",
|
|
128
|
+
decision: "block",
|
|
129
|
+
exitCode: 2,
|
|
130
|
+
exitCodeMeaning: "Install was blocked because the current directory does not map to a package.",
|
|
131
|
+
command: argv,
|
|
132
|
+
commandString,
|
|
133
|
+
configPath: path,
|
|
134
|
+
configLabel: configLabel(path),
|
|
135
|
+
packageManager: rawPlan.manager,
|
|
136
|
+
reasons: [
|
|
137
|
+
{
|
|
138
|
+
code: "package-root-not-found",
|
|
139
|
+
message: "Install blocked: the current directory does not map to a package.json-backed project.",
|
|
140
|
+
suggestion: "Run SafeInstall from a package directory or use -C/--prefix to target one package."
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
summary: "Install blocked.",
|
|
144
|
+
warnings: [],
|
|
145
|
+
affectedPackages: [],
|
|
146
|
+
execution: {
|
|
147
|
+
ranPackageManager: false
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const { issues, plan } = await collectTargetPackages(invocation.packageDir ?? invocation.effectiveCwd, invocation.effectiveCwd, argv);
|
|
152
|
+
if (issues.length > 0) {
|
|
153
|
+
return {
|
|
154
|
+
mode: "install",
|
|
155
|
+
decision: "block",
|
|
156
|
+
exitCode: 2,
|
|
157
|
+
exitCodeMeaning: "Install was blocked by policy before the package manager ran.",
|
|
158
|
+
command: argv,
|
|
159
|
+
commandString,
|
|
160
|
+
configPath: path,
|
|
161
|
+
configLabel: configLabel(path),
|
|
162
|
+
packageManager: plan.manager,
|
|
163
|
+
reasons: issues,
|
|
164
|
+
summary: "Install blocked.",
|
|
165
|
+
warnings: [],
|
|
166
|
+
affectedPackages: [],
|
|
167
|
+
execution: {
|
|
168
|
+
ranPackageManager: false
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (plan.packages.length === 0) {
|
|
173
|
+
return {
|
|
174
|
+
mode: "install",
|
|
175
|
+
decision: "error",
|
|
176
|
+
exitCode: 1,
|
|
177
|
+
exitCodeMeaning: "SafeInstall could not determine what to install.",
|
|
178
|
+
command: argv,
|
|
179
|
+
commandString,
|
|
180
|
+
configPath: path,
|
|
181
|
+
configLabel: configLabel(path),
|
|
182
|
+
packageManager: plan.manager,
|
|
183
|
+
reasons: [
|
|
184
|
+
{
|
|
185
|
+
code: "nothing-to-install",
|
|
186
|
+
message: "Nothing to install: no package arguments were provided and package.json has no dependencies."
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
summary: "Install failed: no packages found.",
|
|
190
|
+
warnings: [],
|
|
191
|
+
affectedPackages: [],
|
|
192
|
+
execution: {
|
|
193
|
+
ranPackageManager: false
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const registryClient = new registry_1.RegistryClient({
|
|
198
|
+
registryUrl: config.registryUrl,
|
|
199
|
+
signal: options.signal
|
|
200
|
+
});
|
|
201
|
+
const evaluations = await (0, evaluations_1.evaluateRequestedPackages)(invocation.packageDir ?? invocation.effectiveCwd, plan.packages, registryClient, config);
|
|
202
|
+
const blocked = evaluations.filter((evaluation) => evaluation.blockedReasons.length > 0);
|
|
203
|
+
const warnings = evaluations.flatMap((evaluation) => evaluation.warnings);
|
|
204
|
+
if (blocked.length > 0) {
|
|
205
|
+
return {
|
|
206
|
+
mode: "install",
|
|
207
|
+
decision: "block",
|
|
208
|
+
exitCode: 2,
|
|
209
|
+
exitCodeMeaning: "Install was blocked by policy before the package manager ran.",
|
|
210
|
+
command: argv,
|
|
211
|
+
commandString,
|
|
212
|
+
configPath: path,
|
|
213
|
+
configLabel: configLabel(path),
|
|
214
|
+
packageManager: plan.manager,
|
|
215
|
+
reasons: blocked.flatMap((evaluation) => evaluation.blockedReasons),
|
|
216
|
+
summary: "Install blocked.",
|
|
217
|
+
warnings,
|
|
218
|
+
affectedPackages: blocked.map(createAffectedPackage),
|
|
219
|
+
execution: {
|
|
220
|
+
ranPackageManager: false
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (!options.jsonMode) {
|
|
225
|
+
(0, output_1.printConfigInfo)(path);
|
|
226
|
+
(0, output_1.printWarnings)(evaluations);
|
|
227
|
+
console.error("Allowed: policy checks passed.");
|
|
228
|
+
}
|
|
229
|
+
(0, signals_1.throwIfAborted)(options.signal);
|
|
230
|
+
const execution = await (0, package_managers_1.runPackageManager)({
|
|
231
|
+
manager: plan.manager,
|
|
232
|
+
managerArgs: plan.managerArgs,
|
|
233
|
+
command: plan.command,
|
|
234
|
+
forwardedArgs: plan.forwardedArgs,
|
|
235
|
+
config,
|
|
236
|
+
cwd,
|
|
237
|
+
signal: options.signal,
|
|
238
|
+
stdio: options.jsonMode ? "pipe" : "inherit"
|
|
239
|
+
});
|
|
240
|
+
return {
|
|
241
|
+
mode: "install",
|
|
242
|
+
decision: "allow",
|
|
243
|
+
exitCode: execution.code,
|
|
244
|
+
exitCodeMeaning: execution.code === 0
|
|
245
|
+
? "Policy checks passed and the package manager completed successfully."
|
|
246
|
+
: "Policy checks passed, but the underlying package manager exited non-zero.",
|
|
247
|
+
command: argv,
|
|
248
|
+
commandString,
|
|
249
|
+
configPath: path,
|
|
250
|
+
configLabel: configLabel(path),
|
|
251
|
+
packageManager: plan.manager,
|
|
252
|
+
reasons: [],
|
|
253
|
+
summary: execution.code === 0
|
|
254
|
+
? "Allowed: policy checks passed."
|
|
255
|
+
: `Allowed by policy, but ${plan.manager} exited with code ${execution.code}.`,
|
|
256
|
+
warnings,
|
|
257
|
+
affectedPackages: plan.packages.map((requested) => ({
|
|
258
|
+
name: requested.name,
|
|
259
|
+
requested: requested.raw,
|
|
260
|
+
sourceType: requested.sourceType,
|
|
261
|
+
reasons: [],
|
|
262
|
+
warnings: []
|
|
263
|
+
})),
|
|
264
|
+
execution: {
|
|
265
|
+
ranPackageManager: true,
|
|
266
|
+
packageManagerExitCode: execution.code,
|
|
267
|
+
stdout: execution.stdout,
|
|
268
|
+
stderr: execution.stderr
|
|
269
|
+
},
|
|
270
|
+
details: {
|
|
271
|
+
suppressHumanOutput: !options.jsonMode
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|