Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ githits code ... Dependency source inspection: search, files, read, grep
| `GITHITS_API_URL` | Override REST API URL | `https://api.githits.com` |
| `GITHITS_CODE_NAV_URL` | Override package/source service URL | `https://pkgseer.dev` |
| `GITHITS_TELEMETRY` | Emit end-of-run timing spans to stderr for local profiling | — |
| `GITHITS_DISABLE_UPDATE_CHECK` | Disable npm latest-version update notices | — |

## Manual Setup

Expand Down
6 changes: 6 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion docs/implementation/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Package/source access uses the package/source service URL from `GITHITS_CODE_NAV

## Local Storage

All configuration lives in `~/.githits/`:
Authentication state lives in `~/.githits/`:

```
~/.githits/ (0700)
Expand All @@ -62,6 +62,17 @@ All configuration lives in `~/.githits/`:

The secure file permissions prevent other users from reading tokens. When writing new files to `~/.githits/`, use `FileSystemService` rather than `node:fs` directly — this enables testing via mock implementations from `src/services/test-helpers.ts`.

Non-secret update-check state uses the XDG config location:

```
~/.config/githits/update-check.json
```

If `XDG_CONFIG_HOME` is set, the update-check cache lives under
`$XDG_CONFIG_HOME/githits/update-check.json`. See
`docs/implementation/update-check.md` for the update-check cache contract and
eligibility rules.

## How Config Flows Through the System

