@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.
- package/README.md +23 -0
- package/client-dist/200.html +1 -0
- package/client-dist/404.html +1 -0
- package/client-dist/_nuxt/B0m2ddpp.js +1 -0
- package/client-dist/_nuxt/BGXTw3vX.js +1 -0
- package/client-dist/_nuxt/BPQ3VLAy.js +1 -0
- package/client-dist/_nuxt/Buea-lGh.js +1 -0
- package/client-dist/_nuxt/Bx6rB4l6.js +30 -0
- package/client-dist/_nuxt/C5YZyYkj.js +1 -0
- package/client-dist/_nuxt/CKDU7L3G.js +203 -0
- package/client-dist/_nuxt/COt5Ahok.js +1 -0
- package/client-dist/_nuxt/CUdacoK5.js +1 -0
- package/client-dist/_nuxt/CV1c1Dy6.js +1 -0
- package/client-dist/_nuxt/CWz0CbFR.js +1 -0
- package/client-dist/_nuxt/CkyjGvrk.js +1 -0
- package/client-dist/_nuxt/Cp-IABpG.js +1 -0
- package/client-dist/_nuxt/Csfq5Kiy.js +1 -0
- package/client-dist/_nuxt/DBQR3AJH.js +1 -0
- package/client-dist/_nuxt/DCqp5aG1.js +1 -0
- package/client-dist/_nuxt/DEUVOdPa.js +1 -0
- package/client-dist/_nuxt/DNgT1MPt.js +1 -0
- package/client-dist/_nuxt/DnzrXTVv.js +1 -0
- package/client-dist/_nuxt/DuhV8rPH.js +1 -0
- package/client-dist/_nuxt/FeFRdv_D.js +1 -0
- package/client-dist/_nuxt/NavigationMenu.DYbev1Hq.css +1 -0
- package/client-dist/_nuxt/R6XH_530.js +1 -0
- package/client-dist/_nuxt/VmRZSMG_.js +1 -0
- package/client-dist/_nuxt/Yzrsuije.js +1 -0
- package/client-dist/_nuxt/_threadId_.DzBjRVHi.css +1 -0
- package/client-dist/_nuxt/builds/latest.json +1 -0
- package/client-dist/_nuxt/builds/meta/80bf8f95-1be2-47c1-8b19-e167bc50d12e.json +1 -0
- package/client-dist/_nuxt/entry.V8kD4EEO.css +1 -0
- package/client-dist/_nuxt/error-404.DdLqw2QE.css +1 -0
- package/client-dist/_nuxt/error-500.CdabC4ss.css +1 -0
- package/client-dist/_nuxt/sqIK1V-V.js +1 -0
- package/client-dist/_nuxt/wDzz0qaB.js +1 -0
- package/client-dist/index.html +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +138 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +80 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +11 -0
- package/dist/http-server.d.ts +21 -0
- package/dist/http-server.js +222 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/ports.d.ts +1 -0
- package/dist/ports.js +20 -0
- package/dist/process-manager.d.ts +28 -0
- package/dist/process-manager.js +157 -0
- package/dist/project-scanner.d.ts +2 -0
- package/dist/project-scanner.js +40 -0
- package/dist/runtime-store.d.ts +21 -0
- package/dist/runtime-store.js +79 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +1 -0
- 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,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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|