@timekast/factory 0.1.6 → 0.1.7
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/lib/atomic-swap.js +29 -1
- package/package.json +1 -1
package/dist/lib/atomic-swap.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* paths). `src/` and dev-owned files are never in the plan, so they are never
|
|
21
21
|
* read, written, or deleted here (design §7.2, §9 out-of-scope).
|
|
22
22
|
*/
|
|
23
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, writeFileSync, } from 'node:fs';
|
|
24
24
|
import path from 'node:path';
|
|
25
25
|
import { CLIError } from './cli-error.js';
|
|
26
26
|
import { TIMEKAST_DIR } from './constants.js';
|
|
@@ -87,6 +87,33 @@ export function validateStagedManifest(stagedDir, manifestRaw) {
|
|
|
87
87
|
}
|
|
88
88
|
return manifest;
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* After deleting a tracked file, remove now-empty parent directories so a removed
|
|
92
|
+
* skill/dir does not leave an empty husk behind (e.g. deleting
|
|
93
|
+
* `.claude/skills/<name>/SKILL.md` should also drop the empty `<name>/` dir).
|
|
94
|
+
*
|
|
95
|
+
* Walks up from the file's directory, removing each empty dir, and stops at the
|
|
96
|
+
* first non-empty dir OR at a structural level — only directories at depth ≥ 3
|
|
97
|
+
* relative to the root are eligible, so `.claude`, `.claude/skills`, `scripts/tools`
|
|
98
|
+
* and the like always survive even if momentarily empty. Best-effort: any fs error
|
|
99
|
+
* just stops the walk. Path-separator agnostic (Windows-safe).
|
|
100
|
+
*/
|
|
101
|
+
function pruneEmptyDirs(rootDir, rel) {
|
|
102
|
+
const root = path.resolve(rootDir);
|
|
103
|
+
let dir = path.dirname(rel);
|
|
104
|
+
while (dir && dir !== '.' && dir.split(/[\\/]/).filter(Boolean).length >= 3) {
|
|
105
|
+
const abs = path.join(root, dir);
|
|
106
|
+
try {
|
|
107
|
+
if (!existsSync(abs) || readdirSync(abs).length > 0)
|
|
108
|
+
break;
|
|
109
|
+
rmdirSync(abs); // empty-dir only; throws (→ caught) if a race re-fills it
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
dir = path.dirname(dir);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
90
117
|
/**
|
|
91
118
|
* Apply the plan atomically with a backup-and-rollback guard.
|
|
92
119
|
*
|
|
@@ -147,6 +174,7 @@ export function applyPlan(rootDir, stagedDir, plan, backupDir, hooks) {
|
|
|
147
174
|
hooks?.afterWrites?.();
|
|
148
175
|
for (const rel of plan.deletes) {
|
|
149
176
|
rmSync(path.join(rootDir, rel), { force: true, recursive: true });
|
|
177
|
+
pruneEmptyDirs(rootDir, rel);
|
|
150
178
|
}
|
|
151
179
|
}
|
|
152
180
|
catch (error) {
|