cdk-agc 1.1.0 → 1.2.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/README.md +13 -15
- package/dist/cleanup.d.mts.map +1 -1
- package/dist/cleanup.mjs +17 -45
- package/dist/cleanup.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,13 +7,12 @@
|
|
|
7
7
|
`cdk-agc` is a fast CLI tool that scans your AWS CDK cloud assembly directory and helps you reclaim disk space:
|
|
8
8
|
|
|
9
9
|
- **Clean `cdk.out` directories**: Remove unused assets while protecting referenced files
|
|
10
|
-
-
|
|
10
|
+
- Only deletes unreferenced `asset.*` directories - all other files are automatically protected
|
|
11
|
+
- Protects recently modified files (configurable with `-k/--keep-hours`)
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
- Recently modified files (configurable retention period)
|
|
16
|
-
- Essential metadata files (`manifest.json`, `tree.json`, `*.template.json`, etc.)
|
|
13
|
+
- **Clean temporary directories** (`-t/--cleanup-tmp`): Delete accumulated temporary CDK directories in `$TMPDIR`
|
|
14
|
+
- Deletes entire directories
|
|
15
|
+
- Only time-based protection with `-k/--keep-hours`
|
|
17
16
|
|
|
18
17
|
This helps optimize storage and streamline CI/CD caching.
|
|
19
18
|
|
|
@@ -25,13 +24,12 @@ This helps optimize storage and streamline CI/CD caching.
|
|
|
25
24
|
- Temporary directories in `$TMPDIR` accumulate
|
|
26
25
|
- Disk space exhaustion from accumulated build artifacts
|
|
27
26
|
- Slow CI/CD pipelines due to large cache sizes
|
|
28
|
-
- No official local cleanup tool (AWS CDK's `cdk gc` only handles cloud-side resources)
|
|
29
27
|
|
|
30
28
|
### Solution
|
|
31
29
|
|
|
32
30
|
`cdk-agc` provides safe, intelligent cleanup with:
|
|
33
31
|
|
|
34
|
-
- **
|
|
32
|
+
- **Reference-based protection**: Only removes assets not referenced in `*.assets.json` files
|
|
35
33
|
- **Time-based protection**: Keeps recent assets for quick rollbacks
|
|
36
34
|
|
|
37
35
|
## Installation
|
|
@@ -53,12 +51,9 @@ npm install -g cdk-agc
|
|
|
53
51
|
### Basic Examples
|
|
54
52
|
|
|
55
53
|
```bash
|
|
56
|
-
# Default: Clean cdk.out, keeping only
|
|
54
|
+
# Default: Clean cdk.out, keeping only referenced assets
|
|
57
55
|
npx cdk-agc
|
|
58
56
|
|
|
59
|
-
# Clean temporary directories in $TMPDIR (can save GBs)
|
|
60
|
-
npx cdk-agc -t
|
|
61
|
-
|
|
62
57
|
# Dry-run: Preview what would be deleted
|
|
63
58
|
npx cdk-agc -d
|
|
64
59
|
|
|
@@ -67,6 +62,9 @@ npx cdk-agc -o ./packages/infra/cdk.out
|
|
|
67
62
|
|
|
68
63
|
# Keep assets modified within the last 24 hours
|
|
69
64
|
npx cdk-agc -k 24
|
|
65
|
+
|
|
66
|
+
# Clean temporary directories in $TMPDIR
|
|
67
|
+
npx cdk-agc -t
|
|
70
68
|
```
|
|
71
69
|
|
|
72
70
|
### Options
|
|
@@ -107,7 +105,7 @@ npx cdk-agc -k 24
|
|
|
107
105
|
|
|
108
106
|
`asset.*` files/directories are **protected** from deletion if they meet **any** of these criteria:
|
|
109
107
|
|
|
110
|
-
1. **Active References**: Referenced in
|
|
108
|
+
1. **Active References**: Referenced in `*.assets.json` files
|
|
111
109
|
2. **Recent Modifications**: Modified within the last N hours (when using `-k/--keep-hours`)
|
|
112
110
|
|
|
113
111
|
## Use Cases
|
|
@@ -147,9 +145,9 @@ npx cdk-agc -o ./apps/web/cdk.out
|
|
|
147
145
|
|
|
148
146
|
### Clean Temporary Directories
|
|
149
147
|
|
|
150
|
-
CDK creates temporary directories in `$TMPDIR` during synthesis (directories starting with `cdk.out`, `cdk-`, or `.cdk`), which can accumulate over time
|
|
148
|
+
CDK creates temporary directories in `$TMPDIR` during synthesis (directories starting with `cdk.out`, `cdk-`, or `.cdk`), which can accumulate over time. Use `-t/--cleanup-tmp` to reclaim disk space.
|
|
151
149
|
|
|
152
|
-
**Note**: This option deletes entire directories
|
|
150
|
+
**Note**: This option deletes entire directories. Time-based protection with `-k/--keep-hours` is the only protection applied.
|
|
153
151
|
|
|
154
152
|
```bash
|
|
155
153
|
# Clean all temporary CDK directories (dry-run first)
|
package/dist/cleanup.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanup.d.mts","names":[],"sources":["../src/cleanup.ts"],"mappings":";UAIiB,cAAA;EACf,MAAA;EACA,MAAA;EACA,SAAA;AAAA;AAAA,UAGe,kBAAA;EACf,MAAA;EACA,SAAA;AAAA;;AAFF;;
|
|
1
|
+
{"version":3,"file":"cleanup.d.mts","names":[],"sources":["../src/cleanup.ts"],"mappings":";UAIiB,cAAA;EACf,MAAA;EACA,MAAA;EACA,SAAA;AAAA;AAAA,UAGe,kBAAA;EACf,MAAA;EACA,SAAA;AAAA;;AAFF;;iBA4IsB,aAAA,CAAc,OAAA,EAAS,cAAA,GAAiB,OAAA;;;AAA9D;iBAgHsB,sBAAA,CAAuB,OAAA,EAAS,kBAAA,GAAqB,OAAA"}
|
package/dist/cleanup.mjs
CHANGED
|
@@ -4,42 +4,32 @@ import os from "os";
|
|
|
4
4
|
|
|
5
5
|
//#region src/cleanup.ts
|
|
6
6
|
/**
|
|
7
|
-
* Recursively collect
|
|
7
|
+
* Recursively collect asset paths from *.assets.json files
|
|
8
8
|
*/
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
collected.add(fullPath);
|
|
13
|
-
let parent = path.dirname(fullPath);
|
|
14
|
-
while (parent !== basePath && parent !== ".") {
|
|
15
|
-
collected.add(parent);
|
|
16
|
-
parent = path.dirname(parent);
|
|
17
|
-
}
|
|
18
|
-
} else if (Array.isArray(obj)) obj.forEach((item) => collectActivePaths(item, basePath, collected));
|
|
19
|
-
else if (obj && typeof obj === "object") Object.values(obj).forEach((value) => collectActivePaths(value, basePath, collected));
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Parse *.assets.json files and collect asset paths (recursively)
|
|
23
|
-
*/
|
|
24
|
-
async function collectAssetPaths(outdir, activePaths) {
|
|
25
|
-
const items = await promises.readdir(outdir, { withFileTypes: true });
|
|
9
|
+
async function collectAssetPaths(dirPath) {
|
|
10
|
+
const activePaths = /* @__PURE__ */ new Set();
|
|
11
|
+
const items = await promises.readdir(dirPath, { withFileTypes: true });
|
|
26
12
|
for (const item of items) {
|
|
27
|
-
const itemPath = path.join(
|
|
28
|
-
if (item.isDirectory())
|
|
29
|
-
|
|
13
|
+
const itemPath = path.join(dirPath, item.name);
|
|
14
|
+
if (item.isDirectory()) {
|
|
15
|
+
(await collectAssetPaths(itemPath)).forEach((p) => activePaths.add(p));
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (!item.name.endsWith(".assets.json")) continue;
|
|
19
|
+
try {
|
|
30
20
|
const content = await promises.readFile(itemPath, "utf-8");
|
|
31
21
|
const assets = JSON.parse(content);
|
|
32
22
|
if (assets.files) for (const fileEntry of Object.values(assets.files)) {
|
|
33
23
|
const entry = fileEntry;
|
|
34
24
|
if (entry.source?.path) {
|
|
35
|
-
const assetPath = path.join(
|
|
25
|
+
const assetPath = path.join(path.dirname(itemPath), entry.source.path);
|
|
36
26
|
activePaths.add(assetPath);
|
|
37
27
|
}
|
|
38
28
|
}
|
|
39
29
|
if (assets.dockerImages) for (const imageEntry of Object.values(assets.dockerImages)) {
|
|
40
30
|
const entry = imageEntry;
|
|
41
31
|
if (entry.source?.directory) {
|
|
42
|
-
const assetPath = path.join(
|
|
32
|
+
const assetPath = path.join(path.dirname(itemPath), entry.source.directory);
|
|
43
33
|
activePaths.add(assetPath);
|
|
44
34
|
}
|
|
45
35
|
}
|
|
@@ -47,20 +37,6 @@ async function collectAssetPaths(outdir, activePaths) {
|
|
|
47
37
|
console.warn(`Warning: Failed to parse ${item.name}:`, error);
|
|
48
38
|
}
|
|
49
39
|
}
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Parse manifest file and get active paths
|
|
53
|
-
*/
|
|
54
|
-
async function getActivePathsFromManifest(outdir) {
|
|
55
|
-
const manifestPath = path.join(outdir, "manifest.json");
|
|
56
|
-
const activePaths = /* @__PURE__ */ new Set();
|
|
57
|
-
try {
|
|
58
|
-
const content = await promises.readFile(manifestPath, "utf-8");
|
|
59
|
-
collectActivePaths(JSON.parse(content), outdir, activePaths);
|
|
60
|
-
await collectAssetPaths(outdir, activePaths);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
throw new Error(`Failed to read manifest.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
63
|
-
}
|
|
64
40
|
return activePaths;
|
|
65
41
|
}
|
|
66
42
|
/**
|
|
@@ -120,13 +96,13 @@ function formatSize(bytes) {
|
|
|
120
96
|
async function cleanupAssets(options) {
|
|
121
97
|
const { outdir, dryRun, keepHours } = options;
|
|
122
98
|
console.log(`Scanning ${outdir}...`);
|
|
123
|
-
console.log(`Protection policy:
|
|
99
|
+
console.log(`Protection policy: Referenced assets + files modified within ${keepHours} hours\n`);
|
|
124
100
|
try {
|
|
125
101
|
await promises.access(outdir);
|
|
126
102
|
} catch {
|
|
127
103
|
throw new Error(`Directory not found: ${outdir}`);
|
|
128
104
|
}
|
|
129
|
-
const activePaths = await
|
|
105
|
+
const activePaths = await collectAssetPaths(outdir);
|
|
130
106
|
const entries = await promises.readdir(outdir);
|
|
131
107
|
const itemsToDelete = [];
|
|
132
108
|
await Promise.all(entries.map(async (entry) => {
|
|
@@ -199,18 +175,14 @@ async function cleanupTempDirectories(options) {
|
|
|
199
175
|
return;
|
|
200
176
|
}
|
|
201
177
|
let totalCleaned = 0;
|
|
202
|
-
let totalProtected = 0;
|
|
203
178
|
let totalSize = 0;
|
|
204
179
|
for (const dir of directories) try {
|
|
205
|
-
if (await shouldProtectDirectory(dir, keepHours))
|
|
206
|
-
totalProtected++;
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
180
|
+
if (await shouldProtectDirectory(dir, keepHours)) continue;
|
|
209
181
|
const size = await calculateSize(dir);
|
|
210
182
|
totalSize += size;
|
|
211
183
|
if (!dryRun) await remove(dir);
|
|
212
184
|
totalCleaned++;
|
|
213
|
-
} catch
|
|
185
|
+
} catch {
|
|
214
186
|
continue;
|
|
215
187
|
}
|
|
216
188
|
if (totalCleaned === 0) {
|
package/dist/cleanup.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanup.mjs","names":["fs"],"sources":["../src/cleanup.ts"],"sourcesContent":["import { promises as fs } from \"fs\";\nimport path from \"path\";\nimport os from \"os\";\n\nexport interface CleanupOptions {\n outdir: string;\n dryRun: boolean;\n keepHours: number;\n}\n\nexport interface TempCleanupOptions {\n dryRun: boolean;\n keepHours: number;\n}\n\ninterface ManifestArtifact {\n type: string;\n properties?: {\n file?: string;\n templateFile?: string;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n}\n\ninterface Manifest {\n version: string;\n artifacts?: Record<string, ManifestArtifact>;\n}\n\n/**\n * Recursively collect all paths referenced in the manifest\n */\nfunction collectActivePaths(obj: unknown, basePath: string, collected: Set<string>): void {\n if (typeof obj === \"string\") {\n const fullPath = path.join(basePath, obj);\n collected.add(fullPath);\n // Also protect parent directories\n let parent = path.dirname(fullPath);\n while (parent !== basePath && parent !== \".\") {\n collected.add(parent);\n parent = path.dirname(parent);\n }\n } else if (Array.isArray(obj)) {\n obj.forEach((item) => collectActivePaths(item, basePath, collected));\n } else if (obj && typeof obj === \"object\") {\n Object.values(obj).forEach((value) => collectActivePaths(value, basePath, collected));\n }\n}\n\n/**\n * Parse *.assets.json files and collect asset paths (recursively)\n */\nasync function collectAssetPaths(outdir: string, activePaths: Set<string>): Promise<void> {\n const items = await fs.readdir(outdir, { withFileTypes: true });\n\n for (const item of items) {\n const itemPath = path.join(outdir, item.name);\n\n if (item.isDirectory()) {\n // Recursively scan subdirectories (e.g., assembly-MyStage/)\n await collectAssetPaths(itemPath, activePaths);\n } else if (item.name.endsWith(\".assets.json\")) {\n // Parse assets.json file\n try {\n const content = await fs.readFile(itemPath, \"utf-8\");\n const assets = JSON.parse(content);\n\n // Collect asset paths from files object\n if (assets.files) {\n for (const fileEntry of Object.values(assets.files)) {\n const entry = fileEntry as { source?: { path?: string } };\n if (entry.source?.path) {\n const assetPath = path.join(outdir, entry.source.path);\n activePaths.add(assetPath);\n }\n }\n }\n\n // Collect asset paths from dockerImages object\n if (assets.dockerImages) {\n for (const imageEntry of Object.values(assets.dockerImages)) {\n const entry = imageEntry as { source?: { directory?: string } };\n if (entry.source?.directory) {\n const assetPath = path.join(outdir, entry.source.directory);\n activePaths.add(assetPath);\n }\n }\n }\n } catch (error) {\n // Skip malformed asset files\n console.warn(`Warning: Failed to parse ${item.name}:`, error);\n }\n }\n }\n}\n\n/**\n * Parse manifest file and get active paths\n */\nasync function getActivePathsFromManifest(outdir: string): Promise<Set<string>> {\n const manifestPath = path.join(outdir, \"manifest.json\");\n const activePaths = new Set<string>();\n\n try {\n const content = await fs.readFile(manifestPath, \"utf-8\");\n const manifest: Manifest = JSON.parse(content);\n\n collectActivePaths(manifest, outdir, activePaths);\n\n // Also collect paths from *.assets.json files\n await collectAssetPaths(outdir, activePaths);\n } catch (error) {\n throw new Error(\n `Failed to read manifest.json: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n return activePaths;\n}\n\n/**\n * Check if file/directory should be protected from deletion\n */\nasync function isProtected(\n itemPath: string,\n activePaths: Set<string>,\n keepHours: number,\n): Promise<boolean> {\n // Protect paths referenced in manifest\n if (activePaths.has(itemPath)) {\n return true;\n }\n\n // Protect files/directories within retention period\n if (keepHours > 0) {\n const stats = await fs.stat(itemPath);\n const ageHours = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60);\n if (ageHours <= keepHours) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Calculate size of file or directory\n */\nasync function calculateSize(itemPath: string): Promise<number> {\n const stats = await fs.stat(itemPath);\n\n if (stats.isFile()) {\n return stats.size;\n }\n\n if (stats.isDirectory()) {\n const entries = await fs.readdir(itemPath);\n const sizes = await Promise.all(\n entries.map((entry) => calculateSize(path.join(itemPath, entry))),\n );\n return sizes.reduce((sum, size) => sum + size, 0);\n }\n\n return 0;\n}\n\n/**\n * Recursively remove file or directory\n */\nasync function remove(itemPath: string): Promise<void> {\n const stats = await fs.stat(itemPath);\n\n if (stats.isDirectory()) {\n await fs.rm(itemPath, { recursive: true, force: true });\n } else {\n await fs.unlink(itemPath);\n }\n}\n\n/**\n * Format bytes to human-readable string\n */\nfunction formatSize(bytes: number): string {\n const units = [\"B\", \"KB\", \"MB\", \"GB\"];\n let size = bytes;\n let unitIndex = 0;\n\n while (size >= 1024 && unitIndex < units.length - 1) {\n size /= 1024;\n unitIndex++;\n }\n\n return `${size.toFixed(2)} ${units[unitIndex]}`;\n}\n\n/**\n * Clean up cdk.out directory\n */\nexport async function cleanupAssets(options: CleanupOptions): Promise<void> {\n const { outdir, dryRun, keepHours } = options;\n\n console.log(`Scanning ${outdir}...`);\n console.log(`Protection policy: Active manifest + files modified within ${keepHours} hours\\n`);\n\n // Check directory exists\n try {\n await fs.access(outdir);\n } catch {\n throw new Error(`Directory not found: ${outdir}`);\n }\n\n // Collect paths referenced in manifest\n const activePaths = await getActivePathsFromManifest(outdir);\n\n // Scan directory items\n const entries = await fs.readdir(outdir);\n const itemsToDelete: Array<{ path: string; size: number }> = [];\n\n await Promise.all(\n entries.map(async (entry) => {\n const itemPath = path.join(outdir, entry);\n\n // Only consider asset.* files/directories for deletion\n if (!entry.startsWith(\"asset.\")) {\n return;\n }\n\n if (await isProtected(itemPath, activePaths, keepHours)) {\n return;\n }\n\n const size = await calculateSize(itemPath);\n itemsToDelete.push({ path: itemPath, size });\n }),\n );\n\n // Display results\n if (itemsToDelete.length === 0) {\n console.log(`✓ No unused assets found.`);\n return;\n }\n\n const totalSize = itemsToDelete.reduce((sum, item) => sum + item.size, 0);\n\n console.log(`Found ${itemsToDelete.length} unused item(s):\\n`);\n itemsToDelete.forEach((item) => {\n const relativePath = path.relative(outdir, item.path);\n console.log(` - ${relativePath} (${formatSize(item.size)})`);\n });\n\n console.log(`\\nTotal size to reclaim: ${formatSize(totalSize)}\\n`);\n\n if (dryRun) {\n console.log(\"Dry-run mode: No files were deleted.\");\n } else {\n // Delete in parallel\n await Promise.all(itemsToDelete.map((item) => remove(item.path)));\n console.log(\"✓ Cleanup completed successfully.\");\n }\n}\n\n/**\n * Find all cdk.out temporary directories in $TMPDIR\n */\nasync function findTempDirectories(): Promise<string[]> {\n const tmpdir = os.tmpdir();\n const directories: string[] = [];\n\n try {\n const items = await fs.readdir(tmpdir, { withFileTypes: true });\n\n for (const item of items) {\n // Match directories starting with \"cdk.out\", \"cdk-\", or \".cdk\"\n if (\n item.isDirectory() &&\n (item.name.startsWith(\"cdk.out\") ||\n item.name.startsWith(\"cdk-\") ||\n item.name.startsWith(\".cdk\"))\n ) {\n const dirPath = path.join(tmpdir, item.name);\n directories.push(dirPath);\n }\n }\n } catch (error) {\n console.warn(`Warning: Failed to scan $TMPDIR (${tmpdir}):`, error);\n }\n\n return directories;\n}\n\n/**\n * Check if directory should be protected based on age\n */\nasync function shouldProtectDirectory(dirPath: string, keepHours: number): Promise<boolean> {\n if (keepHours <= 0) {\n return false;\n }\n\n try {\n const stats = await fs.stat(dirPath);\n const ageHours = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60);\n return ageHours <= keepHours;\n } catch {\n return false;\n }\n}\n\n/**\n * Clean up all temporary CDK output directories\n */\nexport async function cleanupTempDirectories(options: TempCleanupOptions): Promise<void> {\n const { dryRun, keepHours } = options;\n const tmpdir = os.tmpdir();\n\n console.log(`Scanning ${tmpdir}...`);\n console.log(`Protection policy: Time-based (directories modified within ${keepHours} hours)\\n`);\n\n const directories = await findTempDirectories();\n\n if (directories.length === 0) {\n console.log(\"✓ No temporary CDK directories found.\");\n return;\n }\n\n let totalCleaned = 0;\n let totalProtected = 0;\n let totalSize = 0;\n\n for (const dir of directories) {\n try {\n // Check if directory should be protected by age\n if (await shouldProtectDirectory(dir, keepHours)) {\n totalProtected++;\n continue;\n }\n\n // Calculate size before deletion\n const size = await calculateSize(dir);\n totalSize += size;\n\n if (!dryRun) {\n await remove(dir);\n }\n\n totalCleaned++;\n } catch (error) {\n // Silently continue on error\n continue;\n }\n }\n\n if (totalCleaned === 0) {\n console.log(\"✓ No temporary CDK directories to clean.\");\n return;\n }\n\n console.log(`Found ${totalCleaned} temporary CDK directory(ies)\\n`);\n console.log(`Total size to reclaim: ${formatSize(totalSize)}\\n`);\n\n if (dryRun) {\n console.log(\"Dry-run mode: No files were deleted.\");\n } else {\n console.log(\"✓ Cleanup completed successfully.\");\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cleanup.mjs","names":["fs"],"sources":["../src/cleanup.ts"],"sourcesContent":["import { promises as fs } from \"fs\";\nimport path from \"path\";\nimport os from \"os\";\n\nexport interface CleanupOptions {\n outdir: string;\n dryRun: boolean;\n keepHours: number;\n}\n\nexport interface TempCleanupOptions {\n dryRun: boolean;\n keepHours: number;\n}\n\n/**\n * Recursively collect asset paths from *.assets.json files\n */\nasync function collectAssetPaths(dirPath: string): Promise<Set<string>> {\n const activePaths = new Set<string>();\n const items = await fs.readdir(dirPath, { withFileTypes: true });\n\n for (const item of items) {\n const itemPath = path.join(dirPath, item.name);\n\n // Recursively scan subdirectories (e.g., assembly-MyStage/)\n if (item.isDirectory()) {\n const subPaths = await collectAssetPaths(itemPath);\n subPaths.forEach((p) => activePaths.add(p));\n continue;\n }\n\n // Only process *.assets.json files\n if (!item.name.endsWith(\".assets.json\")) {\n continue;\n }\n\n // Parse assets.json file\n try {\n const content = await fs.readFile(itemPath, \"utf-8\");\n const assets = JSON.parse(content);\n\n // Collect asset paths from files object\n if (assets.files) {\n for (const fileEntry of Object.values(assets.files)) {\n const entry = fileEntry as { source?: { path?: string } };\n if (entry.source?.path) {\n const assetPath = path.join(path.dirname(itemPath), entry.source.path);\n activePaths.add(assetPath);\n }\n }\n }\n\n // Collect asset paths from dockerImages object\n if (assets.dockerImages) {\n for (const imageEntry of Object.values(assets.dockerImages)) {\n const entry = imageEntry as { source?: { directory?: string } };\n if (entry.source?.directory) {\n const assetPath = path.join(path.dirname(itemPath), entry.source.directory);\n activePaths.add(assetPath);\n }\n }\n }\n } catch (error) {\n // Skip malformed asset files\n console.warn(`Warning: Failed to parse ${item.name}:`, error);\n }\n }\n\n return activePaths;\n}\n\n/**\n * Check if file/directory should be protected from deletion\n */\nasync function isProtected(\n itemPath: string,\n activePaths: Set<string>,\n keepHours: number,\n): Promise<boolean> {\n // Protect assets referenced in *.assets.json files\n if (activePaths.has(itemPath)) {\n return true;\n }\n\n // Protect files/directories within retention period\n if (keepHours > 0) {\n const stats = await fs.stat(itemPath);\n const ageHours = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60);\n if (ageHours <= keepHours) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Calculate size of file or directory\n */\nasync function calculateSize(itemPath: string): Promise<number> {\n const stats = await fs.stat(itemPath);\n\n if (stats.isFile()) {\n return stats.size;\n }\n\n if (stats.isDirectory()) {\n const entries = await fs.readdir(itemPath);\n const sizes = await Promise.all(\n entries.map((entry) => calculateSize(path.join(itemPath, entry))),\n );\n return sizes.reduce((sum, size) => sum + size, 0);\n }\n\n return 0;\n}\n\n/**\n * Recursively remove file or directory\n */\nasync function remove(itemPath: string): Promise<void> {\n const stats = await fs.stat(itemPath);\n\n if (stats.isDirectory()) {\n await fs.rm(itemPath, { recursive: true, force: true });\n } else {\n await fs.unlink(itemPath);\n }\n}\n\n/**\n * Format bytes to human-readable string\n */\nfunction formatSize(bytes: number): string {\n const units = [\"B\", \"KB\", \"MB\", \"GB\"];\n let size = bytes;\n let unitIndex = 0;\n\n while (size >= 1024 && unitIndex < units.length - 1) {\n size /= 1024;\n unitIndex++;\n }\n\n return `${size.toFixed(2)} ${units[unitIndex]}`;\n}\n\n/**\n * Clean up cdk.out directory\n */\nexport async function cleanupAssets(options: CleanupOptions): Promise<void> {\n const { outdir, dryRun, keepHours } = options;\n\n console.log(`Scanning ${outdir}...`);\n console.log(`Protection policy: Referenced assets + files modified within ${keepHours} hours\\n`);\n\n // Check directory exists\n try {\n await fs.access(outdir);\n } catch {\n throw new Error(`Directory not found: ${outdir}`);\n }\n\n // Collect asset paths referenced in *.assets.json files\n const activePaths = await collectAssetPaths(outdir);\n\n // Scan directory items\n const entries = await fs.readdir(outdir);\n const itemsToDelete: Array<{ path: string; size: number }> = [];\n\n await Promise.all(\n entries.map(async (entry) => {\n const itemPath = path.join(outdir, entry);\n\n // Only consider asset.* files/directories for deletion\n if (!entry.startsWith(\"asset.\")) {\n return;\n }\n\n if (await isProtected(itemPath, activePaths, keepHours)) {\n return;\n }\n\n const size = await calculateSize(itemPath);\n itemsToDelete.push({ path: itemPath, size });\n }),\n );\n\n // Display results\n if (itemsToDelete.length === 0) {\n console.log(`✓ No unused assets found.`);\n return;\n }\n\n const totalSize = itemsToDelete.reduce((sum, item) => sum + item.size, 0);\n\n console.log(`Found ${itemsToDelete.length} unused item(s):\\n`);\n itemsToDelete.forEach((item) => {\n const relativePath = path.relative(outdir, item.path);\n console.log(` - ${relativePath} (${formatSize(item.size)})`);\n });\n\n console.log(`\\nTotal size to reclaim: ${formatSize(totalSize)}\\n`);\n\n if (dryRun) {\n console.log(\"Dry-run mode: No files were deleted.\");\n } else {\n // Delete in parallel\n await Promise.all(itemsToDelete.map((item) => remove(item.path)));\n console.log(\"✓ Cleanup completed successfully.\");\n }\n}\n\n/**\n * Find all cdk.out temporary directories in $TMPDIR\n */\nasync function findTempDirectories(): Promise<string[]> {\n const tmpdir = os.tmpdir();\n const directories: string[] = [];\n\n try {\n const items = await fs.readdir(tmpdir, { withFileTypes: true });\n\n for (const item of items) {\n // Match directories starting with \"cdk.out\", \"cdk-\", or \".cdk\"\n if (\n item.isDirectory() &&\n (item.name.startsWith(\"cdk.out\") ||\n item.name.startsWith(\"cdk-\") ||\n item.name.startsWith(\".cdk\"))\n ) {\n const dirPath = path.join(tmpdir, item.name);\n directories.push(dirPath);\n }\n }\n } catch (error) {\n console.warn(`Warning: Failed to scan $TMPDIR (${tmpdir}):`, error);\n }\n\n return directories;\n}\n\n/**\n * Check if directory should be protected based on age\n */\nasync function shouldProtectDirectory(dirPath: string, keepHours: number): Promise<boolean> {\n if (keepHours <= 0) {\n return false;\n }\n\n try {\n const stats = await fs.stat(dirPath);\n const ageHours = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60);\n return ageHours <= keepHours;\n } catch {\n return false;\n }\n}\n\n/**\n * Clean up all temporary CDK output directories\n */\nexport async function cleanupTempDirectories(options: TempCleanupOptions): Promise<void> {\n const { dryRun, keepHours } = options;\n const tmpdir = os.tmpdir();\n\n console.log(`Scanning ${tmpdir}...`);\n console.log(`Protection policy: Time-based (directories modified within ${keepHours} hours)\\n`);\n\n const directories = await findTempDirectories();\n\n if (directories.length === 0) {\n console.log(\"✓ No temporary CDK directories found.\");\n return;\n }\n\n let totalCleaned = 0;\n let totalSize = 0;\n\n for (const dir of directories) {\n try {\n // Check if directory should be protected by age\n if (await shouldProtectDirectory(dir, keepHours)) {\n continue;\n }\n\n // Calculate size before deletion\n const size = await calculateSize(dir);\n totalSize += size;\n\n if (!dryRun) {\n await remove(dir);\n }\n\n totalCleaned++;\n } catch {\n // Silently continue on error\n continue;\n }\n }\n\n if (totalCleaned === 0) {\n console.log(\"✓ No temporary CDK directories to clean.\");\n return;\n }\n\n console.log(`Found ${totalCleaned} temporary CDK directory(ies)\\n`);\n console.log(`Total size to reclaim: ${formatSize(totalSize)}\\n`);\n\n if (dryRun) {\n console.log(\"Dry-run mode: No files were deleted.\");\n } else {\n console.log(\"✓ Cleanup completed successfully.\");\n }\n}\n"],"mappings":"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"}
|