Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
00e00b6
feat(cli): Add ui5 cache clean command
d3xter666 Jul 17, 2026
165e1d5
refactor: Remove stale exports
d3xter666 Jul 20, 2026
7127332
test: Add test cases
d3xter666 Jul 20, 2026
039d2ac
fix: Cherry-pick missing methods
d3xter666 Jul 20, 2026
847d71f
docs: Update documentation
d3xter666 Jul 20, 2026
9be8052
docs: Update JSdocs to reflect latest changes
d3xter666 Jul 21, 2026
1775b7f
refactor: Rename orphaned dirs to start with underscore
d3xter666 Jul 21, 2026
1c9f39d
docs: Adjust JSdoc comments
d3xter666 Jul 21, 2026
6afae23
docs: Update JSDoc
d3xter666 Jul 21, 2026
89e915f
fix(cli): Resolve ui5DataDir without shared resolver
d3xter666 Jul 21, 2026
a0444d5
test(cli): Cover cache ui5DataDir precedence
d3xter666 Jul 21, 2026
964b9bf
docs: Adjust documentation
d3xter666 Jul 22, 2026
af2b848
refactor: Align cleaners on common API
d3xter666 Jul 22, 2026
5cf3d53
docs: Adjust Troubleshooting cleanup section
d3xter666 Jul 22, 2026
fa33c70
refactor: DRY cleanups
d3xter666 Jul 22, 2026
f43f72d
test: Add more test cases
d3xter666 Jul 22, 2026
78ec7dd
refactor: Comment stale invocations
d3xter666 Jul 22, 2026
c4e20b3
test: Fix missed stubs
d3xter666 Jul 22, 2026
7ff7198
fix: Address potential race condition during dir rename
d3xter666 Jul 22, 2026
a8f783d
refactor: Long running Db cleanup
d3xter666 Jul 22, 2026
2d6012d
refactor: Optimize Db Vacuum
d3xter666 Jul 22, 2026
cee91c7
refactor: Cleanups and UX alignment
d3xter666 Jul 22, 2026
712bb5f
test: Improve coverage
d3xter666 Jul 22, 2026
d0455ca
docs: Update docs
d3xter666 Jul 22, 2026
8c0abf1
refactor: Remove redundant code
d3xter666 Jul 22, 2026
074e096
fix: Minor Bugs
d3xter666 Jul 23, 2026
29d84be
fix: ESLint issues
d3xter666 Jul 23, 2026
bcaa7fa
refactor: Return statements
d3xter666 Jul 23, 2026
aa59ae8
docs: Fix JSDoc comments
d3xter666 Jul 23, 2026
0e631fa
feat: Add usage warning for cache clean command
d3xter666 Jul 23, 2026
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
30 changes: 19 additions & 11 deletions internal/documentation/docs/pages/Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,36 @@ UI5 CLI stores several kinds of data under your user's home directory in `~/.ui5
| `~/.ui5/buildCache/` | Build cache used by `ui5 build` and `ui5 serve` (see [Build Cache Control](./Builder.md#build-cache-control)) | Yes — rebuilt on next `ui5 build` / `ui5 serve` |
| `~/.ui5/server/` | Locally generated SSL certificate and private key for HTTPS / HTTP/2 mode | Yes — regenerated on next HTTPS server start; the new certificate must be re-trusted |

::: warning
Only remove these directories when no UI5 CLI process and no `@ui5/*` API consumer is actively running. Deleting files that are in use can cause running builds or servers to fail or produce inconsistent results.
:::

#### Resolution

To free disk space, remove the relevant subdirectory.

To only remove framework downloads:
Use the dedicated cache clean command, which 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/`)

If a previous `ui5 cache clean` was interrupted (e.g. process killed or system crash), the command also detects and removes any leftover data from that interrupted operation, listed as separate entries:
- **Orphaned UI5 Framework packages** — incomplete framework directories left over from a previously interrupted cleanup (`~/.ui5/_framework_to_delete_*/`)
- **Orphaned build cache (Db)** — freed database pages not yet reclaimed during a previously interrupted cleanup

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).
:::

::: warning
Only remove these directories, or run `ui5 cache clean`, when no UI5 CLI process and no `@ui5/*` API consumer is actively running. Running `ui5 cache clean` while `ui5 build` or `ui5 serve` is in progress can break the running process and lead to failed or inconsistent results.
Comment thread
d3xter666 marked this conversation as resolved.
:::

## Environment Variables
Expand Down
332 changes: 332 additions & 0 deletions packages/cli/lib/cli/commands/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
import chalk from "chalk";
import path from "node:path";
import os from "node:os";
import process from "node:process";
import baseMiddleware from "../middlewares/base.js";
import Configuration from "@ui5/project/config/Configuration";
import FrameworkCache from "@ui5/project/ui5Framework/cache";
import CacheManager from "@ui5/project/build/cache/CacheManager";

const LABEL_FRAMEWORK = "UI5 Framework packages";
const LABEL_BUILD = "Build cache (Db)";
const LABEL_ORPHANED_FRAMEWORK = "Orphaned UI5 Framework packages";
const LABEL_ORPHANED_BUILD = "Orphaned build cache (Db)";
const CACHE_CLEAN_WARNING =
"Only run ui5 cache clean when no UI5 CLI process and no @ui5/* API consumer is actively running.";
const CACHE_CLEAN_WARNING_IMPACT =
"Running ui5 cache clean while ui5 build or ui5 serve is in progress can break the running process " +
"and lead to failed or inconsistent results.";
const CACHE_CLEAN_HELP_USAGE =
`WARNING: ${CACHE_CLEAN_WARNING}\n${CACHE_CLEAN_WARNING_IMPACT}\n\nUsage: ui5 cache clean [options]`;
// Pad main labels to equal width for two-column alignment (orphaned labels are bold headers, not padded)
const LABEL_WIDTH = Math.max(LABEL_FRAMEWORK.length, LABEL_BUILD.length);

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
.usage(CACHE_CLEAN_HELP_USAGE)
.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");
},
middlewares: [baseMiddleware],
});
};

/**
* 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);
}

function displayCacheCleanWarning() {
process.stderr.write(`${chalk.bold.yellow("Warning:")} ${chalk.italic(CACHE_CLEAN_WARNING)}\n`);
process.stderr.write(`${chalk.italic(CACHE_CLEAN_WARNING_IMPACT)}\n\n`);
}

/**
* Display information about the cached data that will be removed,
* including the absolute paths and details about the framework and build caches.
* Orphaned entries (from previously interrupted cleans) are shown as separate
* items only when present.
*
* @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
* @param {Array<{absPath: string, size: number}>} data.buildAdditionalInfo
*/
async function displayCacheInfo({
frameworkInfo,
buildInfo,
frameworkAbsPath,
buildAbsPath,
buildPreSize,
orphanedInfo,
buildAdditionalInfo,
}) {
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 ? ` (${detail})` : ""}\n`
);
}
if (orphanedInfo?.length > 0) {
process.stderr.write(
` ${chalk.yellow("•")} ${chalk.bold(LABEL_ORPHANED_FRAMEWORK)}\n`
);
for (const orphan of orphanedInfo) {
const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount);
process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`);
}
}
if (buildAdditionalInfo?.length > 0) {
process.stderr.write(
` ${chalk.yellow("•")} ${chalk.bold(LABEL_ORPHANED_BUILD)}\n`
);
for (const entry of buildAdditionalInfo) {
const detail = entry.size > 0 ? formatSize(entry.size) : "";
process.stderr.write(` ${chalk.dim(entry.absPath)}${detail ? ` (${detail})` : ""}\n`);
}
}
process.stderr.write("\n");
}

