Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions node-packages/wp-tooling/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to `@rtcamp/wp-tooling` are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## Unreleased

### Fixed

- Feature finalization rolls back successful hooks when detection or state persistence fails, and defers next-step messages until persistence succeeds. Setup/reinit identity and version writes share rollback protection. Rename batches reuse one plan, support chains and swaps, and detect case-insensitive destination collisions. Cleanup targets and explicit example markers reject malformed values before mutation.

- Init validates shared setup/manage selections before mutation, confirms setup choices before applying identity, and uses current persisted tokens for reinitialization. Version-only edits now update files and identity; reinit preserves one-shot example choices and refuses corrupt identity.
- Feature hooks are awaited inside their rollback journal. Cancellation/no-op manage calls leave persisted state unchanged; failed transitions stop, retain shared dependency ownership, and report rollback failures.
- Init rejects rename collisions, unsafe configured paths and symlink escapes, propagates filesystem and external-step failures, and reports partial setup instead of unconditional success. Identity edits roll back file changes on failure and preserve unrelated metadata. Fully rolled-back feature transitions leave saved state untouched, and dependency/script values must be strings. Git-step failures report that project setup completed; failures during project setup still require inspecting partial changes before retrying.

## [1.0.0] - 2026-07-30

### Added
Expand Down
59 changes: 36 additions & 23 deletions node-packages/wp-tooling/src/init/cleanup.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,52 @@
'use strict';

const fs = require('fs');
const { resolveWithin } = require('./transform');
const path = require('path');
const { resolveWithin, validateRelativePath } = require('./transform');

/**
* Delete each target (file or directory) under `root` if it exists. Targets that
* resolve outside the project root are refused, never deleted.
* Validate every cleanup target before deletion starts.
*
* @param {string} root - Project root.
* @param {string[]} targets - Project-relative paths to remove.
* @param {Object} ui - `@rtcamp/wp-tooling/ui`.
* @return {number} Count of targets removed.
* @return {Object[]} Validated relative and absolute paths.
*/
const resolveCleanupTargets = (root, targets = []) => {
if (!Array.isArray(targets)) {
throw new Error(
`Expected cleanup.targets to be an array, received ${JSON.stringify(targets)}`
);
}
return targets.map((target) => {
validateRelativePath(target);
const full = resolveWithin(root, target);
if (full === path.resolve(root)) {
throw new Error(`Refusing to remove the project root: ${target}`);
}
return { target, full };
});
};

/**
* Delete validated cleanup targets, skipping paths that no longer exist.
*
* @param {string} root Project root.
* @param {string[]} targets Relative paths to remove.
* @param {Object} ui Status output.
* @return {number} Number of targets removed.
*/
const runCleanup = (root, targets, ui) => {
const resolved = resolveCleanupTargets(root, targets);
let removed = 0;
(targets || []).forEach((target) => {
let full;
try {
full = resolveWithin(root, target);
} catch (err) {
ui.warn(err.message);
return;
}
for (const { target, full } of resolved) {
if (!fs.existsSync(full)) {
return;
continue;
}
try {
fs.rmSync(full, { recursive: true, force: true });
ui.info(`removed ${target}`);
removed++;
} catch (err) {
ui.warn(`Could not remove ${target}: ${err.message}`);
}
});
fs.rmSync(full, { recursive: true, force: true });
ui.info(`removed ${target}`);
removed++;
}
return removed;
};

module.exports = { runCleanup };
module.exports = { runCleanup, resolveCleanupTargets };
22 changes: 11 additions & 11 deletions node-packages/wp-tooling/src/init/examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,20 @@ const expandGlob = (root, pattern) => {
let entries = [];
try {
entries = fs.readdirSync(base);
} catch {
} catch (error) {
if (!['ENOENT', 'ENOTDIR'].includes(error.code)) {
throw error;
}
return;
}
entries.forEach((name) => {
if (re.test(name)) {
next.push(path.join(base, name));
next.push(
resolveWithin(
root,
path.relative(root, path.join(base, name))
)
);
}
});
});
Expand Down Expand Up @@ -142,15 +150,7 @@ const applyExamples = (config, root, ui, removeKeys) => {
// Strip this group's markers from its registration files (drop the code too
// when removing). Only regions tagged with this group's marker are touched.
(group.strip || []).forEach((rel) => {
let file;
try {
file = resolveWithin(root, rel);
} catch (err) {
if (ui && ui.warn) {
ui.warn(err.message);
}
return;
}
const file = resolveWithin(root, rel);
if (!fs.existsSync(file)) {
return;
}
Expand Down
Loading