From 13733ab9e2b7b0f3bb922e9142411847c2ea764c Mon Sep 17 00:00:00 2001 From: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com> Date: Sun, 9 Aug 2026 20:27:21 +0800 Subject: [PATCH 1/4] fix(amp): restore root plugin entry in install script The hardened install.sh from 0c7ee116 added atomic staging/rollback but dropped the Amp-discoverable root plugin entry (plugins/nowledge-mem.ts). Amp only loads single-file plugins from a root .ts/.js entry inside the plugins directory; a bare bundle dir is not discoverable, so `amp plugins list` showed nothing after install. The README and the zh integration docs both describe the root entry, so the script regressed. Restore the entry inside the existing atomic flow: - stage plugins/nowledge-mem.ts alongside the bundle and skill - add it to restore_previous and install_staged so a failed step rolls back entry + bundle + skill together (no partial update state) - add pre-flight checks for the staged entry - re-run is a safe atomic update of all three Tests: - static contract test re-asserts PLUGIN_ENTRY_DEST/STAGED_ENTRY/BACKUP_ENTRY and the re-export line that 0c7ee116 removed - new: first install creates entry+bundle+skill, entry target resolves - new: re-run is idempotent (artifacts stable) - new: pre-flight failure leaves a prior install untouched README troubleshooting now points at the root entry file. The zh integration doc already matches; no change there. --- nowledge-mem-amp-plugin/README.md | 2 +- nowledge-mem-amp-plugin/scripts/install.sh | 45 +++++++++-- tests/plugin_e2e/test_key_plugins_e2e.py | 94 ++++++++++++++++++++++ 3 files changed, 134 insertions(+), 7 deletions(-) diff --git a/nowledge-mem-amp-plugin/README.md b/nowledge-mem-amp-plugin/README.md index ec5eb623..be17075e 100644 --- a/nowledge-mem-amp-plugin/README.md +++ b/nowledge-mem-amp-plugin/README.md @@ -133,7 +133,7 @@ For multi-agent setups, set `NMEM_AGENT_ID=` per spawned Amp worker. - **nmem not found.** Install with `pip install nmem-cli`, or on Arch Linux `yay -S nmem-cli` / `paru -S nmem-cli`, then run `nmem status` to verify. - **Server not responding.** Start the Nowledge Mem desktop app, or check `nmem status` for diagnostics. -- **Plugin not loading.** Re-run `./scripts/install.sh`, confirm the files exist under `~/.config/amp/plugins/nowledge-mem/`, and restart Amp. +- **Plugin not loading.** Re-run `./scripts/install.sh`, confirm the root entry `${XDG_CONFIG_HOME:-$HOME/.config}/amp/plugins/nowledge-mem.ts` and the bundle beside it exist, and restart Amp. ## Links diff --git a/nowledge-mem-amp-plugin/scripts/install.sh b/nowledge-mem-amp-plugin/scripts/install.sh index ff5890f7..9d419528 100755 --- a/nowledge-mem-amp-plugin/scripts/install.sh +++ b/nowledge-mem-amp-plugin/scripts/install.sh @@ -2,11 +2,15 @@ # # Installs the Nowledge Mem plugin and skill into Amp's per-user directories. # +# entry -> ${XDG_CONFIG_HOME:-~/.config}/amp/plugins/nowledge-mem.ts # plugin -> ${XDG_CONFIG_HOME:-~/.config}/amp/plugins/nowledge-mem/ # skill -> ${XDG_CONFIG_HOME:-~/.config}/amp/skills/nowledge-mem/ # -# Re-running the script updates an existing installation. Restart Amp after -# installing or updating so the plugin and skill are picked up. +# Amp discovers a single-file plugin from a root .ts/.js entry inside the +# plugins directory; the entry re-exports the bundle's default plugin so a +# bare bundle directory alone is not loadable. Re-running the script updates +# an existing installation. Restart Amp after installing or updating so the +# plugin and skill are picked up. set -euo pipefail @@ -15,6 +19,7 @@ AMP_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/amp" PLUGINS_DIR="$AMP_CONFIG_DIR/plugins" SKILLS_DIR="$AMP_CONFIG_DIR/skills" PLUGIN_DEST="$PLUGINS_DIR/$PLUGIN_NAME" +PLUGIN_ENTRY_DEST="$PLUGINS_DIR/$PLUGIN_NAME.ts" SKILL_DEST="$SKILLS_DIR/$PLUGIN_NAME" # Resolve the directory this script lives in, so the command works regardless @@ -33,11 +38,19 @@ trap cleanup EXIT STAGED_PLUGIN="$STAGING_DIR/plugin" STAGED_SKILL="$STAGING_DIR/skill" +STAGED_ENTRY="$STAGING_DIR/entry.ts" BACKUP_PLUGIN="$STAGING_DIR/old-plugin" BACKUP_SKILL="$STAGING_DIR/old-skill" +BACKUP_ENTRY="$STAGING_DIR/old-entry.ts" +# Restore the previously active entry, bundle, and skill after a failed +# replacement. Removes any partially moved-in files first so the backups land +# at their original paths. restore_previous() { - rm -rf "$PLUGIN_DEST" "$SKILL_DEST" + rm -rf "$PLUGIN_DEST" "$SKILL_DEST" "$PLUGIN_ENTRY_DEST" + if [ -e "$BACKUP_ENTRY" ]; then + mv "$BACKUP_ENTRY" "$PLUGIN_ENTRY_DEST" + fi if [ -e "$BACKUP_PLUGIN" ]; then mv "$BACKUP_PLUGIN" "$PLUGIN_DEST" fi @@ -47,7 +60,11 @@ restore_previous() { } install_staged() { + if [ -e "$PLUGIN_ENTRY_DEST" ] && ! mv "$PLUGIN_ENTRY_DEST" "$BACKUP_ENTRY"; then + return 1 + fi if [ -e "$PLUGIN_DEST" ] && ! mv "$PLUGIN_DEST" "$BACKUP_PLUGIN"; then + restore_previous return 1 fi if [ -d "$STAGED_SKILL" ] && [ -e "$SKILL_DEST" ] && ! mv "$SKILL_DEST" "$BACKUP_SKILL"; then @@ -62,21 +79,29 @@ install_staged() { restore_previous return 1 fi - rm -rf "$BACKUP_PLUGIN" "$BACKUP_SKILL" + if ! mv "$STAGED_ENTRY" "$PLUGIN_ENTRY_DEST"; then + restore_previous + return 1 + fi + rm -rf "$BACKUP_PLUGIN" "$BACKUP_SKILL" "$BACKUP_ENTRY" } echo "Installing Nowledge Mem for Amp" echo " source: $PLUGIN_SRC" +echo " entry: $PLUGIN_ENTRY_DEST" echo " plugin: $PLUGIN_DEST" echo " skill: $SKILL_DEST" -# Stage the plugin and skill before touching the active installation. +# Stage the plugin, skill, and root entry before touching the active +# installation. The entry re-exports the bundle's default plugin so Amp +# discovers it as a single-file plugin (see the header comment). cp -R "$PLUGIN_SRC" "$STAGED_PLUGIN" if [ -d "$STAGED_PLUGIN/skills/$PLUGIN_NAME" ]; then cp -R "$STAGED_PLUGIN/skills/$PLUGIN_NAME" "$STAGED_SKILL" fi +printf 'export { default } from "./nowledge-mem/src/index.ts"\n' > "$STAGED_ENTRY" -# Validate the staged plugin entrypoint exists before replacement. +# Validate the staged bundle, skill, and entry before replacement. if [ ! -f "$STAGED_PLUGIN/src/index.ts" ]; then echo "Error: staged plugin is missing src/index.ts" >&2 exit 1 @@ -85,6 +110,14 @@ if [ ! -f "$STAGED_PLUGIN/skills/$PLUGIN_NAME/SKILL.md" ]; then echo "Error: staged plugin is missing skills/$PLUGIN_NAME/SKILL.md" >&2 exit 1 fi +if [ ! -f "$STAGED_ENTRY" ]; then + echo "Error: staged root entry was not written" >&2 + exit 1 +fi +if ! grep -q 'export { default } from "./nowledge-mem/src/index.ts"' "$STAGED_ENTRY"; then + echo "Error: staged root entry does not re-export the bundle default" >&2 + exit 1 +fi if ! install_staged; then echo "Error: could not replace the active installation; previous files were restored." >&2 diff --git a/tests/plugin_e2e/test_key_plugins_e2e.py b/tests/plugin_e2e/test_key_plugins_e2e.py index 59aad4a4..f5245d4e 100644 --- a/tests/plugin_e2e/test_key_plugins_e2e.py +++ b/tests/plugin_e2e/test_key_plugins_e2e.py @@ -1688,6 +1688,12 @@ def test_amp_plugin_static_contract_is_self_contained(): assert "STAGED_PLUGIN=" in install_sh assert "install_staged" in install_sh assert 'skills/$PLUGIN_NAME/SKILL.md' in install_sh + # The script installs an Amp-discoverable root plugin entry that re-exports + # the bundle default, alongside the bundle dir and skill. + assert "PLUGIN_ENTRY_DEST=" in install_sh + assert "STAGED_ENTRY=" in install_sh + assert "BACKUP_ENTRY=" in install_sh + assert 'export { default } from "./nowledge-mem/src/index.ts"' in install_sh # No raw secrets are checked into the package. assert "NMEM_API_KEY" in readme @@ -2677,3 +2683,91 @@ def test_opencode_live_tool_thread_capture(e2e_context: E2EContext, tmp_path: Pa space=e2e_context.space, env=env, ) + + +def _run_amp_install(xdg_home: Path, *, script: Path | None = None) -> subprocess.CompletedProcess[str]: + """Run the Amp plugin install script isolated under an XDG_CONFIG_HOME root. + + Returns the CompletedProcess regardless of exit code so callers can assert + on success or failure. The script resolves its own source dir, so `script` + defaults to the in-tree install.sh. + """ + install_script = script if script is not None else AMP_PLUGIN / "scripts" / "install.sh" + return subprocess.run( + ["bash", str(install_script)], + env={**os.environ, "XDG_CONFIG_HOME": str(xdg_home)}, + text=True, + capture_output=True, + timeout=60, + ) + + +def _assert_amp_artifacts(xdg_home: Path) -> None: + plugins_dir = xdg_home / "amp" / "plugins" + entry = plugins_dir / "nowledge-mem.ts" + bundle_index = plugins_dir / "nowledge-mem" / "src" / "index.ts" + skill = xdg_home / "amp" / "skills" / "nowledge-mem" / "SKILL.md" + assert entry.is_file(), f"root plugin entry missing: {entry}" + assert ( + 'export { default } from "./nowledge-mem/src/index.ts"' in entry.read_text(encoding="utf-8") + ), "root entry does not re-export the bundle default" + # The entry's import target must resolve on disk, otherwise Amp cannot load it. + assert bundle_index.is_file(), f"entry target missing: {bundle_index}" + assert skill.is_file(), f"skill missing: {skill}" + + +def test_amp_install_script_creates_root_entry_bundle_and_skill(tmp_path: Path): + """First install writes the Amp-discoverable root entry, the bundle it + re-exports, and the skill, all under the resolved XDG config directory.""" + result = _run_amp_install(tmp_path) + assert result.returncode == 0, result.stderr + _assert_amp_artifacts(tmp_path) + + +def test_amp_install_script_is_idempotent(tmp_path: Path): + """Re-running the script is a safe atomic update: all artifacts survive and + the root entry content is stable across runs.""" + first = _run_amp_install(tmp_path) + assert first.returncode == 0, first.stderr + entry = tmp_path / "amp" / "plugins" / "nowledge-mem.ts" + bundle_index = tmp_path / "amp" / "plugins" / "nowledge-mem" / "src" / "index.ts" + entry_before = entry.read_text(encoding="utf-8") + bundle_before = bundle_index.read_text(encoding="utf-8") + + second = _run_amp_install(tmp_path) + assert second.returncode == 0, second.stderr + _assert_amp_artifacts(tmp_path) + assert entry.read_text(encoding="utf-8") == entry_before + assert bundle_index.read_text(encoding="utf-8") == bundle_before + + +def test_amp_install_script_keeps_prior_install_when_preflight_fails(tmp_path: Path): + """A failed pre-flight check must not disturb a prior valid installation: + the script stages everything before touching the active install, so a + missing source file fails fast and leaves the live entry/bundle/skill + intact.""" + # Establish a known-good installation first. + seed = _run_amp_install(tmp_path) + assert seed.returncode == 0, seed.stderr + entry = tmp_path / "amp" / "plugins" / "nowledge-mem.ts" + bundle_index = tmp_path / "amp" / "plugins" / "nowledge-mem" / "src" / "index.ts" + skill = tmp_path / "amp" / "skills" / "nowledge-mem" / "SKILL.md" + entry_before = entry.read_text(encoding="utf-8") + bundle_before = bundle_index.read_text(encoding="utf-8") + skill_before = skill.read_text(encoding="utf-8") + + # Build a broken source tree (missing src/index.ts) and run its install + # script against the same config root; the staged-bundle pre-flight check + # must abort before the live install is touched. + broken_src = tmp_path / "broken-amp-plugin-src" + shutil.copytree(AMP_PLUGIN, broken_src) + (broken_src / "src" / "index.ts").unlink() + failed = _run_amp_install(tmp_path, script=broken_src / "scripts" / "install.sh") + + assert failed.returncode != 0 + assert "staged plugin is missing src/index.ts" in failed.stderr + # The prior installation is untouched. + _assert_amp_artifacts(tmp_path) + assert entry.read_text(encoding="utf-8") == entry_before + assert bundle_index.read_text(encoding="utf-8") == bundle_before + assert skill.read_text(encoding="utf-8") == skill_before From 705c3e673d96830bc18a3a722d31ffee50434636 Mon Sep 17 00:00:00 2001 From: IceCodeNew <32576256+IceCodeNew@users.noreply.github.com> Date: Sun, 9 Aug 2026 20:57:19 +0800 Subject: [PATCH 2/4] fix(amp): only delete/restored backed-up artifacts on install rollback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit review of PR #488. restore_previous removed PLUGIN_DEST/SKILL_DEST/PLUGIN_ENTRY_DEST unconditionally, then restored only backups that existed. If a backup mv failed (e.g. bundle backup at the entry-then-bundle step), the live artifact was still in place (failed mv keeps its source), but restore_previous deleted it and had no backup to restore from — the live bundle/skill was destroyed. Track each successful backup with a flag (ENTRY/BUNDLE/SKILL_BACKED_UP, set to 1 only after the mv succeeds). restore_previous now removes only destinations whose backup succeeded and restores only those backups, so the rm and restore sets always match regardless of where the sequence fails. Also fix README troubleshooting: ./scripts/install.sh is not valid from the repo root; use bash nowledge-mem-amp-plugin/scripts/install.sh (matches Setup and integrations.json). Tests: - new test_amp_install_script_preserves_prior_install_when_backup_move_fails: chmod 0500 the live bundle dir so the bundle backup mv fails; assert prior entry+bundle+skill all intact afterwards - existing amp tests still green Verified: vitest 189 passed, shellcheck 0 findings, pytest -k amp 6 passed. --- nowledge-mem-amp-plugin/README.md | 2 +- nowledge-mem-amp-plugin/scripts/install.sh | 50 ++++++++++++++++------ tests/plugin_e2e/test_key_plugins_e2e.py | 34 +++++++++++++++ 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/nowledge-mem-amp-plugin/README.md b/nowledge-mem-amp-plugin/README.md index be17075e..2bbaf74d 100644 --- a/nowledge-mem-amp-plugin/README.md +++ b/nowledge-mem-amp-plugin/README.md @@ -133,7 +133,7 @@ For multi-agent setups, set `NMEM_AGENT_ID=` per spawned Amp worker. - **nmem not found.** Install with `pip install nmem-cli`, or on Arch Linux `yay -S nmem-cli` / `paru -S nmem-cli`, then run `nmem status` to verify. - **Server not responding.** Start the Nowledge Mem desktop app, or check `nmem status` for diagnostics. -- **Plugin not loading.** Re-run `./scripts/install.sh`, confirm the root entry `${XDG_CONFIG_HOME:-$HOME/.config}/amp/plugins/nowledge-mem.ts` and the bundle beside it exist, and restart Amp. +- **Plugin not loading.** Re-run `bash nowledge-mem-amp-plugin/scripts/install.sh` from the `community` repository root, confirm the root entry `${XDG_CONFIG_HOME:-$HOME/.config}/amp/plugins/nowledge-mem.ts` and the bundle beside it exist, and restart Amp. ## Links diff --git a/nowledge-mem-amp-plugin/scripts/install.sh b/nowledge-mem-amp-plugin/scripts/install.sh index 9d419528..306b3a50 100755 --- a/nowledge-mem-amp-plugin/scripts/install.sh +++ b/nowledge-mem-amp-plugin/scripts/install.sh @@ -43,33 +43,55 @@ BACKUP_PLUGIN="$STAGING_DIR/old-plugin" BACKUP_SKILL="$STAGING_DIR/old-skill" BACKUP_ENTRY="$STAGING_DIR/old-entry.ts" +# Track which backups were created so restore_previous only removes/restores +# artifacts that have a valid backup. A failed backup leaves the live artifact +# in place (a failed mv keeps its source); the matching flag stays 0, so the +# live artifact is never deleted without a backup to restore from. +ENTRY_BACKED_UP=0 +BUNDLE_BACKED_UP=0 +SKILL_BACKED_UP=0 + # Restore the previously active entry, bundle, and skill after a failed -# replacement. Removes any partially moved-in files first so the backups land -# at their original paths. +# replacement. Only destinations whose backup succeeded are removed, and only +# those backups are restored, so the rm and restore sets always match. restore_previous() { - rm -rf "$PLUGIN_DEST" "$SKILL_DEST" "$PLUGIN_ENTRY_DEST" - if [ -e "$BACKUP_ENTRY" ]; then + if [ "$ENTRY_BACKED_UP" -eq 1 ]; then + rm -rf "$PLUGIN_ENTRY_DEST" mv "$BACKUP_ENTRY" "$PLUGIN_ENTRY_DEST" fi - if [ -e "$BACKUP_PLUGIN" ]; then + if [ "$BUNDLE_BACKED_UP" -eq 1 ]; then + rm -rf "$PLUGIN_DEST" mv "$BACKUP_PLUGIN" "$PLUGIN_DEST" fi - if [ -e "$BACKUP_SKILL" ]; then + if [ "$SKILL_BACKED_UP" -eq 1 ]; then + rm -rf "$SKILL_DEST" mv "$BACKUP_SKILL" "$SKILL_DEST" fi } install_staged() { - if [ -e "$PLUGIN_ENTRY_DEST" ] && ! mv "$PLUGIN_ENTRY_DEST" "$BACKUP_ENTRY"; then - return 1 + if [ -e "$PLUGIN_ENTRY_DEST" ]; then + if mv "$PLUGIN_ENTRY_DEST" "$BACKUP_ENTRY"; then + ENTRY_BACKED_UP=1 + else + return 1 + fi fi - if [ -e "$PLUGIN_DEST" ] && ! mv "$PLUGIN_DEST" "$BACKUP_PLUGIN"; then - restore_previous - return 1 + if [ -e "$PLUGIN_DEST" ]; then + if mv "$PLUGIN_DEST" "$BACKUP_PLUGIN"; then + BUNDLE_BACKED_UP=1 + else + restore_previous + return 1 + fi fi - if [ -d "$STAGED_SKILL" ] && [ -e "$SKILL_DEST" ] && ! mv "$SKILL_DEST" "$BACKUP_SKILL"; then - restore_previous - return 1 + if [ -d "$STAGED_SKILL" ] && [ -e "$SKILL_DEST" ]; then + if mv "$SKILL_DEST" "$BACKUP_SKILL"; then + SKILL_BACKED_UP=1 + else + restore_previous + return 1 + fi fi if ! mv "$STAGED_PLUGIN" "$PLUGIN_DEST"; then restore_previous diff --git a/tests/plugin_e2e/test_key_plugins_e2e.py b/tests/plugin_e2e/test_key_plugins_e2e.py index f5245d4e..9cc1c6b1 100644 --- a/tests/plugin_e2e/test_key_plugins_e2e.py +++ b/tests/plugin_e2e/test_key_plugins_e2e.py @@ -2771,3 +2771,37 @@ def test_amp_install_script_keeps_prior_install_when_preflight_fails(tmp_path: P assert entry.read_text(encoding="utf-8") == entry_before assert bundle_index.read_text(encoding="utf-8") == bundle_before assert skill.read_text(encoding="utf-8") == skill_before + + +def test_amp_install_script_preserves_prior_install_when_backup_move_fails(tmp_path: Path): + """A failed backup of the live bundle must not delete it. restore_previous + only removes/restores artifacts whose backup succeeded, so a bundle whose + backup `mv` fails (e.g. read-only source) is left in place and the rest of + the prior install (entry, skill) is restored from their successful backups.""" + seed = _run_amp_install(tmp_path) + assert seed.returncode == 0, seed.stderr + entry = tmp_path / "amp" / "plugins" / "nowledge-mem.ts" + bundle_dir = tmp_path / "amp" / "plugins" / "nowledge-mem" + bundle_index = bundle_dir / "src" / "index.ts" + skill = tmp_path / "amp" / "skills" / "nowledge-mem" / "SKILL.md" + entry_before = entry.read_text(encoding="utf-8") + bundle_before = bundle_index.read_text(encoding="utf-8") + skill_before = skill.read_text(encoding="utf-8") + + # Make the live bundle dir un-mv-able so the bundle backup step fails. The + # entry backup (a plain file) still succeeds first, exercising the path + # where some backups exist and one does not. + bundle_dir.chmod(0o500) + try: + failed = _run_amp_install(tmp_path) + finally: + bundle_dir.chmod(0o755) + + assert failed.returncode != 0 + assert "could not replace the active installation" in failed.stderr + # The prior installation is fully intact; the bundle was never deleted. + _assert_amp_artifacts(tmp_path) + assert entry.read_text(encoding="utf-8") == entry_before + assert bundle_index.read_text(encoding="utf-8") == bundle_before + assert skill.read_text(encoding="utf-8") == skill_before + From 7a8a5d99a0eefc3a2546e3231eb3c12dc452067f Mon Sep 17 00:00:00 2001 From: Wey Gu Date: Mon, 10 Aug 2026 12:27:09 +0800 Subject: [PATCH 3/4] fix(amp): clean partial installs on rollback --- nowledge-mem-amp-plugin/scripts/install.sh | 31 ++++++++++--- tests/plugin_e2e/test_key_plugins_e2e.py | 54 +++++++++++++++++----- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/nowledge-mem-amp-plugin/scripts/install.sh b/nowledge-mem-amp-plugin/scripts/install.sh index 306b3a50..7ad3ba29 100755 --- a/nowledge-mem-amp-plugin/scripts/install.sh +++ b/nowledge-mem-amp-plugin/scripts/install.sh @@ -50,22 +50,33 @@ BACKUP_ENTRY="$STAGING_DIR/old-entry.ts" ENTRY_BACKED_UP=0 BUNDLE_BACKED_UP=0 SKILL_BACKED_UP=0 +ENTRY_INSTALLED=0 +BUNDLE_INSTALLED=0 +SKILL_INSTALLED=0 # Restore the previously active entry, bundle, and skill after a failed # replacement. Only destinations whose backup succeeded are removed, and only -# those backups are restored, so the rm and restore sets always match. +# those backups are restored. Fresh artifacts that were installed before a +# later step failed are removed so failed first installs do not leave partial +# Amp-visible state behind. restore_previous() { if [ "$ENTRY_BACKED_UP" -eq 1 ]; then rm -rf "$PLUGIN_ENTRY_DEST" mv "$BACKUP_ENTRY" "$PLUGIN_ENTRY_DEST" + elif [ "$ENTRY_INSTALLED" -eq 1 ]; then + rm -rf "$PLUGIN_ENTRY_DEST" fi if [ "$BUNDLE_BACKED_UP" -eq 1 ]; then rm -rf "$PLUGIN_DEST" mv "$BACKUP_PLUGIN" "$PLUGIN_DEST" + elif [ "$BUNDLE_INSTALLED" -eq 1 ]; then + rm -rf "$PLUGIN_DEST" fi if [ "$SKILL_BACKED_UP" -eq 1 ]; then rm -rf "$SKILL_DEST" mv "$BACKUP_SKILL" "$SKILL_DEST" + elif [ "$SKILL_INSTALLED" -eq 1 ]; then + rm -rf "$SKILL_DEST" fi } @@ -93,15 +104,23 @@ install_staged() { return 1 fi fi - if ! mv "$STAGED_PLUGIN" "$PLUGIN_DEST"; then + if mv "$STAGED_PLUGIN" "$PLUGIN_DEST"; then + BUNDLE_INSTALLED=1 + else restore_previous return 1 fi - if [ -d "$STAGED_SKILL" ] && ! mv "$STAGED_SKILL" "$SKILL_DEST"; then - restore_previous - return 1 + if [ -d "$STAGED_SKILL" ]; then + if mv "$STAGED_SKILL" "$SKILL_DEST"; then + SKILL_INSTALLED=1 + else + restore_previous + return 1 + fi fi - if ! mv "$STAGED_ENTRY" "$PLUGIN_ENTRY_DEST"; then + if mv "$STAGED_ENTRY" "$PLUGIN_ENTRY_DEST"; then + ENTRY_INSTALLED=1 + else restore_previous return 1 fi diff --git a/tests/plugin_e2e/test_key_plugins_e2e.py b/tests/plugin_e2e/test_key_plugins_e2e.py index 9cc1c6b1..c7f528fa 100644 --- a/tests/plugin_e2e/test_key_plugins_e2e.py +++ b/tests/plugin_e2e/test_key_plugins_e2e.py @@ -1693,6 +1693,9 @@ def test_amp_plugin_static_contract_is_self_contained(): assert "PLUGIN_ENTRY_DEST=" in install_sh assert "STAGED_ENTRY=" in install_sh assert "BACKUP_ENTRY=" in install_sh + assert "ENTRY_INSTALLED=" in install_sh + assert "BUNDLE_INSTALLED=" in install_sh + assert "SKILL_INSTALLED=" in install_sh assert 'export { default } from "./nowledge-mem/src/index.ts"' in install_sh # No raw secrets are checked into the package. @@ -2775,9 +2778,9 @@ def test_amp_install_script_keeps_prior_install_when_preflight_fails(tmp_path: P def test_amp_install_script_preserves_prior_install_when_backup_move_fails(tmp_path: Path): """A failed backup of the live bundle must not delete it. restore_previous - only removes/restores artifacts whose backup succeeded, so a bundle whose - backup `mv` fails (e.g. read-only source) is left in place and the rest of - the prior install (entry, skill) is restored from their successful backups.""" + only removes/restores artifacts whose backup succeeded, so a failed bundle + backup leaves the bundle in place and restores the entry from its successful + backup.""" seed = _run_amp_install(tmp_path) assert seed.returncode == 0, seed.stderr entry = tmp_path / "amp" / "plugins" / "nowledge-mem.ts" @@ -2788,14 +2791,20 @@ def test_amp_install_script_preserves_prior_install_when_backup_move_fails(tmp_p bundle_before = bundle_index.read_text(encoding="utf-8") skill_before = skill.read_text(encoding="utf-8") - # Make the live bundle dir un-mv-able so the bundle backup step fails. The - # entry backup (a plain file) still succeeds first, exercising the path - # where some backups exist and one does not. - bundle_dir.chmod(0o500) - try: - failed = _run_amp_install(tmp_path) - finally: - bundle_dir.chmod(0o755) + # Inject a deterministic bundle-backup failure after the entry backup has + # succeeded. chmod on the bundle dir is not portable here: POSIX rename + # checks the parent directory, not the moved directory's own write bit. + broken_src = tmp_path / "broken-backup-amp-plugin-src" + shutil.copytree(AMP_PLUGIN, broken_src) + install_script = broken_src / "scripts" / "install.sh" + script = install_script.read_text(encoding="utf-8") + script = script.replace( + 'if mv "$PLUGIN_DEST" "$BACKUP_PLUGIN"; then', + 'if false && mv "$PLUGIN_DEST" "$BACKUP_PLUGIN"; then', + 1, + ) + install_script.write_text(script, encoding="utf-8") + failed = _run_amp_install(tmp_path, script=install_script) assert failed.returncode != 0 assert "could not replace the active installation" in failed.stderr @@ -2805,3 +2814,26 @@ def test_amp_install_script_preserves_prior_install_when_backup_move_fails(tmp_p assert bundle_index.read_text(encoding="utf-8") == bundle_before assert skill.read_text(encoding="utf-8") == skill_before + +def test_amp_install_script_removes_fresh_partial_install_when_entry_move_fails(tmp_path: Path): + """If a first install fails after moving the bundle and skill but before + moving the root entry, rollback removes the new partial install. Amp should + not see a half-installed bundle without the discoverable entry.""" + broken_src = tmp_path / "broken-entry-amp-plugin-src" + shutil.copytree(AMP_PLUGIN, broken_src) + install_script = broken_src / "scripts" / "install.sh" + script = install_script.read_text(encoding="utf-8") + script = script.replace( + 'if mv "$STAGED_ENTRY" "$PLUGIN_ENTRY_DEST"; then', + 'rm -f "$STAGED_ENTRY"\n if mv "$STAGED_ENTRY" "$PLUGIN_ENTRY_DEST"; then', + 1, + ) + install_script.write_text(script, encoding="utf-8") + + failed = _run_amp_install(tmp_path, script=install_script) + + assert failed.returncode != 0 + assert "could not replace the active installation" in failed.stderr + assert not (tmp_path / "amp" / "plugins" / "nowledge-mem.ts").exists() + assert not (tmp_path / "amp" / "plugins" / "nowledge-mem").exists() + assert not (tmp_path / "amp" / "skills" / "nowledge-mem").exists() From 305a27f3a49bb365db2680035cc9381759ada7f8 Mon Sep 17 00:00:00 2001 From: Wey Gu Date: Mon, 10 Aug 2026 12:29:20 +0800 Subject: [PATCH 4/4] chore(amp): bump plugin after install fix --- integrations.json | 2 +- nowledge-mem-amp-plugin/CHANGELOG.md | 11 +++++++++++ nowledge-mem-amp-plugin/package.json | 2 +- tests/plugin_e2e/test_key_plugins_e2e.py | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/integrations.json b/integrations.json index 93bc37ae..44e521c5 100644 --- a/integrations.json +++ b/integrations.json @@ -1028,7 +1028,7 @@ "name": "Amp", "category": "coding", "type": "plugin", - "version": "0.1.0", + "version": "0.1.1", "directory": "nowledge-mem-amp-plugin", "transport": "cli+http", "capabilities": { diff --git a/nowledge-mem-amp-plugin/CHANGELOG.md b/nowledge-mem-amp-plugin/CHANGELOG.md index 2a86c3b1..64887453 100644 --- a/nowledge-mem-amp-plugin/CHANGELOG.md +++ b/nowledge-mem-amp-plugin/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.1.1] - 2026-08-10 + +### Fixed + +- Restored the Amp-discoverable root plugin entry at `plugins/nowledge-mem.ts` + during install and update. Existing `0.1.0` installs may have copied the + bundle without the root entry, so Amp could miss the plugin. +- Hardened install rollback so failed updates preserve the previous entry, + bundle, and skill, and failed first installs do not leave a partial Amp + plugin behind. + ## [0.1.0] - 2026-08-08 ### Added diff --git a/nowledge-mem-amp-plugin/package.json b/nowledge-mem-amp-plugin/package.json index 24d9b45e..ad0dfaf9 100644 --- a/nowledge-mem-amp-plugin/package.json +++ b/nowledge-mem-amp-plugin/package.json @@ -1,6 +1,6 @@ { "name": "amp-nowledge-mem", - "version": "0.1.0", + "version": "0.1.1", "description": "Nowledge Mem plugin for Amp. Cross-tool knowledge with agent-end thread capture, Context Bundle, search, save, and handoffs.", "type": "module", "main": "src/index.ts", diff --git a/tests/plugin_e2e/test_key_plugins_e2e.py b/tests/plugin_e2e/test_key_plugins_e2e.py index c7f528fa..84df5476 100644 --- a/tests/plugin_e2e/test_key_plugins_e2e.py +++ b/tests/plugin_e2e/test_key_plugins_e2e.py @@ -1626,7 +1626,7 @@ def test_amp_plugin_static_contract_is_self_contained(): # Package manifest and registry entry agree on the basics. assert pkg["name"] == "amp-nowledge-mem" - assert pkg["version"] == "0.1.0" + assert pkg["version"] == "0.1.1" assert pkg["type"] == "module" assert pkg["main"] == "src/index.ts" assert "@ampcode/plugin" in pkg["peerDependencies"]