/**
* Display the result of the cache cleanup operation.
* Orphaned entries are shown as separate items only when present.
*
* @param {object} data
* @param {{libraryCount: number, versionCount: number}|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
* @param {Array<{absPath: string, size: number}>} data.buildAdditionalResult
*/
async function displayCleanupResult({
frameworkResult,
buildResult,
frameworkAbsPath,
buildAbsPath,
buildPreSize,
orphanedInfoWithAbsPaths,
buildAdditionalResult,
}) {
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 (buildResult) {
const detail = buildPreSize > 0 ? formatSize(buildPreSize) : "";
process.stderr.write(
`${chalk.green("✓")} Removed ${chalk.bold(LABEL_BUILD)}` +
` (${buildAbsPath}${detail ? ` · ${detail}` : ""})\n`
);
}
if (orphanedInfoWithAbsPaths?.length > 0) {
process.stderr.write(`${chalk.green("✓")} Removed ${chalk.bold(LABEL_ORPHANED_FRAMEWORK)}\n`);
for (const orphan of orphanedInfoWithAbsPaths) {
const detail = formatFrameworkStats(orphan.libraryCount, orphan.versionCount);
process.stderr.write(` ${chalk.dim(orphan.absPath)} (${detail})\n`);
}
}
if (buildAdditionalResult?.length > 0) {
process.stderr.write(`${chalk.green("✓")} Removed ${chalk.bold(LABEL_ORPHANED_BUILD)}\n`);
for (const entry of buildAdditionalResult) {
const detail = entry.size > 0 ? formatSize(entry.size) : "";
process.stderr.write(` ${chalk.dim(entry.absPath)}${detail ? ` (freed ${detail})` : ""}\n`);
}
}

// Success summary
const cleaned = [];
if (frameworkResult) {
cleaned.push(LABEL_FRAMEWORK);
}
if (buildResult) {
cleaned.push(LABEL_BUILD);
}
if (orphanedInfoWithAbsPaths?.length > 0) {
cleaned.push(LABEL_ORPHANED_FRAMEWORK);
}
if (buildAdditionalResult?.length > 0) {
cleaned.push(LABEL_ORPHANED_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;
}
displayCacheCleanWarning();
const {default: yesno} = await import("yesno");
return yesno({
question: "Do you want to continue? (y/N)",
defaultValue: false
});
}

async function resolveCacheUi5DataDir() {
// TODO: Consolidate ui5DataDir resolution once PR #1456 follow-up cleanup is done.
// Keep behavior aligned with existing main-branch resolution order.
let ui5DataDir = process.env.UI5_DATA_DIR;
if (!ui5DataDir) {
const config = await Configuration.fromFile();
ui5DataDir = config.getUi5DataDir();
}
if (ui5DataDir) {
return path.resolve(process.cwd(), ui5DataDir);
}
return path.join(os.homedir(), ".ui5");
}

async function handleCache(argv) {
const ui5DataDir = await resolveCacheUi5DataDir();

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

const [frameworkInfo, orphanedInfo, buildInfo, buildAdditionalInfo] = await Promise.all([
FrameworkCache.getCacheInfo(ui5DataDir),
FrameworkCache.getAdditionalCacheInfo(ui5DataDir),
CacheManager.getCacheInfo(ui5DataDir),
CacheManager.getAdditionalCacheInfo(ui5DataDir),
]);

if (!frameworkInfo && !buildInfo && orphanedInfo.length === 0 && buildAdditionalInfo.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)})
);
const preCleanBuildAdditionalInfo = buildAdditionalInfo.map(
(o) => ({...o, absPath: path.join(ui5DataDir, o.path)})
);

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

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, additionalBuildResult] = await Promise.all([
FrameworkCache.cleanAdditional(ui5DataDir),
CacheManager.cleanAdditional(ui5DataDir),
]);
const orphanedInfoWithAbsPaths = additionalFrameworkResult.map(
(o) => ({...o, absPath: path.join(ui5DataDir, o.path)})
);
const buildAdditionalResult = preCleanBuildAdditionalInfo.length > 0 ?
additionalBuildResult.map((o) => ({...o, absPath: path.join(ui5DataDir, o.path)})) :
[];

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

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