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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ Codex Git is a planned local Git surface for Codex Desktop. This repository curr
## Commands

```sh
npm install
npm ci
npm run dev
```

The placeholder UI is available at the URL printed by Vite. The server scaffold can be started separately:
The launcher attaches the placeholder Git Surface through the standalone Host Adapter and prints its URL plus the loopback health URL. The surface uses port `5173` by default; `CODEX_GIT_SURFACE_PORT` and `CODEX_GIT_PORT` override the listener ports.

The server scaffold can also be started separately:

```sh
npm run dev:server
Expand All @@ -43,14 +45,14 @@ npm run build
## Workspace layout

```text
apps/launcher Runtime composition placeholder
apps/launcher Standalone runtime composition root
apps/server Loopback server scaffold
apps/ui Standalone React placeholder surface
packages/protocol Shared protocol types
packages/repository-engine Repository session boundary only
packages/host-adapter Host Adapter boundary
packages/host-adapter/* Codex CDP and standalone adapter placeholders
tests/ Reserved contract, integration, and end-to-end layers
tests/ Contract, integration, end-to-end, and fixture layers
```

# codex-git
11 changes: 10 additions & 1 deletion apps/launcher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,14 @@
"name": "@codex-git/launcher",
"version": "0.0.0",
"private": true,
"type": "module"
"type": "module",
"exports": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"@codex-git/host-adapter-standalone": "*",
"@codex-git/server": "*"
},
"devDependencies": {
"vite": "8.2.2"
}
}
5 changes: 1 addition & 4 deletions apps/launcher/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
export const launcherState = {
product: 'codex-git',
status: 'not-implemented',
} as const;
export * from './standalone-runtime.js';
42 changes: 42 additions & 0 deletions apps/launcher/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { startStandaloneRuntime } from './index.js';

const runtime = await startStandaloneRuntime({
healthPort: readPort('CODEX_GIT_PORT', 0),
surfacePort: readPort('CODEX_GIT_SURFACE_PORT', 5173),
});

console.log(`Codex Git placeholder surface: ${runtime.surfaceUrl.href}`);
console.log(`Codex Git health endpoint: ${runtime.healthUrl.href}`);

let stopping = false;

function stop(): void {
if (stopping) {
return;
}

stopping = true;
void runtime.close().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
}

process.once('SIGINT', stop);
process.once('SIGTERM', stop);

function readPort(name: string, fallback: number): number {
const value = process.env[name];

if (value === undefined) {
return fallback;
}

const port = Number(value);

if (!Number.isInteger(port) || port < 0 || port > 65_535) {
throw new Error(`${name} must be an integer between 0 and 65535`);
}

return port;
}
123 changes: 123 additions & 0 deletions apps/launcher/src/standalone-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { Server } from 'node:http';
import { fileURLToPath } from 'node:url';

import { createAppServer } from '@codex-git/server';
import { StandaloneHostAdapter } from '@codex-git/host-adapter-standalone';
import { createServer as createViteServer, type ViteDevServer } from 'vite';

const loopbackHost = '127.0.0.1';
const uiConfigPath = fileURLToPath(
new URL('../../ui/vite.config.ts', import.meta.url),
);

export interface StandaloneRuntimeOptions {
readonly healthPort?: number;
readonly surfacePort?: number;
}

export interface StandaloneRuntime {
readonly healthUrl: URL;
readonly surfaceUrl: URL;
close(): Promise<void>;
}

export async function startStandaloneRuntime(
options: StandaloneRuntimeOptions = {},
): Promise<StandaloneRuntime> {
const healthServer = createAppServer();
let surfaceServer: ViteDevServer | undefined;
let hostConnection: Awaited<
ReturnType<StandaloneHostAdapter['attach']>
> | null = null;

async function closeResources(): Promise<void> {
await Promise.all([
hostConnection?.dispose(),
surfaceServer?.close(),
closeServer(healthServer),
]);
}

try {
await listen(healthServer, options.healthPort ?? 0);
const healthUrl = serverUrl(healthServer, '/health');

surfaceServer = await createViteServer({
configFile: uiConfigPath,
server: {
host: loopbackHost,
port: options.surfacePort ?? 5173,
strictPort: true,
},
});
await surfaceServer.listen();

const surfaceUrl = serverUrl(surfaceServer.httpServer, '/');
hostConnection = await new StandaloneHostAdapter().attach({
title: 'Codex Git',
url: surfaceUrl,
});

let closed = false;

return {
healthUrl,
surfaceUrl,
async close() {
if (closed) {
return;
}

closed = true;
await closeResources();
},
};
} catch (error) {
await closeResources();
throw error;
}
}

function listen(server: Server, port: number): Promise<void> {
return new Promise((resolve, reject) => {
const handleError = (error: Error) => {
server.off('listening', handleListening);
reject(error);
};
const handleListening = () => {
server.off('error', handleError);
resolve();
};

server.once('error', handleError);
server.once('listening', handleListening);
server.listen(port, loopbackHost);
});
}

function serverUrl(
server: Pick<Server, 'address'> | null,
pathname: string,
): URL {
const address = server?.address();

if (
address === undefined ||
address === null ||
typeof address === 'string'
) {
throw new Error('Standalone runtime server has no TCP address');
}

return new URL(pathname, `http://${loopbackHost}:${address.port}`);
}

function closeServer(server: Server): Promise<void> {
if (!server.listening) {
return Promise.resolve();
}

return new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
7 changes: 6 additions & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,10 @@
"name": "@codex-git/server",
"version": "0.0.0",
"private": true,
"type": "module"
"type": "module",
"exports": "./src/server.ts",
"types": "./src/server.ts",
"dependencies": {
"@codex-git/protocol": "*"
}
}
26 changes: 26 additions & 0 deletions apps/ui/lint-rules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const uiNodeImportGuard = {
files: ['apps/ui/**/*.{ts,tsx}'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: [
'child_process',
'child_process/*',
'fs',
'fs/*',
'node:child_process',
'node:child_process/*',
'node:fs',
'node:fs/*',
],
message:
'The Git Surface cannot import Node filesystem or child-process modules.',
},
],
},
],
},
};
3 changes: 3 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';

