ompchamber 0.5.0 → 0.7.0

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 (89) hide show
  1. package/README.md +67 -6
  2. package/dist/client/index.html +1 -1
  3. package/dist/client/static/css/{index.5291f58223.css → index.51f68210a5.css} +1 -1
  4. package/dist/client/static/js/async/{3y.ededeebde8.js → 3y.4c2596f64c.js} +3 -3
  5. package/dist/client/static/js/async/73.308dff4da8.js +1 -0
  6. package/dist/client/static/js/async/{ty.3d93cf5f5f.js → 7g.a7814350c3.js} +1 -1
  7. package/dist/client/static/js/async/a.bce4542392.js +1 -0
  8. package/dist/client/static/js/async/g.86e2256a70.js +2 -0
  9. package/dist/client/static/js/async/hn.b645041e00.js +1 -0
  10. package/dist/client/static/js/async/{jg.69a4beade8.js → jg.d86ebd4013.js} +1 -1
  11. package/dist/client/static/js/async/l.12dd34e1be.js +1 -0
  12. package/dist/client/static/js/async/ls.9d9a01026c.js +104 -0
  13. package/dist/client/static/js/async/mw.ca1512563c.js +10 -0
  14. package/dist/client/static/js/async/nk.fc7e58325e.js +1 -0
  15. package/dist/client/static/js/async/{q.7fb4d2f374.js → q.542c184fec.js} +3 -3
  16. package/dist/client/static/js/async/t.cda4891526.js +1 -0
  17. package/dist/client/static/js/async/tr.e878658140.js +309 -0
  18. package/dist/client/static/js/async/uf.24ca3d49c3.js +2 -0
  19. package/dist/client/static/js/index.eccad0b70a.js +33 -0
  20. package/dist/client/static/js/{uy.5f791144ce.js → uy.2e8929ed0c.js} +2 -2
  21. package/package.json +15 -5
  22. package/src/cli/lib/args.js +6 -6
  23. package/src/cli/lib/commands/logs.js +76 -34
  24. package/src/cli/lib/commands/restart.js +40 -8
  25. package/src/cli/lib/commands/serve.js +12 -4
  26. package/src/cli/lib/commands/status.js +40 -18
  27. package/src/cli/lib/commands/stop.js +12 -7
  28. package/src/cli/lib/commands/update.js +23 -12
  29. package/src/cli/lib/paths.js +8 -28
  30. package/src/cli/lib/runtime.js +121 -108
  31. package/src/client/components/common/code-editor/index.tsx +5 -1
  32. package/src/client/components/common/code-surface/index.tsx +49 -1
  33. package/src/client/components/common/code-surface/measure.ts +99 -0
  34. package/src/client/components/layout/RightActivityBar.tsx +42 -69
  35. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +5 -6
  36. package/src/client/components/layout/desktop-layout/index.tsx +11 -9
  37. package/src/client/components/layout/desktop-layout/resizer/index.tsx +22 -29
  38. package/src/client/components/layout/desktop-layout/resizer/utils.test.ts +33 -0
  39. package/src/client/components/layout/desktop-layout/resizer/utils.ts +34 -0
  40. package/src/client/components/mobile/RightSidebar.tsx +42 -119
  41. package/src/client/components/mobile/mobile-main-view/Header.tsx +17 -1
  42. package/src/client/components/workspace/editor/index.tsx +4 -1
  43. package/src/client/hooks/chat/omp/index.ts +4 -109
  44. package/src/client/hooks/chat/omp/prompt-send.ts +161 -0
  45. package/src/client/hooks/chat/timeline/send.ts +3 -2
  46. package/src/client/hooks/editor/use-file-editor.test.ts +158 -0
  47. package/src/client/hooks/editor/use-file-editor.ts +35 -19
  48. package/src/server/index.ts +52 -1
  49. package/src/server/lib/lifecycle/bun.ts +18 -0
  50. package/src/server/lib/lifecycle/identity.ts +108 -0
  51. package/src/server/lib/lifecycle/instance.ts +102 -0
  52. package/src/server/lib/lifecycle/launch-mode.test.ts +32 -0
  53. package/src/server/lib/lifecycle/launch-mode.ts +40 -0
  54. package/src/server/lib/lifecycle/occupant.ts +101 -0
  55. package/src/server/lib/lifecycle/paths.ts +50 -0
  56. package/src/server/lib/lifecycle/port-guard.ts +170 -0
  57. package/src/server/lib/lifecycle/probe.ts +121 -0
  58. package/src/server/lib/lifecycle/restart.test.ts +21 -0
  59. package/src/server/lib/lifecycle/restart.ts +111 -0
  60. package/src/server/lib/omp/rpc/session-commands.ts +15 -1
  61. package/src/server/lib/updates/apply.ts +8 -5
  62. package/src/server/lib/updates/install.ts +7 -4
  63. package/src/server/plugins/static.test.ts +63 -0
  64. package/src/server/plugins/static.ts +28 -4
  65. package/src/server/routes/health/probe.ts +9 -0
  66. package/src/shared/lib/markdown/mermaid.ts +8 -0
  67. package/src/shared/lib/omp/session/stream-state.server.ts +16 -1
  68. package/src/shared/lib/workspace/panel-widths.test.ts +5 -5
  69. package/src/shared/lib/workspace/panel-widths.ts +23 -10
  70. package/src/shared/lib/workspace/right-panels.ts +10 -4
  71. package/dist/client/static/js/async/73.50280ce58d.js +0 -1
  72. package/dist/client/static/js/async/a.e05da304c8.js +0 -10
  73. package/dist/client/static/js/async/ar.1e14da5588.js +0 -1
  74. package/dist/client/static/js/async/g.7a4bbd4798.js +0 -2
  75. package/dist/client/static/js/async/hn.7568bdb58b.js +0 -1
  76. package/dist/client/static/js/async/l.102a97fce6.js +0 -104
  77. package/dist/client/static/js/async/l6.c56640c205.js +0 -1
  78. package/dist/client/static/js/async/nk.6de011832e.js +0 -1
  79. package/dist/client/static/js/async/t.cb3fcf8615.js +0 -1
  80. package/dist/client/static/js/async/uf.46ad6209fa.js +0 -2
  81. package/dist/client/static/js/async/vn.e98e2f3b61.js +0 -309
  82. package/dist/client/static/js/index.0ac93ee878.js +0 -33
  83. /package/dist/client/static/js/async/{3y.ededeebde8.js.LICENSE.txt → 3y.4c2596f64c.js.LICENSE.txt} +0 -0
  84. /package/dist/client/static/js/async/{g.7a4bbd4798.js.LICENSE.txt → g.86e2256a70.js.LICENSE.txt} +0 -0
  85. /package/dist/client/static/js/async/{a.e05da304c8.js.LICENSE.txt → mw.ca1512563c.js.LICENSE.txt} +0 -0
  86. /package/dist/client/static/js/async/{q.7fb4d2f374.js.LICENSE.txt → q.542c184fec.js.LICENSE.txt} +0 -0
  87. /package/dist/client/static/js/async/{uf.46ad6209fa.js.LICENSE.txt → uf.24ca3d49c3.js.LICENSE.txt} +0 -0
  88. /package/dist/client/static/js/{index.0ac93ee878.js.LICENSE.txt → index.eccad0b70a.js.LICENSE.txt} +0 -0
  89. /package/dist/client/static/js/{uy.5f791144ce.js.LICENSE.txt → uy.2e8929ed0c.js.LICENSE.txt} +0 -0
