@codori/server 0.0.3 → 0.0.5
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 +34 -0
- package/client-dist/200.html +1 -1
- package/client-dist/404.html +1 -1
- package/client-dist/_nuxt/BsofOenw.js +1 -0
- package/client-dist/_nuxt/C1GdLDfB.js +1 -0
- package/client-dist/_nuxt/{CaHFvrMF.js → CNSSoePX.js} +1 -1
- package/client-dist/_nuxt/CQVB8E20.js +1 -0
- package/client-dist/_nuxt/{Cvj6lHH1.js → CsE-687t.js} +51 -51
- package/client-dist/_nuxt/{Bn41X3Zq.js → DJfsg7Kb.js} +1 -1
- package/client-dist/_nuxt/DcJmCJZR.js +1 -0
- package/client-dist/_nuxt/{CxIrrT6Q.js → Dg2XLMZm.js} +1 -1
- package/client-dist/_nuxt/DhLoSG-h.js +3 -0
- package/client-dist/_nuxt/DhRbzQPR.js +1 -0
- package/client-dist/_nuxt/{B9M-aXlQ.js → OylMiRf9.js} +3 -3
- package/client-dist/_nuxt/builds/latest.json +1 -1
- package/client-dist/_nuxt/builds/meta/468a0ff2-bd27-45c6-bd89-5ac776d98662.json +1 -0
- package/client-dist/_nuxt/{ClvUKBzL.js → ecRbsnab.js} +1 -1
- package/client-dist/index.html +1 -1
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +171 -19
- package/dist/config.d.ts +2 -0
- package/dist/config.js +26 -2
- package/dist/http-server.d.ts +8 -0
- package/dist/http-server.js +59 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/process-manager.d.ts +19 -0
- package/dist/process-manager.js +126 -3
- package/dist/runtime-store.js +5 -3
- package/dist/service-adapters.d.ts +39 -0
- package/dist/service-adapters.js +185 -0
- package/dist/service-update.d.ts +26 -0
- package/dist/service-update.js +196 -0
- package/dist/service.d.ts +86 -0
- package/dist/service.js +616 -0
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
- package/client-dist/_nuxt/B13tqEXg.js +0 -1
- package/client-dist/_nuxt/Bgck3A5L.js +0 -1
- package/client-dist/_nuxt/DS99AY4f.js +0 -1
- package/client-dist/_nuxt/Dp21CzWX.js +0 -1
- package/client-dist/_nuxt/ER2AV0-Z.js +0 -1
- package/client-dist/_nuxt/builds/meta/5f4263d4-eac2-4ff0-9a82-eb8be28751d5.json +0 -1
- package/client-dist/_nuxt/nHwHvv6y.js +0 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
1
|
+
{"id":"468a0ff2-bd27-45c6-bd89-5ac776d98662","timestamp":1776094853390}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id":"468a0ff2-bd27-45c6-bd89-5ac776d98662","timestamp":1776094853390,"prerendered":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as o,u as s,o as a,d as i,a as t,t as r}from"./
|
|
1
|
+
import{_ as o,u as s,o as a,d as i,a as t,t as r}from"./OylMiRf9.js";const u={class:"antialiased bg-white dark:bg-[#020420] dark:text-white font-sans grid min-h-screen overflow-hidden place-content-center text-[#020420] tracking-wide"},l={class:"max-w-520px text-center"},c=["textContent"],d=["textContent"],p=["textContent"],f={__name:"error-500",props:{appName:{type:String,default:"Nuxt"},status:{type:Number,default:500},statusText:{type:String,default:"Internal server error"},description:{type:String,default:"This page is temporarily unavailable."},refresh:{type:String,default:"Refresh this page"}},setup(e){const n=e;return s({title:`${n.status} - ${n.statusText} | ${n.appName}`,script:[{innerHTML:`!function(){const e=document.createElement("link").relList;if(!(e&&e.supports&&e.supports("modulepreload"))){for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const o of e)if("childList"===o.type)for(const e of o.addedNodes)"LINK"===e.tagName&&"modulepreload"===e.rel&&r(e)}).observe(document,{childList:!0,subtree:!0})}function r(e){if(e.ep)return;e.ep=!0;const r=function(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),"use-credentials"===e.crossOrigin?r.credentials="include":"anonymous"===e.crossOrigin?r.credentials="omit":r.credentials="same-origin",r}(e);fetch(e.href,r)}}();`}],style:[{innerHTML:'*,:after,:before{border-color:var(--un-default-border-color,#e5e7eb);border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--un-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}h1,h2{font-size:inherit;font-weight:inherit}h1,h2,p{margin:0}*,:after,:before{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 transparent;--un-ring-shadow:0 0 transparent;--un-shadow-inset: ;--un-shadow:0 0 transparent;--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: }'}]}),(h,m)=>(a(),i("div",u,[t("div",l,[t("h1",{class:"font-semibold leading-none mb-4 sm:text-[110px] tabular-nums text-[80px]",textContent:r(e.status)},null,8,c),t("h2",{class:"font-semibold mb-2 sm:text-3xl text-2xl",textContent:r(e.statusText)},null,8,d),t("p",{class:"mb-4 px-2 text-[#64748B] text-md",textContent:r(e.description)},null,8,p)])]))}},g=o(f,[["__scopeId","data-v-a8fa9b54"]]);export{g as default};
|
package/client-dist/index.html
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="/_nuxt/entry.BFUss7SH.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/_nuxt/
|
|
1
|
+
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="/_nuxt/entry.BFUss7SH.css" crossorigin><link rel="modulepreload" as="script" crossorigin href="/_nuxt/OylMiRf9.js"><script type="module" src="/_nuxt/OylMiRf9.js" crossorigin></script><script>"use strict";(()=>{const t=window,e=document.documentElement,c=["dark","light"],n=getStorageValue("localStorage","nuxt-color-mode")||"system";let i=n==="system"?u():n;const r=e.getAttribute("data-color-mode-forced");r&&(i=r),l(i),t["__NUXT_COLOR_MODE__"]={preference:n,value:i,getColorScheme:u,addColorScheme:l,removeColorScheme:d};function l(o){const s=""+o+"",a="";e.classList?e.classList.add(s):e.className+=" "+s,a&&e.setAttribute("data-"+a,o)}function d(o){const s=""+o+"",a="";e.classList?e.classList.remove(s):e.className=e.className.replace(new RegExp(s,"g"),""),a&&e.removeAttribute("data-"+a)}function f(o){return t.matchMedia("(prefers-color-scheme"+o+")")}function u(){if(t.matchMedia&&f("").media!=="not all"){for(const o of c)if(f(":"+o).matches)return o}return"light"}})();function getStorageValue(t,e){switch(t){case"localStorage":return window.localStorage.getItem(e);case"sessionStorage":return window.sessionStorage.getItem(e);case"cookie":return getCookie(e);default:return null}}function getCookie(t){const c=("; "+window.document.cookie).split("; "+t+"=");if(c.length===2)return c.pop()?.split(";").shift()}</script></head><body><div id="__nuxt" class="isolate"></div><div id="teleports"></div><script>window.__NUXT__={};window.__NUXT__.config={public:{serverBase:"",serverWsBase:""},app:{baseURL:"/",buildId:"468a0ff2-bd27-45c6-bd89-5ac776d98662",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1776094868508,false]</script></body></html>
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
import { type ServiceCommandDependencies } from './service.js';
|
|
3
|
+
export declare const CLI_USAGE: string;
|
|
4
|
+
export declare const resolveCliEntrypointPath: (value: string | undefined) => string | null;
|
|
5
|
+
export declare const isCliEntrypointPath: (argvPath: string | undefined, moduleUrl: string) => boolean;
|
|
6
|
+
export declare const runCli: (argv?: string[], dependencies?: ServiceCommandDependencies) => Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { resolve as resolvePath } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
2
5
|
import { parseArgs } from 'node:util';
|
|
3
6
|
import { asErrorMessage, CodoriError } from './errors.js';
|
|
4
7
|
import { startHttpServer } from './http-server.js';
|
|
5
8
|
import { createRuntimeManager } from './process-manager.js';
|
|
9
|
+
import { installService, restartService, uninstallService } from './service.js';
|
|
6
10
|
const printJson = (value) => {
|
|
7
11
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
8
12
|
};
|
|
@@ -35,6 +39,16 @@ const optionConfig = {
|
|
|
35
39
|
},
|
|
36
40
|
json: {
|
|
37
41
|
type: 'boolean'
|
|
42
|
+
},
|
|
43
|
+
scope: {
|
|
44
|
+
type: 'string'
|
|
45
|
+
},
|
|
46
|
+
yes: {
|
|
47
|
+
type: 'boolean'
|
|
48
|
+
},
|
|
49
|
+
help: {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
short: 'h'
|
|
38
52
|
}
|
|
39
53
|
};
|
|
40
54
|
const coercePort = (value) => {
|
|
@@ -45,22 +59,125 @@ const coercePort = (value) => {
|
|
|
45
59
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
46
60
|
};
|
|
47
61
|
const resolveCliRoot = (value) => value ?? process.cwd();
|
|
48
|
-
const
|
|
62
|
+
export const CLI_USAGE = [
|
|
63
|
+
'Usage:',
|
|
64
|
+
' npx @codori/server <command> [projectId] [options]',
|
|
65
|
+
' codori <command> [projectId] [options]',
|
|
66
|
+
'',
|
|
67
|
+
'Runtime commands:',
|
|
68
|
+
' serve',
|
|
69
|
+
' list',
|
|
70
|
+
' status [projectId]',
|
|
71
|
+
' start <projectId>',
|
|
72
|
+
' stop <projectId>',
|
|
73
|
+
'',
|
|
74
|
+
'Service commands:',
|
|
75
|
+
' install-service',
|
|
76
|
+
' setup-service',
|
|
77
|
+
' restart-service',
|
|
78
|
+
' uninstall-service',
|
|
79
|
+
'',
|
|
80
|
+
'Options:',
|
|
81
|
+
' --root <path>',
|
|
82
|
+
' --host <host>',
|
|
83
|
+
' --port <port>',
|
|
84
|
+
' --scope <user|system>',
|
|
85
|
+
' --yes',
|
|
86
|
+
' --json',
|
|
87
|
+
' --help',
|
|
88
|
+
'',
|
|
89
|
+
'Canonical service examples:',
|
|
90
|
+
' npx @codori/server install-service',
|
|
91
|
+
' npx @codori/server restart-service --root ~/Project/codori',
|
|
92
|
+
' npx @codori/server uninstall-service --root ~/Project/codori',
|
|
93
|
+
'',
|
|
94
|
+
'Installed binary examples:',
|
|
95
|
+
' codori install-service',
|
|
96
|
+
' codori restart-service --root ~/Project/codori'
|
|
97
|
+
].join('\n');
|
|
98
|
+
const printUsage = (stdout = process.stdout) => {
|
|
99
|
+
stdout.write(`${CLI_USAGE}\n`);
|
|
100
|
+
};
|
|
101
|
+
export const resolveCliEntrypointPath = (value) => {
|
|
102
|
+
if (!value) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const resolved = resolvePath(value);
|
|
106
|
+
try {
|
|
107
|
+
return realpathSync(resolved);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return resolved;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
export const isCliEntrypointPath = (argvPath, moduleUrl) => {
|
|
114
|
+
const entryPath = resolveCliEntrypointPath(argvPath);
|
|
115
|
+
const modulePath = resolveCliEntrypointPath(fileURLToPath(moduleUrl));
|
|
116
|
+
return entryPath !== null && entryPath === modulePath;
|
|
117
|
+
};
|
|
118
|
+
const executeServiceCommand = async (command, values, dependencies = {}) => {
|
|
119
|
+
const stdout = dependencies.stdout ?? process.stdout;
|
|
120
|
+
const options = {
|
|
121
|
+
root: values.root,
|
|
122
|
+
host: values.host,
|
|
123
|
+
port: values.port,
|
|
124
|
+
scope: values.scope,
|
|
125
|
+
yes: values.yes ?? false
|
|
126
|
+
};
|
|
127
|
+
switch (command) {
|
|
128
|
+
case 'install-service':
|
|
129
|
+
case 'setup-service': {
|
|
130
|
+
const result = await installService(options, dependencies);
|
|
131
|
+
stdout.write(`Installed service ${result.metadata.serviceName}\n`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
case 'restart-service': {
|
|
135
|
+
const result = await restartService({
|
|
136
|
+
root: values.root,
|
|
137
|
+
scope: values.scope,
|
|
138
|
+
yes: values.yes ?? false
|
|
139
|
+
}, dependencies);
|
|
140
|
+
stdout.write(`Restarted service ${result.metadata.serviceName}\n`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
case 'uninstall-service': {
|
|
144
|
+
const result = await uninstallService({
|
|
145
|
+
root: values.root,
|
|
146
|
+
yes: values.yes ?? false
|
|
147
|
+
}, dependencies);
|
|
148
|
+
stdout.write(`Removed service ${result.metadata.serviceName}\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
export const runCli = async (argv = process.argv.slice(2), dependencies = {}) => {
|
|
49
153
|
const parsed = parseArgs({
|
|
154
|
+
args: argv,
|
|
50
155
|
allowPositionals: true,
|
|
51
156
|
options: optionConfig
|
|
52
157
|
});
|
|
158
|
+
const values = parsed.values;
|
|
53
159
|
const [command = 'serve', maybeProjectId] = parsed.positionals;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
160
|
+
if (values.help) {
|
|
161
|
+
printUsage(dependencies.stdout ?? process.stdout);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (command === 'install-service'
|
|
165
|
+
|| command === 'setup-service'
|
|
166
|
+
|| command === 'restart-service'
|
|
167
|
+
|| command === 'uninstall-service') {
|
|
168
|
+
await executeServiceCommand(command, values, dependencies);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
62
171
|
switch (command) {
|
|
63
172
|
case 'list': {
|
|
173
|
+
const manager = createRuntimeManager({
|
|
174
|
+
configOverrides: {
|
|
175
|
+
root: resolveCliRoot(values.root),
|
|
176
|
+
host: values.host,
|
|
177
|
+
port: coercePort(values.port)
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const json = values.json ?? false;
|
|
64
181
|
const statuses = manager.listProjectStatuses();
|
|
65
182
|
if (json) {
|
|
66
183
|
printJson(statuses);
|
|
@@ -71,6 +188,14 @@ const main = async () => {
|
|
|
71
188
|
return;
|
|
72
189
|
}
|
|
73
190
|
case 'status': {
|
|
191
|
+
const manager = createRuntimeManager({
|
|
192
|
+
configOverrides: {
|
|
193
|
+
root: resolveCliRoot(values.root),
|
|
194
|
+
host: values.host,
|
|
195
|
+
port: coercePort(values.port)
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
const json = values.json ?? false;
|
|
74
199
|
if (maybeProjectId) {
|
|
75
200
|
const status = manager.getProjectStatus(maybeProjectId);
|
|
76
201
|
if (json) {
|
|
@@ -91,6 +216,14 @@ const main = async () => {
|
|
|
91
216
|
return;
|
|
92
217
|
}
|
|
93
218
|
case 'start': {
|
|
219
|
+
const manager = createRuntimeManager({
|
|
220
|
+
configOverrides: {
|
|
221
|
+
root: resolveCliRoot(values.root),
|
|
222
|
+
host: values.host,
|
|
223
|
+
port: coercePort(values.port)
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
const json = values.json ?? false;
|
|
94
227
|
if (!maybeProjectId) {
|
|
95
228
|
throw new CodoriError('MISSING_PROJECT_ID', 'The start command requires a project id.');
|
|
96
229
|
}
|
|
@@ -104,6 +237,14 @@ const main = async () => {
|
|
|
104
237
|
return;
|
|
105
238
|
}
|
|
106
239
|
case 'stop': {
|
|
240
|
+
const manager = createRuntimeManager({
|
|
241
|
+
configOverrides: {
|
|
242
|
+
root: resolveCliRoot(values.root),
|
|
243
|
+
host: values.host,
|
|
244
|
+
port: coercePort(values.port)
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
const json = values.json ?? false;
|
|
107
248
|
if (!maybeProjectId) {
|
|
108
249
|
throw new CodoriError('MISSING_PROJECT_ID', 'The stop command requires a project id.');
|
|
109
250
|
}
|
|
@@ -117,22 +258,33 @@ const main = async () => {
|
|
|
117
258
|
return;
|
|
118
259
|
}
|
|
119
260
|
case 'serve': {
|
|
261
|
+
const manager = createRuntimeManager({
|
|
262
|
+
configOverrides: {
|
|
263
|
+
root: resolveCliRoot(values.root),
|
|
264
|
+
host: values.host,
|
|
265
|
+
port: coercePort(values.port)
|
|
266
|
+
}
|
|
267
|
+
});
|
|
120
268
|
const app = await startHttpServer(manager);
|
|
121
269
|
process.stdout.write(`Running codori server with project root directory: ${manager.config.root}\n`);
|
|
122
270
|
process.stdout.write(`Codori listening on http://${manager.config.server.host}:${manager.config.server.port}\n`);
|
|
271
|
+
process.stdout.write('Private tunnel is not included. Expose Codori through your own network layer such as Tailscale or Cloudflare Tunnel when you need remote access.\n');
|
|
123
272
|
await app.ready();
|
|
124
273
|
return;
|
|
125
274
|
}
|
|
126
275
|
default:
|
|
127
|
-
|
|
276
|
+
printUsage(dependencies.stdout ?? process.stdout);
|
|
128
277
|
}
|
|
129
278
|
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
279
|
+
const isEntrypoint = isCliEntrypointPath(process.argv[1], import.meta.url);
|
|
280
|
+
if (isEntrypoint) {
|
|
281
|
+
void runCli().catch((error) => {
|
|
282
|
+
if (error instanceof CodoriError) {
|
|
283
|
+
process.stderr.write(`${error.code}: ${error.message}\n`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
process.stderr.write(`${asErrorMessage(error)}\n`);
|
|
287
|
+
}
|
|
288
|
+
process.exitCode = 1;
|
|
289
|
+
});
|
|
290
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { CodoriConfig, ConfigOverrides } from './types.js';
|
|
2
|
+
export declare const DEFAULT_SERVER_HOST = "127.0.0.1";
|
|
3
|
+
export declare const DEFAULT_SERVER_PORT = 4310;
|
|
2
4
|
export declare const resolveCodoriHome: (homeDir?: string) => string;
|
|
3
5
|
export declare const resolveCodoriConfigPath: (homeDir?: string) => string;
|
|
4
6
|
export declare const ensureCodoriDirectories: (homeDir?: string) => {
|
package/dist/config.js
CHANGED
|
@@ -2,10 +2,12 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { CodoriError } from './errors.js';
|
|
5
|
-
const DEFAULT_SERVER_HOST = '127.0.0.1';
|
|
6
|
-
const DEFAULT_SERVER_PORT = 4310;
|
|
5
|
+
export const DEFAULT_SERVER_HOST = '127.0.0.1';
|
|
6
|
+
export const DEFAULT_SERVER_PORT = 4310;
|
|
7
7
|
const DEFAULT_PORT_START = 46000;
|
|
8
8
|
const DEFAULT_PORT_END = 46999;
|
|
9
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
10
|
+
const DEFAULT_IDLE_SWEEP_INTERVAL_MS = 60 * 1000;
|
|
9
11
|
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
12
|
export const resolveCodoriHome = (homeDir = os.homedir()) => join(homeDir, '.codori');
|
|
11
13
|
export const resolveCodoriConfigPath = (homeDir = os.homedir()) => join(resolveCodoriHome(homeDir), 'config.json');
|
|
@@ -41,6 +43,18 @@ const ensureValidPort = (value, label) => {
|
|
|
41
43
|
}
|
|
42
44
|
return value;
|
|
43
45
|
};
|
|
46
|
+
const ensureValidDurationMs = (value, label) => {
|
|
47
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {
|
|
48
|
+
throw new CodoriError('INVALID_CONFIG', `${label} must be a positive integer in milliseconds.`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
};
|
|
52
|
+
const ensureValidBoolean = (value, label) => {
|
|
53
|
+
if (typeof value !== 'boolean') {
|
|
54
|
+
throw new CodoriError('INVALID_CONFIG', `${label} must be a boolean.`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
};
|
|
44
58
|
export const resolveConfig = (overrides = {}, homeDir = os.homedir()) => {
|
|
45
59
|
const fileConfig = loadUserConfig(homeDir);
|
|
46
60
|
const root = overrides.root ?? fileConfig.root;
|
|
@@ -62,6 +76,11 @@ export const resolveConfig = (overrides = {}, homeDir = os.homedir()) => {
|
|
|
62
76
|
const resolvedPort = ensureValidPort(port, 'server.port');
|
|
63
77
|
const resolvedPortStart = ensureValidPort(portStart, 'ports.start');
|
|
64
78
|
const resolvedPortEnd = ensureValidPort(portEnd, 'ports.end');
|
|
79
|
+
const idleShutdownEnabled = ensureValidBoolean(overrides.idleShutdownEnabled ?? fileConfig.idleShutdown?.enabled ?? true, 'idleShutdown.enabled');
|
|
80
|
+
const idleShutdownTimeoutMs = ensureValidDurationMs(overrides.idleShutdownTimeoutMs ?? fileConfig.idleShutdown?.timeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS, 'idleShutdown.timeoutMs');
|
|
81
|
+
const idleShutdownSweepIntervalMs = ensureValidDurationMs(overrides.idleShutdownSweepIntervalMs
|
|
82
|
+
?? fileConfig.idleShutdown?.sweepIntervalMs
|
|
83
|
+
?? DEFAULT_IDLE_SWEEP_INTERVAL_MS, 'idleShutdown.sweepIntervalMs');
|
|
65
84
|
if (resolvedPortStart > resolvedPortEnd) {
|
|
66
85
|
throw new CodoriError('INVALID_CONFIG', 'ports.start must be less than or equal to ports.end.');
|
|
67
86
|
}
|
|
@@ -75,6 +94,11 @@ export const resolveConfig = (overrides = {}, homeDir = os.homedir()) => {
|
|
|
75
94
|
ports: {
|
|
76
95
|
start: resolvedPortStart,
|
|
77
96
|
end: resolvedPortEnd
|
|
97
|
+
},
|
|
98
|
+
idleShutdown: {
|
|
99
|
+
enabled: idleShutdownEnabled,
|
|
100
|
+
timeoutMs: idleShutdownTimeoutMs,
|
|
101
|
+
sweepIntervalMs: idleShutdownSweepIntervalMs
|
|
78
102
|
}
|
|
79
103
|
};
|
|
80
104
|
};
|
package/dist/http-server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Fastify, { type FastifyInstance } from 'fastify';
|
|
2
|
+
import { type ServiceUpdateController } from './service-update.js';
|
|
2
3
|
import type { ProjectStatusRecord, StartProjectResult } from './types.js';
|
|
3
4
|
type MaybePromise<T> = T | Promise<T>;
|
|
4
5
|
export type RuntimeManagerLike = {
|
|
@@ -6,6 +7,12 @@ export type RuntimeManagerLike = {
|
|
|
6
7
|
getProjectStatus: (projectId: string) => MaybePromise<ProjectStatusRecord>;
|
|
7
8
|
startProject: (projectId: string) => MaybePromise<StartProjectResult>;
|
|
8
9
|
stopProject: (projectId: string) => MaybePromise<ProjectStatusRecord>;
|
|
10
|
+
noteProjectActivity?: (projectId: string) => MaybePromise<ProjectStatusRecord | void>;
|
|
11
|
+
acquireProjectSession?: (projectId: string) => {
|
|
12
|
+
touchActivity?: (at?: number) => MaybePromise<ProjectStatusRecord | void>;
|
|
13
|
+
release: () => void;
|
|
14
|
+
};
|
|
15
|
+
dispose?: () => MaybePromise<void>;
|
|
9
16
|
config?: {
|
|
10
17
|
server: {
|
|
11
18
|
host: string;
|
|
@@ -16,6 +23,7 @@ export type RuntimeManagerLike = {
|
|
|
16
23
|
export type HttpServerOptions = {
|
|
17
24
|
clientBundleDir?: string | null;
|
|
18
25
|
attachmentsRootDir?: string | null;
|
|
26
|
+
serviceUpdateController?: ServiceUpdateController | null;
|
|
19
27
|
};
|
|
20
28
|
export declare const createHttpServer: (manager: RuntimeManagerLike, options?: HttpServerOptions) => Promise<FastifyInstance>;
|
|
21
29
|
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>>;
|
package/dist/http-server.js
CHANGED
|
@@ -12,6 +12,7 @@ import WebSocket from 'ws';
|
|
|
12
12
|
import { isPathInsideDirectory, persistThreadAttachmentStream, readAttachmentMetadata, resolveProjectAttachmentsDir } from './attachment-store.js';
|
|
13
13
|
import { CodoriError } from './errors.js';
|
|
14
14
|
import { createRuntimeManager } from './process-manager.js';
|
|
15
|
+
import { createServiceUpdateController } from './service-update.js';
|
|
15
16
|
const isCodoriError = (error) => error instanceof CodoriError;
|
|
16
17
|
const resolveBundledClientDir = () => {
|
|
17
18
|
const candidates = [
|
|
@@ -39,6 +40,9 @@ const toStatusCode = (error) => {
|
|
|
39
40
|
case 'INVALID_ATTACHMENT':
|
|
40
41
|
case 'MISSING_ROOT':
|
|
41
42
|
return 400;
|
|
43
|
+
case 'SERVICE_UPDATE_UNAVAILABLE':
|
|
44
|
+
case 'SERVICE_UPDATE_IN_PROGRESS':
|
|
45
|
+
return 409;
|
|
42
46
|
default:
|
|
43
47
|
return 500;
|
|
44
48
|
}
|
|
@@ -68,6 +72,18 @@ const normalizeImageMediaType = (input) => {
|
|
|
68
72
|
}
|
|
69
73
|
return null;
|
|
70
74
|
};
|
|
75
|
+
const touchProjectActivity = async (manager, projectId) => {
|
|
76
|
+
if (!manager.noteProjectActivity) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await resolveValue(manager.noteProjectActivity(projectId));
|
|
80
|
+
};
|
|
81
|
+
const touchProjectActivityInBackground = (manager, projectId, session) => {
|
|
82
|
+
const task = session?.touchActivity
|
|
83
|
+
? resolveValue(session.touchActivity())
|
|
84
|
+
: touchProjectActivity(manager, projectId);
|
|
85
|
+
void task.catch(() => { });
|
|
86
|
+
};
|
|
71
87
|
const wait = async (ms) => new Promise((resolvePromise) => {
|
|
72
88
|
setTimeout(resolvePromise, ms);
|
|
73
89
|
});
|
|
@@ -101,9 +117,13 @@ export const createHttpServer = async (manager, options = {}) => {
|
|
|
101
117
|
const app = Fastify({
|
|
102
118
|
logger: false
|
|
103
119
|
});
|
|
120
|
+
app.addHook('onClose', async () => {
|
|
121
|
+
await resolveValue(manager.dispose?.());
|
|
122
|
+
});
|
|
104
123
|
const clientBundleDir = options.clientBundleDir === undefined
|
|
105
124
|
? resolveBundledClientDir()
|
|
106
125
|
: options.clientBundleDir;
|
|
126
|
+
const serviceUpdateController = options.serviceUpdateController ?? null;
|
|
107
127
|
await app.register(multipart, {
|
|
108
128
|
limits: {
|
|
109
129
|
files: MAX_ATTACHMENTS_PER_MESSAGE,
|
|
@@ -147,6 +167,26 @@ export const createHttpServer = async (manager, options = {}) => {
|
|
|
147
167
|
app.get('/api/projects', async () => ({
|
|
148
168
|
projects: await resolveValue(manager.listProjectStatuses())
|
|
149
169
|
}));
|
|
170
|
+
app.get('/api/service/update', async () => ({
|
|
171
|
+
serviceUpdate: serviceUpdateController
|
|
172
|
+
? await serviceUpdateController.getStatus()
|
|
173
|
+
: {
|
|
174
|
+
enabled: false,
|
|
175
|
+
updateAvailable: false,
|
|
176
|
+
updating: false,
|
|
177
|
+
installedVersion: null,
|
|
178
|
+
latestVersion: null
|
|
179
|
+
}
|
|
180
|
+
}));
|
|
181
|
+
app.post('/api/service/update', async (_request, reply) => {
|
|
182
|
+
if (!serviceUpdateController) {
|
|
183
|
+
throw new CodoriError('SERVICE_UPDATE_UNAVAILABLE', 'Self-update is only available while Codori is running as a registered service.');
|
|
184
|
+
}
|
|
185
|
+
reply.status(202);
|
|
186
|
+
return {
|
|
187
|
+
serviceUpdate: await serviceUpdateController.requestUpdate()
|
|
188
|
+
};
|
|
189
|
+
});
|
|
150
190
|
app.get('/api/projects/:projectId', async (request) => ({
|
|
151
191
|
project: await resolveValue(manager.getProjectStatus(getProjectIdFromRequest(request.params.projectId)))
|
|
152
192
|
}));
|
|
@@ -162,6 +202,7 @@ export const createHttpServer = async (manager, options = {}) => {
|
|
|
162
202
|
app.post('/api/projects/:projectId/attachments', async (request, reply) => {
|
|
163
203
|
const projectId = getProjectIdFromRequest(request.params.projectId);
|
|
164
204
|
const project = await resolveValue(manager.getProjectStatus(projectId));
|
|
205
|
+
await touchProjectActivity(manager, projectId);
|
|
165
206
|
const files = [];
|
|
166
207
|
let threadId = null;
|
|
167
208
|
for await (const part of request.parts()) {
|
|
@@ -212,6 +253,7 @@ export const createHttpServer = async (manager, options = {}) => {
|
|
|
212
253
|
throw new CodoriError('INVALID_ATTACHMENT', 'Missing attachment path.');
|
|
213
254
|
}
|
|
214
255
|
const project = await resolveValue(manager.getProjectStatus(projectId));
|
|
256
|
+
await touchProjectActivity(manager, projectId);
|
|
215
257
|
const allowedRoot = resolveProjectAttachmentsDir(project.projectPath, options.attachmentsRootDir);
|
|
216
258
|
const resolvedPath = resolve(requestedPath);
|
|
217
259
|
if (!isPathInsideDirectory(resolvedPath, allowedRoot)) {
|
|
@@ -270,7 +312,16 @@ export const createHttpServer = async (manager, options = {}) => {
|
|
|
270
312
|
app.get('/api/projects/:projectId/rpc', { websocket: true }, async (clientSocket, request) => {
|
|
271
313
|
const projectId = getProjectIdFromRequest(request.params.projectId);
|
|
272
314
|
const pendingClientMessages = [];
|
|
315
|
+
const session = manager.acquireProjectSession?.(projectId) ?? null;
|
|
273
316
|
let upstream = null;
|
|
317
|
+
let sessionReleased = false;
|
|
318
|
+
const releaseSession = () => {
|
|
319
|
+
if (sessionReleased) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
sessionReleased = true;
|
|
323
|
+
session?.release();
|
|
324
|
+
};
|
|
274
325
|
const closeBoth = (code = 1011, reason = 'proxy error') => {
|
|
275
326
|
if (clientSocket.readyState === clientSocket.OPEN || clientSocket.readyState === clientSocket.CONNECTING) {
|
|
276
327
|
clientSocket.close(code, reason);
|
|
@@ -280,6 +331,7 @@ export const createHttpServer = async (manager, options = {}) => {
|
|
|
280
331
|
}
|
|
281
332
|
};
|
|
282
333
|
clientSocket.on('message', (message, isBinary) => {
|
|
334
|
+
touchProjectActivityInBackground(manager, projectId, session);
|
|
283
335
|
if (upstream?.readyState === WebSocket.OPEN) {
|
|
284
336
|
upstream.send(message, { binary: isBinary });
|
|
285
337
|
return;
|
|
@@ -290,6 +342,7 @@ export const createHttpServer = async (manager, options = {}) => {
|
|
|
290
342
|
closeBoth(1011, 'client websocket failed');
|
|
291
343
|
});
|
|
292
344
|
clientSocket.on('close', () => {
|
|
345
|
+
releaseSession();
|
|
293
346
|
if (upstream && (upstream.readyState === WebSocket.OPEN || upstream.readyState === WebSocket.CONNECTING)) {
|
|
294
347
|
upstream.close();
|
|
295
348
|
}
|
|
@@ -312,6 +365,7 @@ export const createHttpServer = async (manager, options = {}) => {
|
|
|
312
365
|
}
|
|
313
366
|
});
|
|
314
367
|
upstream.on('message', (message, isBinary) => {
|
|
368
|
+
touchProjectActivityInBackground(manager, projectId, session);
|
|
315
369
|
clientSocket.send(message, { binary: isBinary });
|
|
316
370
|
});
|
|
317
371
|
upstream.on('error', () => {
|
|
@@ -360,10 +414,14 @@ export const createHttpServer = async (manager, options = {}) => {
|
|
|
360
414
|
return app;
|
|
361
415
|
};
|
|
362
416
|
export const startHttpServer = async (manager = createRuntimeManager()) => {
|
|
363
|
-
const app = await createHttpServer(manager);
|
|
364
417
|
if (!manager.config) {
|
|
365
418
|
throw new CodoriError('INVALID_CONFIG', 'Manager config is required to start the HTTP server.');
|
|
366
419
|
}
|
|
420
|
+
const app = await createHttpServer(manager, {
|
|
421
|
+
serviceUpdateController: createServiceUpdateController({
|
|
422
|
+
root: manager.config.root
|
|
423
|
+
})
|
|
424
|
+
});
|
|
367
425
|
await app.listen({
|
|
368
426
|
host: manager.config.server.host,
|
|
369
427
|
port: manager.config.server.port
|
package/dist/index.d.ts
CHANGED
|
@@ -4,5 +4,8 @@ export { createHttpServer, startHttpServer } from './http-server.js';
|
|
|
4
4
|
export { findAvailablePort } from './ports.js';
|
|
5
5
|
export { createRuntimeManager, RuntimeManager } from './process-manager.js';
|
|
6
6
|
export { scanProjects } from './project-scanner.js';
|
|
7
|
+
export * from './service-adapters.js';
|
|
8
|
+
export * from './service-update.js';
|
|
7
9
|
export { RuntimeStore } from './runtime-store.js';
|
|
10
|
+
export * from './service.js';
|
|
8
11
|
export type * from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -4,4 +4,7 @@ export { createHttpServer, startHttpServer } from './http-server.js';
|
|
|
4
4
|
export { findAvailablePort } from './ports.js';
|
|
5
5
|
export { createRuntimeManager, RuntimeManager } from './process-manager.js';
|
|
6
6
|
export { scanProjects } from './project-scanner.js';
|
|
7
|
+
export * from './service-adapters.js';
|
|
8
|
+
export * from './service-update.js';
|
|
7
9
|
export { RuntimeStore } from './runtime-store.js';
|
|
10
|
+
export * from './service.js';
|
|
@@ -4,6 +4,10 @@ type CommandFactory = (port: number, project: ProjectRecord) => {
|
|
|
4
4
|
command: string;
|
|
5
5
|
args: string[];
|
|
6
6
|
};
|
|
7
|
+
type ProjectSessionLease = {
|
|
8
|
+
touchActivity: (at?: number) => ProjectStatusRecord;
|
|
9
|
+
release: () => void;
|
|
10
|
+
};
|
|
7
11
|
type RuntimeManagerOptions = {
|
|
8
12
|
homeDir?: string;
|
|
9
13
|
configOverrides?: ConfigOverrides;
|
|
@@ -14,15 +18,30 @@ export declare class RuntimeManager {
|
|
|
14
18
|
readonly config: CodoriConfig;
|
|
15
19
|
readonly store: RuntimeStore;
|
|
16
20
|
private readonly commandFactory;
|
|
21
|
+
private readonly activeSessions;
|
|
22
|
+
private idleReaper;
|
|
23
|
+
private idleSweepInFlight;
|
|
17
24
|
constructor(options?: RuntimeManagerOptions);
|
|
18
25
|
listProjects(): ProjectRecord[];
|
|
19
26
|
private resolveProject;
|
|
20
27
|
private normalizeStatus;
|
|
28
|
+
private getActiveSessionCount;
|
|
29
|
+
private resolveIdleDeadline;
|
|
30
|
+
private writeRuntime;
|
|
31
|
+
private touchRuntimeRecord;
|
|
32
|
+
private incrementActiveSessions;
|
|
33
|
+
private decrementActiveSessions;
|
|
34
|
+
private loadActiveRuntime;
|
|
21
35
|
private readRunningRuntime;
|
|
36
|
+
private touchProjectRuntime;
|
|
37
|
+
noteProjectActivity(projectId: string, at?: number): ProjectStatusRecord;
|
|
38
|
+
acquireProjectSession(projectId: string): ProjectSessionLease;
|
|
22
39
|
listProjectStatuses(): ProjectStatusRecord[];
|
|
23
40
|
getProjectStatus(projectId: string): ProjectStatusRecord;
|
|
24
41
|
startProject(projectId: string): Promise<StartProjectResult>;
|
|
25
42
|
stopProject(projectId: string): Promise<ProjectStatusRecord>;
|
|
43
|
+
reapIdleRuntimes(): Promise<number>;
|
|
44
|
+
dispose(): void;
|
|
26
45
|
}
|
|
27
46
|
export declare const createRuntimeManager: (options?: RuntimeManagerOptions) => RuntimeManager;
|
|
28
47
|
export {};
|