@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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { asErrorMessage, CodoriError } from './errors.js';
|
|
4
|
+
import { startHttpServer } from './http-server.js';
|
|
5
|
+
import { createRuntimeManager } from './process-manager.js';
|
|
6
|
+
const printJson = (value) => {
|
|
7
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
8
|
+
};
|
|
9
|
+
const printStatuses = (records) => {
|
|
10
|
+
for (const record of records) {
|
|
11
|
+
const runtimeDetails = [
|
|
12
|
+
record.status,
|
|
13
|
+
record.port ? `port=${record.port}` : null,
|
|
14
|
+
record.pid ? `pid=${record.pid}` : null
|
|
15
|
+
].filter(Boolean).join(' ');
|
|
16
|
+
process.stdout.write(`${record.projectId}\t${runtimeDetails}\n`);
|
|
17
|
+
if (record.error) {
|
|
18
|
+
process.stdout.write(` error: ${record.error}\n`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const printStartResult = (result) => {
|
|
23
|
+
const action = result.reusedExisting ? 'reused' : 'started';
|
|
24
|
+
process.stdout.write(`${result.projectId}\t${action}\tport=${result.port}\tpid=${result.pid}\n`);
|
|
25
|
+
};
|
|
26
|
+
const optionConfig = {
|
|
27
|
+
root: {
|
|
28
|
+
type: 'string'
|
|
29
|
+
},
|
|
30
|
+
host: {
|
|
31
|
+
type: 'string'
|
|
32
|
+
},
|
|
33
|
+
port: {
|
|
34
|
+
type: 'string'
|
|
35
|
+
},
|
|
36
|
+
json: {
|
|
37
|
+
type: 'boolean'
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const coercePort = (value) => {
|
|
41
|
+
if (!value) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const parsed = Number.parseInt(value, 10);
|
|
45
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
46
|
+
};
|
|
47
|
+
const resolveCliRoot = (value) => value ?? process.cwd();
|
|
48
|
+
const main = async () => {
|
|
49
|
+
const parsed = parseArgs({
|
|
50
|
+
allowPositionals: true,
|
|
51
|
+
options: optionConfig
|
|
52
|
+
});
|
|
53
|
+
const [command = 'serve', maybeProjectId] = parsed.positionals;
|
|
54
|
+
const manager = createRuntimeManager({
|
|
55
|
+
configOverrides: {
|
|
56
|
+
root: resolveCliRoot(parsed.values.root),
|
|
57
|
+
host: parsed.values.host,
|
|
58
|
+
port: coercePort(parsed.values.port)
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const json = parsed.values.json ?? false;
|
|
62
|
+
switch (command) {
|
|
63
|
+
case 'list': {
|
|
64
|
+
const statuses = manager.listProjectStatuses();
|
|
65
|
+
if (json) {
|
|
66
|
+
printJson(statuses);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
printStatuses(statuses);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
case 'status': {
|
|
74
|
+
if (maybeProjectId) {
|
|
75
|
+
const status = manager.getProjectStatus(maybeProjectId);
|
|
76
|
+
if (json) {
|
|
77
|
+
printJson(status);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
printStatuses([status]);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const statuses = manager.listProjectStatuses();
|
|
85
|
+
if (json) {
|
|
86
|
+
printJson(statuses);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
printStatuses(statuses);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
case 'start': {
|
|
94
|
+
if (!maybeProjectId) {
|
|
95
|
+
throw new CodoriError('MISSING_PROJECT_ID', 'The start command requires a project id.');
|
|
96
|
+
}
|
|
97
|
+
const result = await manager.startProject(maybeProjectId);
|
|
98
|
+
if (json) {
|
|
99
|
+
printJson(result);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
printStartResult(result);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
case 'stop': {
|
|
107
|
+
if (!maybeProjectId) {
|
|
108
|
+
throw new CodoriError('MISSING_PROJECT_ID', 'The stop command requires a project id.');
|
|
109
|
+
}
|
|
110
|
+
const result = await manager.stopProject(maybeProjectId);
|
|
111
|
+
if (json) {
|
|
112
|
+
printJson(result);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
printStatuses([result]);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
case 'serve': {
|
|
120
|
+
const app = await startHttpServer(manager);
|
|
121
|
+
process.stdout.write(`Running codori server with project root directory: ${manager.config.root}\n`);
|
|
122
|
+
process.stdout.write(`Codori listening on http://${manager.config.server.host}:${manager.config.server.port}\n`);
|
|
123
|
+
await app.ready();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
default:
|
|
127
|
+
process.stdout.write('Usage: npx @codori/server [serve|list|status|start|stop] [projectId] --root <path> [--host <host>] [--port <port>] [--json]\n');
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
void main().catch((error) => {
|
|
131
|
+
if (error instanceof CodoriError) {
|
|
132
|
+
process.stderr.write(`${error.code}: ${error.message}\n`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
process.stderr.write(`${asErrorMessage(error)}\n`);
|
|
136
|
+
}
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CodoriConfig, ConfigOverrides } from './types.js';
|
|
2
|
+
export declare const resolveCodoriHome: (homeDir?: string) => string;
|
|
3
|
+
export declare const resolveCodoriConfigPath: (homeDir?: string) => string;
|
|
4
|
+
export declare const ensureCodoriDirectories: (homeDir?: string) => {
|
|
5
|
+
codoriHome: string;
|
|
6
|
+
runDir: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const resolveConfig: (overrides?: ConfigOverrides, homeDir?: string) => CodoriConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { CodoriError } from './errors.js';
|
|
5
|
+
const DEFAULT_SERVER_HOST = '127.0.0.1';
|
|
6
|
+
const DEFAULT_SERVER_PORT = 4310;
|
|
7
|
+
const DEFAULT_PORT_START = 46000;
|
|
8
|
+
const DEFAULT_PORT_END = 46999;
|
|
9
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
export const resolveCodoriHome = (homeDir = os.homedir()) => join(homeDir, '.codori');
|
|
11
|
+
export const resolveCodoriConfigPath = (homeDir = os.homedir()) => join(resolveCodoriHome(homeDir), 'config.json');
|
|
12
|
+
export const ensureCodoriDirectories = (homeDir = os.homedir()) => {
|
|
13
|
+
const codoriHome = resolveCodoriHome(homeDir);
|
|
14
|
+
mkdirSync(codoriHome, { recursive: true });
|
|
15
|
+
mkdirSync(join(codoriHome, 'run'), { recursive: true });
|
|
16
|
+
return {
|
|
17
|
+
codoriHome,
|
|
18
|
+
runDir: join(codoriHome, 'run')
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const loadUserConfig = (homeDir = os.homedir()) => {
|
|
22
|
+
const configPath = resolveCodoriConfigPath(homeDir);
|
|
23
|
+
if (!existsSync(configPath)) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new CodoriError('INVALID_CONFIG', `Failed to parse Codori config at ${configPath}.`, error);
|
|
32
|
+
}
|
|
33
|
+
if (!isRecord(parsed)) {
|
|
34
|
+
throw new CodoriError('INVALID_CONFIG', `Codori config at ${configPath} must be a JSON object.`);
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
};
|
|
38
|
+
const ensureValidPort = (value, label) => {
|
|
39
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0 || value > 65535) {
|
|
40
|
+
throw new CodoriError('INVALID_CONFIG', `${label} must be an integer between 1 and 65535.`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
export const resolveConfig = (overrides = {}, homeDir = os.homedir()) => {
|
|
45
|
+
const fileConfig = loadUserConfig(homeDir);
|
|
46
|
+
const root = overrides.root ?? fileConfig.root;
|
|
47
|
+
if (!root || typeof root !== 'string') {
|
|
48
|
+
throw new CodoriError('MISSING_ROOT', 'Project root is required. Pass --root or set ~/.codori/config.json.');
|
|
49
|
+
}
|
|
50
|
+
const host = overrides.host
|
|
51
|
+
?? (typeof fileConfig.server?.host === 'string' ? fileConfig.server.host : undefined)
|
|
52
|
+
?? DEFAULT_SERVER_HOST;
|
|
53
|
+
const port = overrides.port
|
|
54
|
+
?? (typeof fileConfig.server?.port === 'number' ? fileConfig.server.port : undefined)
|
|
55
|
+
?? DEFAULT_SERVER_PORT;
|
|
56
|
+
const portStart = typeof fileConfig.ports?.start === 'number'
|
|
57
|
+
? fileConfig.ports.start
|
|
58
|
+
: DEFAULT_PORT_START;
|
|
59
|
+
const portEnd = typeof fileConfig.ports?.end === 'number'
|
|
60
|
+
? fileConfig.ports.end
|
|
61
|
+
: DEFAULT_PORT_END;
|
|
62
|
+
const resolvedPort = ensureValidPort(port, 'server.port');
|
|
63
|
+
const resolvedPortStart = ensureValidPort(portStart, 'ports.start');
|
|
64
|
+
const resolvedPortEnd = ensureValidPort(portEnd, 'ports.end');
|
|
65
|
+
if (resolvedPortStart > resolvedPortEnd) {
|
|
66
|
+
throw new CodoriError('INVALID_CONFIG', 'ports.start must be less than or equal to ports.end.');
|
|
67
|
+
}
|
|
68
|
+
ensureCodoriDirectories(homeDir);
|
|
69
|
+
return {
|
|
70
|
+
root: resolve(root),
|
|
71
|
+
server: {
|
|
72
|
+
host,
|
|
73
|
+
port: resolvedPort
|
|
74
|
+
},
|
|
75
|
+
ports: {
|
|
76
|
+
start: resolvedPortStart,
|
|
77
|
+
end: resolvedPortEnd
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
};
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class CodoriError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
details;
|
|
4
|
+
constructor(code, message, details) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'CodoriError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export const asErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Fastify, { type FastifyInstance } from 'fastify';
|
|
2
|
+
import type { ProjectStatusRecord, StartProjectResult } from './types.js';
|
|
3
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
export type RuntimeManagerLike = {
|
|
5
|
+
listProjectStatuses: () => MaybePromise<ProjectStatusRecord[]>;
|
|
6
|
+
getProjectStatus: (projectId: string) => MaybePromise<ProjectStatusRecord>;
|
|
7
|
+
startProject: (projectId: string) => MaybePromise<StartProjectResult>;
|
|
8
|
+
stopProject: (projectId: string) => MaybePromise<ProjectStatusRecord>;
|
|
9
|
+
config?: {
|
|
10
|
+
server: {
|
|
11
|
+
host: string;
|
|
12
|
+
port: number;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type HttpServerOptions = {
|
|
17
|
+
clientBundleDir?: string | null;
|
|
18
|
+
};
|
|
19
|
+
export declare const createHttpServer: (manager: RuntimeManagerLike, options?: HttpServerOptions) => Promise<FastifyInstance>;
|
|
20
|
+
export declare const startHttpServer: (manager?: import("./process-manager.js").RuntimeManager) => Promise<Fastify.FastifyInstance<Fastify.RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, Fastify.FastifyBaseLogger, Fastify.FastifyTypeProviderDefault>>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import fastifyStatic from '@fastify/static';
|
|
6
|
+
import websocket from '@fastify/websocket';
|
|
7
|
+
import Fastify, {} from 'fastify';
|
|
8
|
+
import WebSocket from 'ws';
|
|
9
|
+
import { CodoriError } from './errors.js';
|
|
10
|
+
import { createRuntimeManager } from './process-manager.js';
|
|
11
|
+
const isCodoriError = (error) => error instanceof CodoriError;
|
|
12
|
+
const resolveBundledClientDir = () => {
|
|
13
|
+
const candidates = [
|
|
14
|
+
fileURLToPath(new URL('../client-dist', import.meta.url)),
|
|
15
|
+
fileURLToPath(new URL('../../client/.output/public', import.meta.url))
|
|
16
|
+
];
|
|
17
|
+
for (const candidate of candidates) {
|
|
18
|
+
if (existsSync(join(candidate, 'index.html'))) {
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
24
|
+
const toRequestPath = (url) => url.split('?')[0]?.split('#')[0] ?? url;
|
|
25
|
+
const isAssetRequest = (pathname) => /\.[a-z0-9]+$/i.test(pathname);
|
|
26
|
+
const toStatusCode = (error) => {
|
|
27
|
+
switch (error.code) {
|
|
28
|
+
case 'PROJECT_NOT_FOUND':
|
|
29
|
+
return 404;
|
|
30
|
+
case 'INVALID_CONFIG':
|
|
31
|
+
case 'MISSING_PROJECT_ID':
|
|
32
|
+
case 'MISSING_ROOT':
|
|
33
|
+
return 400;
|
|
34
|
+
default:
|
|
35
|
+
return 500;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const getProjectIdFromRequest = (value) => {
|
|
39
|
+
if (!value) {
|
|
40
|
+
throw new CodoriError('MISSING_PROJECT_ID', 'Missing project id.');
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
const resolveValue = async (value) => value;
|
|
45
|
+
const wait = async (ms) => new Promise((resolvePromise) => {
|
|
46
|
+
setTimeout(resolvePromise, ms);
|
|
47
|
+
});
|
|
48
|
+
const canConnectToPort = (port, host = '127.0.0.1', timeoutMs = 200) => new Promise((resolvePromise) => {
|
|
49
|
+
const socket = net.createConnection({ host, port });
|
|
50
|
+
let settled = false;
|
|
51
|
+
const settle = (value) => {
|
|
52
|
+
if (settled) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
settled = true;
|
|
56
|
+
socket.removeAllListeners();
|
|
57
|
+
socket.destroy();
|
|
58
|
+
resolvePromise(value);
|
|
59
|
+
};
|
|
60
|
+
socket.once('connect', () => settle(true));
|
|
61
|
+
socket.once('error', () => settle(false));
|
|
62
|
+
socket.setTimeout(timeoutMs, () => settle(false));
|
|
63
|
+
});
|
|
64
|
+
const waitForPortReady = async (port, host = '127.0.0.1', timeoutMs = 5_000) => {
|
|
65
|
+
const deadline = Date.now() + timeoutMs;
|
|
66
|
+
while (Date.now() < deadline) {
|
|
67
|
+
if (await canConnectToPort(port, host)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
await wait(100);
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
export const createHttpServer = async (manager, options = {}) => {
|
|
75
|
+
const app = Fastify({
|
|
76
|
+
logger: false
|
|
77
|
+
});
|
|
78
|
+
const clientBundleDir = options.clientBundleDir === undefined
|
|
79
|
+
? resolveBundledClientDir()
|
|
80
|
+
: options.clientBundleDir;
|
|
81
|
+
await app.register(websocket);
|
|
82
|
+
if (clientBundleDir) {
|
|
83
|
+
await app.register(fastifyStatic, {
|
|
84
|
+
root: clientBundleDir
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
app.setErrorHandler((error, _request, reply) => {
|
|
88
|
+
if (isCodoriError(error)) {
|
|
89
|
+
reply.status(toStatusCode(error)).send({
|
|
90
|
+
error: {
|
|
91
|
+
code: error.code,
|
|
92
|
+
message: error.message,
|
|
93
|
+
details: error.details ?? null
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
reply.status(500).send({
|
|
99
|
+
error: {
|
|
100
|
+
code: 'INTERNAL_ERROR',
|
|
101
|
+
message: error instanceof Error ? error.message : 'Unknown error.'
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
app.get('/api/projects', async () => ({
|
|
106
|
+
projects: await resolveValue(manager.listProjectStatuses())
|
|
107
|
+
}));
|
|
108
|
+
app.get('/api/projects/:projectId', async (request) => ({
|
|
109
|
+
project: await resolveValue(manager.getProjectStatus(getProjectIdFromRequest(request.params.projectId)))
|
|
110
|
+
}));
|
|
111
|
+
app.get('/api/projects/:projectId/status', async (request) => ({
|
|
112
|
+
project: await resolveValue(manager.getProjectStatus(getProjectIdFromRequest(request.params.projectId)))
|
|
113
|
+
}));
|
|
114
|
+
app.post('/api/projects/:projectId/start', async (request) => ({
|
|
115
|
+
project: await resolveValue(manager.startProject(getProjectIdFromRequest(request.params.projectId)))
|
|
116
|
+
}));
|
|
117
|
+
app.post('/api/projects/:projectId/stop', async (request) => ({
|
|
118
|
+
project: await resolveValue(manager.stopProject(getProjectIdFromRequest(request.params.projectId)))
|
|
119
|
+
}));
|
|
120
|
+
app.get('/api/projects/:projectId/rpc', { websocket: true }, async (clientSocket, request) => {
|
|
121
|
+
const projectId = getProjectIdFromRequest(request.params.projectId);
|
|
122
|
+
const pendingClientMessages = [];
|
|
123
|
+
let upstream = null;
|
|
124
|
+
const closeBoth = (code = 1011, reason = 'proxy error') => {
|
|
125
|
+
if (clientSocket.readyState === clientSocket.OPEN || clientSocket.readyState === clientSocket.CONNECTING) {
|
|
126
|
+
clientSocket.close(code, reason);
|
|
127
|
+
}
|
|
128
|
+
if (upstream && (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING)) {
|
|
129
|
+
upstream.close(code, reason);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
clientSocket.on('message', (message, isBinary) => {
|
|
133
|
+
if (upstream?.readyState === WebSocket.OPEN) {
|
|
134
|
+
upstream.send(message, { binary: isBinary });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
pendingClientMessages.push({ message, isBinary });
|
|
138
|
+
});
|
|
139
|
+
clientSocket.on('error', () => {
|
|
140
|
+
closeBoth(1011, 'client websocket failed');
|
|
141
|
+
});
|
|
142
|
+
clientSocket.on('close', () => {
|
|
143
|
+
if (upstream && (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING)) {
|
|
144
|
+
upstream.close();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
void (async () => {
|
|
148
|
+
const started = await resolveValue(manager.startProject(projectId));
|
|
149
|
+
if (typeof started.port !== 'number') {
|
|
150
|
+
closeBoth(1011, 'runtime port unavailable');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const ready = await waitForPortReady(started.port);
|
|
154
|
+
if (!ready) {
|
|
155
|
+
closeBoth(1011, 'runtime did not become ready');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
upstream = new WebSocket(`ws://127.0.0.1:${started.port}`);
|
|
159
|
+
upstream.once('open', () => {
|
|
160
|
+
for (const entry of pendingClientMessages.splice(0, pendingClientMessages.length)) {
|
|
161
|
+
upstream?.send(entry.message, { binary: entry.isBinary });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
upstream.on('message', (message, isBinary) => {
|
|
165
|
+
clientSocket.send(message, { binary: isBinary });
|
|
166
|
+
});
|
|
167
|
+
upstream.on('error', () => {
|
|
168
|
+
closeBoth(1011, 'upstream websocket failed');
|
|
169
|
+
});
|
|
170
|
+
upstream.on('close', () => {
|
|
171
|
+
if (clientSocket.readyState === clientSocket.OPEN || clientSocket.readyState === clientSocket.CONNECTING) {
|
|
172
|
+
clientSocket.close();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
})().catch(() => {
|
|
176
|
+
closeBoth(1011, 'upstream bootstrap failed');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
if (clientBundleDir) {
|
|
180
|
+
app.setNotFoundHandler((request, reply) => {
|
|
181
|
+
const requestPath = toRequestPath(request.url);
|
|
182
|
+
const acceptsHtml = request.headers.accept?.includes('text/html') ?? false;
|
|
183
|
+
if (request.method !== 'GET') {
|
|
184
|
+
return reply.status(404).send({
|
|
185
|
+
error: {
|
|
186
|
+
code: 'NOT_FOUND',
|
|
187
|
+
message: 'Route not found.'
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (requestPath.startsWith('/api/')) {
|
|
192
|
+
return reply.status(404).send({
|
|
193
|
+
error: {
|
|
194
|
+
code: 'NOT_FOUND',
|
|
195
|
+
message: 'Route not found.'
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (isAssetRequest(requestPath) && !acceptsHtml) {
|
|
200
|
+
return reply.status(404).send({
|
|
201
|
+
error: {
|
|
202
|
+
code: 'NOT_FOUND',
|
|
203
|
+
message: 'Asset not found.'
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return reply.type('text/html').sendFile('index.html');
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return app;
|
|
211
|
+
};
|
|
212
|
+
export const startHttpServer = async (manager = createRuntimeManager()) => {
|
|
213
|
+
const app = await createHttpServer(manager);
|
|
214
|
+
if (!manager.config) {
|
|
215
|
+
throw new CodoriError('INVALID_CONFIG', 'Manager config is required to start the HTTP server.');
|
|
216
|
+
}
|
|
217
|
+
await app.listen({
|
|
218
|
+
host: manager.config.server.host,
|
|
219
|
+
port: manager.config.server.port
|
|
220
|
+
});
|
|
221
|
+
return app;
|
|
222
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { resolveConfig, resolveCodoriConfigPath, resolveCodoriHome } from './config.js';
|
|
2
|
+
export { CodoriError } from './errors.js';
|
|
3
|
+
export { createHttpServer, startHttpServer } from './http-server.js';
|
|
4
|
+
export { findAvailablePort } from './ports.js';
|
|
5
|
+
export { createRuntimeManager, RuntimeManager } from './process-manager.js';
|
|
6
|
+
export { scanProjects } from './project-scanner.js';
|
|
7
|
+
export { RuntimeStore } from './runtime-store.js';
|
|
8
|
+
export type * from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { resolveConfig, resolveCodoriConfigPath, resolveCodoriHome } from './config.js';
|
|
2
|
+
export { CodoriError } from './errors.js';
|
|
3
|
+
export { createHttpServer, startHttpServer } from './http-server.js';
|
|
4
|
+
export { findAvailablePort } from './ports.js';
|
|
5
|
+
export { createRuntimeManager, RuntimeManager } from './process-manager.js';
|
|
6
|
+
export { scanProjects } from './project-scanner.js';
|
|
7
|
+
export { RuntimeStore } from './runtime-store.js';
|
package/dist/ports.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const findAvailablePort: (start: number, end: number) => Promise<number>;
|
package/dist/ports.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { CodoriError } from './errors.js';
|
|
3
|
+
const canListenOnPort = (port) => new Promise((resolve) => {
|
|
4
|
+
const server = net.createServer();
|
|
5
|
+
server.once('error', () => {
|
|
6
|
+
resolve(false);
|
|
7
|
+
});
|
|
8
|
+
server.once('listening', () => {
|
|
9
|
+
server.close(() => resolve(true));
|
|
10
|
+
});
|
|
11
|
+
server.listen(port, '0.0.0.0');
|
|
12
|
+
});
|
|
13
|
+
export const findAvailablePort = async (start, end) => {
|
|
14
|
+
for (let port = start; port <= end; port += 1) {
|
|
15
|
+
if (await canListenOnPort(port)) {
|
|
16
|
+
return port;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
throw new CodoriError('NO_FREE_PORT', `No free TCP port is available in the configured range ${start}-${end}.`);
|
|
20
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RuntimeStore } from './runtime-store.js';
|
|
2
|
+
import type { CodoriConfig, ConfigOverrides, ProjectRecord, ProjectStatusRecord, StartProjectResult } from './types.js';
|
|
3
|
+
type CommandFactory = (port: number, project: ProjectRecord) => {
|
|
4
|
+
command: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
};
|
|
7
|
+
type RuntimeManagerOptions = {
|
|
8
|
+
homeDir?: string;
|
|
9
|
+
configOverrides?: ConfigOverrides;
|
|
10
|
+
config?: CodoriConfig;
|
|
11
|
+
commandFactory?: CommandFactory;
|
|
12
|
+
};
|
|
13
|
+
export declare class RuntimeManager {
|
|
14
|
+
readonly config: CodoriConfig;
|
|
15
|
+
readonly store: RuntimeStore;
|
|
16
|
+
private readonly commandFactory;
|
|
17
|
+
constructor(options?: RuntimeManagerOptions);
|
|
18
|
+
listProjects(): ProjectRecord[];
|
|
19
|
+
private resolveProject;
|
|
20
|
+
private normalizeStatus;
|
|
21
|
+
private readRunningRuntime;
|
|
22
|
+
listProjectStatuses(): ProjectStatusRecord[];
|
|
23
|
+
getProjectStatus(projectId: string): ProjectStatusRecord;
|
|
24
|
+
startProject(projectId: string): Promise<StartProjectResult>;
|
|
25
|
+
stopProject(projectId: string): Promise<ProjectStatusRecord>;
|
|
26
|
+
}
|
|
27
|
+
export declare const createRuntimeManager: (options?: RuntimeManagerOptions) => RuntimeManager;
|
|
28
|
+
export {};
|