@git.zone/tsdisk 1.1.0 → 1.3.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +7 -1
- package/dist_ts/index.js +6 -2
- package/dist_ts/tsdisk.cleanup.d.ts +26 -0
- package/dist_ts/tsdisk.cleanup.js +128 -0
- package/dist_ts/tsdisk.cli.d.ts +44 -8
- package/dist_ts/tsdisk.cli.js +247 -176
- package/dist_ts/tsdisk.plugins.d.ts +2 -2
- package/dist_ts/tsdisk.plugins.js +3 -3
- package/dist_ts/tsdisk.progress.d.ts +31 -0
- package/dist_ts/tsdisk.progress.js +64 -0
- package/dist_ts/tsdisk.scan.d.ts +22 -0
- package/dist_ts/tsdisk.scan.js +147 -0
- package/dist_ts/tsdisk.toolcaches.js +27 -9
- package/dist_ts/tsdisk.usage.d.ts +50 -0
- package/dist_ts/tsdisk.usage.js +194 -0
- package/package.json +4 -4
- package/readme.md +45 -20
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +9 -0
- package/ts/tsdisk.cleanup.ts +120 -0
- package/ts/tsdisk.cli.ts +216 -199
- package/ts/tsdisk.plugins.ts +2 -1
- package/ts/tsdisk.progress.ts +78 -0
- package/ts/tsdisk.scan.ts +151 -0
- package/ts/tsdisk.toolcaches.ts +28 -8
- package/ts/tsdisk.usage.ts +199 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import * as plugins from './tsdisk.plugins.js';
|
|
2
|
+
export function containsPath(parent, child) {
|
|
3
|
+
const relative = plugins.path.relative(parent, child);
|
|
4
|
+
return relative === '' || (!relative.startsWith(`..${plugins.path.sep}`) && relative !== '..' && !plugins.path.isAbsolute(relative));
|
|
5
|
+
}
|
|
6
|
+
/** Preserve explicitly selected nested mounts; subsume overlapping roots on the same device. */
|
|
7
|
+
export async function normalizeScanRoots(roots) {
|
|
8
|
+
const unique = [...new Set(roots.map((root) => plugins.path.resolve(root)))].sort((a, b) => a.length - b.length || a.localeCompare(b));
|
|
9
|
+
const accepted = [];
|
|
10
|
+
for (const path of unique) {
|
|
11
|
+
const stat = await plugins.fs.lstat(path).catch(() => undefined);
|
|
12
|
+
const real = await plugins.fs.realpath(path).catch(() => undefined);
|
|
13
|
+
const dev = stat?.isDirectory() && real === path ? stat.dev : undefined;
|
|
14
|
+
if (dev !== undefined && accepted.some((parent) => parent.dev === dev && containsPath(parent.path, path)))
|
|
15
|
+
continue;
|
|
16
|
+
accepted.push({ path, dev });
|
|
17
|
+
}
|
|
18
|
+
return accepted.map((root) => root.path);
|
|
19
|
+
}
|
|
20
|
+
/** GNU du emits files immediately and inclusive directory totals on completion.
|
|
21
|
+
* Reconcile each completed directory with its observed children, rather than
|
|
22
|
+
* adding inclusive totals twice. The active ancestor chain survives a timeout.
|
|
23
|
+
*/
|
|
24
|
+
export class UsageAccumulator {
|
|
25
|
+
roots;
|
|
26
|
+
trackedPaths;
|
|
27
|
+
priorityPaths;
|
|
28
|
+
pending = '';
|
|
29
|
+
active = new Map();
|
|
30
|
+
largest = new Map();
|
|
31
|
+
tracked = new Map();
|
|
32
|
+
minimumLargestBytes = 0;
|
|
33
|
+
operandBytes = new Map();
|
|
34
|
+
accountingRoots;
|
|
35
|
+
currentRoot;
|
|
36
|
+
measuredBytes = 0;
|
|
37
|
+
records = 0;
|
|
38
|
+
currentPath = '';
|
|
39
|
+
constructor(roots, trackedPaths = new Set(), priorityPaths = []) {
|
|
40
|
+
this.roots = roots;
|
|
41
|
+
this.trackedPaths = trackedPaths;
|
|
42
|
+
this.priorityPaths = priorityPaths;
|
|
43
|
+
this.roots = [...roots].sort((a, b) => b.length - a.length);
|
|
44
|
+
if (roots.some((root) => plugins.path.resolve(root) !== root) || priorityPaths.some((path) => !roots.some((root) => root !== path && containsPath(root, path)))) {
|
|
45
|
+
throw new Error('Usage accounting requires normalized roots and strict descendant priority paths.');
|
|
46
|
+
}
|
|
47
|
+
this.accountingRoots = [...priorityPaths, ...roots].sort((a, b) => b.length - a.length);
|
|
48
|
+
this.trackedPaths = new Set([...trackedPaths, ...roots, ...priorityPaths]);
|
|
49
|
+
}
|
|
50
|
+
consume(chunk) {
|
|
51
|
+
this.pending += chunk;
|
|
52
|
+
let start = 0;
|
|
53
|
+
for (;;) {
|
|
54
|
+
const end = this.pending.indexOf('\0', start);
|
|
55
|
+
if (end < 0)
|
|
56
|
+
break;
|
|
57
|
+
this.record(this.pending.slice(start, end));
|
|
58
|
+
start = end + 1;
|
|
59
|
+
}
|
|
60
|
+
this.pending = this.pending.slice(start);
|
|
61
|
+
if (this.pending.length > 1024 * 1024)
|
|
62
|
+
throw new Error('Invalid du record exceeds 1 MiB.');
|
|
63
|
+
}
|
|
64
|
+
record(record) {
|
|
65
|
+
const separator = record.indexOf('\t');
|
|
66
|
+
const bytes = Number(record.slice(0, separator));
|
|
67
|
+
const path = record.slice(separator + 1);
|
|
68
|
+
const matches = (candidate) => path === candidate || path.startsWith(candidate === '/' ? '/' : `${candidate}/`);
|
|
69
|
+
const root = this.currentRoot && matches(this.currentRoot) ? this.currentRoot : this.accountingRoots.find(matches);
|
|
70
|
+
if (separator < 1 || !Number.isSafeInteger(bytes) || bytes < 0 || !plugins.path.isAbsolute(path) || !root)
|
|
71
|
+
throw new Error('Invalid du output.');
|
|
72
|
+
this.records++;
|
|
73
|
+
this.currentPath = path;
|
|
74
|
+
this.currentRoot = path === root ? undefined : root;
|
|
75
|
+
const delta = bytes - (this.active.get(path) ?? 0);
|
|
76
|
+
this.active.delete(path);
|
|
77
|
+
this.measuredBytes += delta;
|
|
78
|
+
this.operandBytes.set(root, (this.operandBytes.get(root) ?? 0) + delta);
|
|
79
|
+
if (path !== root) {
|
|
80
|
+
for (let parent = plugins.path.dirname(path);; parent = plugins.path.dirname(parent)) {
|
|
81
|
+
this.active.set(parent, (this.active.get(parent) ?? 0) + delta);
|
|
82
|
+
if (parent === root)
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const entry = { path, bytes, complete: true };
|
|
87
|
+
if (this.trackedPaths.has(path))
|
|
88
|
+
this.tracked.set(path, entry);
|
|
89
|
+
// Retain only the largest completed paths, not millions of file records.
|
|
90
|
+
if (this.largest.size < 100 || bytes > this.minimumLargestBytes) {
|
|
91
|
+
this.largest.set(path, entry);
|
|
92
|
+
if (this.largest.size > 100) {
|
|
93
|
+
const sorted = [...this.largest.values()].sort((a, b) => b.bytes - a.bytes);
|
|
94
|
+
this.largest = new Map(sorted.slice(0, 100).map((item) => [item.path, item]));
|
|
95
|
+
}
|
|
96
|
+
this.minimumLargestBytes = Math.min(...[...this.largest.values()].map((item) => item.bytes));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
snapshot() {
|
|
100
|
+
const entries = new Map([...this.largest, ...this.tracked].map(([path, entry]) => [path, { ...entry }]));
|
|
101
|
+
for (const [path, bytes] of this.active)
|
|
102
|
+
entries.set(path, { path, bytes, complete: false });
|
|
103
|
+
// du charges an overlapping subtree to its first operand and omits it from
|
|
104
|
+
// later ancestor totals. Restore those inclusive totals for display only;
|
|
105
|
+
// measuredBytes remains the native, deduplicated sum of operand contributions.
|
|
106
|
+
for (const priority of this.priorityPaths) {
|
|
107
|
+
const bytes = this.operandBytes.get(priority);
|
|
108
|
+
const root = this.roots.find((candidate) => containsPath(candidate, priority));
|
|
109
|
+
if (bytes === undefined || !root)
|
|
110
|
+
continue;
|
|
111
|
+
for (let parent = plugins.path.dirname(priority);; parent = plugins.path.dirname(parent)) {
|
|
112
|
+
const entry = entries.get(parent) ?? { path: parent, bytes: 0, complete: false };
|
|
113
|
+
entry.bytes += bytes;
|
|
114
|
+
entries.set(parent, entry);
|
|
115
|
+
if (parent === root)
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return [...entries.values()].sort((a, b) => b.bytes - a.bytes);
|
|
120
|
+
}
|
|
121
|
+
get endedOnRecordBoundary() { return this.pending.length === 0; }
|
|
122
|
+
}
|
|
123
|
+
/** One streaming native pass; no output export, no symlink traversal, no raw file content reads. */
|
|
124
|
+
export async function measureDiskUsage(roots, options) {
|
|
125
|
+
if (!Number.isFinite(options.timeoutSeconds) || options.timeoutSeconds <= 0)
|
|
126
|
+
throw new Error('Invalid usage timeout.');
|
|
127
|
+
const valid = [];
|
|
128
|
+
const devices = new Map();
|
|
129
|
+
const errors = [];
|
|
130
|
+
for (const root of await normalizeScanRoots(roots)) {
|
|
131
|
+
try {
|
|
132
|
+
const stat = await plugins.fs.lstat(root);
|
|
133
|
+
if (!stat.isDirectory() || await plugins.fs.realpath(root) !== root)
|
|
134
|
+
throw new Error('Usage roots must be real directories without symlink ancestors.');
|
|
135
|
+
valid.push(root);
|
|
136
|
+
devices.set(root, stat.dev);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
errors.push(`${root}: ${error instanceof Error ? error.message : String(error)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const priorities = [];
|
|
143
|
+
for (const path of await normalizeScanRoots(options.priorityPaths ?? [])) {
|
|
144
|
+
const root = [...valid].reverse().find((root) => root !== path && containsPath(root, path));
|
|
145
|
+
if (!root)
|
|
146
|
+
continue;
|
|
147
|
+
const real = await plugins.fs.realpath(path).catch(() => undefined);
|
|
148
|
+
const stat = await plugins.fs.lstat(path).catch(() => undefined);
|
|
149
|
+
if (real === path && stat?.isDirectory() && stat.dev === devices.get(root))
|
|
150
|
+
priorities.push(path);
|
|
151
|
+
}
|
|
152
|
+
// Measure retained project artifacts before broad dependency/source trees.
|
|
153
|
+
// This changes order only: remaining paths are visited by the final roots.
|
|
154
|
+
priorities.sort((a, b) => b.split(plugins.path.sep).length - a.split(plugins.path.sep).length || a.localeCompare(b));
|
|
155
|
+
let argumentBytes = valid.reduce((sum, path) => sum + Buffer.byteLength(path) + 1, 0);
|
|
156
|
+
const argumentLimit = priorities.findIndex((path) => (argumentBytes += Buffer.byteLength(path) + 1) > 128 * 1024);
|
|
157
|
+
if (argumentLimit >= 0)
|
|
158
|
+
priorities.splice(argumentLimit);
|
|
159
|
+
const usage = new UsageAccumulator(valid, new Set(options.trackedPaths), priorities);
|
|
160
|
+
let stderr = errors.join('\n');
|
|
161
|
+
let malformed = false;
|
|
162
|
+
const code = valid.length ? await new Promise((resolve) => {
|
|
163
|
+
const child = plugins.spawn('timeout', ['--kill-after=5s', `${options.timeoutSeconds}s`, 'du', '-a', '-0', '-x', '-B1', '--', ...priorities, ...valid], { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, LC_ALL: 'C' } });
|
|
164
|
+
child.stdout.setEncoding('utf8');
|
|
165
|
+
let lastNotification = 0;
|
|
166
|
+
child.stdout.on('data', (chunk) => {
|
|
167
|
+
if (malformed)
|
|
168
|
+
return;
|
|
169
|
+
try {
|
|
170
|
+
usage.consume(chunk);
|
|
171
|
+
if (Date.now() - lastNotification >= 250) {
|
|
172
|
+
options.onProgress?.(usage);
|
|
173
|
+
lastNotification = Date.now();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
malformed = true;
|
|
178
|
+
stderr += `\n${error instanceof Error ? error.message : String(error)}`;
|
|
179
|
+
// Signal timeout, which forwards termination to its managed process group.
|
|
180
|
+
child.kill('SIGTERM');
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
child.stderr.setEncoding('utf8');
|
|
184
|
+
child.stderr.on('data', (chunk) => { if (stderr.length < 65536)
|
|
185
|
+
stderr += chunk.slice(0, 65536 - stderr.length); });
|
|
186
|
+
child.on('error', (error) => { stderr += `\n${error.message}`; });
|
|
187
|
+
child.on('close', (exitCode) => resolve(exitCode ?? 1));
|
|
188
|
+
}) : 1;
|
|
189
|
+
options.onProgress?.(usage);
|
|
190
|
+
const complete = code === 0 && !stderr.trim() && !malformed && usage.endedOnRecordBoundary;
|
|
191
|
+
const entries = usage.snapshot().map((entry) => ({ ...entry, complete: entry.complete && complete }));
|
|
192
|
+
return { roots: valid, measuredBytes: usage.measuredBytes, entries, records: usage.records, complete, code, stderr };
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHNkaXNrLnVzYWdlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvdHNkaXNrLnVzYWdlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0scUJBQXFCLENBQUM7QUFrQi9DLE1BQU0sVUFBVSxZQUFZLENBQUMsTUFBYyxFQUFFLEtBQWE7SUFDeEQsTUFBTSxRQUFRLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsTUFBTSxFQUFFLEtBQUssQ0FBQyxDQUFDO0lBQ3RELE9BQU8sUUFBUSxLQUFLLEVBQUUsSUFBSSxDQUFDLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBQyxLQUFLLE9BQU8sQ0FBQyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBSSxRQUFRLEtBQUssSUFBSSxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxVQUFVLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQztBQUN2SSxDQUFDO0FBRUQsZ0dBQWdHO0FBQ2hHLE1BQU0sQ0FBQyxLQUFLLFVBQVUsa0JBQWtCLENBQUMsS0FBZTtJQUN0RCxNQUFNLE1BQU0sR0FBRyxDQUFDLEdBQUcsSUFBSSxHQUFHLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsTUFBTSxJQUFJLENBQUMsQ0FBQyxhQUFhLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUN2SSxNQUFNLFFBQVEsR0FBMEMsRUFBRSxDQUFDO0lBQzNELEtBQUssTUFBTSxJQUFJLElBQUksTUFBTSxFQUFFLENBQUM7UUFDMUIsTUFBTSxJQUFJLEdBQUcsTUFBTSxPQUFPLENBQUMsRUFBRSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLENBQUMsU0FBUyxDQUFDLENBQUM7UUFDakUsTUFBTSxJQUFJLEdBQUcsTUFBTSxPQUFPLENBQUMsRUFBRSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLENBQUMsU0FBUyxDQUFDLENBQUM7UUFDcEUsTUFBTSxHQUFHLEdBQUcsSUFBSSxFQUFFLFdBQVcsRUFBRSxJQUFJLElBQUksS0FBSyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQztRQUN4RSxJQUFJLEdBQUcsS0FBSyxTQUFTLElBQUksUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFFLENBQUMsTUFBTSxDQUFDLEdBQUcsS0FBSyxHQUFHLElBQUksWUFBWSxDQUFDLE1BQU0sQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLENBQUM7WUFBRSxTQUFTO1FBQ3BILFFBQVEsQ0FBQyxJQUFJLENBQUMsRUFBRSxJQUFJLEVBQUUsR0FBRyxFQUFFLENBQUMsQ0FBQztJQUMvQixDQUFDO0lBQ0QsT0FBTyxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7QUFDM0MsQ0FBQztBQUVEOzs7R0FHRztBQUNILE1BQU0sT0FBTyxnQkFBZ0I7SUFhUDtJQUF5QjtJQUErQztJQVpwRixPQUFPLEdBQUcsRUFBRSxDQUFDO0lBQ2IsTUFBTSxHQUFHLElBQUksR0FBRyxFQUFrQixDQUFDO0lBQ25DLE9BQU8sR0FBRyxJQUFJLEdBQUcsRUFBdUIsQ0FBQztJQUN6QyxPQUFPLEdBQUcsSUFBSSxHQUFHLEVBQXVCLENBQUM7SUFDekMsbUJBQW1CLEdBQUcsQ0FBQyxDQUFDO0lBQ3hCLFlBQVksR0FBRyxJQUFJLEdBQUcsRUFBa0IsQ0FBQztJQUN6QyxlQUFlLENBQVc7SUFDMUIsV0FBVyxDQUFVO0lBQ3RCLGFBQWEsR0FBRyxDQUFDLENBQUM7SUFDbEIsT0FBTyxHQUFHLENBQUMsQ0FBQztJQUNaLFdBQVcsR0FBRyxFQUFFLENBQUM7SUFFeEIsWUFBb0IsS0FBZSxFQUFVLGVBQTRCLElBQUksR0FBRyxFQUFFLEVBQVUsZ0JBQTBCLEVBQUU7UUFBcEcsVUFBSyxHQUFMLEtBQUssQ0FBVTtRQUFVLGlCQUFZLEdBQVosWUFBWSxDQUF5QjtRQUFVLGtCQUFhLEdBQWIsYUFBYSxDQUFlO1FBQ3RILElBQUksQ0FBQyxLQUFLLEdBQUcsQ0FBQyxHQUFHLEtBQUssQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQzVELElBQUksS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssSUFBSSxDQUFDLElBQUksYUFBYSxDQUFDLElBQUksQ0FBQyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLEtBQUssSUFBSSxJQUFJLFlBQVksQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUM7WUFDaEssTUFBTSxJQUFJLEtBQUssQ0FBQyxrRkFBa0YsQ0FBQyxDQUFDO1FBQ3RHLENBQUM7UUFDRCxJQUFJLENBQUMsZUFBZSxHQUFHLENBQUMsR0FBRyxhQUFhLEVBQUUsR0FBRyxLQUFLLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUN4RixJQUFJLENBQUMsWUFBWSxHQUFHLElBQUksR0FBRyxDQUFDLENBQUMsR0FBRyxZQUFZLEVBQUUsR0FBRyxLQUFLLEVBQUUsR0FBRyxhQUFhLENBQUMsQ0FBQyxDQUFDO0lBQzdFLENBQUM7SUFFTSxPQUFPLENBQUMsS0FBYTtRQUMxQixJQUFJLENBQUMsT0FBTyxJQUFJLEtBQUssQ0FBQztRQUN0QixJQUFJLEtBQUssR0FBRyxDQUFDLENBQUM7UUFDZCxTQUFTLENBQUM7WUFDUixNQUFNLEdBQUcsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsS0FBSyxDQUFDLENBQUM7WUFDOUMsSUFBSSxHQUFHLEdBQUcsQ0FBQztnQkFBRSxNQUFNO1lBQ25CLElBQUksQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsS0FBSyxFQUFFLEdBQUcsQ0FBQyxDQUFDLENBQUM7WUFDNUMsS0FBSyxHQUFHLEdBQUcsR0FBRyxDQUFDLENBQUM7UUFDbEIsQ0FBQztRQUNELElBQUksQ0FBQyxPQUFPLEdBQUcsSUFBSSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDekMsSUFBSSxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sR0FBRyxJQUFJLEdBQUcsSUFBSTtZQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsa0NBQWtDLENBQUMsQ0FBQztJQUM3RixDQUFDO0lBRU8sTUFBTSxDQUFDLE1BQWM7UUFDM0IsTUFBTSxTQUFTLEdBQUcsTUFBTSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUN2QyxNQUFNLEtBQUssR0FBRyxNQUFNLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsU0FBUyxDQUFDLENBQUMsQ0FBQztRQUNqRCxNQUFNLElBQUksR0FBRyxNQUFNLENBQUMsS0FBSyxDQUFDLFNBQVMsR0FBRyxDQUFDLENBQUMsQ0FBQztRQUN6QyxNQUFNLE9BQU8sR0FBRyxDQUFDLFNBQWlCLEVBQUUsRUFBRSxDQUFDLElBQUksS0FBSyxTQUFTLElBQUksSUFBSSxDQUFDLFVBQVUsQ0FBQyxTQUFTLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEdBQUcsU0FBUyxHQUFHLENBQUMsQ0FBQztRQUN4SCxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsV0FBVyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsV0FBVyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQ25ILElBQUksU0FBUyxHQUFHLENBQUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxHQUFHLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsSUFBSTtZQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsb0JBQW9CLENBQUMsQ0FBQztRQUNqSixJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDZixJQUFJLENBQUMsV0FBVyxHQUFHLElBQUksQ0FBQztRQUN4QixJQUFJLENBQUMsV0FBVyxHQUFHLElBQUksS0FBSyxJQUFJLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO1FBQ3BELE1BQU0sS0FBSyxHQUFHLEtBQUssR0FBRyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBQ25ELElBQUksQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ3pCLElBQUksQ0FBQyxhQUFhLElBQUksS0FBSyxDQUFDO1FBQzVCLElBQUksQ0FBQyxZQUFZLENBQUMsR0FBRyxDQUFDLElBQUksRUFBRSxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLEtBQUssQ0FBQyxDQUFDO1FBQ3hFLElBQUksSUFBSSxLQUFLLElBQUksRUFBRSxDQUFDO1lBQ2xCLEtBQUssSUFBSSxNQUFNLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLEdBQUksTUFBTSxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7Z0JBQ3RGLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLE1BQU0sRUFBRSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxHQUFHLEtBQUssQ0FBQyxDQUFDO2dCQUNoRSxJQUFJLE1BQU0sS0FBSyxJQUFJO29CQUFFLE1BQU07WUFDN0IsQ0FBQztRQUNILENBQUM7UUFDRCxNQUFNLEtBQUssR0FBRyxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxDQUFDO1FBQzlDLElBQUksSUFBSSxDQUFDLFlBQVksQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDO1lBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFLEtBQUssQ0FBQyxDQUFDO1FBQy9ELHlFQUF5RTtRQUN6RSxJQUFJLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxHQUFHLEdBQUcsSUFBSSxLQUFLLEdBQUcsSUFBSSxDQUFDLG1CQUFtQixFQUFFLENBQUM7WUFDaEUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFLEtBQUssQ0FBQyxDQUFDO1lBQzlCLElBQUksSUFBSSxDQUFDLE9BQU8sQ0FBQyxJQUFJLEdBQUcsR0FBRyxFQUFFLENBQUM7Z0JBQzVCLE1BQU0sTUFBTSxHQUFHLENBQUMsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxDQUFDLEtBQUssR0FBRyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUM7Z0JBQzVFLElBQUksQ0FBQyxPQUFPLEdBQUcsSUFBSSxHQUFHLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDO1lBQ2hGLENBQUM7WUFDRCxJQUFJLENBQUMsbUJBQW1CLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsR0FBRyxJQUFJLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQztRQUMvRixDQUFDO0lBQ0gsQ0FBQztJQUVNLFFBQVE7UUFDYixNQUFNLE9BQU8sR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLEdBQUcsSUFBSSxDQUFDLE9BQU8sRUFBRSxHQUFHLElBQUksQ0FBQyxPQUFPLENBQUMsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksRUFBRSxLQUFLLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQyxJQUFJLEVBQUUsRUFBRSxHQUFHLEtBQUssRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQ3pHLEtBQUssTUFBTSxDQUFDLElBQUksRUFBRSxLQUFLLENBQUMsSUFBSSxJQUFJLENBQUMsTUFBTTtZQUFFLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxRQUFRLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztRQUM3RiwyRUFBMkU7UUFDM0UsMEVBQTBFO1FBQzFFLCtFQUErRTtRQUMvRSxLQUFLLE1BQU0sUUFBUSxJQUFJLElBQUksQ0FBQyxhQUFhLEVBQUUsQ0FBQztZQUMxQyxNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxRQUFRLENBQUMsQ0FBQztZQUM5QyxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLFNBQVMsRUFBRSxFQUFFLENBQUMsWUFBWSxDQUFDLFNBQVMsRUFBRSxRQUFRLENBQUMsQ0FBQyxDQUFDO1lBQy9FLElBQUksS0FBSyxLQUFLLFNBQVMsSUFBSSxDQUFDLElBQUk7Z0JBQUUsU0FBUztZQUMzQyxLQUFLLElBQUksTUFBTSxHQUFHLE9BQU8sQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxHQUFJLE1BQU0sR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDO2dCQUMxRixNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxJQUFJLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxLQUFLLEVBQUUsQ0FBQyxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsQ0FBQztnQkFDakYsS0FBSyxDQUFDLEtBQUssSUFBSSxLQUFLLENBQUM7Z0JBQ3JCLE9BQU8sQ0FBQyxHQUFHLENBQUMsTUFBTSxFQUFFLEtBQUssQ0FBQyxDQUFDO2dCQUMzQixJQUFJLE1BQU0sS0FBSyxJQUFJO29CQUFFLE1BQU07WUFDN0IsQ0FBQztRQUNILENBQUM7UUFDRCxPQUFPLENBQUMsR0FBRyxPQUFPLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsS0FBSyxHQUFHLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUNqRSxDQUFDO0lBRUQsSUFBVyxxQkFBcUIsS0FBSyxPQUFPLElBQUksQ0FBQyxPQUFPLENBQUMsTUFBTSxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUM7Q0FDekU7QUFTRCxvR0FBb0c7QUFDcEcsTUFBTSxDQUFDLEtBQUssVUFBVSxnQkFBZ0IsQ0FBQyxLQUFlLEVBQUUsT0FBc0I7SUFDNUUsSUFBSSxDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLGNBQWMsQ0FBQyxJQUFJLE9BQU8sQ0FBQyxjQUFjLElBQUksQ0FBQztRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsd0JBQXdCLENBQUMsQ0FBQztJQUN2SCxNQUFNLEtBQUssR0FBYSxFQUFFLENBQUM7SUFDM0IsTUFBTSxPQUFPLEdBQUcsSUFBSSxHQUFHLEVBQWtCLENBQUM7SUFDMUMsTUFBTSxNQUFNLEdBQWEsRUFBRSxDQUFDO0lBQzVCLEtBQUssTUFBTSxJQUFJLElBQUksTUFBTSxrQkFBa0IsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ25ELElBQUksQ0FBQztZQUNILE1BQU0sSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLEVBQUUsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDMUMsSUFBSSxDQUFDLElBQUksQ0FBQyxXQUFXLEVBQUUsSUFBSSxNQUFNLE9BQU8sQ0FBQyxFQUFFLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxLQUFLLElBQUk7Z0JBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxpRUFBaUUsQ0FBQyxDQUFDO1lBQ3hKLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7WUFDakIsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBQzlCLENBQUM7UUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO1lBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLElBQUksS0FBSyxLQUFLLFlBQVksS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxDQUFDO1FBQUMsQ0FBQztJQUN4RyxDQUFDO0lBQ0QsTUFBTSxVQUFVLEdBQWEsRUFBRSxDQUFDO0lBQ2hDLEtBQUssTUFBTSxJQUFJLElBQUksTUFBTSxrQkFBa0IsQ0FBQyxPQUFPLENBQUMsYUFBYSxJQUFJLEVBQUUsQ0FBQyxFQUFFLENBQUM7UUFDekUsTUFBTSxJQUFJLEdBQUcsQ0FBQyxHQUFHLEtBQUssQ0FBQyxDQUFDLE9BQU8sRUFBRSxDQUFDLElBQUksQ0FBQyxDQUFDLElBQUksRUFBRSxFQUFFLENBQUMsSUFBSSxLQUFLLElBQUksSUFBSSxZQUFZLENBQUMsSUFBSSxFQUFFLElBQUksQ0FBQyxDQUFDLENBQUM7UUFDNUYsSUFBSSxDQUFDLElBQUk7WUFBRSxTQUFTO1FBQ3BCLE1BQU0sSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLEVBQUUsQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQ3BFLE1BQU0sSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLEVBQUUsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRSxDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQ2pFLElBQUksSUFBSSxLQUFLLElBQUksSUFBSSxJQUFJLEVBQUUsV0FBVyxFQUFFLElBQUksSUFBSSxDQUFDLEdBQUcsS0FBSyxPQUFPLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQztZQUFFLFVBQVUsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDcEcsQ0FBQztJQUNELDJFQUEyRTtJQUMzRSwyRUFBMkU7SUFDM0UsVUFBVSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sSUFBSSxDQUFDLENBQUMsYUFBYSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDckgsSUFBSSxhQUFhLEdBQUcsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDLEdBQUcsRUFBRSxJQUFJLEVBQUUsRUFBRSxDQUFDLEdBQUcsR0FBRyxNQUFNLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQztJQUN0RixNQUFNLGFBQWEsR0FBRyxVQUFVLENBQUMsU0FBUyxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxDQUFDLGFBQWEsSUFBSSxNQUFNLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxHQUFHLEdBQUcsR0FBRyxJQUFJLENBQUMsQ0FBQztJQUNsSCxJQUFJLGFBQWEsSUFBSSxDQUFDO1FBQUUsVUFBVSxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsQ0FBQztJQUN6RCxNQUFNLEtBQUssR0FBRyxJQUFJLGdCQUFnQixDQUFDLEtBQUssRUFBRSxJQUFJLEdBQUcsQ0FBQyxPQUFPLENBQUMsWUFBWSxDQUFDLEVBQUUsVUFBVSxDQUFDLENBQUM7SUFDckYsSUFBSSxNQUFNLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUMvQixJQUFJLFNBQVMsR0FBRyxLQUFLLENBQUM7SUFDdEIsTUFBTSxJQUFJLEdBQUcsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsTUFBTSxJQUFJLE9BQU8sQ0FBUyxDQUFDLE9BQU8sRUFBRSxFQUFFO1FBQ2hFLE1BQU0sS0FBSyxHQUFHLE9BQU8sQ0FBQyxLQUFLLENBQUMsU0FBUyxFQUFFLENBQUMsaUJBQWlCLEVBQUUsR0FBRyxPQUFPLENBQUMsY0FBYyxHQUFHLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsR0FBRyxVQUFVLEVBQUUsR0FBRyxLQUFLLENBQUMsRUFBRSxFQUFFLEtBQUssRUFBRSxDQUFDLFFBQVEsRUFBRSxNQUFNLEVBQUUsTUFBTSxDQUFDLEVBQUUsR0FBRyxFQUFFLEVBQUUsR0FBRyxPQUFPLENBQUMsR0FBRyxFQUFFLE1BQU0sRUFBRSxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7UUFDck8sS0FBSyxDQUFDLE1BQU0sQ0FBQyxXQUFXLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDakMsSUFBSSxnQkFBZ0IsR0FBRyxDQUFDLENBQUM7UUFDekIsS0FBSyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsTUFBTSxFQUFFLENBQUMsS0FBYSxFQUFFLEVBQUU7WUFDeEMsSUFBSSxTQUFTO2dCQUFFLE9BQU87WUFDdEIsSUFBSSxDQUFDO2dCQUNILEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUM7Z0JBQ3JCLElBQUksSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLGdCQUFnQixJQUFJLEdBQUcsRUFBRSxDQUFDO29CQUN6QyxPQUFPLENBQUMsVUFBVSxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUM7b0JBQzVCLGdCQUFnQixHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztnQkFDaEMsQ0FBQztZQUNILENBQUM7WUFBQyxPQUFPLEtBQUssRUFBRSxDQUFDO2dCQUNmLFNBQVMsR0FBRyxJQUFJLENBQUM7Z0JBQ2pCLE1BQU0sSUFBSSxLQUFLLEtBQUssWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO2dCQUN4RSwyRUFBMkU7Z0JBQzNFLEtBQUssQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLENBQUM7WUFDeEIsQ0FBQztRQUNILENBQUMsQ0FBQyxDQUFDO1FBQ0gsS0FBSyxDQUFDLE1BQU0sQ0FBQyxXQUFXLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDakMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsTUFBTSxFQUFFLENBQUMsS0FBYSxFQUFFLEVBQUUsR0FBRyxJQUFJLE1BQU0sQ0FBQyxNQUFNLEdBQUcsS0FBSztZQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxLQUFLLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDNUgsS0FBSyxDQUFDLEVBQUUsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxLQUFLLEVBQUUsRUFBRSxHQUFHLE1BQU0sSUFBSSxLQUFLLEtBQUssQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBQ2xFLEtBQUssQ0FBQyxFQUFFLENBQUMsT0FBTyxFQUFFLENBQUMsUUFBUSxFQUFFLEVBQUUsQ0FBQyxPQUFPLENBQUMsUUFBUSxJQUFJLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDMUQsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNQLE9BQU8sQ0FBQyxVQUFVLEVBQUUsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUM1QixNQUFNLFFBQVEsR0FBRyxJQUFJLEtBQUssQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLElBQUksRUFBRSxJQUFJLENBQUMsU0FBUyxJQUFJLEtBQUssQ0FBQyxxQkFBcUIsQ0FBQztJQUMzRixNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsUUFBUSxFQUFFLENBQUMsR0FBRyxDQUFDLENBQUMsS0FBSyxFQUFFLEVBQUUsQ0FBQyxDQUFDLEVBQUUsR0FBRyxLQUFLLEVBQUUsUUFBUSxFQUFFLEtBQUssQ0FBQyxRQUFRLElBQUksUUFBUSxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBQ3RHLE9BQU8sRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLGFBQWEsRUFBRSxLQUFLLENBQUMsYUFBYSxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsS0FBSyxDQUFDLE9BQU8sRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxDQUFDO0FBQ3ZILENBQUMifQ==
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@git.zone/tsdisk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A disk usage diagnostic CLI for finding large files, caches, logs, and Docker storage.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"license.md"
|
|
21
21
|
],
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@git.zone/tsbuild": "^4.
|
|
24
|
-
"@git.zone/tstest": "^
|
|
25
|
-
"@types/node": "^
|
|
23
|
+
"@git.zone/tsbuild": "^4.5.0",
|
|
24
|
+
"@git.zone/tstest": "^6.1.1",
|
|
25
|
+
"@types/node": "^26.5.1"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"test": "(pnpm run build && tstest test/ --verbose)",
|
package/readme.md
CHANGED
|
@@ -24,9 +24,11 @@ pnpm dlx @git.zone/tsdisk
|
|
|
24
24
|
tsdisk
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The tool prints a structured report covering filesystem usage,
|
|
27
|
+
The tool prints a structured report covering filesystem usage, large files and directories on mounted workspaces such as `/mnt/data`, known user and system cache paths, project-local tool caches, systemd journal usage, deleted open files, and Docker context storage. Both the default human report and `scan` print live progress every five seconds, including the current stage, discovered cache count, measured workspace bytes, potential cleanup bytes, and the largest paths found so far.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Potential cleanup is an estimate of measured cache candidates requiring owner review, not guaranteed free space. Explicitly protected caches and overlapping parent/child candidates are excluded from that estimate. Unknown sizes are counted separately. Hardlinks or shared filesystem extents can reduce actual savings. Backups, checkpoints, and worktrees appear as large disk consumers but are never automatically classified as removable. Confirmed reclaimable bytes remain unknown until the owning tool verifies cleanup eligibility.
|
|
30
|
+
|
|
31
|
+
It asks for `sudo` because root-owned filesystem paths and deleted open files require elevated permissions. The default diagnostic command does not delete caches or prune Docker data. The explicit `cleanup` command previews owning-tool cleanup and requires `--apply` to execute it.
|
|
30
32
|
|
|
31
33
|
### Project Tool Cache Diagnostics
|
|
32
34
|
|
|
@@ -34,13 +36,14 @@ It asks for `sudo` because root-owned filesystem paths and deleted open files re
|
|
|
34
36
|
|
|
35
37
|
- Deno self-extracting staging such as `dist/binaries/.*/.deno_compile_node_modules`
|
|
36
38
|
- Release binary staging under `.nogit/release-binaries/.*/.deno_compile_node_modules`
|
|
37
|
-
- tsbundle temp workspaces under `.nogit/tsbundle-temp`
|
|
39
|
+
- tsbundle temp workspaces under `.nogit/tsbundle-temp` and `.nogit/tsbundle-temp-*`
|
|
40
|
+
- Retained Rust matrix directories and renamed legacy Docker registries (report-only)
|
|
38
41
|
- tsdocker local registry caches under `.nogit/docker-registry`
|
|
39
42
|
- Rust target caches under `rust/target`, `ts_rust/target`, and marked `.nogit/tsrust-target`
|
|
40
43
|
- Stopped `tsdocker-registry-*` containers and their bind mounts
|
|
41
44
|
- Docker buildx state volumes with zero container links, as report-only findings
|
|
42
45
|
|
|
43
|
-
Marked caches
|
|
46
|
+
Markers identify ownership, never prove inactivity. Marked caches require review; `safeToPrune: false` remains explicitly protected. No cache finding receives a raw `rm -rf` command. Unmarked legacy paths, bundle intermediates, and retained matrix runs remain report-only. Bundle cleanup belongs to tsbundle’s own stale-workspace lifecycle.
|
|
44
47
|
|
|
45
48
|
## Environment
|
|
46
49
|
|
|
@@ -53,31 +56,53 @@ Marked caches using `.gitzone-tool-cache.json` are classified as tool-owned. Unm
|
|
|
53
56
|
| `TSDISK_KEEP_NCDU_EXPORT` | Keep the `ncdu` export for later browsing | unset |
|
|
54
57
|
| `TSDISK_INSTALL_NCDU` | Install `ncdu` with `apt-get` if missing | prompt |
|
|
55
58
|
|
|
56
|
-
##
|
|
59
|
+
## Structured Diagnostics
|
|
57
60
|
|
|
58
61
|
```bash
|
|
62
|
+
tsdisk scan --root /mnt/data/foss.global --json
|
|
63
|
+
tsdisk scan --root /mnt/data/project-a --root /mnt/data/project-b
|
|
59
64
|
tsdisk --json
|
|
60
65
|
tsdisk --help
|
|
61
66
|
tsdisk --version
|
|
62
67
|
```
|
|
63
68
|
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
`scan` and `--json` run bounded, noninteractive diagnostics without requesting sudo or installing tools. `--root` selects project-cache discovery and workspace size roots; host filesystem, home/cache, journal, deleted-open-file, and Docker diagnostics are still included. Without roots, discovery uses `/mnt/data` when present and the current directory. The original `tsdisk` human report remains available.
|
|
70
|
+
|
|
71
|
+
The JSON object includes `schemaVersion`, `generatedAt`, `roots`, `toolCaches`, `issues`, `issueCounts`, `usage`, `reclaim`, `sections`, and `complete`. Sections with `kind: "command"` record the executed command, arguments, exit code, stdout, stderr, and completeness. Workspace sections have `kind: "derived"`, `derivedFrom: "usage.entries"`, and `command: "usage-summary"`; their text is a compatibility summary of at most 100 retained paths, not native command output. Capped summaries set `result.truncated: true` and `complete: false`. Use `usage.entries` for structured paths and partial-size flags. Missing tools, command warnings, and insufficient permissions remain visible. JSON stdout contains exactly one document; five-second progress updates go to stderr.
|
|
72
|
+
|
|
73
|
+
`usage` records measured bytes, processed record count, the largest paths, cache paths, and completeness. It streams one native `du -a -0 -x -B1` pass over the selected roots, retaining partial ancestor totals even when a large directory exceeds the time budget. Discovered `.nogit` directories are measured first, so retained checkpoints and worktrees become visible before broad dependency trees. GNU du counts overlapping operands and hardlinks once; the report reconstructs inclusive ancestor totals without adding those bytes twice. `complete: false` sizes are lower bounds; human output marks them with `+`. Inclusive parent/child sizes must not be added together. File records are consumed as they arrive rather than retained in memory or exported to disk. The native pass includes dependencies, backups, and arbitrary large files. Overlapping roots on the same device are scanned once; explicitly selected nested mounts are retained.
|
|
74
|
+
|
|
75
|
+
`reclaim.candidateBytes` counts measured cache candidates without parent/child overlap; `unknownSizeCount` counts unsized candidates. `confirmedReclaimableBytes` is `null` and `status` is `owner-review-required`: discovery does not run project-installed cleanup tools or establish inactivity.
|
|
76
|
+
|
|
77
|
+
Cache discovery records read failures, invalid markers, symlinks, filesystem boundaries, and exhausted depth, entry, or time budgets. Unknown sizes stay absent rather than becoming zero. Symlink roots and roots with symlink ancestors are rejected. Standard dependency and generated-code directories are intentionally excluded. Cache byte sizes are allocated disk usage, not a promise of reclaimable space: parent/child findings and hardlinks may overlap.
|
|
78
|
+
|
|
79
|
+
Cache discovery visits directories breadth first, so nearby projects precede deep worktrees. It is bounded to depth 8, 50,000 directory/symlink inspections, and 45 seconds per root. Ordinary source files do not consume the directory budget; each directory listing is capped at 4,096 entries so a huge flat directory cannot starve its siblings. `issueCounts` retains totals by reason; `issues` contains at most 200 discovery examples per root, plus sizing issues. Sizing has a 45-second budget and a 500-cache limit. Workspace usage has one `DU_TIMEOUT` budget (default 300 seconds, plus five seconds for termination). Additional command diagnostics have a 120-second total budget, with at most 15 seconds per command. Discovery limits are cooperative between filesystem operations. `scan` exits `0` for a complete report, `2` for an incomplete report, or `1` for invalid input/failure. Consumers of the earlier `--json` shape can still read `toolCaches` and workspace command sections.
|
|
80
|
+
|
|
81
|
+
## Cleanup
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Project paths are explicit; cleanup never recurses across a workspace.
|
|
85
|
+
tsdisk cleanup --root /mnt/data/project --only=docker
|
|
86
|
+
# pnpm has no prune dry-run: preview identifies its configured store.
|
|
87
|
+
tsdisk cleanup --root /mnt/data/project --only=pnpm --json
|
|
88
|
+
# Execute only after reviewing the command and its scope.
|
|
89
|
+
tsdisk cleanup --root /mnt/data/project --only=pnpm --apply --yes
|
|
79
90
|
```
|
|
80
91
|
|
|
92
|
+
Cleanup defaults to a preview of `rust,docker` for the current project. `--dry-run` explicitly selects preview mode. Repeat `--root` to select multiple projects, and use `--only=rust,docker,pnpm` to choose categories. `pnpm` is opt-in because the configured store may be shared. Tsdisk never deletes a pnpm store directory.
|
|
93
|
+
|
|
94
|
+
Rust requires `@git.zone/tsrust` 1.13.2 or later; Docker requires `@git.zone/tsdocker` 3.6.0 or later. Rust and Docker use their project-installed `node_modules/.bin/tsrust` and `tsdocker`; missing tools block the batch without downloading anything. Rust previews pass `--days 14` by default; `--days N` changes only Rust retention. Docker cleanup uses the owning tool’s identity, mount, and build checks; its abandoned-container scope can span projects in the current Docker context. Registry directories are project-local. Bundle, matrix, and legacy registry findings do not become execution targets.
|
|
95
|
+
|
|
96
|
+
Rust rechecks retention, markers, cache identity, and active use at execution time. Active Rust tools or inaccessible process state (including protected same-user processes) block removal. Linux tsrust builds and pruning share a cache guard.
|
|
97
|
+
|
|
98
|
+
All owner previews must succeed without truncated output before any execution. `--apply` prints the plan and asks for confirmation; noninteractive or JSON execution additionally requires `--yes`. Each apply invokes the owning tool again so its safety checks run at execution time. A failed command stops subsequent actions and returns a nonzero exit code; completed actions are not rolled back. Commands time out after 120 seconds for preview or 240 seconds for apply, with a five-second termination grace period. Review owning-tool state before retrying an interrupted operation.
|
|
99
|
+
|
|
100
|
+
Cleanup runs as the project owner, without sudo, and refuses filesystem roots and symlink project paths. No plans or application state are persisted. This is a Linux CLI and uses `du`, `df`, `findmnt`, and GNU `timeout` from the host.
|
|
101
|
+
|
|
102
|
+
## Library API
|
|
103
|
+
|
|
104
|
+
`scanWorkspace(root, { maxDepth, maxEntries, timeoutMs, onProgress? })` returns findings, issues, issue counts, usage-priority paths, completion status, and the directory/symlink inspection count. `collectProjectToolCacheReport(context, roots?)` adds sizing. `collectDiskReport(context, roots?)` adds streaming workspace usage and host command diagnostics; `context.onProgress` receives live state updates. `measureDiskUsage(roots, { timeoutSeconds, trackedPaths?, priorityPaths?, onProgress? })` performs the standalone native usage pass. Priority paths only reorder descendants on the selected filesystem; they cannot expand scan scope. `estimateReclaimableSpace(findings)` returns the conservative candidate summary. The existing classification and byte-formatting exports remain available. `parseCliOptions(args)` and `createCleanupPlan(options)` expose validated CLI options and cleanup command plans; creating a plan does not execute cleanup.
|
|
105
|
+
|
|
81
106
|
## License and Legal Information
|
|
82
107
|
|
|
83
108
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/index.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
export { commitinfo } from './00_commitinfo_data.js';
|
|
2
2
|
export {
|
|
3
3
|
formatBytes,
|
|
4
|
+
collectDiskReport,
|
|
5
|
+
collectProjectToolCacheReport,
|
|
6
|
+
diagnosticCommandComplete,
|
|
4
7
|
main,
|
|
5
8
|
parseDuOutput,
|
|
6
9
|
runCli,
|
|
7
10
|
} from './tsdisk.cli.js';
|
|
8
11
|
export * from './tsdisk.toolcaches.js';
|
|
12
|
+
export * from './tsdisk.scan.js';
|
|
13
|
+
export * from './tsdisk.cleanup.js';
|
|
14
|
+
export * from './tsdisk.usage.js';
|
|
15
|
+
export { DiskProgressReporter, estimateReclaimableSpace } from './tsdisk.progress.js';
|
|
16
|
+
export type { IDiskProgress, IReclaimEstimate } from './tsdisk.progress.js';
|
|
17
|
+
export type { IRuntimeContext, IDiagnosticSection } from './tsdisk.cli.js';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as plugins from './tsdisk.plugins.js';
|
|
2
|
+
|
|
3
|
+
export type TCleanupCategory = 'rust' | 'docker' | 'pnpm';
|
|
4
|
+
|
|
5
|
+
export interface ICleanupAction {
|
|
6
|
+
category: TCleanupCategory;
|
|
7
|
+
cwd: string;
|
|
8
|
+
command: string;
|
|
9
|
+
previewArgs?: string[];
|
|
10
|
+
applyArgs: string[];
|
|
11
|
+
scope: string;
|
|
12
|
+
blocked?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ICleanupPlan {
|
|
16
|
+
dryRun: boolean;
|
|
17
|
+
actions: ICleanupAction[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ICliOptions {
|
|
21
|
+
command: 'diagnose' | 'scan' | 'cleanup' | 'help' | 'version';
|
|
22
|
+
roots: string[];
|
|
23
|
+
categories: TCleanupCategory[];
|
|
24
|
+
json: boolean;
|
|
25
|
+
apply: boolean;
|
|
26
|
+
yes: boolean;
|
|
27
|
+
days: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseCliOptions(args: string[]): ICliOptions {
|
|
31
|
+
const options: ICliOptions = { command: 'diagnose', roots: [], categories: ['rust', 'docker'], json: false, apply: false, yes: false, days: 14 };
|
|
32
|
+
let commandSeen = false;
|
|
33
|
+
let dryRun = false;
|
|
34
|
+
let cleanupOption = false;
|
|
35
|
+
for (let index = 0; index < args.length; index++) {
|
|
36
|
+
const arg = args[index];
|
|
37
|
+
if (arg === '--help' || arg === '-h') { options.command = 'help'; return options; }
|
|
38
|
+
if (arg === '--version' || arg === '-v') { options.command = 'version'; return options; }
|
|
39
|
+
if (arg === 'scan' || arg === 'cleanup') {
|
|
40
|
+
if (commandSeen) throw new Error('Specify one command: scan or cleanup.');
|
|
41
|
+
options.command = arg;
|
|
42
|
+
commandSeen = true;
|
|
43
|
+
} else if (arg === '--json') options.json = true;
|
|
44
|
+
else if (arg === '--apply') { options.apply = true; cleanupOption = true; }
|
|
45
|
+
else if (arg === '--yes' || arg === '-y') { options.yes = true; cleanupOption = true; }
|
|
46
|
+
else if (arg === '--dry-run' || arg === '-n') { dryRun = true; cleanupOption = true; }
|
|
47
|
+
else if (['--root', '--only', '--days'].some((flag) => arg === flag || arg.startsWith(`${flag}=`))) {
|
|
48
|
+
const separator = arg.indexOf('=');
|
|
49
|
+
const name = separator < 0 ? arg : arg.slice(0, separator);
|
|
50
|
+
const value = separator < 0 ? args[++index] : arg.slice(separator + 1);
|
|
51
|
+
if (!value || value.startsWith('--')) throw new Error(`Missing value for ${name}.`);
|
|
52
|
+
if (name === '--root') options.roots.push(plugins.path.resolve(value));
|
|
53
|
+
else if (name === '--only') {
|
|
54
|
+
cleanupOption = true;
|
|
55
|
+
const values = value.split(',');
|
|
56
|
+
if (values.some((category) => !['rust', 'docker', 'pnpm'].includes(category))) throw new Error('Cleanup categories: rust,docker,pnpm.');
|
|
57
|
+
options.categories = [...new Set(values)] as TCleanupCategory[];
|
|
58
|
+
} else {
|
|
59
|
+
cleanupOption = true;
|
|
60
|
+
options.days = Number(value);
|
|
61
|
+
if (!Number.isSafeInteger(options.days) || options.days < 1) throw new Error('--days must be a positive integer.');
|
|
62
|
+
}
|
|
63
|
+
} else throw new Error(`Unknown argument: ${arg}`);
|
|
64
|
+
}
|
|
65
|
+
if (cleanupOption && options.command !== 'cleanup') throw new Error('Cleanup options require the cleanup command.');
|
|
66
|
+
if (dryRun && options.apply) throw new Error('--dry-run and --apply cannot be combined.');
|
|
67
|
+
if (options.yes && !options.apply) throw new Error('--yes requires --apply.');
|
|
68
|
+
if (options.command === 'diagnose' && (options.roots.length || options.json)) options.command = 'scan';
|
|
69
|
+
options.roots = [...new Set(options.roots)];
|
|
70
|
+
return options;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Build project-scoped owner commands. No discovered marker authorizes deletion. */
|
|
74
|
+
export async function createCleanupPlan(options: ICliOptions): Promise<ICleanupPlan> {
|
|
75
|
+
if (options.command !== 'cleanup') throw new Error('Cleanup command required.');
|
|
76
|
+
if (!Number.isSafeInteger(options.days) || options.days < 1) throw new Error('Invalid retention period.');
|
|
77
|
+
if (!options.categories.length || options.categories.some((category) => !['rust', 'docker', 'pnpm'].includes(category))) throw new Error('Invalid cleanup category.');
|
|
78
|
+
const plan: ICleanupPlan = { dryRun: !options.apply, actions: [] };
|
|
79
|
+
for (const root of new Set(options.roots.length ? options.roots : [process.cwd()])) {
|
|
80
|
+
const cwd = plugins.path.resolve(root);
|
|
81
|
+
const stat = await plugins.fs.lstat(cwd);
|
|
82
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || await plugins.fs.realpath(cwd) !== cwd || cwd === plugins.path.parse(cwd).root) {
|
|
83
|
+
throw new Error(`Cleanup requires a real project directory: ${cwd}`);
|
|
84
|
+
}
|
|
85
|
+
// Running project-installed tools as root would grant project code host-wide authority.
|
|
86
|
+
if (process.getuid?.() === 0) throw new Error('Run cleanup as the project owner, without sudo.');
|
|
87
|
+
if (process.getuid && stat.uid !== process.getuid()) throw new Error(`Cleanup root is not owned by the current user: ${cwd}`);
|
|
88
|
+
for (const category of new Set(options.categories)) {
|
|
89
|
+
if (category === 'pnpm') {
|
|
90
|
+
plan.actions.push({ category, cwd, command: 'pnpm', applyArgs: ['store', 'prune'], scope: 'Prune unreferenced packages from the pnpm store configured for this project. The store may be shared. pnpm provides no prune dry-run.' });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const binary = category === 'rust' ? 'tsrust' : 'tsdocker';
|
|
94
|
+
const command = plugins.path.join(cwd, 'node_modules', '.bin', binary);
|
|
95
|
+
const args = category === 'rust' ? ['prune', '--days', String(options.days)] : ['prune'];
|
|
96
|
+
const action: ICleanupAction = {
|
|
97
|
+
category, cwd, command, previewArgs: args, applyArgs: [...args, '--apply'],
|
|
98
|
+
scope: category === 'rust'
|
|
99
|
+
? `Project's marked managed Rust targets; newest-file retention ${options.days} days, revalidated by tsrust.`
|
|
100
|
+
: 'Project registry caches and tsdocker-owned abandoned containers in the current Docker context; container scope may include other projects. tsdocker revalidates owner identity and active mounts.',
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
await plugins.fs.access(command, plugins.constants.X_OK);
|
|
104
|
+
const packagePath = plugins.path.join(cwd, 'node_modules', '@git.zone', binary, 'package.json');
|
|
105
|
+
const manifest = JSON.parse(await plugins.fs.readFile(packagePath, 'utf8')) as { name?: string; version?: string };
|
|
106
|
+
if (manifest.name !== `@git.zone/${binary}`) throw new Error('Unexpected owning package.');
|
|
107
|
+
const version = /^(\d+)\.(\d+)\.(\d+)$/.exec(manifest.version ?? '')?.slice(1).map(Number);
|
|
108
|
+
const minimum = category === 'docker' ? [3, 6, 0] : [1, 13, 2];
|
|
109
|
+
const different = version?.findIndex((part, index) => part !== minimum[index]);
|
|
110
|
+
if (!version || (different !== undefined && different !== -1 && version[different] < minimum[different])) {
|
|
111
|
+
action.blocked = `@git.zone/${binary} ${minimum.join('.')} or later is required for the documented prune contract.`;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
action.blocked = `Install a current @git.zone/${binary} in this project before cleanup. No tool is downloaded automatically.`;
|
|
115
|
+
}
|
|
116
|
+
plan.actions.push(action);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return plan;
|
|
120
|
+
}
|