From 052fd91e6e03c4b03fdd0681b82f26cc9b03e7a8 Mon Sep 17 00:00:00 2001 From: Cesar-M-Diaz Date: Thu, 30 Jul 2026 16:40:13 +0200 Subject: [PATCH 1/4] spec: proposal for version aware cli + update flow --- .../changes/add-update-flow/.openspec.yaml | 2 + openspec/changes/add-update-flow/design.md | 387 ++++++++++++++++++ openspec/changes/add-update-flow/proposal.md | 83 ++++ .../specs/release-versioning/spec.md | 143 +++++++ .../add-update-flow/specs/update-flow/spec.md | 255 ++++++++++++ openspec/changes/add-update-flow/tasks.md | 107 +++++ 6 files changed, 977 insertions(+) create mode 100644 openspec/changes/add-update-flow/.openspec.yaml create mode 100644 openspec/changes/add-update-flow/design.md create mode 100644 openspec/changes/add-update-flow/proposal.md create mode 100644 openspec/changes/add-update-flow/specs/release-versioning/spec.md create mode 100644 openspec/changes/add-update-flow/specs/update-flow/spec.md create mode 100644 openspec/changes/add-update-flow/tasks.md diff --git a/openspec/changes/add-update-flow/.openspec.yaml b/openspec/changes/add-update-flow/.openspec.yaml new file mode 100644 index 0000000..a15fd7e --- /dev/null +++ b/openspec/changes/add-update-flow/.openspec.yaml @@ -0,0 +1,2 @@ +schema: ns-workflow +created: 2026-07-30 diff --git a/openspec/changes/add-update-flow/design.md b/openspec/changes/add-update-flow/design.md new file mode 100644 index 0000000..6119f77 --- /dev/null +++ b/openspec/changes/add-update-flow/design.md @@ -0,0 +1,387 @@ +# Design + +## Architecture + +The update feature is additive to the existing CLI and installer architecture. It does not move installation ownership into the shared CLI: each native harness remains responsible for its own staged plugin, Pi remains package-owned, and OpenCode/fallback installs continue through the existing installer. + +The CLI adds an update coordinator that separates four concerns: + +1. inventory: determine what is installed and how it is owned; +2. discovery: resolve local and latest versions without mutation; +3. planning: produce deterministic, reviewable update actions; and +4. execution: run approved actions through typed strategies and summarize results. + +```text +packages/core/src/cli.ts + | + v +packages/core/src/update/coordinator.ts + | | | + v v v + inventory version sources target strategies + | | | + +------------+-------------+ + | + v + sanitized UpdateSummary +``` + +The coordinator never performs OAuth. Existing setup/auth modules are outside the update dependency graph. + +Release preparation is a separate maintainer-side script. Runtime update code reads version metadata but never edits repository release files. + +## Module Boundaries + +### Runtime update modules + +`packages/core/src/update/types.ts` + +- Defines target, plan, result, summary, version, and error contracts. +- Contains no filesystem, network, or process behavior. + +`packages/core/src/update/coordinator.ts` + +- Resolves requested scope (`cli`, one harness, or all detected targets). +- Produces the plan before mutation. +- Applies confirmation rules. +- Executes targets sequentially and isolates per-target failures. +- Owns overall exit/result semantics, not target-specific commands. + +`packages/core/src/update/inventory.ts` + +- Reuses harness adapters and tracking readers to classify each harness as native, fallback, package-owned, or not installed. +- Reads the running CLI/package metadata. +- Adds optional version discovery without changing existing installation detection contracts. + +`packages/core/src/update/version-source.ts` + +- Reads and validates `latest` metadata from npm for `nsolid-plugin` and `nsolid-pi-plugin`. +- Reads the GitHub-root `bundle.json` once for native Git targets. +- Applies bounded request timeouts and semantic-version validation. +- Returns `unknown` rather than treating missing version evidence as current. + +`packages/core/src/update/package-manager.ts` + +- Detects npm or pnpm only from positive installation-path/package-manager evidence. +- Produces a fixed executable plus argument array. +- Returns unsupported for workspaces, `npx`, local checkouts, and ambiguous launchers. + +`packages/core/src/update/command-runner.ts` + +- Wraps `spawn`/`spawnSync` with `shell: false`. +- Accepts executable and argument arrays, controlled environment additions, timeout, and output mode. +- Redacts tokens, authorization headers, and credential paths from captured diagnostics. +- Is injected in tests so no real package manager or harness command runs. + +`packages/core/src/update/strategies/*.ts` + +- One strategy per ownership model: CLI package, Claude, Codex, Antigravity, Pi, and fallback. +- Strategies receive an immutable plan item and execution context. +- Strategies cannot broaden scope or switch from native to fallback ownership after failure. + +`packages/core/src/update/antigravity-transaction.ts` + +- Resolves only known NodeSource staged plugin paths. +- Creates a temporary backup before replacement. +- Validates the newly staged root by checking `plugin.json`, `bundle.json`, and canonical skill presence. +- Restores the backup if reinstall or validation fails. + +### Existing modules extended + +`packages/core/src/cli.ts` + +- Adds `version` and `update` cases, with bare `--version` as an alias for human-readable version reporting. +- Adds `--check` and `--all`. +- Rejects `--all` with `--harness` before calling the coordinator. +- Keeps JSON on stdout and progress/diagnostics on stderr. + +`packages/core/src/index.ts` + +- Exports programmatic `getVersionInfo()`, `checkUpdates()`, and `update()` functions and their public types. +- Existing setup/install/uninstall APIs remain unchanged. + +`packages/core/src/harnesses/` + +- Native detection may expose optional installed version and staged root. +- Existing adapter methods keep their signatures; additive optional methods or helper functions are preferred. + +`packages/core/src/skills/skill-tracker.ts` + +- Fallback tracking may add an optional `bundleVersion` for future checks. +- Readers must accept existing tracking files that omit it. + +### Release modules + +`scripts/prepare-release.mjs` + +- Accepts `patch`, `minor`, `major`, or an explicit greater semantic version. +- Treats root `bundle.json.version` as the canonical current release version. +- Snapshots every controlled file before mutation. +- Updates source version files, runs existing bundle/root generators, validates results, and restores snapshots on failure. +- Never invokes Git mutation, pack, publish, or registry authentication. + +`scripts/check-release-version.mjs` + +- Compares package and generated versions with the root bundle. +- Calls/reuses existing bundle and root-manifest checks. +- Activates release mode only when invoked through `pnpm release:check --release`. +- In release mode, compares the explicit plugin payload allowlist from the Release Versioning specification with the latest semantic-version tag and rejects an unchanged version. + +Root package scripts: + +```json +{ + "release:prepare": "node scripts/prepare-release.mjs", + "release:check": "node scripts/check-release-version.mjs" +} +``` + +The private root package version remains `0.0.0`. + +## Interfaces and Contracts + +```typescript +export type UpdateTarget = + | 'cli' + | 'claude' + | 'codex' + | 'opencode' + | 'antigravity' + | 'pi' + +export type UpdateOwnership = + | 'global-package' + | 'native-plugin' + | 'package-owned' + | 'fallback' + +export type UpdateStatus = + | 'current' + | 'update-available' + | 'updated' + | 'skipped' + | 'not-installed' + | 'unknown' + | 'failed' + +export interface VersionInfo { + current?: string + latest?: string + status: 'current' | 'update-available' | 'newer-than-registry' | 'unknown' +} + +export interface UpdateOptions { + harness?: HarnessType + all?: boolean + check?: boolean + yes?: boolean + json?: boolean + verbose?: boolean + noColor?: boolean + commandRunner?: CommandRunner + confirm?: UpdateConfirmation +} + +export interface UpdatePlanItem { + target: UpdateTarget + ownership: UpdateOwnership + installed: boolean + version: VersionInfo + executable?: string + args?: readonly string[] + requiresConfirmation: boolean + restartHint?: string +} + +export interface UpdateResult { + target: UpdateTarget + ownership: UpdateOwnership + status: UpdateStatus + currentVersion?: string + resultingVersion?: string + changed: boolean + restartHint?: string + rollbackCommand?: string + error?: { + code: string + message: string + } +} + +export interface UpdateSummary { + checkOnly: boolean + results: UpdateResult[] + counts: Record + success: boolean +} + +export interface CommandSpec { + executable: string + args: readonly string[] + cwd?: string + timeoutMs: number +} + +export interface CommandRunner { + run(spec: CommandSpec): Promise +} + +export interface UpdateStrategy { + readonly target: UpdateTarget + plan(context: UpdateContext): Promise + execute(item: UpdatePlanItem, context: UpdateContext): Promise +} +``` + +Rules enforced by these contracts: + +- `check` stops after planning/version resolution and never calls `execute`. +- Command arguments are arrays; a shell command string is not part of the contract. +- `error.message` is sanitized and suitable for JSON output. +- An absent version is represented as `unknown`, never coerced to `current`. +- Strategies return data; the CLI formatter owns human-readable output. +- A completed check whose result is `update-available` is successful and exits zero; lookup, validation, or execution failures remain non-zero. + +### Fixed harness command plans + +| Target | Native/package action | Success guidance | +|---|---|---| +| CLI npm | `npm install -g nsolid-plugin@` | invoke CLI again | +| CLI pnpm | `pnpm add -g nsolid-plugin@` | invoke CLI again | +| Claude | `claude plugin update nsolid-plugin@nodesource` | `/reload-plugins` or restart | +| Codex | `codex plugin marketplace upgrade nodesource` | start a new session | +| Antigravity | `agy plugin uninstall nsolid-plugin`, then install Git URL | restart AGY | +| Pi | `pi update npm:nsolid-pi-plugin` | `/reload` or restart | +| Fallback/OpenCode | latest published CLI executes `install --harness ` | restart harness if needed | + +No user-derived string is interpolated into an executable shell command. + +The Codex command plan is provisional until Task 6 verifies it against a disposable real installation. Implementing the Codex strategy is blocked on evidence that `marketplace upgrade` refreshes the already-installed plugin, not only marketplace metadata. If it does not, the design and specification must be amended before implementation to use the documented plugin remove/add lifecycle and to cover configuration preservation. + +## Data Flow + +### Check-only flow + +```mermaid +sequenceDiagram + participant User + participant CLI + participant Coordinator + participant Inventory + participant Registry + + User->>CLI: update [scope] --check + CLI->>Coordinator: checkUpdates(options) + Coordinator->>Inventory: detect targets and local versions + Inventory-->>Coordinator: installed targets + Coordinator->>Registry: resolve latest versions + Registry-->>Coordinator: validated versions or unknown/error + Coordinator-->>CLI: UpdateSummary(checkOnly=true) + CLI-->>User: human output or one JSON document + Note over Coordinator: No strategy execute method is called +``` + +### Mutating update flow + +```mermaid +sequenceDiagram + participant User + participant CLI + participant Coordinator + participant Strategy + participant ExternalCLI + + User->>CLI: update [scope] + CLI->>Coordinator: build plan + Coordinator-->>CLI: ordered plan + CLI-->>User: display plan and request confirmation + User-->>CLI: confirm or --yes + loop each target, sequentially + Coordinator->>Strategy: execute(planItem) + Strategy->>ExternalCLI: spawn executable + fixed args + ExternalCLI-->>Strategy: exit/status/output + Strategy-->>Coordinator: sanitized UpdateResult + end + Coordinator-->>CLI: aggregate summary + CLI-->>User: per-target result and restart guidance +``` + +CLI self-update is planned first, but the running process does not dynamically import the newly installed package. Remaining already-planned harness strategies execute from the current process. The user must invoke the CLI again to use new CLI code. + +### Antigravity replacement transaction + +```mermaid +sequenceDiagram + participant Updater + participant FS + participant AGY + + Updater->>FS: locate known staged N|Solid root + Updater->>FS: copy staged root to temporary backup + Updater->>AGY: uninstall nsolid-plugin + Updater->>AGY: install GitHub root + alt install and validation succeed + Updater->>FS: remove temporary backup + else install or validation fails + Updater->>FS: restore backup to staged root + Updater-->>Updater: return failed + rollback status + end +``` + +### Release preparation + +```mermaid +sequenceDiagram + participant Maintainer + participant Prepare + participant Files + participant Generators + participant Check + + Maintainer->>Prepare: release:prepare -- patch|minor|major| + Prepare->>Files: read and snapshot controlled files + Prepare->>Prepare: validate increasing semver + Prepare->>Files: update three source versions + Prepare->>Generators: bundle sync + root manifest generation + Prepare->>Check: validate complete synchronization + alt validation succeeds + Prepare-->>Maintainer: version + changed-file summary + else any stage fails + Prepare->>Files: restore all snapshots + Prepare-->>Maintainer: failing stage, non-zero exit + end +``` + +## Error Handling and Safety + +- Network lookups have explicit timeouts and schema validation. +- Missing executables use a distinct error code from command failure. +- Process output is bounded before being retained in results. +- Existing logger redaction is applied to verbose diagnostics. +- `--all` catches errors at the target boundary and continues with independent targets. +- Confirmation is mandatory for mutable non-interactive operations unless `--yes` is present. +- Antigravity backup paths are created with restrictive permissions in an OS temporary directory and always cleaned after success. +- Update does not invoke setup, login, or auth modules. +- Release scripts snapshot only an explicit allowlist; rollback never performs broad Git or recursive workspace resets. + +## Migration Strategy + +This is an additive migration. + +1. Add pure types, semantic-version comparison, command runner, and version sources with unit tests. +2. Add inventory and target strategies behind programmatic APIs. +3. Add CLI parsing/formatting and integration tests. +4. Add release preparation/check scripts and fixture tests. +5. Add optional fallback tracking version while preserving reads of legacy tracking files. +6. Update README/package documentation. +7. Ship the feature in a new minor CLI release because it adds public commands; existing `1.0.x` install/setup behavior remains compatible. + +Deployment order: + +1. Merge version-bearing root manifests and update implementation. +2. Publish the new `nsolid-plugin` package. +3. Publish the same-version `nsolid-pi-plugin` package. +4. Push the matching semantic Git tag. +5. Verify update checks and actual updates from clean fixture homes for every harness. + +The update command is useful immediately for future releases; the first release containing it is still installed through the existing manual npm/native update instructions. diff --git a/openspec/changes/add-update-flow/proposal.md b/openspec/changes/add-update-flow/proposal.md new file mode 100644 index 0000000..e53968a --- /dev/null +++ b/openspec/changes/add-update-flow/proposal.md @@ -0,0 +1,83 @@ +# Proposal + +## Problem Statement + +N|Solid Plugin is distributed through several independent owners: + +- the `nsolid-plugin` CLI is published to npm; +- Claude and Codex clone a Git-backed marketplace and cache installed plugin versions; +- Antigravity stages a copy of the GitHub plugin without exposing a plugin update command; +- Pi owns its skills through the `nsolid-pi-plugin` npm package; and +- OpenCode receives skills and MCP configuration through the fallback CLI installer. + +Publishing new skills or runtime fixes therefore does not produce one consistent update experience. The current CLI has no `version`, `update`, or update-check command, users must know harness-specific commands, and a push to `main` can remain invisible to version-keyed caches when release metadata is not bumped. Maintainers must also update several version fields and generated manifests manually, which makes a partially versioned release possible. + +## Proposed Solution + +Add an explicit, version-aware update workflow for both maintainers and users. + +For users: + +- add `nsolid-plugin version` (with bare `nsolid-plugin --version` as an alias) and `nsolid-plugin update`; +- make plain `update` target the npm CLI, `--harness ` target one harness installation, and `--all` target the CLI plus every detected N|Solid installation; +- add `--check` for a read-only status check and retain `--json`, `--yes`, `--verbose`, and `--no-color` behavior where applicable; +- delegate native updates to the owning harness: + - Claude: refresh/update `nsolid-plugin@nodesource`; + - Codex: upgrade the `nodesource` Git marketplace and refresh the installed version; + - Antigravity: safely reinstall the GitHub-root plugin with backup/rollback; + - Pi: update `npm:nsolid-pi-plugin`; + - OpenCode, which has no native package owner, and other fallback installations: reinstall from the latest published CLI bundle; +- preserve credentials and non-NodeSource configuration throughout updates; +- isolate failures during `--all` so one harness failure does not prevent remaining updates, while returning a failing exit status and a per-target summary. + +For maintainers: + +- establish one release version across `bundle.json`, the core npm package, the Pi npm package, and generated version-bearing manifests; +- add release preparation/check tooling that performs or validates version propagation without publishing, tagging, or pushing; +- require update-visible releases to increment the version before generated root manifests are committed. + +## Rollback Plan + +- The CLI update path records the previously installed CLI version and prints the exact package-manager command needed to restore it. +- Native harness updates rely on the harness owner’s cache where available. Antigravity creates a temporary backup of the staged NodeSource plugin and restores it if reinstall fails. +- Fallback installers continue using their existing config backups and idempotent merge behavior. +- Update operations never delete shared NodeSource credentials. +- The feature can be reverted by removing the new command/service modules and scripts; existing `setup`, `install`, `doctor`, `restore`, and `uninstall` contracts remain unchanged. +- A bad release can be rolled back by republishing or reinstalling the prior known-good package/plugin version and restoring generated manifests from the corresponding Git tag. + +## Affected Components + +- `packages/core/src/cli.ts` — new commands and update flags. +- `packages/core/src/index.ts` — public update/version API surface. +- `packages/core/src/update/` — update planning, version comparison, command execution, result contracts, and package-manager detection. +- `packages/core/src/harnesses/` — harness-owned update strategies and native/fallback installation detection. +- `packages/core/src/index.ts` (the `doctor` function) and formatting utilities — optional update availability in health/status output. +- `packages/core/test/unit/update/` — version, planning, detection, safety, and output tests. +- `packages/core/test/integration/` — mocked CLI/harness update flows, partial failures, rollback, and exit codes. +- `bundle.json`, `packages/core/package.json`, `packages/pi-plugin/package.json` — coordinated release version. +- `scripts/` and root `package.json` — release preparation and drift checks. +- `.claude-plugin/marketplace.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, and `packages/core/bundle.json` — generated version-bearing outputs. +- `README.md` and package READMEs — user and maintainer update instructions. +- `openspec/specs/installation-and-auth/spec.md` — referenced compatibility contract that update must preserve; unchanged by this proposal. + +## Success Criteria + +- `nsolid-plugin version` and its bare `--version` alias report the running CLI and bundled plugin versions without network access; the command form also supports JSON output. +- `nsolid-plugin update --check` performs no writes or subprocess mutations and clearly reports whether the CLI is current. +- Each supported harness has a deterministic update strategy with actionable output when its CLI is unavailable or its installation type is unsupported. +- `nsolid-plugin update --all` updates all detected targets, preserves credentials and user-owned configuration, summarizes every target, and exits non-zero on any failure. +- Interactive destructive/replacement steps require confirmation; `--yes` enables non-interactive automation. +- Network, registry, missing-binary, permission, corrupt-state, and partial-update failures are covered by tests and never expose credentials. +- Release preparation propagates one requested semantic version to every version-bearing source/generated file and never publishes, tags, commits, or pushes. +- Release checking fails when package, bundle, or generated manifest versions drift. +- Existing installation, authentication, uninstall, restore, doctor, lint, build, and test behavior remains green. + +Acceptance tests: + +1. Mock npm reporting a newer CLI version and verify check-only, confirmed update, declined update, and rollback guidance. +2. Mock current and newer Claude/Codex plugin versions and verify the owning marketplace/update commands and restart guidance. +3. Simulate an Antigravity reinstall failure and verify restoration of the prior staged plugin. +4. Mock a Pi package update and an OpenCode fallback refresh from the latest CLI bundle. +5. Run `--all` with one failed target and verify later targets still run, credentials remain untouched, and the final exit code is non-zero. +6. Prepare a patch version in a fixture repository and verify all version-bearing files become equal while no publish/tag/push command executes. +7. Introduce version drift in each controlled file and verify the release check identifies the exact mismatch. diff --git a/openspec/changes/add-update-flow/specs/release-versioning/spec.md b/openspec/changes/add-update-flow/specs/release-versioning/spec.md new file mode 100644 index 0000000..8dcca8a --- /dev/null +++ b/openspec/changes/add-update-flow/specs/release-versioning/spec.md @@ -0,0 +1,143 @@ +# Release Versioning Specification + +## ADDED Requirements + +### Requirement: Atomic release version preparation + +Release tooling SHALL accept `patch`, `minor`, `major`, or an explicit increasing semantic version and propagate it across source packages and generated version-bearing manifests without publishing or Git mutation. + +#### Scenario: Prepare a patch release + +**Given** every controlled version is synchronized at a valid stable semantic version +**And** the working tree contains the intended release changes +**When** the maintainer runs the release preparation command with `patch` +**Then** the command computes the next patch version +**And** writes it to `bundle.json`, `packages/core/package.json`, and `packages/pi-plugin/package.json` +**And** synchronizes `packages/core/bundle.json` +**And** regenerates `.claude-plugin/marketplace.json`, `.claude-plugin/plugin.json`, and `.codex-plugin/plugin.json` +**And** leaves manifests without an explicit version field schema-valid +**And** prints the resulting version and changed files + +#### Scenario: Prepare a minor or major release + +**Given** every controlled version is synchronized at a valid stable semantic version +**When** the maintainer runs the release preparation command with `minor` or `major` +**Then** the command increments the requested semantic-version component +**And** resets lower-order components to zero +**And** propagates the resulting version to every controlled version-bearing file +**And** refreshes generated manifests through existing generators + +#### Scenario: Prepare an explicit semantic version + +**Given** the maintainer supplies a version greater than the current stable version +**When** release preparation runs +**Then** the exact supplied version is propagated to every controlled version-bearing file +**And** generated manifests are refreshed through existing generators +**And** no unrelated source file is modified + +#### Scenario: Release preparation has no external side effects + +**Given** release preparation succeeds +**When** the command completes +**Then** it has not committed, tagged, pushed, packed, published, authenticated, or contacted a package registry +**And** package-local materialized skill directories are absent +**And** the maintainer can review the Git diff before external release actions + +#### Scenario: Reject invalid or non-incrementing versions + +**Given** the requested version is invalid, equal to the current version, or lower +**When** release preparation validates the request +**Then** it fails before writing any file +**And** identifies the invalid current/requested relationship +**And** leaves the working tree unchanged + +#### Scenario: Atomic release preparation failure + +**Given** the requested version is valid +**When** writing or generation fails after preparation starts +**Then** every controlled file is restored to its pre-command content +**And** the command exits non-zero +**And** reports the failing stage +**And** no partially synchronized release remains + +### Requirement: Release version drift detection + +Release checking SHALL compare every controlled version and generated artifact with canonical `bundle.json.version` without repairing in check mode. + +#### Scenario: Check synchronized release versions + +**Given** the repository contains source and generated release metadata +**When** the maintainer runs the release version check +**Then** it compares package and generated versions with `bundle.json.version` +**And** validates root manifests against existing generators +**And** validates `packages/core/bundle.json` against the root bundle +**And** succeeds only when every controlled value and artifact is synchronized + +#### Scenario: Release version drift is detected + +**Given** one or more controlled files contain a different version or stale content +**When** the release version check runs +**Then** it exits non-zero +**And** lists every drifted file with expected and actual versions when available +**And** recommends preparation or synchronization +**And** does not repair files + +### Requirement: Plugin payload changes require an update-visible version + +Release checking SHALL reject payload changes whose explicit bundle version still matches the most recent release tag. + +Release mode SHALL be activated only by `pnpm release:check --release`. For this comparison, “plugin payload files” is the following explicit allowlist: + +- `skills/**` +- `bundle.json` +- `.claude-plugin/marketplace.json` +- `.claude-plugin/plugin.json` +- `.agents/plugins/marketplace.json` +- `.codex-plugin/plugin.json` +- `.claude-mcp.json` +- `.mcp.json` +- `plugin.json` +- `mcp_config.json` +- `scripts/mcp-wrapper.js` + +#### Scenario: Skill changes retain the previous release version + +**Given** committed plugin payload files differ from the most recent release tag +**And** `bundle.json.version` still equals the version represented by that tag +**When** the maintainer runs `pnpm release:check --release` +**Then** it fails with guidance to prepare a new semantic version +**And** prevents a release that version-keyed harness caches would treat as unchanged + +### Requirement: Manual publication remains ordered and external + +Release preparation SHALL leave publication to the maintainer while defining the required package and Git ordering. + +#### Scenario: Manual publication order + +**Given** preparation and quality checks succeeded +**When** the maintainer performs the external release +**Then** `nsolid-plugin@` is published before `nsolid-pi-plugin@` +**And** Pi resolves its `workspace:*` dependency to the same core version +**And** the commit and tag containing generated root manifests are pushed +**And** publication remains outside the preparation command + +#### Scenario: Interrupted package materialization is cleaned + +**Given** pack or publish materialized package-local skills +**When** publication is interrupted or only one package completes +**Then** the existing cleanup command removes `packages/core/skills/` and `packages/pi-plugin/skills/` +**And** canonical root `skills/` remains unchanged + +### Requirement: Preserve canonical release boundaries + +Release tooling SHALL keep root skills/bundle canonical and exclude non-release metadata from version synchronization. + +#### Scenario: Preserve canonical and private package state + +**Given** release preparation or checking runs +**When** it evaluates controlled files +**Then** root `skills/` and `bundle.json` remain canonical +**And** existing generators remain the writers of generated manifests +**And** Antigravity metadata remains schema-valid while staged `bundle.json` carries its version +**And** the private workspace root package remains `0.0.0` +**And** existing plugin and bundle synchronization commands remain available diff --git a/openspec/changes/add-update-flow/specs/update-flow/spec.md b/openspec/changes/add-update-flow/specs/update-flow/spec.md new file mode 100644 index 0000000..3292c98 --- /dev/null +++ b/openspec/changes/add-update-flow/specs/update-flow/spec.md @@ -0,0 +1,255 @@ +# Update Flow Specification + +## ADDED Requirements + +### Requirement: Running version reporting + +The CLI SHALL expose the running npm package version and bundled plugin version without network access or mutation. + +#### Scenario: Report running versions + +**Given** the `nsolid-plugin` CLI is runnable +**When** the user runs `nsolid-plugin version` +**Then** the command reports the running `nsolid-plugin` package version +**And** reports the bundled plugin version from `bundle.json` +**And** `--json` returns a stable object containing `cliVersion` and `bundleVersion` +**And** the command performs no network requests or writes + +#### Scenario: Report versions with the conventional flag + +**Given** the `nsolid-plugin` CLI is runnable +**When** the user runs bare `nsolid-plugin --version` +**Then** the command is an alias for the human-readable `nsolid-plugin version` output +**And** reports both the running CLI package version and bundled plugin version +**And** performs no network requests or writes + +### Requirement: Read-only update checks + +The updater SHALL compare installed and latest versions without invoking any mutating strategy when `--check` is supplied. + +#### Scenario: Check whether the CLI is current + +**Given** the npm registry reports a stable `latest` version for `nsolid-plugin` +**When** the user runs `nsolid-plugin update --check` +**Then** the command compares the running CLI semantic version with the registry version +**And** reports `current`, `update-available`, or `newer-than-registry` +**And** does not invoke a package manager or modify any file +**And** `--json` returns the current version, latest version, status, and target identifier +**And** exits successfully, including when the status is `update-available` + +#### Scenario: Check every detected target + +**Given** multiple N|Solid installations are detectable +**When** the user runs `nsolid-plugin update --all --check` +**Then** every target is inspected without invoking install, update, uninstall, package-manager, tracking, or configuration mutations +**And** targets whose installed version cannot be determined report `unknown` +**And** the command distinguishes `unknown` from `current` + +#### Scenario: Registry lookup fails + +**Given** npm is unreachable, times out, returns invalid data, or returns a non-semantic version +**When** the user checks or performs an update +**Then** the command reports the registry failure without exposing response bodies containing credentials +**And** performs no update +**And** exits non-zero +**And** preserves the current installation + +### Requirement: Safe CLI self-update + +The default `nsolid-plugin update` scope SHALL update only a positively identified global CLI installation and SHALL require approval before mutation. + +#### Scenario: CLI update with a supported global package manager + +**Given** the CLI was installed globally by npm or pnpm +**And** the registry reports a newer stable version +**When** the user runs `nsolid-plugin update` +**Then** the command displays the current version, target version, package manager, and exact planned operation +**And** asks for confirmation in an interactive terminal +**And** after confirmation invokes the detected package manager with a fixed argument array to install `nsolid-plugin@` +**And** verifies the child process succeeded +**And** reports that a new shell or command invocation may be required +**And** prints the exact command for restoring the previous version + +#### Scenario: User declines a CLI update + +**Given** an update is available +**When** the user declines the confirmation +**Then** no package-manager process runs +**And** the result is `skipped` +**And** the command exits successfully + +#### Scenario: Non-interactive CLI update + +**Given** an update is available +**And** standard input is not interactive +**When** the user runs `nsolid-plugin update` without `--yes` +**Then** the command performs no mutation +**And** exits non-zero with guidance to pass `--yes` +**When** the user reruns with `--yes` +**Then** the command performs the displayed fixed update plan without prompting + +#### Scenario: CLI is already current + +**Given** the running CLI version equals the registry `latest` version +**When** the user runs `nsolid-plugin update` +**Then** no package-manager process runs +**And** the command reports `already current` +**And** exits successfully + +#### Scenario: Unsupported CLI installation source + +**Given** the running CLI was launched from a workspace, local path, `npx`, or an installation source that cannot be safely identified +**When** the user runs `nsolid-plugin update` +**Then** the command does not guess a package manager or modify the installation +**And** reports the latest version when it can be resolved +**And** prints safe manual commands for npm, pnpm, and `npx -y nsolid-plugin@latest` + +### Requirement: Harness-owned update strategies + +The updater SHALL preserve native/package ownership and delegate each supported harness update to a deterministic strategy without starting OAuth. + +#### Scenario: Update one installed native harness + +**Given** the requested harness has a detected native N|Solid plugin installation +**When** the user runs `nsolid-plugin update --harness ` +**Then** only that harness target is planned +**And** the command delegates to the harness-owned update strategy +**And** shared NodeSource credentials remain unchanged +**And** no OAuth browser or callback server starts +**And** the result includes versions when discoverable, status, and restart guidance + +#### Scenario: Update Claude native plugin + +**Given** `nsolid-plugin@nodesource` is installed natively in Claude +**And** the `claude` executable is available +**When** the Claude update strategy runs +**Then** it invokes `claude plugin update nsolid-plugin@nodesource` with a fixed executable and argument array +**And** reports `/reload-plugins` or restart guidance +**And** does not run the fallback installer + +#### Scenario: Update Codex native plugin + +**Given** `nsolid-plugin@nodesource` is installed natively in Codex +**And** the `codex` executable is available +**When** the Codex update strategy runs +**Then** it invokes `codex plugin marketplace upgrade nodesource` +**And** verifies the marketplace refresh succeeded +**And** reports that a new Codex session is required +**And** does not remove the installed plugin or configuration + +#### Scenario: Update Pi package-owned skills + +**Given** `npm:nsolid-pi-plugin` is installed in Pi +**And** the `pi` executable is available +**When** the Pi update strategy runs +**Then** it invokes `pi update npm:nsolid-pi-plugin` +**And** does not copy Pi skills into user-level skill directories +**And** reports `/reload` or restart guidance +**And** leaves Pi MCP configuration and NodeSource credentials intact + +#### Scenario: Update OpenCode or another fallback installation + +**Given** the target is OpenCode, which has no native plugin/package update owner, or another target uses the N|Solid fallback/direct installer +**When** its update strategy runs +**Then** it resolves the latest published `nsolid-plugin` CLI bundle +**And** reruns the latest fallback installer only for that harness +**And** reuses existing idempotent skill and MCP merge behavior +**And** preserves non-NodeSource artifacts and valid credentials +**And** creates the normal configuration backup before config mutation + +#### Scenario: Requested harness is not installed + +**Given** neither a native nor fallback N|Solid installation is detected for the requested harness +**When** the user runs `nsolid-plugin update --harness ` +**Then** no install is performed implicitly +**And** the target result is `not-installed` +**And** the command prints appropriate installation guidance + +#### Scenario: Required harness executable is missing + +**Given** a native N|Solid installation is detected +**But** its owning executable is unavailable on `PATH` +**When** its update strategy runs +**Then** no fallback replacement is attempted automatically +**And** the target fails with a missing-executable error +**And** output identifies the missing executable and manual command + +### Requirement: Transactional Antigravity replacement + +The Antigravity strategy SHALL back up and validate the staged NodeSource plugin because AGY has no native plugin-update command. + +#### Scenario: Update Antigravity native plugin + +**Given** the GitHub-root N|Solid plugin is staged by Antigravity +**And** the `agy` executable is available +**When** the Antigravity update strategy runs +**Then** it creates a temporary backup of the existing staged NodeSource plugin +**And** confirms replacement unless `--yes` was supplied +**And** invokes the supported uninstall/install sequence for `https://github.com/NodeSource/nsolid-plugin.git` +**And** removes the backup only after the new staged plugin validates +**And** preserves `~/.agents/.nodesource-auth.json` + +#### Scenario: Antigravity reinstall fails + +**Given** the previous Antigravity plugin was backed up +**When** uninstall succeeds but reinstall or validation fails +**Then** the updater restores the previous staged plugin atomically where supported +**And** reports whether rollback succeeded +**And** exits non-zero +**And** provides a manual reinstall command + +### Requirement: Deterministic multi-target orchestration + +The updater SHALL plan targets before mutation, execute them sequentially in deterministic order, and isolate target failures. + +#### Scenario: Update every detected target + +**Given** one or more N|Solid CLI or harness installations are detected +**When** the user runs `nsolid-plugin update --all` +**Then** the updater displays one ordered plan +**And** updates the CLI target first when supported +**And** updates detected harness targets sequentially in deterministic harness order +**And** records a result for every planned target +**And** prints counts for updated, current, skipped, not-installed, and failed + +#### Scenario: One target fails during update-all + +**Given** multiple update targets were planned +**When** one target fails +**Then** remaining independent targets are attempted +**And** the summary includes the failed target and actionable error +**And** the overall process exits non-zero +**And** no credential value appears in logs or JSON + +#### Scenario: Conflicting update scopes + +**Given** the user supplies both `--all` and `--harness` +**When** argument validation runs +**Then** the command rejects the invocation before network access or mutation +**And** explains that the scopes are mutually exclusive + +### Requirement: Stable and sanitized update output + +Update results SHALL support human-readable and machine-readable output without mixing progress into JSON or exposing secrets. + +#### Scenario: Structured update output + +**Given** the user passes `--json` +**When** an update or check completes +**Then** standard output contains exactly one valid JSON document +**And** progress and diagnostics are written to standard error +**And** each result contains `target`, `ownership`, `status`, optional versions, `changed`, optional restart guidance, and sanitized errors + +### Requirement: Preserve existing installation behavior + +Update operations SHALL retain all existing setup, installation, authentication, backup, merge, tracking, and uninstall safety contracts. + +#### Scenario: Preserve credentials and user-owned configuration + +**Given** the user has valid NodeSource credentials and non-NodeSource skills or MCP servers +**When** any update strategy succeeds, fails, or rolls back +**Then** credentials remain present and unchanged +**And** non-NodeSource skills and MCP entries remain unchanged +**And** update never invokes setup, login, or OAuth +**And** native strategy failure never silently switches to fallback ownership +**And** all external commands run without a shell and with fixed argument arrays diff --git a/openspec/changes/add-update-flow/tasks.md b/openspec/changes/add-update-flow/tasks.md new file mode 100644 index 0000000..91b1e65 --- /dev/null +++ b/openspec/changes/add-update-flow/tasks.md @@ -0,0 +1,107 @@ +# Tasks + +## Task 1: Define update contracts and semantic-version behavior + +- [ ] **Description**: Add the pure update target, ownership, status, plan, result, summary, command, and strategy types defined in the design. Implement strict stable semantic-version parsing/comparison without adding a runtime dependency. +- **Depends on**: None +- **Files**: `packages/core/src/update/types.ts`, `packages/core/src/update/version.ts`, `packages/core/test/unit/update/version.test.ts` +- **Testing**: Cover valid versions, invalid registry values, equal/newer/older comparisons, and deterministic result/count shapes. References: Update Flow “Report running versions” and “Check whether the CLI is current.” + +## Task 2: Add safe command execution and version sources + +- [ ] **Description**: Implement the injected shell-free command runner, bounded/sanitized output, executable lookup, npm registry client, and GitHub-root bundle version source with explicit timeouts and validation. +- **Depends on**: Task 1 +- **Files**: `packages/core/src/update/command-runner.ts`, `packages/core/src/update/version-source.ts`, `packages/core/test/unit/update/command-runner.test.ts`, `packages/core/test/unit/update/version-source.test.ts` +- **Testing**: Mock success, timeout, missing executable, invalid JSON/version, non-zero exit, oversized output, and secret-bearing diagnostics. Verify all subprocess calls use `shell: false` and argument arrays. References: Update Flow “Registry lookup fails,” “Required harness executable is missing,” and “Preserve credentials and user-owned configuration.” + +## Task 3: Detect CLI installation ownership + +- [ ] **Description**: Read the running package/bundle versions and detect npm or pnpm global ownership only from positive evidence. Return unsupported for workspace, local, `npx`, or ambiguous execution. +- **Depends on**: Tasks 1–2 +- **Files**: `packages/core/src/update/package-manager.ts`, `packages/core/src/update/inventory.ts`, `packages/core/test/unit/update/package-manager.test.ts`, `packages/core/test/unit/update/inventory.test.ts` +- **Testing**: Use fixture paths/environments for npm, pnpm, `npx`, workspace, broken symlink, and ambiguous launchers. Verify unsupported sources produce guidance without mutation. References: Update Flow “Unsupported CLI installation source.” + +## Task 4: Implement CLI package update strategy + +- [ ] **Description**: Implement CLI check/update planning, confirmation metadata, npm/pnpm command generation, post-command reporting, and exact previous-version rollback guidance. +- **Depends on**: Tasks 1–3 +- **Files**: `packages/core/src/update/strategies/cli-package.ts`, `packages/core/test/unit/update/cli-package.test.ts` +- **Testing**: Cover current, update available, newer-than-registry, declined, `--yes`, unsupported source, failed package manager, and rollback command. References: all CLI-specific scenarios in Update Flow. + +## Task 5: Extend harness inventory and version evidence + +- [ ] **Description**: Reuse native detection and fallback tracking to classify Claude, Codex, OpenCode, Antigravity, and Pi ownership. Add optional installed version/staged root evidence and backward-compatible `bundleVersion` tracking for fallback installs. +- **Depends on**: Tasks 1–3 +- **Files**: `packages/core/src/update/inventory.ts`, `packages/core/src/harnesses/*.ts`, `packages/core/src/skills/skill-tracker.ts`, related harness/tracker tests +- **Testing**: Cover native, fallback, package-owned, missing, corrupt metadata, legacy tracking without `bundleVersion`, and version-unknown states for every harness. References: Update Flow “Requested harness is not installed,” “Check every detected target,” “Update one installed native harness,” and “Preserve credentials and user-owned configuration.” + +## Task 6: Implement Claude and Codex native strategies + +- [ ] **Blocking verification**: Before implementing `codex.ts`, use a disposable real Codex installation to verify whether `codex plugin marketplace upgrade nodesource` refreshes the version and content of an already-installed plugin rather than only marketplace metadata. Record the tested versions and command/output evidence. If it does not refresh the installed copy, stop and amend the design, Update Flow specification, and this task to use the documented plugin remove/add lifecycle with configuration-preservation coverage. +- [ ] **Description**: Add strategies that generate and execute the fixed Claude plugin update and Codex marketplace upgrade commands, retain native ownership, and return restart/reload guidance. +- **Depends on**: Tasks 2 and 5 +- **Files**: `packages/core/src/update/strategies/claude.ts`, `packages/core/src/update/strategies/codex.ts`, corresponding unit tests +- **Testing**: Mock successful refresh, already-current output, command failure, missing executable, alternate detected plugin IDs/marketplace names where supported, and verify no fallback/auth call. References: Update Flow “Update Claude native plugin” and “Update Codex native plugin.” + +## Task 7: Implement Pi and fallback/OpenCode strategies + +- [ ] **Description**: Add the package-owned Pi update strategy and latest-published-CLI fallback refresh strategy. Reuse existing idempotent installation, backup, merge, and tracking code rather than duplicating it. +- **Depends on**: Tasks 2 and 5 +- **Files**: `packages/core/src/update/strategies/pi.ts`, `packages/core/src/update/strategies/fallback.ts`, corresponding unit/integration tests +- **Testing**: Verify the exact Pi source, package-owned skill boundaries, OpenCode skill refresh, fallback MCP merge, configuration backup, preserved credentials, and no implicit install for an absent target. References: Update Flow “Update Pi package-owned skills,” “Update OpenCode or a fallback installation,” and “Requested harness is not installed.” + +## Task 8: Implement transactional Antigravity update + +- [ ] **Description**: Add known-path staging detection, restrictive temporary backup, confirmed uninstall/install, new-root validation, successful cleanup, and rollback restoration. +- **Depends on**: Tasks 2 and 5 +- **Files**: `packages/core/src/update/antigravity-transaction.ts`, `packages/core/src/update/strategies/antigravity.ts`, related unit/integration tests +- **Testing**: Cover both supported staged roots, successful replacement, declined confirmation, uninstall failure, install failure, validation failure, rollback success/failure, cleanup, and credential preservation. References: Update Flow “Update Antigravity native plugin” and “Antigravity reinstall fails.” + +## Task 9: Build the coordinator and programmatic API + +- [ ] **Description**: Implement scope validation, deterministic target ordering, check-only short circuit, plan confirmation, sequential execution, per-target failure isolation, aggregation, and public `getVersionInfo()`, `checkUpdates()`, and `update()` exports. +- **Depends on**: Tasks 4 and 6–8 +- **Files**: `packages/core/src/update/coordinator.ts`, `packages/core/src/update/index.ts`, `packages/core/src/index.ts`, coordinator/API tests +- **Testing**: Cover CLI-only default, one harness, `--all`, `--all` plus harness rejection, check-only no-execute guarantee, one failure with later success, empty inventory, status counts, and overall success/exit semantics. References: Update Flow “Update every detected target,” “One target fails during update-all,” “Check every detected target,” and “Conflicting update scopes.” + +## Task 10: Add CLI commands and output formatting + +- [ ] **Description**: Add `version` and `update` command parsing, bare `--version`, `--check`/`--all`, confirmation integration, human-readable summaries, JSON-only stdout, stderr progress, help text, and exit-code mapping. +- **Depends on**: Task 9 +- **Files**: `packages/core/src/cli.ts`, `packages/core/src/utils/format.ts` or a new update formatter, CLI help/unit/integration tests +- **Testing**: Spawn the CLI with every supported scope/output combination. Assert exact JSON parseability, no ANSI in JSON, no mutation in check mode, prompts only when appropriate, sanitized errors, bare `--version` parity, and exit zero when a successful check reports `update-available`. References: Update Flow “Structured update output,” “Report versions with the conventional flag,” and “Non-interactive CLI update.” + +## Task 11: Add atomic release preparation + +- [ ] **Description**: Implement `release:prepare` for `patch`, `minor`, `major`, and explicit increasing versions. Snapshot the controlled allowlist, update the three source versions, invoke existing bundle/root generators, validate, restore on failure, and leave the private root version untouched. +- **Depends on**: Task 1 +- **Files**: `scripts/prepare-release.mjs`, `package.json`, generator exports/refactors if needed, `packages/core/test/unit/scripts/prepare-release.test.ts` +- **Testing**: Use isolated fixture roots to verify patch/minor/major/explicit propagation, generated files, invalid/equal/lower rejection with zero writes, mid-stage rollback, unrelated-file preservation, no package skill materialization, and absence of publish/Git/network side effects. References: Release Versioning “Prepare a patch release” through “Atomic release preparation failure.” + +## Task 12: Add release drift and payload checks + +- [ ] **Description**: Implement `release:check`, including source/package equality, generated artifact checks, exact mismatch reporting, and cleanup-state validation. When and only when `--release` is present, compare the specification's explicit payload allowlist with the latest semantic-version tag and validate that payload changes have an update-visible version. +- **Depends on**: Task 11 +- **Files**: `scripts/check-release-version.mjs`, `package.json`, script fixture tests +- **Testing**: Introduce drift independently in every controlled file, stale generated output, unchanged version with changes in each payload allowlist category, malformed/missing tag state, and materialized package skills. Verify normal and `--release` check modes never repair. References: Release Versioning “Check synchronized release versions,” “Release version drift is detected,” and “Skill changes require an update-visible version.” + +## Task 13: Add end-to-end update regression coverage + +- [ ] **Description**: Exercise the public CLI against isolated homes and fake harness executables/registries, including mixed native/fallback ownership and partial failure. +- **Depends on**: Tasks 9–12 +- **Files**: `packages/core/test/integration/update-flow.test.ts`, test fixtures/helpers, `scripts/test-marketplace-install.js` where update assertions fit +- **Testing**: Cover all acceptance tests from the proposal on Linux-compatible fixtures and keep paths/commands portable for macOS and Windows CI. Assert credentials and non-NodeSource configurations are byte-for-byte preserved. + +## Task 14: Document user and maintainer workflows + +- [ ] **Description**: Document CLI self-update, per-harness update ownership, check/JSON/automation modes, AGY replacement behavior, rollback guidance, version propagation, manual publication order, and the first-release bootstrap limitation. +- **Depends on**: Tasks 10–12 +- **Files**: `README.md`, `packages/core/README.md`, `packages/pi-plugin/README.md` +- **Testing**: Validate every documented command against CLI help/tests and ensure no documentation implies that a Git push alone updates version-keyed caches. References: both specifications and Design “Migration Strategy.” + +## Task 15: Run release-quality gates + +- [ ] **Description**: Run version drift checks, source/plugin checks, lint, type checking/build, all unit/integration tests, marketplace install tests, and package dry-run inspection for both publishable packages. +- **Depends on**: Tasks 13–14 +- **Files**: No production files unless a gate exposes a defect +- **Testing**: `pnpm release:check --release`, `pnpm plugin:check`, `pnpm lint`, `pnpm build`, `pnpm test`, `pnpm test:marketplace`, plus dry-run package contents confirming updated skills and same-version Pi/core dependency resolution. From e1cd2e96415d471a014a9359f4e644c68a27fa38 Mon Sep 17 00:00:00 2001 From: Cesar-M-Diaz Date: Fri, 31 Jul 2026 15:59:23 +0200 Subject: [PATCH 2/4] docs(openspec): harden add-update-flow contracts - Add explicit version, source, installation, and rollback contracts - Handle newer-than-registry and unsupported update outcomes - Preserve native marketplace identities and separate fallback installations - Reject non-canonical Pi sources without mutation - Restore Antigravity staged files and import manifest on rollback - Align proposal, update-flow spec, and implementation tasks --- openspec/changes/add-update-flow/design.md | 104 ++++++++++++++---- openspec/changes/add-update-flow/proposal.md | 27 +++-- .../add-update-flow/specs/update-flow/spec.md | 66 +++++++++-- openspec/changes/add-update-flow/tasks.md | 34 +++--- 4 files changed, 170 insertions(+), 61 deletions(-) diff --git a/openspec/changes/add-update-flow/design.md b/openspec/changes/add-update-flow/design.md index 6119f77..a810c84 100644 --- a/openspec/changes/add-update-flow/design.md +++ b/openspec/changes/add-update-flow/design.md @@ -49,9 +49,10 @@ Release preparation is a separate maintainer-side script. Runtime update code re `packages/core/src/update/inventory.ts` -- Reuses harness adapters and tracking readers to classify each harness as native, fallback, package-owned, or not installed. +- Reuses harness adapters and tracking readers to return one installation record per detected native, fallback, or package-owned installation; native and fallback records for the same harness are not collapsed. - Reads the running CLI/package metadata. -- Adds optional version discovery without changing existing installation detection contracts. +- Carries validated source identity (plugin ID/marketplace, package source, or fallback provenance) into each plan item without changing existing installation detection contracts. +- Treats local, pinned, ambiguous, or otherwise unsupported update sources as `unsupported` instead of substituting a different source. `packages/core/src/update/version-source.ts` @@ -82,9 +83,9 @@ Release preparation is a separate maintainer-side script. Runtime update code re `packages/core/src/update/antigravity-transaction.ts` - Resolves only known NodeSource staged plugin paths. -- Creates a temporary backup before replacement. -- Validates the newly staged root by checking `plugin.json`, `bundle.json`, and canonical skill presence. -- Restores the backup if reinstall or validation fails. +- Creates a temporary backup before replacement containing the staged root and the N|Solid entry in `~/.gemini/config/import_manifest.json`. +- Validates the newly staged root by checking `plugin.json`, `bundle.json`, canonical skill presence, and source registration in the import manifest. +- Restores both the staged root and the saved manifest entry if reinstall or validation fails, preserving unrelated manifest imports. ### Existing modules extended @@ -141,6 +142,8 @@ The private root package version remains `0.0.0`. ## Interfaces and Contracts ```typescript +import type { HarnessType } from '../types.js' + export type UpdateTarget = | 'cli' | 'claude' @@ -155,19 +158,44 @@ export type UpdateOwnership = | 'package-owned' | 'fallback' +export type VersionStatus = + | 'current' + | 'update-available' + | 'newer-than-registry' + | 'unknown' + export type UpdateStatus = | 'current' | 'update-available' + | 'newer-than-registry' | 'updated' | 'skipped' | 'not-installed' + | 'unsupported' | 'unknown' | 'failed' export interface VersionInfo { current?: string latest?: string - status: 'current' | 'update-available' | 'newer-than-registry' | 'unknown' + status: VersionStatus +} + +export type UpdateSource = + | { kind: 'global-package'; packageManager: 'npm' | 'pnpm'; packageName: 'nsolid-plugin' } + | { kind: 'marketplace'; pluginId: string; marketplace: string } + | { kind: 'pi-package'; spec: 'npm:nsolid-pi-plugin' } + | { kind: 'unsupported'; source: string; reason: 'local' | 'git' | 'pinned' | 'ambiguous' } + | { kind: 'antigravity-git'; url: 'https://github.com/NodeSource/nsolid-plugin.git' } + | { kind: 'fallback'; bundleVersion?: string } + +export interface UpdateInstallation { + installationId: string + target: UpdateTarget + ownership: UpdateOwnership + installed: boolean + source: UpdateSource + version: VersionInfo } export interface UpdateOptions { @@ -183,9 +211,11 @@ export interface UpdateOptions { } export interface UpdatePlanItem { + installationId: string target: UpdateTarget ownership: UpdateOwnership installed: boolean + source: UpdateSource version: VersionInfo executable?: string args?: readonly string[] @@ -193,15 +223,29 @@ export interface UpdatePlanItem { restartHint?: string } +export interface UpdateConfirmationContext { + items: readonly UpdatePlanItem[] +} + +export type UpdateConfirmation = ( + context: UpdateConfirmationContext +) => boolean | Promise + export interface UpdateResult { + installationId: string target: UpdateTarget ownership: UpdateOwnership status: UpdateStatus currentVersion?: string + latestVersion?: string resultingVersion?: string changed: boolean restartHint?: string rollbackCommand?: string + rollback?: { + attempted: boolean + succeeded?: boolean + } error?: { code: string message: string @@ -222,13 +266,27 @@ export interface CommandSpec { timeoutMs: number } +export interface CommandResult { + exitCode: number | null + signal?: NodeJS.Signals + stdout: string + stderr: string + timedOut: boolean +} + export interface CommandRunner { run(spec: CommandSpec): Promise } +export interface UpdateContext { + options: Readonly + commandRunner: CommandRunner +} + export interface UpdateStrategy { readonly target: UpdateTarget - plan(context: UpdateContext): Promise + readonly ownership: UpdateOwnership + plan(installation: UpdateInstallation, context: UpdateContext): Promise execute(item: UpdatePlanItem, context: UpdateContext): Promise } ``` @@ -239,8 +297,11 @@ Rules enforced by these contracts: - Command arguments are arrays; a shell command string is not part of the contract. - `error.message` is sanitized and suitable for JSON output. - An absent version is represented as `unknown`, never coerced to `current`. +- A detected installation source that cannot be updated safely is represented as `unsupported`, never replaced with a different source. - Strategies return data; the CLI formatter owns human-readable output. -- A completed check whose result is `update-available` is successful and exits zero; lookup, validation, or execution failures remain non-zero. +- A completed check whose result is `update-available`, `newer-than-registry`, or `unsupported` is informational and exits zero; lookup, validation, or execution failures remain non-zero. +- A mutating update with `newer-than-registry` performs no downgrade and exits zero; a mutating `unsupported` result exits non-zero with manual guidance. +- A declined plan produces `skipped` results and exits zero. ### Fixed harness command plans @@ -248,13 +309,15 @@ Rules enforced by these contracts: |---|---|---| | CLI npm | `npm install -g nsolid-plugin@` | invoke CLI again | | CLI pnpm | `pnpm add -g nsolid-plugin@` | invoke CLI again | -| Claude | `claude plugin update nsolid-plugin@nodesource` | `/reload-plugins` or restart | -| Codex | `codex plugin marketplace upgrade nodesource` | start a new session | +| Claude | `claude plugin update ` | `/reload-plugins` or restart | +| Codex | `codex plugin marketplace upgrade ` | start a new session | | Antigravity | `agy plugin uninstall nsolid-plugin`, then install Git URL | restart AGY | -| Pi | `pi update npm:nsolid-pi-plugin` | `/reload` or restart | +| Pi | `pi update npm:nsolid-pi-plugin` for the canonical npm source only | `/reload` or restart | | Fallback/OpenCode | latest published CLI executes `install --harness ` | restart harness if needed | -No user-derived string is interpolated into an executable shell command. +Marketplace IDs and package sources are passed as separate arguments only after strict validation. A native ID is accepted only when it matches `nsolid-plugin@[A-Za-z0-9][A-Za-z0-9._-]*`; the base name alone, malformed IDs, control characters, whitespace, and ambiguous matches return `unsupported`. The only supported Pi source is the exact `npm:nsolid-pi-plugin`; local, Git, pinned, or ambiguous Pi sources return `unsupported`. No user-derived string is interpolated into an executable shell command. + +The planner emits one item per `UpdateInstallation`. If a harness has both native and fallback artifacts, both items remain visible and are updated independently; a native failure never switches to fallback ownership. The Codex command plan is provisional until Task 6 verifies it against a disposable real installation. Implementing the Codex strategy is blocked on evidence that `marketplace upgrade` refreshes the already-installed plugin, not only marketplace metadata. If it does not, the design and specification must be amended before implementation to use the documented plugin remove/add lifecycle and to cover configuration preservation. @@ -272,8 +335,8 @@ sequenceDiagram User->>CLI: update [scope] --check CLI->>Coordinator: checkUpdates(options) - Coordinator->>Inventory: detect targets and local versions - Inventory-->>Coordinator: installed targets + Coordinator->>Inventory: detect installations, sources, and local versions + Inventory-->>Coordinator: installation records Coordinator->>Registry: resolve latest versions Registry-->>Coordinator: validated versions or unknown/error Coordinator-->>CLI: UpdateSummary(checkOnly=true) @@ -296,7 +359,7 @@ sequenceDiagram Coordinator-->>CLI: ordered plan CLI-->>User: display plan and request confirmation User-->>CLI: confirm or --yes - loop each target, sequentially + loop each installation, sequentially Coordinator->>Strategy: execute(planItem) Strategy->>ExternalCLI: spawn executable + fixed args ExternalCLI-->>Strategy: exit/status/output @@ -317,13 +380,13 @@ sequenceDiagram participant AGY Updater->>FS: locate known staged N|Solid root - Updater->>FS: copy staged root to temporary backup + Updater->>FS: snapshot staged root and N|Solid import entry Updater->>AGY: uninstall nsolid-plugin Updater->>AGY: install GitHub root alt install and validation succeed - Updater->>FS: remove temporary backup + Updater->>FS: remove temporary backup after root + registration validation else install or validation fails - Updater->>FS: restore backup to staged root + Updater->>FS: restore staged root and import registration Updater-->>Updater: return failed + rollback status end ``` @@ -358,9 +421,10 @@ sequenceDiagram - Missing executables use a distinct error code from command failure. - Process output is bounded before being retained in results. - Existing logger redaction is applied to verbose diagnostics. -- `--all` catches errors at the target boundary and continues with independent targets. +- `--all` catches errors at the installation boundary and continues with independent installation records, including native and fallback records for the same harness. - Confirmation is mandatory for mutable non-interactive operations unless `--yes` is present. -- Antigravity backup paths are created with restrictive permissions in an OS temporary directory and always cleaned after success. +- Marketplace IDs are validated before becoming arguments; local, pinned, and ambiguous Pi sources are never silently replaced. +- Antigravity backup paths are created with restrictive permissions in an OS temporary directory and always cleaned after success. Rollback validates both the staged root and the saved import-manifest registration. - Update does not invoke setup, login, or auth modules. - Release scripts snapshot only an explicit allowlist; rollback never performs broad Git or recursive workspace resets. diff --git a/openspec/changes/add-update-flow/proposal.md b/openspec/changes/add-update-flow/proposal.md index e53968a..65a4dfd 100644 --- a/openspec/changes/add-update-flow/proposal.md +++ b/openspec/changes/add-update-flow/proposal.md @@ -19,13 +19,13 @@ Add an explicit, version-aware update workflow for both maintainers and users. For users: - add `nsolid-plugin version` (with bare `nsolid-plugin --version` as an alias) and `nsolid-plugin update`; -- make plain `update` target the npm CLI, `--harness ` target one harness installation, and `--all` target the CLI plus every detected N|Solid installation; +- make plain `update` target the npm CLI, `--harness ` target every detected installation for one harness, and `--all` target the CLI plus every detected N|Solid installation; - add `--check` for a read-only status check and retain `--json`, `--yes`, `--verbose`, and `--no-color` behavior where applicable; - delegate native updates to the owning harness: - - Claude: refresh/update `nsolid-plugin@nodesource`; - - Codex: upgrade the `nodesource` Git marketplace and refresh the installed version; - - Antigravity: safely reinstall the GitHub-root plugin with backup/rollback; - - Pi: update `npm:nsolid-pi-plugin`; + - Claude: refresh/update the detected `nsolid-plugin@` identity; + - Codex: upgrade the detected Git marketplace and refresh the installed version; + - Antigravity: safely reinstall the GitHub-root plugin with staged-root and import-manifest backup/rollback; + - Pi: update the canonical `npm:nsolid-pi-plugin` source, while rejecting local, pinned, Git, or ambiguous sources; - OpenCode, which has no native package owner, and other fallback installations: reinstall from the latest published CLI bundle; - preserve credentials and non-NodeSource configuration throughout updates; - isolate failures during `--all` so one harness failure does not prevent remaining updates, while returning a failing exit status and a per-target summary. @@ -39,7 +39,8 @@ For maintainers: ## Rollback Plan - The CLI update path records the previously installed CLI version and prints the exact package-manager command needed to restore it. -- Native harness updates rely on the harness owner’s cache where available. Antigravity creates a temporary backup of the staged NodeSource plugin and restores it if reinstall fails. +- A CLI newer than the registry is reported and left unchanged; this proposal has no implicit downgrade path. +- Native harness updates rely on the harness owner’s cache where available. Antigravity creates a temporary backup of the staged NodeSource plugin and its import-manifest registration and restores both if reinstall fails. - Fallback installers continue using their existing config backups and idempotent merge behavior. - Update operations never delete shared NodeSource credentials. - The feature can be reverted by removing the new command/service modules and scripts; existing `setup`, `install`, `doctor`, `restore`, and `uninstall` contracts remain unchanged. @@ -51,6 +52,7 @@ For maintainers: - `packages/core/src/index.ts` — public update/version API surface. - `packages/core/src/update/` — update planning, version comparison, command execution, result contracts, and package-manager detection. - `packages/core/src/harnesses/` — harness-owned update strategies and native/fallback installation detection. +- `~/.gemini/config/import_manifest.json` — Antigravity plugin registration included in the transactional backup/rollback contract. - `packages/core/src/index.ts` (the `doctor` function) and formatting utilities — optional update availability in health/status output. - `packages/core/test/unit/update/` — version, planning, detection, safety, and output tests. - `packages/core/test/integration/` — mocked CLI/harness update flows, partial failures, rollback, and exit codes. @@ -64,8 +66,9 @@ For maintainers: - `nsolid-plugin version` and its bare `--version` alias report the running CLI and bundled plugin versions without network access; the command form also supports JSON output. - `nsolid-plugin update --check` performs no writes or subprocess mutations and clearly reports whether the CLI is current. -- Each supported harness has a deterministic update strategy with actionable output when its CLI is unavailable or its installation type is unsupported. +- Each supported harness has a deterministic update strategy with actionable output when its CLI is unavailable, its installation type is unsupported, or its source identity cannot be safely reused. - `nsolid-plugin update --all` updates all detected targets, preserves credentials and user-owned configuration, summarizes every target, and exits non-zero on any failure. +- Native and fallback installations detected for the same harness are represented and updated as separate targets; one ownership never silently replaces the other. - Interactive destructive/replacement steps require confirmation; `--yes` enables non-interactive automation. - Network, registry, missing-binary, permission, corrupt-state, and partial-update failures are covered by tests and never expose credentials. - Release preparation propagates one requested semantic version to every version-bearing source/generated file and never publishes, tags, commits, or pushes. @@ -74,10 +77,10 @@ For maintainers: Acceptance tests: -1. Mock npm reporting a newer CLI version and verify check-only, confirmed update, declined update, and rollback guidance. -2. Mock current and newer Claude/Codex plugin versions and verify the owning marketplace/update commands and restart guidance. -3. Simulate an Antigravity reinstall failure and verify restoration of the prior staged plugin. -4. Mock a Pi package update and an OpenCode fallback refresh from the latest CLI bundle. -5. Run `--all` with one failed target and verify later targets still run, credentials remain untouched, and the final exit code is non-zero. +1. Mock npm reporting a newer, equal, and older-than-registry CLI version and verify check-only, confirmed update, no-downgrade behavior, declined update, and rollback guidance. +2. Mock current and newer Claude/Codex plugin versions, including alternate marketplace IDs, and verify the detected source commands and restart guidance. +3. Simulate an Antigravity reinstall failure and verify restoration of both the prior staged plugin and its import-manifest registration. +4. Mock canonical and non-canonical Pi sources plus an OpenCode fallback refresh from the latest CLI bundle. +5. Run `--all` with coexisting native/fallback installations and one failed target; verify every installation is represented, later targets still run, credentials remain untouched, and the final exit code is non-zero. 6. Prepare a patch version in a fixture repository and verify all version-bearing files become equal while no publish/tag/push command executes. 7. Introduce version drift in each controlled file and verify the release check identifies the exact mismatch. diff --git a/openspec/changes/add-update-flow/specs/update-flow/spec.md b/openspec/changes/add-update-flow/specs/update-flow/spec.md index 3292c98..91b2b0c 100644 --- a/openspec/changes/add-update-flow/specs/update-flow/spec.md +++ b/openspec/changes/add-update-flow/specs/update-flow/spec.md @@ -37,11 +37,21 @@ The updater SHALL compare installed and latest versions without invoking any mut **And** `--json` returns the current version, latest version, status, and target identifier **And** exits successfully, including when the status is `update-available` +#### Scenario: Do not downgrade a CLI newer than the registry + +**Given** the running CLI semantic version is greater than the registry `latest` +**When** the user runs `nsolid-plugin update` +**Then** the command reports `newer-than-registry` +**And** displays the current and registry versions +**And** does not invoke a package manager or modify the installation +**And** exits successfully +**And** does not provide an implicit downgrade path + #### Scenario: Check every detected target **Given** multiple N|Solid installations are detectable **When** the user runs `nsolid-plugin update --all --check` -**Then** every target is inspected without invoking install, update, uninstall, package-manager, tracking, or configuration mutations +**Then** every detected installation is inspected without invoking install, update, uninstall, package-manager, tracking, or configuration mutations **And** targets whose installed version cannot be determined report `unknown` **And** the command distinguishes `unknown` from `current` @@ -103,6 +113,8 @@ The default `nsolid-plugin update` scope SHALL update only a positively identifi **Then** the command does not guess a package manager or modify the installation **And** reports the latest version when it can be resolved **And** prints safe manual commands for npm, pnpm, and `npx -y nsolid-plugin@latest` +**And** the result status is `unsupported` +**And** a mutating update exits non-zero while a read-only check exits successfully ### Requirement: Harness-owned update strategies @@ -118,28 +130,37 @@ The updater SHALL preserve native/package ownership and delegate each supported **And** no OAuth browser or callback server starts **And** the result includes versions when discoverable, status, and restart guidance +#### Scenario: Preserve each detected native source identity + +**Given** Claude or Codex records `nsolid-plugin@` under a marketplace other than `nodesource` +**When** the corresponding native update strategy runs +**Then** Claude uses the detected complete plugin ID +**And** Codex upgrades the detected marketplace +**And** the strategy never substitutes `nodesource` +**And** an unqualified, malformed, or ambiguous ID returns `unsupported` without mutation + #### Scenario: Update Claude native plugin -**Given** `nsolid-plugin@nodesource` is installed natively in Claude +**Given** `nsolid-plugin@` is installed natively in Claude **And** the `claude` executable is available **When** the Claude update strategy runs -**Then** it invokes `claude plugin update nsolid-plugin@nodesource` with a fixed executable and argument array +**Then** it invokes `claude plugin update nsolid-plugin@` with a fixed executable and argument array **And** reports `/reload-plugins` or restart guidance **And** does not run the fallback installer #### Scenario: Update Codex native plugin -**Given** `nsolid-plugin@nodesource` is installed natively in Codex +**Given** `nsolid-plugin@` is installed natively in Codex **And** the `codex` executable is available **When** the Codex update strategy runs -**Then** it invokes `codex plugin marketplace upgrade nodesource` +**Then** it invokes `codex plugin marketplace upgrade ` **And** verifies the marketplace refresh succeeded **And** reports that a new Codex session is required **And** does not remove the installed plugin or configuration #### Scenario: Update Pi package-owned skills -**Given** `npm:nsolid-pi-plugin` is installed in Pi +**Given** the canonical `npm:nsolid-pi-plugin` source is installed in Pi **And** the `pi` executable is available **When** the Pi update strategy runs **Then** it invokes `pi update npm:nsolid-pi-plugin` @@ -147,6 +168,15 @@ The updater SHALL preserve native/package ownership and delegate each supported **And** reports `/reload` or restart guidance **And** leaves Pi MCP configuration and NodeSource credentials intact +#### Scenario: Reject a non-canonical Pi source + +**Given** Pi detects a local, Git, version-pinned, or ambiguous source for `nsolid-pi-plugin` +**When** the user runs a Pi update +**Then** the result status is `unsupported` +**And** no package source is substituted +**And** no Pi package or configuration is mutated +**And** the output provides manual guidance + #### Scenario: Update OpenCode or another fallback installation **Given** the target is OpenCode, which has no native plugin/package update owner, or another target uses the N|Solid fallback/direct installer @@ -157,6 +187,15 @@ The updater SHALL preserve native/package ownership and delegate each supported **And** preserves non-NodeSource artifacts and valid credentials **And** creates the normal configuration backup before config mutation +#### Scenario: Update coexisting native and fallback installations + +**Given** the same harness has both a detected native plugin and tracked fallback artifacts +**When** the user runs `nsolid-plugin update --harness ` or `nsolid-plugin update --all` +**Then** the plan contains one installation item for each ownership +**And** each item has a distinct installation identifier and source evidence +**And** native and fallback updates execute independently +**And** a native failure does not switch ownership or hide the fallback result + #### Scenario: Requested harness is not installed **Given** neither a native nor fallback N|Solid installation is detected for the requested harness @@ -186,7 +225,8 @@ The Antigravity strategy SHALL back up and validate the staged NodeSource plugin **Then** it creates a temporary backup of the existing staged NodeSource plugin **And** confirms replacement unless `--yes` was supplied **And** invokes the supported uninstall/install sequence for `https://github.com/NodeSource/nsolid-plugin.git` -**And** removes the backup only after the new staged plugin validates +**And** validates `plugin.json`, `bundle.json`, canonical skill presence, and the N|Solid entry in `~/.gemini/config/import_manifest.json` +**And** removes the backup only after the new staged plugin and registration validate **And** preserves `~/.agents/.nodesource-auth.json` #### Scenario: Antigravity reinstall fails @@ -194,6 +234,7 @@ The Antigravity strategy SHALL back up and validate the staged NodeSource plugin **Given** the previous Antigravity plugin was backed up **When** uninstall succeeds but reinstall or validation fails **Then** the updater restores the previous staged plugin atomically where supported +**And** restores the previous N|Solid import-manifest entry while preserving unrelated imports **And** reports whether rollback succeeded **And** exits non-zero **And** provides a manual reinstall command @@ -206,11 +247,11 @@ The updater SHALL plan targets before mutation, execute them sequentially in det **Given** one or more N|Solid CLI or harness installations are detected **When** the user runs `nsolid-plugin update --all` -**Then** the updater displays one ordered plan +**Then** the updater displays one ordered plan containing every detected installation **And** updates the CLI target first when supported -**And** updates detected harness targets sequentially in deterministic harness order -**And** records a result for every planned target -**And** prints counts for updated, current, skipped, not-installed, and failed +**And** updates detected installation targets sequentially in deterministic harness and ownership order +**And** records a result for every planned installation +**And** prints counts for every `UpdateStatus`, including `newer-than-registry`, `unsupported`, and `unknown` #### Scenario: One target fails during update-all @@ -238,7 +279,7 @@ Update results SHALL support human-readable and machine-readable output without **When** an update or check completes **Then** standard output contains exactly one valid JSON document **And** progress and diagnostics are written to standard error -**And** each result contains `target`, `ownership`, `status`, optional versions, `changed`, optional restart guidance, and sanitized errors +**And** each result contains `installationId`, `target`, `ownership`, `status`, optional `currentVersion` and `latestVersion`, `changed`, optional restart guidance and rollback status, and sanitized errors ### Requirement: Preserve existing installation behavior @@ -252,4 +293,5 @@ Update operations SHALL retain all existing setup, installation, authentication, **And** non-NodeSource skills and MCP entries remain unchanged **And** update never invokes setup, login, or OAuth **And** native strategy failure never silently switches to fallback ownership +**And** source identity is preserved for every supported native/package-owned update **And** all external commands run without a shell and with fixed argument arrays diff --git a/openspec/changes/add-update-flow/tasks.md b/openspec/changes/add-update-flow/tasks.md index 91b1e65..56a7d48 100644 --- a/openspec/changes/add-update-flow/tasks.md +++ b/openspec/changes/add-update-flow/tasks.md @@ -2,7 +2,7 @@ ## Task 1: Define update contracts and semantic-version behavior -- [ ] **Description**: Add the pure update target, ownership, status, plan, result, summary, command, and strategy types defined in the design. Implement strict stable semantic-version parsing/comparison without adding a runtime dependency. +- [ ] **Description**: Add the pure update target, ownership, source, installation, status, plan, result, summary, command, confirmation, context, and strategy types defined in the design. Include `newer-than-registry`, `unsupported`, `latestVersion`, `installationId`, and structured rollback status. Implement strict stable semantic-version parsing/comparison without adding a runtime dependency. - **Depends on**: None - **Files**: `packages/core/src/update/types.ts`, `packages/core/src/update/version.ts`, `packages/core/test/unit/update/version.test.ts` - **Testing**: Cover valid versions, invalid registry values, equal/newer/older comparisons, and deterministic result/count shapes. References: Update Flow “Report running versions” and “Check whether the CLI is current.” @@ -16,60 +16,60 @@ ## Task 3: Detect CLI installation ownership -- [ ] **Description**: Read the running package/bundle versions and detect npm or pnpm global ownership only from positive evidence. Return unsupported for workspace, local, `npx`, or ambiguous execution. +- [ ] **Description**: Read the running package/bundle versions and detect npm or pnpm global ownership only from positive evidence. Return `unsupported` for workspace, local, `npx`, or ambiguous execution, and preserve the detected source evidence on every installation record. - **Depends on**: Tasks 1–2 - **Files**: `packages/core/src/update/package-manager.ts`, `packages/core/src/update/inventory.ts`, `packages/core/test/unit/update/package-manager.test.ts`, `packages/core/test/unit/update/inventory.test.ts` - **Testing**: Use fixture paths/environments for npm, pnpm, `npx`, workspace, broken symlink, and ambiguous launchers. Verify unsupported sources produce guidance without mutation. References: Update Flow “Unsupported CLI installation source.” ## Task 4: Implement CLI package update strategy -- [ ] **Description**: Implement CLI check/update planning, confirmation metadata, npm/pnpm command generation, post-command reporting, and exact previous-version rollback guidance. +- [ ] **Description**: Implement CLI check/update planning, confirmation metadata, npm/pnpm command generation, post-command reporting, exact previous-version rollback guidance, and a no-mutation `newer-than-registry` outcome without implicit downgrade support. - **Depends on**: Tasks 1–3 - **Files**: `packages/core/src/update/strategies/cli-package.ts`, `packages/core/test/unit/update/cli-package.test.ts` - **Testing**: Cover current, update available, newer-than-registry, declined, `--yes`, unsupported source, failed package manager, and rollback command. References: all CLI-specific scenarios in Update Flow. ## Task 5: Extend harness inventory and version evidence -- [ ] **Description**: Reuse native detection and fallback tracking to classify Claude, Codex, OpenCode, Antigravity, and Pi ownership. Add optional installed version/staged root evidence and backward-compatible `bundleVersion` tracking for fallback installs. +- [ ] **Description**: Reuse native detection and fallback tracking to emit one installation record per native, fallback, or package-owned installation. Carry validated Claude/Codex plugin IDs and marketplaces, canonical Pi source evidence, optional installed version/staged root evidence, and backward-compatible `bundleVersion` tracking for fallback installs. Do not collapse native and fallback records for one harness. - **Depends on**: Tasks 1–3 - **Files**: `packages/core/src/update/inventory.ts`, `packages/core/src/harnesses/*.ts`, `packages/core/src/skills/skill-tracker.ts`, related harness/tracker tests -- **Testing**: Cover native, fallback, package-owned, missing, corrupt metadata, legacy tracking without `bundleVersion`, and version-unknown states for every harness. References: Update Flow “Requested harness is not installed,” “Check every detected target,” “Update one installed native harness,” and “Preserve credentials and user-owned configuration.” +- **Testing**: Cover native, fallback, package-owned, coexisting native+fallback, alternate marketplace IDs, canonical and unsupported Pi sources, missing/corrupt metadata, legacy tracking without `bundleVersion`, and version-unknown states for every harness. References: Update Flow “Requested harness is not installed,” “Check every detected target,” “Update one installed native harness,” “Preserve each detected native source identity,” and “Preserve credentials and user-owned configuration.” ## Task 6: Implement Claude and Codex native strategies - [ ] **Blocking verification**: Before implementing `codex.ts`, use a disposable real Codex installation to verify whether `codex plugin marketplace upgrade nodesource` refreshes the version and content of an already-installed plugin rather than only marketplace metadata. Record the tested versions and command/output evidence. If it does not refresh the installed copy, stop and amend the design, Update Flow specification, and this task to use the documented plugin remove/add lifecycle with configuration-preservation coverage. -- [ ] **Description**: Add strategies that generate and execute the fixed Claude plugin update and Codex marketplace upgrade commands, retain native ownership, and return restart/reload guidance. +- [ ] **Description**: Add strategies that generate and execute commands using the validated detected Claude plugin ID and Codex marketplace, retain native ownership, reject incomplete or ambiguous IDs, and return restart/reload guidance. - **Depends on**: Tasks 2 and 5 - **Files**: `packages/core/src/update/strategies/claude.ts`, `packages/core/src/update/strategies/codex.ts`, corresponding unit tests -- **Testing**: Mock successful refresh, already-current output, command failure, missing executable, alternate detected plugin IDs/marketplace names where supported, and verify no fallback/auth call. References: Update Flow “Update Claude native plugin” and “Update Codex native plugin.” +- **Testing**: Mock successful refresh, already-current and newer-than-registry output, command failure, missing executable, alternate detected plugin IDs/marketplace names, malformed/ambiguous IDs, and verify no fallback/auth call. Keep implementation blocked until the real Codex marketplace-refresh spike has evidence. References: Update Flow “Preserve each detected native source identity,” “Update Claude native plugin,” and “Update Codex native plugin.” ## Task 7: Implement Pi and fallback/OpenCode strategies -- [ ] **Description**: Add the package-owned Pi update strategy and latest-published-CLI fallback refresh strategy. Reuse existing idempotent installation, backup, merge, and tracking code rather than duplicating it. +- [ ] **Description**: Add the package-owned Pi update strategy only for the canonical npm source and return `unsupported` for local, Git, pinned, or ambiguous Pi sources. Add the latest-published-CLI fallback refresh strategy, reusing existing idempotent installation, backup, merge, and tracking code rather than duplicating it. - **Depends on**: Tasks 2 and 5 - **Files**: `packages/core/src/update/strategies/pi.ts`, `packages/core/src/update/strategies/fallback.ts`, corresponding unit/integration tests -- **Testing**: Verify the exact Pi source, package-owned skill boundaries, OpenCode skill refresh, fallback MCP merge, configuration backup, preserved credentials, and no implicit install for an absent target. References: Update Flow “Update Pi package-owned skills,” “Update OpenCode or a fallback installation,” and “Requested harness is not installed.” +- **Testing**: Verify the exact canonical Pi source, rejection of non-canonical sources, package-owned skill boundaries, OpenCode skill refresh, fallback MCP merge, configuration backup, preserved credentials, and no implicit install for an absent target. References: Update Flow “Update Pi package-owned skills,” “Reject a non-canonical Pi source,” “Update OpenCode or a fallback installation,” and “Requested harness is not installed.” ## Task 8: Implement transactional Antigravity update -- [ ] **Description**: Add known-path staging detection, restrictive temporary backup, confirmed uninstall/install, new-root validation, successful cleanup, and rollback restoration. +- [ ] **Description**: Add known-path staging detection, restrictive temporary backup of the staged root and N|Solid import-manifest entry, confirmed uninstall/install, new-root plus registration validation, successful cleanup, and rollback restoration that preserves unrelated imports. - **Depends on**: Tasks 2 and 5 - **Files**: `packages/core/src/update/antigravity-transaction.ts`, `packages/core/src/update/strategies/antigravity.ts`, related unit/integration tests -- **Testing**: Cover both supported staged roots, successful replacement, declined confirmation, uninstall failure, install failure, validation failure, rollback success/failure, cleanup, and credential preservation. References: Update Flow “Update Antigravity native plugin” and “Antigravity reinstall fails.” +- **Testing**: Cover both supported staged roots, successful replacement, declined confirmation, uninstall failure, install failure, root/manifest validation failure, rollback success/failure for both components, cleanup, unrelated import preservation, and credential preservation. References: Update Flow “Update Antigravity native plugin” and “Antigravity reinstall fails.” ## Task 9: Build the coordinator and programmatic API -- [ ] **Description**: Implement scope validation, deterministic target ordering, check-only short circuit, plan confirmation, sequential execution, per-target failure isolation, aggregation, and public `getVersionInfo()`, `checkUpdates()`, and `update()` exports. +- [ ] **Description**: Implement scope validation, one-plan-item-per-installation semantics, deterministic target/ownership ordering, check-only short circuit, plan confirmation, sequential execution, per-installation failure isolation, aggregation, and public `getVersionInfo()`, `checkUpdates()`, and `update()` exports. - **Depends on**: Tasks 4 and 6–8 - **Files**: `packages/core/src/update/coordinator.ts`, `packages/core/src/update/index.ts`, `packages/core/src/index.ts`, coordinator/API tests -- **Testing**: Cover CLI-only default, one harness, `--all`, `--all` plus harness rejection, check-only no-execute guarantee, one failure with later success, empty inventory, status counts, and overall success/exit semantics. References: Update Flow “Update every detected target,” “One target fails during update-all,” “Check every detected target,” and “Conflicting update scopes.” +- **Testing**: Cover CLI-only default, one harness selecting multiple installations, `--all`, `--all` plus harness rejection, check-only no-execute guarantee, newer-than-registry no-downgrade, unsupported check/update exit semantics, one failure with later success, coexisting native/fallback records, empty inventory, all status counts, and overall success/exit semantics. References: Update Flow “Update every detected target,” “Update coexisting native and fallback installations,” “Do not downgrade a CLI newer than the registry,” “One target fails during update-all,” “Check every detected target,” and “Conflicting update scopes.” ## Task 10: Add CLI commands and output formatting -- [ ] **Description**: Add `version` and `update` command parsing, bare `--version`, `--check`/`--all`, confirmation integration, human-readable summaries, JSON-only stdout, stderr progress, help text, and exit-code mapping. +- [ ] **Description**: Add `version` and `update` command parsing, bare `--version`, `--check`/`--all`, confirmation integration, human-readable summaries, JSON-only stdout, stderr progress, help text, installation identifiers/source-safe display, and exit-code mapping. - **Depends on**: Task 9 - **Files**: `packages/core/src/cli.ts`, `packages/core/src/utils/format.ts` or a new update formatter, CLI help/unit/integration tests -- **Testing**: Spawn the CLI with every supported scope/output combination. Assert exact JSON parseability, no ANSI in JSON, no mutation in check mode, prompts only when appropriate, sanitized errors, bare `--version` parity, and exit zero when a successful check reports `update-available`. References: Update Flow “Structured update output,” “Report versions with the conventional flag,” and “Non-interactive CLI update.” +- **Testing**: Spawn the CLI with every supported scope/output combination. Assert exact JSON parseability, `latestVersion` and `installationId` presence, no ANSI in JSON, no mutation in check mode, prompts only when appropriate, sanitized errors, bare `--version` parity, no downgrade for `newer-than-registry`, informational exit zero for successful checks, and non-zero mutable exits for unsupported/failure results. References: Update Flow “Structured update output,” “Report versions with the conventional flag,” “Do not downgrade a CLI newer than the registry,” and “Non-interactive CLI update.” ## Task 11: Add atomic release preparation @@ -87,10 +87,10 @@ ## Task 13: Add end-to-end update regression coverage -- [ ] **Description**: Exercise the public CLI against isolated homes and fake harness executables/registries, including mixed native/fallback ownership and partial failure. +- [ ] **Description**: Exercise the public CLI against isolated homes and fake harness executables/registries, including mixed native/fallback ownership, alternate source identities, unsupported Pi sources, Antigravity manifest rollback, and partial failure. - **Depends on**: Tasks 9–12 - **Files**: `packages/core/test/integration/update-flow.test.ts`, test fixtures/helpers, `scripts/test-marketplace-install.js` where update assertions fit -- **Testing**: Cover all acceptance tests from the proposal on Linux-compatible fixtures and keep paths/commands portable for macOS and Windows CI. Assert credentials and non-NodeSource configurations are byte-for-byte preserved. +- **Testing**: Cover all acceptance tests from the proposal on Linux-compatible fixtures and keep paths/commands portable for macOS and Windows CI. Assert credentials, non-NodeSource configurations, unrelated Antigravity manifest imports, and source identities are preserved byte-for-byte where applicable. ## Task 14: Document user and maintainer workflows From abb2132334ec657e472c4be7989e1e47733e411c Mon Sep 17 00:00:00 2001 From: Cesar-M-Diaz Date: Mon, 3 Aug 2026 14:21:18 +0200 Subject: [PATCH 3/4] docs: finalize update flow specification --- openspec/changes/add-update-flow/design.md | 249 +++++++++++++++--- openspec/changes/add-update-flow/proposal.md | 47 ++-- .../add-update-flow/specs/update-flow/spec.md | 169 +++++++++--- openspec/changes/add-update-flow/tasks.md | 55 ++-- 4 files changed, 401 insertions(+), 119 deletions(-) diff --git a/openspec/changes/add-update-flow/design.md b/openspec/changes/add-update-flow/design.md index a810c84..8315c3f 100644 --- a/openspec/changes/add-update-flow/design.md +++ b/openspec/changes/add-update-flow/design.md @@ -2,7 +2,7 @@ ## Architecture -The update feature is additive to the existing CLI and installer architecture. It does not move installation ownership into the shared CLI: each native harness remains responsible for its own staged plugin, Pi remains package-owned, and OpenCode/fallback installs continue through the existing installer. +The update feature is additive to the existing CLI and installer architecture. It does not move installation ownership into the shared CLI: each native harness remains responsible for its own staged plugin, Pi remains package-owned, and initial OpenCode/fallback installs continue through the existing public installer while update-only reconciliation uses the package-internal refresh entrypoint. The CLI adds an update coordinator that separates four concerns: @@ -42,6 +42,7 @@ Release preparation is a separate maintainer-side script. Runtime update code re `packages/core/src/update/coordinator.ts` - Resolves requested scope (`cli`, one harness, or all detected targets). +- Produces one synthetic `ownership: 'none'` item only when an explicitly requested harness has no detected installation. - Produces the plan before mutation. - Applies confirmation rules. - Executes targets sequentially and isolates per-target failures. @@ -51,21 +52,23 @@ Release preparation is a separate maintainer-side script. Runtime update code re - Reuses harness adapters and tracking readers to return one installation record per detected native, fallback, or package-owned installation; native and fallback records for the same harness are not collapsed. - Reads the running CLI/package metadata. -- Carries validated source identity (plugin ID/marketplace, package source, or fallback provenance) into each plan item without changing existing installation detection contracts. +- Carries validated source identity (Claude plugin ID/marketplace/scope/version source, Codex plugin ID/marketplace/version source, Antigravity layout, effective Pi source/scopes, or fallback provenance/executor) into each plan item without changing existing installation detection contracts. - Treats local, pinned, ambiguous, or otherwise unsupported update sources as `unsupported` instead of substituting a different source. `packages/core/src/update/version-source.ts` - Reads and validates `latest` metadata from npm for `nsolid-plugin` and `nsolid-pi-plugin`. -- Reads the GitHub-root `bundle.json` once for native Git targets. +- Resolves Claude/Codex latest-version evidence only from the exact carried marketplace source: a validated Git repository/ref plus relative manifest path, or the detected local marketplace snapshot. It never substitutes the canonical NodeSource GitHub root for an alternate marketplace. +- Reads the canonical GitHub-root `bundle.json` only for fixed-source native Git targets such as Antigravity. - Applies bounded request timeouts and semantic-version validation. -- Returns `unknown` rather than treating missing version evidence as current. +- Returns `unknown` rather than treating missing, local-stale, ambiguous, or unsupported marketplace version evidence as current; native execution may still use the preserved harness-owned ID when its identity is unambiguous. `packages/core/src/update/package-manager.ts` -- Detects npm or pnpm only from positive installation-path/package-manager evidence. +- Detects npm or pnpm only when the real CLI package/entrypoint is contained by that manager's reported global root and the corresponding executable is available; a shim or package-manager environment variable alone is not sufficient evidence. - Produces a fixed executable plus argument array. -- Returns unsupported for workspaces, `npx`, local checkouts, and ambiguous launchers. +- Pins update and rollback package specs to the exact semantic versions resolved during planning. +- Returns unsupported for workspaces, `npx`, local checkouts, Volta/Yarn/Bun ownership, mismatched global roots, and ambiguous launchers. `packages/core/src/update/command-runner.ts` @@ -80,13 +83,30 @@ Release preparation is a separate maintainer-side script. Runtime update code re - Strategies receive an immutable plan item and execution context. - Strategies cannot broaden scope or switch from native to fallback ownership after failure. +`packages/core/src/update/codex-transaction.ts` + +- Refreshes only the detected Git marketplace snapshot before touching the installed plugin; marketplace refresh is not treated as an installed-plugin update. +- Snapshots the exact `nsolid-plugin@` registration, prior enabled state, user-owned plugin fields, and cached installed payload before removal. +- Runs `codex plugin remove ` followed by `codex plugin add ` with fixed argument arrays. +- Validates that the reinstalled local version/content matches the refreshed marketplace entry, then reapplies the prior enabled state and preserves unrelated Codex configuration. +- Restores the saved registration and cached payload if removal succeeds but add or validation fails. + `packages/core/src/update/antigravity-transaction.ts` -- Resolves only known NodeSource staged plugin paths. -- Creates a temporary backup before replacement containing the staged root and the N|Solid entry in `~/.gemini/config/import_manifest.json`. +- Resolves only the two documented global NodeSource layout pairs: shared Antigravity under `~/.gemini/config/` and AGY CLI under `~/.gemini/antigravity-cli/`. +- Creates a temporary backup before replacement containing the detected staged root and the N|Solid entry in that root's matching `import_manifest.json`. - Validates the newly staged root by checking `plugin.json`, `bundle.json`, canonical skill presence, and source registration in the import manifest. - Restores both the staged root and the saved manifest entry if reinstall or validation fails, preserving unrelated manifest imports. +`packages/core/src/update/fallback-transaction.ts` + +- Resolves one available package executor (`npm exec` preferred, otherwise `pnpm dlx`), pins the child package to the exact validated `nsolid-plugin@` selected during planning, and runs it from a restrictive temporary working directory so workspace binaries/configuration cannot shadow the payload. +- Invokes the exact package's dedicated internal `nsolid-plugin-refresh-owned` binary for only the planned harness; the regular `nsolid-plugin install` command and programmatic `install()` contract are not changed. +- The exact child snapshots the target's tracked NodeSource-owned skill directories, affected MCP configuration, and complete tracking state before mutation, using its own bundled payload for ownership/collision preflight. +- Reconciles the installed asset set against the new bundle: complete skill directories are replaced, previously tracked skills absent from the new bundle are removed, and untracked/user-owned paths and unrelated MCP entries are preserved. +- Updates `bundleVersion` only after the new skills, MCP entries, and tracking data validate; restores the snapshot if execution, reconciliation, or validation fails. +- Treats direct artifacts without sufficient tracking ownership as `unsupported` rather than deleting paths by name or prefix. + ### Existing modules extended `packages/core/src/cli.ts` @@ -96,9 +116,15 @@ Release preparation is a separate maintainer-side script. Runtime update code re - Rejects `--all` with `--harness` before calling the coordinator. - Keeps JSON on stdout and progress/diagnostics on stderr. +`packages/core/src/update/refresh-owned-cli.ts` + +- Implements the package-internal `nsolid-plugin-refresh-owned` binary used by an older updater to execute the exact published bundle's fallback transaction. +- Is not listed as a public user workflow and refuses absent, ambiguous, or untracked ownership; it never authenticates or broadens the requested harness. +- Leaves the existing `nsolid-plugin install` command and public `install()` behavior unchanged. + `packages/core/src/index.ts` -- Exports programmatic `getVersionInfo()`, `checkUpdates()`, and `update()` functions and their public types. +- Exports synchronous, read-only `getVersionInfo(): RunningVersionInfo` plus asynchronous `checkUpdates()` and `update()` functions and their public types. - Existing setup/install/uninstall APIs remain unchanged. `packages/core/src/harnesses/` @@ -108,7 +134,7 @@ Release preparation is a separate maintainer-side script. Runtime update code re `packages/core/src/skills/skill-tracker.ts` -- Fallback tracking may add an optional `bundleVersion` for future checks. +- Fallback tracking adds an optional `bundleVersion` and retains enough per-harness ownership/path evidence to reconcile obsolete assets safely. - Readers must accept existing tracking files that omit it. ### Release modules @@ -157,6 +183,7 @@ export type UpdateOwnership = | 'native-plugin' | 'package-owned' | 'fallback' + | 'none' export type VersionStatus = | 'current' @@ -181,13 +208,77 @@ export interface VersionInfo { status: VersionStatus } +export interface RunningVersionInfo { + cliVersion: string + bundleVersion: string +} + +export type ClaudePluginScope = 'user' | 'project' | 'local' | 'managed' + +export type MarketplaceVersionSource = + | { + kind: 'git' + repository: string + revision?: string + manifestPath: string + } + | { + kind: 'local-snapshot' + root: string + manifestPath: string + freshness: 'verified' | 'stale' | 'unknown' + } + | { + kind: 'unknown' + reason: 'missing-metadata' | 'ambiguous' | 'unsupported' + } + +export type PiPackageLocation = + | { scopes: readonly ['user'] } + | { scopes: readonly ['project']; projectRoot: string } + | { scopes: readonly ['user', 'project']; projectRoot: string } + +export type FallbackPackageExecutor = 'npm-exec' | 'pnpm-dlx' + +export type AntigravityLayout = + | { + kind: 'shared' + pluginRoot: '~/.gemini/config/plugins/nsolid-plugin' + manifestPath: '~/.gemini/config/import_manifest.json' + } + | { + kind: 'agy-cli' + pluginRoot: '~/.gemini/antigravity-cli/plugins/nsolid-plugin' + manifestPath: '~/.gemini/antigravity-cli/import_manifest.json' + } + export type UpdateSource = + | { kind: 'none' } | { kind: 'global-package'; packageManager: 'npm' | 'pnpm'; packageName: 'nsolid-plugin' } - | { kind: 'marketplace'; pluginId: string; marketplace: string } - | { kind: 'pi-package'; spec: 'npm:nsolid-pi-plugin' } - | { kind: 'unsupported'; source: string; reason: 'local' | 'git' | 'pinned' | 'ambiguous' } - | { kind: 'antigravity-git'; url: 'https://github.com/NodeSource/nsolid-plugin.git' } - | { kind: 'fallback'; bundleVersion?: string } + | { + kind: 'claude-marketplace' + pluginId: string + marketplace: string + scope: ClaudePluginScope + versionSource: MarketplaceVersionSource + } + | { + kind: 'codex-marketplace' + pluginId: string + marketplace: string + versionSource: MarketplaceVersionSource + } + | ({ + kind: 'pi-package' + spec: 'npm:nsolid-pi-plugin' + } & PiPackageLocation) + | { + kind: 'unsupported' + source: string + reason: 'local' | 'git' | 'pinned' | 'ambiguous' | 'conflicting' | 'untracked' | 'unsupported-manager' + } + | { kind: 'antigravity-git'; url: 'https://github.com/NodeSource/nsolid-plugin.git'; layout: AntigravityLayout } + | { kind: 'fallback'; bundleVersion?: string; executor?: FallbackPackageExecutor } export interface UpdateInstallation { installationId: string @@ -210,6 +301,29 @@ export interface UpdateOptions { confirm?: UpdateConfirmation } +export type UpdatePlanStep = + | { + kind: 'command' + description: string + command: CommandSpec + } + | { + kind: 'filesystem' + description: string + operation: 'backup' | 'replace' | 'reconcile' | 'restore' | 'cleanup' + paths: readonly string[] + } + | { + kind: 'validation' + description: string + checks: readonly string[] + } + +export interface UpdateError { + code: string + message: string +} + export interface UpdatePlanItem { installationId: string target: UpdateTarget @@ -217,8 +331,9 @@ export interface UpdatePlanItem { installed: boolean source: UpdateSource version: VersionInfo - executable?: string - args?: readonly string[] + steps: readonly UpdatePlanStep[] + rollbackSteps: readonly UpdatePlanStep[] + planningError?: UpdateError requiresConfirmation: boolean restartHint?: string } @@ -246,10 +361,7 @@ export interface UpdateResult { attempted: boolean succeeded?: boolean } - error?: { - code: string - message: string - } + error?: UpdateError } export interface UpdateSummary { @@ -263,6 +375,7 @@ export interface CommandSpec { executable: string args: readonly string[] cwd?: string + env?: Readonly> timeoutMs: number } @@ -294,12 +407,16 @@ export interface UpdateStrategy { Rules enforced by these contracts: - `check` stops after planning/version resolution and never calls `execute`. -- Command arguments are arrays; a shell command string is not part of the contract. +- Every command, filesystem mutation, validation, and rollback action is represented as an ordered plan step before confirmation; strategies cannot introduce an undisclosed external command during execution. +- A lookup or validation failure produces a plan item with sanitized `planningError`, empty execute/rollback steps, and `requiresConfirmation: false`. The coordinator converts it to a `failed` result without calling `execute`, while independent items remain executable. +- Command arguments are arrays; a shell command string is not part of the contract. The formatter redacts sensitive environment values and source credentials when displaying a plan. - `error.message` is sanitized and suitable for JSON output. - An absent version is represented as `unknown`, never coerced to `current`. - A detected installation source that cannot be updated safely is represented as `unsupported`, never replaced with a different source. +- `ownership: 'none'` with `source.kind: 'none'` is reserved for the synthetic, non-mutating plan/result produced when an explicitly requested harness has no detected installation. It has empty execute/rollback steps, requires no confirmation, and is never emitted as a detected target under `--all`. +- Marketplace version resolution uses only the `versionSource` carried by the detected Claude or Codex registration. An unknown or stale local source yields `unknown`; it never falls back to the NodeSource marketplace. - Strategies return data; the CLI formatter owns human-readable output. -- A completed check whose result is `update-available`, `newer-than-registry`, or `unsupported` is informational and exits zero; lookup, validation, or execution failures remain non-zero. +- A completed check whose result is `current`, `update-available`, `newer-than-registry`, `unsupported`, or evidence-only `unknown` is informational and exits zero. A timeout, invalid response, or other operational lookup/validation failure is `failed` and remains non-zero. - A mutating update with `newer-than-registry` performs no downgrade and exits zero; a mutating `unsupported` result exits non-zero with manual guidance. - A declined plan produces `skipped` results and exits zero. @@ -307,19 +424,37 @@ Rules enforced by these contracts: | Target | Native/package action | Success guidance | |---|---|---| -| CLI npm | `npm install -g nsolid-plugin@` | invoke CLI again | -| CLI pnpm | `pnpm add -g nsolid-plugin@` | invoke CLI again | -| Claude | `claude plugin update ` | `/reload-plugins` or restart | -| Codex | `codex plugin marketplace upgrade ` | start a new session | -| Antigravity | `agy plugin uninstall nsolid-plugin`, then install Git URL | restart AGY | -| Pi | `pi update npm:nsolid-pi-plugin` for the canonical npm source only | `/reload` or restart | -| Fallback/OpenCode | latest published CLI executes `install --harness ` | restart harness if needed | - -Marketplace IDs and package sources are passed as separate arguments only after strict validation. A native ID is accepted only when it matches `nsolid-plugin@[A-Za-z0-9][A-Za-z0-9._-]*`; the base name alone, malformed IDs, control characters, whitespace, and ambiguous matches return `unsupported`. The only supported Pi source is the exact `npm:nsolid-pi-plugin`; local, Git, pinned, or ambiguous Pi sources return `unsupported`. No user-derived string is interpolated into an executable shell command. +| CLI npm | `npm install --global nsolid-plugin@` | invoke CLI again | +| CLI pnpm | `pnpm add --global nsolid-plugin@` | invoke CLI again | +| Claude | `claude plugin update --scope ` | `/reload-plugins` or restart | +| Codex | `codex plugin marketplace upgrade `, then `codex plugin remove ` and `codex plugin add ` | start a new session | +| Antigravity | `agy plugin uninstall nsolid-plugin`, then `agy plugin install https://github.com/NodeSource/nsolid-plugin.git` | restart AGY | +| Pi user-only | `pi update npm:nsolid-pi-plugin --no-approve` | `/reload` or restart | +| Pi with detected project scope | `pi update npm:nsolid-pi-plugin --approve` after the project root is disclosed and approved | `/reload` or restart | +| Fallback/OpenCode through npm | `npm exec --yes --package=nsolid-plugin@ -- nsolid-plugin-refresh-owned --harness ` inside a fallback transaction | restart harness if needed | +| Fallback/OpenCode through pnpm | `pnpm --package=nsolid-plugin@ dlx nsolid-plugin-refresh-owned --harness ` inside a fallback transaction | restart harness if needed | + +Marketplace IDs, Claude scopes, package versions, Pi scopes, and package sources are passed as separate arguments only after strict validation. A native ID is accepted only when it matches `nsolid-plugin@[A-Za-z0-9][A-Za-z0-9._-]*`; the base name alone, malformed IDs, control characters, whitespace, ambiguous matches, and a Claude installation whose scope cannot be determined return `unsupported`. Marketplace inventory also carries the exact repository/ref and relative manifest path, or the exact local snapshot path and freshness evidence, used for version resolution. Repository credentials are stripped before data reaches plan or result output; traversal-capable manifest paths and ambiguous source metadata return `unknown` or `unsupported` without canonical-source substitution. The only supported Pi identity is the exact unpinned `npm:nsolid-pi-plugin`. Inventory coalesces canonical user/project entries into one Pi target because one `pi update ` invocation updates every matching identity; the discriminated location requires `projectRoot` whenever project scope is present. Any local, Git, pinned, conflicting, or ambiguous matching entry returns `unsupported` for the whole target rather than producing a misleading partial success. No user-derived string is interpolated into an executable shell command. The planner emits one item per `UpdateInstallation`. If a harness has both native and fallback artifacts, both items remain visible and are updated independently; a native failure never switches to fallback ownership. -The Codex command plan is provisional until Task 6 verifies it against a disposable real installation. Implementing the Codex strategy is blocked on evidence that `marketplace upgrade` refreshes the already-installed plugin, not only marketplace metadata. If it does not, the design and specification must be amended before implementation to use the documented plugin remove/add lifecycle and to cover configuration preservation. +The CLI registry lookup resolves the `latest` dist-tag once, validates it as a stable semantic version, and stores that exact version in the immutable plan. Execution never sends `@latest` back to a package manager. npm uses `install --global`; pnpm uses `add --global`. Success requires both a zero child exit and an on-disk package manifest at the positively identified global root whose name/version equal `nsolid-plugin` and the planned version. A failed or mismatched result returns the exact previous-version command for the same manager. + +Pi source detection reads both user and current-project settings, including object-form entries and filters. User and project entries for the same unpinned npm package become one command target. A user-only target passes `--no-approve` so an unrelated current directory cannot broaden the operation. A detected project target records and displays its project root and passes `--approve` only after the update plan is approved; this is a one-command trust decision and does not rewrite Pi trust/settings files. Pi's own updater preserves source entries and package filters. Because `pi update ` does not accept a target version, the registry version observed during planning is a minimum postcondition rather than an executable argument: the strategy reads and reports the actual package-cache version after Pi completes, accepts a newer valid version published during the run, and fails if any affected cache remains older than the planned version. + +OpenCode supports native skills and a separate npm/local plugin system, but the current N|Solid distribution is not registered as an OpenCode plugin. Its owner is therefore the tracked direct installer at `~/.config/opencode/skills/` plus the merged `mcp` entries in `opencode.json(c)`. The updater must not invoke `opencode plugin`. It invokes the exact published N|Solid CLI package's internal `nsolid-plugin-refresh-owned` binary as the payload provider and transaction executor. The existing public `install` flow keeps its idempotent copy/merge semantics and does not acquire stale-asset removal behavior. + +The Codex marketplace command refreshes only the configured Git marketplace snapshot. It is therefore a discovery prerequisite, not an installed-plugin update. The strategy must use the detected complete plugin ID for both `remove` and `add`, preserve the prior registration/enablement and cached payload transactionally, and validate the resulting local version against the refreshed marketplace entry. + +Command semantics were verified on 2026-08-03 against the official harness documentation: + +- Claude documents `claude plugin update --scope ` and version-keyed installed caches: . +- Codex documents marketplace upgrade as refreshing Git marketplace snapshots and exposes plugin install/remove as separate operations: and . +- Google documents AGY plugin management plus the update sequence as uninstalling the old plugin and installing the new source: and . +- npm and pnpm document exact-version global installation through `npm install --global @` and `pnpm add --global @`: and . +- Pi documents `pi update `, unpinned npm updates, separate user/project caches, project trust flags, and identity-based user/project deduplication: and . +- OpenCode documents global skills under `~/.config/opencode/skills/`; its npm/local plugin system is separate from those direct skill directories: and . +- npm and pnpm document exact package execution through `npm exec --package=@` and `pnpm dlx @`: and . ## Data Flow @@ -352,6 +487,7 @@ sequenceDiagram participant CLI participant Coordinator participant Strategy + participant FS participant ExternalCLI User->>CLI: update [scope] @@ -361,8 +497,14 @@ sequenceDiagram User-->>CLI: confirm or --yes loop each installation, sequentially Coordinator->>Strategy: execute(planItem) - Strategy->>ExternalCLI: spawn executable + fixed args - ExternalCLI-->>Strategy: exit/status/output + loop each approved plan step, in order + alt filesystem or validation step + Strategy->>FS: declared operation and paths/checks + else command step + Strategy->>ExternalCLI: declared executable + fixed args + ExternalCLI-->>Strategy: exit/status/output + end + end Strategy-->>Coordinator: sanitized UpdateResult end Coordinator-->>CLI: aggregate summary @@ -371,6 +513,27 @@ sequenceDiagram CLI self-update is planned first, but the running process does not dynamically import the newly installed package. Remaining already-planned harness strategies execute from the current process. The user must invoke the CLI again to use new CLI code. +### Codex replacement transaction + +```mermaid +sequenceDiagram + participant Updater + participant FS + participant Codex + + Updater->>Codex: marketplace upgrade detected marketplace + Codex-->>Updater: refreshed snapshot or failure + Updater->>FS: snapshot plugin registration, enablement, and cached payload + Updater->>Codex: plugin remove detected plugin ID + Updater->>Codex: plugin add detected plugin ID + alt add and local-version validation succeed + Updater->>FS: restore prior enablement/user-owned fields and remove backup + else remove, add, or validation fails + Updater->>FS: restore prior registration and cached payload + Updater-->>Updater: return failed + rollback status + end +``` + ### Antigravity replacement transaction ```mermaid @@ -379,8 +542,8 @@ sequenceDiagram participant FS participant AGY - Updater->>FS: locate known staged N|Solid root - Updater->>FS: snapshot staged root and N|Solid import entry + Updater->>FS: locate one supported staged-root/manifest pair + Updater->>FS: snapshot detected staged root and matching import entry Updater->>AGY: uninstall nsolid-plugin Updater->>AGY: install GitHub root alt install and validation succeed @@ -421,10 +584,14 @@ sequenceDiagram - Missing executables use a distinct error code from command failure. - Process output is bounded before being retained in results. - Existing logger redaction is applied to verbose diagnostics. -- `--all` catches errors at the installation boundary and continues with independent installation records, including native and fallback records for the same harness. +- `--all` catches version-lookup, planning, and execution errors at the installation boundary and continues with independent installation records, including native and fallback records for the same harness. - Confirmation is mandatory for mutable non-interactive operations unless `--yes` is present. -- Marketplace IDs are validated before becoming arguments; local, pinned, and ambiguous Pi sources are never silently replaced. -- Antigravity backup paths are created with restrictive permissions in an OS temporary directory and always cleaned after success. Rollback validates both the staged root and the saved import-manifest registration. +- Marketplace IDs and Claude scopes are validated before becoming arguments; local, pinned, conflicting, and ambiguous Pi sources are never silently replaced. +- CLI and fallback package execution uses the exact immutable version from the plan; mutable dist-tags are not passed during mutation, and a package-manager success without matching on-disk version evidence is a failure. +- Pi user-only updates pass `--no-approve`; `--approve` is used only when the immutable plan identifies and displays a project-scoped canonical package. Canonical entries across both scopes are updated once, while conflicting/pinned entries block automatic mutation. +- Codex removes the installed plugin only after marketplace refresh and backup succeed. Rollback validates the restored registration and cached payload while preserving unrelated `config.toml` entries. +- Antigravity accepts only one unambiguous documented layout pair. Backup paths are created with restrictive permissions in an OS temporary directory and always cleaned after success. Rollback validates both the staged root and its matching saved import-manifest registration. +- OpenCode/fallback replacement mutates only paths and MCP entries proven to be owned by tracking. The transaction restores overwritten and stale-removed skill directories, config, and tracking together after any failed child execution or validation. - Update does not invoke setup, login, or auth modules. - Release scripts snapshot only an explicit allowlist; rollback never performs broad Git or recursive workspace resets. @@ -436,7 +603,7 @@ This is an additive migration. 2. Add inventory and target strategies behind programmatic APIs. 3. Add CLI parsing/formatting and integration tests. 4. Add release preparation/check scripts and fixture tests. -5. Add optional fallback tracking version while preserving reads of legacy tracking files. +5. Extend fallback tracking with optional bundle version and path-level ownership, preserving reads of legacy tracking files, then add transactional direct-install reconciliation behind the package-internal update entrypoint without changing public install semantics. 6. Update README/package documentation. 7. Ship the feature in a new minor CLI release because it adds public commands; existing `1.0.x` install/setup behavior remains compatible. diff --git a/openspec/changes/add-update-flow/proposal.md b/openspec/changes/add-update-flow/proposal.md index 65a4dfd..01c28cc 100644 --- a/openspec/changes/add-update-flow/proposal.md +++ b/openspec/changes/add-update-flow/proposal.md @@ -21,12 +21,13 @@ For users: - add `nsolid-plugin version` (with bare `nsolid-plugin --version` as an alias) and `nsolid-plugin update`; - make plain `update` target the npm CLI, `--harness ` target every detected installation for one harness, and `--all` target the CLI plus every detected N|Solid installation; - add `--check` for a read-only status check and retain `--json`, `--yes`, `--verbose`, and `--no-color` behavior where applicable; +- update a positively identified npm- or pnpm-owned global CLI with the exact semantic version resolved during planning, then verify the installed package on disk instead of trusting only the package-manager exit code; - delegate native updates to the owning harness: - - Claude: refresh/update the detected `nsolid-plugin@` identity; - - Codex: upgrade the detected Git marketplace and refresh the installed version; - - Antigravity: safely reinstall the GitHub-root plugin with staged-root and import-manifest backup/rollback; - - Pi: update the canonical `npm:nsolid-pi-plugin` source, while rejecting local, pinned, Git, or ambiguous sources; - - OpenCode, which has no native package owner, and other fallback installations: reinstall from the latest published CLI bundle; + - Claude: update the detected `nsolid-plugin@` identity at its detected installation scope and resolve version evidence only from that marketplace's carried source metadata; + - Codex: refresh the detected Git marketplace snapshot, then transactionally remove and add the same detected plugin identity because marketplace refresh does not update the installed copy; version checks never substitute a canonical marketplace for the detected source; + - Antigravity: safely reinstall the GitHub-root plugin with backup/rollback of the detected AGY or shared Antigravity staged-root/import-manifest pair; + - Pi: update the canonical unpinned `npm:nsolid-pi-plugin` identity once across its detected user/project scopes, while rejecting local, pinned, Git, conflicting, or ambiguous sources; + - OpenCode, whose N|Solid installation is direct rather than an OpenCode plugin, and other tracked fallback installations: invoke an internal exact-package refresh binary to transactionally reconcile tracked assets, including removal of obsolete NodeSource-owned assets, without changing public `install` semantics; - preserve credentials and non-NodeSource configuration throughout updates; - isolate failures during `--all` so one harness failure does not prevent remaining updates, while returning a failing exit status and a per-target summary. @@ -38,10 +39,13 @@ For maintainers: ## Rollback Plan -- The CLI update path records the previously installed CLI version and prints the exact package-manager command needed to restore it. +- The CLI update path records the previously installed CLI version, pins both update and rollback commands to exact semantic versions, verifies the resulting global package root, and prints the exact package-manager command needed to restore it. - A CLI newer than the registry is reported and left unchanged; this proposal has no implicit downgrade path. -- Native harness updates rely on the harness owner’s cache where available. Antigravity creates a temporary backup of the staged NodeSource plugin and its import-manifest registration and restores both if reinstall fails. -- Fallback installers continue using their existing config backups and idempotent merge behavior. +- Claude delegates to its native in-place update command while preserving the detected plugin ID and installation scope. +- Codex snapshots the exact plugin registration, enablement, and cached payload before the documented marketplace-refresh plus remove/add sequence, and restores that snapshot if reinstall or validation fails. +- Antigravity creates a temporary backup of the detected staged NodeSource plugin and its matching import-manifest registration and restores both if reinstall fails. +- Pi delegates package replacement to `pi update` without rewriting its settings entries, package filters, MCP configuration, or credentials. +- OpenCode/fallback refresh snapshots every tracked NodeSource-owned skill path, affected MCP entries/config file, and tracking state before replacement; it restores them if exact-version execution, stale-asset reconciliation, or validation fails. - Update operations never delete shared NodeSource credentials. - The feature can be reverted by removing the new command/service modules and scripts; existing `setup`, `install`, `doctor`, `restore`, and `uninstall` contracts remain unchanged. - A bad release can be rolled back by republishing or reinstalling the prior known-good package/plugin version and restoring generated manifests from the corresponding Git tag. @@ -50,13 +54,16 @@ For maintainers: - `packages/core/src/cli.ts` — new commands and update flags. - `packages/core/src/index.ts` — public update/version API surface. -- `packages/core/src/update/` — update planning, version comparison, command execution, result contracts, and package-manager detection. +- `packages/core/src/update/` — update planning, version comparison, command execution, result contracts, package-manager detection, and the package-internal owned-asset refresh entrypoint. - `packages/core/src/harnesses/` — harness-owned update strategies and native/fallback installation detection. -- `~/.gemini/config/import_manifest.json` — Antigravity plugin registration included in the transactional backup/rollback contract. -- `packages/core/src/index.ts` (the `doctor` function) and formatting utilities — optional update availability in health/status output. +- `~/.pi/agent/settings.json`, a detected current-project `.pi/settings.json`, and their corresponding Pi package caches — source/scope evidence for package-owned updates; project access is disclosed/approved and settings remain unchanged. +- `~/.config/opencode/skills/`, `~/.config/opencode/opencode.jsonc`, and fallback tracking/backups — transactional direct-install reconciliation for OpenCode. +- `~/.codex/config.toml` and the detected Codex plugin cache — exact plugin registration/enablement and prior payload included in transactional reinstall rollback. +- `~/.gemini/config/{plugins,import_manifest.json}` and `~/.gemini/antigravity-cli/{plugins,import_manifest.json}` — supported Antigravity staged-root/registration pairs included in transactional backup/rollback. +- Update formatting utilities — plan, progress, summary, and sanitized machine-readable output for the new commands; existing `doctor` behavior remains unchanged. - `packages/core/test/unit/update/` — version, planning, detection, safety, and output tests. - `packages/core/test/integration/` — mocked CLI/harness update flows, partial failures, rollback, and exit codes. -- `bundle.json`, `packages/core/package.json`, `packages/pi-plugin/package.json` — coordinated release version. +- `bundle.json`, `packages/core/package.json`, `packages/pi-plugin/package.json` — coordinated release version and registration of the internal fallback-refresh binary. - `scripts/` and root `package.json` — release preparation and drift checks. - `.claude-plugin/marketplace.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, and `packages/core/bundle.json` — generated version-bearing outputs. - `README.md` and package READMEs — user and maintainer update instructions. @@ -67,6 +74,7 @@ For maintainers: - `nsolid-plugin version` and its bare `--version` alias report the running CLI and bundled plugin versions without network access; the command form also supports JSON output. - `nsolid-plugin update --check` performs no writes or subprocess mutations and clearly reports whether the CLI is current. - Each supported harness has a deterministic update strategy with actionable output when its CLI is unavailable, its installation type is unsupported, or its source identity cannot be safely reused. +- Claude and Codex version checks use only the source metadata carried by their detected marketplace; missing, stale, or unsupported evidence reports `unknown` instead of reading the NodeSource marketplace. - `nsolid-plugin update --all` updates all detected targets, preserves credentials and user-owned configuration, summarizes every target, and exits non-zero on any failure. - Native and fallback installations detected for the same harness are represented and updated as separate targets; one ownership never silently replaces the other. - Interactive destructive/replacement steps require confirmation; `--yes` enables non-interactive automation. @@ -77,10 +85,11 @@ For maintainers: Acceptance tests: -1. Mock npm reporting a newer, equal, and older-than-registry CLI version and verify check-only, confirmed update, no-downgrade behavior, declined update, and rollback guidance. -2. Mock current and newer Claude/Codex plugin versions, including alternate marketplace IDs, and verify the detected source commands and restart guidance. -3. Simulate an Antigravity reinstall failure and verify restoration of both the prior staged plugin and its import-manifest registration. -4. Mock canonical and non-canonical Pi sources plus an OpenCode fallback refresh from the latest CLI bundle. -5. Run `--all` with coexisting native/fallback installations and one failed target; verify every installation is represented, later targets still run, credentials remain untouched, and the final exit code is non-zero. -6. Prepare a patch version in a fixture repository and verify all version-bearing files become equal while no publish/tag/push command executes. -7. Introduce version drift in each controlled file and verify the release check identifies the exact mismatch. +1. Mock npm reporting a newer, equal, and older-than-registry CLI version and verify check-only, exact-version npm/pnpm commands, on-disk post-install validation, no-downgrade behavior, declined update, unsupported wrappers, and exact rollback guidance. +2. Mock current and newer Claude/Codex plugin versions, including alternate marketplace IDs, source repositories, stale local snapshots, and Claude installation scopes; verify exact-source version resolution, no canonical-source substitution, Claude’s scoped native update, and Codex’s marketplace-refresh plus transactional remove/add flow. +3. Simulate Codex and Antigravity reinstall failures and verify restoration of the prior plugin registration, enablement, cached/staged payload, and matching manifest state. +4. Mock user-only, project-only, and combined canonical Pi scopes plus pinned/conflicting sources; verify one scope-aware native update command and unchanged settings. +5. Refresh a tracked OpenCode installation through the internal exact-version npm and pnpm refresh binary; verify atomic skill replacement, stale tracked-skill removal, MCP merge, tracking update, rollback, and unchanged public `install` behavior without modifying untracked/user-owned artifacts. +6. Run `--all` with coexisting native/fallback installations and one failed target or version lookup; verify every installation is represented, later independent targets still run, credentials remain untouched, and the final exit code is non-zero. +7. Prepare a patch version in a fixture repository and verify all version-bearing files become equal while no publish/tag/push command executes. +8. Introduce version drift in each controlled file and verify the release check identifies the exact mismatch. diff --git a/openspec/changes/add-update-flow/specs/update-flow/spec.md b/openspec/changes/add-update-flow/specs/update-flow/spec.md index 91b2b0c..064013d 100644 --- a/openspec/changes/add-update-flow/specs/update-flow/spec.md +++ b/openspec/changes/add-update-flow/specs/update-flow/spec.md @@ -57,12 +57,14 @@ The updater SHALL compare installed and latest versions without invoking any mut #### Scenario: Registry lookup fails -**Given** npm is unreachable, times out, returns invalid data, or returns a non-semantic version +**Given** the required npm, fixed native-Git, or exact carried marketplace version source for one target is unreachable, times out, returns invalid data, or returns a non-semantic version **When** the user checks or performs an update -**Then** the command reports the registry failure without exposing response bodies containing credentials -**And** performs no update -**And** exits non-zero -**And** preserves the current installation +**Then** the command reports a sanitized lookup failure for the affected target without exposing response bodies, repository credentials, or credential paths +**And** represents the affected installation in the ordered plan with a sanitized planning error and no mutation steps +**And** performs no mutation for the affected target +**And** an `--all` invocation continues planning or executing remaining independent targets and records their results +**And** the overall invocation exits non-zero +**And** preserves every installation whose lookup failed ### Requirement: Safe CLI self-update @@ -75,10 +77,20 @@ The default `nsolid-plugin update` scope SHALL update only a positively identifi **When** the user runs `nsolid-plugin update` **Then** the command displays the current version, target version, package manager, and exact planned operation **And** asks for confirmation in an interactive terminal -**And** after confirmation invokes the detected package manager with a fixed argument array to install `nsolid-plugin@` -**And** verifies the child process succeeded +**And** freezes the resolved semantic version in the plan rather than passing the mutable `latest` tag during execution +**And** after confirmation invokes `npm install --global nsolid-plugin@` or `pnpm add --global nsolid-plugin@` with a fixed argument array +**And** verifies both that the child process succeeded and that the positively identified global package root contains `nsolid-plugin` at the resolved version **And** reports that a new shell or command invocation may be required -**And** prints the exact command for restoring the previous version +**And** prints the same package manager's exact command for restoring `nsolid-plugin@` + +#### Scenario: Package manager exits successfully without installing the planned CLI + +**Given** an exact CLI update was approved +**When** the package-manager process exits successfully but the identified global package root is missing, belongs to a different package, or reports a version other than the planned version +**Then** the update result is `failed` +**And** the command does not report the CLI as updated +**And** prints the exact previous-version restore command +**And** exits non-zero #### Scenario: User declines a CLI update @@ -108,11 +120,11 @@ The default `nsolid-plugin update` scope SHALL update only a positively identifi #### Scenario: Unsupported CLI installation source -**Given** the running CLI was launched from a workspace, local path, `npx`, or an installation source that cannot be safely identified +**Given** the running CLI was launched from a workspace, local path, `npx`, Volta, Yarn, Bun, or an installation source/global root that cannot be safely identified as npm or pnpm owned **When** the user runs `nsolid-plugin update` **Then** the command does not guess a package manager or modify the installation **And** reports the latest version when it can be resolved -**And** prints safe manual commands for npm, pnpm, and `npx -y nsolid-plugin@latest` +**And** prints safe exact-version manual commands for npm, pnpm, ephemeral execution, and the detected wrapper/source when known **And** the result status is `unsupported` **And** a mutating update exits non-zero while a read-only check exits successfully @@ -134,17 +146,21 @@ The updater SHALL preserve native/package ownership and delegate each supported **Given** Claude or Codex records `nsolid-plugin@` under a marketplace other than `nodesource` **When** the corresponding native update strategy runs -**Then** Claude uses the detected complete plugin ID -**And** Codex upgrades the detected marketplace +**Then** Claude uses the detected complete plugin ID and installation scope +**And** Codex refreshes the detected marketplace and reinstalls the detected complete plugin ID +**And** inventory carries that marketplace's exact repository/ref and relative manifest path, or its exact local snapshot path and freshness evidence, for version resolution +**And** latest-version lookup reads only that carried source +**And** missing, stale, ambiguous, traversal-capable, or unsupported version-source evidence reports `unknown` or `unsupported` without querying the NodeSource marketplace **And** the strategy never substitutes `nodesource` -**And** an unqualified, malformed, or ambiguous ID returns `unsupported` without mutation +**And** an unqualified, malformed, or ambiguous ID, or a Claude installation with unknown scope, returns `unsupported` without mutation #### Scenario: Update Claude native plugin -**Given** `nsolid-plugin@` is installed natively in Claude +**Given** `nsolid-plugin@` is installed natively in Claude at a detected `user`, `project`, `local`, or `managed` scope **And** the `claude` executable is available **When** the Claude update strategy runs -**Then** it invokes `claude plugin update nsolid-plugin@` with a fixed executable and argument array +**Then** it invokes `claude plugin update nsolid-plugin@ --scope ` with a fixed executable and argument array +**And** verifies the native update command succeeded **And** reports `/reload-plugins` or restart guidance **And** does not run the fallback installer @@ -155,37 +171,108 @@ The updater SHALL preserve native/package ownership and delegate each supported **When** the Codex update strategy runs **Then** it invokes `codex plugin marketplace upgrade ` **And** verifies the marketplace refresh succeeded +**And** treats that command only as a marketplace snapshot refresh, not as an installed-plugin update +**And** creates a restrictive temporary backup of the exact plugin registration, enabled state, user-owned plugin fields, and cached installed payload +**And** confirms replacement unless `--yes` was supplied +**And** invokes `codex plugin remove nsolid-plugin@` followed by `codex plugin add nsolid-plugin@` with fixed argument arrays +**And** verifies the resulting local version/content matches the refreshed marketplace entry +**And** reapplies the prior enabled state and preserves unrelated Codex configuration **And** reports that a new Codex session is required -**And** does not remove the installed plugin or configuration +**And** does not run the fallback installer + +#### Scenario: Codex reinstall fails + +**Given** the prior Codex plugin registration and cached payload were backed up +**When** removal succeeds but add or installed-version validation fails +**Then** the updater restores the prior plugin registration, enabled state, user-owned fields, and cached payload +**And** preserves unrelated `~/.codex/config.toml` entries +**And** reports whether rollback succeeded +**And** exits non-zero +**And** provides the exact detected plugin remove/add commands for manual recovery #### Scenario: Update Pi package-owned skills -**Given** the canonical `npm:nsolid-pi-plugin` source is installed in Pi +**Given** the exact unpinned `npm:nsolid-pi-plugin` source is installed in Pi user settings, current-project settings, or both **And** the `pi` executable is available **When** the Pi update strategy runs -**Then** it invokes `pi update npm:nsolid-pi-plugin` +**Then** inventory coalesces every canonical matching scope into one Pi update target +**And** the plan displays whether user and/or project package caches will be updated and displays the project root when applicable +**And** a user-only target invokes `pi update npm:nsolid-pi-plugin --no-approve` +**And** a target containing the detected project scope invokes `pi update npm:nsolid-pi-plugin --approve` only after the plan is approved +**And** verifies every affected package cache contains `nsolid-pi-plugin` at a valid version no older than the registry version observed during planning +**And** reports the actual installed version, accepting a newer version published while Pi's native unpinned update was running **And** does not copy Pi skills into user-level skill directories **And** reports `/reload` or restart guidance -**And** leaves Pi MCP configuration and NodeSource credentials intact +**And** leaves Pi source entries, object-form package filters, trust settings, MCP configuration, and NodeSource credentials intact + +#### Scenario: Same canonical Pi identity exists in both scopes + +**Given** exact unpinned `npm:nsolid-pi-plugin` entries exist in both user and current-project settings +**When** the Pi update strategy plans and executes the update +**Then** the plan contains one package-owned Pi target with both scopes +**And** invokes the Pi update command exactly once +**And** does not report one duplicate result per scope #### Scenario: Reject a non-canonical Pi source -**Given** Pi detects a local, Git, version-pinned, or ambiguous source for `nsolid-pi-plugin` +**Given** Pi detects a local, Git, version-pinned, conflicting, or ambiguous source/entry for `nsolid-pi-plugin` in any matching user or current-project scope **When** the user runs a Pi update **Then** the result status is `unsupported` **And** no package source is substituted +**And** a canonical entry in another scope is not partially updated while the conflicting entry remains effective **And** no Pi package or configuration is mutated **And** the output provides manual guidance #### Scenario: Update OpenCode or another fallback installation -**Given** the target is OpenCode, which has no native plugin/package update owner, or another target uses the N|Solid fallback/direct installer +**Given** N|Solid is tracked as a direct OpenCode installation, rather than as an OpenCode npm/local plugin, or another target uses the tracked N|Solid fallback installer **When** its update strategy runs -**Then** it resolves the latest published `nsolid-plugin` CLI bundle -**And** reruns the latest fallback installer only for that harness -**And** reuses existing idempotent skill and MCP merge behavior -**And** preserves non-NodeSource artifacts and valid credentials -**And** creates the normal configuration backup before config mutation +**Then** it resolves and freezes the exact stable `nsolid-plugin` registry version in the plan +**And** requires an available supported package executor +**And** snapshots the target's tracked NodeSource-owned skill directories, affected MCP configuration, and complete tracking state before replacement +**And** invokes either `npm exec --yes --package=nsolid-plugin@ -- nsolid-plugin-refresh-owned --harness ` or `pnpm --package=nsolid-plugin@ dlx nsolid-plugin-refresh-owned --harness ` with fixed argument arrays +**And** runs the package executor from a restrictive temporary working directory where a workspace-local `nsolid-plugin` binary cannot shadow the resolved payload +**And** the internal refresh binary refuses absent, ambiguous, or untracked ownership and does not broaden the planned harness +**And** does not invoke `opencode plugin` +**And** completely replaces tracked skill directories, removes previously tracked skills absent from the new bundle, and merges only the new bundle's NodeSource MCP entries +**And** preserves untracked/user-owned skill paths, unrelated MCP entries, other configuration, and valid credentials +**And** validates installed skills, MCP entries, tracking paths, and `bundleVersion` before deleting the backup + +#### Scenario: No supported exact-package executor is available + +**Given** a tracked direct/fallback installation is updateable but neither `npm exec` nor `pnpm dlx` is available +**When** its update strategy is planned +**Then** the target is `unsupported` +**And** no backup, child process, or filesystem mutation runs +**And** the output provides the exact planned package version and manual commands + +#### Scenario: Direct/fallback refresh cannot prove ownership + +**Given** N|Solid-like skills or MCP entries exist but sufficient per-harness tracking ownership is absent or ambiguous +**When** a direct/fallback update is planned +**Then** the target is `unsupported` +**And** no path is selected from an `ns-` prefix or name-only guess +**And** no package executor or filesystem mutation runs +**And** the output provides repair/reinstall guidance + +#### Scenario: New fallback bundle collides with an untracked destination + +**Given** the exact new bundle contains a skill whose target path already exists but is not owned by the target's fallback tracking +**When** the child installer performs its preflight +**Then** the refresh fails before overwriting that path +**And** the untracked path remains byte-for-byte unchanged +**And** the surrounding transaction restores any earlier mutation from the same refresh +**And** the output identifies the conflicting destination without exposing its contents + +#### Scenario: OpenCode or fallback refresh fails + +**Given** a tracked direct/fallback installation was backed up +**When** exact-package execution, skill reconciliation, MCP merge, tracking update, or post-install validation fails +**Then** the updater restores the prior tracked skill directories, affected configuration, and tracking state +**And** restores stale tracked assets removed during reconciliation +**And** preserves unrelated OpenCode/fallback artifacts +**And** reports whether rollback succeeded +**And** exits non-zero #### Scenario: Update coexisting native and fallback installations @@ -201,6 +288,7 @@ The updater SHALL preserve native/package ownership and delegate each supported **Given** neither a native nor fallback N|Solid installation is detected for the requested harness **When** the user runs `nsolid-plugin update --harness ` **Then** no install is performed implicitly +**And** the coordinator emits one non-mutating item with `ownership: none` and `source.kind: none` for the requested harness **And** the target result is `not-installed` **And** the command prints appropriate installation guidance @@ -219,22 +307,31 @@ The Antigravity strategy SHALL back up and validate the staged NodeSource plugin #### Scenario: Update Antigravity native plugin -**Given** the GitHub-root N|Solid plugin is staged by Antigravity +**Given** the GitHub-root N|Solid plugin is staged in exactly one supported layout **And** the `agy` executable is available **When** the Antigravity update strategy runs -**Then** it creates a temporary backup of the existing staged NodeSource plugin +**Then** the layout is either `~/.gemini/config/plugins/nsolid-plugin` with `~/.gemini/config/import_manifest.json` or `~/.gemini/antigravity-cli/plugins/nsolid-plugin` with `~/.gemini/antigravity-cli/import_manifest.json` +**And** it creates a temporary backup of the detected staged NodeSource plugin and matching N|Solid import-manifest entry **And** confirms replacement unless `--yes` was supplied -**And** invokes the supported uninstall/install sequence for `https://github.com/NodeSource/nsolid-plugin.git` -**And** validates `plugin.json`, `bundle.json`, canonical skill presence, and the N|Solid entry in `~/.gemini/config/import_manifest.json` +**And** invokes `agy plugin uninstall nsolid-plugin` followed by `agy plugin install https://github.com/NodeSource/nsolid-plugin.git` +**And** validates `plugin.json`, `bundle.json`, canonical skill presence, and the N|Solid entry in the detected matching import manifest **And** removes the backup only after the new staged plugin and registration validate **And** preserves `~/.agents/.nodesource-auth.json` +#### Scenario: Antigravity layout is ambiguous or unsupported + +**Given** both supported staged layouts are present, or the detected plugin root has no matching supported manifest location +**When** the Antigravity update strategy plans an update +**Then** the result status is `unsupported` +**And** no AGY command or filesystem mutation runs +**And** the output identifies the conflicting or unsupported paths + #### Scenario: Antigravity reinstall fails **Given** the previous Antigravity plugin was backed up **When** uninstall succeeds but reinstall or validation fails **Then** the updater restores the previous staged plugin atomically where supported -**And** restores the previous N|Solid import-manifest entry while preserving unrelated imports +**And** restores the previous N|Solid entry in the matching detected import manifest while preserving unrelated imports **And** reports whether rollback succeeded **And** exits non-zero **And** provides a manual reinstall command @@ -248,8 +345,10 @@ The updater SHALL plan targets before mutation, execute them sequentially in det **Given** one or more N|Solid CLI or harness installations are detected **When** the user runs `nsolid-plugin update --all` **Then** the updater displays one ordered plan containing every detected installation +**And** each plan item lists every ordered external command, filesystem mutation, validation, and rollback step before confirmation **And** updates the CLI target first when supported **And** updates detected installation targets sequentially in deterministic harness and ownership order +**And** execution introduces no external command absent from the approved plan **And** records a result for every planned installation **And** prints counts for every `UpdateStatus`, including `newer-than-registry`, `unsupported`, and `unknown` @@ -295,3 +394,11 @@ Update operations SHALL retain all existing setup, installation, authentication, **And** native strategy failure never silently switches to fallback ownership **And** source identity is preserved for every supported native/package-owned update **And** all external commands run without a shell and with fixed argument arrays + +#### Scenario: Preserve the public install contract + +**Given** a tracked fallback installation needs transactional stale-asset reconciliation during update +**When** the updater executes the exact published package +**Then** it uses the package-internal `nsolid-plugin-refresh-owned` entrypoint +**And** the public `nsolid-plugin install` command and programmatic `install()` API retain their existing copy, merge, tracking, collision, and repeat-install behavior +**And** invoking `install` outside an update does not remove stale tracked assets under the new update-only reconciliation rules diff --git a/openspec/changes/add-update-flow/tasks.md b/openspec/changes/add-update-flow/tasks.md index 56a7d48..7f2220a 100644 --- a/openspec/changes/add-update-flow/tasks.md +++ b/openspec/changes/add-update-flow/tasks.md @@ -2,74 +2,73 @@ ## Task 1: Define update contracts and semantic-version behavior -- [ ] **Description**: Add the pure update target, ownership, source, installation, status, plan, result, summary, command, confirmation, context, and strategy types defined in the design. Include `newer-than-registry`, `unsupported`, `latestVersion`, `installationId`, and structured rollback status. Implement strict stable semantic-version parsing/comparison without adding a runtime dependency. +- [ ] **Description**: Add the pure running-version, update target, ownership (including the explicit no-installation sentinel), marketplace-version-source, discriminated Pi location, installation, status, ordered execute/rollback plan-step, sanitized planning-error, result, summary, command, confirmation, context, and strategy types defined in the design. Include `cliVersion`, `bundleVersion`, `newer-than-registry`, `unsupported`, `latestVersion`, `installationId`, structured rollback status, and optional controlled command environment additions. Implement strict stable semantic-version parsing/comparison without adding a runtime dependency. - **Depends on**: None - **Files**: `packages/core/src/update/types.ts`, `packages/core/src/update/version.ts`, `packages/core/test/unit/update/version.test.ts` -- **Testing**: Cover valid versions, invalid registry values, equal/newer/older comparisons, and deterministic result/count shapes. References: Update Flow “Report running versions” and “Check whether the CLI is current.” +- **Testing**: Cover valid versions, invalid registry values, equal/newer/older comparisons, type-level Pi project-root requirements, multi-step execute/rollback plans, and deterministic result/count shapes. References: Update Flow “Report running versions,” “Check whether the CLI is current,” and “Update every detected target.” ## Task 2: Add safe command execution and version sources -- [ ] **Description**: Implement the injected shell-free command runner, bounded/sanitized output, executable lookup, npm registry client, and GitHub-root bundle version source with explicit timeouts and validation. +- [ ] **Description**: Implement the injected shell-free command runner, bounded/sanitized output, executable lookup, npm registry client, exact-version npm-exec/pnpm-dlx package executors, exact carried Claude/Codex marketplace version sources, and the fixed canonical GitHub-root Antigravity bundle source with explicit timeouts and validation. Never substitute the NodeSource marketplace for alternate, missing, stale, or unsupported marketplace evidence. - **Depends on**: Task 1 - **Files**: `packages/core/src/update/command-runner.ts`, `packages/core/src/update/version-source.ts`, `packages/core/test/unit/update/command-runner.test.ts`, `packages/core/test/unit/update/version-source.test.ts` -- **Testing**: Mock success, timeout, missing executable, invalid JSON/version, non-zero exit, oversized output, and secret-bearing diagnostics. Verify all subprocess calls use `shell: false` and argument arrays. References: Update Flow “Registry lookup fails,” “Required harness executable is missing,” and “Preserve credentials and user-owned configuration.” +- **Testing**: Mock success, timeout, missing executable, invalid JSON/version, non-zero exit, oversized output, alternate Git repository/ref/manifest sources, fresh and stale local snapshots, traversal-capable paths, and secret-bearing diagnostics. Verify all subprocess calls use `shell: false` and argument arrays, source credentials are redacted, and no canonical-source substitution occurs. References: Update Flow “Registry lookup fails,” “Preserve each detected native source identity,” “Required harness executable is missing,” and “Preserve credentials and user-owned configuration.” ## Task 3: Detect CLI installation ownership -- [ ] **Description**: Read the running package/bundle versions and detect npm or pnpm global ownership only from positive evidence. Return `unsupported` for workspace, local, `npx`, or ambiguous execution, and preserve the detected source evidence on every installation record. +- [ ] **Description**: Read the running package/bundle versions and detect npm or pnpm global ownership only when the real package/entrypoint is contained by that manager's reported global root. Return `unsupported` for workspace, local, `npx`, Volta, Yarn, Bun, mismatched-root, or ambiguous execution, and preserve the detected source evidence on every installation record. - **Depends on**: Tasks 1–2 - **Files**: `packages/core/src/update/package-manager.ts`, `packages/core/src/update/inventory.ts`, `packages/core/test/unit/update/package-manager.test.ts`, `packages/core/test/unit/update/inventory.test.ts` -- **Testing**: Use fixture paths/environments for npm, pnpm, `npx`, workspace, broken symlink, and ambiguous launchers. Verify unsupported sources produce guidance without mutation. References: Update Flow “Unsupported CLI installation source.” +- **Testing**: Use fixture paths/environments for npm, pnpm, `npx`, Volta, Yarn, Bun, workspace, broken symlink, manager-reported root mismatch, and ambiguous launchers. Verify unsupported sources produce exact-version guidance without mutation. References: Update Flow “Unsupported CLI installation source.” ## Task 4: Implement CLI package update strategy -- [ ] **Description**: Implement CLI check/update planning, confirmation metadata, npm/pnpm command generation, post-command reporting, exact previous-version rollback guidance, and a no-mutation `newer-than-registry` outcome without implicit downgrade support. +- [ ] **Description**: Implement CLI check/update planning that freezes the resolved semantic version, confirmation metadata, exact-version npm/pnpm command generation, on-disk global package/version verification, exact previous-version rollback guidance, and a no-mutation `newer-than-registry` outcome without implicit downgrade support. - **Depends on**: Tasks 1–3 - **Files**: `packages/core/src/update/strategies/cli-package.ts`, `packages/core/test/unit/update/cli-package.test.ts` -- **Testing**: Cover current, update available, newer-than-registry, declined, `--yes`, unsupported source, failed package manager, and rollback command. References: all CLI-specific scenarios in Update Flow. +- **Testing**: Cover current, update available, immutable planned version despite a changed dist-tag, newer-than-registry, declined, `--yes`, unsupported source, failed package manager, successful child with missing/wrong on-disk package version, and exact rollback command. References: all CLI-specific scenarios in Update Flow. ## Task 5: Extend harness inventory and version evidence -- [ ] **Description**: Reuse native detection and fallback tracking to emit one installation record per native, fallback, or package-owned installation. Carry validated Claude/Codex plugin IDs and marketplaces, canonical Pi source evidence, optional installed version/staged root evidence, and backward-compatible `bundleVersion` tracking for fallback installs. Do not collapse native and fallback records for one harness. +- [ ] **Description**: Reuse native detection and fallback tracking to emit one installation record per native, fallback, or package-owned installation. Carry validated Claude plugin ID/marketplace/scope and Codex plugin ID/marketplace together with each registration's exact sanitized repository/ref/relative-manifest source or local-snapshot/freshness evidence; carry the effective Pi source in a discriminated user/project location that requires the project root whenever project scope is present; and carry the detected Antigravity staged-root/matching-manifest layout. Require path-level ownership evidence for direct/fallback updates, retain optional installed-version evidence, and add backward-compatible `bundleVersion` tracking. Do not collapse native and fallback records for one harness. - **Depends on**: Tasks 1–3 - **Files**: `packages/core/src/update/inventory.ts`, `packages/core/src/harnesses/*.ts`, `packages/core/src/skills/skill-tracker.ts`, related harness/tracker tests -- **Testing**: Cover native, fallback, package-owned, coexisting native+fallback, alternate marketplace IDs, canonical and unsupported Pi sources, missing/corrupt metadata, legacy tracking without `bundleVersion`, and version-unknown states for every harness. References: Update Flow “Requested harness is not installed,” “Check every detected target,” “Update one installed native harness,” “Preserve each detected native source identity,” and “Preserve credentials and user-owned configuration.” +- **Testing**: Cover native, fallback, package-owned, coexisting native+fallback, alternate marketplace IDs and repositories, every Claude installation scope, unknown scope, missing/ambiguous/stale marketplace evidence without canonical fallback, Pi user-only/project-only/both scopes with required project roots, object-form filters, pinned/conflicting Pi entries, both Antigravity layout pairs, ambiguous layouts, missing/corrupt metadata, direct artifacts without ownership, legacy tracking without `bundleVersion`, and version-unknown states for every harness. References: Update Flow “Requested harness is not installed,” “Check every detected target,” “Update one installed native harness,” “Preserve each detected native source identity,” and “Preserve credentials and user-owned configuration.” ## Task 6: Implement Claude and Codex native strategies -- [ ] **Blocking verification**: Before implementing `codex.ts`, use a disposable real Codex installation to verify whether `codex plugin marketplace upgrade nodesource` refreshes the version and content of an already-installed plugin rather than only marketplace metadata. Record the tested versions and command/output evidence. If it does not refresh the installed copy, stop and amend the design, Update Flow specification, and this task to use the documented plugin remove/add lifecycle with configuration-preservation coverage. -- [ ] **Description**: Add strategies that generate and execute commands using the validated detected Claude plugin ID and Codex marketplace, retain native ownership, reject incomplete or ambiguous IDs, and return restart/reload guidance. +- [ ] **Description**: Add a Claude strategy that invokes the native update command with the validated detected plugin ID and installation scope. Add a transactional Codex strategy that refreshes the detected marketplace snapshot, snapshots the exact plugin registration/enablement/user-owned fields and cached payload, removes and adds the same detected plugin ID, validates local versus refreshed version/content, and restores the snapshot on failure. Retain native ownership, reject incomplete/ambiguous identities, and return restart/reload guidance. - **Depends on**: Tasks 2 and 5 -- **Files**: `packages/core/src/update/strategies/claude.ts`, `packages/core/src/update/strategies/codex.ts`, corresponding unit tests -- **Testing**: Mock successful refresh, already-current and newer-than-registry output, command failure, missing executable, alternate detected plugin IDs/marketplace names, malformed/ambiguous IDs, and verify no fallback/auth call. Keep implementation blocked until the real Codex marketplace-refresh spike has evidence. References: Update Flow “Preserve each detected native source identity,” “Update Claude native plugin,” and “Update Codex native plugin.” +- **Files**: `packages/core/src/update/strategies/claude.ts`, `packages/core/src/update/strategies/codex.ts`, `packages/core/src/update/codex-transaction.ts`, corresponding unit/integration tests +- **Testing**: Cover Claude user/project/local/managed scopes and verify the exact `--scope` argument. For Codex, cover marketplace-refresh failure before mutation, backup failure, remove failure, add failure, local/remote version mismatch, rollback success/failure, prior enabled/disabled state, preserved unrelated config/cache entries, alternate detected plugin IDs/marketplace names, malformed/ambiguous IDs, already-current and newer-than-registry output, missing executables, and no fallback/auth call. References: Update Flow “Preserve each detected native source identity,” “Update Claude native plugin,” “Update Codex native plugin,” and “Codex reinstall fails.” ## Task 7: Implement Pi and fallback/OpenCode strategies -- [ ] **Description**: Add the package-owned Pi update strategy only for the canonical npm source and return `unsupported` for local, Git, pinned, or ambiguous Pi sources. Add the latest-published-CLI fallback refresh strategy, reusing existing idempotent installation, backup, merge, and tracking code rather than duplicating it. +- [ ] **Description**: Add the package-owned Pi strategy for only the exact unpinned canonical npm identity. Coalesce matching user/project scopes into one command, use `--no-approve` for user-only and disclosed/confirmed `--approve` for project scope, preserve settings/filter objects, and reject local, Git, pinned, conflicting, or ambiguous entries without partial scope updates. Add a package-internal `nsolid-plugin-refresh-owned` binary and an exact-version npm-exec/pnpm-dlx fallback transaction that invokes it for one planned harness. The internal entrypoint performs bundle-aware ownership/collision preflight, snapshots tracked skills/config/tracking, fully replaces owned skill directories, removes stale tracked assets, validates `bundleVersion`, rejects untracked destinations, and rolls back all owned state on failure. Do not change the public `nsolid-plugin install` command or programmatic `install()` contract. - **Depends on**: Tasks 2 and 5 -- **Files**: `packages/core/src/update/strategies/pi.ts`, `packages/core/src/update/strategies/fallback.ts`, corresponding unit/integration tests -- **Testing**: Verify the exact canonical Pi source, rejection of non-canonical sources, package-owned skill boundaries, OpenCode skill refresh, fallback MCP merge, configuration backup, preserved credentials, and no implicit install for an absent target. References: Update Flow “Update Pi package-owned skills,” “Reject a non-canonical Pi source,” “Update OpenCode or a fallback installation,” and “Requested harness is not installed.” +- **Files**: `packages/core/src/update/strategies/pi.ts`, `packages/core/src/update/strategies/fallback.ts`, `packages/core/src/update/fallback-transaction.ts`, `packages/core/src/update/refresh-owned-cli.ts`, `packages/core/package.json`, `packages/core/src/skills/skill-copier.ts`, `packages/core/src/skills/skill-tracker.ts`, `packages/core/src/mcp/mcp-config-writer.ts`, corresponding unit/integration tests +- **Testing**: Verify Pi user-only/project-only/both scopes, one invocation for duplicate canonical identity, exact `--approve`/`--no-approve`, unchanged source/filter/trust settings, rejection of non-canonical/conflicting sources, package-owned skill boundaries, an unchanged-too-old cache, and a newer version published during native execution. For OpenCode/fallback, cover deterministic npm-then-pnpm executor selection, exact immutable package version, the exact internal-binary command arrays, isolated temporary cwd with a conflicting local binary, complete replacement, stale tracked-skill removal, an untracked new-bundle destination collision, MCP merge, tracking/version update, missing ownership, missing executor, child/reconciliation/validation failures, rollback of every component, preserved credentials/user artifacts, no implicit install for an absent target, and regression coverage proving repeated public `install` behavior is unchanged. References: Update Flow “Update Pi package-owned skills,” “Same canonical Pi identity exists in both scopes,” “Reject a non-canonical Pi source,” “Update OpenCode or another fallback installation,” “No supported exact-package executor is available,” “Direct/fallback refresh cannot prove ownership,” “New fallback bundle collides with an untracked destination,” “OpenCode or fallback refresh fails,” “Requested harness is not installed,” and “Preserve the public install contract.” ## Task 8: Implement transactional Antigravity update -- [ ] **Description**: Add known-path staging detection, restrictive temporary backup of the staged root and N|Solid import-manifest entry, confirmed uninstall/install, new-root plus registration validation, successful cleanup, and rollback restoration that preserves unrelated imports. +- [ ] **Description**: Detect exactly one supported Antigravity layout pair: shared `~/.gemini/config/{plugins,import_manifest.json}` or AGY CLI `~/.gemini/antigravity-cli/{plugins,import_manifest.json}`. Add restrictive temporary backup of the detected staged root and matching N|Solid manifest entry, confirmed uninstall/install, new-root plus registration validation, successful cleanup, and rollback restoration that preserves unrelated imports. Return `unsupported` without mutation for ambiguous or unmatched layouts. - **Depends on**: Tasks 2 and 5 - **Files**: `packages/core/src/update/antigravity-transaction.ts`, `packages/core/src/update/strategies/antigravity.ts`, related unit/integration tests -- **Testing**: Cover both supported staged roots, successful replacement, declined confirmation, uninstall failure, install failure, root/manifest validation failure, rollback success/failure for both components, cleanup, unrelated import preservation, and credential preservation. References: Update Flow “Update Antigravity native plugin” and “Antigravity reinstall fails.” +- **Testing**: Cover both supported staged-root/manifest pairs, both-present ambiguity, unmatched root/manifest, successful replacement, declined confirmation, uninstall failure, install failure, root/manifest validation failure, rollback success/failure for both components, cleanup, unrelated import preservation, and credential preservation. References: Update Flow “Update Antigravity native plugin,” “Antigravity layout is ambiguous or unsupported,” and “Antigravity reinstall fails.” ## Task 9: Build the coordinator and programmatic API -- [ ] **Description**: Implement scope validation, one-plan-item-per-installation semantics, deterministic target/ownership ordering, check-only short circuit, plan confirmation, sequential execution, per-installation failure isolation, aggregation, and public `getVersionInfo()`, `checkUpdates()`, and `update()` exports. +- [ ] **Description**: Implement scope validation, one-plan-item-per-installation semantics, a synthetic non-mutating `none` item for an explicitly requested absent harness, complete ordered execute/rollback steps before confirmation, deterministic target/ownership ordering, check-only short circuit, plan confirmation, sequential execution, per-installation lookup/execution failure isolation, aggregation, and public `getVersionInfo()`, `checkUpdates()`, and `update()` exports. Represent lookup/validation failures as sanitized non-mutating plan items, convert them to failed results without execution, and reject any strategy execution that attempts an external command absent from its approved immutable plan. - **Depends on**: Tasks 4 and 6–8 - **Files**: `packages/core/src/update/coordinator.ts`, `packages/core/src/update/index.ts`, `packages/core/src/index.ts`, coordinator/API tests -- **Testing**: Cover CLI-only default, one harness selecting multiple installations, `--all`, `--all` plus harness rejection, check-only no-execute guarantee, newer-than-registry no-downgrade, unsupported check/update exit semantics, one failure with later success, coexisting native/fallback records, empty inventory, all status counts, and overall success/exit semantics. References: Update Flow “Update every detected target,” “Update coexisting native and fallback installations,” “Do not downgrade a CLI newer than the registry,” “One target fails during update-all,” “Check every detected target,” and “Conflicting update scopes.” +- **Testing**: Cover CLI-only default, one harness selecting multiple installations, `--all`, `--all` plus harness rejection, check-only no-execute guarantee, multi-command Codex/Antigravity and transactional fallback plans, undisclosed-command rejection, newer-than-registry no-downgrade, unsupported check/update exit semantics, one lookup or execution failure with later success, coexisting native/fallback records, empty inventory, all status counts, and overall success/exit semantics. References: Update Flow “Registry lookup fails,” “Update every detected target,” “Update coexisting native and fallback installations,” “Do not downgrade a CLI newer than the registry,” “One target fails during update-all,” “Check every detected target,” and “Conflicting update scopes.” ## Task 10: Add CLI commands and output formatting -- [ ] **Description**: Add `version` and `update` command parsing, bare `--version`, `--check`/`--all`, confirmation integration, human-readable summaries, JSON-only stdout, stderr progress, help text, installation identifiers/source-safe display, and exit-code mapping. +- [ ] **Description**: Add `version` and `update` command parsing, bare `--version`, `--check`/`--all`, confirmation integration, complete ordered execute/rollback plan display, human-readable summaries, JSON-only stdout, stderr progress, help text, installation identifiers/source-safe display, and exit-code mapping. - **Depends on**: Task 9 - **Files**: `packages/core/src/cli.ts`, `packages/core/src/utils/format.ts` or a new update formatter, CLI help/unit/integration tests -- **Testing**: Spawn the CLI with every supported scope/output combination. Assert exact JSON parseability, `latestVersion` and `installationId` presence, no ANSI in JSON, no mutation in check mode, prompts only when appropriate, sanitized errors, bare `--version` parity, no downgrade for `newer-than-registry`, informational exit zero for successful checks, and non-zero mutable exits for unsupported/failure results. References: Update Flow “Structured update output,” “Report versions with the conventional flag,” “Do not downgrade a CLI newer than the registry,” and “Non-interactive CLI update.” +- **Testing**: Spawn the CLI with every supported scope/output combination. Assert exact JSON parseability, `latestVersion` and `installationId` presence, complete multi-step plans before prompts, no ANSI in JSON, no mutation in check mode, prompts only when appropriate, redacted source/environment values, sanitized errors, bare `--version` parity, no downgrade for `newer-than-registry`, informational exit zero for successful checks, and non-zero mutable exits for unsupported/failure results. References: Update Flow “Structured update output,” “Update every detected target,” “Report versions with the conventional flag,” “Do not downgrade a CLI newer than the registry,” and “Non-interactive CLI update.” ## Task 11: Add atomic release preparation @@ -83,18 +82,18 @@ - [ ] **Description**: Implement `release:check`, including source/package equality, generated artifact checks, exact mismatch reporting, and cleanup-state validation. When and only when `--release` is present, compare the specification's explicit payload allowlist with the latest semantic-version tag and validate that payload changes have an update-visible version. - **Depends on**: Task 11 - **Files**: `scripts/check-release-version.mjs`, `package.json`, script fixture tests -- **Testing**: Introduce drift independently in every controlled file, stale generated output, unchanged version with changes in each payload allowlist category, malformed/missing tag state, and materialized package skills. Verify normal and `--release` check modes never repair. References: Release Versioning “Check synchronized release versions,” “Release version drift is detected,” and “Skill changes require an update-visible version.” +- **Testing**: Introduce drift independently in every controlled file, stale generated output, unchanged version with changes in each payload allowlist category, malformed/missing tag state, and materialized package skills. Verify normal and `--release` check modes never repair. References: Release Versioning “Check synchronized release versions,” “Release version drift is detected,” and “Skill changes retain the previous release version.” ## Task 13: Add end-to-end update regression coverage -- [ ] **Description**: Exercise the public CLI against isolated homes and fake harness executables/registries, including mixed native/fallback ownership, alternate source identities, unsupported Pi sources, Antigravity manifest rollback, and partial failure. +- [ ] **Description**: Exercise the public CLI against isolated homes and fake package managers/executors/harness executables/registries, including exact-version CLI install verification, mixed native/fallback ownership, alternate marketplace identities and version sources, Claude scopes, Codex transactional reinstall/rollback, Pi scope/trust combinations, OpenCode internal-refresh reconciliation/rollback with unchanged public install behavior, unsupported Pi/fallback sources, both Antigravity layout/manifest pairs, and lookup/execution partial failure. - **Depends on**: Tasks 9–12 - **Files**: `packages/core/test/integration/update-flow.test.ts`, test fixtures/helpers, `scripts/test-marketplace-install.js` where update assertions fit -- **Testing**: Cover all acceptance tests from the proposal on Linux-compatible fixtures and keep paths/commands portable for macOS and Windows CI. Assert credentials, non-NodeSource configurations, unrelated Antigravity manifest imports, and source identities are preserved byte-for-byte where applicable. +- **Testing**: Cover all acceptance tests from the proposal on Linux-compatible fixtures and keep paths/commands portable for macOS and Windows CI. Assert credentials, non-NodeSource configurations, unrelated Codex configuration/cache entries, unrelated Antigravity manifest imports, and source identities/scopes are preserved byte-for-byte where applicable. ## Task 14: Document user and maintainer workflows -- [ ] **Description**: Document CLI self-update, per-harness update ownership, check/JSON/automation modes, AGY replacement behavior, rollback guidance, version propagation, manual publication order, and the first-release bootstrap limitation. +- [ ] **Description**: Document exact-version CLI self-update and unsupported wrappers, per-harness update ownership and marketplace-source preservation, check/JSON/automation modes, Pi user/project trust behavior, OpenCode direct-install ownership and update-only transactional replacement while public install behavior remains unchanged, AGY replacement behavior, rollback guidance, version propagation, manual publication order, and the first-release bootstrap limitation. Do not advertise the package-internal refresh binary as a user workflow. - **Depends on**: Tasks 10–12 - **Files**: `README.md`, `packages/core/README.md`, `packages/pi-plugin/README.md` - **Testing**: Validate every documented command against CLI help/tests and ensure no documentation implies that a Git push alone updates version-keyed caches. References: both specifications and Design “Migration Strategy.” @@ -104,4 +103,4 @@ - [ ] **Description**: Run version drift checks, source/plugin checks, lint, type checking/build, all unit/integration tests, marketplace install tests, and package dry-run inspection for both publishable packages. - **Depends on**: Tasks 13–14 - **Files**: No production files unless a gate exposes a defect -- **Testing**: `pnpm release:check --release`, `pnpm plugin:check`, `pnpm lint`, `pnpm build`, `pnpm test`, `pnpm test:marketplace`, plus dry-run package contents confirming updated skills and same-version Pi/core dependency resolution. +- **Testing**: `pnpm release:check --release`, `pnpm plugin:check`, `pnpm lint`, `pnpm build`, `pnpm test`, `pnpm test:marketplace`, plus dry-run package contents confirming updated skills, the packaged internal refresh binary, unchanged public install entrypoints, and same-version Pi/core dependency resolution. From 5812b0f2c0cc511280e5893ca827e4684021c73e Mon Sep 17 00:00:00 2001 From: Cesar-M-Diaz Date: Mon, 3 Aug 2026 22:49:03 +0200 Subject: [PATCH 4/4] spec: address update flow review gaps --- openspec/changes/add-update-flow/design.md | 105 +++++++++++++---- openspec/changes/add-update-flow/proposal.md | 18 +-- .../specs/release-versioning/spec.md | 32 +++++- .../add-update-flow/specs/update-flow/spec.md | 108 ++++++++++++++---- openspec/changes/add-update-flow/tasks.md | 20 ++-- 5 files changed, 213 insertions(+), 70 deletions(-) diff --git a/openspec/changes/add-update-flow/design.md b/openspec/changes/add-update-flow/design.md index 8315c3f..74c1fc3 100644 --- a/openspec/changes/add-update-flow/design.md +++ b/openspec/changes/add-update-flow/design.md @@ -57,9 +57,9 @@ Release preparation is a separate maintainer-side script. Runtime update code re `packages/core/src/update/version-source.ts` -- Reads and validates `latest` metadata from npm for `nsolid-plugin` and `nsolid-pi-plugin`. -- Resolves Claude/Codex latest-version evidence only from the exact carried marketplace source: a validated Git repository/ref plus relative manifest path, or the detected local marketplace snapshot. It never substitutes the canonical NodeSource GitHub root for an alternate marketplace. -- Reads the canonical GitHub-root `bundle.json` only for fixed-source native Git targets such as Antigravity. +- Reads and validates `latest` metadata from the detected npm registry for `nsolid-plugin` and `nsolid-pi-plugin`, retaining the normalized registry origin, exact tarball URL, version, and registry-provided integrity digest as one immutable artifact identity. +- Resolves every supported Git marketplace ref to a full commit object ID before planning, reads the manifest and content digest from that commit, and carries the repository, commit, relative manifest path, and digest together. A missing revision, mutable ref that cannot be resolved, or source that cannot bind lookup and execution to that commit is `unsupported`. +- Resolves the canonical GitHub-root Antigravity source to a full commit and reads `bundle.json` from that commit; a moving default branch is never the executable identity. - Applies bounded request timeouts and semantic-version validation. - Returns `unknown` rather than treating missing, local-stale, ambiguous, or unsupported marketplace version evidence as current; native execution may still use the preserved harness-owned ID when its identity is unambiguous. @@ -67,7 +67,8 @@ Release preparation is a separate maintainer-side script. Runtime update code re - Detects npm or pnpm only when the real CLI package/entrypoint is contained by that manager's reported global root and the corresponding executable is available; a shim or package-manager environment variable alone is not sufficient evidence. - Produces a fixed executable plus argument array. -- Pins update and rollback package specs to the exact semantic versions resolved during planning. +- Downloads only the planned tarball from the planned registry, verifies its integrity before execution, and gives npm/pnpm the verified local tarball rather than re-resolving `name@version` through ambient registry configuration. +- Verifies post-update package identity against the planned name, version, registry provenance, and integrity/content digest rather than accepting version equality alone. - Returns unsupported for workspaces, `npx`, local checkouts, Volta/Yarn/Bun ownership, mismatched global roots, and ambiguous launchers. `packages/core/src/update/command-runner.ts` @@ -100,11 +101,13 @@ Release preparation is a separate maintainer-side script. Runtime update code re `packages/core/src/update/fallback-transaction.ts` -- Resolves one available package executor (`npm exec` preferred, otherwise `pnpm dlx`), pins the child package to the exact validated `nsolid-plugin@` selected during planning, and runs it from a restrictive temporary working directory so workspace binaries/configuration cannot shadow the payload. -- Invokes the exact package's dedicated internal `nsolid-plugin-refresh-owned` binary for only the planned harness; the regular `nsolid-plugin install` command and programmatic `install()` contract are not changed. -- The exact child snapshots the target's tracked NodeSource-owned skill directories, affected MCP configuration, and complete tracking state before mutation, using its own bundled payload for ownership/collision preflight. +- Resolves one available package executor (`npm exec` preferred, otherwise `pnpm dlx`), verifies the planned `nsolid-plugin` tarball integrity, and runs that verified local artifact from a restrictive temporary working directory so workspace binaries/configuration cannot shadow the payload. +- Before launching the child, the parent creates and fsyncs a restrictive durable journal plus complete snapshot of the selected installation's tracked skill directories, affected MCP fields, links, and tracking state. The journal records `prepared`, `mutating`, and `committed` phases and remains recoverable if the package executor or child times out, crashes, or is killed. +- Invokes the exact package's dedicated internal `nsolid-plugin-refresh-owned` binary with a parent-created transaction manifest. The manifest binds `installationId`, harness, canonical owned paths, tracking-file path and digest, and field-level MCP ownership; the regular `nsolid-plugin install` command and programmatic `install()` contract are not changed. +- The child validates that the live tracking digest, installation identity, canonical paths, and MCP fields still equal the approved manifest before mutation. It refuses stale, sibling, broadened, or ambiguous identity rather than rediscovering a target from `--harness` alone. - Reconciles the installed asset set against the new bundle: complete skill directories are replaced, previously tracked skills absent from the new bundle are removed, and untracked/user-owned paths and unrelated MCP entries are preserved. -- Updates `bundleVersion` only after the new skills, MCP entries, and tracking data validate; restores the snapshot if execution, reconciliation, or validation fails. +- Updates `bundleVersion` only after the new skills, MCP entries, and tracking data validate. The parent marks the journal committed and removes it only after post-update validation; otherwise it restores its snapshot independently of child-process availability. +- On every later update invocation, the parent recovers or reports any non-committed journal before planning new mutation. - Treats direct artifacts without sufficient tracking ownership as `unsupported` rather than deleting paths by name or prefix. ### Existing modules extended @@ -134,8 +137,8 @@ Release preparation is a separate maintainer-side script. Runtime update code re `packages/core/src/skills/skill-tracker.ts` -- Fallback tracking adds an optional `bundleVersion` and retains enough per-harness ownership/path evidence to reconcile obsolete assets safely. -- Readers must accept existing tracking files that omit it. +- Fallback tracking adds an optional `bundleVersion`, canonical per-installation skill/link paths, and MCP ownership evidence per JSON field/value so shared paths and user-modified fields cannot be claimed by name alone. +- Readers accept legacy tracking for reporting, but automatic mutation is `unsupported` until the selected installation has complete per-path and field-level ownership evidence; compatibility never authorizes a name-only write or deletion. ### Release modules @@ -152,7 +155,8 @@ Release preparation is a separate maintainer-side script. Runtime update code re - Compares package and generated versions with the root bundle. - Calls/reuses existing bundle and root-manifest checks. - Activates release mode only when invoked through `pnpm release:check --release`. -- In release mode, compares the explicit plugin payload allowlist from the Release Versioning specification with the latest semantic-version tag and rejects an unchanged version. +- In release mode, compares the explicit published-payload allowlist from the Release Versioning specification with the highest eligible local semantic-version tag whose peeled commit is an ancestor of `HEAD`, and rejects an unchanged version. +- Accepts exactly `X.Y.Z` and `vX.Y.Z` tag names, handles lightweight and annotated tags by peeling to commits, ignores non-semantic and non-ancestor tags, and fails explicitly for missing/malformed-only tags, ambiguous duplicate versions, or shallow history that prevents proving ancestry. Root package scripts: @@ -219,14 +223,17 @@ export type MarketplaceVersionSource = | { kind: 'git' repository: string - revision?: string + revision: string + commit: string manifestPath: string + contentDigest: string } | { kind: 'local-snapshot' root: string manifestPath: string freshness: 'verified' | 'stale' | 'unknown' + contentDigest: string } | { kind: 'unknown' @@ -240,6 +247,45 @@ export type PiPackageLocation = export type FallbackPackageExecutor = 'npm-exec' | 'pnpm-dlx' +export interface NpmArtifactIdentity { + kind: 'npm' + packageName: 'nsolid-plugin' | 'nsolid-pi-plugin' + version: string + registry: string + tarball: string + integrity: string +} + +export interface GitArtifactIdentity { + kind: 'git' + repository: string + commit: string + contentDigest: string +} + +export interface LocalArtifactIdentity { + kind: 'local-snapshot' + root: string + contentDigest: string +} + +export type ResolvedArtifactIdentity = NpmArtifactIdentity | GitArtifactIdentity | LocalArtifactIdentity + +export interface FallbackTransactionIdentity { + installationId: string + harness: HarnessType + trackingPath: string + trackingDigest: string + ownedSkillPaths: readonly string[] + ownedLinkPaths: readonly string[] + ownedMcpFields: readonly { + configPath: string + server: string + field: string + expectedDigest: string + }[] +} + export type AntigravityLayout = | { kind: 'shared' @@ -331,6 +377,8 @@ export interface UpdatePlanItem { installed: boolean source: UpdateSource version: VersionInfo + artifact?: ResolvedArtifactIdentity + fallbackTransaction?: FallbackTransactionIdentity steps: readonly UpdatePlanStep[] rollbackSteps: readonly UpdatePlanStep[] planningError?: UpdateError @@ -369,6 +417,7 @@ export interface UpdateSummary { results: UpdateResult[] counts: Record success: boolean + exitCode: 0 | 1 | 2 } export interface CommandSpec { @@ -415,32 +464,38 @@ Rules enforced by these contracts: - A detected installation source that cannot be updated safely is represented as `unsupported`, never replaced with a different source. - `ownership: 'none'` with `source.kind: 'none'` is reserved for the synthetic, non-mutating plan/result produced when an explicitly requested harness has no detected installation. It has empty execute/rollback steps, requires no confirmation, and is never emitted as a detected target under `--all`. - Marketplace version resolution uses only the `versionSource` carried by the detected Claude or Codex registration. An unknown or stale local source yields `unknown`; it never falls back to the NodeSource marketplace. +- A mutating plan that depends on Git carries a full immutable commit and content digest; a plan that depends on npm carries registry, tarball, version, and integrity; and a verified local snapshot carries its canonical root and content digest. Execution and post-update validation use that same `artifact` identity and never re-resolve a mutable ref, dist-tag, package name/version, ambient registry, or changed snapshot. +- A project-scoped Pi command has `cwd` equal to the canonical captured `projectRoot`. Immediately before execution the strategy revalidates that directory identity, effective `.pi/settings.json` entry, scopes, source, and cache roots still match the approved plan; drift produces a non-mutating failure. +- A fallback child receives and validates the exact `fallbackTransaction` manifest approved by the parent. Harness-only rediscovery is not an executable identity. - Strategies return data; the CLI formatter owns human-readable output. -- A completed check whose result is `current`, `update-available`, `newer-than-registry`, `unsupported`, or evidence-only `unknown` is informational and exits zero. A timeout, invalid response, or other operational lookup/validation failure is `failed` and remains non-zero. -- A mutating update with `newer-than-registry` performs no downgrade and exits zero; a mutating `unsupported` result exits non-zero with manual guidance. +- A completed check whose result is `current`, `update-available`, `newer-than-registry`, `unsupported`, or evidence-only `unknown` is informational and exits `0`. A timeout, invalid response, or other operational lookup/validation failure is `failed` and exits `1`. +- A mutating update with `newer-than-registry` performs no downgrade and exits `0`; a mutating `unsupported` result exits `2` with manual guidance. - A declined plan produces `skipped` results and exits zero. +- Exit code `0` means a completed update/check or an intentional informational no-op. Exit code `1` means an operational lookup, planning, execution, validation, rollback, or recovery failure. Exit code `2` means the requested mutation was unavailable without operational failure because approval was missing or the result was `not-installed`, `unsupported`, or mutation-blocking `unknown`. In aggregate results, code `1` takes precedence over code `2`. +- A read-only check with `not-installed`, `unsupported`, or evidence-only `unknown` exits `0`. A mutating invocation with any such unavailable result exits `2` unless another item failed and requires exit `1`. +- An empty `--all` inventory is an explicit successful no-op: it exits `0`, emits `results: []` and zero counts in JSON, and reports that no targets were detected in human output. ### Fixed harness command plans | Target | Native/package action | Success guidance | |---|---|---| -| CLI npm | `npm install --global nsolid-plugin@` | invoke CLI again | -| CLI pnpm | `pnpm add --global nsolid-plugin@` | invoke CLI again | +| CLI npm | verify the planned tarball integrity, then `npm install --global ` | invoke CLI again | +| CLI pnpm | verify the planned tarball integrity, then `pnpm add --global ` | invoke CLI again | | Claude | `claude plugin update --scope ` | `/reload-plugins` or restart | | Codex | `codex plugin marketplace upgrade `, then `codex plugin remove ` and `codex plugin add ` | start a new session | -| Antigravity | `agy plugin uninstall nsolid-plugin`, then `agy plugin install https://github.com/NodeSource/nsolid-plugin.git` | restart AGY | +| Antigravity | `agy plugin uninstall nsolid-plugin`, then install the canonical repository pinned to the planned full commit | restart AGY | | Pi user-only | `pi update npm:nsolid-pi-plugin --no-approve` | `/reload` or restart | | Pi with detected project scope | `pi update npm:nsolid-pi-plugin --approve` after the project root is disclosed and approved | `/reload` or restart | -| Fallback/OpenCode through npm | `npm exec --yes --package=nsolid-plugin@ -- nsolid-plugin-refresh-owned --harness ` inside a fallback transaction | restart harness if needed | -| Fallback/OpenCode through pnpm | `pnpm --package=nsolid-plugin@ dlx nsolid-plugin-refresh-owned --harness ` inside a fallback transaction | restart harness if needed | +| Fallback/OpenCode through npm | execute `nsolid-plugin-refresh-owned --transaction ` from the integrity-verified local npm tarball | restart harness if needed | +| Fallback/OpenCode through pnpm | execute `nsolid-plugin-refresh-owned --transaction ` from the integrity-verified local pnpm tarball | restart harness if needed | Marketplace IDs, Claude scopes, package versions, Pi scopes, and package sources are passed as separate arguments only after strict validation. A native ID is accepted only when it matches `nsolid-plugin@[A-Za-z0-9][A-Za-z0-9._-]*`; the base name alone, malformed IDs, control characters, whitespace, ambiguous matches, and a Claude installation whose scope cannot be determined return `unsupported`. Marketplace inventory also carries the exact repository/ref and relative manifest path, or the exact local snapshot path and freshness evidence, used for version resolution. Repository credentials are stripped before data reaches plan or result output; traversal-capable manifest paths and ambiguous source metadata return `unknown` or `unsupported` without canonical-source substitution. The only supported Pi identity is the exact unpinned `npm:nsolid-pi-plugin`. Inventory coalesces canonical user/project entries into one Pi target because one `pi update ` invocation updates every matching identity; the discriminated location requires `projectRoot` whenever project scope is present. Any local, Git, pinned, conflicting, or ambiguous matching entry returns `unsupported` for the whole target rather than producing a misleading partial success. No user-derived string is interpolated into an executable shell command. The planner emits one item per `UpdateInstallation`. If a harness has both native and fallback artifacts, both items remain visible and are updated independently; a native failure never switches to fallback ownership. -The CLI registry lookup resolves the `latest` dist-tag once, validates it as a stable semantic version, and stores that exact version in the immutable plan. Execution never sends `@latest` back to a package manager. npm uses `install --global`; pnpm uses `add --global`. Success requires both a zero child exit and an on-disk package manifest at the positively identified global root whose name/version equal `nsolid-plugin` and the planned version. A failed or mismatched result returns the exact previous-version command for the same manager. +The CLI registry lookup resolves the `latest` dist-tag once from the effective registry, validates it as a stable semantic version, and stores registry origin, exact tarball URL, version, and integrity in the immutable plan. Execution never sends `@latest` or `name@version` back to a package manager: it downloads the planned tarball, verifies integrity, and installs that verified local artifact. Success requires both a zero child exit and on-disk package evidence whose name, version, and content digest match the planned artifact. A failed or mismatched result returns the exact previous-artifact guidance for the same manager. -Pi source detection reads both user and current-project settings, including object-form entries and filters. User and project entries for the same unpinned npm package become one command target. A user-only target passes `--no-approve` so an unrelated current directory cannot broaden the operation. A detected project target records and displays its project root and passes `--approve` only after the update plan is approved; this is a one-command trust decision and does not rewrite Pi trust/settings files. Pi's own updater preserves source entries and package filters. Because `pi update ` does not accept a target version, the registry version observed during planning is a minimum postcondition rather than an executable argument: the strategy reads and reports the actual package-cache version after Pi completes, accepts a newer valid version published during the run, and fails if any affected cache remains older than the planned version. +Pi source detection reads both user and current-project settings, including object-form entries and filters. User and project entries for the same unpinned npm package become one command target. A user-only target passes `--no-approve` so an unrelated current directory cannot broaden the operation. A detected project target records the canonical project root and directory identity, displays it, and sets the command `cwd` to that exact root. Immediately before invoking `pi update`, the strategy re-reads the effective user/project entries, scopes, source, and cache roots and refuses mutation if they differ from the approved plan. It passes `--approve` only after this revalidation and plan approval. Because `pi update ` does not accept a target version, the planned registry artifact is a minimum postcondition; every affected cache must retain provenance for that registry and integrity/content evidence for the resulting package, including a newer valid publication observed during execution. OpenCode supports native skills and a separate npm/local plugin system, but the current N|Solid distribution is not registered as an OpenCode plugin. Its owner is therefore the tracked direct installer at `~/.config/opencode/skills/` plus the merged `mcp` entries in `opencode.json(c)`. The updater must not invoke `opencode plugin`. It invokes the exact published N|Solid CLI package's internal `nsolid-plugin-refresh-owned` binary as the payload provider and transaction executor. The existing public `install` flow keeps its idempotent copy/merge semantics and does not acquire stale-asset removal behavior. @@ -587,13 +642,13 @@ sequenceDiagram - `--all` catches version-lookup, planning, and execution errors at the installation boundary and continues with independent installation records, including native and fallback records for the same harness. - Confirmation is mandatory for mutable non-interactive operations unless `--yes` is present. - Marketplace IDs and Claude scopes are validated before becoming arguments; local, pinned, conflicting, and ambiguous Pi sources are never silently replaced. -- CLI and fallback package execution uses the exact immutable version from the plan; mutable dist-tags are not passed during mutation, and a package-manager success without matching on-disk version evidence is a failure. -- Pi user-only updates pass `--no-approve`; `--approve` is used only when the immutable plan identifies and displays a project-scoped canonical package. Canonical entries across both scopes are updated once, while conflicting/pinned entries block automatic mutation. +- CLI and fallback package execution uses the registry, tarball, and integrity identity from the plan; mutable dist-tags and ambient registry resolution are not used during mutation, and package-manager success without matching on-disk content evidence is a failure. +- Pi user-only updates pass `--no-approve`; `--approve` is used only when the immutable plan identifies, displays, executes within, and immediately revalidates a project-scoped canonical package root. Canonical entries across both scopes are updated once, while changed/conflicting/pinned entries block automatic mutation. - Codex removes the installed plugin only after marketplace refresh and backup succeed. Rollback validates the restored registration and cached payload while preserving unrelated `config.toml` entries. - Antigravity accepts only one unambiguous documented layout pair. Backup paths are created with restrictive permissions in an OS temporary directory and always cleaned after success. Rollback validates both the staged root and its matching saved import-manifest registration. -- OpenCode/fallback replacement mutates only paths and MCP entries proven to be owned by tracking. The transaction restores overwritten and stale-removed skill directories, config, and tracking together after any failed child execution or validation. +- OpenCode/fallback replacement mutates only paths and MCP fields bound to the approved installation manifest. The parent-owned durable journal restores overwritten and stale-removed skill directories, config, and tracking after child failure, timeout, signal, or interrupted prior execution. - Update does not invoke setup, login, or auth modules. -- Release scripts snapshot only an explicit allowlist; rollback never performs broad Git or recursive workspace resets. +- Release scripts snapshot only explicit controlled files; rollback never performs broad Git or recursive workspace resets. Release payload checking includes runtime source inputs that are compiled or copied into both published packages. ## Migration Strategy diff --git a/openspec/changes/add-update-flow/proposal.md b/openspec/changes/add-update-flow/proposal.md index 01c28cc..c1853b5 100644 --- a/openspec/changes/add-update-flow/proposal.md +++ b/openspec/changes/add-update-flow/proposal.md @@ -21,13 +21,13 @@ For users: - add `nsolid-plugin version` (with bare `nsolid-plugin --version` as an alias) and `nsolid-plugin update`; - make plain `update` target the npm CLI, `--harness ` target every detected installation for one harness, and `--all` target the CLI plus every detected N|Solid installation; - add `--check` for a read-only status check and retain `--json`, `--yes`, `--verbose`, and `--no-color` behavior where applicable; -- update a positively identified npm- or pnpm-owned global CLI with the exact semantic version resolved during planning, then verify the installed package on disk instead of trusting only the package-manager exit code; +- update a positively identified npm- or pnpm-owned global CLI from the exact registry tarball and integrity identity resolved during planning, then verify the installed content on disk instead of trusting only the package-manager exit code or semantic version; - delegate native updates to the owning harness: - Claude: update the detected `nsolid-plugin@` identity at its detected installation scope and resolve version evidence only from that marketplace's carried source metadata; - Codex: refresh the detected Git marketplace snapshot, then transactionally remove and add the same detected plugin identity because marketplace refresh does not update the installed copy; version checks never substitute a canonical marketplace for the detected source; - - Antigravity: safely reinstall the GitHub-root plugin with backup/rollback of the detected AGY or shared Antigravity staged-root/import-manifest pair; - - Pi: update the canonical unpinned `npm:nsolid-pi-plugin` identity once across its detected user/project scopes, while rejecting local, pinned, Git, conflicting, or ambiguous sources; - - OpenCode, whose N|Solid installation is direct rather than an OpenCode plugin, and other tracked fallback installations: invoke an internal exact-package refresh binary to transactionally reconcile tracked assets, including removal of obsolete NodeSource-owned assets, without changing public `install` semantics; + - Antigravity: safely reinstall the GitHub-root plugin from a planned immutable commit with backup/rollback of the detected AGY or shared Antigravity staged-root/import-manifest pair; + - Pi: update the canonical unpinned `npm:nsolid-pi-plugin` identity once across its detected user/project scopes from the captured/revalidated project root, while rejecting changed, local, pinned, Git, conflicting, or ambiguous sources; + - OpenCode, whose N|Solid installation is direct rather than an OpenCode plugin, and other tracked fallback installations: invoke an internal integrity-verified refresh binary using an exact parent-owned installation manifest and durable recovery journal, including removal of obsolete NodeSource-owned assets without changing public `install` semantics; - preserve credentials and non-NodeSource configuration throughout updates; - isolate failures during `--all` so one harness failure does not prevent remaining updates, while returning a failing exit status and a per-target summary. @@ -39,13 +39,13 @@ For maintainers: ## Rollback Plan -- The CLI update path records the previously installed CLI version, pins both update and rollback commands to exact semantic versions, verifies the resulting global package root, and prints the exact package-manager command needed to restore it. +- The CLI update path records the previously installed CLI artifact, binds update and rollback to registry/tarball/integrity identities, verifies the resulting global package content, and prints exact recovery guidance. - A CLI newer than the registry is reported and left unchanged; this proposal has no implicit downgrade path. - Claude delegates to its native in-place update command while preserving the detected plugin ID and installation scope. - Codex snapshots the exact plugin registration, enablement, and cached payload before the documented marketplace-refresh plus remove/add sequence, and restores that snapshot if reinstall or validation fails. - Antigravity creates a temporary backup of the detected staged NodeSource plugin and its matching import-manifest registration and restores both if reinstall fails. - Pi delegates package replacement to `pi update` without rewriting its settings entries, package filters, MCP configuration, or credentials. -- OpenCode/fallback refresh snapshots every tracked NodeSource-owned skill path, affected MCP entries/config file, and tracking state before replacement; it restores them if exact-version execution, stale-asset reconciliation, or validation fails. +- The fallback parent durably snapshots the exact installation's owned skill/link paths, field-level MCP state, and tracking before launching the child; it restores or recovers them after child failure, timeout, signal, interrupted execution, reconciliation failure, or validation failure. - Update operations never delete shared NodeSource credentials. - The feature can be reverted by removing the new command/service modules and scripts; existing `setup`, `install`, `doctor`, `restore`, and `uninstall` contracts remain unchanged. - A bad release can be rolled back by republishing or reinstalling the prior known-good package/plugin version and restoring generated manifests from the corresponding Git tag. @@ -75,7 +75,7 @@ For maintainers: - `nsolid-plugin update --check` performs no writes or subprocess mutations and clearly reports whether the CLI is current. - Each supported harness has a deterministic update strategy with actionable output when its CLI is unavailable, its installation type is unsupported, or its source identity cannot be safely reused. - Claude and Codex version checks use only the source metadata carried by their detected marketplace; missing, stale, or unsupported evidence reports `unknown` instead of reading the NodeSource marketplace. -- `nsolid-plugin update --all` updates all detected targets, preserves credentials and user-owned configuration, summarizes every target, and exits non-zero on any failure. +- `nsolid-plugin update --all` updates all detected targets, preserves credentials and user-owned configuration, summarizes every target, and uses deterministic exit codes for success/no-op, operational failure, and unavailable mutation, including an explicit successful empty inventory. - Native and fallback installations detected for the same harness are represented and updated as separate targets; one ownership never silently replaces the other. - Interactive destructive/replacement steps require confirmation; `--yes` enables non-interactive automation. - Network, registry, missing-binary, permission, corrupt-state, and partial-update failures are covered by tests and never expose credentials. @@ -85,11 +85,11 @@ For maintainers: Acceptance tests: -1. Mock npm reporting a newer, equal, and older-than-registry CLI version and verify check-only, exact-version npm/pnpm commands, on-disk post-install validation, no-downgrade behavior, declined update, unsupported wrappers, and exact rollback guidance. +1. Mock npm reporting a newer, equal, and older-than-registry CLI artifact and verify registry/tarball/integrity binding, on-disk content validation, no-downgrade behavior, declined update, unsupported wrappers, and exact rollback guidance. 2. Mock current and newer Claude/Codex plugin versions, including alternate marketplace IDs, source repositories, stale local snapshots, and Claude installation scopes; verify exact-source version resolution, no canonical-source substitution, Claude’s scoped native update, and Codex’s marketplace-refresh plus transactional remove/add flow. 3. Simulate Codex and Antigravity reinstall failures and verify restoration of the prior plugin registration, enablement, cached/staged payload, and matching manifest state. 4. Mock user-only, project-only, and combined canonical Pi scopes plus pinned/conflicting sources; verify one scope-aware native update command and unchanged settings. -5. Refresh a tracked OpenCode installation through the internal exact-version npm and pnpm refresh binary; verify atomic skill replacement, stale tracked-skill removal, MCP merge, tracking update, rollback, and unchanged public `install` behavior without modifying untracked/user-owned artifacts. +5. Refresh a tracked OpenCode installation through the internal integrity-verified binary and parent transaction manifest; verify identity revalidation, atomic skill replacement, field-level MCP ownership, parent rollback, next-run recovery, and unchanged public `install` behavior without modifying sibling or user-owned artifacts. 6. Run `--all` with coexisting native/fallback installations and one failed target or version lookup; verify every installation is represented, later independent targets still run, credentials remain untouched, and the final exit code is non-zero. 7. Prepare a patch version in a fixture repository and verify all version-bearing files become equal while no publish/tag/push command executes. 8. Introduce version drift in each controlled file and verify the release check identifies the exact mismatch. diff --git a/openspec/changes/add-update-flow/specs/release-versioning/spec.md b/openspec/changes/add-update-flow/specs/release-versioning/spec.md index 8dcca8a..3505b70 100644 --- a/openspec/changes/add-update-flow/specs/release-versioning/spec.md +++ b/openspec/changes/add-update-flow/specs/release-versioning/spec.md @@ -84,11 +84,13 @@ Release checking SHALL compare every controlled version and generated artifact w ### Requirement: Plugin payload changes require an update-visible version -Release checking SHALL reject payload changes whose explicit bundle version still matches the most recent release tag. +Release checking SHALL reject payload changes whose explicit bundle or package version still matches the highest eligible semantic release tag reachable from `HEAD`. Release mode SHALL be activated only by `pnpm release:check --release`. For this comparison, “plugin payload files” is the following explicit allowlist: - `skills/**` +- `packages/core/src/**` +- `packages/pi-plugin/index.js` - `bundle.json` - `.claude-plugin/marketplace.json` - `.claude-plugin/plugin.json` @@ -102,12 +104,38 @@ Release mode SHALL be activated only by `pnpm release:check --release`. For this #### Scenario: Skill changes retain the previous release version -**Given** committed plugin payload files differ from the most recent release tag +**Given** committed, staged, unstaged, or untracked plugin payload files differ from the selected semantic release tag **And** `bundle.json.version` still equals the version represented by that tag **When** the maintainer runs `pnpm release:check --release` **Then** it fails with guidance to prepare a new semantic version **And** prevents a release that version-keyed harness caches would treat as unchanged +#### Scenario: Published runtime changes retain the previous release version + +**Given** committed, staged, unstaged, or untracked files under `packages/core/src/**` or `packages/pi-plugin/index.js` differ from the selected semantic release tag +**And** the corresponding package version still equals the version represented by that tag +**When** the maintainer runs `pnpm release:check --release` +**Then** it fails with guidance to prepare a new semantic version +**And** it prevents an attempt to publish different runtime bytes under an immutable npm package version + +#### Scenario: Select the semantic release baseline deterministically + +**Given** the local repository contains lightweight or annotated tags whose names exactly match `X.Y.Z` or `vX.Y.Z` +**And** their peeled commits are ancestors of `HEAD` +**When** release mode selects its baseline +**Then** it selects the eligible tag with the highest stable semantic version, independent of tag creation date +**And** treats the optional lowercase `v` as a naming prefix rather than part of the version +**And** ignores non-semantic tags and semantic tags whose commits are not ancestors of `HEAD` +**And** fails as ambiguous if both prefixed and unprefixed eligible tags represent the selected version but peel to different commits + +#### Scenario: Release baseline is unavailable + +**Given** no eligible semantic release tag is available locally, all candidate tags are malformed, or shallow history prevents proving tag ancestry +**When** the maintainer runs `pnpm release:check --release` +**Then** it exits non-zero before evaluating the payload diff +**And** distinguishes missing tags, malformed-only tag state, and incomplete shallow history +**And** reports that remote tags are not considered until they are fetched into the local repository + ### Requirement: Manual publication remains ordered and external Release preparation SHALL leave publication to the maintainer while defining the required package and Git ordering. diff --git a/openspec/changes/add-update-flow/specs/update-flow/spec.md b/openspec/changes/add-update-flow/specs/update-flow/spec.md index 064013d..3da87fc 100644 --- a/openspec/changes/add-update-flow/specs/update-flow/spec.md +++ b/openspec/changes/add-update-flow/specs/update-flow/spec.md @@ -63,12 +63,12 @@ The updater SHALL compare installed and latest versions without invoking any mut **And** represents the affected installation in the ordered plan with a sanitized planning error and no mutation steps **And** performs no mutation for the affected target **And** an `--all` invocation continues planning or executing remaining independent targets and records their results -**And** the overall invocation exits non-zero +**And** the overall invocation exits with code `1` **And** preserves every installation whose lookup failed ### Requirement: Safe CLI self-update -The default `nsolid-plugin update` scope SHALL update only a positively identified global CLI installation and SHALL require approval before mutation. +The default `nsolid-plugin update` scope SHALL update only a positively identified global CLI installation, SHALL require approval before mutation, and SHALL bind registry discovery, package execution, and post-update validation to one integrity-verified artifact. #### Scenario: CLI update with a supported global package manager @@ -77,20 +77,21 @@ The default `nsolid-plugin update` scope SHALL update only a positively identifi **When** the user runs `nsolid-plugin update` **Then** the command displays the current version, target version, package manager, and exact planned operation **And** asks for confirmation in an interactive terminal -**And** freezes the resolved semantic version in the plan rather than passing the mutable `latest` tag during execution -**And** after confirmation invokes `npm install --global nsolid-plugin@` or `pnpm add --global nsolid-plugin@` with a fixed argument array -**And** verifies both that the child process succeeded and that the positively identified global package root contains `nsolid-plugin` at the resolved version +**And** freezes the effective registry origin, exact tarball URL, stable version, and registry-provided integrity digest in the plan rather than passing the mutable `latest` tag during execution +**And** downloads only that tarball and verifies its integrity before confirmation can authorize installation +**And** after confirmation invokes npm or pnpm with the verified local tarball and a fixed argument array, without ambient registry resolution +**And** verifies both that the child process succeeded and that the positively identified global package root contains `nsolid-plugin` with the planned version and content identity **And** reports that a new shell or command invocation may be required **And** prints the same package manager's exact command for restoring `nsolid-plugin@` #### Scenario: Package manager exits successfully without installing the planned CLI **Given** an exact CLI update was approved -**When** the package-manager process exits successfully but the identified global package root is missing, belongs to a different package, or reports a version other than the planned version +**When** the package-manager process exits successfully but the identified global package root is missing, belongs to a different package, reports a version other than the planned version, or cannot prove the planned content identity **Then** the update result is `failed` **And** the command does not report the CLI as updated **And** prints the exact previous-version restore command -**And** exits non-zero +**And** exits with code `1` #### Scenario: User declines a CLI update @@ -106,7 +107,7 @@ The default `nsolid-plugin update` scope SHALL update only a positively identifi **And** standard input is not interactive **When** the user runs `nsolid-plugin update` without `--yes` **Then** the command performs no mutation -**And** exits non-zero with guidance to pass `--yes` +**And** exits with code `2` and guidance to pass `--yes` **When** the user reruns with `--yes` **Then** the command performs the displayed fixed update plan without prompting @@ -126,11 +127,11 @@ The default `nsolid-plugin update` scope SHALL update only a positively identifi **And** reports the latest version when it can be resolved **And** prints safe exact-version manual commands for npm, pnpm, ephemeral execution, and the detected wrapper/source when known **And** the result status is `unsupported` -**And** a mutating update exits non-zero while a read-only check exits successfully +**And** a mutating update exits with code `2` while a read-only check exits with code `0` ### Requirement: Harness-owned update strategies -The updater SHALL preserve native/package ownership and delegate each supported harness update to a deterministic strategy without starting OAuth. +The updater SHALL preserve native/package ownership, bind discovery and execution to the same immutable source identity, and delegate each supported harness update to a deterministic strategy without starting OAuth. #### Scenario: Update one installed native harness @@ -148,9 +149,11 @@ The updater SHALL preserve native/package ownership and delegate each supported **When** the corresponding native update strategy runs **Then** Claude uses the detected complete plugin ID and installation scope **And** Codex refreshes the detected marketplace and reinstalls the detected complete plugin ID -**And** inventory carries that marketplace's exact repository/ref and relative manifest path, or its exact local snapshot path and freshness evidence, for version resolution +**And** inventory carries that marketplace's exact repository/ref and relative manifest path, or its exact local snapshot path, freshness evidence, and content digest, for version resolution +**And** planning resolves every supported Git ref to a full commit object ID and content digest used by both lookup and execution **And** latest-version lookup reads only that carried source -**And** missing, stale, ambiguous, traversal-capable, or unsupported version-source evidence reports `unknown` or `unsupported` without querying the NodeSource marketplace +**And** a local snapshot must retain the planned digest through execution and post-update validation +**And** a missing revision, mutable ref that cannot be resolved and honored by the harness, stale snapshot, ambiguous source, traversal-capable path, or unsupported version-source evidence reports `unknown` or `unsupported` without querying the NodeSource marketplace **And** the strategy never substitutes `nodesource` **And** an unqualified, malformed, or ambiguous ID, or a Claude installation with unknown scope, returns `unsupported` without mutation @@ -159,8 +162,10 @@ The updater SHALL preserve native/package ownership and delegate each supported **Given** `nsolid-plugin@` is installed natively in Claude at a detected `user`, `project`, `local`, or `managed` scope **And** the `claude` executable is available **When** the Claude update strategy runs -**Then** it invokes `claude plugin update nsolid-plugin@ --scope ` with a fixed executable and argument array +**Then** the carried marketplace source resolves to an immutable commit and content digest that Claude can honor for this update +**And** it invokes `claude plugin update nsolid-plugin@ --scope ` with a fixed executable and argument array **And** verifies the native update command succeeded +**And** verifies the installed payload matches the planned commit/content identity rather than version alone **And** reports `/reload-plugins` or restart guidance **And** does not run the fallback installer @@ -175,7 +180,7 @@ The updater SHALL preserve native/package ownership and delegate each supported **And** creates a restrictive temporary backup of the exact plugin registration, enabled state, user-owned plugin fields, and cached installed payload **And** confirms replacement unless `--yes` was supplied **And** invokes `codex plugin remove nsolid-plugin@` followed by `codex plugin add nsolid-plugin@` with fixed argument arrays -**And** verifies the resulting local version/content matches the refreshed marketplace entry +**And** verifies the refreshed marketplace snapshot and resulting local payload match the planned commit and content digest **And** reapplies the prior enabled state and preserves unrelated Codex configuration **And** reports that a new Codex session is required **And** does not run the fallback installer @@ -187,7 +192,7 @@ The updater SHALL preserve native/package ownership and delegate each supported **Then** the updater restores the prior plugin registration, enabled state, user-owned fields, and cached payload **And** preserves unrelated `~/.codex/config.toml` entries **And** reports whether rollback succeeded -**And** exits non-zero +**And** exits with code `1` **And** provides the exact detected plugin remove/add commands for manual recovery #### Scenario: Update Pi package-owned skills @@ -199,7 +204,11 @@ The updater SHALL preserve native/package ownership and delegate each supported **And** the plan displays whether user and/or project package caches will be updated and displays the project root when applicable **And** a user-only target invokes `pi update npm:nsolid-pi-plugin --no-approve` **And** a target containing the detected project scope invokes `pi update npm:nsolid-pi-plugin --approve` only after the plan is approved +**And** that command's `cwd` is the canonical project root captured by inventory +**And** immediately before execution the strategy revalidates the project directory identity, effective settings entries, scopes, canonical package source, and affected cache roots against the approved plan +**And** any drift fails without invoking `pi update` **And** verifies every affected package cache contains `nsolid-pi-plugin` at a valid version no older than the registry version observed during planning +**And** verifies the resulting caches retain the planned registry provenance and integrity/content evidence **And** reports the actual installed version, accepting a newer version published while Pi's native unpinned update was running **And** does not copy Pi skills into user-level skill directories **And** reports `/reload` or restart guidance @@ -227,17 +236,29 @@ The updater SHALL preserve native/package ownership and delegate each supported **Given** N|Solid is tracked as a direct OpenCode installation, rather than as an OpenCode npm/local plugin, or another target uses the tracked N|Solid fallback installer **When** its update strategy runs -**Then** it resolves and freezes the exact stable `nsolid-plugin` registry version in the plan +**Then** it resolves and freezes the exact `nsolid-plugin` registry, tarball, stable version, and integrity in the plan **And** requires an available supported package executor -**And** snapshots the target's tracked NodeSource-owned skill directories, affected MCP configuration, and complete tracking state before replacement -**And** invokes either `npm exec --yes --package=nsolid-plugin@ -- nsolid-plugin-refresh-owned --harness ` or `pnpm --package=nsolid-plugin@ dlx nsolid-plugin-refresh-owned --harness ` with fixed argument arrays +**And** the parent creates and durably records a complete snapshot of the selected installation's owned skill/link paths, owned MCP fields, and tracking state before launching a package executor +**And** invokes `nsolid-plugin-refresh-owned --transaction ` from the integrity-verified local tarball with a fixed argument array **And** runs the package executor from a restrictive temporary working directory where a workspace-local `nsolid-plugin` binary cannot shadow the resolved payload -**And** the internal refresh binary refuses absent, ambiguous, or untracked ownership and does not broaden the planned harness +**And** the parent manifest binds the exact `installationId`, harness, canonical paths, tracking path and digest, and field-level MCP ownership approved in the plan +**And** the internal refresh binary revalidates that identity and refuses absent, stale, sibling, broadened, ambiguous, or untracked ownership **And** does not invoke `opencode plugin` **And** completely replaces tracked skill directories, removes previously tracked skills absent from the new bundle, and merges only the new bundle's NodeSource MCP entries **And** preserves untracked/user-owned skill paths, unrelated MCP entries, other configuration, and valid credentials **And** validates installed skills, MCP entries, tracking paths, and `bundleVersion` before deleting the backup +Fallback mutation SHALL be authorized by an exact parent-owned installation manifest and SHALL remain recoverable without cooperation from the package-executor child. + +#### Scenario: Fallback tracking or ownership changes after planning + +**Given** a fallback plan and parent transaction manifest were approved +**And** the tracking file, an owned path, a shared-path membership, or an owned MCP field changes before the child starts mutation +**When** the internal refresh validates the manifest +**Then** it fails without mutating any installation +**And** it does not rediscover another installation from the harness name +**And** a user-modified MCP field or sibling installation remains unchanged + #### Scenario: No supported exact-package executor is available **Given** a tracked direct/fallback installation is updateable but neither `npm exec` nor `pnpm dlx` is available @@ -272,7 +293,24 @@ The updater SHALL preserve native/package ownership and delegate each supported **And** restores stale tracked assets removed during reconciliation **And** preserves unrelated OpenCode/fallback artifacts **And** reports whether rollback succeeded -**And** exits non-zero +**And** exits with code `1` + +#### Scenario: Fallback child terminates after mutation + +**Given** the parent durably recorded a complete snapshot and marked the fallback journal `mutating` +**When** npm, pnpm, or the internal refresh process times out, crashes, receives a signal, or exits without a structured rollback result after mutation began +**Then** the parent restores the selected installation from its own snapshot +**And** records whether parent-owned recovery succeeded +**And** retains an incomplete journal when automatic restoration cannot be proven complete +**And** exits with code `1` + +#### Scenario: Recover an interrupted fallback transaction on the next run + +**Given** a prior invocation left a non-committed durable fallback journal +**When** any later update invocation starts +**Then** recovery runs before new inventory or mutation +**And** restores and validates the exact recorded installation or reports a recovery failure +**And** no new update plan executes while unresolved recovery state remains #### Scenario: Update coexisting native and fallback installations @@ -291,6 +329,7 @@ The updater SHALL preserve native/package ownership and delegate each supported **And** the coordinator emits one non-mutating item with `ownership: none` and `source.kind: none` for the requested harness **And** the target result is `not-installed` **And** the command prints appropriate installation guidance +**And** a mutating invocation exits with code `2`, while a read-only check exits with code `0` #### Scenario: Required harness executable is missing @@ -303,7 +342,7 @@ The updater SHALL preserve native/package ownership and delegate each supported ### Requirement: Transactional Antigravity replacement -The Antigravity strategy SHALL back up and validate the staged NodeSource plugin because AGY has no native plugin-update command. +The Antigravity strategy SHALL install a commit-pinned source and back up and validate the staged NodeSource plugin because AGY has no native plugin-update command. #### Scenario: Update Antigravity native plugin @@ -313,8 +352,9 @@ The Antigravity strategy SHALL back up and validate the staged NodeSource plugin **Then** the layout is either `~/.gemini/config/plugins/nsolid-plugin` with `~/.gemini/config/import_manifest.json` or `~/.gemini/antigravity-cli/plugins/nsolid-plugin` with `~/.gemini/antigravity-cli/import_manifest.json` **And** it creates a temporary backup of the detected staged NodeSource plugin and matching N|Solid import-manifest entry **And** confirms replacement unless `--yes` was supplied -**And** invokes `agy plugin uninstall nsolid-plugin` followed by `agy plugin install https://github.com/NodeSource/nsolid-plugin.git` +**And** resolves the canonical repository to a full commit and invokes `agy plugin uninstall nsolid-plugin` followed by installation of that commit-pinned Git source **And** validates `plugin.json`, `bundle.json`, canonical skill presence, and the N|Solid entry in the detected matching import manifest +**And** verifies the staged payload matches the planned commit/content digest **And** removes the backup only after the new staged plugin and registration validate **And** preserves `~/.agents/.nodesource-auth.json` @@ -333,7 +373,7 @@ The Antigravity strategy SHALL back up and validate the staged NodeSource plugin **Then** the updater restores the previous staged plugin atomically where supported **And** restores the previous N|Solid entry in the matching detected import manifest while preserving unrelated imports **And** reports whether rollback succeeded -**And** exits non-zero +**And** exits with code `1` **And** provides a manual reinstall command ### Requirement: Deterministic multi-target orchestration @@ -358,9 +398,18 @@ The updater SHALL plan targets before mutation, execute them sequentially in det **When** one target fails **Then** remaining independent targets are attempted **And** the summary includes the failed target and actionable error -**And** the overall process exits non-zero +**And** the overall process exits with code `1` **And** no credential value appears in logs or JSON +#### Scenario: Update-all detects no targets + +**Given** no CLI or harness installation is detected +**When** the user runs `nsolid-plugin update --all` or `nsolid-plugin update --all --check` +**Then** no confirmation, child process, or filesystem mutation occurs +**And** the result is an explicit successful no-op with `results: []` and zero counts for every status +**And** human output reports that no targets were detected +**And** the process exits with code `0` + #### Scenario: Conflicting update scopes **Given** the user supplies both `--all` and `--harness` @@ -378,8 +427,19 @@ Update results SHALL support human-readable and machine-readable output without **When** an update or check completes **Then** standard output contains exactly one valid JSON document **And** progress and diagnostics are written to standard error +**And** the summary contains `exitCode` with the exact process code selected from `0`, `1`, or `2` **And** each result contains `installationId`, `target`, `ownership`, `status`, optional `currentVersion` and `latestVersion`, `changed`, optional restart guidance and rollback status, and sanitized errors +#### Scenario: Exit codes distinguish unavailable mutation from failure + +**Given** an update or check has completed +**When** the CLI maps its summary to a process exit code +**Then** code `0` represents completed work or an intentional informational no-op, including checks, `current`, `newer-than-registry`, `skipped`, and an empty `--all` +**And** code `1` represents an operational lookup, planning, execution, validation, rollback, or recovery failure +**And** code `2` represents a requested mutation that was unavailable without operational failure because approval was missing or its result was `not-installed`, `unsupported`, or mutation-blocking `unknown` +**And** code `1` takes precedence over code `2` for aggregate results +**And** JSON status data remains present so automation can distinguish individual results sharing an exit category + ### Requirement: Preserve existing installation behavior Update operations SHALL retain all existing setup, installation, authentication, backup, merge, tracking, and uninstall safety contracts. diff --git a/openspec/changes/add-update-flow/tasks.md b/openspec/changes/add-update-flow/tasks.md index 7f2220a..b79071f 100644 --- a/openspec/changes/add-update-flow/tasks.md +++ b/openspec/changes/add-update-flow/tasks.md @@ -9,10 +9,10 @@ ## Task 2: Add safe command execution and version sources -- [ ] **Description**: Implement the injected shell-free command runner, bounded/sanitized output, executable lookup, npm registry client, exact-version npm-exec/pnpm-dlx package executors, exact carried Claude/Codex marketplace version sources, and the fixed canonical GitHub-root Antigravity bundle source with explicit timeouts and validation. Never substitute the NodeSource marketplace for alternate, missing, stale, or unsupported marketplace evidence. +- [ ] **Description**: Implement the injected shell-free command runner, bounded/sanitized output, executable lookup, npm registry client, integrity-verified tarball execution, exact carried Claude/Codex marketplace sources resolved to immutable commits/content digests, and the canonical GitHub-root Antigravity source resolved to a full commit with explicit timeouts and validation. Bind lookup, execution, and post-update verification to the same npm registry/tarball/integrity or Git commit/content identity. Never substitute the NodeSource marketplace for alternate, missing, stale, or unsupported marketplace evidence. - **Depends on**: Task 1 - **Files**: `packages/core/src/update/command-runner.ts`, `packages/core/src/update/version-source.ts`, `packages/core/test/unit/update/command-runner.test.ts`, `packages/core/test/unit/update/version-source.test.ts` -- **Testing**: Mock success, timeout, missing executable, invalid JSON/version, non-zero exit, oversized output, alternate Git repository/ref/manifest sources, fresh and stale local snapshots, traversal-capable paths, and secret-bearing diagnostics. Verify all subprocess calls use `shell: false` and argument arrays, source credentials are redacted, and no canonical-source substitution occurs. References: Update Flow “Registry lookup fails,” “Preserve each detected native source identity,” “Required harness executable is missing,” and “Preserve credentials and user-owned configuration.” +- **Testing**: Mock success, timeout, missing executable, invalid JSON/version, non-zero exit, oversized output, alternate Git repository/ref/manifest sources, missing or moving refs, commit/content mismatch, alternate registries serving the same version with different bytes, tarball/integrity mismatch, fresh and stale local snapshots, traversal-capable paths, and secret-bearing diagnostics. Verify all subprocess calls use `shell: false` and argument arrays, source credentials are redacted, and no canonical-source substitution or ambient registry re-resolution occurs. References: Update Flow “Registry lookup fails,” “Preserve each detected native source identity,” “Required harness executable is missing,” and “Preserve credentials and user-owned configuration.” ## Task 3: Detect CLI installation ownership @@ -30,7 +30,7 @@ ## Task 5: Extend harness inventory and version evidence -- [ ] **Description**: Reuse native detection and fallback tracking to emit one installation record per native, fallback, or package-owned installation. Carry validated Claude plugin ID/marketplace/scope and Codex plugin ID/marketplace together with each registration's exact sanitized repository/ref/relative-manifest source or local-snapshot/freshness evidence; carry the effective Pi source in a discriminated user/project location that requires the project root whenever project scope is present; and carry the detected Antigravity staged-root/matching-manifest layout. Require path-level ownership evidence for direct/fallback updates, retain optional installed-version evidence, and add backward-compatible `bundleVersion` tracking. Do not collapse native and fallback records for one harness. +- [ ] **Description**: Reuse native detection and fallback tracking to emit one installation record per native, fallback, or package-owned installation. Carry validated Claude plugin ID/marketplace/scope and Codex plugin ID/marketplace together with each registration's exact sanitized repository/ref/relative-manifest source or local-snapshot/freshness evidence; carry the effective Pi source in a discriminated user/project location that requires the canonical project root and directory identity whenever project scope is present; and carry the detected Antigravity staged-root/matching-manifest layout. Require per-installation canonical skill/link paths and field-level MCP ownership evidence for direct/fallback updates, retain optional installed-version evidence, and add backward-compatible `bundleVersion` tracking. Do not collapse native and fallback records for one harness. - **Depends on**: Tasks 1–3 - **Files**: `packages/core/src/update/inventory.ts`, `packages/core/src/harnesses/*.ts`, `packages/core/src/skills/skill-tracker.ts`, related harness/tracker tests - **Testing**: Cover native, fallback, package-owned, coexisting native+fallback, alternate marketplace IDs and repositories, every Claude installation scope, unknown scope, missing/ambiguous/stale marketplace evidence without canonical fallback, Pi user-only/project-only/both scopes with required project roots, object-form filters, pinned/conflicting Pi entries, both Antigravity layout pairs, ambiguous layouts, missing/corrupt metadata, direct artifacts without ownership, legacy tracking without `bundleVersion`, and version-unknown states for every harness. References: Update Flow “Requested harness is not installed,” “Check every detected target,” “Update one installed native harness,” “Preserve each detected native source identity,” and “Preserve credentials and user-owned configuration.” @@ -44,14 +44,14 @@ ## Task 7: Implement Pi and fallback/OpenCode strategies -- [ ] **Description**: Add the package-owned Pi strategy for only the exact unpinned canonical npm identity. Coalesce matching user/project scopes into one command, use `--no-approve` for user-only and disclosed/confirmed `--approve` for project scope, preserve settings/filter objects, and reject local, Git, pinned, conflicting, or ambiguous entries without partial scope updates. Add a package-internal `nsolid-plugin-refresh-owned` binary and an exact-version npm-exec/pnpm-dlx fallback transaction that invokes it for one planned harness. The internal entrypoint performs bundle-aware ownership/collision preflight, snapshots tracked skills/config/tracking, fully replaces owned skill directories, removes stale tracked assets, validates `bundleVersion`, rejects untracked destinations, and rolls back all owned state on failure. Do not change the public `nsolid-plugin install` command or programmatic `install()` contract. +- [ ] **Description**: Add the package-owned Pi strategy for only the exact unpinned canonical npm identity. Coalesce matching user/project scopes into one command, use `--no-approve` for user-only and disclosed/confirmed `--approve` for project scope, set project commands to the captured canonical root, and revalidate directory identity/settings/source/cache roots immediately before execution. Add a package-internal `nsolid-plugin-refresh-owned` binary executed from an integrity-verified tarball. Before launching it, the parent creates a durable snapshot/journal and passes a transaction manifest binding installation ID, canonical paths, tracking digest, and field-level MCP ownership. The child refuses stale or broadened identity, and the parent restores or recovers interrupted mutation independently of child availability. Do not change the public `nsolid-plugin install` command or programmatic `install()` contract. - **Depends on**: Tasks 2 and 5 - **Files**: `packages/core/src/update/strategies/pi.ts`, `packages/core/src/update/strategies/fallback.ts`, `packages/core/src/update/fallback-transaction.ts`, `packages/core/src/update/refresh-owned-cli.ts`, `packages/core/package.json`, `packages/core/src/skills/skill-copier.ts`, `packages/core/src/skills/skill-tracker.ts`, `packages/core/src/mcp/mcp-config-writer.ts`, corresponding unit/integration tests -- **Testing**: Verify Pi user-only/project-only/both scopes, one invocation for duplicate canonical identity, exact `--approve`/`--no-approve`, unchanged source/filter/trust settings, rejection of non-canonical/conflicting sources, package-owned skill boundaries, an unchanged-too-old cache, and a newer version published during native execution. For OpenCode/fallback, cover deterministic npm-then-pnpm executor selection, exact immutable package version, the exact internal-binary command arrays, isolated temporary cwd with a conflicting local binary, complete replacement, stale tracked-skill removal, an untracked new-bundle destination collision, MCP merge, tracking/version update, missing ownership, missing executor, child/reconciliation/validation failures, rollback of every component, preserved credentials/user artifacts, no implicit install for an absent target, and regression coverage proving repeated public `install` behavior is unchanged. References: Update Flow “Update Pi package-owned skills,” “Same canonical Pi identity exists in both scopes,” “Reject a non-canonical Pi source,” “Update OpenCode or another fallback installation,” “No supported exact-package executor is available,” “Direct/fallback refresh cannot prove ownership,” “New fallback bundle collides with an untracked destination,” “OpenCode or fallback refresh fails,” “Requested harness is not installed,” and “Preserve the public install contract.” +- **Testing**: Verify Pi user-only/project-only/both scopes, exact planned `cwd`, root/settings replacement between plan and execution, one invocation for duplicate canonical identity, exact `--approve`/`--no-approve`, unchanged source/filter/trust settings, rejection of non-canonical/conflicting sources, package-owned skill boundaries, and registry/content postconditions. For OpenCode/fallback, cover integrity-verified execution, exact transaction-manifest command arrays, isolated temporary cwd, shared paths, user-modified MCP fields, tracking digest/path/installation changes after approval, child timeout/crash/signal after each mutation boundary, parent rollback, next-run journal recovery, incomplete recovery, complete replacement, stale removal, collisions, missing ownership/executor, preserved user artifacts, no implicit install, and unchanged public `install` behavior. References: the Pi and fallback scenarios in Update Flow. ## Task 8: Implement transactional Antigravity update -- [ ] **Description**: Detect exactly one supported Antigravity layout pair: shared `~/.gemini/config/{plugins,import_manifest.json}` or AGY CLI `~/.gemini/antigravity-cli/{plugins,import_manifest.json}`. Add restrictive temporary backup of the detected staged root and matching N|Solid manifest entry, confirmed uninstall/install, new-root plus registration validation, successful cleanup, and rollback restoration that preserves unrelated imports. Return `unsupported` without mutation for ambiguous or unmatched layouts. +- [ ] **Description**: Detect exactly one supported Antigravity layout pair and resolve the canonical repository to a full immutable commit/content identity used for both lookup and installation. Add restrictive temporary backup of the detected staged root and matching N|Solid manifest entry, confirmed commit-pinned uninstall/install, content plus registration validation, successful cleanup, and rollback restoration that preserves unrelated imports. Return `unsupported` without mutation when layout or immutable source binding is unavailable. - **Depends on**: Tasks 2 and 5 - **Files**: `packages/core/src/update/antigravity-transaction.ts`, `packages/core/src/update/strategies/antigravity.ts`, related unit/integration tests - **Testing**: Cover both supported staged-root/manifest pairs, both-present ambiguity, unmatched root/manifest, successful replacement, declined confirmation, uninstall failure, install failure, root/manifest validation failure, rollback success/failure for both components, cleanup, unrelated import preservation, and credential preservation. References: Update Flow “Update Antigravity native plugin,” “Antigravity layout is ambiguous or unsupported,” and “Antigravity reinstall fails.” @@ -61,14 +61,14 @@ - [ ] **Description**: Implement scope validation, one-plan-item-per-installation semantics, a synthetic non-mutating `none` item for an explicitly requested absent harness, complete ordered execute/rollback steps before confirmation, deterministic target/ownership ordering, check-only short circuit, plan confirmation, sequential execution, per-installation lookup/execution failure isolation, aggregation, and public `getVersionInfo()`, `checkUpdates()`, and `update()` exports. Represent lookup/validation failures as sanitized non-mutating plan items, convert them to failed results without execution, and reject any strategy execution that attempts an external command absent from its approved immutable plan. - **Depends on**: Tasks 4 and 6–8 - **Files**: `packages/core/src/update/coordinator.ts`, `packages/core/src/update/index.ts`, `packages/core/src/index.ts`, coordinator/API tests -- **Testing**: Cover CLI-only default, one harness selecting multiple installations, `--all`, `--all` plus harness rejection, check-only no-execute guarantee, multi-command Codex/Antigravity and transactional fallback plans, undisclosed-command rejection, newer-than-registry no-downgrade, unsupported check/update exit semantics, one lookup or execution failure with later success, coexisting native/fallback records, empty inventory, all status counts, and overall success/exit semantics. References: Update Flow “Registry lookup fails,” “Update every detected target,” “Update coexisting native and fallback installations,” “Do not downgrade a CLI newer than the registry,” “One target fails during update-all,” “Check every detected target,” and “Conflicting update scopes.” +- **Testing**: Cover CLI-only default, one harness selecting multiple installations, `--all`, `--all` plus harness rejection, check-only no-execute guarantee, multi-command Codex/Antigravity and parent-journaled fallback plans, undisclosed-command rejection, newer-than-registry no-downgrade, exact exit codes `0`/`1`/`2`, precedence in mixed results, not-installed, mutation-blocking unknown, one lookup or execution failure with later success, coexisting native/fallback records, explicit empty-inventory success, all status counts, and overall success/exit semantics. References: the orchestration and output scenarios in Update Flow. ## Task 10: Add CLI commands and output formatting - [ ] **Description**: Add `version` and `update` command parsing, bare `--version`, `--check`/`--all`, confirmation integration, complete ordered execute/rollback plan display, human-readable summaries, JSON-only stdout, stderr progress, help text, installation identifiers/source-safe display, and exit-code mapping. - **Depends on**: Task 9 - **Files**: `packages/core/src/cli.ts`, `packages/core/src/utils/format.ts` or a new update formatter, CLI help/unit/integration tests -- **Testing**: Spawn the CLI with every supported scope/output combination. Assert exact JSON parseability, `latestVersion` and `installationId` presence, complete multi-step plans before prompts, no ANSI in JSON, no mutation in check mode, prompts only when appropriate, redacted source/environment values, sanitized errors, bare `--version` parity, no downgrade for `newer-than-registry`, informational exit zero for successful checks, and non-zero mutable exits for unsupported/failure results. References: Update Flow “Structured update output,” “Update every detected target,” “Report versions with the conventional flag,” “Do not downgrade a CLI newer than the registry,” and “Non-interactive CLI update.” +- **Testing**: Spawn the CLI with every supported scope/output combination. Assert exact JSON parseability including `exitCode`, complete plans before prompts, no ANSI in JSON, no mutation in check mode, prompts only when appropriate, redaction, bare `--version` parity, and exact process/summary codes: `0` for completed or informational no-op, `1` for operational failure, and `2` for unavailable mutation or missing approval, including precedence and empty `--all`. References: Update Flow “Structured update output,” “Update every detected target,” “Report versions with the conventional flag,” “Do not downgrade a CLI newer than the registry,” and “Non-interactive CLI update.” ## Task 11: Add atomic release preparation @@ -79,10 +79,10 @@ ## Task 12: Add release drift and payload checks -- [ ] **Description**: Implement `release:check`, including source/package equality, generated artifact checks, exact mismatch reporting, and cleanup-state validation. When and only when `--release` is present, compare the specification's explicit payload allowlist with the latest semantic-version tag and validate that payload changes have an update-visible version. +- [ ] **Description**: Implement `release:check`, including source/package equality, generated artifact checks, exact mismatch reporting, and cleanup-state validation. When and only when `--release` is present, select the specification's highest eligible local semantic-version tag by name, peeled commit, and `HEAD` ancestry; then compare the complete published-payload allowlist and validate that payload changes have an update-visible version. - **Depends on**: Task 11 - **Files**: `scripts/check-release-version.mjs`, `package.json`, script fixture tests -- **Testing**: Introduce drift independently in every controlled file, stale generated output, unchanged version with changes in each payload allowlist category, malformed/missing tag state, and materialized package skills. Verify normal and `--release` check modes never repair. References: Release Versioning “Check synchronized release versions,” “Release version drift is detected,” and “Skill changes retain the previous release version.” +- **Testing**: Introduce drift independently in every controlled file, stale generated output, unchanged version with changes in each payload allowlist category including `packages/core/src/**` and `packages/pi-plugin/index.js`, committed/staged/unstaged/untracked payload changes, and materialized package skills. Cover `X.Y.Z`/`vX.Y.Z`, lightweight/annotated tags, non-semantic tags, non-ancestor tags, duplicate-version ambiguity, missing/malformed-only local tags, remote-only tags, and shallow history. Verify deterministic highest-eligible selection and that normal and `--release` modes never repair. References: all Release Versioning baseline and payload scenarios. ## Task 13: Add end-to-end update regression coverage