@gitkraken/shared-tools

v0.3.0

Published

Port interfaces, shared types, and undo-manifest machinery for @gitkraken core-tools.

Readme

@gitkraken/shared-tools

Port interfaces, shared types, and undo-manifest machinery for the core-tools monorepo. Domain packages (like @gitkraken/compose-tools) depend on this package; consumers implement its ports with their own git and AI providers.

Ports

GitPort

Raw git command execution. Throws on non-zero exit code.

interface GitPort {
    exec(args: string[], options?: GitExecOptions): Promise<string>;
}

interface GitExecOptions {
    env?: Record<string, string>;
    stdin?: string;
    signal?: AbortSignal;
}

All higher-level git operations (diffing, commit building, ref management, rebase orchestration, etc.) are utility functions in domain packages that compose calls to exec(). Consumers never implement higher-level operations on this interface — see ComposeGitPort in @gitkraken/compose-tools for the optional higher-level op surface specific to that domain.

signal is forwarded from every domain workflow that accepts an AbortSignal. Adapters should wire it into their git-process execution so cancellation terminates the process.

AiModelPort

Stateless text generation. Callers pass the full message history each call — session threading is the caller's responsibility, not the model's.

interface AiModelPort {
    generate(params: AiGenerateParams): Promise<AiGenerateResult>;
}

interface AiGenerateParams {
    system?: string;
    messages: AiMessage[];
    maxTokens?: number;
    temperature?: number;
    signal?: AbortSignal;
}

interface AiGenerateResult {
    text: string;
    usage?: AiTokenUsage;
}

signal is forwarded from every AI workflow. Adapters should wire it into the underlying LLM request so in-flight calls cancel.

StoragePort

Async key/value store for a library's own state, already scoped by the host. No TTL; get returns null for an absent key. createMemoryStorage() is a non-durable Map-backed implementation for tests.

interface StoragePort {
    get(key: string): Promise<string | null>;
    set(key: string, value: string): Promise<void>;
    delete(key: string): Promise<void>;
}

TelemetryPort

Fire-and-forget telemetry sink; implementations must not throw. noopTelemetry is the default when the host injects nothing. Callers pass attributes that can hold URLs or error text through redactUrlCredentials.

interface TelemetryPort {
    event(name: string, attributes?: TelemetryAttributes): void;
    exception(error: unknown, attributes?: TelemetryAttributes): void;
}

FileSystemPort

Filesystem access over absolute, host-supplied paths. Absence is null; remove is a no-op when missing and throws on a non-empty directory unless recursive is set; mode is a POSIX permission that non-POSIX hosts ignore.

interface FileSystemPort {
    readFile(path: string): Promise<string | null>;
    writeFile(path: string, content: string, options?: { mode?: number }): Promise<void>;
    remove(path: string, options?: { recursive?: boolean }): Promise<void>;
    readDir(path: string): Promise<string[] | null>;
}

Implementing the ports

Consumers wrap their own git and AI providers as adapters. Adapters can layer telemetry, caching, error handling, cancellation, priority queuing, or any other behavior — all invisible to the domain packages.

Example: GitPort adapter

import type { GitPort } from '@gitkraken/shared-tools';

const git: GitPort = {
    exec: async (args, options) => {
        const result = await myGitProvider.run(args, {
            cwd: '/path/to/repo',
            env: options?.env,
            stdin: options?.stdin,
            signal: options?.signal,
        });
        return result.stdout;
    },
};

Example: AiModelPort adapter

import type { AiModelPort } from '@gitkraken/shared-tools';

const ai: AiModelPort = {
    generate: async (params) => {
        const response = await myAiProvider.chat(params.messages, {
            system: params.system,
            maxTokens: params.maxTokens,
            temperature: params.temperature,
            signal: params.signal,
        });
        return {
            text: response.text,
            usage: { inputTokens: response.inputTokens, outputTokens: response.outputTokens },
        };
    },
};

Undo manifests

Domain packages use these primitives to record every git mutation an operation performs and later replay the inverse. The machinery is shared because its shape is general — any operation that moves refs or changes the workdir can build a manifest out of it.

capturePreOperationState(git, options?)

Snapshots the pre-operation state before the workflow touches anything:

  • Current HEAD SHA and branch name.
  • A detached stash commit of the current workdir (via git stash create) when the workdir is dirty — stored as a loose object, not added to the stash list.

Undo uses this snapshot to restore HEAD and the workdir. The capture helper is careful not to mutate the real git index when it builds the stash — it uses a temp index via GIT_INDEX_FILE so the consumer's staged state is never perturbed.

UndoStore

Reads and writes UndoManifest objects into the repo's object database under refs/undo/<id>. Manifests survive branch operations and are removed automatically when undo runs. Provides save(manifest), load(id), list(), delete(id).

generateUndoId()

Emits a stable UUID for a new undo manifest.

undoOperation(input) and validateUndo(input)

The core undo execution and dry-run validator. Domain packages (like compose) wrap these with their own input types — undoCompose in compose-tools delegates to undoOperation after translating.

Exports

Values:

  • createMemoryStorage
  • noopTelemetry
  • redactUrlCredentials
  • capturePreOperationState
  • generateUndoId
  • UndoStore
  • undoOperation
  • validateUndo

Types:

  • GitPort, GitExecOptions
  • AiModelPort, AiGenerateParams, AiGenerateResult, AiMessage, AiTokenUsage
  • StoragePort, TelemetryPort, TelemetryAttributes, FileSystemPort
  • AiCreditUsage, GetAiCreditUsage
  • PreOperationState, UndoManifest, UndoId
  • RefKind, RefIdentity, RefSnapshot, RefMovement
  • UndoBlocker, UndoWarning, UndoValidation
  • UndoInput, UndoForceOptions, UndoResult, ValidateUndoInput

Also exported: refKindToFullRefname(kind, name) for resolving a kind-tagged identity to a full refname.

Runtime environment

This package uses Node.js built-ins (node:crypto, node:fs, node:os, node:path) in the undo machinery. Type-only imports are safe in any environment; runtime usage of UndoStore, capturePreOperationState, generateUndoId, undoOperation, validateUndo requires a Node.js runtime.

Contributing

This package follows the monorepo's standard tooling — see the root CONTRIBUTING.md. Local commands:

pnpm --filter @gitkraken/shared-tools build
pnpm --filter @gitkraken/shared-tools typecheck
pnpm --filter @gitkraken/shared-tools test

Design constraints for changes to ports

The ports are intentionally minimal and stable. Adding methods to GitPort or AiModelPort is a breaking change for every consumer — every implementation must be updated. Before proposing a new method:

  1. Confirm the operation cannot be expressed as a utility function built on top of exec() or generate().
  2. Confirm the operation is needed by multiple domain packages (not just one).
  3. Confirm no consumer can reasonably implement it differently in their adapter.

A new port may land ahead of its consumers when a stacked change needs the contract in place before the packages that use it.

In practice, new methods should be rare. The default answer to "should this go in the port?" is no — put it in a domain package as a utility that calls exec() / generate() internally, or in a domain-specific port interface (see ComposeGitPort in compose-tools).