import { uiNodeImportGuard } from './apps/ui/lint-rules.js';

export default tseslint.config(
{
ignores: ['coverage/', 'dist/', 'node_modules/'],
Expand All @@ -24,4 +26,5 @@ export default tseslint.config(
],
},
},
uiNodeImportGuard,
);
14 changes: 12 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
],
"scripts": {
"build": "npm run typecheck && vite build --config apps/ui/vite.config.ts",
"dev": "vite --config apps/ui/vite.config.ts",
"dev": "tsx apps/launcher/src/main.ts",
"dev:server": "tsx watch apps/server/src/main.ts",
"format": "prettier --write .",
"format:check": "prettier --check .",
Expand Down
2 changes: 2 additions & 0 deletions packages/host-adapter/standalone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"version": "0.0.0",
"private": true,
"type": "module",
"exports": "./src/index.ts",
"types": "./src/index.ts",
"dependencies": {
"@codex-git/host-adapter": "*"
}
Expand Down
31 changes: 31 additions & 0 deletions packages/host-adapter/standalone/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type {
HostAdapter,
HostConnection,
HostContext,
NativeActionResult,
SurfaceDescriptor,
} from '@codex-git/host-adapter';

const standaloneContext = {
projectPath: null,
theme: 'system',
} satisfies HostContext;

class StandaloneHostConnection implements HostConnection {
async *contexts(): AsyncIterable<HostContext> {
yield standaloneContext;
}

async perform(): Promise<NativeActionResult> {
return { status: 'unsupported' };
}

async dispose(): Promise<void> {}
}

export class StandaloneHostAdapter implements HostAdapter {
async attach(surface: SurfaceDescriptor): Promise<HostConnection> {
void surface;
return new StandaloneHostConnection();
}
}
4 changes: 1 addition & 3 deletions packages/host-adapter/standalone/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
import type { HostAdapter } from '@codex-git/host-adapter';

export type StandaloneHostAdapterContract = HostAdapter;
export { StandaloneHostAdapter } from './adapter.js';
2 changes: 1 addition & 1 deletion tests/contract/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Contract tests

Repository Engine and protocol contract tests will live here once their behavior is implemented.
Public module contracts are verified here. The bootstrap covers the standalone Host Adapter; Repository Engine and protocol contracts join this layer with their implementation issues.
25 changes: 25 additions & 0 deletions tests/contract/standalone-host-adapter.contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';

import { StandaloneHostAdapter } from '@codex-git/host-adapter-standalone';

describe('StandaloneHostAdapter contract', () => {
it('publishes the standalone Host Context after attaching a surface', async () => {
const adapter = new StandaloneHostAdapter();
const connection = await adapter.attach({
title: 'Codex Git',
url: new URL('http://127.0.0.1:4173'),
});

const contexts = connection.contexts()[Symbol.asyncIterator]();

expect(await contexts.next()).toEqual({
done: false,
value: {
projectPath: null,
theme: 'system',
},
});

await connection.dispose();
});
});
2 changes: 1 addition & 1 deletion tests/e2e/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# End-to-end tests

Standalone and Codex-hosted end-to-end tests will live here once Host Adapters are implemented.
Complete runtime tests live here. The bootstrap exercises the standalone launcher, health endpoint, Host Adapter, and placeholder Git Surface; Codex-hosted coverage joins this layer with its Host Adapter issue.
Loading