@codori/server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +23 -0
  2. package/client-dist/200.html +1 -0
  3. package/client-dist/404.html +1 -0
  4. package/client-dist/_nuxt/B0m2ddpp.js +1 -0
  5. package/client-dist/_nuxt/BGXTw3vX.js +1 -0
  6. package/client-dist/_nuxt/BPQ3VLAy.js +1 -0
  7. package/client-dist/_nuxt/Buea-lGh.js +1 -0
  8. package/client-dist/_nuxt/Bx6rB4l6.js +30 -0
  9. package/client-dist/_nuxt/C5YZyYkj.js +1 -0
  10. package/client-dist/_nuxt/CKDU7L3G.js +203 -0
  11. package/client-dist/_nuxt/COt5Ahok.js +1 -0
  12. package/client-dist/_nuxt/CUdacoK5.js +1 -0
  13. package/client-dist/_nuxt/CV1c1Dy6.js +1 -0
  14. package/client-dist/_nuxt/CWz0CbFR.js +1 -0
  15. package/client-dist/_nuxt/CkyjGvrk.js +1 -0
  16. package/client-dist/_nuxt/Cp-IABpG.js +1 -0
  17. package/client-dist/_nuxt/Csfq5Kiy.js +1 -0
  18. package/client-dist/_nuxt/DBQR3AJH.js +1 -0
  19. package/client-dist/_nuxt/DCqp5aG1.js +1 -0
  20. package/client-dist/_nuxt/DEUVOdPa.js +1 -0
  21. package/client-dist/_nuxt/DNgT1MPt.js +1 -0
  22. package/client-dist/_nuxt/DnzrXTVv.js +1 -0
  23. package/client-dist/_nuxt/DuhV8rPH.js +1 -0
  24. package/client-dist/_nuxt/FeFRdv_D.js +1 -0
  25. package/client-dist/_nuxt/NavigationMenu.DYbev1Hq.css +1 -0
  26. package/client-dist/_nuxt/R6XH_530.js +1 -0
  27. package/client-dist/_nuxt/VmRZSMG_.js +1 -0
  28. package/client-dist/_nuxt/Yzrsuije.js +1 -0
  29. package/client-dist/_nuxt/_threadId_.DzBjRVHi.css +1 -0
  30. package/client-dist/_nuxt/builds/latest.json +1 -0
  31. package/client-dist/_nuxt/builds/meta/80bf8f95-1be2-47c1-8b19-e167bc50d12e.json +1 -0
  32. package/client-dist/_nuxt/entry.V8kD4EEO.css +1 -0
  33. package/client-dist/_nuxt/error-404.DdLqw2QE.css +1 -0
  34. package/client-dist/_nuxt/error-500.CdabC4ss.css +1 -0
  35. package/client-dist/_nuxt/sqIK1V-V.js +1 -0
  36. package/client-dist/_nuxt/wDzz0qaB.js +1 -0
  37. package/client-dist/index.html +1 -0
  38. package/dist/cli.d.ts +2 -0
  39. package/dist/cli.js +138 -0
  40. package/dist/config.d.ts +8 -0
  41. package/dist/config.js +80 -0
  42. package/dist/errors.d.ts +6 -0
  43. package/dist/errors.js +11 -0
  44. package/dist/http-server.d.ts +21 -0
  45. package/dist/http-server.js +222 -0
  46. package/dist/index.d.ts +8 -0
  47. package/dist/index.js +7 -0
  48. package/dist/ports.d.ts +1 -0
  49. package/dist/ports.js +20 -0
  50. package/dist/process-manager.d.ts +28 -0
  51. package/dist/process-manager.js +157 -0
  52. package/dist/project-scanner.d.ts +2 -0
  53. package/dist/project-scanner.js +40 -0
  54. package/dist/runtime-store.d.ts +21 -0
  55. package/dist/runtime-store.js +79 -0
  56. package/dist/types.d.ts +40 -0
  57. package/dist/types.js +1 -0
  58. package/package.json +49 -0