package/package.json CHANGED
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "name": "ompchamber",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Developer web console and diagnostic chamber for AI Oh-My-Pi — Elysia + Preact on Bun.",
5
+ "license": "UNLICENSED",
6
+ "author": "Wijanarko Putra Rajeb <wijanarko.rajeb@gmail.com>",
5
7
  "type": "module",
6
8
  "engines": {
7
9
  "bun": ">=1.4.0"
@@ -32,12 +34,12 @@
32
34
  "tsconfig.json"
33
35
  ],
34
36
  "scripts": {
35
- "dev:server": "bun run --hot src/server/index.ts",
37
+ "dev:server": "NODE_ENV=development bun run --hot src/server/index.ts --ompchamber-server",
36
38
  "dev:client": "rsbuild build --watch",
37
- "dev": "NODE_ENV=development bun run dev:server & bun run dev:client; wait",
38
- "dev:lan": "HOST=0.0.0.0 NODE_ENV=development bun run dev:server & bun run dev:client; wait",
39
+ "dev": "bun run dev:server & bun run dev:client; wait",
40
+ "dev:lan": "HOST=0.0.0.0 bun run dev:server & bun run dev:client; wait",
39
41
  "build": "rsbuild build",
40
- "start": "NODE_ENV=production bun run src/server/index.ts",
42
+ "start": "NODE_ENV=production bun run src/server/index.ts --ompchamber-server",
41
43
  "typecheck": "tsc",
42
44
  "clean": "rm -rf dist",
43
45
  "lint": "tsc --noEmit",
@@ -65,10 +67,18 @@
65
67
  "devDependencies": {
66
68
  "@rsbuild/core": "^2.2.7",
67
69
  "@rsbuild/plugin-preact": "^2.1.0",
70
+ "@semantic-release/changelog": "^7.0.0",
71
+ "@semantic-release/commit-analyzer": "^13.0.1",
72
+ "@semantic-release/git": "^11.0.1",
73
+ "@semantic-release/github": "^12.0.10",
74
+ "@semantic-release/npm": "^13.2.0",
75
+ "@semantic-release/release-notes-generator": "^14.1.1",
68
76
  "@tailwindcss/postcss": "^4.1.14",
69
77
  "@types/bun": "^1.4.2",
70
78
  "@types/node": "^22.14.0",
79
+ "conventional-changelog-conventionalcommits": "^9.3.1",
71
80
  "happy-dom": "^20.14.5",
81
+ "semantic-release": "^25.0.9",
72
82
  "tailwindcss": "^4.1.14",
73
83
  "typescript": "~5.8.2"
74
84
  }
@@ -188,20 +188,20 @@ USAGE:
188
188
  COMMANDS:
189
189
  serve Start the OMPChamber web server (default when no command is given)
190
190
  update Update OMPChamber to the latest GitHub release
191
- stop Stop the running OMPChamber instance
192
- restart Stop and then start the server again
193
- status Show the status of the running instance
194
- logs Print or follow the server log output
191
+ stop Stop running OMPChamber instances (every one, or --port <port>)
192
+ restart Stop and start again (every CLI-started instance, or --port <port>)
193
+ status Show running instances (every one, or --port <port>)
194
+ logs Print or follow server logs (every instance, or --port <port>)
195
195
 
196
196
  OPTIONS:
197
- -p, --port <port> Web server port (default: 3000)
197
+ -p, --port <port> Web server port (default: 3000); scopes status/stop/restart/logs
198
198
  --host <address> Bind address (default: 127.0.0.1)
199
199
  --hostname <address> Alias for --host
200
200
  --lan Bind to 0.0.0.0 for LAN access
201
201
  --prod Run the production build instead of the dev server
202
202
  --foreground Run the server in the foreground (no daemon)
203
203
  --no-daemon Alias for --foreground
204
- --all Apply the command to every running instance
204
+ --all Explicit form of the default for stop/restart (every instance)
205
205
  -c, --check Report whether a newer release exists without installing it
206
206
  --force Reinstall even when already up to date
207
207
  --no-restart Do not restart a running instance after updating
@@ -1,8 +1,14 @@
1
- // `ompchamber logs` — print or follow the server log file.
2
-
3
- import { findLiveInstance } from '@/cli/lib/runtime.js';
1
+ // `ompchamber logs` — print or follow the server logs.
2
+ //
3
+ // Scoping matches `status`/`stop`/`restart`: without `--port` the logs of every
4
+ // live instance are shown, each behind a `==> path <==` header, and `--port
5
+ // <port>` restricts the output to that instance. With nothing running, the log
6
+ // of the requested (or default) port is shown so a stopped server's output is
7
+ // still readable.
8
+
9
+ import { findLiveInstance, listLiveInstances } from '@/cli/lib/runtime.js';
4
10
  import { getLogFilePath } from '@/cli/lib/paths.js';
5
- import { log, printJson, fail, isJson, isQuiet } from '@/cli/lib/output.js';
11
+ import { log, warn, printJson, fail, isJson, isQuiet } from '@/cli/lib/output.js';
6
12
  const DEFAULT_LINES = 50;
7
13
  const FOLLOW_INTERVAL_MS = 300;
8
14
 
@@ -39,36 +45,47 @@ function writeLines(lines) {
39
45
  }
40
46
  }
