nobrainer-tech-flow 2.4.0 → 2.4.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +1 -0
- package/CLAUDE.md +1 -0
- package/README.md +4 -3
- package/assets/nobrainer-tech-logo.svg +1 -1
- package/assets/plugin-logo.png +0 -0
- package/docs/INSTALL.md +4 -4
- package/docs/RELEASING.md +24 -0
- package/docs/releases/v2.4.1.md +63 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install.sh +32 -4
- package/scripts/recommend_flow_setup.py +24 -9
- package/skills/nobrainer-tech-flow/VERSION +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "nobrainer-tech-flow",
|
|
10
10
|
"description": "From task to done. nobrainer-tech-flow is one workflow of 18 modules for AI coding agents: it scopes the task, delegates ready work, checks the result and hands over the evidence, including what was not checked.",
|
|
11
|
-
"version": "2.4.
|
|
11
|
+
"version": "2.4.1",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "NoBrainer Tech"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nobrainer-tech-flow",
|
|
3
3
|
"description": "From task to done. nobrainer-tech-flow is one workflow of 18 modules for AI coding agents: it scopes the task, delegates ready work, checks the result and hands over the evidence, including what was not checked.",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NoBrainer Tech"
|
|
7
7
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nobrainer-tech-flow",
|
|
3
3
|
"description": "From task to done. nobrainer-tech-flow is one workflow of 18 modules for AI coding agents: it scopes the task, delegates ready work, checks the result and hands over the evidence, including what was not checked.",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NoBrainer Tech",
|
|
7
7
|
"url": "https://nobrainer.tech"
|
package/AGENTS.md
CHANGED
|
@@ -23,6 +23,7 @@ Respect host hierarchy; user instructions override skill guidelines. Apply [deli
|
|
|
23
23
|
- `tests/` — deterministic and behavioral regression gates.
|
|
24
24
|
- `docs/COMPATIBILITY.md` and `docs/TESTING.md` — proof boundaries.
|
|
25
25
|
- `docs/SKILL_CURATION.md` — admission, ownership and retirement decisions.
|
|
26
|
+
- `docs/RELEASING.md`: releases.
|
|
26
27
|
|
|
27
28
|
## Portable skill contract
|
|
28
29
|
|
package/CLAUDE.md
CHANGED
|
@@ -23,6 +23,7 @@ Respect host hierarchy; user instructions override skill guidelines. Apply [deli
|
|
|
23
23
|
- `tests/` — deterministic and behavioral regression gates.
|
|
24
24
|
- `docs/COMPATIBILITY.md` and `docs/TESTING.md` — proof boundaries.
|
|
25
25
|
- `docs/SKILL_CURATION.md` — admission, ownership and retirement decisions.
|
|
26
|
+
- `docs/RELEASING.md`: releases.
|
|
26
27
|
|
|
27
28
|
## Portable skill contract
|
|
28
29
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<a href="https://nobrainer.tech">
|
|
3
|
-
<img src="assets/nobrainer-tech-logo.svg" width="96" alt="
|
|
3
|
+
<img src="assets/nobrainer-tech-logo.svg" width="96" alt="NoBrainer Tech logo">
|
|
4
4
|
</a>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
@@ -534,9 +534,10 @@ and [Testing](docs/TESTING.md) for acceptance evidence.
|
|
|
534
534
|
**What's new in 2.4.0:** nobrainer-tech-flow can ask before risky commands and
|
|
535
535
|
shows whether its proof still matches your files. See [RELEASE-NOTES.md](RELEASE-NOTES.md).
|
|
536
536
|
|
|
537
|
-
Current source version: **2.4.
|
|
537
|
+
Current source version: **2.4.1**, which fixes undo after a rearranged profile. Check the
|
|
538
538
|
[latest published GitHub release](https://github.com/nobrainer-tech/nobrainer-tech-flow/releases/latest)
|
|
539
|
-
for distribution and the [2.4.
|
|
539
|
+
for distribution and the [2.4.1](docs/releases/v2.4.1.md) and
|
|
540
|
+
[2.4.0 verification records](docs/releases/v2.4.0.md) for
|
|
540
541
|
the current scope, reproducible checks and client-runtime limits. The unchanged command runner keeps its
|
|
541
542
|
[v1.7.0 verification scope](docs/releases/v1.7.0.md). Source publication does not
|
|
542
543
|
imply client marketplace discovery or improved model reasoning. The earlier
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M126 0H476A36 36 0 0 1 512 36V386A126 126 0 0 1 386 512H36A36 36 0 0 1 0 476V126A126 126 0 0 1 126 0Z" fill="#3B65F0"/><path d="M384 384H121A24 24 0 0 1 97 358A288 288 0 0 1 358 97A24 24 0 0 1 384 121Z" fill="#FFF7EC"/><rect x="240" y="240" width="104" height="104" fill="#F04A14"/></svg>
|
package/assets/plugin-logo.png
CHANGED
|
Binary file
|
package/docs/INSTALL.md
CHANGED
|
@@ -14,9 +14,9 @@ upgraded merely because the new source is installed nearby.
|
|
|
14
14
|
Node 18+ and Python 3.11+ are prerequisites. Preview the setup first:
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
|
-
npx nobrainer-tech-flow@2.4.
|
|
18
|
-
npx nobrainer-tech-flow@2.4.
|
|
19
|
-
npx nobrainer-tech-flow@2.4.
|
|
17
|
+
npx nobrainer-tech-flow@2.4.1 --client codex
|
|
18
|
+
npx nobrainer-tech-flow@2.4.1 --client codex --apply
|
|
19
|
+
npx nobrainer-tech-flow@2.4.1 --client codex --undo --apply
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Use claude for Claude Code, opencode or copilot for those clients. Add --home PATH
|
|
@@ -24,7 +24,7 @@ for an isolated profile. These commands become available once this version is
|
|
|
24
24
|
published on npm; a source candidate is not registry availability.
|
|
25
25
|
|
|
26
26
|
Application copies the release files into the chosen home directory's
|
|
27
|
-
.nobrainer-tech-flow-npm/2.4.
|
|
27
|
+
.nobrainer-tech-flow-npm/2.4.1 and verifies the copy. That folder sits beside
|
|
28
28
|
~/.nobrainer-tech-flow, where the Git installation suggests a clone, so cleaning
|
|
29
29
|
the clone cannot delete the files the skill links point to. Releases before 2.3.2
|
|
30
30
|
kept their copy in .nobrainer-tech-flow/npm; undo such a setup with the command of
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Releasing nobrainer-tech-flow
|
|
2
|
+
|
|
3
|
+
This repository is the one product to promote: stars, issues and links point here.
|
|
4
|
+
Every channel below carries the same version from the same tag, and its links
|
|
5
|
+
(homepage, repository, support) lead back here. A stable release is finished only
|
|
6
|
+
when each channel reads back that version, or the gap is reported:
|
|
7
|
+
|
|
8
|
+
1. GitHub release from the annotated `vX.Y.Z` tag, with
|
|
9
|
+
`nobrainer-tech-flow-X.Y.Z-plugin.zip` (`scripts/build_plugin.py`) and the
|
|
10
|
+
`npm pack` tarball built from the merge commit; download each asset and compare its
|
|
11
|
+
SHA-256 with the digest of that same file before upload.
|
|
12
|
+
2. npm: run the `Publish npm` workflow from the tag, then read back
|
|
13
|
+
`npm view nobrainer-tech-flow version`.
|
|
14
|
+
3. Claude Code and Codex marketplaces in this repository: they follow the default
|
|
15
|
+
branch, so install `nobrainer-tech-flow@nobrainer-tech` in a throwaway profile while
|
|
16
|
+
`main` still equals the tag, and read back the version and the installed commit.
|
|
17
|
+
4. `nobrainer-tech/nobrainer-tech-flow-plugin`: export the release ZIP there
|
|
18
|
+
through a PR, update `SOURCE.json`, run its unmodified
|
|
19
|
+
`_maintainers/verify_release.py`, then release it. It is the hook-free copy that
|
|
20
|
+
the Claude directory tracks, not a second project to promote.
|
|
21
|
+
5. Claude directory (claude.ai/directory/manage) picks up that repository's
|
|
22
|
+
default branch; OpenAI plugin directory (platform.openai.com/plugins) takes the
|
|
23
|
+
native ZIP upload. Both submissions and publication clicks are owner-gated.
|
|
24
|
+
6. Changelog on https://nobrainer.tech/flow/changelog/ after owner preview.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# nobrainer-tech-flow 2.4.1: undo after a rearranged profile
|
|
2
|
+
|
|
3
|
+
Release date: 2026-10-19.
|
|
4
|
+
|
|
5
|
+
Two fixes to the setup record from an independent review of the 2.3.2 installer
|
|
6
|
+
(issue #82). In both cases the undo used to stop with exit 3 and change nothing, so no
|
|
7
|
+
data was lost, but the skill links stayed installed until the record was fixed by hand.
|
|
8
|
+
|
|
9
|
+
## What changes
|
|
10
|
+
|
|
11
|
+
- **Undo works after a link joins two folders the setup made.** After a Codex setup on
|
|
12
|
+
a fresh profile, moving `~/.agents/skills` to `~/.codex/skills` and replacing
|
|
13
|
+
`~/.agents` with a link to `~/.codex` makes two entries of `created_dirs` lead to
|
|
14
|
+
one folder. `install.sh`, `install.py` and the guided `--rollback` refused that record
|
|
15
|
+
as listing a folder twice. They now look for a repeated entry by its spelling in the
|
|
16
|
+
record, so a link added after the setup no longer counts as a repeat. The undo
|
|
17
|
+
removes the links, leaves the `~/.agents` link and everything in `~/.codex` as they
|
|
18
|
+
are, and prints a `PRESERVED` line for the folder that now lies behind the link. A
|
|
19
|
+
record that lists the same path twice is still refused with exit 3. When two
|
|
20
|
+
different entries lead to one folder, the record cannot show which of them the setup
|
|
21
|
+
made, so the undo removes neither: each one stays with a `PRESERVED` line, while the
|
|
22
|
+
links and the other recorded folders are still removed.
|
|
23
|
+
- **A folder whose real path holds a line break stops the setup before it writes.**
|
|
24
|
+
The record lists each folder the setup makes with the links on the way resolved.
|
|
25
|
+
When a link on the way leads to a folder whose name holds a line break or another
|
|
26
|
+
control character (for example `~/.claude` linked to such a folder), `install.sh`
|
|
27
|
+
wrote that path into the record split over two lines, so apply exited 0 and the
|
|
28
|
+
undo later refused the record. The Python installer wrote such a path for a folder
|
|
29
|
+
behind a link that is not on the skills path (for example `CODEX_HOME` set to a
|
|
30
|
+
folder still to be made behind such a link). Both installers now refuse the apply
|
|
31
|
+
with exit 2 and an error that names the folder, before anything is written, as they
|
|
32
|
+
already did for a path written with such a character. `install.sh` also keeps a
|
|
33
|
+
line break at the end of a folder name when it resolves links; it used to drop it.
|
|
34
|
+
|
|
35
|
+
## Verification record
|
|
36
|
+
|
|
37
|
+
Local checks (macOS, Python 3.14.5):
|
|
38
|
+
|
|
39
|
+
- Two new tests in `tests.test_shell_installer` run each case with `install.sh` under
|
|
40
|
+
`/bin/sh`, dash, bash in POSIX mode and zsh emulating sh, and with `install.py`.
|
|
41
|
+
Against the 2.4.0 scripts they failed in 23 subtests: every `install.sh` run of both
|
|
42
|
+
cases, the `install.py` undo of the rearranged profile, and the `install.py` apply
|
|
43
|
+
with `CODEX_HOME` behind a link. The `install.py` apply with `~/.claude` linked to
|
|
44
|
+
such a folder was already refused. Without the trailing line break fix, 8 `install.sh`
|
|
45
|
+
subtests for a folder name that ends in a line break still failed.
|
|
46
|
+
- The case "a folder listed under two spellings" was removed from the test of damaged
|
|
47
|
+
records, because such a record is now accepted; the case of a folder listed twice
|
|
48
|
+
stays.
|
|
49
|
+
- A third test, from a review of this fix, lists an existing empty home under its real
|
|
50
|
+
path and through a link to it. The undo exits 0 and the home stays in all 5 runs; with
|
|
51
|
+
the first version of this fix, the undo removed the home in all 5.
|
|
52
|
+
- `python3 -m unittest tests.test_shell_installer tests.test_one_command_installer
|
|
53
|
+
tests.test_guided_onboarding tests.test_installer tests.test_install_guards`: 205
|
|
54
|
+
tests passed, 2 skipped.
|
|
55
|
+
- `sh -n` and `dash -n` on `scripts/install.sh`, `shellcheck -S error` and
|
|
56
|
+
`git diff --check` passed.
|
|
57
|
+
|
|
58
|
+
The line break case is not tested on Windows, which does not allow such a file name.
|
|
59
|
+
|
|
60
|
+
## Scope
|
|
61
|
+
|
|
62
|
+
This is an installer fix. The record format, the module set and the instruction
|
|
63
|
+
block do not change.
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nobrainer-tech-flow",
|
|
3
3
|
"description": "From task to done. nobrainer-tech-flow is one workflow of 18 modules for AI coding agents: it scopes the task, delegates ready work, checks the result and hands over the evidence, including what was not checked.",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.1",
|
|
5
5
|
"contextFileName": "GEMINI.md"
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nobrainer-tech-flow",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "From task to done. nobrainer-tech-flow is one workflow of 18 modules for AI coding agents: it scopes the task, delegates ready work, checks the result and hands over the evidence, including what was not checked.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": ".opencode/plugins/nobrainer-tech-skills.js",
|
package/plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "nobrainer-tech-flow",
|
|
4
4
|
"description": "From task to done. nobrainer-tech-flow is one workflow of 18 modules for AI coding agents: it scopes the task, delegates ready work, checks the result and hands over the evidence, including what was not checked.",
|
|
5
|
-
"version": "2.4.
|
|
5
|
+
"version": "2.4.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "NoBrainer Tech",
|
|
8
8
|
"url": "https://nobrainer.tech"
|
package/scripts/install.sh
CHANGED
|
@@ -169,7 +169,9 @@ physical() {
|
|
|
169
169
|
return
|
|
170
170
|
fi
|
|
171
171
|
done
|
|
172
|
-
|
|
172
|
+
# The slash keeps a trailing line break of a folder name across the substitution.
|
|
173
|
+
_p_head=$(cd -P "$_p_head" 2>/dev/null && printf '%s/' "$PWD") || _p_head=$1/ _p_tail=
|
|
174
|
+
_p_head=${_p_head%/}
|
|
173
175
|
printf '%s%s\n' "${_p_head%/}" "$_p_tail"
|
|
174
176
|
}
|
|
175
177
|
|
|
@@ -543,7 +545,9 @@ under_link() {
|
|
|
543
545
|
# The folders a record lists must be ones the setup can have made: each an absolute,
|
|
544
546
|
# plain path listed once, that is, or lies above, the skills folder, the folder of the
|
|
545
547
|
# instruction file or the folder of the record. Anything else is a damaged record that an undo must not act on. A path
|
|
546
|
-
# may be spelled with or without the links on the way, so both spellings count.
|
|
548
|
+
# may be spelled with or without the links on the way, so both spellings count. Two entries
|
|
549
|
+
# may still lead to one folder: a link put in place of one of them since the setup does that.
|
|
550
|
+
# The undo removes no folder of such a group.
|
|
547
551
|
above() {
|
|
548
552
|
case "$2/" in "$1"/*) return 0 ;; esac
|
|
549
553
|
return 1
|
|
@@ -591,9 +595,9 @@ check_dirs() {
|
|
|
591
595
|
fi
|
|
592
596
|
[ -n "$_k_ok" ] || exit 1
|
|
593
597
|
case $_k_seen in *"
|
|
594
|
-
$
|
|
598
|
+
$_k_dir
|
|
595
599
|
"*) exit 1 ;; esac
|
|
596
|
-
_k_seen="$_k_seen$
|
|
600
|
+
_k_seen="$_k_seen$_k_dir
|
|
597
601
|
"
|
|
598
602
|
done
|
|
599
603
|
}
|
|
@@ -922,6 +926,16 @@ install() {
|
|
|
922
926
|
sha256 /dev/null >/dev/null || die 2 "this script needs sha256sum, shasum or openssl to record the setup"
|
|
923
927
|
|
|
924
928
|
created= dirs_to_make=
|
|
929
|
+
# A folder this run makes is recorded as it really is, links resolved. A line break or a
|
|
930
|
+
# control character in that form would leave a record no undo can read, so such a run
|
|
931
|
+
# stops before anything is written.
|
|
932
|
+
note_dirs "$home"
|
|
933
|
+
if [ "$link_count" -gt 0 ]; then note_dirs "$skills_dir"; fi
|
|
934
|
+
if [ "$file_status" != present ] && [ "$inherit" = 0 ]; then note_dirs "${target%/*}"; fi
|
|
935
|
+
printf '%s' "$dirs_to_make" | while IFS= read -r _a_dir; do
|
|
936
|
+
reject_control "$(physical "$_a_dir")" "the real path of $_a_dir"
|
|
937
|
+
done || exit 2
|
|
938
|
+
dirs_to_make=
|
|
925
939
|
# The record's folder is made before anything else is written: when it cannot be made,
|
|
926
940
|
# the run stops here instead of leaving links and a block that no record describes. It
|
|
927
941
|
# counts among the folders the setup made: a home that does not exist yet is made for
|
|
@@ -1283,9 +1297,23 @@ undo() {
|
|
|
1283
1297
|
rm -f "$state"
|
|
1284
1298
|
# The record is gone, so the folder it sat in may be empty now. A folder that was there
|
|
1285
1299
|
# before the setup is not listed, and a folder that holds anything stays.
|
|
1300
|
+
# Two entries that lead to one folder cannot both be what the setup made, and the record
|
|
1301
|
+
# cannot say which one is, so no entry of such a group is removed. The groups are found
|
|
1302
|
+
# before any folder goes.
|
|
1286
1303
|
_u_gone=
|
|
1287
1304
|
if [ -n "$st_dirs" ]; then
|
|
1305
|
+
_u_dupes=$(printf '%s' "$st_dirs" | while IFS= read -r _u_dir; do physical "$_u_dir"; done | LC_ALL=C sort | uniq -d)
|
|
1306
|
+
_u_shared=
|
|
1307
|
+
if [ -n "$_u_dupes" ]; then
|
|
1308
|
+
_u_shared=$(printf '%s' "$st_dirs" | while IFS= read -r _u_dir; do
|
|
1309
|
+
if printf '%s\n' "$_u_dupes" | grep -q -x -F -e "$(physical "$_u_dir")"; then printf '%s\n' "$_u_dir"; fi
|
|
1310
|
+
done)
|
|
1311
|
+
fi
|
|
1288
1312
|
_u_gone=$(printf '%s' "$st_dirs" | LC_ALL=C sort -r | while IFS= read -r _u_dir; do
|
|
1313
|
+
if [ -n "$_u_shared" ] && printf '%s\n' "$_u_shared" | grep -q -x -F -e "$_u_dir"; then
|
|
1314
|
+
printf 'PRESERVED: %s: another folder in the record leads to the same folder, so it is left as it is\n' "$_u_dir"
|
|
1315
|
+
continue
|
|
1316
|
+
fi
|
|
1289
1317
|
if [ ! -d "$_u_dir" ] || [ -L "$_u_dir" ]; then continue; fi
|
|
1290
1318
|
if under_link "$_u_dir"; then
|
|
1291
1319
|
printf 'PRESERVED: %s: a folder above it is a link now, so it is left as it is\n' "$_u_dir"
|
|
@@ -651,6 +651,8 @@ def recorded_directories(state: dict[str, object], record: Path) -> list[Path]:
|
|
|
651
651
|
A path may be spelled with or without the links on the way, so both spellings count. A
|
|
652
652
|
folder the setup made is listed as it really is, and a base spelled with ``..`` reaches
|
|
653
653
|
it on the way without lying below it, so the real form of each folder on that way counts.
|
|
654
|
+
An entry may be listed once. Two entries may still lead to one folder: a link put in place
|
|
655
|
+
of one of them since the setup does that. The undo removes no folder of such a group.
|
|
654
656
|
"""
|
|
655
657
|
|
|
656
658
|
raw = state.get("created_dirs", [])
|
|
@@ -675,9 +677,9 @@ def recorded_directories(state: dict[str, object], record: Path) -> list[Path]:
|
|
|
675
677
|
or not any(form == place or form in place.parents for form in forms for place in places)
|
|
676
678
|
):
|
|
677
679
|
raise ValueError(f"created_dirs names a folder this setup cannot have created: {item}")
|
|
678
|
-
if
|
|
680
|
+
if path in seen:
|
|
679
681
|
raise ValueError("created_dirs must list each folder the setup created once")
|
|
680
|
-
seen.add(
|
|
682
|
+
seen.add(path)
|
|
681
683
|
directories.append(path)
|
|
682
684
|
return directories
|
|
683
685
|
|
|
@@ -698,18 +700,26 @@ def is_link(path: Path) -> bool:
|
|
|
698
700
|
return bool(attributes & getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0))
|
|
699
701
|
|
|
700
702
|
|
|
701
|
-
def remove_empty_directories(directories: list[Path]) -> tuple[list[Path], list[Path]]:
|
|
703
|
+
def remove_empty_directories(directories: list[Path]) -> tuple[list[Path], list[Path], list[Path]]:
|
|
702
704
|
"""Remove each of these folders that is still empty, innermost first.
|
|
703
705
|
|
|
704
|
-
Returns the folders removed
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
act on another tree.
|
|
706
|
+
Returns the folders removed, the folders kept because a folder above them is a link
|
|
707
|
+
now, and the folders kept because another entry leads to the same folder. The record
|
|
708
|
+
lists each folder with the links on the way resolved, so such a link means the profile
|
|
709
|
+
was rearranged since the setup, and removing the folder would act on another tree. Two
|
|
710
|
+
entries that lead to one folder cannot both be what the setup made, and the record
|
|
711
|
+
cannot say which one is, so neither is removed.
|
|
708
712
|
"""
|
|
709
713
|
|
|
714
|
+
real = [physical_path(path) for path in directories]
|
|
715
|
+
shared = {path for path, form in zip(directories, real) if real.count(form) > 1}
|
|
710
716
|
removed: list[Path] = []
|
|
711
717
|
kept: list[Path] = []
|
|
718
|
+
kept_shared: list[Path] = []
|
|
712
719
|
for path in sorted(directories, key=lambda item: len(item.parts), reverse=True):
|
|
720
|
+
if path in shared:
|
|
721
|
+
kept_shared.append(path)
|
|
722
|
+
continue
|
|
713
723
|
try:
|
|
714
724
|
if is_link(path) or not path.is_dir():
|
|
715
725
|
continue
|
|
@@ -720,7 +730,7 @@ def remove_empty_directories(directories: list[Path]) -> tuple[list[Path], list[
|
|
|
720
730
|
except OSError:
|
|
721
731
|
continue
|
|
722
732
|
removed.append(path)
|
|
723
|
-
return removed, kept
|
|
733
|
+
return removed, kept, kept_shared
|
|
724
734
|
|
|
725
735
|
|
|
726
736
|
def state_path(home: Path, override: Path | None, client: str) -> Path:
|
|
@@ -1126,11 +1136,13 @@ def rollback(args: argparse.Namespace, state_file: Path) -> int:
|
|
|
1126
1136
|
print(f"ROLLBACK_READBACK: removed={','.join(removed) or 'none'}; profile=preserved")
|
|
1127
1137
|
return 3
|
|
1128
1138
|
state_file.unlink()
|
|
1129
|
-
removed_dirs, kept_dirs = remove_empty_directories(directories)
|
|
1139
|
+
removed_dirs, kept_dirs, shared_dirs = remove_empty_directories(directories)
|
|
1130
1140
|
for directory in removed_dirs:
|
|
1131
1141
|
print(f"REMOVED_DIR: {directory}")
|
|
1132
1142
|
for directory in kept_dirs:
|
|
1133
1143
|
print(f"PRESERVED: {directory}: a folder above it is a link now, so it is left as it is")
|
|
1144
|
+
for directory in shared_dirs:
|
|
1145
|
+
print(f"PRESERVED: {directory}: another folder in the record leads to the same folder, so it is left as it is")
|
|
1134
1146
|
if "created_dirs" not in state:
|
|
1135
1147
|
# A record from before the folder list still gets its skills folder removed when empty.
|
|
1136
1148
|
# One with the list says which folders the setup made, so no other is touched.
|
|
@@ -1366,6 +1378,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1366
1378
|
# The record's own folder counts: a home that does not exist yet is made for it alone
|
|
1367
1379
|
# when a client variable puts the skills and the instructions elsewhere.
|
|
1368
1380
|
new_directories = missing_directories(destination, profile_target.parent, state.parent)
|
|
1381
|
+
# They are recorded as they really are, links resolved, and a path the record cannot
|
|
1382
|
+
# hold stops the run here, before anything is written.
|
|
1383
|
+
require_utf8(*(physical_path(path) for path in new_directories))
|
|
1369
1384
|
# Inspect every target before writing any of them. This keeps a later
|
|
1370
1385
|
# conflict from leaving a partial selection installed.
|
|
1371
1386
|
for name in install_names:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
2.4.
|
|
1
|
+
2.4.1
|