@git.zone/tsdisk 1.1.0 → 1.2.1

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.
@@ -1,6 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { createReadStream, type ReadStream } from 'node:fs';
2
+ import { constants, createReadStream, type ReadStream } from 'node:fs';
3
3
  import * as fs from 'node:fs/promises';
4
4
  import * as path from 'node:path';
5
5
  import { createInterface } from 'node:readline/promises';
6
- export { createInterface, createReadStream, fs, path, spawn, type ReadStream, };
6
+ export { constants, createInterface, createReadStream, fs, path, spawn, type ReadStream, };
@@ -1,8 +1,8 @@
1
1
  // node native
2
2
  import { spawn } from 'node:child_process';
3
- import { createReadStream } from 'node:fs';
3
+ import { constants, createReadStream } from 'node:fs';
4
4
  import * as fs from 'node:fs/promises';
5
5
  import * as path from 'node:path';
6
6
  import { createInterface } from 'node:readline/promises';
7
- export { createInterface, createReadStream, fs, path, spawn, };
8
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHNkaXNrLnBsdWdpbnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy90c2Rpc2sucGx1Z2lucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjO0FBQ2QsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQzNDLE9BQU8sRUFBRSxnQkFBZ0IsRUFBbUIsTUFBTSxTQUFTLENBQUM7QUFDNUQsT0FBTyxLQUFLLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUN2QyxPQUFPLEtBQUssSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUNsQyxPQUFPLEVBQUUsZUFBZSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFFekQsT0FBTyxFQUNMLGVBQWUsRUFDZixnQkFBZ0IsRUFDaEIsRUFBRSxFQUNGLElBQUksRUFDSixLQUFLLEdBRU4sQ0FBQyJ9
7
+ export { constants, createInterface, createReadStream, fs, path, spawn, };
8
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHNkaXNrLnBsdWdpbnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy90c2Rpc2sucGx1Z2lucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjO0FBQ2QsT0FBTyxFQUFFLEtBQUssRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQzNDLE9BQU8sRUFBRSxTQUFTLEVBQUUsZ0JBQWdCLEVBQW1CLE1BQU0sU0FBUyxDQUFDO0FBQ3ZFLE9BQU8sS0FBSyxFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDdkMsT0FBTyxLQUFLLElBQUksTUFBTSxXQUFXLENBQUM7QUFDbEMsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRXpELE9BQU8sRUFDTCxTQUFTLEVBQ1QsZUFBZSxFQUNmLGdCQUFnQixFQUNoQixFQUFFLEVBQ0YsSUFBSSxFQUNKLEtBQUssR0FFTixDQUFDIn0=
@@ -0,0 +1,19 @@
1
+ import { type IToolCacheFinding } from './tsdisk.toolcaches.js';
2
+ export interface IWorkspaceScanOptions {
3
+ maxDepth: number;
4
+ timeoutMs: number;
5
+ maxEntries: number;
6
+ }
7
+ export interface IScanIssue {
8
+ path: string;
9
+ reason: 'read-error' | 'invalid-marker' | 'depth-limit' | 'entry-limit' | 'timeout' | 'mount-boundary' | 'symlink' | 'size-unavailable';
10
+ detail: string;
11
+ }
12
+ export interface IWorkspaceScanResult {
13
+ root: string;
14
+ findings: IToolCacheFinding[];
15
+ issues: IScanIssue[];
16
+ complete: boolean;
17
+ visitedEntries: number;
18
+ }
19
+ export declare function scanWorkspace(root: string, options: IWorkspaceScanOptions): Promise<IWorkspaceScanResult>;
@@ -0,0 +1,118 @@
1
+ import * as plugins from './tsdisk.plugins.js';
2
+ import { classifyToolCachePath, parseToolCacheMarker, toolCacheMarkerFile } from './tsdisk.toolcaches.js';
3
+ const excludedDirectories = new Set(['.git', 'node_modules', 'dist_ts', 'dist_ts_web', 'coverage', '.pnpm-store']);
4
+ export async function scanWorkspace(root, options) {
5
+ for (const name of ['maxDepth', 'timeoutMs', 'maxEntries']) {
6
+ const value = options[name];
7
+ if (!Number.isSafeInteger(value) || value < (name === 'maxDepth' ? 0 : 1)) {
8
+ throw new Error(`Invalid scan option ${name}: ${value}`);
9
+ }
10
+ }
11
+ const result = { root: plugins.path.resolve(root), findings: [], issues: [], complete: true, visitedEntries: 0 };
12
+ const deadline = Date.now() + options.timeoutMs;
13
+ let stopped = false;
14
+ const issue = (path, reason, detail) => {
15
+ result.complete = false;
16
+ result.issues.push({ path, reason, detail });
17
+ };
18
+ const limited = (path) => {
19
+ if (stopped)
20
+ return true;
21
+ if (Date.now() >= deadline || result.visitedEntries >= options.maxEntries) {
22
+ issue(path, Date.now() >= deadline ? 'timeout' : 'entry-limit', 'Scan stopped before visiting all entries.');
23
+ stopped = true;
24
+ }
25
+ return stopped;
26
+ };
27
+ let rootDev;
28
+ try {
29
+ const stat = await plugins.fs.lstat(result.root);
30
+ if (stat.isSymbolicLink() || await plugins.fs.realpath(result.root) !== result.root) {
31
+ issue(result.root, 'symlink', 'Scan roots must not contain symbolic links.');
32
+ return result;
33
+ }
34
+ if (!stat.isDirectory())
35
+ throw new Error('Scan root is not a directory.');
36
+ rootDev = stat.dev;
37
+ }
38
+ catch (error) {
39
+ issue(result.root, 'read-error', error instanceof Error ? error.message : String(error));
40
+ return result;
41
+ }
42
+ const visit = async (directory, depth) => {
43
+ if (limited(directory))
44
+ return;
45
+ result.visitedEntries++;
46
+ try {
47
+ const stat = await plugins.fs.lstat(directory);
48
+ if (stat.isSymbolicLink()) {
49
+ issue(directory, 'symlink', 'Symbolic link skipped.');
50
+ return;
51
+ }
52
+ if (stat.dev !== rootDev) {
53
+ issue(directory, 'mount-boundary', 'Different filesystem skipped.');
54
+ return;
55
+ }
56
+ const entries = await plugins.fs.readdir(directory, { withFileTypes: true });
57
+ let marker;
58
+ if (entries.some((entry) => entry.name === toolCacheMarkerFile)) {
59
+ const markerPath = plugins.path.join(directory, toolCacheMarkerFile);
60
+ const markerStat = await plugins.fs.lstat(markerPath);
61
+ if (!markerStat.isFile() || markerStat.size > 65536) {
62
+ issue(markerPath, 'invalid-marker', 'Marker must be a regular file of at most 64 KiB.');
63
+ }
64
+ else {
65
+ marker = parseToolCacheMarker(await plugins.fs.readFile(markerPath, 'utf8'));
66
+ if (!marker)
67
+ issue(markerPath, 'invalid-marker', 'Invalid cache ownership marker.');
68
+ }
69
+ }
70
+ const heuristic = classifyToolCachePath(directory);
71
+ const classification = classifyToolCachePath(directory, marker);
72
+ if (classification)
73
+ result.findings.push({ path: directory, ...classification });
74
+ // Cache contents are not workspaces. Registry session directories can carry their own markers.
75
+ if (classification && heuristic?.kind !== 'docker-registry-cache')
76
+ return;
77
+ const children = entries.filter((entry) => !excludedDirectories.has(entry.name));
78
+ if (depth >= options.maxDepth) {
79
+ if (children.some((entry) => entry.isDirectory() || entry.isSymbolicLink()))
80
+ issue(directory, 'depth-limit', `Maximum scan depth ${options.maxDepth} reached.`);
81
+ return;
82
+ }
83
+ for (const child of children) {
84
+ const childPath = plugins.path.join(directory, child.name);
85
+ if (limited(childPath))
86
+ return;
87
+ if (!child.isDirectory() && !child.isSymbolicLink()) {
88
+ result.visitedEntries++;
89
+ continue;
90
+ }
91
+ if (child.isSymbolicLink()) {
92
+ result.visitedEntries++;
93
+ issue(childPath, 'symlink', 'Symbolic link skipped.');
94
+ continue;
95
+ }
96
+ if (heuristic?.kind === 'docker-registry-cache') {
97
+ // Only direct sessions, never registry blob trees.
98
+ const sessionMarker = await plugins.fs.lstat(plugins.path.join(childPath, toolCacheMarkerFile)).catch((error) => {
99
+ if (error.code === 'ENOENT')
100
+ return undefined;
101
+ throw error;
102
+ });
103
+ if (!sessionMarker) {
104
+ result.visitedEntries++;
105
+ continue;
106
+ }
107
+ }
108
+ await visit(childPath, depth + 1);
109
+ }
110
+ }
111
+ catch (error) {
112
+ issue(directory, 'read-error', error instanceof Error ? error.message : String(error));
113
+ }
114
+ };
115
+ await visit(result.root, 0);
116
+ return result;
117
+ }
118
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHNkaXNrLnNjYW4uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy90c2Rpc2suc2Nhbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLHFCQUFxQixDQUFDO0FBQy9DLE9BQU8sRUFBRSxxQkFBcUIsRUFBRSxvQkFBb0IsRUFBRSxtQkFBbUIsRUFBMEIsTUFBTSx3QkFBd0IsQ0FBQztBQXNCbEksTUFBTSxtQkFBbUIsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxjQUFjLEVBQUUsU0FBUyxFQUFFLGFBQWEsRUFBRSxVQUFVLEVBQUUsYUFBYSxDQUFDLENBQUMsQ0FBQztBQUVuSCxNQUFNLENBQUMsS0FBSyxVQUFVLGFBQWEsQ0FBQyxJQUFZLEVBQUUsT0FBOEI7SUFDOUUsS0FBSyxNQUFNLElBQUksSUFBSSxDQUFDLFVBQVUsRUFBRSxXQUFXLEVBQUUsWUFBWSxDQUFVLEVBQUUsQ0FBQztRQUNwRSxNQUFNLEtBQUssR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDNUIsSUFBSSxDQUFDLE1BQU0sQ0FBQyxhQUFhLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxHQUFHLENBQUMsSUFBSSxLQUFLLFVBQVUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO1lBQzFFLE1BQU0sSUFBSSxLQUFLLENBQUMsdUJBQXVCLElBQUksS0FBSyxLQUFLLEVBQUUsQ0FBQyxDQUFDO1FBQzNELENBQUM7SUFDSCxDQUFDO0lBQ0QsTUFBTSxNQUFNLEdBQXlCLEVBQUUsSUFBSSxFQUFFLE9BQU8sQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxFQUFFLFFBQVEsRUFBRSxFQUFFLEVBQUUsTUFBTSxFQUFFLEVBQUUsRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLGNBQWMsRUFBRSxDQUFDLEVBQUUsQ0FBQztJQUN2SSxNQUFNLFFB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
@@ -1,12 +1,12 @@
1
- import * as path from 'node:path';
1
+ import { path } from './tsdisk.plugins.js';
2
2
  export const toolCacheMarkerFile = '.gitzone-tool-cache.json';
