Commit graph

2 commits

Author SHA1 Message Date
Gergő Magyar
6bb99767ff
fix(auto-sync): HTTPS remotes, OpenSSH image, and rc embeddings (#3378)
* fix(docker): install OpenSSH in the CLI runtime image

Auto-sync requires git SSH remotes, but the published image omitted
openssh-client so every clone failed with ssh: not found.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auto-sync): accept HTTPS remotes and reclone failed checkouts

Allowlisted HTTPS URLs can clone without SSH keys, and a timed-out
clone with no remote.origin is quarantined instead of blocking forever.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(auto-sync): honor .gitnexusrc embeddings and warn on empty vectors

Auto-sync analyze now reads embeddings from the clone's project config,
and query reports when an index has no vectors so keyword fallback is visible.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(review): warn when CodeEmbedding table is missing (U5)

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): wrap long openssh-client test line for prettier

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(query): keep keyword-only indexes off query.warning

Empty or missing CodeEmbedding is the default index. Put the #3372 notice in a once-per-backend log line so FTS-success query results stay warning-free.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(review): bound rc reads and quarantine only a missing origin

Drop the implementation plan from the branch. Auto-sync reads .gitnexusrc through the bounded control-file reader, and a git config failure no longer relocates a live checkout. The same allowlisted repo can switch between SSH and HTTPS without a refused pull.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(auto-sync): share repo identity and skip a second origin read

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(auto-sync): clean temp fixtures and cover nested embeddings precedence

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(auto-sync): skip the symlink rc fixture on Windows

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(auto-sync): reject symlink rc files on every platform

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(ci): run auto-sync symlink and clone tests on Windows and macOS

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(git-clone): keep Windows CI on file URLs and POSIX permission checks

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(git-clone): keep the SSH-to-HTTPS origin check offline

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-25 19:42:26 +01:00
Gergő Magyar
4f9d595c73
fix(docker): ship runtime-needed published assets (hooks/, skills/) into the image (#2130) (#2132)
* fix(docker): copy hooks/ into Dockerfile.cli runtime stage (#2130)

`gitnexus analyze` inside the official image (akonlabs/gitnexus,
ghcr.io/abhigyanpatwari/gitnexus) crashed at startup with:

    Error: Cannot find module '../../hooks/claude/resolve-analyze-cmd.cjs'
    Require stack:
    - /app/gitnexus/dist/cli/resolve-invocation.js

`dist/cli/resolve-invocation.js` does
`createRequire(import.meta.url)('../../hooks/claude/resolve-analyze-cmd.cjs')`
at module load (it is the single source of truth for the npm-11 npx-crash
invocation decision, #1939), and `analyze.ts` statically imports it. The
Dockerfile.cli runtime stage copied dist/node_modules/package.json/the
duckdb script/vendor but never `hooks/`, so the require throws before the
command does any work. `hooks/` is in package.json `files`, so npm already
ships it — Docker was the only distribution dropping it.

Fix: copy `hooks/` into the runtime stage, mirroring what npm publishes.

Also add `test/unit/dockerfile-runtime-asset-parity.test.ts`: a regression
guard that derives every out-of-dist `require()`/`createRequire()` target
from source and asserts each is a runtime-stage `COPY`. Scoped to the
require family (not `fs.access`/`new URL`), so it locks the #2130 class
without false-flagging the intentionally-omitted, gracefully-degrading
`web/` and `skills/` assets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(docker): also ship skills/ into the runtime image

Follow-up to the hooks/ fix: `skills/` is another published runtime asset
(in package.json `files`) the Docker image dropped. The CLI reads the
bundled SKILL.md templates from `<pkg>/skills/` for `gitnexus analyze
--skills` (ai-context skill generation) and `gitnexus setup`/`uninstall`
(installing skills into editor configs). Unlike the hooks/ require(), these
reads degrade SILENTLY when the dir is absent — `--skills` writes minimal
placeholder content (ai-context.ts), `setup` installs zero skills
(setup.ts readdir → []) — so the image looked fine but produced wrong
output. Copy `skills/` so the image is fully usable for all CLI tooling.

`web/` (also in `files`) is intentionally NOT shipped: this image never
builds gitnexus-web (the builder doesn't copy it, build.js logs "skipping
web UI"), so it is API-only by design — the UI is the separate
Dockerfile.web image / hosted app. The duckdb script is the only runtime
asset needed from scripts/, so that stays a single-file copy.

Extends the runtime-asset-parity guard with an explicit skills/ assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(test): correct stale docstring that listed skills/ as not copied

The 2nd commit on this branch added a skills/ COPY + an it('copies skills/…')
assertion, but the top-of-file docstring still grouped skills/ with web/ as
'intentionally not copied / out of scope'. Drop skills/ from that sentence and
note it is shipped (and covered by its own test). web/ remains the sole
fs-accessed-but-uncopied example. Documentation-only; assertions unchanged.

* fix(test): make runtime-stage detection case-insensitive on AS

Docker accepts a lowercase `as runtime`; the parity guard's stage-detection
regex was case-sensitive on `AS`, so a future Dockerfile reformat would empty
the parsed COPY set and trip the named assertions. Add the /i flag.

* fix(test): stop runtime-stage COPY parsing at the next FROM

runtimeStageCopiedSources scanned from the runtime FROM to EOF. Bound the scan
to the runtime stage (start after its FROM, break on the next FROM) so a build
stage added after runtime can't have its COPY lines misattributed. No-op today
(runtime is the last stage); the copied set is unchanged.

* fix(test): assert at least one runtime COPY is parsed (no vacuous pass)

If the runtime FROM or the /app/gitnexus/ source prefix ever stops matching,
the copied set goes empty and the parity assertion passes vacuously. Add an
explicit copied.length>0 guard so that failure mode is loud and named.

* fix(test): strip line comments before require-scanning

requiredExternalAssets() regex-scanned raw source, so a future doc-comment such
as a commented-out require('../../web/x') in a shallow src file would resolve
outside dist/ and spuriously fail the parity guard. Strip // line comments
first. Block comments are deliberately not stripped (a naive block strip mangles
slash-star inside string/glob literals). Verified the real-tree scanner output
is byte-identical with and without the strip, and resolve-invocation.ts's
multi-line createRequire is still detected. (Also swaps a stray non-ASCII glyph
in the prior commit's comment for ASCII.)

* fix(test): account for aliased + computed module-load requires (fail-closed)

The parity scanner only matched string-literal require/createRequire, so it
missed module-load requires via aliased createRequire bindings and computed
paths — and already failed to see community-processor.ts's
`_require(leidenPath)` -> vendor/leiden, making the "every out-of-dist asset"
claim untrue.

Broaden the scan:
- Discover per-file createRequire bindings (requireCJS, _require, …) and match
  their literal-arg calls; keep the createRequire(...)('…') IIFE form.
- Detect COMPUTED (non-literal) requires and gate them on MODULE-LOAD position
  (brace-depth 0), so the four in-function computed requires that target
  node_modules/package.json (optional-grammars, native-check, capabilities,
  parse-cache) are correctly out of charter and ignored. A module-load computed
  require must be vetted in KNOWN_COMPUTED_REQUIRES (seed: community-processor ->
  vendor/leiden) or the test FAILS CLOSED for manual review.
- Allowlist entries are coverage-checked via isCovered, never trusted: a new
  test removes the `vendor` COPY from a fixture and asserts leiden surfaces as
  uncovered (so deleting a COPY can't silently pass — the #2130 class).
- Exclude `<id>.resolve(...)` (a path lookup, not a load).
- Upgrade the comment stripper to a string-aware pass that removes line AND
  block comments without mangling slash-star inside string/glob literals — the
  computed branch needs JSDoc requires (e.g. javascript/index.ts) gone, and the
  literal scan output stays byte-identical.

Honest claim wording: the 4th test now says coverage = resolvable + vetted
module-load requires, unrecognized computed requires fail for review. Adds
unit tests for fail-closed, aliased-literal, and in-function-ignored paths.

* fix(test): also scan shipped .cjs/.mjs assets for sibling requires

The guard only scanned src/**/*.ts, so hand-written shipped runtime files were
invisible — and they DO require siblings: hooks/claude/gitnexus-hook.cjs and
hooks/antigravity/gitnexus-antigravity-hook.cjs each require('./hook-lock.cjs'),
'./hook-db-lock-probe.cjs', './resolve-analyze-cmd.cjs'. Add a second pass over
shipped .cjs/.mjs assets (the runtime COPY set minus dep/data roots), resolving
each relative require against the asset's OWN package-relative dir and checking
COPY coverage — by prefix, NOT on-disk existence: the antigravity hook's
'./hook-lock.cjs' resolves to hooks/antigravity/hook-lock.cjs (which doesn't
physically exist; hook-lock.cjs lives under hooks/claude) yet is covered by the
whole-hooks COPY. All 6 shipped sibling requires resolve under the hooks COPY.

* fix(docker): move hooks/skills COPYs past the DuckDB FTS RUN

The hooks/ and skills/ COPYs sat between the vendor COPY and the DuckDB
FTS-extension install RUN, so any edit to hook/skill content invalidated that
RUN's cache layer — which performs a one-time network INSTALL of the extension
(~tens of seconds per affected build). The COPYs have no input dependency on the
DuckDB step; relocate them to after it (before USER node) so stable
infrastructure layers are not rebuilt on hook/skill churn. Image contents are
unchanged. The runtime-asset-parity guard still detects both (its scan covers
the whole runtime stage), and the two are consolidated under one comment.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 08:38:37 +01:00