@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.
Files changed (44) hide show
  1. package/README.md +34 -0
  2. package/client-dist/200.html +1 -1
  3. package/client-dist/404.html +1 -1
  4. package/client-dist/_nuxt/BsofOenw.js +1 -0
  5. package/client-dist/_nuxt/C1GdLDfB.js +1 -0
  6. package/client-dist/_nuxt/{CaHFvrMF.js → CNSSoePX.js} +1 -1
  7. package/client-dist/_nuxt/CQVB8E20.js +1 -0
  8. package/client-dist/_nuxt/{Cvj6lHH1.js → CsE-687t.js} +51 -51
  9. package/client-dist/_nuxt/{Bn41X3Zq.js → DJfsg7Kb.js} +1 -1
  10. package/client-dist/_nuxt/DcJmCJZR.js +1 -0
  11. package/client-dist/_nuxt/{CxIrrT6Q.js → Dg2XLMZm.js} +1 -1
  12. package/client-dist/_nuxt/DhLoSG-h.js +3 -0
  13. package/client-dist/_nuxt/DhRbzQPR.js +1 -0
  14. package/client-dist/_nuxt/{B9M-aXlQ.js → OylMiRf9.js} +3 -3
  15. package/client-dist/_nuxt/builds/latest.json +1 -1
  16. package/client-dist/_nuxt/builds/meta/468a0ff2-bd27-45c6-bd89-5ac776d98662.json +1 -0
  17. package/client-dist/_nuxt/{ClvUKBzL.js → ecRbsnab.js} +1 -1
  18. package/client-dist/index.html +1 -1
  19. package/dist/cli.d.ts +5 -1
  20. package/dist/cli.js +171 -19
  21. package/dist/config.d.ts +2 -0
  22. package/dist/config.js +26 -2
  23. package/dist/http-server.d.ts +8 -0
  24. package/dist/http-server.js +59 -1
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +3 -0
  27. package/dist/process-manager.d.ts +19 -0
  28. package/dist/process-manager.js +126 -3
  29. package/dist/runtime-store.js +5 -3
  30. package/dist/service-adapters.d.ts +39 -0
  31. package/dist/service-adapters.js +185 -0
  32. package/dist/service-update.d.ts +26 -0
  33. package/dist/service-update.js +196 -0
  34. package/dist/service.d.ts +86 -0
  35. package/dist/service.js +616 -0
  36. package/dist/types.d.ts +13 -0
  37. package/package.json +1 -1
  38. package/client-dist/_nuxt/B13tqEXg.js +0 -1
  39. package/client-dist/_nuxt/Bgck3A5L.js +0 -1
  40. package/client-dist/_nuxt/DS99AY4f.js +0 -1
  41. package/client-dist/_nuxt/Dp21CzWX.js +0 -1
  42. package/client-dist/_nuxt/ER2AV0-Z.js +0 -1
  43. package/client-dist/_nuxt/builds/meta/5f4263d4-eac2-4ff0-9a82-eb8be28751d5.json +0 -1
  44. package/client-dist/_nuxt/nHwHvv6y.js +0 -3
@@ -1 +1 @@
1
- {"id":"5f4263d4-eac2-4ff0-9a82-eb8be28751d5","timestamp":1775888199612}
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"./B9M-aXlQ.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};
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};
@@ -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/B9M-aXlQ.js"><script type="module" src="/_nuxt/B9M-aXlQ.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:"5f4263d4-eac2-4ff0-9a82-eb8be28751d5",buildAssetsDir:"/_nuxt/",cdnURL:""}}</script><script type="application/json" data-nuxt-data="nuxt-app" data-ssr="false" id="__NUXT_DATA__">[{"prerenderedAt":1,"serverRendered":2},1775888214387,false]</script></body></html>
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
- export {};
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 main = async () => {
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
- 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;
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
- process.stdout.write('Usage: npx @codori/server [serve|list|status|start|stop] [projectId] --root <path> [--host <host>] [--port <port>] [--json]\n');
276
+ printUsage(dependencies.stdout ?? process.stdout);
128
277
  }
129
278
  };
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
- });
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
  };
@@ -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>>;
@@ -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 {};