nx
Version:
730 lines (729 loc) • 30.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.showTargetInfoHandler = showTargetInfoHandler;
exports.showTargetInputsHandler = showTargetInputsHandler;
exports.showTargetOutputsHandler = showTargetOutputsHandler;
const path_1 = require("path");
const calculate_default_project_name_1 = require("../../config/calculate-default-project-name");
const configuration_1 = require("../../config/configuration");
const project_graph_1 = require("../../project-graph/project-graph");
const task_hasher_1 = require("../../hasher/task-hasher");
const utils_1 = require("../../tasks-runner/utils");
const find_matching_projects_1 = require("../../utils/find-matching-projects");
const output_1 = require("../../utils/output");
const split_target_1 = require("../../utils/split-target");
const workspace_root_1 = require("../../utils/workspace-root");
// ── Entry points ─────────────────────────────────────────────────────
async function showTargetInfoHandler(args) {
performance.mark('code-loading:end');
performance.measure('code-loading', 'init-local', 'code-loading:end');
const graph = await (0, project_graph_1.createProjectGraphAsync)();
const nxJson = (0, configuration_1.readNxJson)();
const { projectName, targetName, configurationName } = resolveTargetIdentifier(args, graph, nxJson);
const node = resolveProjectNode(projectName, graph);
const targetConfig = node.data.targets?.[targetName];
if (!targetConfig) {
return reportTargetNotFound(projectName, targetName, node);
}
const configuration = configurationName ?? args.configuration;
if (configuration) {
validateConfiguration(projectName, targetName, configuration, targetConfig);
}
const data = resolveTargetInfoData(projectName, targetName, configuration, node, graph, nxJson);
renderTargetInfo(data, args);
}
async function showTargetInputsHandler(args) {
performance.mark('code-loading:end');
performance.measure('code-loading', 'init-local', 'code-loading:end');
const graph = await (0, project_graph_1.createProjectGraphAsync)();
const nxJson = (0, configuration_1.readNxJson)();
const { projectName, targetName, configurationName } = resolveTargetIdentifier(args, graph, nxJson);
const node = resolveProjectNode(projectName, graph);
const targetConfig = node.data.targets?.[targetName];
if (!targetConfig) {
return reportTargetNotFound(projectName, targetName, node);
}
const configuration = configurationName ?? args.configuration;
const hashInputs = await resolveInputFiles(projectName, targetName, configuration, graph, nxJson);
if (args.check !== undefined) {
const checkItems = deduplicateFolderEntries(args.check);
const results = checkItems.map((input) => resolveCheckFromInputs(input, projectName, targetName, hashInputs));
if (results.length >= 2) {
renderBatchCheckInputs(results, projectName, targetName);
}
else {
for (const data of results) {
renderCheckInput(data);
}
}
for (const data of results) {
process.exitCode ||=
data.isInput || data.containedInputFiles.length ? 0 : 1;
}
return;
}
const configuredInputs = targetConfig.inputs;
renderInputs({ project: projectName, target: targetName, ...hashInputs }, configuredInputs, args);
}
async function showTargetOutputsHandler(args) {
performance.mark('code-loading:end');
performance.measure('code-loading', 'init-local', 'code-loading:end');
const graph = await (0, project_graph_1.createProjectGraphAsync)();
const nxJson = (0, configuration_1.readNxJson)();
const { projectName, targetName, configurationName } = resolveTargetIdentifier(args, graph, nxJson);
const node = resolveProjectNode(projectName, graph);
const targetConfig = node.data.targets?.[targetName];
if (!targetConfig) {
return reportTargetNotFound(projectName, targetName, node);
}
const configuration = configurationName ?? args.configuration;
if (configuration) {
validateConfiguration(projectName, targetName, configuration, targetConfig);
}
const outputsData = resolveOutputsData(projectName, targetName, configuration, node);
if (args.check !== undefined) {
const checkItems = deduplicateFolderEntries(args.check);
const results = checkItems.map((output) => resolveCheckOutputData(output, outputsData));
if (results.length >= 2) {
renderBatchCheckOutputs(results, outputsData.project, outputsData.target);
}
else {
for (const data of results) {
renderCheckOutput(data);
}
}
for (const data of results) {
process.exitCode ||=
data.matchedOutput ||
data.containedOutputPaths.length ||
data.containedExpandedOutputs.length
? 0
: 1;
}
return;
}
renderOutputs(outputsData, args);
}
// ── Target identifier & project resolution ───────────────────────────
function resolveTargetIdentifier(args, graph, nxJson) {
if (!args.target) {
output_1.output.error({
title: 'No target specified.',
bodyLines: [
`Please specify a target using:`,
` nx show target <project:target>`,
` nx show target <target> (infers project from cwd)`,
],
});
process.exit(1);
}
const defaultProjectName = (0, calculate_default_project_name_1.calculateDefaultProjectName)(process.cwd(), workspace_root_1.workspaceRoot, (0, project_graph_1.readProjectsConfigurationFromProjectGraph)(graph), nxJson);
const [project, target, config] = (0, split_target_1.splitTarget)(args.target, graph, {
currentProject: defaultProjectName,
});
if (project && target) {
return {
projectName: project,
targetName: target,
configurationName: config,
};
}
const targetName = project; // splitTarget returns the string as the first element
const projectName = defaultProjectName;
if (!projectName) {
output_1.output.error({
title: `Could not infer project from the current working directory.`,
bodyLines: [
`Please specify the project explicitly:`,
` nx show target <project>:${targetName}`,
``,
`Or run this command from within a project directory.`,
],
});
process.exit(1);
}
return { projectName, targetName };
}
function resolveProjectNode(projectName, graph) {
let node = graph.nodes[projectName];
if (!node) {
const projects = (0, find_matching_projects_1.findMatchingProjects)([projectName], graph.nodes);
if (projects.length === 1) {
node = graph.nodes[projects[0]];
}
else if (projects.length > 1) {
output_1.output.error({
title: `Multiple projects matched "${projectName}":`,
bodyLines: projects.length > 100 ? [...projects.slice(0, 100), '...'] : projects,
});
process.exit(1);
}
else {
output_1.output.error({
title: `Could not find project "${projectName}".`,
});
process.exit(1);
}
}
return node;
}
function reportTargetNotFound(projectName, targetName, node) {
const availableTargets = Object.keys(node.data.targets ?? {});
output_1.output.error({
title: `Target "${targetName}" not found for project "${projectName}".`,
bodyLines: availableTargets.length
? [`Available targets:`, ...availableTargets.map((t) => ` - ${t}`)]
: [`This project has no targets configured.`],
});
process.exit(1);
}
function validateConfiguration(projectName, targetName, configuration, targetConfig) {
const availableConfigs = Object.keys(targetConfig.configurations ?? {});
if (!availableConfigs.includes(configuration)) {
output_1.output.error({
title: `Configuration "${configuration}" not found for target "${projectName}:${targetName}".`,
bodyLines: availableConfigs.length
? [
`Available configurations:`,
...availableConfigs.map((c) => ` - ${c}`),
]
: [`This target has no configurations.`],
});
process.exit(1);
}
}
// ── Data resolvers ───────────────────────────────────────────────────
function resolveTargetInfoData(projectName, targetName, configuration, node, graph, nxJson) {
const targetConfig = node.data.targets[targetName];
const allTargetNames = new Set();
for (const n of Object.values(graph.nodes)) {
for (const t of Object.keys(n.data.targets ?? {})) {
allTargetNames.add(t);
}
}
const extraTargetDeps = Object.fromEntries(Object.entries(nxJson.targetDefaults ?? {})
.filter(([, config]) => config.dependsOn)
.map(([name, config]) => [name, config.dependsOn]));
const depConfigs = (0, utils_1.getDependencyConfigs)({ project: projectName, target: targetName }, extraTargetDeps, graph, [...allTargetNames]);
const configurations = Object.keys(targetConfig.configurations ?? {});
const command = targetConfig.metadata?.scriptContent ??
targetConfig.options?.command ??
(targetConfig.options?.commands?.length === 1
? targetConfig.options.commands[0]
: undefined);
const dependsOn = depConfigs && depConfigs.length > 0
? depConfigs.flatMap((dep) => {
const projects = resolveDependencyProjects(dep, projectName, graph);
return projects.map((p) => `${p}:${dep.target}`);
})
: undefined;
return {
project: projectName,
target: targetName,
...(configuration ? { configuration } : {}),
executor: targetConfig.executor,
...(command ? { command } : {}),
options: {
...targetConfig.options,
...(configuration
? targetConfig.configurations?.[configuration]
: undefined),
},
...(targetConfig.inputs
? { inputs: expandInputsForDisplay(targetConfig.inputs, node, nxJson) }
: {}),
...(targetConfig.outputs
? { outputs: targetConfig.outputs }
: {}),
...(dependsOn ? { dependsOn } : {}),
...(configurations.length > 0 ? { configurations } : {}),
...(targetConfig.defaultConfiguration
? { defaultConfiguration: targetConfig.defaultConfiguration }
: {}),
cache: targetConfig.cache ?? false,
parallelism: targetConfig.parallelism ?? true,
continuous: targetConfig.continuous ?? false,
};
}
/**
* Uses the HashPlanInspector (native hash planner) to resolve the complete
* set of inputs that affect a task's cache hash. Returns structured HashInputs
* with files, runtime, environment, depOutputs, and external arrays.
*/
async function resolveInputFiles(projectName, targetName, configuration, graph, nxJson) {
const { HashPlanInspector } = (await Promise.resolve().then(() => require('../../hasher/hash-plan-inspector')));
const inspector = new HashPlanInspector(graph, workspace_root_1.workspaceRoot, nxJson);
await inspector.init();
const plan = inspector.inspectTaskInputs({
project: projectName,
target: targetName,
configuration,
});
const targetConfig = graph.nodes[projectName]?.data?.targets?.[targetName];
const effectiveConfig = configuration ?? targetConfig?.defaultConfiguration;
const taskId = effectiveConfig
? `${projectName}:${targetName}:${effectiveConfig}`
: `${projectName}:${targetName}`;
const result = plan[taskId];
if (!result) {
throw new Error(`Could not find hash plan for task "${taskId}". Available tasks: ${Object.keys(plan).join(', ')}`);
}
return result;
}
function resolveCheckFromInputs(rawValue, projectName, targetName, inputs) {
// Check non-file categories first (exact match on raw value)
const isEnvironment = inputs.environment.includes(rawValue);
const isRuntime = inputs.runtime.includes(rawValue);
const isExternal = inputs.external.includes(rawValue);
const isDepOutput = inputs.depOutputs.includes(rawValue);
if (isEnvironment || isRuntime || isExternal || isDepOutput) {
const matchedCategory = isEnvironment
? 'environment'
: isRuntime
? 'runtime'
: isExternal
? 'external'
: 'depOutputs';
return {
value: rawValue,
file: rawValue,
project: projectName,
target: targetName,
isInput: true,
matchedCategory,
containedInputFiles: [],
};
}
// Resolve path relative to cwd for file checking
const fileToCheck = normalizePath(rawValue);
const isFile = inputs.files.includes(fileToCheck);
let containedInputFiles = [];
if (!isFile) {
// Empty string means workspace root — all files are contained
if (fileToCheck === '') {
containedInputFiles = inputs.files;
}
else {
const dirPrefix = fileToCheck.endsWith('/')
? fileToCheck
: fileToCheck + '/';
containedInputFiles = inputs.files.filter((f) => f.startsWith(dirPrefix));
}
}
return {
value: rawValue,
file: fileToCheck,
project: projectName,
target: targetName,
isInput: isFile,
matchedCategory: isFile
? 'files'
: containedInputFiles.length > 0
? 'files'
: null,
containedInputFiles,
};
}
function resolveOutputsData(projectName, targetName, configuration, node) {
const resolvedOutputs = (0, utils_1.getOutputsForTargetAndConfiguration)({ project: projectName, target: targetName, configuration }, {}, node);
// Detect outputs containing {options.*} that were dropped because the
// referenced option is not set. getOutputsForTargetAndConfiguration silently
// omits these — the resolved list will have fewer entries than configured.
const targetConfig = node.data.targets?.[targetName];
const configuredOutputs = targetConfig?.outputs ?? [];
const mergedOptions = {
...targetConfig?.options,
...(configuration
? targetConfig?.configurations?.[configuration]
: undefined),
};
const unresolvedOutputs = configuredOutputs.filter((o) => {
if (!/\{options\./.test(o))
return false;
const unresolved = o.match(/\{options\.([^}]+)\}/g);
return unresolved?.some((token) => {
const key = token.slice('{options.'.length, -1);
return mergedOptions[key] === undefined;
});
});
let expandedOutputs;
try {
const { expandOutputs } = require('../../native');
expandedOutputs = expandOutputs(workspace_root_1.workspaceRoot, resolvedOutputs);
}
catch {
expandedOutputs = resolvedOutputs;
}
return {
project: projectName,
target: targetName,
outputPaths: resolvedOutputs,
expandedOutputs,
unresolvedOutputs,
};
}
function resolveCheckOutputData(rawFileToCheck, outputsData) {
const fileToCheck = normalizePath(rawFileToCheck);
const { outputPaths, expandedOutputs } = outputsData;
// Check if the file is an output — try configured paths first (prefix match
// covers directory outputs), then fall back to expanded outputs (handles globs).
let matchedOutput = null;
for (const outputPath of outputPaths) {
const normalizedOutput = outputPath.replace(/\\/g, '/');
if (fileToCheck === normalizedOutput ||
fileToCheck.startsWith(normalizedOutput + '/')) {
matchedOutput = outputPath;
break;
}
}
if (!matchedOutput && expandedOutputs.includes(fileToCheck)) {
matchedOutput = fileToCheck;
}
// If the path isn't directly an output, check if it's a directory containing outputs
let containedOutputPaths = [];
let containedExpandedOutputs = [];
if (!matchedOutput) {
if (fileToCheck === '') {
containedOutputPaths = [...outputPaths];
containedExpandedOutputs = [...expandedOutputs];
}
else {
const dirPrefix = fileToCheck.endsWith('/')
? fileToCheck
: fileToCheck + '/';
containedOutputPaths = outputPaths.filter((o) => o.replace(/\\/g, '/').startsWith(dirPrefix));
containedExpandedOutputs = expandedOutputs.filter((o) => o.replace(/\\/g, '/').startsWith(dirPrefix));
}
}
return {
value: rawFileToCheck,
file: fileToCheck,
project: outputsData.project,
target: outputsData.target,
matchedOutput,
containedOutputPaths,
containedExpandedOutputs,
};
}
// ── Renderers ────────────────────────────────────────────────────────
function renderTargetInfo(data, args) {
if (args.json) {
console.log(JSON.stringify(data, null, 2));
return;
}
const c = pc();
console.log(`${c.bold('Target')}: ${c.cyan(data.project)}:${c.green(data.target)}`);
if (data.executor)
console.log(`${c.bold('Executor')}: ${data.executor}`);
if (data.command)
console.log(`${c.bold('Command')}: ${data.command}`);
if (data.configuration)
console.log(`${c.bold('Configuration')}: ${data.configuration}`);
if (Object.keys(data.options).length > 0) {
console.log(`${c.bold('Options')}:`);
for (const [key, value] of Object.entries(data.options)) {
console.log(` ${key}: ${JSON.stringify(value)}`);
}
}
if (data.inputs && data.inputs.length > 0) {
console.log(`${c.bold('Inputs')}:`);
const sortedInputs = [...data.inputs].sort((a, b) => {
const aIsString = typeof a === 'string';
const bIsString = typeof b === 'string';
// Objects come after strings
if (!aIsString && bIsString)
return 1;
if (aIsString && !bIsString)
return -1;
// Both are strings
if (aIsString && bIsString) {
const aIsDep = a.startsWith('^');
const bIsDep = b.startsWith('^');
// Dependency inputs (^) come after regular inputs
if (aIsDep && !bIsDep)
return 1;
if (!aIsDep && bIsDep)
return -1;
return a < b ? -1 : a > b ? 1 : 0;
}
return 0;
});
for (const input of sortedInputs) {
console.log(` - ${typeof input === 'string' ? input : JSON.stringify(input)}`);
}
}
if (data.outputs && data.outputs.length > 0) {
console.log(`${c.bold('Outputs')}:`);
for (const o of data.outputs)
console.log(` - ${o}`);
}
if (data.dependsOn && data.dependsOn.length > 0) {
console.log(`${c.bold('Depends On')}:`);
for (const taskId of data.dependsOn) {
console.log(` ${taskId}`);
}
}
if (data.configurations && data.configurations.length > 0) {
console.log(`${c.bold('Configurations')}: ${data.configurations.join(', ')}${data.defaultConfiguration
? ` (default: ${data.defaultConfiguration})`
: ''}`);
}
console.log(`${c.bold('Cache')}: ${data.cache}`);
console.log(`${c.bold('Parallelism')}: ${data.parallelism}`);
console.log(`${c.bold('Continuous')}: ${data.continuous}`);
}
function renderInputs(data, configuredInputs, args) {
if (args.json) {
const jsonData = data;
// Inline omitEmptyArrays
const result = {};
for (const [k, v] of Object.entries(jsonData)) {
if (Array.isArray(v) && v.length === 0)
continue;
result[k] = v;
}
console.log(JSON.stringify(result, null, 2));
return;
}
const c = pc();
console.log(`${c.bold('Inputs for')} ${c.cyan(data.project)}:${c.green(data.target)}`);
// Show configured input groups (named inputs like "production", "^production")
if (configuredInputs && configuredInputs.length > 0) {
printList('Configured inputs', configuredInputs.map((i) => typeof i === 'string' ? i : JSON.stringify(i)));
}
printList('External dependencies', [...data.external].sort());
printList('Runtime inputs', [...data.runtime].sort());
printList('Environment variables', [...data.environment].sort());
printList(`Files (${data.files.length})`, [...data.files, ...data.depOutputs].sort());
}
function renderCheckInput(data) {
const c = pc();
const categoryLabel = data.matchedCategory
? ` (${data.matchedCategory})`
: '';
if (data.isInput) {
console.log(`${c.green('✓')} ${c.bold(data.value)} is an input for ${c.cyan(data.project)}:${c.green(data.target)}${categoryLabel}`);
}
else if (data.containedInputFiles.length > 0) {
console.log(`${c.yellow('~')} ${c.bold(data.file)} is a directory containing ${c.bold(String(data.containedInputFiles.length))} input file(s) for ${c.cyan(data.project)}:${c.green(data.target)}`);
for (const f of [...data.containedInputFiles].sort())
console.log(` ${f}`);
}
else {
console.log(`${c.red('✗')} ${c.bold(data.value)} is ${c.red('not')} an input for ${c.cyan(data.project)}:${c.green(data.target)}`);
}
}
function renderOutputs(data, args) {
if (args.json) {
// Inline omitEmptyArrays
const jsonData = data;
const result = {};
for (const [k, v] of Object.entries(jsonData)) {
if (Array.isArray(v) && v.length === 0)
continue;
result[k] = v;
}
console.log(JSON.stringify(result, null, 2));
return;
}
const c = pc();
console.log(`${c.bold('Output paths for')} ${c.cyan(data.project)}:${c.green(data.target)}`);
if (data.outputPaths.length > 0) {
printList('Configured outputs', data.outputPaths);
}
if (data.expandedOutputs.length > 0) {
printList('Resolved outputs', data.expandedOutputs);
}
if (data.unresolvedOutputs.length > 0) {
printList(`${c.yellow('Unresolved outputs')} (option not set)`, data.unresolvedOutputs);
}
if (data.outputPaths.length === 0 && data.unresolvedOutputs.length === 0) {
console.log(`\n No outputs configured for this target.`);
}
}
function renderCheckOutput(data) {
const isDirectoryContainingOutputs = data.containedOutputPaths.length > 0 ||
data.containedExpandedOutputs.length > 0;
const c = pc();
const displayPath = data.value || data.file;
if (data.matchedOutput) {
console.log(`${c.green('✓')} ${c.bold(displayPath)} is an output of ${c.cyan(data.project)}:${c.green(data.target)}`);
}
else if (isDirectoryContainingOutputs) {
const uniquePaths = new Set([
...data.containedOutputPaths,
...data.containedExpandedOutputs,
]);
console.log(`${c.yellow('~')} ${c.bold(displayPath)} is a directory containing ${c.bold(String(uniquePaths.size))} output path(s) for ${c.cyan(data.project)}:${c.green(data.target)}`);
// Only show expanded outputs (not configured paths) per review feedback
const extraExpanded = data.containedExpandedOutputs.filter((o) => !data.containedOutputPaths.includes(o));
if (extraExpanded.length > 0) {
printList('Expanded outputs', extraExpanded);
}
}
else {
console.log(`${c.red('✗')} ${c.bold(displayPath)} is ${c.red('not')} an output of ${c.cyan(data.project)}:${c.green(data.target)}`);
}
}
// ── Helpers ──────────────────────────────────────────────────────────
/**
* Expands named inputs (e.g. "production", "default") to their definitions
* so the user can see what each named input resolves to. Inputs prefixed
* with "^" (dependency inputs) are kept as-is since they reference inputs
* from dependent projects. Object-type inputs pass through unchanged.
*/
function expandInputsForDisplay(inputs, node, nxJson) {
const namedInputs = (0, task_hasher_1.getNamedInputs)(nxJson, node);
const result = [];
for (const input of inputs) {
if (typeof input === 'string') {
if (input.startsWith('^')) {
// Dependency input — keep as-is
result.push(input);
}
else if (namedInputs[input]) {
// Named input — expand inline
result.push(...namedInputs[input]);
}
else {
result.push(input);
}
}
else if ('input' in input) {
// Object with { input: "namedInput" } — expand the reference
const name = input.input;
if (!name.startsWith('^') && namedInputs[name]) {
result.push(...namedInputs[name]);
}
else {
result.push(input);
}
}
else {
result.push(input);
}
}
return result;
}
/**
* When shell glob expansion is used, both directories and files within
* those directories may appear. Remove any directory entry whose children
* are already present in the list.
*/
function deduplicateFolderEntries(items) {
const normalized = items.map((item) => ({
original: item,
path: normalizePath(item),
}));
return normalized
.filter(({ path }) => {
const dirPrefix = path.endsWith('/') ? path : path + '/';
const hasChildInList = normalized.some((other) => other.path !== path && other.path.startsWith(dirPrefix));
return !hasChildInList;
})
.map(({ original }) => original);
}
function renderBatchCheckInputs(results, projectName, targetName) {
const matched = [];
const directories = [];
const unmatched = [];
for (const data of results) {
if (data.isInput) {
matched.push(data.value);
}
else if (data.containedInputFiles.length > 0) {
directories.push({
value: data.file,
count: data.containedInputFiles.length,
});
}
else {
unmatched.push(data.value);
}
}
const c = pc();
const label = `${c.cyan(projectName)}:${c.green(targetName)}`;
if (matched.length > 0 || directories.length > 0) {
console.log(`\n${c.green('✓')} These arguments were inputs for ${label}:`);
for (const v of matched)
console.log(` ${v}`);
for (const d of directories) {
console.log(` ${d.value} (directory containing ${d.count} input files)`);
}
}
if (unmatched.length > 0) {
console.log(`\n${c.red('✗')} These arguments were ${c.red('not')} inputs for ${label}:`);
for (const v of unmatched)
console.log(` ${v}`);
}
}
function renderBatchCheckOutputs(results, projectName, targetName) {
const matched = [];
const directories = [];
const unmatched = [];
for (const data of results) {
if (data.matchedOutput) {
matched.push(data.value);
}
else if (data.containedOutputPaths.length > 0 ||
data.containedExpandedOutputs.length > 0) {
const uniqueCount = new Set([
...data.containedOutputPaths,
...data.containedExpandedOutputs,
]).size;
directories.push({ value: data.file, count: uniqueCount });
}
else {
unmatched.push(data.value);
}
}
const c = pc();
const label = `${c.cyan(projectName)}:${c.green(targetName)}`;
if (matched.length > 0 || directories.length > 0) {
console.log(`\n${c.green('✓')} These arguments were outputs of ${label}:`);
for (const v of matched)
console.log(` ${v}`);
for (const d of directories) {
console.log(` ${d.value} (directory containing ${d.count} output paths)`);
}
}
if (unmatched.length > 0) {
console.log(`\n${c.red('✗')} These arguments were ${c.red('not')} outputs of ${label}:`);
for (const v of unmatched)
console.log(` ${v}`);
}
}
function resolveDependencyProjects(dep, projectName, graph) {
if (dep.projects && dep.projects.length > 0)
return dep.projects;
if (dep.dependencies) {
const depEdges = graph.dependencies[projectName] ?? [];
return depEdges
.filter((edge) => {
const depNode = graph.nodes[edge.target];
return depNode && depNode.data.targets?.[dep.target];
})
.map((edge) => edge.target);
}
return [projectName];
}
/**
* Converts a user-provided path into a workspace-relative path for comparison
* against project file maps. Resolves relative to process.cwd() so that
* --check arguments work correctly from any directory.
*/
function normalizePath(p) {
const absolute = (0, path_1.resolve)(process.cwd(), p);
return (0, path_1.relative)(workspace_root_1.workspaceRoot, absolute).replace(/\\/g, '/');
}
let _pc;
function pc() {
return (_pc ??= require('picocolors'));
}
function printList(header, items, prefix = '\n') {
if (items.length === 0)
return;
console.log(`${prefix}${pc().bold(header)}:`);
for (const item of items)
console.log(` ${item}`);
}