Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions internal/documentation/docs/pages/Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,27 @@ Only remove these directories when no UI5 CLI process and no `@ui5/*` API consum

#### Resolution

To free disk space, remove the relevant subdirectory.

To only remove framework downloads:
Use the dedicated cache clean command, which safely removes all cached data:

```sh
rm -rf ~/.ui5/framework/
ui5 cache clean
```

To only remove the build cache:
This displays the cache location, the amount of data that gets removed, and asks for confirmation before proceeding. To skip the confirmation prompt (for example in CI environments), use the `--yes` flag:

```sh
rm -rf ~/.ui5/buildCache/
ui5 cache clean --yes
```

The command removes the following cached data:
- **UI5 framework packages** — downloaded UI5 library files (`~/.ui5/framework/`)
- **Build cache (Db)** — build data (`~/.ui5/buildCache/`)
- **Orphaned framework data** — incomplete framework directories left over from previously interrupted cleanup operations (`~/.ui5/.framework_to_delete_*/`)

Any required framework dependencies will be re-downloaded during the next UI5 CLI invocation.

::: info
If you have configured a custom data directory via `UI5_DATA_DIR` or `ui5 config set ui5DataDir`, replace `~/.ui5/` with that path. See [Changing UI5 CLI's Data Directory](#changing-ui5-cli-s-data-directory).
If you have configured a custom data directory via `UI5_DATA_DIR` or `ui5 config set ui5DataDir`, the `ui5 cache clean` command will clean up that location instead of the default `~/.ui5/`. See [Changing UI5 CLI's Data Directory](#changing-ui5-cli-s-data-directory).
:::

## Environment Variables
Expand Down
292 changes: 292 additions & 0 deletions packages/cli/lib/cli/commands/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
import chalk from "chalk";
import path from "node:path";
import process from "node:process";
import baseMiddleware from "../middlewares/base.js";
import {resolveUi5DataDir} from "@ui5/project/utils/dataDir";
import * as frameworkCache from "@ui5/project/ui5Framework/cache";
import CacheManager from "@ui5/project/build/cache/CacheManager";

const cacheCommand = {
command: "cache",
describe: "Manage the UI5 CLI cache (downloaded framework packages and build data)",
middlewares: [baseMiddleware],
handler: handleCache
};

cacheCommand.builder = function(cli) {
return cli
.demandCommand(1, "Command required. Available command is 'clean'")
.command("clean", "Remove all cached UI5 data", {
handler: handleCache,
builder: function(yargs) {
return yargs
.option("yes", {
alias: "y",
describe: "Skip the confirmation prompt, e.g. for use in CI pipelines",
default: false,
type: "boolean",
})
.example("$0 cache clean",
"Remove all cached UI5 data after confirmation")
.example("$0 cache clean --yes",
"Remove all cached UI5 data without confirmation (e.g. in CI scenarios)")
.example("UI5_DATA_DIR=/custom/path $0 cache clean",
"Remove cached data from a non-default UI5 data directory")
.epilogue(
"The cache is stored in the UI5 data directory (default: ~/.ui5).\n" +
"Override the location with the UI5_DATA_DIR environment variable or\n" +
"the 'ui5 config set ui5DataDir' configuration option (see 'ui5 config --help').\n\n" +
"The following cache types are removed:\n" +
" UI5 framework packages: Downloaded UI5 library files " +
"(~/.ui5/framework/)\n" +
" Build cache (Db): Build data " +
"(~/.ui5/buildCache/)\n" +
" Orphaned framework data: Incomplete directories from previously interrupted cleanups\n" +
" (~/.ui5/_framework_to_delete_*/)"
);
},
middlewares: [baseMiddleware],
});
};

const LABEL_FRAMEWORK = "UI5 Framework packages";
const LABEL_BUILD = "Build cache (Db)";
// Pad labels to equal width for two-column alignment
const LABEL_WIDTH = Math.max(LABEL_FRAMEWORK.length, LABEL_BUILD.length);

/**
* Format a byte size as a human-readable string.
*
* @param {number} bytes Size in bytes
* @returns {string} Formatted size string
*/
function formatSize(bytes) {
if (bytes < 1024) {
return `${bytes} B`;
} else if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
} else if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}

/**
* Format framework cache stats as a human-readable detail string.
* E.g. "1,189 versions of 155 libraries" or "1 version of 1 library".
*
* @param {number} libraryCount
* @param {number} versionCount
* @returns {string}
*/
function formatFrameworkStats(libraryCount, versionCount) {
const v = `${versionCount.toLocaleString("en-US")} ${versionCount === 1 ? "version" : "versions"}`;
const l = `${libraryCount.toLocaleString("en-US")} ${libraryCount === 1 ? "library" : "libraries"}`;
return `${v} of ${l}`;
}

/**
* Pad a label to the shared column width.
*
* @param {string} label
* @returns {string}
*/
function padLabel(label) {
return label.padEnd(LABEL_WIDTH);
}

/**
* Display information about the cached data that will be removed,
* including the absolute paths and details about the framework and build caches,
* and any orphaned staging directories from previously interrupted clean operations.
*
* @param {object} data
* @param {object|null} data.frameworkInfo
* @param {object|null} data.buildInfo
* @param {string|null} data.frameworkAbsPath
* @param {string|null} data.buildAbsPath
* @param {number} data.buildPreSize
* @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.orphanedInfo
*/
async function displayCacheInfo({
frameworkInfo,
buildInfo,
frameworkAbsPath,
buildAbsPath,
buildPreSize,
orphanedInfo,
}) {
process.stderr.write(chalk.bold("\nThe following cached data will be removed:\n\n"));
if (frameworkInfo) {
const detail = formatFrameworkStats(frameworkInfo.libraryCount, frameworkInfo.versionCount);
process.stderr.write(
` ${chalk.yellow("•")} ${padLabel(LABEL_FRAMEWORK)} ${frameworkAbsPath} (${detail})\n`
);
}
if (buildInfo) {
const detail = buildPreSize > 0 ? formatSize(buildPreSize) : "";
process.stderr.write(
` ${chalk.yellow("•")} ${padLabel(LABEL_BUILD)} ${buildAbsPath} (${detail})\n`
);
}
if (orphanedInfo && orphanedInfo.length > 0) {
process.stderr.write(
` ${chalk.yellow("•")} ${chalk.bold("Orphaned framework data")}` +
` (incomplete previous clean — ` +
`${orphanedInfo.length} director${orphanedInfo.length === 1 ? "y" : "ies"})\n`
);
for (const orphan of orphanedInfo) {
const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount);
process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`);
}
}
process.stderr.write("\n");
}

/**
* Display the result of the cache cleanup operation,
* including which caches were removed and their details,
* and any orphaned staging directories that were also cleaned up.
*
* @param {object} data
* @param {object|null} data.frameworkResult
* @param {object|null} data.buildResult
* @param {string|null} data.frameworkAbsPath
* @param {string|null} data.buildAbsPath
* @param {number} data.buildPreSize
* @param {Array<{absPath: string, libraryCount: number, versionCount: number}>} data.orphanedInfoWithAbsPaths
*/
async function displayCleanupResult({
frameworkResult,
buildResult,
frameworkAbsPath,
buildAbsPath,
buildPreSize,
orphanedInfoWithAbsPaths,
}) {
process.stderr.write("\n");
if (frameworkResult && frameworkAbsPath) {
const detail = formatFrameworkStats(
frameworkResult.libraryCount,
frameworkResult.versionCount,
);
process.stderr.write(
`${chalk.green("✓")} Removed ${chalk.bold(LABEL_FRAMEWORK)}` +
` (${frameworkAbsPath} · ${detail})\n`,
);
}
if (orphanedInfoWithAbsPaths && orphanedInfoWithAbsPaths.length > 0) {
process.stderr.write(
`${chalk.green("✓")} Removed ${chalk.bold("Orphaned framework data")}` +
` (${orphanedInfoWithAbsPaths.length}` +
` director${orphanedInfoWithAbsPaths.length === 1 ? "y" : "ies"})\n`
);
for (const orphan of orphanedInfoWithAbsPaths) {
const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount);
process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`);
}
}
if (buildResult) {
const detail = buildPreSize > 0 ? formatSize(buildPreSize) : "";
process.stderr.write(
`${chalk.green("✓")} Removed ${chalk.bold(LABEL_BUILD)}` +
` (${buildAbsPath}${detail ? ` · ${detail}` : ""})\n`,
);
}