@@ -0,0 +1,157 @@
1
+ import { spawn } from 'node:child_process';
2
+ import os from 'node:os';
3
+ import { resolveConfig } from './config.js';
4
+ import { CodoriError } from './errors.js';
5
+ import { findAvailablePort } from './ports.js';
6
+ import { scanProjects } from './project-scanner.js';
7
+ import { RuntimeStore } from './runtime-store.js';
8
+ const CODORI_STOP_TIMEOUT_MS = 3_000;
9
+ const CODORI_STOP_POLL_MS = 50;
10
+ const defaultCommandFactory = (port) => ({
11
+ command: process.env.CODORI_CODEX_BIN ?? 'codex',
12
+ args: ['app-server', '--listen', `ws://0.0.0.0:${port}`]
13
+ });
14
+ const isProcessAlive = (pid) => {
15
+ try {
16
+ process.kill(pid, 0);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ };
23
+ const wait = async (ms) => new Promise((resolvePromise) => {
24
+ setTimeout(resolvePromise, ms);
25
+ });
26
+ const waitForExit = async (pid, timeoutMs) => {
27
+ const deadline = Date.now() + timeoutMs;
28
+ while (Date.now() < deadline) {
29
+ if (!isProcessAlive(pid)) {
30
+ return true;
31
+ }
32
+ await wait(CODORI_STOP_POLL_MS);
33
+ }
34
+ return !isProcessAlive(pid);
35
+ };
36
+ const spawnDetached = async (command, args, cwd) => new Promise((resolvePromise, reject) => {
37
+ const child = spawn(command, args, {
38
+ cwd,
39
+ detached: true,
40
+ stdio: 'ignore',
41
+ windowsHide: true
42
+ });
43
+ child.once('error', reject);
44
+ child.once('spawn', () => {
45
+ child.unref();
46
+ resolvePromise(child);
47
+ });
48
+ });
49
+ export class RuntimeManager {
50
+ config;
51
+ store;
52
+ commandFactory;
53
+ constructor(options = {}) {
54
+ this.config = options.config ?? resolveConfig(options.configOverrides, options.homeDir);
55
+ this.store = new RuntimeStore(options.homeDir);
56
+ this.commandFactory = options.commandFactory ?? defaultCommandFactory;
57
+ }
58
+ listProjects() {
59
+ return scanProjects(this.config.root);
60
+ }
61
+ resolveProject(projectId) {
62
+ const project = this.listProjects().find(entry => entry.id === projectId);
63
+ if (!project) {
64
+ throw new CodoriError('PROJECT_NOT_FOUND', `Project "${projectId}" was not found under ${this.config.root}.`);
65
+ }
66
+ return project;
67
+ }
68
+ normalizeStatus(project, runtime, error) {
69
+ return {
70
+ projectId: project.id,
71
+ projectPath: project.path,
72
+ status: error ? 'error' : runtime ? 'running' : 'stopped',
73
+ pid: runtime?.pid ?? null,
74
+ port: runtime?.port ?? null,
75
+ startedAt: runtime?.startedAt ?? null,
76
+ error
77
+ };
78
+ }
79
+ readRunningRuntime(project) {
80
+ const loaded = this.store.load(project.path);
81
+ if (loaded.kind === 'missing') {
82
+ return this.normalizeStatus(project, null, null);
83
+ }
84
+ if (loaded.kind === 'invalid') {
85
+ return this.normalizeStatus(project, null, loaded.error);
86
+ }
87
+ if (!isProcessAlive(loaded.record.pid)) {
88
+ this.store.remove(project.path);
89
+ return this.normalizeStatus(project, null, null);
90
+ }
91
+ return this.normalizeStatus(project, loaded.record, null);
92
+ }
93
+ listProjectStatuses() {
94
+ return this.listProjects().map(project => this.readRunningRuntime(project));
95
+ }
96
+ getProjectStatus(projectId) {
97
+ return this.readRunningRuntime(this.resolveProject(projectId));
98
+ }
99
+ async startProject(projectId) {
100
+ const project = this.resolveProject(projectId);
101
+ const loaded = this.store.load(project.path);
102
+ if (loaded.kind === 'valid' && isProcessAlive(loaded.record.pid)) {
103
+ return {
104
+ ...this.normalizeStatus(project, loaded.record, null),
105
+ reusedExisting: true
106
+ };
107
+ }
108
+ if (loaded.kind !== 'missing') {
109
+ this.store.remove(project.path);
110
+ }
111
+ const port = await findAvailablePort(this.config.ports.start, this.config.ports.end);
112
+ const command = this.commandFactory(port, project);
113
+ const child = await spawnDetached(command.command, command.args, project.path);
114
+ if (typeof child.pid !== 'number') {
115
+ throw new CodoriError('PROCESS_START_FAILED', `Failed to determine PID for project "${projectId}".`);
116
+ }
117
+ const runtime = {
118
+ projectId: project.id,
119
+ projectPath: project.path,
120
+ pid: child.pid,
121
+ port,
122
+ startedAt: Date.now()
123
+ };
124
+ this.store.write(runtime);
125
+ return {
126
+ ...this.normalizeStatus(project, runtime, null),
127
+ reusedExisting: false
128
+ };
129
+ }
130
+ async stopProject(projectId) {
131
+ const project = this.resolveProject(projectId);
132
+ const loaded = this.store.load(project.path);
133
+ if (loaded.kind === 'missing') {
134
+ return this.normalizeStatus(project, null, null);
135
+ }
136
+ if (loaded.kind === 'invalid') {
137
+ this.store.remove(project.path);
138
+ return this.normalizeStatus(project, null, null);
139
+ }
140
+ if (isProcessAlive(loaded.record.pid)) {
141
+ process.kill(loaded.record.pid, 'SIGTERM');
142
+ const exited = await waitForExit(loaded.record.pid, CODORI_STOP_TIMEOUT_MS);
143
+ if (!exited) {
144
+ process.kill(loaded.record.pid, 'SIGKILL');
145
+ await waitForExit(loaded.record.pid, CODORI_STOP_TIMEOUT_MS);
146
+ }
147
+ }
148
+ this.store.remove(project.path);
149
+ return this.normalizeStatus(project, null, null);
150
+ }
151
+ }
152
+ export const createRuntimeManager = (options = {}) => new RuntimeManager({
153
+ homeDir: options.homeDir ?? os.homedir(),
154
+ configOverrides: options.configOverrides,
155
+ config: options.config,
156
+ commandFactory: options.commandFactory
157
+ });
@@ -0,0 +1,2 @@
1
+ import type { ProjectRecord } from './types.js';
2
+ export declare const scanProjects: (rootDirectory: string) => ProjectRecord[];
@@ -0,0 +1,40 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import { join, relative, resolve, sep } from 'node:path';
3
+ const IGNORED_DIRECTORY_NAMES = new Set([
4
+ '.git',
5
+ '.nuxt',
6
+ '.output',
7
+ 'build',
8
+ 'coverage',
9
+ 'dist',
10
+ 'node_modules'
11
+ ]);
12
+ const toProjectId = (root, path) => relative(root, path).split(sep).join('/');
13
+ export const scanProjects = (rootDirectory) => {
14
+ const root = resolve(rootDirectory);
15
+ const projects = [];
16
+ const queue = [root];
17
+ while (queue.length > 0) {
18
+ const current = queue.shift();
19
+ const entries = readdirSync(current, { withFileTypes: true });
20
+ if (entries.some(entry => entry.isDirectory() && entry.name === '.git')) {
21
+ if (current !== root) {
22
+ projects.push({
23
+ id: toProjectId(root, current),
24
+ path: current
25
+ });
26
+ }
27
+ continue;
28
+ }
29
+ for (const entry of entries) {
30
+ if (!entry.isDirectory()) {
31
+ continue;
32
+ }
33
+ if (IGNORED_DIRECTORY_NAMES.has(entry.name)) {
34
+ continue;
35
+ }
36
+ queue.push(join(current, entry.name));
37
+ }
38
+ }
39
+ return projects.sort((left, right) => left.id.localeCompare(right.id));
40
+ };
@@ -0,0 +1,21 @@
1
+ import type { RuntimeRecord } from './types.js';
2
+ export type RuntimeLoadResult = {
3
+ kind: 'missing';
4
+ path: string;
5
+ } | {
6
+ kind: 'invalid';
7
+ path: string;
8
+ error: string;
9
+ } | {
10
+ kind: 'valid';
11
+ path: string;
12
+ record: RuntimeRecord;
13
+ };
14
+ export declare class RuntimeStore {
15
+ readonly runDir: string;
16
+ constructor(homeDir?: string);
17
+ resolveRuntimePath(projectPath: string): string;
18
+ load(projectPath: string): RuntimeLoadResult;
19
+ write(record: RuntimeRecord): void;
20
+ remove(projectPath: string): void;
21
+ }
@@ -0,0 +1,79 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import os from 'node:os';
4
+ import { join, resolve } from 'node:path';
5
+ import { ensureCodoriDirectories, resolveCodoriHome } from './config.js';
6
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ const normalizeRuntimeRecord = (value) => {
8
+ if (!isRecord(value)) {
9
+ return null;
10
+ }
11
+ const { projectId, projectPath, pid, port, startedAt } = value;
12
+ if (typeof projectId !== 'string'
13
+ || typeof projectPath !== 'string'
14
+ || typeof pid !== 'number'
15
+ || !Number.isInteger(pid)
16
+ || typeof port !== 'number'
17
+ || !Number.isInteger(port)
18
+ || typeof startedAt !== 'number') {
19
+ return null;
20
+ }
21
+ return {
22
+ projectId,
23
+ projectPath,
24
+ pid,
25
+ port,
26
+ startedAt
27
+ };
28
+ };
29
+ export class RuntimeStore {
30
+ runDir;
31
+ constructor(homeDir = os.homedir()) {
32
+ ensureCodoriDirectories(homeDir);
33
+ this.runDir = join(resolveCodoriHome(homeDir), 'run');
34
+ }
35
+ resolveRuntimePath(projectPath) {
36
+ const normalizedProjectPath = resolve(projectPath);
37
+ const digest = createHash('sha256').update(normalizedProjectPath).digest('hex').slice(0, 16);
38
+ return join(this.runDir, `${digest}.pid.json`);
39
+ }
40
+ load(projectPath) {
41
+ const runtimePath = this.resolveRuntimePath(projectPath);
42
+ if (!existsSync(runtimePath)) {
43
+ return {
44
+ kind: 'missing',
45
+ path: runtimePath
46
+ };
47
+ }
48
+ try {
49
+ const parsed = JSON.parse(readFileSync(runtimePath, 'utf8'));
50
+ const record = normalizeRuntimeRecord(parsed);
51
+ if (!record) {
52
+ return {
53
+ kind: 'invalid',
54
+ path: runtimePath,
55
+ error: 'Runtime metadata is malformed.'
56
+ };
57
+ }
58
+ return {
59
+ kind: 'valid',
60
+ path: runtimePath,
61
+ record
62
+ };
63
+ }
64
+ catch (error) {
65
+ return {
66
+ kind: 'invalid',
67
+ path: runtimePath,
68
+ error: error instanceof Error ? error.message : String(error)
69
+ };
70
+ }
71
+ }
72
+ write(record) {
73
+ const runtimePath = this.resolveRuntimePath(record.projectPath);
74
+ writeFileSync(runtimePath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
75
+ }
76
+ remove(projectPath) {
77
+ rmSync(this.resolveRuntimePath(projectPath), { force: true });
78
+ }
79
+ }
@@ -0,0 +1,40 @@
1
+ export type CodoriConfig = {
2
+ root: string;
3
+ server: {
4
+ host: string;
5
+ port: number;
6
+ };
7
+ ports: {
8
+ start: number;
9
+ end: number;
10
+ };
11
+ };
12
+ export type ConfigOverrides = {
13
+ root?: string;
14
+ host?: string;
15
+ port?: number;
16
+ };
17
+ export type ProjectRecord = {
18
+ id: string;
19
+ path: string;
20
+ };
21
+ export type RuntimeRecord = {
22
+ projectId: string;
23
+ projectPath: string;
24
+ pid: number;
25
+ port: number;
26
+ startedAt: number;
27
+ };
28
+ export type ProjectRuntimeStatus = 'running' | 'stopped' | 'error';
29
+ export type ProjectStatusRecord = {
30
+ projectId: string;
31
+ projectPath: string;
32
+ status: ProjectRuntimeStatus;
33
+ pid: number | null;
34
+ port: number | null;
35
+ startedAt: number | null;
36
+ error: string | null;
37
+ };
38
+ export type StartProjectResult = ProjectStatusRecord & {
39
+ reusedExisting: boolean;
40
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@codori/server",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "description": "Codori server for Git project discovery, Codex runtime management, and bundled dashboard serving.",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "codori": "dist/cli.js"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/comfuture/codori.git",
18
+ "directory": "packages/server"
19
+ },
20
+ "homepage": "https://github.com/comfuture/codori#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/comfuture/codori/issues"
23
+ },
24
+ "keywords": [
25
+ "codori",
26
+ "codex",
27
+ "remote-coding",
28
+ "nuxt",
29
+ "fastify"
30
+ ],
31
+ "files": [
32
+ "dist",
33
+ "client-dist",
34
+ "README.md"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.build.json && node ./scripts/sync-client-bundle.mjs",
38
+ "lint": "eslint src test --ext .ts",
39
+ "prepack": "pnpm --dir ../client build && pnpm run build",
40
+ "typecheck": "tsc -p tsconfig.json",
41
+ "test": "vitest run"
42
+ },
43
+ "dependencies": {
44
+ "@fastify/static": "^9.1.0",
45
+ "@fastify/websocket": "^11.2.0",
46
+ "fastify": "^5.6.1",
47
+ "ws": "^8.18.3"
48
+ }
49
+ }