```
Expand Down Expand Up @@ -99,5 +110,6 @@ Commands receive the full `Dependencies` object. Services receive only what they
| `src/container.ts` | Auth priority logic and dependency wiring |
| `src/services/auth-storage.ts` | File-based token storage with secure permissions |
| `src/services/filesystem-service.ts` | File system abstraction for testable storage |
| `src/services/update-check-service.ts` | Non-secret update-check cache and npm latest lookup |
| `src/commands/auth-status.ts` | Diagnosing current auth state (reached via `githits auth status`) |
| `src/commands/mcp.ts` | MCP tool registration and deferred-auth startup behavior |
128 changes: 128 additions & 0 deletions docs/implementation/update-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Update Check

## Purpose

The CLI warns users when their installed `githits` package is behind the npm
`latest` dist-tag. This supports sticky installs such as `npm i -g githits`
without introducing forced updates, backend policy, or automatic package-manager
execution.

## Background

Generated MCP configs already run GitHits through:

```sh
npx -y githits@latest mcp start
```

That path asks the package manager for the latest published version at startup.
The update checker targets long-lived local/global installs where the binary
stays on the installed version until the user updates it.

## Behavior

The check is advisory only:

- it never blocks the command
- network, cache, and parse failures fail open silently
- notices are written to stderr only
- stdout is never touched
- every eligible invocation reports the stale version once a newer npm `latest`
is known

The notice format is:

```text
Update available: githits 0.2.0 -> 0.3.0
Run: npm i -g githits@latest
```

## Registry Source

The checker fetches npm dist-tags directly:

```text
https://registry.npmjs.org/-/package/githits/dist-tags
```

Only the `latest` field is used. The CLI does not shell out to `npm info`,
because subprocess behavior depends on the user's package-manager setup and is
slower than one HTTPS request.

## Cache

Update-check state is stored under the XDG config location:

```text
~/.config/githits/update-check.json
```

When `XDG_CONFIG_HOME` is set, the path is:

```text
$XDG_CONFIG_HOME/githits/update-check.json
```

The directory is created with mode `0o700`; the cache file is written with mode
`0o600`. The cache contains no credentials.

Cache shape:

```json
{
"checkedAt": "2026-04-28T12:00:00.000Z",
"latestVersion": "0.3.0"
}
```

The CLI checks npm at most once every 24 hours. If the cached latest version is
newer than the running CLI, the notice is printed on every eligible invocation.
When the cache is stale, the CLI refreshes npm first and falls back to the cached
notice if the refresh fails. A missing or malformed cache is treated as stale.
Concurrent writes use last-writer-wins semantics, and redundant fetches from
racing processes are acceptable.

## Eligibility

The CLI decides eligibility from raw argv and process state before Commander
parses the command.

Checks are skipped for:

- help and version invocations
- CI
- non-TTY stderr
- truthy `GITHITS_DISABLE_UPDATE_CHECK`
- likely ephemeral package-runner invocations (`npx`, `bunx`, npm/bun `exec`)
- MCP stdio server invocations

MCP has two forms:

- `githits mcp start` always starts the stdio server and is skipped
- `githits mcp` starts stdio when stdin or stdout is non-TTY and is skipped
- interactive `githits mcp` shows setup instructions and remains eligible

## Implementation

Key modules:

| File | Purpose |
|---|---|
| `src/services/update-check-service.ts` | Registry fetch, cache handling, eligibility helpers, notice formatting |
| `src/cli/update-check.ts` | Cancellable background task and post-command stderr notice |
| `src/cli.ts` | Starts and flushes the update-check task around Commander parsing |
| `src/services/update-check-service.test.ts` | Service and eligibility coverage |
| `src/cli/update-check.test.ts` | CLI orchestration coverage |

The service accepts injected dependencies for the current version, fetcher,
clock, and file-system service. Tests should mock those dependencies rather than
patch global state.

## Future Work

This mechanism is not a hard compatibility gate. Future phases can add:

- CDN-hosted version policy with `recommended` and `minimumSupported`
- recurring checks for long-running MCP servers
- `githits update`
- backend `426 Upgrade Required` enforcement
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@
"commander": "^14.0.2",
"jsonc-parser": "^3.3.1",
"open": "^11.0.0",
"semver": "^7.7.4",
"zod": "^4.1.13"
},
"devDependencies": {
"@biomejs/biome": "^2.3.8",
"@types/bun": "latest",
"@types/semver": "^7.7.1",
"bunup": "^0.16.10",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
Expand Down
28 changes: 26 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#!/usr/bin/env node
import { Command } from "commander";
import { version } from "../package.json";
import {
runWithUpdateCheckFlush,
startUpdateCheckTaskForInvocation,
} from "./cli/update-check.js";
import {
registerAuthStatusCommand,
registerCodeCommandGroup,
Expand All @@ -15,6 +19,10 @@ import {
registerPkgCommandGroup,
registerUnifiedSearchCommands,
} from "./commands/index.js";
import {
FileSystemServiceImpl,
NpmRegistryUpdateCheckService,
} from "./services/index.js";
import {
endTelemetrySpan,
flushTelemetry,
Expand All @@ -24,10 +32,23 @@ import {
} from "./shared/index.js";

const program = new Command();
const argv = process.argv.slice(2);
const commandSpans = new WeakMap<
Command,
ReturnType<typeof startTelemetrySpan>
>();
const updateCheckTask = startUpdateCheckTaskForInvocation({
args: argv,
env: process.env,
stderrIsTTY: process.stderr.isTTY === true,
stdinIsTTY: process.stdin.isTTY === true,
stdoutIsTTY: process.stdout.isTTY === true,
createService: () =>
new NpmRegistryUpdateCheckService({
currentVersion: version,
fileSystemService: new FileSystemServiceImpl(),
}),
});

if (isTelemetryEnabled()) {
process.once("exit", (exitCode) => {
Expand Down Expand Up @@ -82,7 +103,6 @@ registerMcpCommand(program);
registerExampleCommand(program);
registerLanguagesCommand(program);
registerFeedbackCommand(program);
const argv = process.argv.slice(2);
const registrationArgv = stripRootRegistrationOptions(argv);

if (shouldEagerLoadSearchCommands(registrationArgv)) {
Expand Down Expand Up @@ -113,7 +133,11 @@ const authCommand = program
.description("Manage authentication with GitHits.");
registerAuthStatusCommand(authCommand);

await withTelemetrySpan("cli.parse", () => program.parseAsync());
await runWithUpdateCheckFlush(
() => withTelemetrySpan("cli.parse", () => program.parseAsync()),
updateCheckTask,
{ stderr: process.stderr },
);

/**
* Commander supports root options before subcommands, e.g.
Expand Down
Loading
Loading