// Success summary
const cleaned = [];
if (frameworkResult) {
cleaned.push(LABEL_FRAMEWORK);
}
if (orphanedInfoWithAbsPaths && orphanedInfoWithAbsPaths.length > 0) {
cleaned.push("Orphaned framework data");
}
if (buildResult) {
cleaned.push(LABEL_BUILD);
}
process.stderr.write(
`\n${chalk.green("Success:")} Cleaned ${cleaned.join(" and ")}\n`,
);
}

/**
* Prompt the user for confirmation before proceeding with cache cleanup.
*
* @param {Yargs.Arguments} argv
* @returns {Promise<boolean>} Confirmation result
*/
async function getConfirmation(argv) {
if (argv.yes) {
return true;
}
const {default: yesno} = await import("yesno");
return yesno({
question: "Do you want to continue? (y/N)",
defaultValue: false
});
}

async function handleCache(argv) {
const ui5DataDir = await resolveUi5DataDir({projectRootPath: process.cwd()});

process.stderr.write(`Checking cache at ${chalk.bold(ui5DataDir)} …\n`);

const [frameworkInfo, buildInfo, orphanedInfo] = await Promise.all([
frameworkCache.getCacheInfo(ui5DataDir),
CacheManager.getCacheInfo(ui5DataDir),
frameworkCache.getOrphanedInfo(ui5DataDir),
]);

if (!frameworkInfo && !buildInfo && orphanedInfo.length === 0) {
process.stderr.write("Nothing to clean\n");
return;
}

// Compute absolute paths once — producers return relative sub-path segments
const frameworkAbsPath = frameworkInfo ? path.join(ui5DataDir, frameworkInfo.path) : null;
const buildAbsPath = buildInfo ? path.join(ui5DataDir, buildInfo.path) : null;
const buildPreSize = buildInfo?.size ?? 0;
const preCleanOrphanedInfo = orphanedInfo.map(
(o) => ({...o, absPath: path.join(ui5DataDir, o.path)})
);

await displayCacheInfo({
frameworkInfo,
buildInfo,
frameworkAbsPath,
buildAbsPath,
buildPreSize,
orphanedInfo: preCleanOrphanedInfo,
});

const confirmed = await getConfirmation(argv);
if (!confirmed) {
process.stderr.write("Cancelled\n");
return;
}

const [frameworkResult, buildResult] = await Promise.all([
frameworkCache.cleanCache(ui5DataDir),
CacheManager.cleanCache(ui5DataDir),
]);

const [additionalFrameworkResult] = await Promise.all([
frameworkCache.cleanAdditional(ui5DataDir),
CacheManager.cleanAdditional(ui5DataDir),
]);
const orphanedInfoWithAbsPaths = additionalFrameworkResult.map(
(o) => ({...o, absPath: path.join(ui5DataDir, o.path)})
);

await displayCleanupResult({
frameworkResult,
buildResult,
frameworkAbsPath,
buildAbsPath,
buildPreSize,
orphanedInfoWithAbsPaths,
});
}

export default cacheCommand;
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"pretty-hrtime": "^1.0.3",
"semver": "^7.8.5",
"update-notifier": "^7.3.1",
"yargs": "^18.0.0"
"yargs": "^18.0.0",
"yesno": "^0.4.0"
},
"devDependencies": {
"@istanbuljs/esm-loader-hook": "^0.3.0",
Expand Down
Loading
Loading