41
47
 
42
- function sleep(ms) {
43
- return new Promise((resolve) => setTimeout(resolve, ms));
48
+ async function readTail(target, lineCount) {
49
+ const content = await Bun.file(target.logFile).text();
50
+ return { port: target.port, logFile: target.logFile, lines: splitLines(content).slice(-lineCount) };
44
51
  }
45
52
 
46
- async function followFile(filePath, initialCount) {
47
- const initial = await Bun.file(filePath).text();
48
- writeLines(splitLines(initial).slice(-initialCount));
53
+ /** Per-file read position, so following several logs keeps them independent. */
54
+ async function followFiles(targets, multiple) {
55
+ const state = [];
56
+ for (const target of targets) {
57
+ let position = 0;
58
+ try {
59
+ position = (await Bun.file(target.logFile).stat()).size;
60
+ } catch {
61
+ position = 0;
62
+ }
63
+ state.push({ target, position });
64
+ }
49
65
 
50
- let position = Buffer.byteLength(initial, 'utf8');
51
66
  let running = true;
52
67
  const stop = () => { running = false; };
53
-
54
68
  process.once('SIGINT', stop);
55
69
  process.once('SIGTERM', stop);
70
+
56
71
  try {
57
72
  while (running) {
58
- await sleep(FOLLOW_INTERVAL_MS);
59
-
60
- let size;
61
- try {
62
- size = (await Bun.file(filePath).stat()).size;
63
- } catch {
64
- continue;
73
+ await Bun.sleep(FOLLOW_INTERVAL_MS);
74
+ for (const entry of state) {
75
+ let size;
76
+ try {
77
+ size = (await Bun.file(entry.target.logFile).stat()).size;
78
+ } catch {
79
+ continue;
80
+ }
81
+ if (size < entry.position) entry.position = 0;
82
+ if (size === entry.position) continue;
83
+
84
+ if (multiple) process.stdout.write(`==> ${entry.target.logFile} <==\n`);
85
+ const chunk = await Bun.file(entry.target.logFile).slice(entry.position, size).text();
86
+ entry.position = size;
87
+ process.stdout.write(chunk);
65
88
  }
66
- if (size < position) position = 0;
67
- if (size === position) continue;
68
-
69
- const chunk = await Bun.file(filePath).slice(position, size).text();
70
- position = size;
71
- process.stdout.write(chunk);
72
89
  }
73
90
  } finally {
74
91
  process.removeListener('SIGINT', stop);
@@ -80,11 +97,28 @@ export async function run(options) {
80
97
  const json = isJson();
81
98
  const quiet = isQuiet();
82
99
  const requested = explicitPort(options);
83
- const live = await findLiveInstance(requested);
84
- const logFile = live?.logFile ?? getLogFilePath(requested ?? resolvePort(options));
100
+ const fallbackPort = requested ?? resolvePort(options);
85
101
 
86
- if (!(await Bun.file(logFile).exists())) {
87
- fail(`No log file found at ${logFile}.\nStart the server first with \`ompchamber serve\`.`);
102
+ const instances = requested !== null
103
+ ? [await findLiveInstance(requested)].filter(Boolean)
104
+ : await listLiveInstances();
105
+
106
+ const candidates = instances.length > 0
107
+ ? instances.map((entry) => ({ port: entry.port, logFile: entry.logFile ?? getLogFilePath(entry.port) }))
108
+ : [{ port: fallbackPort, logFile: getLogFilePath(fallbackPort) }];
109
+
110
+ const targets = [];
111
+ const skipped = [];
112
+ for (const candidate of candidates) {
113
+ if (await Bun.file(candidate.logFile).exists()) targets.push(candidate);
114
+ else skipped.push(candidate);
115
+ }
116
+ if (targets.length === 0) {
117
+ // A server started outside the CLI (`bun run dev`) logs to its own terminal,
118
+ // so its recorded port has no file here even though it is very much alive.
119
+ fail(skipped.length > 0 && instances.length > 0
120
+ ? `No log file at ${skipped[0].logFile}.\nPort ${skipped[0].port} was not started by the CLI — its output goes to the terminal that started it.`
121
+ : `No log file found at ${candidates[0].logFile}.\nStart the server first with \`ompchamber serve\`.`);
88
122
  }
89
123
 
90
124
  const requestedLines = Number(options?.lines);
@@ -93,16 +127,24 @@ export async function run(options) {
93
127
  : DEFAULT_LINES;
94
128
 
95
129
  if (json) {
96
- printJson({ logFile, lines: splitLines(await Bun.file(logFile).text()).slice(-lineCount) });
130
+ const tails = [];
131
+ for (const target of targets) tails.push(await readTail(target, lineCount));
132
+ printJson({ instances: tails, skipped: skipped.map((entry) => ({ port: entry.port, logFile: entry.logFile })) });
97
133
  return;
98
134
  }
99
135
 
100
- if (!quiet) log(`Log file: ${logFile}`);
136
+ const multiple = targets.length > 1;
137
+ if (!quiet) {
138
+ log(multiple ? `Log files: ${targets.length} instances` : `Log file: ${targets[0].logFile}`);
139
+ if (skipped.length > 0) {
140
+ warn(`Skipped ${skipped.length} instance(s) without a CLI log file: port ${skipped.map((entry) => entry.port).join(', ')}`);
141
+ }
142
+ }
101
143
 
102
- if (options?.follow) {
103
- await followFile(logFile, lineCount);
104
- return;
144
+ for (const target of targets) {
145
+ if (multiple) process.stdout.write(`==> ${target.logFile} <==\n`);
146
+ writeLines(splitLines(await Bun.file(target.logFile).text()).slice(-lineCount));
105
147
  }
106
148
 
107
- writeLines(splitLines(await Bun.file(logFile).text()).slice(-lineCount));
149
+ if (options?.follow) await followFiles(targets, multiple);
108
150
  }
@@ -1,9 +1,19 @@
1
- // `ompchamber restart` — stop the live instance (if any), then serve again.
1
+ // `ompchamber restart` — stop live instances, then serve again.
2
+ //
3
+ // Scoping matches `status` and `stop`: without `--port` every live instance is
4
+ // restarted on its own recorded port/host/mode, and `--port <port>` restricts
5
+ // the restart to that one. With nothing running, the command is simply `serve`.
6
+ //
7
+ // Instances the CLI did not start are left alone, exactly as `ompchamber update`
8
+ // leaves them: `bun run dev` and `bun run start` own their own process, so
9
+ // replacing it would break a loop OMPChamber does not manage. `ompchamber stop`
10
+ // is still the deliberate way to end one.
2
11
 
3
- import { findLiveInstance, stopInstance } from '@/cli/lib/runtime.js';
12
+ import { findLiveInstance, listLiveInstances, stopInstance } from '@/cli/lib/runtime.js';
4
13
  import { STOP_TIMEOUT_MS } from '@/cli/lib/process-lifecycle.js';
5
14
  import { log, isJson, isQuiet } from '@/cli/lib/output.js';
6
15
  import { run as runServe } from '@/cli/lib/commands/serve.js';
16
+ import { isAutoRestartable, skipRestartNote } from '@/server/lib/lifecycle/restart';
7
17
 
8
18
  function explicitPort(options) {
9
19
  const raw = options?.port;
@@ -15,14 +25,36 @@ function explicitPort(options) {
15
25
  export async function run(options, ctx) {
16
26
  const json = isJson();
17
27
  const quiet = isQuiet();
18
- const live = await findLiveInstance(explicitPort(options));
28
+ const requested = explicitPort(options);
19
29
 
20
- if (live) {
21
- if (!json && !quiet) {
22
- log(`Stopping OMPChamber on port ${live.port} (pid ${live.pid})...`);
30
+ const targets = requested !== null
31
+ ? [await findLiveInstance(requested)].filter(Boolean)
32
+ : await listLiveInstances();
33
+
34
+ if (targets.length === 0) return runServe(options, ctx);
35
+
36
+ for (const live of targets) {
37
+ if (!isAutoRestartable(live.launchMode)) {
38
+ if (!json && !quiet) log(`Skipping OMPChamber on port ${live.port}: ${skipRestartNote(live.launchMode)}.`);
39
+ continue;
23
40
  }
41
+
42
+ if (!json && !quiet) log(`Stopping OMPChamber on port ${live.port} (pid ${live.pid})...`);
24
43
  await stopInstance(live, { timeoutMs: STOP_TIMEOUT_MS });
25
- }
26
44
 
27
- return runServe(options, ctx);
45
+ // Reuse the recorded port/host/mode so an instance started on a non-default
46
+ // port, or bound to the LAN, comes back the way it was found.
47
+ await runServe(
48
+ {
49
+ ...options,
50
+ port: live.port,
51
+ host: live.host,
52
+ lan: live.host === '0.0.0.0',
53
+ prod: live.mode === 'prod',
54
+ foreground: false,
55
+ all: false,
56
+ },
57
+ ctx,
58
+ );
59
+ }
28
60
  }
@@ -6,8 +6,8 @@ import {
6
6
  buildServeInvocation,
7
7
  waitForHealth,
8
8
  findLiveInstance,
9
- probeHost,
10
9
  } from '@/cli/lib/runtime.js';
10
+ import { probeHost } from '@/server/lib/lifecycle/probe';
11
11
  import { log, ok, warn, fail, printJson, isJson, isQuiet } from '@/cli/lib/output.js';
12
12
  import { wireChildProcessLifecycle } from '@/cli/lib/process-lifecycle.js';
13
13
  import { ompStartupError } from '@/server/lib/omp/core/startup';
@@ -64,14 +64,22 @@ export async function run(options, ctx) {
64
64
 
65
65
  const existing = await findLiveInstance(port);
66
66
  if (existing) {
67
- fail(`An OMPChamber instance is already running on port ${port} (pid ${existing.pid}).\nUse \`ompchamber restart\` or a different \`--port\`.`);
67
+ // Refused here as well as in the server (lib/lifecycle/port-guard) so the
68
+ // CLI reports it immediately instead of spawning a child that exits during
69
+ // the health wait. Nothing is stopped: freeing a port is `ompchamber stop`.
70
+ fail(
71
+ `Port ${port} is already served by OMPChamber (pid ${existing.pid}, ${existing.mode}).`
72
+ + '\n Nothing was stopped — a starting instance never stops another one.'
73
+ + `\n Stop it first: ompchamber stop --port ${port}`
74
+ + `\n Or start on another port: ompchamber serve --port <port>`,
75
+ );
68
76
  }
69
77
 
70
78
  if (options?.foreground) {
71
79
  return runForeground({ pkgRoot, mode, port, host, quiet });
72
80
  }
73
81
 
74
- const { entry } = spawnDetachedServer({ pkgRoot, mode, port, host });
82
+ const { entry } = spawnDetachedServer({ pkgRoot, mode, port, host, launchMode: 'daemon' });
75
83
 
76
84
  if (!quiet && !json) {
77
85
  log(`Starting OMPChamber (${mode}) on ${entry.url} (pid ${entry.pid})...`);
@@ -96,7 +104,7 @@ export async function run(options, ctx) {
96
104
  }
97
105
 
98
106
  function runForeground({ pkgRoot, mode, port, host, quiet }) {
99
- const { file, args, env } = buildServeInvocation({ pkgRoot, mode, port, host });
107
+ const { file, args, env } = buildServeInvocation({ pkgRoot, mode, port, host, launchMode: 'foreground' });
100
108
  const url = `http://${probeHost(host)}:${port}`;
101
109
  if (!quiet) {
102
110
  log(`Running OMPChamber (${mode}) in the foreground on ${url} (Ctrl+C to stop)...`);
@@ -1,6 +1,13 @@
1
- // `ompchamber status` — report the live instance, its health and log path.
1
+ // `ompchamber status` — report the live instances, their health and log paths.
2
+ //
3
+ // Without `--port` every live instance is reported: a CLI server on one port, a
4
+ // `bun run dev` server on another and a production server on a third are three
5
+ // separate processes, and a status command that showed only the first would
6
+ // hide the two you forgot about. `--port <port>` narrows the report to that
7
+ // instance.
2
8
 
3
- import { findLiveInstance, waitForHealth, probeHost } from '@/cli/lib/runtime.js';
9
+ import { findLiveInstance, listLiveInstances, waitForHealth } from '@/cli/lib/runtime.js';
10
+ import { probeHost } from '@/server/lib/lifecycle/probe';
4
11
  import { log, ok, printJson, isJson } from '@/cli/lib/output.js';
5
12
 
6
13
  function explicitPort(options) {
@@ -30,34 +37,49 @@ function formatHealth(health) {
30
37
  return `ok (version ${version}${mock})`;
31
38
  }
32
39
 
40
+ /** One block per instance; the field names match the single-instance output. */
41
+ function printInstance(entry) {
42
+ log(` instance: ${entry.port}`);
43
+ log(` pid: ${entry.pid}`);
44
+ log(` port: ${entry.port}`);
45
+ log(` url: ${entry.url ?? `http://${probeHost(entry.host)}:${entry.port}`}`);
46
+ log(` mode: ${entry.mode ?? 'unknown'}`);
47
+ log(` launch: ${entry.launchMode ?? 'unknown'}${entry.source ? ` (${entry.source})` : ''}`);
48
+ log(` uptime: ${formatUptime(entry.startedAt)}`);
49
+ log(` health: ${formatHealth(entry.health)}`);
50
+ log(` log: ${entry.logFile}`);
51
+ }
52
+
33
53
  export async function run(options) {
34
54
  const json = isJson();
35
- const live = await findLiveInstance(explicitPort(options));
55
+ const requested = explicitPort(options);
56
+
57
+ const instances = requested !== null
58
+ ? [await findLiveInstance(requested)].filter(Boolean)
59
+ : await listLiveInstances();
36
60
 
37
- if (!live) {
61
+ if (instances.length === 0) {
38
62
  if (json) {
39
- printJson({ running: false });
63
+ printJson(requested !== null ? { running: false, port: requested } : { running: false, instances: [] });
40
64
  return;
41
65
  }
42
- log('OMPChamber is not running.');
66
+ log(requested !== null ? `No OMPChamber instance on port ${requested}.` : 'OMPChamber is not running.');
43
67
  return;
44
68
  }
45
69
 
46
- const health = await waitForHealth(live.port, live.host, 4000);
47
- const merged = { ...live, running: true, health };
70
+ const reported = [];
71
+ for (const entry of instances) {
72
+ reported.push({ ...entry, health: await waitForHealth(entry.port, entry.host, 4000) });
73
+ }
48
74
 
49
75
  if (json) {
50
- printJson(merged);
76
+ printJson(requested !== null ? { ...reported[0], running: true } : { running: true, instances: reported });
51
77
  return;
52
78
  }
53
79
 
54
- ok('OMPChamber is running');
55
- log(` instance: ${live.port}`);
56
- log(` pid: ${live.pid}`);
57
- log(` port: ${live.port}`);
58
- log(` url: ${live.url ?? `http://${probeHost(live.host)}:${live.port}`}`);
59
- log(` mode: ${live.mode ?? 'unknown'}`);
60
- log(` uptime: ${formatUptime(live.startedAt)}`);
61
- log(` health: ${formatHealth(health)}`);
62
- log(` log: ${live.logFile}`);
80
+ ok(reported.length === 1 ? 'OMPChamber is running' : `OMPChamber is running — ${reported.length} instances`);
81
+ for (const [index, entry] of reported.entries()) {
82
+ if (index > 0) log('');
83
+ printInstance(entry);
84
+ }
63
85
  }
@@ -1,6 +1,6 @@
1
1
  // `ompchamber stop` — stop one or every live OMPChamber instance.
2
2
 
3
- import { listRegistries, findLiveInstance, isProcessAlive, stopInstance } from '@/cli/lib/runtime.js';
3
+ import { listLiveInstances, findLiveInstance, stopInstance } from '@/cli/lib/runtime.js';
4
4
  import { STOP_TIMEOUT_MS } from '@/cli/lib/process-lifecycle.js';
5
5
  import { ok, warn, printJson, isJson } from '@/cli/lib/output.js';
6
6
 
@@ -27,13 +27,18 @@ function explicitPort(options) {
27
27
 
28
28
  export async function run(options) {
29
29
  const json = isJson();
30
+ const requested = explicitPort(options);
30
31
 
32
+ // No `--port` means every live instance: a CLI server, a dev run and a
33
+ // production server are three processes, and `stop` without a port is the
34
+ // command that clears all of them. `--all` is the explicit spelling of the
35
+ // same default, kept because it reads well in scripts.
31
36
  let targets;
32
- if (options?.all) {
33
- targets = (await listRegistries()).filter((entry) => entry && isProcessAlive(Number(entry.pid)));
34
- } else {
35
- const live = await findLiveInstance(explicitPort(options));
37
+ if (requested !== null) {
38
+ const live = await findLiveInstance(requested);
36
39
  targets = live ? [live] : [];
40
+ } else {
41
+ targets = await listLiveInstances();
37
42
  }
38
43
 
39
44
  if (targets.length === 0) {
@@ -41,9 +46,9 @@ export async function run(options) {
41
46
  printJson({ stopped: [] });
42
47
  return;
43
48
  }
44
- warn(options?.all
49
+ warn(requested === null
45
50
  ? 'No running OMPChamber instances found.'
46
- : `No running OMPChamber instance on port ${explicitPort(options) ?? resolvePort(options)}.`);
51
+ : `No running OMPChamber instance on port ${requested}.`);
47
52
  return;
48
53
  }
49
54
 
@@ -2,32 +2,40 @@
2
2
  //
3
3
  // The installation mechanics live in the shared update engine so the CLI and
4
4
  // the console's "Update" button do the same thing; this command owns the
5
- // terminal presentation and restarts any instance still serving the previous
6
- // build.
5
+ // terminal presentation and restarts the instances still serving the previous
6
+ // build (the ones the CLI started — a server running from source is left to its
7
+ // own launcher).
7
8
 
8
9
  import { color, configure, error, log, ok, warn, printJson, isJson, isQuiet } from '@/cli/lib/output.js';
9
- import { listRegistries, findLiveInstance, isProcessAlive, stopInstance } from '@/cli/lib/runtime.js';
10
+ import { listLiveInstances, findLiveInstance, stopInstance } from '@/cli/lib/runtime.js';
10
11
  import { STOP_TIMEOUT_MS } from '@/cli/lib/process-lifecycle.js';
11
12
  import { run as runServe } from '@/cli/lib/commands/serve.js';
13
+ import { isAutoRestartable, skipRestartNote } from '@/server/lib/lifecycle/restart';
12
14
  import { resolveInstallContext, resolveOmpChamberVersion, updateOmpChamber } from '@/server/lib/updates/install';
13
15
 
14
16
  /**
15
17
  * Live instances to restart after an update: every one with `--all`, otherwise
16
18
  * the first. Their recorded port/host/mode is reused, so a server started on a
17
- * non-default port comes back on that same port.
19
+ * non-default port comes back on that same port. Instances started outside the
20
+ * CLI (`bun run dev`, `bun run start`, a supervisor) are reported and left
21
+ * running — their own launcher is what applies the new files.
18
22
  */
19
23
  async function liveInstances(options) {
20
- if (options?.all) {
21
- const entries = await listRegistries();
22
- return entries.filter((entry) => entry && isProcessAlive(Number(entry.pid)));
23
- }
24
+ if (options?.all) return await listLiveInstances();
24
25
  const live = await findLiveInstance(null);
25
26
  return live ? [live] : [];
26
27
  }
27
28
 
28
29
  async function restartInstances(options, ctx) {
29
30
  const restarted = [];
31
+ const skipped = [];
30
32
  for (const entry of await liveInstances(options)) {
33
+ if (!isAutoRestartable(entry.launchMode)) {
34
+ skipped.push({ port: entry.port, pid: entry.pid, launchMode: entry.launchMode });
35
+ log(`Skipping OMPChamber on port ${entry.port}: ${skipRestartNote(entry.launchMode)}.`);
36
+ continue;
37
+ }
38
+
31
39
  log(`Restarting OMPChamber on port ${entry.port} (pid ${entry.pid})...`);
32
40
  await stopInstance(entry, { timeoutMs: STOP_TIMEOUT_MS });
33
41
  await runServe(
@@ -44,7 +52,7 @@ async function restartInstances(options, ctx) {
44
52
  );
45
53
  restarted.push(entry.port);
46
54
  }
47
- return restarted;
55
+ return { restarted, skipped };
48
56
  }
49
57
 
50
58
  async function runCheck(pkgRoot, json) {
@@ -94,8 +102,10 @@ export async function run(options, ctx) {
94
102
  // Restarting prints through the serve command; silence it so stdout stays
95
103
  // one JSON document.
96
104
  if (result.success && result.updated && shouldRestart) configure({ json: false, quiet: true });
97
- const restarted = result.success && result.updated && shouldRestart ? await restartInstances(options, ctx) : [];
98
- printJson({ ...result, restarted });
105
+ const { restarted, skipped } = result.success && result.updated && shouldRestart
106
+ ? await restartInstances(options, ctx)
107
+ : { restarted: [], skipped: [] };
108
+ printJson({ ...result, restarted, skipped });
99
109
  if (!result.success) process.exitCode = 1;
100
110
  return;
101
111
  }
@@ -114,7 +124,8 @@ export async function run(options, ctx) {
114
124
  return;
115
125
  }
116
126
 
117
- if ((await restartInstances(options, ctx)).length === 0) {
127
+ const { restarted, skipped } = await restartInstances(options, ctx);
128
+ if (restarted.length === 0 && skipped.length === 0) {
118
129
  log('No running instance to restart.');
119
130
  }
120
131
  }
@@ -1,29 +1,16 @@
1
1
  import fs from 'node:fs';
2
- import { joinPath, resolvePath, homeDir } from '@/cli/lib/path-utils.js';
2
+ import { getDataDir, getRunDir } from '@/server/lib/lifecycle/paths';
3
+ import { joinPath } from '@/cli/lib/path-utils.js';
3
4
 
4
5
  /**
5
- * Resolve the OMPChamber data directory.
6
+ * CLI-side paths.
6
7
  *
7
- * Matches the convention used by app/db.server.ts so the CLI and the web
8
- * console always agree on where state lives.
8
+ * The data directory and the per-port run directory are owned by
9
+ * `src/server/lib/lifecycle/paths` — the server writes its instance record
10
+ * there, and the CLI must read the same file. Only the log location is
11
+ * CLI-specific: the CLI redirects the daemon's stdout into it, so the server
12
+ * never needs to know where the logs live.
9
13
  */
10
- export function getDataDir() {
11
- const override = Bun.env.OMPCHAMBER_DATA_DIR;
12
- if (typeof override === 'string' && override.trim().length > 0) {
13
- return resolvePath(override.trim());
14
- }
15
- return joinPath(homeDir(), '.ompchamber');
16
- }
17
-
18
- /**
19
- * Directory holding per-instance registry files (`<port>.json`).
20
- * Created on demand with owner-only permissions.
21
- */
22
- export function getRunDir() {
23
- const dir = joinPath(getDataDir(), 'run');
24
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
25
- return dir;
26
- }
27
14
 
28
15
  /**
29
16
  * Directory holding per-instance log files.
@@ -34,13 +21,6 @@ export function getLogsDir() {
34
21
  return dir;
35
22
  }
36
23
 
37
- /**
38
- * Registry file describing the running instance bound to `port`.
39
- */
40
- export function getRegistryPath(port) {
41
- return joinPath(getRunDir(), `${port}.json`);
42
- }
43
-
44
24
  /**
45
25
  * Log file for the running instance bound to `port`.
46
26
  */