diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index 6edc498..580db7b 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -38,7 +38,7 @@ jobs: fetch-depth: 0 - name: Install Nix - uses: DeterminateSystems/determinate-nix-action@v3.17.1 + uses: DeterminateSystems/determinate-nix-action@v3.17.3 - name: Verify checked-in release version parity shell: bash diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index cdb5b87..a5364bc 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -39,7 +39,7 @@ jobs: fetch-depth: 0 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 20 registry-url: https://registry.npmjs.org diff --git a/.github/workflows/release-sce-linux-arm.yml b/.github/workflows/release-sce-linux-arm.yml index 00827c0..37d4c07 100644 --- a/.github/workflows/release-sce-linux-arm.yml +++ b/.github/workflows/release-sce-linux-arm.yml @@ -23,7 +23,7 @@ jobs: ref: ${{ inputs.release_ref }} - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v19 + uses: DeterminateSystems/nix-installer-action@v22 - name: Build canonical Linux ARM CLI release artifacts run: | @@ -32,7 +32,7 @@ jobs: --out-dir dist - name: Upload Linux ARM CLI release artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sce-release-aarch64-unknown-linux-gnu path: dist/ diff --git a/.github/workflows/release-sce-linux.yml b/.github/workflows/release-sce-linux.yml index 648aa58..2c154b2 100644 --- a/.github/workflows/release-sce-linux.yml +++ b/.github/workflows/release-sce-linux.yml @@ -23,7 +23,7 @@ jobs: ref: ${{ inputs.release_ref }} - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v19 + uses: DeterminateSystems/nix-installer-action@v22 - name: Build canonical Linux CLI release artifacts run: | @@ -32,7 +32,7 @@ jobs: --out-dir dist - name: Upload Linux CLI release artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sce-release-x86_64-unknown-linux-gnu path: dist/ diff --git a/.github/workflows/release-sce-macos-arm.yml b/.github/workflows/release-sce-macos-arm.yml index addc303..84c0940 100644 --- a/.github/workflows/release-sce-macos-arm.yml +++ b/.github/workflows/release-sce-macos-arm.yml @@ -23,7 +23,7 @@ jobs: ref: ${{ inputs.release_ref }} - name: Install Nix - uses: DeterminateSystems/determinate-nix-action@v3.17.1 + uses: DeterminateSystems/determinate-nix-action@v3.17.3 - name: Build canonical macOS ARM CLI release artifacts run: | @@ -32,7 +32,7 @@ jobs: --out-dir dist - name: Upload macOS ARM CLI release artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: sce-release-aarch64-apple-darwin path: dist/ diff --git a/.github/workflows/release-sce.yml b/.github/workflows/release-sce.yml index e9bc39b..347a516 100644 --- a/.github/workflows/release-sce.yml +++ b/.github/workflows/release-sce.yml @@ -165,10 +165,10 @@ jobs: ref: ${{ needs.resolve-release.outputs.tag }} - name: Install Nix - uses: DeterminateSystems/determinate-nix-action@v3.17.1 + uses: DeterminateSystems/determinate-nix-action@v3.17.3 - name: Download CLI release artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: sce-release-* path: dist/cli diff --git a/.gitignore b/.gitignore index 0e5b494..86f47c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .direnv/ node_modules/ cli/target/ +integrations/**/target/ cli/assets/generated/ result* dist/ diff --git a/context/architecture.md b/context/architecture.md index 2bebf40..980070d 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -27,7 +27,7 @@ Current target renderer helper modules: - `config/pkl/renderers/metadata-coverage-check.pkl` - `config/pkl/generate.pkl` (single multi-file generation entrypoint) - `config/pkl/check-generated.sh` (dev-shell integration stale-output detection against committed generated files) -- `nix flake check` / `checks..{cli-tests,cli-clippy,cli-fmt,npm-bun-tests,npm-biome-check,npm-biome-format,config-lib-bun-tests,config-lib-biome-check,config-lib-biome-format}` (root-flake check derivations for the current CLI + JS validation inventory) +- `nix flake check` / `checks..{cli-tests,cli-clippy,cli-fmt,integrations-install-tests,integrations-install-clippy,integrations-install-fmt,npm-bun-tests,npm-biome-check,npm-biome-format,config-lib-bun-tests,config-lib-biome-check,config-lib-biome-format}` (root-flake check derivations for the current CLI, `integrations/install` runner, and JS validation inventory) - `.github/workflows/pkl-generated-parity.yml` (CI wrapper that runs the parity check for pushes to `main` and pull requests targeting `main`) The scaffold provides stable canonical content-unit identifiers and reusable target-agnostic text primitives for all planned authored generated classes (agents, commands, skills, shared runtime assets, OpenCode plugin entrypoints, and generated OpenCode package manifests). @@ -69,6 +69,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - The current implemented first-wave install/distribution surface for the `sce` CLI is limited to repo-flake Nix, Cargo, and npm; `Homebrew` is deferred from the active implementation stage. - Nix-managed build/release entrypoints are the source of truth for first-wave build outputs and release automation. +- The root flake now also owns an opt-in install-channel integration-test app surface, `apps.install-channel-integration-tests`, which provides the execution contract for npm/Bun/Cargo install verification without enrolling that heavier coverage in default `checks.`. - Repo-root `.version` is the canonical checked-in release version authority across GitHub Releases, Cargo publication, and npm publication. - GitHub Releases are the canonical publication surface for release archives, checksums, and release-manifest assets. - Cargo/crates.io and npm registry publication belong to separate downstream publish stages that consume already-versioned checked-in package metadata rather than inventing release versions during workflow execution. @@ -101,7 +102,7 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/services/sync.rs` runs the local adapter through a lazily initialized shared tokio current-thread runtime, applies bounded resilience policy to the local smoke operation, and composes a placeholder cloud-sync abstraction (`CloudSyncGateway`) so local Turso validation and deferred cloud planning remain separated. - `cli/src/services/` contains module boundaries for config, setup, doctor, hooks, sync, version, completion, and local DB adapters with explicit trait seams for future implementations. - `cli/README.md` is the crate-local onboarding and usage source of truth for placeholder behavior, safety limitations, and roadmap mapping back to service contracts. -- `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.93.1.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with a filtered repo-root source that preserves the Cargo tree plus `cli/assets/hooks`, then injects generated OpenCode/Claude config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets, runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed derivations so package and check surfaces share the same toolchain, source-root handling, and dependency-artifact cache path where applicable, and now also exposes directory-scoped JS validation derivations for both `npm/` and `config/lib/bash-policy-plugin/`, split into Bun tests, Biome lint/check, and Biome format verification per target. `.github/workflows/publish-crates.yml` follows the same asset-preparation rule but runs Cargo packaging from a temporary clean repository copy so crates.io publish no longer needs `--allow-dirty`. +- `flake.nix` applies `rust-overlay` (`oxalica/rust-overlay`) to nixpkgs, pins `rust-bin.stable.1.93.1.default` with `rustfmt` + `clippy`, reads the package/check version from repo-root `.version`, builds `packages.sce` through Crane (`buildDepsOnly` -> `buildPackage`) with a filtered repo-root source that preserves the Cargo tree plus `cli/assets/hooks`, then injects generated OpenCode/Claude config payloads and schema inputs into a temporary `cli/assets/generated/` mirror during derivation unpack so `cli/build.rs` can package the crate without requiring committed generated crate assets, runs `cli-tests`, `cli-clippy`, and `cli-fmt` plus the dedicated `integrations-install-tests`, `integrations-install-clippy`, and `integrations-install-fmt` derivations through Crane-backed paths so both Rust crates have first-class default-flake verification, exposes directory-scoped JS validation derivations for both `npm/` and `config/lib/bash-policy-plugin/`, and also exposes the non-default `apps.install-channel-integration-tests` flake app for install-channel integration coverage outside the default check set. `.github/workflows/publish-crates.yml` follows the same asset-preparation rule but runs Cargo packaging from a temporary clean repository copy so crates.io publish no longer needs `--allow-dirty`. - `flake.nix` exposes release install/run surfaces as `packages.sce` (`packages.default = packages.sce`) plus `apps.sce` and `apps.default`, all targeting `${packages.sce}/bin/sce`; this keeps repo-local and remote flake run/install flows (`nix run .`, `nix run github:crocoder-dev/shared-context-engineering`, `nix profile install github:crocoder-dev/shared-context-engineering`) aligned to the same packaged CLI output. - `biome.json` at the repository root is the canonical Biome configuration for the current JS tooling slice and deliberately scopes coverage to `npm/**` plus `config/lib/bash-policy-plugin/**` while excluding package-local `node_modules/**`; `flake.nix` exposes Biome through the default dev shell rather than through package-local installs. - `cli/Cargo.toml` now keeps crates.io publication-ready package metadata for the `sce` crate, and `cli/README.md` is the Cargo install surface for crates.io (`cargo install sce`), git (`cargo install --git https://github.com/crocoder-dev/shared-context-engineering sce --locked`), and local checkout (`cargo install --path cli --locked`) guidance. Tokio remains intentionally constrained (`default-features = false`) with current-thread runtime usage plus timer-backed bounded resilience wrappers for retry/timeout behavior. diff --git a/context/context-map.md b/context/context-map.md index 6444f3a..a616bb3 100644 --- a/context/context-map.md +++ b/context/context-map.md @@ -42,6 +42,7 @@ Feature/domain context: - `context/sce/bash-tool-policy-enforcement-contract.md` (approved bash-tool blocking contract plus the implementation target for generated OpenCode enforcement, including config schema, argv-prefix matching, fixed preset catalog/messages, and precedence rules) - `context/sce/generated-opencode-plugin-registration.md` (current generated OpenCode plugin-registration contract, canonical Pkl ownership, generated manifest/plugin paths, and TypeScript source ownership; Claude bash-policy enforcement has been removed from generated outputs) - `context/sce/cli-first-install-channels-contract.md` (current first-wave `sce` install/distribution contract covering supported channels, canonical naming, `.version` release authority, and Nix-owned build policy) +- `context/sce/optional-install-channel-integration-test-entrypoint.md` (current opt-in flake app contract for install-channel integration coverage, including thin flake delegation to the Rust runner, shared harness ownership, real npm+Bun+Cargo install flows, channel selector semantics, and the explicit non-default execution boundary) - `context/sce/cli-release-artifact-contract.md` (shared `sce` release artifact naming, checksum/manifest outputs, GitHub Releases as the canonical artifact publication surface, and the current three-target Linux/macOS release workflow topology) - `context/sce/cli-npm-distribution-contract.md` (implemented `sce` npm launcher package, release-manifest/checksum-verified native binary install flow, the supported darwin/arm64 plus linux x64+arm64 npm platform matrix, and dedicated `.github/workflows/publish-npm.yml` downstream npm publish-stage contract) - `context/sce/cli-cargo-distribution-contract.md` (implemented `sce` Cargo publication posture plus supported crates.io, git, and local checkout install guidance, dedicated crates.io publish workflow, and ephemeral crate-local generated-asset mirror requirement for published builds) diff --git a/context/glossary.md b/context/glossary.md index 9787baa..e7d75c7 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -10,7 +10,7 @@ - `canonical OpenCode plugin registration source`: Shared Pkl-authored plugin-registration definition in `config/pkl/base/opencode.pkl`, re-exported from `config/pkl/renderers/common.pkl` as the canonical plugin list/path JSON consumed by OpenCode renderers before they emit generated `opencode.json` manifests. - `generated OpenCode plugin registration contract`: Current generated-config contract where `config/.opencode/opencode.json` and `config/automated/.opencode/opencode.json` serialize the OpenCode `plugin` field from canonical Pkl sources for SCE-managed plugins only; the current registered path is `./plugins/sce-bash-policy.ts`. Claude does not use an OpenCode-style plugin manifest; bash-policy enforcement for Claude has been removed from generated outputs. - `root Biome contract`: Repository-root formatting/linting contract owned by `biome.json`, currently scoped only to `npm/**` and `config/lib/bash-policy-plugin/**` with package-local `node_modules/**` excluded; the canonical execution path is the root Nix dev shell (`nix develop -c biome ...`). -- `cli flake checks`: Check derivations in root `flake.nix` (`checks..cli-tests`, `cli-clippy`, `cli-fmt`) plus `pkl-parity`, split `npm/` JS checks (`npm-bun-tests`, `npm-biome-check`, `npm-biome-format`), and split `config/lib/bash-policy-plugin/` JS checks (`config-lib-bun-tests`, `config-lib-biome-check`, `config-lib-biome-format`); invoked via `nix flake check` at repo root. +- `cli flake checks`: Check derivations in root `flake.nix` (`checks..cli-tests`, `cli-clippy`, `cli-fmt`), dedicated `integrations/install` runner checks (`integrations-install-tests`, `integrations-install-clippy`, `integrations-install-fmt`), plus `pkl-parity`, split `npm/` JS checks (`npm-bun-tests`, `npm-biome-check`, `npm-biome-format`), and split `config/lib/bash-policy-plugin/` JS checks (`config-lib-bun-tests`, `config-lib-biome-check`, `config-lib-biome-format`); invoked via `nix flake check` at repo root. - `npm JS flake checks`: The current `npm/` validation slice exposed by root `flake.nix`: `npm-bun-tests` runs only `bun test ./test/*.test.js`, `npm-biome-check` runs only Biome lint/check with formatter verification disabled, and `npm-biome-format` runs only Biome format verification with linter checks disabled. - `config-lib JS flake checks`: The current `config/lib/bash-policy-plugin/` validation slice exposed by root `flake.nix`: `config-lib-bun-tests` runs `bun test`, `config-lib-biome-check` runs Biome lint/check with formatter verification disabled, and `config-lib-biome-format` runs Biome format verification with linter checks disabled, all scoped to `config/lib/bash-policy-plugin/` only. - `cli rust overlay toolchain`: Toolchain contract in root `flake.nix` that applies `rust-overlay.overlays.default`, pins `rust-bin.stable.1.93.1.default` with `rustfmt` + `clippy`, uses that toolchain across both Crane package and check derivations, and keeps toolchain selection explicit rather than inheriting nixpkgs defaults. @@ -122,3 +122,4 @@ - `publish-crates workflow`: Dedicated crates.io publish automation in `.github/workflows/publish-crates.yml` that runs after a GitHub release is published (or by manual dispatch), validates `.version`, `cli/Cargo.toml`, and the requested release tag remain aligned, supports a dry-run validation path, and requires `CARGO_REGISTRY_TOKEN` for real publication. - `clean publish workspace`: Temporary `.git`-free copy of the checked-out repository used by the crates.io publish workflow so ephemeral generated crate assets can be prepared locally without forcing Cargo publish to run with `--allow-dirty`. - `cli generated asset mirror`: Ephemeral crate-local mirror under `cli/assets/generated/` created from canonical `config/` outputs just before Cargo packaging/builds so `cli/build.rs`, `cli/src/services/setup.rs`, and `cli/src/services/config.rs` can compile and package correctly without committing generated crate assets. +- `install-channel integration-test entrypoint`: Optional root-flake app exposed as `nix run .#install-channel-integration-tests -- --channel ` that reserves a stable Nix-owned execution path for heavier install-and-run CLI channel coverage without adding that coverage to default `nix flake check`; current Rust-runner coverage includes real npm, Bun, and Cargo install-and-verify flows for all three first-wave channels. diff --git a/context/overview.md b/context/overview.md index 4e39cd6..635d8c7 100644 --- a/context/overview.md +++ b/context/overview.md @@ -26,13 +26,14 @@ The `sync` placeholder performs a local Turso smoke check through a lazily initi The repository-root flake (`flake.nix`) now applies a Rust overlay-backed stable toolchain pinned to `1.93.1` (with `rustfmt` and `clippy`), reads package/check version from the repo-root `.version` file, builds `packages.sce` through a Crane `buildDepsOnly` + `buildPackage` pipeline with filtered package sources for the Cargo tree plus required embedded config/assets, and runs `cli-tests`, `cli-clippy`, and `cli-fmt` through Crane-backed check derivations (`cargoTest`, `cargoClippy`, `cargoFmt`) that reuse the same filtered source/toolchain setup. The root flake also exposes release install/run outputs directly as `packages.sce` (with `packages.default = packages.sce`) plus `apps.sce` and `apps.default`, so `nix build .#default`, `nix run . -- --help`, `nix run .#sce -- --help`, and `nix profile install github:crocoder-dev/shared-context-engineering` all target the packaged `sce` binary through the same flake-owned entrypoints. The CLI Cargo package metadata now includes crates.io publication-ready fields with crate-local install guidance in `cli/README.md`; supported Cargo install paths are `cargo install sce`, `cargo install --git https://github.com/crocoder-dev/shared-context-engineering sce --locked`, and local `cargo install --path cli --locked`. The crate also keeps `cargo clippy --manifest-path cli/Cargo.toml` warnings-denied through `cli/Cargo.toml` lint configuration, so an extra `-- -D warnings` flag is redundant. -The repository-root flake is now the single Nix entrypoint for both repo tooling and CLI packaging/checks, so root-level `nix flake check` evaluates the Crane-backed CLI checks (`cli-tests`, `cli-clippy`, `cli-fmt`) plus six split JavaScript check derivations: `npm-bun-tests`, `npm-biome-check`, `npm-biome-format`, `config-lib-bun-tests`, `config-lib-biome-check`, and `config-lib-biome-format`, without nested-flake indirection. For Cargo packaging/builds, the crate now compiles against a temporary `cli/assets/generated/` mirror prepared from canonical `config/` outputs during Nix builds and crates.io publish runs rather than from a committed crate-local snapshot. +The repository-root flake is now the single Nix entrypoint for both repo tooling and CLI packaging/checks, so root-level `nix flake check` evaluates the Crane-backed CLI checks (`cli-tests`, `cli-clippy`, `cli-fmt`), the dedicated `integrations/install` runner checks (`integrations-install-tests`, `integrations-install-clippy`, `integrations-install-fmt`), plus six split JavaScript check derivations: `npm-bun-tests`, `npm-biome-check`, `npm-biome-format`, `config-lib-bun-tests`, `config-lib-biome-check`, and `config-lib-biome-format`, without nested-flake indirection. For Cargo packaging/builds, the crate now compiles against a temporary `cli/assets/generated/` mirror prepared from canonical `config/` outputs during Nix builds and crates.io publish runs rather than from a committed crate-local snapshot. Local developer Nix tuning guidance now lives in `AGENTS.md`, including optional user-level `~/.config/nix/nix.conf` recommendations for `max-jobs` and `cores` plus an explicit system-level-only note for `auto-optimise-store`. The Pkl authoring layer owns generated OpenCode plugin registration for SCE-managed plugins: `config/pkl/base/opencode.pkl` defines the canonical plugin entries, `config/pkl/renderers/common.pkl` re-exports the shared plugin list for renderer use, and generated `config/.opencode/opencode.json` plus `config/automated/.opencode/opencode.json` register `./plugins/sce-bash-policy.ts` through OpenCode's `plugin` field. Claude does not use an OpenCode-style plugin manifest; bash-policy enforcement for Claude has been removed from generated outputs. The current first-wave CLI install/distribution contract is now defined for `sce`: the active implemented channel set is repo-flake Nix, Cargo, and npm; `Homebrew` is deferred from the current implementation stage. Nix-managed build/release entrypoints are the source of truth for this rollout, npm consumes Nix-produced release artifacts, and repo-root `.version` is the canonical checked-in release version source that release packaging and downstream Cargo/npm publication must match. The shared release artifact foundation is now implemented through root-flake apps `release-artifacts` and `release-manifest`, which emit canonical `sce-v-.tar.gz` archives, SHA-256 checksum files, merged manifest outputs, and a detached `sce-v-release-manifest.json.sig` produced from a non-repo private signing key; the npm distribution surface is now implemented as a checked-in `npm/` launcher package plus root-flake `release-npm-package`, which packs `sce-v-npm.tgz`, refuses mismatched checked-in package metadata, and installs the native CLI by downloading the release manifest plus detached signature, verifying the manifest with the bundled npm public key, and only then checksum-verifying the matching GitHub release archive at npm `postinstall` time. GitHub Releases are the canonical publication surface for those release artifacts, while crates.io and npm registry publication are separate non-bumping publish stages under the approved release topology. GitHub CLI release automation now lives in dedicated `release-sce*.yml` workflows split by Linux, Linux ARM, and macOS ARM, and `.github/workflows/release-sce.yml` now orchestrates those three reusable platform lanes before assembling the signed release manifest, npm tarball, and GitHub release payload. The orchestrator now tags/releases the checked-in `.version` directly and rejects version mismatches instead of generating a new semver during workflow execution, `.github/workflows/publish-crates.yml` is the dedicated crates.io publish stage triggered from a published GitHub release or manual dispatch with the same `.version`/tag/Cargo parity checks and a clean temporary repo copy for Cargo packaging, and `release-agents.yml` remains Tessl-only. The current supported automated release target matrix is `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, and `aarch64-apple-darwin`; npm launcher platform support remains a separate current-state surface documented in the npm distribution contract and launcher code. The downstream publish-stage implementation is now complete for both registries: `.github/workflows/publish-crates.yml` publishes the checked-in crate version after `.version`/tag/Cargo parity checks, and `.github/workflows/publish-npm.yml` publishes the checked-in npm package after `.version`/tag/npm parity checks plus verification of the canonical `sce-v-npm.tgz` GitHub release asset. The repository root now also owns the canonical Biome contract for the current JavaScript tooling slice: `biome.json` scopes formatting/linting to `npm/` and `config/lib/bash-policy-plugin/` only, and the root Nix dev shell provides the `biome` binary so contributors do not need a host-installed formatter/linter for those areas. +The root flake now also exposes an explicit opt-in install-channel integration-test app, `nix run .#install-channel-integration-tests -- --channel `, which remains outside the default `nix flake check` path while the Rust runner now executes real npm, Bun, and Cargo install-and-verify flows for all three first-wave channels. Shared Context Plan and Shared Context Code remain separate agent roles by design; planning (`/change-to-plan`) and implementation (`/next-task`) stay split while shared baseline guidance is deduplicated via canonical skill-owned contracts. Their shared baseline doctrine (core principles, `context/` authority, and quality posture) is defined once as canonical snippets in `config/pkl/base/shared-content.pkl` and composed into both agent bodies during generation. The `/next-task` command body is intentionally thin orchestration: readiness gating + phase sequencing are command-owned, while detailed implementation/context-sync contracts are skill-owned (`sce-plan-review`, `sce-task-execution`, `sce-context-sync`). The generated OpenCode command doc now also emits machine-readable frontmatter for this chain via `entry-skill: sce-plan-review` and an ordered `skills` list. diff --git a/context/patterns.md b/context/patterns.md index 58c2afd..478943f 100644 --- a/context/patterns.md +++ b/context/patterns.md @@ -24,6 +24,7 @@ - Expose operational workflows as flake apps so commands are stable and system-mapped across supported `flake-utils` default systems. - Current repo command contracts: - For flake app outputs, include `meta.description` so `nix flake check` app validation stays warning-free. +- When install/integration coverage is heavier than the default repository validation baseline, expose it as an explicit opt-in flake app instead of adding it to `checks.` prematurely. ## First-wave install/distribution rollout @@ -138,7 +139,7 @@ - For hosted rewrite mapping seams, resolve candidates deterministically in strict precedence order (patch-id exact, then range-diff score, then fuzzy score), classify top-score ties as `ambiguous`, enforce low-confidence unresolved behavior below `0.60`, and preserve stable outcome ordering via canonical candidate SHA sorting. - For hosted reconciliation observability, publish run-level mapped/unmapped counts, confidence histogram buckets, runtime timing, and normalized error-class labels so retry/quality drift can be monitored without requiring a full dashboard surface. - Keep crate-local onboarding docs in `cli/README.md` and sanity-check command examples against actual `sce` output whenever command messaging changes. -- Keep CLI Rust verification in flake checks under stable named derivations re-exported by the root flake: `checks..cli-tests`, `checks..cli-clippy`, and `checks..cli-fmt`. +- Keep Rust verification in flake checks under stable named derivations re-exported by the root flake: `checks..cli-tests`, `checks..cli-clippy`, `checks..cli-fmt`, `checks..integrations-install-tests`, `checks..integrations-install-clippy`, and `checks..integrations-install-fmt`. - In `flake.nix`, select the Rust toolchain via an explicit Rust overlay (`rust-overlay`) and thread that toolchain through Crane package/check derivations so CLI builds and checks do not rely on implicit nixpkgs Rust defaults. - For installable CLI release surfaces in the root flake, expose an explicit named package plus default alias (`packages.sce` and `packages.default = packages.sce`) and pair it with a runnable app output (`apps.sce`) that points to the packaged binary path. - For root-flake CLI release metadata, source the package/check version from repo-root `.version` and trim it at eval time so packaged outputs stay aligned without hardcoded semver strings in `flake.nix`. diff --git a/context/plans/doctor-human-text-integration-audit.md b/context/plans/doctor-human-text-integration-audit.md deleted file mode 100644 index ee5fc50..0000000 --- a/context/plans/doctor-human-text-integration-audit.md +++ /dev/null @@ -1,141 +0,0 @@ -# Plan: doctor-human-text-integration-audit - -## Change summary - -Update the human-facing `sce doctor` text output so it renders the approved sectioned layout, reports repo-root integration health by presence only, prints bracketed status tokens (`[PASS]`, `[FAIL]`, `[MISS]`), colorizes `[PASS]` green plus `[MISS]`/`[FAIL]` red in human text mode, and adopts the simplified label/path presentation shown by the approved target example. Keep JSON output and current `--fix` behavior unchanged. - -## Success criteria - -- `sce doctor` text output uses the approved human-only layout with these sections in order: `Environment`, `Configuration`, `Repository`, `Git Hooks`, `Integrations`. -- The header line renders as `SCE doctor diagnose` in diagnose mode, not `SCE doctor (diagnose) PASS`. -- Human text statuses use the exact bracketed vocabulary `[PASS]`, `[FAIL]`, and `[MISS]`. -- Status meaning is fixed as: `[PASS]` = healthy, `[FAIL]` = SCE will not work unless fixed, `[MISS]` = required file is missing. -- Human text renders `[PASS]` in green and `[MISS]`/`[FAIL]` in red when color output is enabled by the shared styling policy; non-color environments still render the exact bracketed tokens without ANSI noise. -- Human text rows use the simplified `label (path)` form instead of the current `label: state (path)` form when a path is present. -- Environment/configuration rows drop redundant state words such as `present` and `expected` from the human text line when the status token already communicates health. -- Repository rows use the simplified labels `Repository` and `Hooks`; the current split `Repository root`, `Hooks path source`, and `Effective hooks directory` wording does not remain in the approved text output. -- Text-mode integrations use exactly these groups: `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, `OpenCode skills`. -- Integration checks inspect repo-root installed artifacts only and validate file presence only; they do not inspect file contents. -- A single missing file under any integration group causes that group to render `[FAIL]`, with missing child rows rendered as `[MISS]`. -- Integration parent rows render only the group name in healthy cases; they do not append prose such as `all required files present`. -- Integration child rows render as `[STATUS] relative/path (absolute/path)` in text mode. -- Git hook text output is simplified to top-level hook presence rows only; no nested text rows for `content` or `executable` remain in the human output. -- Existing JSON output shape and semantics remain unchanged. -- Existing `sce doctor --fix` behavior remains unchanged. - -## Constraints and non-goals - -- In scope: human text rendering, text-only status classification/styling for the approved sections, header/label/path formatting cleanup, repo-root integration presence inventory, and regression coverage for unchanged JSON/`--fix` behavior. -- Out of scope: changing JSON output, broadening `--fix`, adding content-drift inspection for integrations, changing hook repair semantics, or introducing new integration group names. -- Repo-root artifacts are the only source of truth for integration checks in this change; generated `config/.opencode/**` trees are not inspected by doctor for this task. -- For `agents`, `commands`, and `skills`, doctor should treat the installed repo-root trees as required inventory and fail the group if any expected installed file is missing. -- Assumption: the implementation will derive the required integration inventory from the installed repo-root artifact trees and/or the existing embedded setup asset catalog without changing the JSON contract. -- Assumption: text colorization will reuse the existing shared styling service and its TTY/`NO_COLOR` behavior instead of introducing doctor-specific color toggles. -- Assumption: the simplified `Hooks` repository row should display the effective hooks directory path only, not the hooks-path source metadata, in human text mode. - -## Task stack - -- [x] T01: `Lock doctor text-mode contract for human layout and status rules` (status:done) - - Task ID: T01 - - Goal: Update the doctor service contract and rendering rules so text mode has the approved section order, exact `PASS`/`FAIL`/`MISS` vocabulary, simplified hook rows, and fixed parent/child status semantics. - - Boundaries (in/out of scope): In - text-mode layout rules, status meaning, parent-group failure rules, hook text simplification. Out - JSON shape changes, `--fix` behavior changes, new machine-readable fields. - - Done when: doctor text rendering requirements are encoded clearly enough that implementation can update the formatter without ambiguity, including the rule that integrations fail if any child file is missing and hook nested text rows are removed. - - Verification notes (commands or checks): Review `context/sce/agent-trace-hook-doctor.md` and implementation plan acceptance checks for exact section names, status vocabulary, and explicit JSON/`--fix` non-goals. - - Completed: 2026-04-02 - - Files changed: `context/sce/agent-trace-hook-doctor.md`, `context/context-map.md` - - Evidence: Contract file now captures exact human text section order, `PASS`/`FAIL`/`MISS` semantics, simplified hook rows, fixed integration group names, parent/child missing-file behavior, and explicit JSON/`--fix` non-goals. - - Notes: This task intentionally locks the downstream implementation contract without changing Rust runtime behavior; later tasks own formatter/runtime updates and regression coverage. - -- [x] T02: `Implement text-only doctor layout and status mapping` (status:done) - - Task ID: T02 - - Goal: Change the text formatter in `cli/src/services/doctor.rs` to emit the approved human layout, exact status labels, simplified hook rows, and final summary line while preserving current diagnosis data sources. - - Boundaries (in/out of scope): In - text rendering code, summary counting, section ordering, and text status mapping. Out - changing doctor JSON serialization, altering check execution logic unrelated to text-mode needs. - - Done when: text-mode `sce doctor` renders the approved section stack and footer, hook rows are top-level-only in text mode, and PASS/FAIL/MISS labels match the approved semantics without changing JSON output. - - Verification notes (commands or checks): Add/update doctor text rendering tests in the CLI test suite; verify expected snapshots/strings for section order, hook rows, and footer problem count. - - Completed: 2026-04-02 - - Files changed: `cli/src/services/doctor.rs`, `context/overview.md`, `context/glossary.md`, `context/cli/cli-command-surface.md`, `context/sce/agent-trace-hook-doctor.md` - - Evidence: Added formatter + unit coverage for section order, top-level-only hook rows, and `PASS`/`FAIL`/`MISS` labels; `nix flake check` passed. - - Notes: Text mode now uses the approved sectioned layout and summary footer while leaving JSON output and fix-mode semantics unchanged; repo-root multi-group integration inventory remains deferred to `T03`. - -- [x] T03: `Add repo-root integration presence inventory checks` (status:done) - - Task ID: T03 - - Goal: Teach doctor to inspect repo-root installed OpenCode integrations for `plugins`, `agents`, `commands`, and `skills`, reporting presence-only child rows and group failure when any required file is missing. - - Boundaries (in/out of scope): In - repo-root `.opencode/**` inventory resolution, exact four integration groups, missing-file detection, and text-mode child/group classification. Out - content validation, generated `config/.opencode/**` inspection, Claude assets, or new repair actions. - - Done when: doctor identifies missing repo-root installed integration files, renders missing children as `MISS`, renders the affected integration group as `FAIL`, and leaves groups as `PASS` only when every required file is present. - - Verification notes (commands or checks): Add focused service tests with temporary repo-root `.opencode/` fixtures covering all-present, single-missing, and multi-missing cases across all four integration groups. - - Completed: 2026-04-02 - - Files changed: `cli/src/services/doctor.rs` - - Evidence: Added repo-root `.opencode/` integration inventory + text rendering/tests for plugins, agents, commands, and skills; `nix flake check` passed. - - Notes: Doctor now derives required repo-root OpenCode inventory from installed asset expectations, treats missing required files as blocking `RepoAssets` errors, and keeps checks presence-only without changing JSON or `--fix` behavior. - -- [x] T04: `Bracket and colorize doctor text status tokens` (status:done) - - Task ID: T04 - - Goal: Update doctor human text rendering so every status token appears as `[PASS]`, `[MISS]`, or `[FAIL]`, with `[PASS]` styled green and `[MISS]`/`[FAIL]` styled red through the shared styling service, while also switching rows to the approved simplified label/path presentation. - - Boundaries (in/out of scope): In - text-mode token formatting, color application, diagnose header text, simplified row labels, parent-row wording cleanup, and tests covering color-enabled and color-disabled rendering. Out - JSON output changes, new doctor status categories, or styling changes outside doctor text status tokens. - - Done when: human text doctor output emits bracketed status tokens everywhere, applies green/red styling only when the shared color policy allows it, preserves exact plain-text bracketed tokens when color is disabled, renders `SCE doctor diagnose` as the diagnose header, removes redundant prose like `present`, `expected`, and `all required files present`, and formats rows as `label (path)` / `relative/path (absolute/path)` per the approved example. - - Verification notes (commands or checks): Add/update doctor text rendering tests for bracketed tokens, color-enabled ANSI styling expectations, no-color/plain-text expectations, diagnose header text, simplified repository/config labels, and integration parent/child row formatting under the shared styling helpers. - - Completed: 2026-04-02 - - Files changed: `cli/src/services/doctor.rs` - - Evidence: `nix flake check`; `nix run .#pkl-check-generated` - - Notes: Human doctor text now renders bracketed status tokens, colorizes them through the shared styling policy when enabled, removes redundant state prose from healthy rows, simplifies repository labels to `Repository` and `Hooks`, and renders integration parent rows without appended success prose. - -- [x] T05: `Protect unchanged JSON and fix-mode behavior` (status:done) - - Task ID: T05 - - Goal: Add regression coverage proving that the human text changes do not modify JSON output shape/semantics or current `sce doctor --fix` behavior. - - Boundaries (in/out of scope): In - regression tests for JSON contract stability and unchanged fix behavior. Out - new JSON fields, new fix actions, or expanded remediation ownership. - - Done when: automated coverage demonstrates that text-mode changes are isolated, JSON output remains byte-for-byte or semantically identical under the same fixtures, and existing fix flows still behave as before. - - Verification notes (commands or checks): Add/update JSON-mode and `--fix` tests in the CLI suite; compare representative outputs before/after under the same controlled fixtures. - - Completed: 2026-04-02 - - Files changed: `cli/src/services/doctor.rs` - - Evidence: Added JSON diagnose/fix regression assertions plus `--fix` execution coverage for preserved auto-fix/manual-follow-up behavior; `nix flake check`; `nix run .#pkl-check-generated`. - - Notes: This task was verify-only for durable context because it adds regression coverage without changing current-state doctor behavior. - -- [x] T06: `Validate doctor changes and sync current-state context` (status:done) - - Task ID: T06 - - Goal: Run final validation and update durable context so future sessions reflect the new human text doctor contract and repo-root integration presence rules. - - Boundaries (in/out of scope): In - full verification, cleanup, and context sync for important behavior changes. Out - follow-on UX polish beyond the approved contract. - - Done when: required verification passes, temporary scaffolding is removed, and context files reflect the resulting current-state doctor contract, including bracketed status tokens and approved color semantics. - - Verification notes (commands or checks): `nix run .#pkl-check-generated`; `nix flake check`; sync `context/overview.md`, `context/glossary.md`, and focused doctor context files if implementation changes the current-state contract. - - Completed: 2026-04-02 - - Files changed: `context/sce/agent-trace-hook-doctor.md`, `context/plans/doctor-human-text-integration-audit.md` - - Evidence: `nix flake check`; `nix run .#pkl-check-generated`. - - Notes: Root shared files (`context/overview.md`, `context/architecture.md`, `context/glossary.md`, `context/patterns.md`, `context/context-map.md`) already matched code truth; final sync corrected stale future-task wording in the focused doctor contract file so it is current-state oriented. - -## Open questions - -- None at plan time; blocking scope decisions have been resolved by the human for text layout, integration grouping, presence-only checks, bracketed status tokens, color intent, and unchanged JSON/`--fix` behavior. - -## Validation Report - -### Commands run - -- `nix flake check` -> exit 0 (CLI tests, clippy, fmt, pkl parity, npm/config-lib JS checks passed) -- `nix run .#pkl-check-generated` -> exit 0 (`Generated outputs are up to date.`) - -### Temporary scaffolding - -- No temporary scaffolding was introduced for this plan's final task. - -### Success-criteria verification - -- [x] `sce doctor` text output uses the approved human-only layout with sections in order -> covered by doctor text rendering tests in `cli/src/services/doctor.rs` and retained in current-state context (`context/sce/agent-trace-hook-doctor.md`, `context/cli/cli-command-surface.md`, `context/overview.md`) -- [x] Diagnose header renders as `SCE doctor diagnose` -> covered by doctor text rendering tests in `cli/src/services/doctor.rs` -- [x] Human text statuses use exact `[PASS]`, `[FAIL]`, `[MISS]` vocabulary -> covered by doctor text rendering tests in `cli/src/services/doctor.rs` and documented in `context/sce/agent-trace-hook-doctor.md` -- [x] Human text colorizes `[PASS]` green and `[MISS]`/`[FAIL]` red when color is enabled -> covered by doctor text rendering tests in `cli/src/services/doctor.rs` and documented in `context/overview.md` / `context/glossary.md` -- [x] Human text rows use simplified `label (path)` presentation and remove redundant prose -> covered by doctor text rendering tests and documented in `context/sce/agent-trace-hook-doctor.md` -- [x] Repository rows use simplified `Repository` and `Hooks` labels -> covered by current text rendering implementation and documented in `context/overview.md` -- [x] Integrations use exactly `OpenCode plugins`, `OpenCode agents`, `OpenCode commands`, `OpenCode skills` -> covered by doctor integration tests in `cli/src/services/doctor.rs` and documented in `context/sce/agent-trace-hook-doctor.md` -- [x] Integration checks are repo-root installed-artifact presence only -> covered by doctor integration tests and documented in `context/overview.md`, `context/cli/cli-command-surface.md`, and `context/sce/agent-trace-hook-doctor.md` -- [x] Missing integration files render child `[MISS]` and parent `[FAIL]` -> covered by doctor integration tests in `cli/src/services/doctor.rs` -- [x] Git hook text output is simplified to top-level hook presence rows only -> covered by doctor text rendering tests in `cli/src/services/doctor.rs` -- [x] Existing JSON output shape and semantics remain unchanged -> covered by JSON regression tests in `cli/src/services/doctor.rs` -- [x] Existing `sce doctor --fix` behavior remains unchanged -> covered by fix-mode regression tests in `cli/src/services/doctor.rs` - -### Context verification - -- Verified `context/overview.md`, `context/architecture.md`, `context/glossary.md`, `context/patterns.md`, and `context/context-map.md` against code truth. -- Updated `context/sce/agent-trace-hook-doctor.md` to remove stale future-task framing and keep the file current-state oriented. - -### Residual risks - -- None identified within the approved scope. diff --git a/context/plans/drop-macos-intel-release-support.md b/context/plans/drop-macos-intel-release-support.md deleted file mode 100644 index a8ea94b..0000000 --- a/context/plans/drop-macos-intel-release-support.md +++ /dev/null @@ -1,106 +0,0 @@ -# Plan: drop-macos-intel-release-support - -## Change summary - -Stop supporting macOS Intel in the CLI release pipeline. Remove the macOS Intel workflow lane, job names, artifact handling, and supported-matrix references so the repository's current release contract reflects Apple Silicon-only macOS support. - -## Success criteria - -- The release orchestrator no longer invokes a macOS Intel reusable workflow or waits on a macOS Intel build job. -- The repository no longer contains the dedicated macOS Intel release workflow or `macos-intel`-named release jobs/artifact producers for the CLI release flow. -- The npm launcher no longer advertises or resolves `darwin/x64` / `x86_64-apple-darwin` as a supported install target. -- Current-state release/context docs no longer advertise `x86_64-apple-darwin` as a supported automated release target. -- Validation confirms the release workflow graph, artifact contract references, and context files are internally consistent after the removal. - -## Constraints and non-goals - -- In scope: release workflow topology, supported-matrix documentation, and current-state context updates required to reflect the removal. -- In scope: npm launcher platform mapping, npm-facing support docs, and tests that currently encode macOS Intel support. -- In scope: removing `build-macos-intel` / `build-macos-intel-artifacts` references and the dedicated macOS Intel workflow file. -- Out of scope: introducing Rosetta-based Intel cross-build support. -- Out of scope: changing Linux release lanes, Cargo/npm downstream publish behavior, or non-macOS artifact naming. -- Out of scope: broader installer/distribution redesign beyond removing macOS Intel support from current code truth. - -## Task stack - -- [x] T01: `Remove macOS Intel release workflow lane` (status:done) - - Task ID: T01 - - Goal: Remove the macOS Intel reusable workflow entrypoint and the orchestrator references that currently schedule and require that lane. - - Boundaries (in/out of scope): In - `.github/workflows/release-sce.yml` orchestration edges, required job list, success gate logic, and deletion of `.github/workflows/release-sce-macos-intel.yml`. Out - changes to surviving Linux/macOS ARM lane behavior, artifact format, or release manifest assembly logic unrelated to the removed lane. - - Done when: The top-level release workflow no longer defines `build-macos-intel`, no longer depends on a macOS Intel result in the release job, and the dedicated macOS Intel reusable workflow file is removed. - - Verification notes (commands or checks): Inspect `.github/workflows/release-sce.yml` to confirm no `build-macos-intel` references remain; verify `.github/workflows/release-sce-macos-intel.yml` is absent; run the repo validation flow that covers workflow/config integrity. - - Completed: 2026-04-02 - - Files changed: `.github/workflows/release-sce.yml`, `.github/workflows/release-sce-macos-intel.yml`, `context/plans/drop-macos-intel-release-support.md` - - Evidence: targeted workflow reference searches passed; `nix flake check` passed - - Notes: Removed the macOS Intel reusable workflow lane and release-gate dependency without changing surviving lane behavior. - -- [x] T02: `Remove macOS Intel support from release contract docs` (status:done) - - Task ID: T02 - - Goal: Update release-contract documentation and related current-state wording so macOS support is described as Apple Silicon-only. - - Boundaries (in/out of scope): In - focused context files plus npm-facing docs that describe release topology, workflow names, supported target matrices, or npm install support. Out - unrelated CLI/install docs that do not mention the release matrix. - - Done when: Current-state context and npm-facing docs no longer list `x86_64-apple-darwin`, no longer reference `.github/workflows/release-sce-macos-intel.yml`, and no longer describe a four-target matrix that includes macOS Intel. - - Verification notes (commands or checks): Inspect `context/sce/cli-release-artifact-contract.md`, `context/overview.md`, `context/context-map.md`, `context/glossary.md`, and `npm/README.md` as needed to confirm macOS Intel references were removed or corrected consistently. - - Completed: 2026-04-02 - - Files changed: `context/overview.md`, `context/plans/drop-macos-intel-release-support.md` - - Evidence: targeted context/doc reference searches passed after narrowing scope; `context/sce/cli-release-artifact-contract.md` already matched the 3-target automated release contract - - Notes: Kept this task strictly scoped to release-contract/current-state docs; npm launcher docs/code were deferred to T03 and are now resolved there. - -- [x] T03: `Remove macOS Intel npm launcher support` (status:done) - - Task ID: T03 - - Goal: Remove macOS Intel from the npm launcher's supported platform map, user-facing errors/docs, and test coverage. - - Boundaries (in/out of scope): In - `npm/lib/platform.js`, npm tests, and npm-facing support messaging/documentation. Out - redesigning the npm installer flow or adding Rosetta-based fallback behavior. - - Done when: The npm launcher no longer maps `darwin/x64` to `x86_64-apple-darwin`, unsupported-platform messaging no longer advertises `darwin/x64`, and npm tests/docs align with the reduced matrix. - - Verification notes (commands or checks): Inspect `npm/lib/platform.js`, `npm/README.md`, and npm tests for removed `darwin/x64` / `x86_64-apple-darwin` support references; run the narrow npm test slice that covers platform resolution/install behavior. - - Completed: 2026-04-02 - - Files changed: `npm/lib/platform.js`, `npm/test/platform.test.js`, `npm/README.md`, `context/sce/cli-npm-distribution-contract.md`, `context/context-map.md`, `context/glossary.md`, `context/plans/drop-macos-intel-release-support.md` - - Evidence: `nix develop -c sh -c 'cd npm && bun test ./test/platform.test.js'` passed; `nix develop -c sh -c 'cd npm && bun test ./test/*.test.js'` passed; targeted reference searches removed active `darwin/x64` / `x86_64-apple-darwin` npm support references from current-state docs - - Notes: Removed macOS Intel npm launcher support without adding alternate fallback behavior. - -- [x] T04: `Sync remaining current-state references to Apple Silicon-only macOS support` (status:done) - - Task ID: T04 - - Goal: Remove any remaining repository references that imply macOS Intel release support and align current-state files with the new supported matrix. - - Boundaries (in/out of scope): In - residual references in repo-owned docs/config metadata touched by the release-support change. Out - speculative future platform additions or Rosetta-based replacement work. - - Done when: Residual `macos-intel`, `release-sce-macos-intel`, `build-macos-intel`, `darwin/x64`, and `x86_64-apple-darwin` support references tied to the current release matrix are either removed or intentionally retained only where historical/deferred wording is explicitly required. - - Verification notes (commands or checks): Run targeted repository searches for `macos-intel`, `release-sce-macos-intel`, `build-macos-intel`, `darwin/x64`, and `x86_64-apple-darwin`; confirm each remaining match is intentional and not advertising active support. - - Completed: 2026-04-02 - - Files changed: `flake.nix`, `npm/test/platform.test.js`, `context/sce/cli-release-artifact-contract.md`, `context/plans/drop-macos-intel-release-support.md` - - Evidence: targeted repo searches now leave only intentional plan-history references; `nix develop -c sh -c 'cd npm && bun test ./test/platform.test.js'` passed; `nix run .#release-artifacts -- --version 0.2.0-pre-alpha-v1 --out-dir ` passed on the supported Linux host - - Notes: Removed the last active macOS Intel release-target mapping from the flake release-artifact helper; remaining plan-file references are historical execution context rather than active support. - -- [x] T05: `Run validation and cleanup for release-matrix removal` (status:done) - - Task ID: T05 - - Goal: Execute final validation, ensure no stale references remain, and leave context aligned with the removed macOS Intel lane. - - Boundaries (in/out of scope): In - final repo verification, generated/parity checks if touched surfaces require them, and plan/context cleanup for this change. Out - new feature work or unrelated release refactors. - - Done when: Required validation passes, any temporary scaffolding/search leftovers are removed, and the plan/task evidence is sufficient for handoff completion. - - Verification notes (commands or checks): Run `nix run .#pkl-check-generated` if generated/config surfaces changed; run `nix flake check`; perform a final search for removed lane identifiers and confirm current-state context matches code truth. - - Completed: 2026-04-02 - - Files changed: `context/plans/drop-macos-intel-release-support.md` - - Evidence: `nix flake check` passed; final repository search for `x86_64-apple-darwin|darwin/x64|macos-intel|release-sce-macos-intel|build-macos-intel` left only intentional plan-history references; no temporary scaffolding remained - - Notes: `nix run .#pkl-check-generated` was not required because this plan did not touch generated config surfaces. - -## Open questions - -- None. - -## Validation Report - -### Commands run - -- `nix flake check` -> exit 0 (all repo checks passed; included `cli-tests`, `cli-clippy`, `cli-fmt`, `pkl-parity`, `npm-bun-tests`, `npm-biome-check`, `npm-biome-format`, `config-lib-bun-tests`, `config-lib-biome-check`, `config-lib-biome-format`) -- `rg -n "x86_64-apple-darwin|darwin/x64|macos-intel|release-sce-macos-intel|build-macos-intel"` -> exit 0 (matches remained only in `context/plans/drop-macos-intel-release-support.md` as intentional execution history) - -### Failed checks and follow-ups - -- None. - -### Success-criteria verification - -- [x] The release orchestrator no longer invokes a macOS Intel reusable workflow or waits on a macOS Intel build job -> confirmed by prior T01 workflow removal and preserved by final search with no remaining active workflow references outside plan history -- [x] The repository no longer contains the dedicated macOS Intel release workflow or `macos-intel`-named release jobs/artifact producers for the CLI release flow -> confirmed by final search with no active non-plan matches -- [x] The npm launcher no longer advertises or resolves `darwin/x64` / `x86_64-apple-darwin` as a supported install target -> confirmed by prior T03/T04 changes plus `nix flake check` coverage of npm tests and final search showing no active non-plan matches -- [x] Current-state release/context docs no longer advertise `x86_64-apple-darwin` as a supported automated release target -> confirmed by current context files and final search showing no active current-state doc matches outside plan history -- [x] Validation confirms the release workflow graph, artifact contract references, and context files are internally consistent after the removal -> confirmed by `nix flake check` exit 0 and root/domain context verification during T05 context sync - -### Residual risks - -- None identified. diff --git a/context/sce/optional-install-channel-integration-test-entrypoint.md b/context/sce/optional-install-channel-integration-test-entrypoint.md new file mode 100644 index 0000000..a2e853d --- /dev/null +++ b/context/sce/optional-install-channel-integration-test-entrypoint.md @@ -0,0 +1,29 @@ +# Optional install-channel integration-test entrypoint + +The repository exposes an explicit opt-in flake app for install-channel integration coverage: + +- `nix run .#install-channel-integration-tests -- --channel ` + +## Current contract + +- The public entrypoint remains `apps.install-channel-integration-tests` in `flake.nix`. +- It stays intentionally excluded from `checks.` and therefore does not run during default `nix flake check`. +- The accepted channel selector contract remains `npm`, `bun`, `cargo`, or `all`. + +## Current implementation split + +- `flake.nix` owns only the public opt-in app surface plus thin delegation to the standalone Rust runner. +- `integrations/install/` contains the standalone Rust runner binary, `install-channel-integration-tests`, with the same `--channel ` selector contract. +- The flake app exports `SCE_INSTALL_CHANNEL_SCE_BIN` pointing at the packaged `sce` binary, adds Node/npm to PATH, and then execs the Rust runner so the public Nix entrypoint stays stable while orchestration stays out of inline flake shell code. +- The Rust runner now owns the shared harness behavior: channel-scoped temporary roots, isolated `HOME`/`XDG_*`/npm/Bun/Cargo state, executable resolution inside the isolated PATH, and centralized deterministic command assertions for the installed `sce` binary. +- The npm channel now stages a local `@crocoder-dev/sce@.version` package fixture with the packaged `sce` binary preloaded into `runtime/`, installs that tarball into isolated npm state with download skipping enabled, and then reuses the shared Rust execution path to run both `sce version` and `sce doctor --format json` against the installed npm launcher path; the current `doctor` check only requires successful completion, not output inspection. +- The Bun channel now reuses the same staged local npm-package fixture shape as npm, performs a real isolated `bun add --global ` install with download skipping enabled, and then reuses the shared Rust execution path to run deterministic `sce version` against the installed Bun launcher path. +- The Cargo channel now performs a real isolated `cargo install --path cli --locked` install from the repository root, reuses the shared Rust execution path to run deterministic `sce version` against the installed Cargo binary, and completes the first-wave install-channel coverage for all three supported channels. + +## Current execution posture + +- The Rust runner already has dedicated default-flake checks: `integrations-install-fmt`, `integrations-install-clippy`, and `integrations-install-tests`. +- The opt-in app remains outside default `nix flake check`. +- Real npm, Bun, and Cargo install orchestration now run through the Rust runner behind the unchanged selector contract; all three first-wave channels are implemented. + +See also: [../overview.md](../overview.md), [../architecture.md](../architecture.md), [../patterns.md](../patterns.md) diff --git a/flake.nix b/flake.nix index 6c1891c..a0bb5c4 100644 --- a/flake.nix +++ b/flake.nix @@ -75,6 +75,15 @@ ]; }; + integrationsInstallSrc = pkgs.lib.fileset.toSource { + root = workspaceRoot; + fileset = pkgs.lib.fileset.unions [ + ./integrations/install/Cargo.toml + ./integrations/install/Cargo.lock + ./integrations/install/src + ]; + }; + # Fixed-output derivation to fetch Bun dependencies # The output hash must be updated when package.json or bun.lock changes configLibBashPolicyDeps = pkgs.stdenv.mkDerivation { @@ -144,6 +153,43 @@ } ); + integrationsInstallCargoArgs = { + pname = "sce-install-channel-integration-tests"; + version = "0.1.0"; + src = integrationsInstallSrc; + cargoToml = ./integrations/install/Cargo.toml; + cargoLock = ./integrations/install/Cargo.lock; + strictDeps = true; + doCheck = false; + + nativeBuildInputs = [ + rustToolchain + ]; + + postUnpack = '' + cd "$sourceRoot/integrations/install" + sourceRoot="." + ''; + }; + + integrationsInstallCargoArtifacts = craneLib.buildDepsOnly ( + integrationsInstallCargoArgs + // { + pname = "sce-install-channel-integration-tests-deps"; + } + ); + + integrationsInstallPackage = craneLib.buildPackage ( + integrationsInstallCargoArgs + // { + cargoArtifacts = integrationsInstallCargoArtifacts; + meta = { + mainProgram = "install-channel-integration-tests"; + description = "Opt-in install-channel integration runner for sce"; + }; + } + ); + scePackage = craneLib.buildPackage ( commonCargoArgs // { @@ -588,6 +634,21 @@ ''; }; + installChannelIntegrationTestsApp = pkgs.writeShellApplication { + name = "install-channel-integration-tests"; + runtimeInputs = [ + pkgs.nodejs + pkgs.bun + rustToolchain + ]; + text = '' + set -euo pipefail + + export SCE_INSTALL_CHANNEL_SCE_BIN="${scePackage}/bin/sce" + exec "${integrationsInstallPackage}/bin/install-channel-integration-tests" "$@" + ''; + }; + pklParityCheck = pkgs.runCommand "pkl-parity-check" { @@ -802,6 +863,30 @@ } ); + integrations-install-tests = craneLib.cargoTest ( + integrationsInstallCargoArgs + // { + pname = "sce-integrations-install-tests"; + cargoArtifacts = integrationsInstallCargoArtifacts; + } + ); + + integrations-install-clippy = craneLib.cargoClippy ( + integrationsInstallCargoArgs + // { + pname = "sce-integrations-install-clippy"; + cargoArtifacts = integrationsInstallCargoArtifacts; + cargoClippyExtraArgs = "--all-targets --all-features"; + } + ); + + integrations-install-fmt = craneLib.cargoFmt ( + integrationsInstallCargoArgs + // { + pname = "sce-integrations-install-fmt"; + } + ); + pkl-parity = pklParityCheck; npm-bun-tests = npmTests; @@ -848,6 +933,14 @@ description = "Build sce npm package tarball"; }; }; + + install-channel-integration-tests = { + type = "app"; + program = "${installChannelIntegrationTestsApp}/bin/install-channel-integration-tests"; + meta = { + description = "Run opt-in install-channel integration entrypoint"; + }; + }; }; devShells.default = pkgs.mkShell { diff --git a/integrations/install/Cargo.lock b/integrations/install/Cargo.lock new file mode 100644 index 0000000..9501898 --- /dev/null +++ b/integrations/install/Cargo.lock @@ -0,0 +1,591 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "sce-install-channel-integration-tests" +version = "0.2.0-pre-alpha-v2" +dependencies = [ + "clap", + "fs_extra", + "tempfile", + "thiserror", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/integrations/install/Cargo.toml b/integrations/install/Cargo.toml new file mode 100644 index 0000000..2d4bddd --- /dev/null +++ b/integrations/install/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sce-install-channel-integration-tests" +version = "0.2.0-pre-alpha-v2" +edition = "2021" +publish = false + +[[bin]] +name = "install-channel-integration-tests" +path = "src/main.rs" + +[dependencies] +clap = { version = "4", features = ["derive"] } +fs_extra = "1" +tempfile = "3" +thiserror = "2" diff --git a/integrations/install/src/channels/bun.rs b/integrations/install/src/channels/bun.rs new file mode 100644 index 0000000..28691d3 --- /dev/null +++ b/integrations/install/src/channels/bun.rs @@ -0,0 +1,77 @@ +use std::path::Path; + +use crate::error::HarnessError; +use crate::harness::{ChannelHarness, HarnessRequest}; + +use super::npm::{build_local_npm_fixture, find_repo_root}; + +pub(crate) fn run( + request: HarnessRequest, + explicit_repo_root: Option<&Path>, +) -> Result<(), HarnessError> { + let harness = ChannelHarness::new(request.channel())?; + println!("{}", harness.setup_message()); + + let repo_root = find_repo_root(request.channel().as_str(), explicit_repo_root)?; + let package_tarball = + build_local_npm_fixture(&harness, &repo_root, request.channel().as_str())?; + + install_bun_package(&harness, &repo_root, &package_tarball)?; + + let sce_binary = harness.resolve_program_in_harness_bins("sce")?; + let version_output = harness.assert_sce_version_success(&sce_binary)?; + harness.assert_sce_doctor_success(&sce_binary)?; + + println!("{}", harness.version_success_message(&version_output)); + println!( + "[PASS] channel={} sce doctor completed successfully via installed bun launcher.", + request.channel().as_str() + ); + println!( + "bun install-and-verify flow passed for channel={} via the Rust runner (mode={}).", + request.channel().as_str(), + request.mode().as_str() + ); + Ok(()) +} + +fn install_bun_package( + harness: &ChannelHarness, + repo_root: &Path, + package_tarball: &Path, +) -> Result<(), HarnessError> { + let bun = harness.resolve_program("bun")?; + let install_output = harness.run_command_in_dir_with_env( + &bun, + [ + std::ffi::OsStr::new("add"), + std::ffi::OsStr::new("--global"), + package_tarball.as_os_str(), + ], + repo_root, + [("SCE_NPM_SKIP_DOWNLOAD", "1")], + )?; + + if !install_output.status.success() { + return Err(HarnessError::BunInstallFailed { + channel: "bun".to_string(), + tarball: package_tarball.to_path_buf(), + stdout: if install_output.stdout.is_empty() { + None + } else { + Some(install_output.stdout) + }, + stderr: if install_output.stderr.is_empty() { + None + } else { + Some(install_output.stderr) + }, + }); + } + + println!( + "[PASS] channel=bun bun global install completed from {}", + package_tarball.display() + ); + Ok(()) +} diff --git a/integrations/install/src/channels/cargo.rs b/integrations/install/src/channels/cargo.rs new file mode 100644 index 0000000..d0dbabe --- /dev/null +++ b/integrations/install/src/channels/cargo.rs @@ -0,0 +1,74 @@ +use std::path::Path; + +use crate::error::HarnessError; +use crate::harness::{ChannelHarness, HarnessRequest}; + +use super::npm::find_repo_root; + +pub(crate) fn run( + request: HarnessRequest, + explicit_repo_root: Option<&Path>, +) -> Result<(), HarnessError> { + let harness = ChannelHarness::new(request.channel())?; + println!("{}", harness.setup_message()); + + let repo_root = find_repo_root(request.channel().as_str(), explicit_repo_root)?; + let cli_path = repo_root.join("cli"); + + install_cargo_package(&harness, &cli_path)?; + + let sce_binary = harness.resolve_program("sce")?; + let version_output = harness.assert_sce_version_success(&sce_binary)?; + harness.assert_sce_doctor_success(&sce_binary)?; + + println!("{}", harness.version_success_message(&version_output)); + println!( + "[PASS] channel={} sce doctor completed successfully via installed cargo launcher.", + request.channel().as_str() + ); + println!( + "cargo install-and-verify flow passed for channel={} via the Rust runner (mode={}).", + request.channel().as_str(), + request.mode().as_str() + ); + Ok(()) +} + +fn install_cargo_package(harness: &ChannelHarness, cli_path: &Path) -> Result<(), HarnessError> { + let cargo = harness.resolve_program("cargo")?; + + let install_output = harness.run_command_in_dir_with_env( + &cargo, + [ + std::ffi::OsStr::new("install"), + std::ffi::OsStr::new("--path"), + cli_path.as_os_str(), + std::ffi::OsStr::new("--locked"), + ], + cli_path, + std::iter::empty::<(&str, &str)>(), + )?; + + if !install_output.status.success() { + return Err(HarnessError::CargoInstallFailed { + channel: "cargo".to_string(), + path: cli_path.to_path_buf(), + stdout: if install_output.stdout.is_empty() { + None + } else { + Some(install_output.stdout) + }, + stderr: if install_output.stderr.is_empty() { + None + } else { + Some(install_output.stderr) + }, + }); + } + + println!( + "[PASS] channel=cargo cargo install completed from {}", + cli_path.display() + ); + Ok(()) +} diff --git a/integrations/install/src/channels/mod.rs b/integrations/install/src/channels/mod.rs new file mode 100644 index 0000000..468fba0 --- /dev/null +++ b/integrations/install/src/channels/mod.rs @@ -0,0 +1,46 @@ +mod bun; +mod cargo; +mod npm; + +use std::path::Path; + +use crate::error::HarnessError; +use crate::harness::{HarnessMode, HarnessRequest}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum Channel { + Npm, + Bun, + Cargo, +} + +impl Channel { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Npm => "npm", + Self::Bun => "bun", + Self::Cargo => "cargo", + } + } +} + +pub(crate) struct ChannelRunner; + +impl ChannelRunner { + pub(crate) fn run( + &self, + channels: &[Channel], + repo_root: Option<&Path>, + ) -> Result<(), HarnessError> { + for channel in channels { + let request = HarnessRequest::new(*channel, HarnessMode::SharedHarnessSmoke); + match channel { + Channel::Npm => npm::run(request, repo_root)?, + Channel::Bun => bun::run(request, repo_root)?, + Channel::Cargo => cargo::run(request, repo_root)?, + }; + } + + Ok(()) + } +} diff --git a/integrations/install/src/channels/npm.rs b/integrations/install/src/channels/npm.rs new file mode 100644 index 0000000..c4c4778 --- /dev/null +++ b/integrations/install/src/channels/npm.rs @@ -0,0 +1,214 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::error::HarnessError; +use crate::harness::{copy_directory_recursive, ChannelHarness, HarnessRequest}; +use crate::platform::set_executable_permissions; + +pub(crate) fn run( + request: HarnessRequest, + explicit_repo_root: Option<&Path>, +) -> Result<(), HarnessError> { + let harness = ChannelHarness::new(request.channel())?; + println!("{}", harness.setup_message()); + + let repo_root = find_repo_root(request.channel().as_str(), explicit_repo_root)?; + let package_tarball = + build_local_npm_fixture(&harness, &repo_root, request.channel().as_str())?; + + install_npm_package(&harness, &repo_root, &package_tarball)?; + + let sce_binary = harness.resolve_program_in_harness_bins("sce")?; + let version_output = harness.assert_sce_version_success(&sce_binary)?; + harness.assert_sce_doctor_success(&sce_binary)?; + + println!("{}", harness.version_success_message(&version_output)); + println!( + "[PASS] channel={} sce doctor completed successfully via installed npm launcher.", + request.channel().as_str() + ); + println!( + "npm install-and-verify flow passed for channel={} via the Rust runner (mode={}).", + request.channel().as_str(), + request.mode().as_str() + ); + Ok(()) +} + +fn install_npm_package( + harness: &ChannelHarness, + repo_root: &Path, + package_tarball: &Path, +) -> Result<(), HarnessError> { + let npm = harness.resolve_program("npm")?; + + let install_output = harness.run_command_in_dir_with_env( + &npm, + [ + std::ffi::OsStr::new("install"), + std::ffi::OsStr::new("--global"), + package_tarball.as_os_str(), + ], + repo_root, + [("SCE_NPM_SKIP_DOWNLOAD", "1")], + )?; + + if !install_output.status.success() { + return Err(HarnessError::NpmInstallFailed { + channel: "npm".to_string(), + tarball: package_tarball.to_path_buf(), + stdout: if install_output.stdout.is_empty() { + None + } else { + Some(install_output.stdout) + }, + stderr: if install_output.stderr.is_empty() { + None + } else { + Some(install_output.stderr) + }, + }); + } + + println!( + "[PASS] channel=npm npm install completed from {}", + package_tarball.display() + ); + Ok(()) +} + +pub(super) fn find_repo_root( + channel_name: &str, + explicit_root: Option<&Path>, +) -> Result { + // First, check explicit path if provided + if let Some(explicit) = explicit_root { + if explicit.join("flake.nix").is_file() { + return Ok(explicit.to_path_buf()); + } + } + + // Fall back to upward walk from current directory + let mut current = std::env::current_dir().map_err(|e| HarnessError::CurrentDir { + error: e.to_string(), + })?; + + loop { + if current.join("flake.nix").is_file() { + return Ok(current); + } + + if !current.pop() { + return Err(HarnessError::RepoRootMissing { + channel: channel_name.to_string(), + }); + } + } +} + +pub(super) fn build_local_npm_fixture( + harness: &ChannelHarness, + repo_root: &Path, + channel_name: &str, +) -> Result { + let fixture_root = harness.create_temp_subdir("npm-package-fixture")?; + let stage_dir = fixture_root.join("package"); + let pack_dir = fixture_root.join("packed"); + let npm_source_dir = repo_root.join("npm"); + let packaged_sce_binary = harness.resolve_sce_binary()?; + + copy_directory_recursive(&npm_source_dir, &stage_dir)?; + add_runtime_to_staged_package_manifest(&stage_dir.join("package.json"))?; + fs::create_dir_all(stage_dir.join("runtime")).map_err(|e| HarnessError::DirectoryCreate { + path: stage_dir.join("runtime"), + error: e.to_string(), + })?; + fs::create_dir_all(&pack_dir).map_err(|e| HarnessError::DirectoryCreate { + path: pack_dir.clone(), + error: e.to_string(), + })?; + + let staged_binary = stage_dir.join("runtime/sce"); + fs::copy(&packaged_sce_binary, &staged_binary).map_err(|e| HarnessError::BinaryStage { + binary: packaged_sce_binary.clone(), + path: staged_binary.clone(), + error: e.to_string(), + })?; + + set_executable_permissions(&staged_binary)?; + + let npm = harness.resolve_program("npm")?; + let pack_output = harness.run_command_in_dir_with_env( + &npm, + [ + std::ffi::OsStr::new("pack"), + std::ffi::OsStr::new("--silent"), + stage_dir.as_os_str(), + ], + &pack_dir, + [("SCE_NPM_SKIP_DOWNLOAD", "1")], + )?; + + if !pack_output.status.success() { + return Err(HarnessError::NpmPackFailed { + channel: channel_name.to_string(), + stdout: if pack_output.stdout.is_empty() { + None + } else { + Some(pack_output.stdout) + }, + stderr: if pack_output.stderr.is_empty() { + None + } else { + Some(pack_output.stderr) + }, + }); + } + + let package_name = pack_output + .stdout + .lines() + .last() + .map(str::trim) + .filter(|line: &&str| !line.is_empty()) + .ok_or_else(|| HarnessError::NpmPackNoTarball { + channel: channel_name.to_string(), + })?; + let package_tarball = pack_dir.join(package_name); + + if !package_tarball.is_file() { + return Err(HarnessError::NpmPackTarballMissing { + channel: channel_name.to_string(), + path: package_tarball.clone(), + }); + } + + println!( + "[PASS] channel={channel_name} local package fixture built from @.version state: {}", + package_tarball.display() + ); + Ok(package_tarball) +} + +fn add_runtime_to_staged_package_manifest(package_json_path: &Path) -> Result<(), HarnessError> { + let package_json = + fs::read_to_string(package_json_path).map_err(|e| HarnessError::FileRead { + path: package_json_path.to_path_buf(), + error: e.to_string(), + })?; + let updated_package_json = package_json.replace( + "\"lib\",\n\t\t\"README.md\"", + "\"lib\",\n\t\t\"runtime\",\n\t\t\"README.md\"", + ); + + if updated_package_json == package_json { + return Err(HarnessError::ManifestInject { + path: package_json_path.to_path_buf(), + }); + } + + fs::write(package_json_path, updated_package_json).map_err(|e| HarnessError::FileWrite { + path: package_json_path.to_path_buf(), + error: e.to_string(), + }) +} diff --git a/integrations/install/src/cli.rs b/integrations/install/src/cli.rs new file mode 100644 index 0000000..4908213 --- /dev/null +++ b/integrations/install/src/cli.rs @@ -0,0 +1,41 @@ +use std::path::PathBuf; + +use clap::Parser; + +use crate::channels::Channel; + +/// Opt-in install-channel integration runner for `sce`. +/// +/// The npm and Bun channels now perform real install-and-verify flows through the +/// Rust runner, while Cargo remains a shared-harness smoke path until a later task. +#[derive(Parser, Debug)] +#[command(name = "install-channel-integration-tests")] +pub(crate) struct Args { + /// Channel selector: npm, bun, cargo, or all (default: all) + #[arg(short, long, value_enum, default_value = "all")] + pub(crate) channel: ChannelArg, + + /// Explicit repository root path (must contain flake.nix) + #[arg(long, value_name = "PATH")] + pub(crate) repo_root: Option, +} + +/// Channel selector for integration tests. +#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)] +pub(crate) enum ChannelArg { + Npm, + Bun, + Cargo, + All, +} + +impl From for Vec { + fn from(arg: ChannelArg) -> Self { + match arg { + ChannelArg::Npm => vec![Channel::Npm], + ChannelArg::Bun => vec![Channel::Bun], + ChannelArg::Cargo => vec![Channel::Cargo], + ChannelArg::All => vec![Channel::Npm, Channel::Bun, Channel::Cargo], + } + } +} diff --git a/integrations/install/src/error.rs b/integrations/install/src/error.rs new file mode 100644 index 0000000..88d79ce --- /dev/null +++ b/integrations/install/src/error.rs @@ -0,0 +1,140 @@ +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub(crate) enum HarnessError { + #[error("failed to create temp directory: {0}")] + TempDirCreate(String), + + #[error("failed to create {path}: {error}")] + DirectoryCreate { path: PathBuf, error: String }, + + #[error("failed to copy directory from {src} to {dest}: {error}")] + DirectoryCopy { + src: PathBuf, + dest: PathBuf, + error: String, + }, + + #[error( + "Unable to resolve executable '{program}' for channel={channel}. Ensure it is on PATH." + )] + ExecutableResolve { program: String, channel: String }, + + #[error("Unable to resolve executable '{program}' for channel={channel}. Set {env} or ensure it is on PATH.")] + SceBinaryResolve { + program: String, + channel: String, + env: String, + }, + + #[error("[FAIL] channel={channel} expected executable not found: {path} ({reason})")] + ExecutableNotFound { + channel: String, + path: PathBuf, + reason: String, + }, + + #[error("[FAIL] channel={channel} sce version failed via {path}\nstderr: {stderr:?}")] + SceVersionFailed { + channel: String, + path: PathBuf, + stderr: Option, + }, + + #[error("[FAIL] channel={channel} unexpected sce version output: {output}")] + SceVersionUnexpected { channel: String, output: String }, + + #[error("[FAIL] channel={channel} expected empty stderr for sce version.\n{stderr}")] + SceVersionStderr { channel: String, stderr: String }, + + #[error("[FAIL] channel={channel} failed to run {program}: {error}")] + CommandFailed { + channel: String, + program: String, + error: String, + }, + + #[error("[FAIL] channel={channel} command {program} exited with status {status}\nstdout: {stdout}\nstderr: {stderr}")] + CommandExitedNonZero { + channel: String, + program: String, + status: i32, + stdout: String, + stderr: String, + }, + + #[error("failed to inspect {path}: {error}")] + FileInspect { path: PathBuf, error: String }, + + #[error("failed to set executable permissions on {path}: {error}")] + PermissionSet { path: PathBuf, error: String }, + + #[error("failed to read {path}: {error}")] + FileRead { path: PathBuf, error: String }, + + #[error("failed to write {path}: {error}")] + FileWrite { path: PathBuf, error: String }, + + #[error("[FAIL] channel={channel} npm install failed for {tarball}\nstdout: {stdout:?}\nstderr: {stderr:?}")] + NpmInstallFailed { + channel: String, + tarball: PathBuf, + stdout: Option, + stderr: Option, + }, + + #[error("[FAIL] channel={channel} npm pack failed for local fixture\nstdout: {stdout:?}\nstderr: {stderr:?}")] + NpmPackFailed { + channel: String, + stdout: Option, + stderr: Option, + }, + + #[error("[FAIL] channel={channel} npm pack did not report a tarball name.")] + NpmPackNoTarball { channel: String }, + + #[error("[FAIL] channel={channel} expected packed tarball was not created: {path}")] + NpmPackTarballMissing { channel: String, path: PathBuf }, + + #[error("[FAIL] channel={channel} bun global install failed for {tarball}\nstdout: {stdout:?}\nstderr: {stderr:?}")] + BunInstallFailed { + channel: String, + tarball: PathBuf, + stdout: Option, + stderr: Option, + }, + + #[error("[FAIL] channel={channel} cargo install failed for {path}\nstdout: {stdout:?}\nstderr: {stderr:?}")] + CargoInstallFailed { + channel: String, + path: PathBuf, + stdout: Option, + stderr: Option, + }, + + #[error("failed to inject runtime/ into staged package manifest {path}")] + ManifestInject { path: PathBuf }, + + #[error("failed to stage {binary} into {path}: {error}")] + BinaryStage { + binary: PathBuf, + path: PathBuf, + error: String, + }, + + #[error("failed to resolve current directory: {error}")] + CurrentDir { error: String }, + + #[error("[FAIL] channel={channel} could not locate repository root containing flake.nix.")] + RepoRootMissing { channel: String }, + + #[error("executable permissions are only supported on Unix systems")] + UnixOnly, + + #[error("path is not a file: {path}")] + NotAFile { path: PathBuf }, + + #[error("missing execute permission for file: {path}")] + MissingExecutePermission { path: PathBuf }, +} diff --git a/integrations/install/src/harness.rs b/integrations/install/src/harness.rs new file mode 100644 index 0000000..a158c23 --- /dev/null +++ b/integrations/install/src/harness.rs @@ -0,0 +1,428 @@ +use std::env; +use std::ffi::{OsStr, OsString}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitStatus}; + +use fs_extra::dir; +use tempfile::TempDir; + +use crate::channels::Channel; +use crate::error::HarnessError; +use crate::platform::ensure_executable; + +const SCE_BINARY_ENV: &str = "SCE_INSTALL_CHANNEL_SCE_BIN"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum HarnessMode { + SharedHarnessSmoke, +} + +impl HarnessMode { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::SharedHarnessSmoke => "shared-harness-smoke", + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct HarnessRequest { + channel: Channel, + mode: HarnessMode, +} + +impl HarnessRequest { + pub(crate) fn new(channel: Channel, mode: HarnessMode) -> Self { + Self { channel, mode } + } + + pub(crate) fn channel(&self) -> Channel { + self.channel + } + + pub(crate) fn mode(&self) -> HarnessMode { + self.mode + } +} + +pub(crate) struct ChannelHarness { + channel: Channel, + temp_dir: TempDir, + workdir: PathBuf, + original_path: OsString, +} + +impl ChannelHarness { + pub(crate) fn new(channel: Channel) -> Result { + let temp_dir = tempfile::Builder::new() + .prefix(&format!("sce-install-channel-{}.", channel.as_str())) + .tempdir() + .map_err(|e| HarnessError::TempDirCreate(e.to_string()))?; + let root = temp_dir.path().to_path_buf(); + let workdir = root.join("workdir"); + + let harness = Self { + channel, + temp_dir, + workdir, + original_path: env::var_os("PATH").unwrap_or_default(), + }; + + harness.create_layout()?; + Ok(harness) + } + + #[cfg(test)] + pub(crate) fn root(&self) -> &Path { + self.temp_dir.path() + } + + pub(crate) fn setup_message(&self) -> String { + format!( + "[PASS] channel={} isolated harness ready: {}", + self.channel.as_str(), + self.temp_dir.path().display() + ) + } + + pub(crate) fn resolve_sce_binary(&self) -> Result { + if let Some(binary) = env::var_os(SCE_BINARY_ENV) { + return self.resolve_executable_in_paths( + binary.as_os_str(), + &self.path_with_harness_bins(), + true, + ); + } + + self.resolve_executable_in_paths(OsStr::new("sce"), &self.path_with_harness_bins(), true) + } + + pub(crate) fn assert_sce_version_success( + &self, + binary_path: &Path, + ) -> Result { + ensure_executable(binary_path).map_err(|e| match e { + HarnessError::UnixOnly => HarnessError::ExecutableNotFound { + channel: self.channel.as_str().to_string(), + path: binary_path.to_path_buf(), + reason: "not executable or not found".to_string(), + }, + _ => e, + })?; + + let output = self.run_command(binary_path, ["version"])?; + + if !output.status.success() { + return Err(HarnessError::SceVersionFailed { + channel: self.channel.as_str().to_string(), + path: binary_path.to_path_buf(), + stderr: if output.stderr.is_empty() { + None + } else { + Some(output.stderr) + }, + }); + } + + if !is_valid_version_output(&output.stdout) { + return Err(HarnessError::SceVersionUnexpected { + channel: self.channel.as_str().to_string(), + output: output.stdout, + }); + } + + if !output.stderr.is_empty() { + return Err(HarnessError::SceVersionStderr { + channel: self.channel.as_str().to_string(), + stderr: output.stderr, + }); + } + + Ok(output.stdout) + } + + pub(crate) fn version_success_message(&self, version_output: &str) -> String { + format!( + "[PASS] channel={} deterministic sce version assertion succeeded: {}", + self.channel.as_str(), + version_output + ) + } + + pub(crate) fn assert_sce_doctor_success(&self, binary_path: &Path) -> Result<(), HarnessError> { + let output = self.run_command(binary_path, ["doctor", "--format", "json"])?; + + if !output.status.success() { + return Err(HarnessError::CommandExitedNonZero { + channel: self.channel.as_str().to_string(), + program: binary_path.display().to_string(), + status: output.status.code().unwrap_or(-1), + stdout: output.stdout, + stderr: output.stderr, + }); + } + + Ok(()) + } + + pub(crate) fn create_temp_subdir(&self, name: &str) -> Result { + let path = self.temp_dir.path().join(name); + fs::create_dir_all(&path).map_err(|e| HarnessError::DirectoryCreate { + path: path.clone(), + error: e.to_string(), + })?; + Ok(path) + } + + pub(crate) fn resolve_program(&self, program: &str) -> Result { + self.resolve_executable(OsStr::new(program)) + } + + pub(crate) fn resolve_program_in_harness_bins( + &self, + program: &str, + ) -> Result { + self.resolve_executable_in_harness_bins(OsStr::new(program)) + } + + pub(crate) fn run_command( + &self, + program: &Path, + args: I, + ) -> Result + where + I: IntoIterator, + S: AsRef, + { + self.run_command_in_dir_with_env( + program, + args, + &self.workdir, + std::iter::empty::<(&str, &str)>(), + ) + } + + pub(crate) fn run_command_in_dir_with_env( + &self, + program: &Path, + args: I, + current_dir: &Path, + extra_env: impl IntoIterator, + ) -> Result + where + I: IntoIterator, + S: AsRef, + K: AsRef, + V: AsRef, + { + let mut command = Command::new(program); + command + .args(args) + .current_dir(current_dir) + .env("SCE_INSTALL_CHANNEL", self.channel.as_str()) + .env("SCE_CHANNEL_HARNESS_ROOT", self.temp_dir.path()) + .env("SCE_CHANNEL_WORKDIR", &self.workdir) + .env("HOME", self.home_dir()) + .env("XDG_CONFIG_HOME", self.xdg_config_home()) + .env("XDG_STATE_HOME", self.xdg_state_home()) + .env("XDG_CACHE_HOME", self.xdg_cache_home()) + .env("NPM_CONFIG_PREFIX", self.npm_prefix_dir()) + .env("NPM_CONFIG_CACHE", self.npm_cache_dir()) + .env("BUN_INSTALL", self.bun_install_dir()) + .env("CARGO_HOME", self.cargo_home_dir()) + .env("CARGO_TARGET_DIR", self.cargo_target_dir()) + .env("PATH", self.path_with_harness_bins()); + + for (key, value) in extra_env { + command.env(key, value); + } + + let output = command.output().map_err(|e| HarnessError::CommandFailed { + channel: self.channel.as_str().to_string(), + program: program.display().to_string(), + error: e.to_string(), + })?; + + Ok(CommandOutput { + status: output.status, + stdout: normalize_output(&output.stdout), + stderr: normalize_output(&output.stderr), + }) + } + + fn create_layout(&self) -> Result<(), HarnessError> { + for path in [ + self.workdir.clone(), + self.home_dir(), + self.xdg_config_home(), + self.xdg_state_home(), + self.xdg_cache_home(), + self.npm_prefix_bin(), + self.npm_cache_dir(), + self.bun_install_bin(), + self.cargo_home_bin(), + self.cargo_target_dir(), + ] { + fs::create_dir_all(&path).map_err(|e| HarnessError::DirectoryCreate { + path: path.clone(), + error: e.to_string(), + })?; + } + + Ok(()) + } + + fn resolve_executable(&self, program: &OsStr) -> Result { + self.resolve_executable_in_paths(program, &self.path_with_harness_bins(), false) + } + + fn resolve_executable_in_harness_bins(&self, program: &OsStr) -> Result { + self.resolve_executable_in_paths(program, &self.harness_bins_only_path(), false) + } + + fn resolve_executable_in_paths( + &self, + program: &OsStr, + paths: &OsStr, + is_sce_binary: bool, + ) -> Result { + let candidate = Path::new(program); + if candidate.components().count() > 1 { + return Ok(candidate.to_path_buf()); + } + + for path_entry in env::split_paths(paths) { + let resolved = path_entry.join(candidate); + if ensure_executable(&resolved).is_ok() { + return Ok(resolved); + } + } + + if is_sce_binary { + Err(HarnessError::SceBinaryResolve { + program: candidate.display().to_string(), + channel: self.channel.as_str().to_string(), + env: SCE_BINARY_ENV.to_string(), + }) + } else { + Err(HarnessError::ExecutableResolve { + program: candidate.display().to_string(), + channel: self.channel.as_str().to_string(), + }) + } + } + + fn home_dir(&self) -> PathBuf { + self.temp_dir.path().join("home") + } + + fn xdg_config_home(&self) -> PathBuf { + self.temp_dir.path().join("xdg/config") + } + + fn xdg_state_home(&self) -> PathBuf { + self.temp_dir.path().join("xdg/state") + } + + fn xdg_cache_home(&self) -> PathBuf { + self.temp_dir.path().join("xdg/cache") + } + + fn npm_prefix_dir(&self) -> PathBuf { + self.temp_dir.path().join("npm/prefix") + } + + fn npm_prefix_bin(&self) -> PathBuf { + self.npm_prefix_dir().join("bin") + } + + fn npm_cache_dir(&self) -> PathBuf { + self.temp_dir.path().join("npm/cache") + } + + fn bun_install_dir(&self) -> PathBuf { + self.temp_dir.path().join("bun") + } + + fn bun_install_bin(&self) -> PathBuf { + self.bun_install_dir().join("bin") + } + + fn cargo_home_dir(&self) -> PathBuf { + self.temp_dir.path().join("cargo/home") + } + + fn cargo_home_bin(&self) -> PathBuf { + self.cargo_home_dir().join("bin") + } + + fn cargo_target_dir(&self) -> PathBuf { + self.temp_dir.path().join("cargo/target") + } + + fn path_with_harness_bins(&self) -> OsString { + let mut paths = vec![ + self.npm_prefix_bin(), + self.bun_install_bin(), + self.cargo_home_bin(), + ]; + paths.extend(env::split_paths(&self.original_path)); + env::join_paths(paths).expect("harness paths should be valid") + } + + fn harness_bins_only_path(&self) -> OsString { + let paths = vec![ + self.npm_prefix_bin(), + self.bun_install_bin(), + self.cargo_home_bin(), + ]; + env::join_paths(paths).expect("harness paths should be valid") + } +} + +pub(crate) struct CommandOutput { + pub(crate) status: ExitStatus, + pub(crate) stdout: String, + pub(crate) stderr: String, +} + +fn normalize_output(bytes: &[u8]) -> String { + String::from_utf8_lossy(bytes) + .replace('\r', "") + .trim_end_matches('\n') + .to_string() +} + +fn is_valid_version_output(output: &str) -> bool { + let mut parts = output.splitn(3, ' '); + let binary = parts.next().unwrap_or_default(); + let version = parts.next().unwrap_or_default(); + let profile = parts.next().unwrap_or_default(); + + !binary.is_empty() + && !binary.contains(char::is_whitespace) + && !version.is_empty() + && !version.contains(char::is_whitespace) + && profile.starts_with('(') + && profile.ends_with(')') + && profile.len() > 2 +} + +pub(crate) fn copy_directory_recursive( + source: &Path, + destination: &Path, +) -> Result<(), HarnessError> { + let mut options = dir::CopyOptions::new(); + options.overwrite = true; + options.copy_inside = true; + options.content_only = true; + + dir::copy(source, destination, &options).map_err(|e| HarnessError::DirectoryCopy { + src: source.to_path_buf(), + dest: destination.to_path_buf(), + error: e.to_string(), + })?; + + Ok(()) +} diff --git a/integrations/install/src/main.rs b/integrations/install/src/main.rs new file mode 100644 index 0000000..a429269 --- /dev/null +++ b/integrations/install/src/main.rs @@ -0,0 +1,29 @@ +mod channels; +mod cli; +mod error; +mod harness; +mod platform; + +use std::process::ExitCode; + +use channels::ChannelRunner; +use clap::Parser; +use cli::Args; +use error::HarnessError; + +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("{error}"); + ExitCode::from(1) + } + } +} + +fn run() -> Result<(), HarnessError> { + let args = Args::parse(); + let channels = Vec::from(args.channel); + let runner = ChannelRunner; + runner.run(&channels, args.repo_root.as_deref()) +} diff --git a/integrations/install/src/platform/mod.rs b/integrations/install/src/platform/mod.rs new file mode 100644 index 0000000..08e9bdd --- /dev/null +++ b/integrations/install/src/platform/mod.rs @@ -0,0 +1,17 @@ +#[cfg(unix)] +mod unix; + +#[cfg(unix)] +pub use unix::{ensure_executable, set_executable_permissions}; + +#[cfg(not(unix))] +pub fn ensure_executable(_path: &std::path::Path) -> Result<(), crate::error::HarnessError> { + Err(crate::error::HarnessError::UnixOnly) +} + +#[cfg(not(unix))] +pub fn set_executable_permissions( + _path: &std::path::Path, +) -> Result<(), crate::error::HarnessError> { + Err(crate::error::HarnessError::UnixOnly) +} diff --git a/integrations/install/src/platform/unix.rs b/integrations/install/src/platform/unix.rs new file mode 100644 index 0000000..4f7f6e2 --- /dev/null +++ b/integrations/install/src/platform/unix.rs @@ -0,0 +1,43 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::path::Path; + +use crate::error::HarnessError; + +pub fn ensure_executable(path: &Path) -> Result<(), HarnessError> { + let metadata = path.metadata().map_err(|e| HarnessError::FileInspect { + path: path.to_path_buf(), + error: e.to_string(), + })?; + + if !metadata.is_file() { + return Err(HarnessError::NotAFile { + path: path.to_path_buf(), + }); + } + + if metadata.permissions().mode() & 0o111 == 0 { + return Err(HarnessError::MissingExecutePermission { + path: path.to_path_buf(), + }); + } + + Ok(()) +} + +pub fn set_executable_permissions(path: &Path) -> Result<(), HarnessError> { + let mut permissions = fs::metadata(path) + .map_err(|e| HarnessError::FileInspect { + path: path.to_path_buf(), + error: e.to_string(), + })? + .permissions(); + let current_mode = permissions.mode(); + let new_mode = current_mode | 0o111; + permissions.set_mode(new_mode); + fs::set_permissions(path, permissions).map_err(|e| HarnessError::PermissionSet { + path: path.to_path_buf(), + error: e.to_string(), + })?; + Ok(()) +}