* test(#1854): failing-first coverage for user-files-backup restore Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(#1854): offer restore for user-added files backed up on update Adds a restore-custom-files gsd-tools verb and wires it into update.md as a restore_custom_files step: plan, compatibility-check against the newly installed release, then restore only on explicit opt-in. The backup is never deleted, a shipped path is never overwritten, and a single unwritable entry does not abort the rest. Also drops the jq pipe from update-context field extraction (#2589 class, missed by that sweep) and repairs a broken code fence in docs/CLI-TOOLS.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#1854): reject symlinked restore destinations and backup roots Self-review of the restore path found two write-through holes: copyFileSync follows a symlinked destination, so a link planted at the restore target wrote outside the config dir with every ancestor still a real directory; and statSync on the backup root followed a link, letting the walk read arbitrary files and present them as the user's own backup. Both now lstat. Also marks the report's path/detail strings as untrusted data in update.md so the rendered step cannot carry instructions into the runtime model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#1854): move the update-context jq guard into the #2589 sweep update.md joins the AUDITED list rather than carrying a duplicate assertion in the backup-restore suite, and the guard gains a negative-proof companion so 'no jq pipe' cannot pass by the fields simply no longer being read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#1854): validate manifest files map shape before trusting it Security review flagged that Object.keys on a non-plain-object files field yields numeric-index keys matching nothing, so the managed-path check dies silently while manifest_found still reports true. Shape, not just type (ADR-227): an array or scalar files map is now an unusable manifest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#1854): size the restore prompt by eligible_count Spec review found the prompt was driven by entries.length, so a backup holding only blocked entries asked "Restore 1 file(s)?" when accepting would restore zero. The question now reads eligible_count, and an all-blocked backup reports its reasons instead of offering a choice that cannot be honored. The decline path names the resolved backup_dir rather than the bare directory name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(#1854): use t.skip on hosts without symlink support A bare return in a node:test body registers as a PASS, so the four symlink guards silently reported green on unprivileged Windows instead of skipping. Adds the dangling-link destination case the security review called out, and moves outside-dir teardown to t.after so a failing assert cannot leak it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#1854): unfence restore hint, regen goldens, widen install timeout Three gate failures from the c99d612a5 run, all root-caused: 1. capability-registry (3): update.md's decline message put an instructional 'gsd-tools ...' line in an UNTAGGED fence, and the guard treats untagged fences as shell blocks. Retagged both display blocks as text and switched the hint to the resolved 'node <config-dir>/.../gsd-tools.cjs' form users can actually paste. 2. golden-install-parity (19): update.md and gsd-tools.cjs ship, so every runtime fixture moved. Regenerated; the diff is exactly those two hashes per fixture, no other drift. 3. install.test.cjs (5): one real failure, four cascades. The Cursor suite's before hook died on 'spawnSync ETIMEDOUT' at the 60s cap while the node22 lane passed the SAME commit in 12.7s. A full install measures 13-30s idle, so 60s was under 2x headroom and shrinks with every file added to the payload. Raised to 120s, matching the heavy case already in this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#1854): backfill changeset pr number to 2679 --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
7.9 KiB
How to update GSD Core
Update an existing GSD Core install to the latest release, preview the changelog before committing, and recover any local customisations that the update would overwrite.
What you need: The same runtime GSD is installed for. The update command re-runs the installer under the hood, so it needs Node.js and npx available (same requirement as the original install).
The standard update path
From inside your AI runtime, run:
/gsd-update
GSD will:
- Detect the installed version and install scope (global or local).
- Check npm for the latest release of
@opengsd/gsd-core. - Fetch the changelog and show you what changed between your installed version and the latest.
- Ask for confirmation before touching anything.
- Back up any user-added files found inside GSD-managed directories to
gsd-user-files-backup/. - Run the installer (
npx @opengsd/gsd-core@latest --<runtime> --<scope>). - Clear the update-check cache so the statusline indicator resets.
- Offer to restore the user-added files it backed up in step 5.
- Report whether locally modified GSD files were backed up to
gsd-local-patches/.
Restart your runtime after the update to pick up new commands and agents.
Flags
| Flag | What it does |
|---|---|
--sync |
After updating, sync skills from the GSD registry |
--reapply |
After updating, merge locally modified GSD files back in from gsd-local-patches/ |
--next / --rc |
Target the @next RC dist-tag instead of @latest (installs or refreshes a release candidate; see ADR #660) |
/gsd-update --sync # Update and sync skills
/gsd-update --reapply # Update and reapply local patches
/gsd-update --next # Install from the @next RC dist-tag
Install or refresh a release candidate
GSD publishes release candidates on the @next npm dist-tag (established by ADR #660). To install or refresh from that channel:
/gsd-update --next
# or equivalently:
/gsd-update --rc
The full update flow applies — scope/runtime detection, changelog preview, custom-file backup, and cache clearing all run normally. The only difference is that check-latest-version.cjs resolves the @next tag and npx installs from @opengsd/gsd-core@next.
Only latest and next are supported channels; no arbitrary dist-tag can be passed (the script enforces an allowlist and exits with code 2 on an invalid tag).
Omitting --next/--rc keeps targeting @latest (stable channel, no change in behavior).
Reviewing the changelog before updating
/gsd-update always shows the changelog diff between your installed version and the latest before it asks for confirmation. You do not need to visit GitHub separately. The output looks like:
## GSD Update Available
Installed: 1.39.0
Latest: 1.41.0
### What's New
────────────────────────────────────────────────────────────
[changelog entries for 1.40.0 and 1.41.0]
────────────────────────────────────────────────────────────
Proceed with update? [Yes, update now / No, cancel]
If the changelog cannot be fetched (no network access, npm outage), the update still proceeds after confirmation — it does not block on changelog availability.
Recovering local customisations
Files you added inside GSD-managed directories
If you placed custom files inside directories that GSD owns (for example, custom agents prefixed with gsd- or extra files in commands/gsd/), the installer detects them and copies them to gsd-user-files-backup/ before wiping those directories.
After the new version is installed, the update offers to put them back. You get a list of what was backed up, then a choice:
- Restore them now — each file is copied back to its original location and the update reports what it restored.
- Leave them in the backup — nothing is copied; the backup stays exactly where it is.
Either way the backup is never deleted, so declining is not destructive and you can restore later.
Before copying anything back, the restore runs a compatibility pass against the version that was just installed and attaches a warning to any file that looks like it may no longer work — one that references a workflow or /gsd: command the new release retired, or a skill missing its name / description frontmatter. Warnings are advisory: the file is still restored, with the warning shown next to it, so you can decide whether to fix it.
Two cases are skipped rather than restored, because restoring would destroy something:
- The new release now ships a file at that exact path (your custom file would overwrite GSD's).
- A different file is already sitting at that path (restoring would overwrite your current version).
Both stay in the backup and are reported with the reason.
To restore later — or after an update where you declined — run the same operation directly:
node <config-dir>/gsd-core/bin/gsd-tools.cjs restore-custom-files --config-dir <config-dir> --apply
Drop --apply to preview what would be restored without writing anything.
Files you placed outside GSD-managed directories — custom agents not prefixed with gsd-, custom commands outside commands/gsd/, your CLAUDE.md files, and custom hooks — are never touched by the installer.
GSD files you modified directly
If you edited a file that GSD installed (for example, tweaking an agent's system prompt), the installer detects the modification via a hash comparison against its manifest, backs the file up to gsd-local-patches/, and then replaces it with the new version. After the update:
/gsd-update --reapply
This merges your modifications from gsd-local-patches/ back into the newly installed files.
If you skipped --reapply after a previous update and want to apply patches now:
/gsd-update --reapply
It is safe to run --reapply on its own without triggering a new download — if you are already on the latest version, GSD skips the install step and goes straight to reapplying patches.
When npm is unavailable
If npx @opengsd/gsd-core@latest fails due to an npm outage, network restrictions, or because you are working from the source repository, use the manual update procedure in docs/manual-update.md. That document covers pulling the latest commit, building the hooks dist, and running node bin/install.js directly.
If you are already on the latest version
/gsd-update exits early with a confirmation message — no download, no install, no restart needed.
Installer migrations
Each GSD release may include installer migrations that rename, move, or retire managed files. The migration layer runs automatically before the new package payload is written. Migrations that would affect files you have modified prompt for confirmation rather than acting silently. For the full design and runtime-configuration contract registry, see docs/installer-migrations.md.
Related
CLI version-skew warning
GSD warns (to stderr, non-blocking) when the resolved gsd-tools.cjs is outside your project root while a project-local install exists — a sign that a global install (often a retired @gsd-build/sdk canary) is shadowing your project-local GSD. The warning names the resolved path and, for the @gsd-build/sdk case, gives the removal command:
npm uninstall -g @gsd-build/sdk
If you see this warning, remove the stale global package so gsd_run resolves the project-local install.