@webpieces/rules-config 0.4.662 → 0.4.663
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/package.json +1 -1
- package/src/home-config.d.ts +28 -9
- package/src/home-config.js +62 -30
- package/src/home-config.js.map +1 -1
- package/src/index.d.ts +4 -1
- package/src/index.js +26 -6
- package/src/index.js.map +1 -1
- package/src/orphan-dir-archive.d.ts +109 -0
- package/src/orphan-dir-archive.js +256 -0
- package/src/orphan-dir-archive.js.map +1 -0
- package/src/orphan-dir-scan.d.ts +89 -0
- package/src/orphan-dir-scan.js +150 -0
- package/src/orphan-dir-scan.js.map +1 -0
- package/src/orphan-dir-sweep.d.ts +69 -0
- package/src/orphan-dir-sweep.js +142 -0
- package/src/orphan-dir-sweep.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orphan-dir-scan.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/orphan-dir-scan.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\nimport { spawnSync } from 'child_process';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * Finds ORPHAN DIRECTORIES — the package directories an `nx g move` leaves behind on every clone, which\n * git cannot remove and which then accumulate on every developer's machine forever.\n *\n * ─── WHY GIT LEAVES THEM ──────────────────────────────────────────────────────────────────────────────\n * Git tracks files, not directories. When a move deletes every tracked file under `libraries/apis/foo/`,\n * git removes that directory ONLY if it is then truly empty — and it never is, because the build already\n * put an IGNORED `dist/` and `node_modules/` inside it. So the corpse stands, on every clone, and there\n * is no git setting that changes this (`core.pruneIgnoredDirs` does not exist).\n *\n * ─── WHY THE PREDICATE IS GIT'S OWN ANSWER AND NOT A HAND-ROLLED WALK ─────────────────────────────────\n * `git clean -Xdn` collapses a directory to a SINGLE entry exactly when everything beneath it is ignored,\n * and it does that using the repo's real ignore engine — nested `.gitignore` files, `!` negations and\n * all. Re-implementing that is how a sweeper ends up disagreeing with git about what is disposable.\n *\n * It also hands us the safety property for free, which is the reason this can run unattended: a directory\n * holding even ONE untracked-but-not-ignored file is NOT reported, so a developer's uncommitted new\n * package can never be a candidate. We do not check that ourselves; git's own output already encodes it.\n *\n * ─── THE ONE DISCRIMINATOR THAT NEEDS NO NAME LIST ────────────────────────────────────────────────────\n * `clean -Xdn` reports two very different things the same way:\n *\n * libraries/apis/pg-dataaccess-api/ ← a corpse: nothing tracked survives under it\n * libraries/apis/live-api/dist/ ← a LIVE project's build output, there on purpose\n *\n * They are separated by asking whether the directory ITSELF matches an ignore rule. `dist/` does\n * (`.gitignore:19:dist/`); `pg-dataaccess-api` does not, which is precisely the statement that this\n * directory was MEANT to hold tracked files — and none are left. No list of artifact directory names is\n * needed, and none is maintained, so the rule adapts to whatever any given consumer repo ignores.\n *\n * That also yields the opt-out, and it is one a developer would reach for anyway: adding `my-sandbox/`\n * to `.gitignore` makes the directory ignore-matched, which permanently protects it from this sweep.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class OrphanDirScanner {\n /**\n * Every orphan directory under `repoRoot`, as repo-RELATIVE paths with no trailing slash. Empty when\n * the tree is clean, when `repoRoot` is not a git repository, or when git cannot be run at all — a\n * sweeper is never important enough to fail somebody's checkout over.\n */\n scan(repoRoot: string): OrphanCandidate[] {\n const reported = this.reportedByGitClean(repoRoot);\n const candidates: OrphanCandidate[] = [];\n for (const relative of reported) {\n if (!this.isSweepable(relative)) continue;\n if (this.isIgnoredItself(repoRoot, relative)) continue;\n candidates.push(new OrphanCandidate(relative, path.join(repoRoot, relative)));\n }\n return candidates.sort((left: OrphanCandidate, right: OrphanCandidate): number =>\n left.relativePath.localeCompare(right.relativePath));\n }\n\n /**\n * The DIRECTORY paths `git clean -Xdn` would remove, relative to `repoRoot` and slash-stripped.\n *\n * Only entries ending in `/` are directories; a bare path is a single ignored FILE (`.env`,\n * `.claude/settings.local.json`) and is none of this sweep's business. `Would skip repository` lines\n * are git telling us it found a nested repo — a linked worktree — which it refuses to descend into,\n * and so do we.\n */\n private reportedByGitClean(repoRoot: string): string[] {\n const raw = this.git(repoRoot, ['clean', '-Xdn']);\n if (raw === null) return [];\n const paths: string[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed.startsWith(WOULD_REMOVE)) continue;\n const target = trimmed.slice(WOULD_REMOVE.length).trim();\n if (!target.endsWith('/')) continue;\n paths.push(target.slice(0, -1));\n }\n return paths;\n }\n\n /**\n * True when git matches an ignore rule against the DIRECTORY ITSELF — i.e. it is a build artifact\n * somebody declared disposable-but-expected, not a corpse. `check-ignore -q` exits 0 on a match and 1\n * on no match, so a null answer here (any non-zero exit, including a git we could not run) means\n * \"cannot confirm\", and the directory is SPARED. Every uncertainty in this class resolves that way.\n */\n private isIgnoredItself(repoRoot: string, relative: string): boolean {\n return this.git(repoRoot, ['check-ignore', '-q', '--', relative]) !== null;\n }\n\n /**\n * The two structural exclusions, both about where a moved nx PROJECT can possibly live.\n *\n * DOT SEGMENTS: `.nx/`, `.idea/`, `.webpieces/` and friends are reported whole by `clean -Xdn` (their\n * contents are ignored, the directories themselves are not) and would otherwise pass the discriminator\n * above perfectly. They are tool state, they are supposed to be there, and no nx project is named with\n * a leading dot. Excluding the whole class by shape beats naming today's four and meeting a fifth.\n *\n * DEPTH: a moved project lives at `libraries/x`, `apps/x`, `packages/y/z` — never at the top level.\n * Requiring depth >= 2 costs nothing real and takes every top-level directory in the repo permanently\n * out of reach of an automated `mv`.\n */\n private isSweepable(relative: string): boolean {\n const segments = relative.split('/');\n if (segments.length < MIN_DEPTH) return false;\n return !segments.some((segment: string): boolean => segment.startsWith('.'));\n }\n\n /**\n * `git <args>` in `repoRoot`, or null when git exits non-zero or could not be run at all.\n *\n * spawnSync rather than execFileSync because a non-zero exit is ORDINARY here — `check-ignore` says\n * \"not ignored\" that way — and a status code is a better answer to that question than an exception.\n *\n * An ARGUMENT ARRAY, never a shell string: these paths come from a filesystem scan and may hold\n * spaces, quotes or `$`, and interpolating them into a shell would be building a command injection\n * into the one tool whose entire job is moving directories around.\n */\n private git(repoRoot: string, args: string[]): string | null {\n const result = spawnSync('git', ['-C', repoRoot, ...args], {\n encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: GIT_OUTPUT_LIMIT,\n });\n if (result.error !== undefined || result.status !== 0) return null;\n return result.stdout;\n }\n}\n\n/** One directory the sweep may archive. Data-only (per CLAUDE.md — classes, not interfaces, for data). */\nexport class OrphanCandidate {\n /** Repo-relative, no trailing slash — e.g. `libraries/apis/pg-dataaccess-api`. */\n relativePath: string;\n /** The same directory as an absolute path, which is what any `mv` needs. */\n absolutePath: string;\n\n constructor(relativePath: string, absolutePath: string) {\n this.relativePath = relativePath;\n this.absolutePath = absolutePath;\n }\n}\n\nconst WOULD_REMOVE = 'Would remove ';\n// `libraries/foo` is depth 2. Anything shallower is a top-level directory — see isSweepable().\nconst MIN_DEPTH = 2;\n// `clean -Xdn` on a large monorepo prints a few hundred lines; 16MB is a ceiling no real repo approaches.\nconst GIT_OUTPUT_LIMIT = 16 * 1024 * 1024;\n"]}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { OrphanCandidate, OrphanDirScanner } from './orphan-dir-scan';
|
|
2
|
+
import { ArchivedOrphan, FailedOrphan, OrphanDirArchiver, OrphanSweepResult } from './orphan-dir-archive';
|
|
3
|
+
import { HomeConfigService } from './home-config';
|
|
4
|
+
/**
|
|
5
|
+
* The orphan-directory sweep, end to end: find the corpses an `nx g move` left behind, and — only on a
|
|
6
|
+
* machine that has explicitly opted in — archive them where they can be recovered.
|
|
7
|
+
*
|
|
8
|
+
* ─── WHY THE DEFAULT IS REPORT-ONLY ───────────────────────────────────────────────────────────────────
|
|
9
|
+
* `experimental.orphan-dir-sweep` in `~/.webpieces/config.json` defaults to false, like every other key
|
|
10
|
+
* in that optional, machine-local file. That gives the author of this feature exactly one thing nothing
|
|
11
|
+
* else can: they can run it live across all of their own clones for a release, while every colleague
|
|
12
|
+
* pulling the same version gets a report and an untouched working tree.
|
|
13
|
+
*
|
|
14
|
+
* The default direction is the whole safety argument, and it only points one way: absent means REPORT.
|
|
15
|
+
* Nobody can discover this feature by finding their directories somewhere else — the machine has to say
|
|
16
|
+
* `true` out loud first, and the key is greppable precisely because the permissive state is a token
|
|
17
|
+
* rather than an absence.
|
|
18
|
+
*
|
|
19
|
+
* ─── WHY IT NEVER THROWS ──────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
* Every caller is a command whose real work is a checkout and a pull. Tidying is worth doing on the back
|
|
21
|
+
* of that, and worth nothing at all if it can fail the thing it rode in on, so every failure below
|
|
22
|
+
* resolves to a line of prose in the report. The one exception is deliberate: a MALFORMED
|
|
23
|
+
* `~/.webpieces/config.json` propagates, because that file is the user's own instruction to this code and
|
|
24
|
+
* silently ignoring a broken one is how a machine ends up in a state its owner did not choose.
|
|
25
|
+
*/
|
|
26
|
+
export declare class OrphanDirSweeper {
|
|
27
|
+
private readonly scanner;
|
|
28
|
+
private readonly archiver;
|
|
29
|
+
private readonly homeConfigService;
|
|
30
|
+
constructor(scanner: OrphanDirScanner, archiver: OrphanDirArchiver, homeConfigService: HomeConfigService);
|
|
31
|
+
/**
|
|
32
|
+
* Scan `repoRoot`, and archive what was found when this machine has opted in. `now` is a parameter
|
|
33
|
+
* so specs pin the sweep id rather than racing the clock.
|
|
34
|
+
*/
|
|
35
|
+
sweep(repoRoot: string, now: Date): OrphanSweepReport;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What one sweep found and did, and how to say it out loud. Data-only apart from `render()`, which is
|
|
39
|
+
* the report's own presentation of itself rather than business logic living somewhere else.
|
|
40
|
+
*/
|
|
41
|
+
export declare class OrphanSweepReport {
|
|
42
|
+
/** Was archiving switched on for this machine? False means nothing on disk was touched. */
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
/** Every orphan directory found, whether or not it was archived. */
|
|
45
|
+
found: readonly OrphanCandidate[];
|
|
46
|
+
/** The archive, or null when this was a report-only run or there was nothing to archive. */
|
|
47
|
+
result: OrphanSweepResult | null;
|
|
48
|
+
/** How many aged sweep directories were reaped from the trash on this run. */
|
|
49
|
+
reapedSweeps: number;
|
|
50
|
+
constructor(enabled: boolean, found: readonly OrphanCandidate[], result: OrphanSweepResult | null, reapedSweeps: number);
|
|
51
|
+
/**
|
|
52
|
+
* The human-facing block, or empty string when there is nothing to say. A clean tree prints NOTHING —
|
|
53
|
+
* this rides along on somebody else's command, and a tidier that announces having found no work is a
|
|
54
|
+
* tidier people start ignoring, and then stop reading the run where it did find some.
|
|
55
|
+
*/
|
|
56
|
+
render(): string;
|
|
57
|
+
/**
|
|
58
|
+
* The opted-out form: name what was found, name the one edit that turns archiving on, and touch
|
|
59
|
+
* nothing. It names the KEY rather than describing it, because the reader is usually an agent and the
|
|
60
|
+
* mechanical edit is the only part of this it can act on.
|
|
61
|
+
*/
|
|
62
|
+
private renderReportOnly;
|
|
63
|
+
private renderArchived;
|
|
64
|
+
private heading;
|
|
65
|
+
/** Everything archived on this run — empty on a report-only run. Convenience for callers and specs. */
|
|
66
|
+
archived(): readonly ArchivedOrphan[];
|
|
67
|
+
/** Everything found but not movable — empty on a report-only run. */
|
|
68
|
+
skipped(): readonly FailedOrphan[];
|
|
69
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OrphanSweepReport = exports.OrphanDirSweeper = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const orphan_dir_scan_1 = require("./orphan-dir-scan");
|
|
7
|
+
const orphan_dir_archive_1 = require("./orphan-dir-archive");
|
|
8
|
+
const home_config_1 = require("./home-config");
|
|
9
|
+
/**
|
|
10
|
+
* The orphan-directory sweep, end to end: find the corpses an `nx g move` left behind, and — only on a
|
|
11
|
+
* machine that has explicitly opted in — archive them where they can be recovered.
|
|
12
|
+
*
|
|
13
|
+
* ─── WHY THE DEFAULT IS REPORT-ONLY ───────────────────────────────────────────────────────────────────
|
|
14
|
+
* `experimental.orphan-dir-sweep` in `~/.webpieces/config.json` defaults to false, like every other key
|
|
15
|
+
* in that optional, machine-local file. That gives the author of this feature exactly one thing nothing
|
|
16
|
+
* else can: they can run it live across all of their own clones for a release, while every colleague
|
|
17
|
+
* pulling the same version gets a report and an untouched working tree.
|
|
18
|
+
*
|
|
19
|
+
* The default direction is the whole safety argument, and it only points one way: absent means REPORT.
|
|
20
|
+
* Nobody can discover this feature by finding their directories somewhere else — the machine has to say
|
|
21
|
+
* `true` out loud first, and the key is greppable precisely because the permissive state is a token
|
|
22
|
+
* rather than an absence.
|
|
23
|
+
*
|
|
24
|
+
* ─── WHY IT NEVER THROWS ──────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
* Every caller is a command whose real work is a checkout and a pull. Tidying is worth doing on the back
|
|
26
|
+
* of that, and worth nothing at all if it can fail the thing it rode in on, so every failure below
|
|
27
|
+
* resolves to a line of prose in the report. The one exception is deliberate: a MALFORMED
|
|
28
|
+
* `~/.webpieces/config.json` propagates, because that file is the user's own instruction to this code and
|
|
29
|
+
* silently ignoring a broken one is how a machine ends up in a state its owner did not choose.
|
|
30
|
+
*/
|
|
31
|
+
let OrphanDirSweeper = class OrphanDirSweeper {
|
|
32
|
+
scanner;
|
|
33
|
+
archiver;
|
|
34
|
+
homeConfigService;
|
|
35
|
+
constructor(scanner, archiver, homeConfigService) {
|
|
36
|
+
this.scanner = scanner;
|
|
37
|
+
this.archiver = archiver;
|
|
38
|
+
this.homeConfigService = homeConfigService;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Scan `repoRoot`, and archive what was found when this machine has opted in. `now` is a parameter
|
|
42
|
+
* so specs pin the sweep id rather than racing the clock.
|
|
43
|
+
*/
|
|
44
|
+
sweep(repoRoot, now) {
|
|
45
|
+
const candidates = this.scanner.scan(repoRoot);
|
|
46
|
+
const enabled = this.homeConfigService.load().orphanDirSweep;
|
|
47
|
+
if (candidates.length === 0)
|
|
48
|
+
return new OrphanSweepReport(enabled, candidates, null, 0);
|
|
49
|
+
if (!enabled)
|
|
50
|
+
return new OrphanSweepReport(false, candidates, null, 0);
|
|
51
|
+
const result = this.archiver.archive(repoRoot, candidates, now);
|
|
52
|
+
return new OrphanSweepReport(true, candidates, result, this.archiver.reapAged(repoRoot, now));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
exports.OrphanDirSweeper = OrphanDirSweeper;
|
|
56
|
+
exports.OrphanDirSweeper = OrphanDirSweeper = tslib_1.__decorate([
|
|
57
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
58
|
+
tslib_1.__metadata("design:paramtypes", [orphan_dir_scan_1.OrphanDirScanner,
|
|
59
|
+
orphan_dir_archive_1.OrphanDirArchiver,
|
|
60
|
+
home_config_1.HomeConfigService])
|
|
61
|
+
], OrphanDirSweeper);
|
|
62
|
+
/**
|
|
63
|
+
* What one sweep found and did, and how to say it out loud. Data-only apart from `render()`, which is
|
|
64
|
+
* the report's own presentation of itself rather than business logic living somewhere else.
|
|
65
|
+
*/
|
|
66
|
+
class OrphanSweepReport {
|
|
67
|
+
/** Was archiving switched on for this machine? False means nothing on disk was touched. */
|
|
68
|
+
enabled;
|
|
69
|
+
/** Every orphan directory found, whether or not it was archived. */
|
|
70
|
+
found;
|
|
71
|
+
/** The archive, or null when this was a report-only run or there was nothing to archive. */
|
|
72
|
+
result;
|
|
73
|
+
/** How many aged sweep directories were reaped from the trash on this run. */
|
|
74
|
+
reapedSweeps;
|
|
75
|
+
constructor(enabled, found, result, reapedSweeps) {
|
|
76
|
+
this.enabled = enabled;
|
|
77
|
+
this.found = found;
|
|
78
|
+
this.result = result;
|
|
79
|
+
this.reapedSweeps = reapedSweeps;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The human-facing block, or empty string when there is nothing to say. A clean tree prints NOTHING —
|
|
83
|
+
* this rides along on somebody else's command, and a tidier that announces having found no work is a
|
|
84
|
+
* tidier people start ignoring, and then stop reading the run where it did find some.
|
|
85
|
+
*/
|
|
86
|
+
render() {
|
|
87
|
+
if (this.found.length === 0)
|
|
88
|
+
return '';
|
|
89
|
+
if (!this.enabled)
|
|
90
|
+
return this.renderReportOnly();
|
|
91
|
+
return this.renderArchived();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The opted-out form: name what was found, name the one edit that turns archiving on, and touch
|
|
95
|
+
* nothing. It names the KEY rather than describing it, because the reader is usually an agent and the
|
|
96
|
+
* mechanical edit is the only part of this it can act on.
|
|
97
|
+
*/
|
|
98
|
+
renderReportOnly() {
|
|
99
|
+
return `${this.heading()}\n`
|
|
100
|
+
+ ` Nothing was moved — archiving is off on this machine.\n`
|
|
101
|
+
+ ` To archive them (never delete: they move to .webpieces/trash/<sweepId>/ with a recover=\n`
|
|
102
|
+
+ ` command printed for each), add this key in ~/.webpieces/config.json — every key in that\n`
|
|
103
|
+
+ ` file is optional, so this one line beside whatever is already there is the whole edit:\n\n`
|
|
104
|
+
+ ` { "${home_config_1.HOME_EXPERIMENTAL_SECTION}": { "${home_config_1.HOME_KEY_ORPHAN_DIR_SWEEP}": true } }\n`;
|
|
105
|
+
}
|
|
106
|
+
renderArchived() {
|
|
107
|
+
const lines = [this.heading()];
|
|
108
|
+
const result = this.result;
|
|
109
|
+
if (result === null)
|
|
110
|
+
return `${lines.join('\n')}\n`;
|
|
111
|
+
lines.push(` archived to ${result.sweepDir}`);
|
|
112
|
+
for (const moved of result.moved) {
|
|
113
|
+
lines.push(` ${moved.relativePath}`);
|
|
114
|
+
lines.push(` recover=${moved.recoverCommand}`);
|
|
115
|
+
}
|
|
116
|
+
for (const failure of result.failed) {
|
|
117
|
+
lines.push(` SKIPPED ${failure.relativePath} — ${failure.reason}`);
|
|
118
|
+
}
|
|
119
|
+
if (result.manifestError !== null) {
|
|
120
|
+
lines.push(` WARNING: the manifest could not be written — ${result.manifestError}`);
|
|
121
|
+
lines.push(' The recover= lines above are the only copy. Save them before closing this terminal.');
|
|
122
|
+
}
|
|
123
|
+
if (this.reapedSweeps > 0) {
|
|
124
|
+
lines.push(` reaped ${this.reapedSweeps} sweep(s) older than 30 days from the trash`);
|
|
125
|
+
}
|
|
126
|
+
return `${lines.join('\n')}\n`;
|
|
127
|
+
}
|
|
128
|
+
heading() {
|
|
129
|
+
const count = this.found.length;
|
|
130
|
+
return `orphan directories (left by a project move; every file under them is git-ignored): ${count}`;
|
|
131
|
+
}
|
|
132
|
+
/** Everything archived on this run — empty on a report-only run. Convenience for callers and specs. */
|
|
133
|
+
archived() {
|
|
134
|
+
return this.result === null ? [] : this.result.moved;
|
|
135
|
+
}
|
|
136
|
+
/** Everything found but not movable — empty on a report-only run. */
|
|
137
|
+
skipped() {
|
|
138
|
+
return this.result === null ? [] : this.result.failed;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.OrphanSweepReport = OrphanSweepReport;
|
|
142
|
+
//# sourceMappingURL=orphan-dir-sweep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orphan-dir-sweep.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/orphan-dir-sweep.ts"],"names":[],"mappings":";;;;AAAA,yCAA2D;AAE3D,uDAAsE;AACtE,6DAA0G;AAC1G,+CAAwG;AAExG;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEI,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IAEJ;IACA;IACA;IAHrB,YACqB,OAAyB,EACzB,QAA2B,EAC3B,iBAAoC;QAFpC,YAAO,GAAP,OAAO,CAAkB;QACzB,aAAQ,GAAR,QAAQ,CAAmB;QAC3B,sBAAiB,GAAjB,iBAAiB,CAAmB;IACtD,CAAC;IAEJ;;;OAGG;IACH,KAAK,CAAC,QAAgB,EAAE,GAAS;QAC7B,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,cAAc,CAAC;QAC7D,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,iBAAiB,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACxF,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,iBAAiB,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACvE,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,CAAC,CAAC;QAChE,OAAO,IAAI,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;IAClG,CAAC;CACJ,CAAA;AAnBY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGP,kCAAgB;QACf,sCAAiB;QACR,+BAAiB;GAJhD,gBAAgB,CAmB5B;AAED;;;GAGG;AACH,MAAa,iBAAiB;IAC1B,2FAA2F;IAC3F,OAAO,CAAU;IACjB,oEAAoE;IACpE,KAAK,CAA6B;IAClC,4FAA4F;IAC5F,MAAM,CAA2B;IACjC,8EAA8E;IAC9E,YAAY,CAAS;IAErB,YAAY,OAAgB,EAAE,KAAiC,EAAE,MAAgC,EAC7F,YAAoB;QACpB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACrC,CAAC;IAED;;;;OAIG;IACH,MAAM;QACF,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACvC,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC,cAAc,EAAE,CAAC;IACjC,CAAC;IAED;;;;OAIG;IACK,gBAAgB;QACpB,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI;cACtB,2DAA2D;cAC3D,6FAA6F;cAC7F,6FAA6F;cAC7F,8FAA8F;cAC9F,YAAY,uCAAyB,SAAS,uCAAyB,eAAe,CAAC;IACjG,CAAC;IAEO,cAAc;QAClB,MAAM,KAAK,GAAa,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC3B,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;QACpD,KAAK,CAAC,IAAI,CAAC,iBAAiB,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC/C,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YAC/B,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC;YACxC,KAAK,CAAC,IAAI,CAAC,iBAAiB,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClC,KAAK,CAAC,IAAI,CAAC,eAAe,OAAO,CAAC,YAAY,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,IAAI,MAAM,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;YAChC,KAAK,CAAC,IAAI,CAAC,kDAAkD,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;YACrF,KAAK,CAAC,IAAI,CAAC,uFAAuF,CAAC,CAAC;QACxG,CAAC;QACD,IAAI,IAAI,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,YAAY,6CAA6C,CAAC,CAAC;QAC3F,CAAC;QACD,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IACnC,CAAC;IAEO,OAAO;QACX,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;QAChC,OAAO,sFAAsF,KAAK,EAAE,CAAC;IACzG,CAAC;IAED,uGAAuG;IACvG,QAAQ;QACJ,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;IACzD,CAAC;IAED,qEAAqE;IACrE,OAAO;QACH,OAAO,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC1D,CAAC;CACJ;AA/ED,8CA+EC","sourcesContent":["import { injectable, bindingScopeValues } from 'inversify';\n\nimport { OrphanCandidate, OrphanDirScanner } from './orphan-dir-scan';\nimport { ArchivedOrphan, FailedOrphan, OrphanDirArchiver, OrphanSweepResult } from './orphan-dir-archive';\nimport { HOME_EXPERIMENTAL_SECTION, HOME_KEY_ORPHAN_DIR_SWEEP, HomeConfigService } from './home-config';\n\n/**\n * The orphan-directory sweep, end to end: find the corpses an `nx g move` left behind, and — only on a\n * machine that has explicitly opted in — archive them where they can be recovered.\n *\n * ─── WHY THE DEFAULT IS REPORT-ONLY ───────────────────────────────────────────────────────────────────\n * `experimental.orphan-dir-sweep` in `~/.webpieces/config.json` defaults to false, like every other key\n * in that optional, machine-local file. That gives the author of this feature exactly one thing nothing\n * else can: they can run it live across all of their own clones for a release, while every colleague\n * pulling the same version gets a report and an untouched working tree.\n *\n * The default direction is the whole safety argument, and it only points one way: absent means REPORT.\n * Nobody can discover this feature by finding their directories somewhere else — the machine has to say\n * `true` out loud first, and the key is greppable precisely because the permissive state is a token\n * rather than an absence.\n *\n * ─── WHY IT NEVER THROWS ──────────────────────────────────────────────────────────────────────────────\n * Every caller is a command whose real work is a checkout and a pull. Tidying is worth doing on the back\n * of that, and worth nothing at all if it can fail the thing it rode in on, so every failure below\n * resolves to a line of prose in the report. The one exception is deliberate: a MALFORMED\n * `~/.webpieces/config.json` propagates, because that file is the user's own instruction to this code and\n * silently ignoring a broken one is how a machine ends up in a state its owner did not choose.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class OrphanDirSweeper {\n constructor(\n private readonly scanner: OrphanDirScanner,\n private readonly archiver: OrphanDirArchiver,\n private readonly homeConfigService: HomeConfigService,\n ) {}\n\n /**\n * Scan `repoRoot`, and archive what was found when this machine has opted in. `now` is a parameter\n * so specs pin the sweep id rather than racing the clock.\n */\n sweep(repoRoot: string, now: Date): OrphanSweepReport {\n const candidates = this.scanner.scan(repoRoot);\n const enabled = this.homeConfigService.load().orphanDirSweep;\n if (candidates.length === 0) return new OrphanSweepReport(enabled, candidates, null, 0);\n if (!enabled) return new OrphanSweepReport(false, candidates, null, 0);\n const result = this.archiver.archive(repoRoot, candidates, now);\n return new OrphanSweepReport(true, candidates, result, this.archiver.reapAged(repoRoot, now));\n }\n}\n\n/**\n * What one sweep found and did, and how to say it out loud. Data-only apart from `render()`, which is\n * the report's own presentation of itself rather than business logic living somewhere else.\n */\nexport class OrphanSweepReport {\n /** Was archiving switched on for this machine? False means nothing on disk was touched. */\n enabled: boolean;\n /** Every orphan directory found, whether or not it was archived. */\n found: readonly OrphanCandidate[];\n /** The archive, or null when this was a report-only run or there was nothing to archive. */\n result: OrphanSweepResult | null;\n /** How many aged sweep directories were reaped from the trash on this run. */\n reapedSweeps: number;\n\n constructor(enabled: boolean, found: readonly OrphanCandidate[], result: OrphanSweepResult | null,\n reapedSweeps: number) {\n this.enabled = enabled;\n this.found = found;\n this.result = result;\n this.reapedSweeps = reapedSweeps;\n }\n\n /**\n * The human-facing block, or empty string when there is nothing to say. A clean tree prints NOTHING —\n * this rides along on somebody else's command, and a tidier that announces having found no work is a\n * tidier people start ignoring, and then stop reading the run where it did find some.\n */\n render(): string {\n if (this.found.length === 0) return '';\n if (!this.enabled) return this.renderReportOnly();\n return this.renderArchived();\n }\n\n /**\n * The opted-out form: name what was found, name the one edit that turns archiving on, and touch\n * nothing. It names the KEY rather than describing it, because the reader is usually an agent and the\n * mechanical edit is the only part of this it can act on.\n */\n private renderReportOnly(): string {\n return `${this.heading()}\\n`\n + ` Nothing was moved — archiving is off on this machine.\\n`\n + ` To archive them (never delete: they move to .webpieces/trash/<sweepId>/ with a recover=\\n`\n + ` command printed for each), add this key in ~/.webpieces/config.json — every key in that\\n`\n + ` file is optional, so this one line beside whatever is already there is the whole edit:\\n\\n`\n + ` { \"${HOME_EXPERIMENTAL_SECTION}\": { \"${HOME_KEY_ORPHAN_DIR_SWEEP}\": true } }\\n`;\n }\n\n private renderArchived(): string {\n const lines: string[] = [this.heading()];\n const result = this.result;\n if (result === null) return `${lines.join('\\n')}\\n`;\n lines.push(` archived to ${result.sweepDir}`);\n for (const moved of result.moved) {\n lines.push(` ${moved.relativePath}`);\n lines.push(` recover=${moved.recoverCommand}`);\n }\n for (const failure of result.failed) {\n lines.push(` SKIPPED ${failure.relativePath} — ${failure.reason}`);\n }\n if (result.manifestError !== null) {\n lines.push(` WARNING: the manifest could not be written — ${result.manifestError}`);\n lines.push(' The recover= lines above are the only copy. Save them before closing this terminal.');\n }\n if (this.reapedSweeps > 0) {\n lines.push(` reaped ${this.reapedSweeps} sweep(s) older than 30 days from the trash`);\n }\n return `${lines.join('\\n')}\\n`;\n }\n\n private heading(): string {\n const count = this.found.length;\n return `orphan directories (left by a project move; every file under them is git-ignored): ${count}`;\n }\n\n /** Everything archived on this run — empty on a report-only run. Convenience for callers and specs. */\n archived(): readonly ArchivedOrphan[] {\n return this.result === null ? [] : this.result.moved;\n }\n\n /** Everything found but not movable — empty on a report-only run. */\n skipped(): readonly FailedOrphan[] {\n return this.result === null ? [] : this.result.failed;\n }\n}\n"]}
|