3
3
  export function parseToolCacheMarker(rawArg) {
4
4
  try {
5
5
  const marker = JSON.parse(rawArg);
6
- if (typeof marker.owner === 'string' &&
7
- typeof marker.kind === 'string' &&
8
- marker.safeToPrune === true &&
9
- typeof marker.createdAt === 'string' &&
6
+ if (typeof marker.owner === 'string' && marker.owner.length > 0 &&
7
+ typeof marker.kind === 'string' && marker.kind.length > 0 &&
8
+ typeof marker.safeToPrune === 'boolean' &&
9
+ typeof marker.createdAt === 'string' && Number.isFinite(Date.parse(marker.createdAt)) &&
10
10
  marker.schemaVersion === 1) {
11
11
  return marker;
12
12
  }
@@ -48,8 +48,10 @@ export function classifyToolCachePath(pathArg, markerArg) {
48
48
  return {
49
49
  ownerGuess: markerArg.owner,
50
50
  kind: markerArg.kind,
51
- risk: markerArg.safeToPrune ? 'safe-marked' : 'review',
52
- suggestedCleanupCommand: `rm -rf ${quotePath(pathArg)}`,
51
+ risk: 'review',
52
+ suggestedCleanupCommand: markerArg.safeToPrune
53
+ ? `report-only: ownership marker is not proof of inactivity; use the owning tool to review ${quotePath(pathArg)}`
54
+ : `protected: owner explicitly disallows pruning ${quotePath(pathArg)}`,
53
55
  marker: markerArg,
54
56
  };
55
57
  }
@@ -69,7 +71,7 @@ export function classifyToolCachePath(pathArg, markerArg) {
69
71
  suggestedCleanupCommand: `report-only: verify ownership before removing ${quotePath(path.dirname(pathArg))}`,
70
72
  };
71
73
  }
72
- if (normalized.endsWith('/.nogit/tsbundle-temp')) {
74
+ if (/\/\.nogit\/tsbundle-temp(?:-[^/]+)?$/.test(normalized)) {
73
75
  return {
74
76
  ownerGuess: '@git.zone/tsbundle',
75
77
  kind: 'bundle-temp-workspace',
@@ -77,6 +79,22 @@ export function classifyToolCachePath(pathArg, markerArg) {
77
79
  suggestedCleanupCommand: `report-only: verify ownership before removing ${quotePath(pathArg)}`,
78
80
  };
79
81
  }
82
+ if (/\/\.nogit\/tsrust-matrix(?:-archive|-probes)?$/.test(normalized)) {
83
+ return {
84
+ ownerGuess: '@git.zone/tsrust',
85
+ kind: 'rust-matrix-artifacts',
86
+ risk: 'report-only',
87
+ suggestedCleanupCommand: `report-only: retained matrix runs may be needed for recovery; inspect ${quotePath(pathArg)}`,
88
+ };
89
+ }
90
+ if (/\/\.nogit\/(?:docker-registry\.[^/]+|docker-registry-v1-cache|dockerimagestore)$/.test(normalized)) {
91
+ return {
92
+ ownerGuess: '@git.zone/tsdocker',
93
+ kind: 'legacy-docker-registry-cache',
94
+ risk: 'report-only',
95
+ suggestedCleanupCommand: `report-only: verify release recovery requirements before removing ${quotePath(pathArg)}`,
96
+ };
97
+ }
80
98
  if (normalized.endsWith('/.nogit/docker-registry') || normalized.includes('/.nogit/docker-registry/')) {
81
99
  return {
82
100
  ownerGuess: '@git.zone/tsdocker',
@@ -103,4 +121,4 @@ export function classifyToolCachePath(pathArg, markerArg) {
103
121
  }
104
122
  return undefined;
105
123
  }
106
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHNkaXNrLnRvb2xjYWNoZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy90c2Rpc2sudG9vbGNhY2hlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssSUFBSSxNQUFNLFdBQVcsQ0FBQztBQUVsQyxNQUFNLENBQUMsTUFBTSxtQkFBbUIsR0FBRywwQkFBMEIsQ0FBQztBQXlCOUQsTUFBTSxVQUFVLG9CQUFvQixDQUFDLE1BQWM7SUFDakQsSUFBSSxDQUFDO1FBQ0gsTUFBTSxNQUFNLEdBQUcsSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQThCLENBQUM7UUFDL0QsSUFDRSxPQUFPLE1BQU0sQ0FBQyxLQUFLLEtBQUssUUFBUTtZQUNoQyxPQUFPLE1BQU0sQ0FBQyxJQUFJLEtBQUssUUFBUTtZQUMvQixNQUFNLENBQUMsV0FBVyxLQUFLLElBQUk7WUFDM0IsT0FBTyxNQUFNLENBQUMsU0FBUyxLQUFLLFFBQVE7WUFDcEMsTUFBTSxDQUFDLGFBQWEsS0FBSyxDQUFDLEVBQzFCLENBQUM7WUFDRCxPQUFPLE1BQTBCLENBQUM7UUFDcEMsQ0FBQztJQUNILENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBRUQsT0FBTyxTQUFTLENBQUM7QUFDbkIsQ0FBQztBQUVELFNBQVMsU0FBUyxDQUFDLE9BQWU7SUFDaEMsT0FBTyxJQUFJLE9BQU8sQ0FBQyxPQUFPLENBQUMsWUFBWSxFQUFFLE1BQU0sQ0FBQyxHQUFHLENBQUM7QUFDdEQsQ0FBQztBQUVELFNBQVMsY0FBYyxDQUFDLE9BQWU7SUFDckMsT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxLQUFLLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDbkQsQ0FBQztBQUVELFNBQVMsZ0JBQWdCLENBQUMsT0FBZTtJQUN2QyxNQUFNLFVBQVUsR0FBRyxjQUFjLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDM0MsTUFBTSxVQUFVLEdBQUcsVUFBVSxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUNsRCxJQUFJLFVBQVUsR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUNuQixPQUFPLFVBQVUsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLFVBQVUsQ0FBQyxDQUFDO0lBQ3pDLENBQUM7SUFDRCxNQUFNLGVBQWUsR0FBRyxVQUFVLENBQUMsT0FBTyxDQUFDLGNBQWMsQ0FBQyxDQUFDO0lBQzNELElBQUksZUFBZSxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ3hCLE9BQU8sVUFBVSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsZUFBZSxDQUFDLENBQUM7SUFDOUMsQ0FBQztJQUNELE1BQU0saUJBQWlCLEdBQUcsVUFBVSxDQUFDLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBQ2hFLElBQUksaUJBQWlCLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDMUIsT0FBTyxVQUFVLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxpQkFBaUIsQ0FBQyxDQUFDO0lBQ2hELENBQUM7SUFDRCxNQUFNLFNBQVMsR0FBRyxVQUFVLENBQUMsT0FBTyxDQUFDLGlCQUFpQixDQUFDLENBQUM7SUFDeEQsSUFBSSxTQUFTLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDbEIsT0FBTyxVQUFVLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxTQUFTLENBQUMsQ0FBQztJQUN4QyxDQUFDO0lBQ0QsT0FBTyxJQUFJLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0FBQy9CLENBQUM7QUFFRCxNQUFNLFVBQVUscUJBQXFCLENBQUMsT0FBZSxFQUFFLFNBQTRCO0lBQ2pGLE1BQU0sVUFBVSxHQUFHLGNBQWMsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUMzQyxJQUFJLFNBQVMsRUFBRSxDQUFDO1FBQ2QsT0FBTztZQUNMLFVBQVUsRUFBRSxTQUFTLENBQUMsS0FBSztZQUMzQixJQUFJLEVBQUUsU0FBUyxDQUFDLElBQUk7WUFDcEIsSUFBSSxFQUFFLFNBQVMsQ0FBQyxXQUFXLENBQUMsQ0FBQyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsUUFBUTtZQUN0RCx1QkFBdUIsRUFBRSxVQUFVLFNBQVMsQ0FBQyxPQUFPLENBQUMsRUFBRTtZQUN2RCxNQUFNLEVBQUUsU0FBUztTQUNsQixDQUFDO0lBQ0osQ0FBQztJQUVELElBQUksZ0VBQWdFLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQUM7UUFDdEYsT0FBTztZQUNMLFVBQVUsRUFBRSxrQkFBa0I7WUFDOUIsSUFBSSxFQUFFLDhCQUE4QjtZQUNwQyxJQUFJLEVBQUUsYUFBYTtZQUNuQix1QkFBdUIsRUFBRSxpREFBaUQsU0FBUyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUMsRUFBRTtTQUM3RyxDQUFDO0lBQ0osQ0FBQztJQUVELElBQUksMkVBQTJFLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQUM7UUFDakcsT0FBTztZQUNMLFVBQVUsRUFBRSxrQkFBa0I7WUFDOUIsSUFBSSxFQUFFLHNDQUFzQztZQUM1QyxJQUFJLEVBQUUsYUFBYTtZQUNuQix1QkFBdUIsRUFBRSxpREFBaUQsU0FBUyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUMsRUFBRTtTQUM3RyxDQUFDO0lBQ0osQ0FBQztJQUVELElBQUksVUFBVSxDQUFDLFFBQVEsQ0FBQyx1QkFBdUIsQ0FBQyxFQUFFLENBQUM7UUFDakQsT0FBTztZQUNMLFVBQVUsRUFBRSxvQkFBb0I7WUFDaEMsSUFBSSxFQUFFLHVCQUF1QjtZQUM3QixJQUFJLEVBQUUsYUFBYTtZQUNuQix1QkFBdUIsRUFBRSxpREFBaUQsU0FBUyxDQUFDLE9BQU8sQ0FBQyxFQUFFO1NBQy9GLENBQUM7SUFDSixDQUFDO0lBRUQsSUFBSSxVQUFVLENBQUMsUUFBUSxDQUFDLHlCQUF5QixDQUFDLElBQUksVUFBVSxDQUFDLFFBQVEsQ0FBQywwQkFBMEIsQ0FBQyxFQUFFLENBQUM7UUFDdEcsT0FBTztZQUNMLFVBQVUsRUFBRSxvQkFBb0I7WUFDaEMsSUFBSSxFQUFFLHVCQUF1QjtZQUM3QixJQUFJLEVBQUUsYUFBYTtZQUNuQix1QkFBdUIsRUFBRSxnREFBZ0QsU0FBUyxDQUFDLGdCQUFnQixDQUFDLE9BQU8sQ0FBQyxDQUFDLG9DQUFvQztTQUNsSixDQUFDO0lBQ0osQ0FBQztJQUVELElBQUksVUFBVSxDQUFDLFFBQVEsQ0FBQyxjQUFjLENBQUMsSUFBSSxVQUFVLENBQUMsUUFBUSxDQUFDLGlCQUFpQixDQUFDLEVBQUUsQ0FBQztRQUNsRixPQUFPO1lBQ0wsVUFBVSxFQUFFLHdCQUF3QjtZQUNwQyxJQUFJLEVBQUUsbUJBQW1CO1lBQ3pCLElBQUksRUFBRSxhQUFhO1lBQ25CLHVCQUF1QixFQUFFLDhDQUE4QyxTQUFTLENBQUMsZ0JBQWdCLENBQUMsT0FBTyxDQUFDLENBQUMsNENBQTRDO1NBQ3hKLENBQUM7SUFDSixDQUFDO0lBRUQsSUFBSSxVQUFVLENBQUMsUUFBUSxDQUFDLHVCQUF1QixDQUFDLEVBQUUsQ0FBQztRQUNqRCxPQUFPO1lBQ0wsVUFBVSxFQUFFLGtCQUFrQjtZQUM5QixJQUFJLEVBQUUsbUJBQW1CO1lBQ3pCLElBQUksRUFBRSxhQUFhO1lBQ25CLHVCQUF1QixFQUFFLDhDQUE4QyxTQUFTLENBQUMsZ0JBQWdCLENBQUMsT0FBTyxDQUFDLENBQUMsNENBQTRDO1NBQ3hKLENBQUM7SUFDSixDQUFDO0lBRUQsT0FBTyxTQUFTLENBQUM7QUFDbkIsQ0FBQyJ9
124
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHNkaXNrLnRvb2xjYWNoZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy90c2Rpc2sudG9vbGNhY2hlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsSUFBSSxFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFM0MsTUFBTSxDQUFDLE1BQU0sbUJBQW1CLEdBQUcsMEJBQTBCLENBQUM7QUF5QjlELE1BQU0sVUFBVSxvQkFBb0IsQ0FBQyxNQUFjO0lBQ2pELElBQUksQ0FBQztRQUNILE1BQU0sTUFBTSxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsTUFBTSxDQUE4QixDQUFDO1FBQy9ELElBQ0UsT0FBTyxNQUFNLENBQUMsS0FBSyxLQUFLLFFBQVEsSUFBSSxNQUFNLENBQUMsS0FBSyxDQUFDLE1BQU0sR0FBRyxDQUFDO1lBQzNELE9BQU8sTUFBTSxDQUFDLElBQUksS0FBSyxRQUFRLElBQUksTUFBTSxDQUFDLElBQUksQ0FBQyxNQUFNLEdBQUcsQ0FBQztZQUN6RCxPQUFPLE1BQU0sQ0FBQyxXQUFXLEtBQUssU0FBUztZQUN2QyxPQUFPLE1BQU0sQ0FBQyxTQUFTLEtBQUssUUFBUSxJQUFJLE1BQU0sQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUM7WUFDckYsTUFBTSxDQUFDLGFBQWEsS0FBSyxDQUFDLEVBQzFCLENBQUM7WUFDRCxPQUFPLE1BQTBCLENBQUM7UUFDcEMsQ0FBQztJQUNILENBQUM7SUFBQyxNQUFNLENBQUM7UUFDUCxPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBRUQsT0FBTyxTQUFTLENBQUM7QUFDbkIsQ0FBQztBQUVELFNBQVMsU0FBUyxDQUFDLE9BQWU7SUFDaEMsT0FBTyxJQUFJLE9BQU8sQ0FBQyxPQUFPLENBQUMsWUFBWSxFQUFFLE1BQU0sQ0FBQyxHQUFHLENBQUM7QUFDdEQsQ0FBQztBQUVELFNBQVMsY0FBYyxDQUFDLE9B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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@git.zone/tsdisk",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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.4.2",
24
- "@git.zone/tstest": "^3.6.6",
25
- "@types/node": "^25.9.3"
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
@@ -26,7 +26,7 @@ tsdisk
26
26
 
27
27
  The tool prints a structured report covering filesystem usage, top-level `du` breakdowns, known user and system cache paths, project-local tool caches on mounted workspaces such as `/mnt/data`, systemd journal usage, deleted open files, and Docker context storage.
28
28
 
29
- It asks for `sudo` because root-owned filesystem paths and deleted open files require elevated permissions. Cleanup commands are printed as suggestions only; `tsdisk` does not delete caches or prune Docker data.
29
+ 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
30
 
31
31
  ### Project Tool Cache Diagnostics
32
32
 
@@ -34,13 +34,14 @@ It asks for `sudo` because root-owned filesystem paths and deleted open files re
34
34
 
35
35
  - Deno self-extracting staging such as `dist/binaries/.*/.deno_compile_node_modules`
36
36
  - Release binary staging under `.nogit/release-binaries/.*/.deno_compile_node_modules`
37
- - tsbundle temp workspaces under `.nogit/tsbundle-temp`
37
+ - tsbundle temp workspaces under `.nogit/tsbundle-temp` and `.nogit/tsbundle-temp-*`
38
+ - Retained Rust matrix directories and renamed legacy Docker registries (report-only)
38
39
  - tsdocker local registry caches under `.nogit/docker-registry`
39
40
  - Rust target caches under `rust/target`, `ts_rust/target`, and marked `.nogit/tsrust-target`
40
41
  - Stopped `tsdocker-registry-*` containers and their bind mounts
41
42
  - Docker buildx state volumes with zero container links, as report-only findings
42
43
 
43
- Marked caches using `.gitzone-tool-cache.json` are classified as tool-owned. Unmarked legacy paths are report-only and do not receive destructive cleanup commands.
44
+ 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
45
 
45
46
  ## Environment
46
47
 
@@ -53,31 +54,49 @@ Marked caches using `.gitzone-tool-cache.json` are classified as tool-owned. Unm
53
54
  | `TSDISK_KEEP_NCDU_EXPORT` | Keep the `ncdu` export for later browsing | unset |
54
55
  | `TSDISK_INSTALL_NCDU` | Install `ncdu` with `apt-get` if missing | prompt |
55
56
 
56
- ## Commands
57
+ ## Structured Diagnostics
57
58
 
58
59
  ```bash
60
+ tsdisk scan --root /mnt/data/foss.global --json
61
+ tsdisk scan --root /mnt/data/project-a --root /mnt/data/project-b
59
62
  tsdisk --json
60
63
  tsdisk --help
61
64
  tsdisk --version
62
65
  ```
63
66
 
64
- `tsdisk --json` prints the project tool-cache diagnostics as structured JSON:
65
-
66
- ```json
67
- {
68
- "toolCaches": [
69
- {
70
- "path": "/mnt/data/project/.nogit/tsbundle-temp",
71
- "ownerGuess": "@git.zone/tsbundle",
72
- "kind": "bundle-temp-workspace",
73
- "risk": "report-only",
74
- "suggestedCleanupCommand": "report-only: verify ownership before removing \"/mnt/data/project/.nogit/tsbundle-temp\"",
75
- "bytes": 123456
76
- }
77
- ]
78
- }
67
+ `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.
68
+
69
+ The JSON object includes `schemaVersion`, `generatedAt`, `roots`, `toolCaches`, `issues`, `sections`, and `complete`. Each command section records its command, arguments, exit code, stdout, stderr, and completeness. Missing tools and insufficient permissions remain visible. JSON output contains one document; diagnostics never print progress into it.
70
+
71
+ 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.
72
+
73
+ Discovery is bounded to depth 8, 50,000 visited entries, and 45 seconds per root. Sizing has a 45-second budget and a 500-cache limit. Additional command diagnostics have a 120-second total budget, with at most 15 seconds per command. 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`.
74
+
75
+ ## Cleanup
76
+
77
+ ```bash
78
+ # Project paths are explicit; cleanup never recurses across a workspace.
79
+ tsdisk cleanup --root /mnt/data/project --only=docker
80
+ # pnpm has no prune dry-run: preview identifies its configured store.
81
+ tsdisk cleanup --root /mnt/data/project --only=pnpm --json
82
+ # Execute only after reviewing the command and its scope.
83
+ tsdisk cleanup --root /mnt/data/project --only=pnpm --apply --yes
79
84
  ```
80
85
 
86
+ 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.
87
+
88
+ 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.
89
+
90
+ 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.
91
+
92
+ 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.
93
+
94
+ 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.
95
+
96
+ ## Library API
97
+
98
+ `scanWorkspace(root, { maxDepth, maxEntries, timeoutMs })` returns findings, issues, completion status, and the visited-entry count. `collectProjectToolCacheReport(context, roots?)` adds sizing. `collectDiskReport(context, roots?)` adds host command diagnostics. 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.
99
+
81
100
  ## License and Legal Information
82
101
 
83
102
  This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@git.zone/tsdisk',
6
- version: '1.1.0',
6
+ version: '1.2.1',
7
7
  description: 'A disk usage diagnostic CLI for finding large files, caches, logs, and Docker storage.'
8
8
  }
package/ts/index.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  export { commitinfo } from './00_commitinfo_data.js';
2
2
  export {
3
3
  formatBytes,
4
+ collectDiskReport,
5
+ collectProjectToolCacheReport,
4
6
  main,
5
7
  parseDuOutput,
6
8
  runCli,
7
9
  } from './tsdisk.cli.js';
8
10
  export * from './tsdisk.toolcaches.js';
11
+ export * from './tsdisk.scan.js';
12
+ export * from './tsdisk.cleanup.js';
13
+ 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
+ }