@openchamber/web 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +34 -0
  2. package/bin/cli.js +561 -0
  3. package/dist/apple-touch-icon-120x120.png +0 -0
  4. package/dist/apple-touch-icon-152x152.png +0 -0
  5. package/dist/apple-touch-icon-167x167.png +0 -0
  6. package/dist/apple-touch-icon-180x180.png +0 -0
  7. package/dist/apple-touch-icon.png +0 -0
  8. package/dist/apple-touch-icon.svg +18 -0
  9. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  10. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  12. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  18. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  27. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  30. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  33. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  36. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  39. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  51. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  54. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  57. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  60. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  62. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  65. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  68. package/dist/assets/MonacoDiffViewer-J2AIDXvs.js +1 -0
  69. package/dist/assets/ToolOutputDialog-B0y5ge-3.js +5 -0
  70. package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  71. package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  72. package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  73. package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  74. package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  75. package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  76. package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  77. package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  78. package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  79. package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  80. package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  81. package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  82. package/dist/assets/index-iDfKTtMQ.css +1 -0
  83. package/dist/assets/index-kNntYPVa.js +2 -0
  84. package/dist/assets/main-BEJ2XliY.css +1 -0
  85. package/dist/assets/main-Ba339xde.js +59 -0
  86. package/dist/assets/vendor--B3aGWKBE.css +32 -0
  87. package/dist/assets/vendor-.pnpm-B1ce5n1Z.js +3192 -0
  88. package/dist/favicon-16.png +0 -0
  89. package/dist/favicon-32.png +0 -0
  90. package/dist/index.html +197 -0
  91. package/dist/logo-dark.svg +4 -0
  92. package/dist/logo-light.svg +4 -0
  93. package/dist/site.webmanifest +36 -0
  94. package/dist/vite.svg +1 -0
  95. package/package.json +92 -0
  96. package/public/apple-touch-icon-120x120.png +0 -0
  97. package/public/apple-touch-icon-152x152.png +0 -0
  98. package/public/apple-touch-icon-167x167.png +0 -0
  99. package/public/apple-touch-icon-180x180.png +0 -0
  100. package/public/apple-touch-icon.png +0 -0
  101. package/public/apple-touch-icon.svg +18 -0
  102. package/public/favicon-16.png +0 -0
  103. package/public/favicon-32.png +0 -0
  104. package/public/logo-dark.svg +4 -0
  105. package/public/logo-light.svg +4 -0
  106. package/public/site.webmanifest +36 -0
  107. package/public/vite.svg +1 -0
  108. package/server/index.d.ts +28 -0
  109. package/server/index.js +3038 -0
  110. package/server/lib/git-identity-storage.js +108 -0
  111. package/server/lib/git-service.js +899 -0
  112. package/server/lib/opencode-config.js +471 -0
  113. package/server/lib/opencode-config.js.d.ts +12 -0
  114. package/server/lib/ui-auth.js +266 -0
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @openchamber/web
2
+
3
+ Web interface for the [OpenCode](https://opencode.ai) AI coding agent.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm add -g @openchamber/web
9
+
10
+ openchamber # Start on port 3000
11
+ openchamber --port 8080 # Custom port
12
+ openchamber --daemon # Background mode
13
+ openchamber --ui-password secret # Password-protect UI
14
+ openchamber stop # Stop server
15
+ ```
16
+
17
+ ## Prerequisites
18
+
19
+ - [OpenCode CLI](https://opencode.ai) installed and running (`opencode serve`)
20
+ - Node.js 20+
21
+
22
+ ## Features
23
+
24
+ - Integrated terminal
25
+ - Git operations with identity management and AI commit message generation
26
+ - Beautiful themes (Flexoki Light/Dark)
27
+ - Mobile-optimized with edge-swipe gestures
28
+ - Rich permission cards with syntax-highlighted operation previews
29
+ - Smart tool visualization (inline diffs, file trees, results highlighting)
30
+ - Per-agent permission mode control
31
+
32
+ ## License
33
+
34
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { spawn, spawnSync } from 'child_process';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ const DEFAULT_PORT = 3000;
12
+ const PACKAGE_JSON = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
13
+
14
+ function parseArgs() {
15
+ const args = process.argv.slice(2);
16
+ const envPassword = process.env.OPENCHAMBER_UI_PASSWORD || undefined;
17
+ const options = { port: DEFAULT_PORT, daemon: false, uiPassword: envPassword };
18
+ let command = 'serve';
19
+
20
+ const consumeValue = (currentIndex, inlineValue) => {
21
+ if (typeof inlineValue === 'string' && inlineValue.length > 0) {
22
+ return { value: inlineValue, nextIndex: currentIndex };
23
+ }
24
+ const candidate = args[currentIndex + 1];
25
+ if (typeof candidate === 'string' && !candidate.startsWith('-')) {
26
+ return { value: candidate, nextIndex: currentIndex + 1 };
27
+ }
28
+ return { value: undefined, nextIndex: currentIndex };
29
+ };
30
+
31
+ for (let i = 0; i < args.length; i++) {
32
+ const arg = args[i];
33
+
34
+ if (arg.startsWith('-')) {
35
+ let optionName;
36
+ let inlineValue;
37
+
38
+ if (arg.startsWith('--')) {
39
+ const eqIndex = arg.indexOf('=');
40
+ optionName = eqIndex >= 0 ? arg.slice(2, eqIndex) : arg.slice(2);
41
+ inlineValue = eqIndex >= 0 ? arg.slice(eqIndex + 1) : undefined;
42
+ } else {
43
+ optionName = arg.slice(1);
44
+ inlineValue = undefined;
45
+ }
46
+
47
+ switch (optionName) {
48
+ case 'port':
49
+ case 'p': {
50
+ const { value, nextIndex } = consumeValue(i, inlineValue);
51
+ i = nextIndex;
52
+ const parsed = parseInt(value ?? '', 10);
53
+ options.port = Number.isFinite(parsed) ? parsed : DEFAULT_PORT;
54
+ break;
55
+ }
56
+ case 'daemon':
57
+ case 'd':
58
+ options.daemon = true;
59
+ break;
60
+ case 'ui-password': {
61
+ const { value, nextIndex } = consumeValue(i, inlineValue);
62
+ i = nextIndex;
63
+ options.uiPassword = typeof value === 'string' ? value : '';
64
+ break;
65
+ }
66
+ case 'help':
67
+ case 'h':
68
+ showHelp();
69
+ process.exit(0);
70
+ break;
71
+ case 'version':
72
+ case 'v':
73
+ console.log(PACKAGE_JSON.version);
74
+ process.exit(0);
75
+ break;
76
+ }
77
+ } else {
78
+ command = arg;
79
+ }
80
+ }
81
+
82
+ return { command, options };
83
+ }
84
+
85
+ function showHelp() {
86
+ console.log(`
87
+ OpenChamber - Web interface for the OpenCode AI coding agent
88
+
89
+ USAGE:
90
+ openchamber [COMMAND] [OPTIONS]
91
+
92
+ COMMANDS:
93
+ serve Start the web server (default)
94
+ stop Stop running instance(s)
95
+ restart Stop and start the server
96
+ status Show server status
97
+
98
+ OPTIONS:
99
+ -p, --port Web server port (default: ${DEFAULT_PORT})
100
+ --ui-password Protect browser UI with single password
101
+ -d, --daemon Run in background (serve command)
102
+ -h, --help Show help
103
+ -v, --version Show version
104
+
105
+ ENVIRONMENT:
106
+ OPENCHAMBER_UI_PASSWORD Alternative to --ui-password flag
107
+
108
+ EXAMPLES:
109
+ openchamber # Start on default port 3000
110
+ openchamber --port 8080 # Start on port 8080
111
+ openchamber serve --daemon # Start in background
112
+ openchamber stop # Stop all running instances
113
+ openchamber stop --port 3000 # Stop specific instance
114
+ openchamber status # Check status
115
+ `);
116
+ }
117
+
118
+ const WINDOWS_EXTENSIONS = process.platform === 'win32'
119
+ ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
120
+ .split(';')
121
+ .map((ext) => ext.trim().toLowerCase())
122
+ .filter(Boolean)
123
+ .map((ext) => (ext.startsWith('.') ? ext : `.${ext}`))
124
+ : [''];
125
+
126
+ function isExecutable(filePath) {
127
+ try {
128
+ const stats = fs.statSync(filePath);
129
+ if (!stats.isFile()) {
130
+ return false;
131
+ }
132
+ if (process.platform === 'win32') {
133
+ return true;
134
+ }
135
+ fs.accessSync(filePath, fs.constants.X_OK);
136
+ return true;
137
+ } catch (error) {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ function resolveExplicitBinary(candidate) {
143
+ if (!candidate) {
144
+ return null;
145
+ }
146
+ if (candidate.includes(path.sep) || path.isAbsolute(candidate)) {
147
+ const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
148
+ return isExecutable(resolved) ? resolved : null;
149
+ }
150
+ return null;
151
+ }
152
+
153
+ function searchPathFor(command) {
154
+ const pathValue = process.env.PATH || '';
155
+ const segments = pathValue.split(path.delimiter).filter(Boolean);
156
+ for (const dir of segments) {
157
+ for (const ext of WINDOWS_EXTENSIONS) {
158
+ const fileName = process.platform === 'win32' ? `${command}${ext}` : command;
159
+ const candidate = path.join(dir, fileName);
160
+ if (isExecutable(candidate)) {
161
+ return candidate;
162
+ }
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+
168
+ async function checkOpenCodeCLI() {
169
+ if (process.env.OPENCODE_BINARY) {
170
+ const override = resolveExplicitBinary(process.env.OPENCODE_BINARY);
171
+ if (override) {
172
+ process.env.OPENCODE_BINARY = override;
173
+ return override;
174
+ }
175
+ console.warn(`Warning: OPENCODE_BINARY="${process.env.OPENCODE_BINARY}" is not an executable file. Falling back to PATH lookup.`);
176
+ }
177
+
178
+ const resolvedFromPath = searchPathFor('opencode');
179
+ if (resolvedFromPath) {
180
+ process.env.OPENCODE_BINARY = resolvedFromPath;
181
+ return resolvedFromPath;
182
+ }
183
+
184
+ if (process.platform !== 'win32') {
185
+ const shellCandidates = [];
186
+ if (process.env.SHELL) {
187
+ shellCandidates.push(process.env.SHELL);
188
+ }
189
+ shellCandidates.push('/bin/bash', '/bin/zsh', '/bin/sh');
190
+
191
+ for (const shellPath of shellCandidates) {
192
+ if (!shellPath || !isExecutable(shellPath)) {
193
+ continue;
194
+ }
195
+ try {
196
+ const result = spawnSync(shellPath, ['-lic', 'command -v opencode'], {
197
+ encoding: 'utf8',
198
+ stdio: ['ignore', 'pipe', 'pipe'],
199
+ });
200
+ if (result.status === 0) {
201
+ const candidate = result.stdout.trim().split(/\s+/).pop();
202
+ if (candidate && isExecutable(candidate)) {
203
+ const dir = path.dirname(candidate);
204
+ const currentPath = process.env.PATH || '';
205
+ const segments = currentPath.split(path.delimiter).filter(Boolean);
206
+ if (!segments.includes(dir)) {
207
+ segments.unshift(dir);
208
+ process.env.PATH = segments.join(path.delimiter);
209
+ }
210
+ process.env.OPENCODE_BINARY = candidate;
211
+ return candidate;
212
+ }
213
+ }
214
+ } catch (error) {
215
+
216
+ }
217
+ }
218
+ } else {
219
+ try {
220
+ const result = spawnSync('where', ['opencode'], {
221
+ encoding: 'utf8',
222
+ stdio: ['ignore', 'pipe', 'pipe'],
223
+ });
224
+ if (result.status === 0) {
225
+ const candidate = result.stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
226
+ if (candidate && isExecutable(candidate)) {
227
+ process.env.OPENCODE_BINARY = candidate;
228
+ return candidate;
229
+ }
230
+ }
231
+ } catch (error) {
232
+
233
+ }
234
+ }
235
+
236
+ console.error('Error: Unable to locate the opencode CLI on PATH.');
237
+ console.error(`Current PATH: ${process.env.PATH || '<empty>'}`);
238
+ console.error('Ensure the CLI is installed and reachable, or set OPENCODE_BINARY to its full path.');
239
+ process.exit(1);
240
+ }
241
+
242
+ async function getPidFilePath(port) {
243
+ const os = await import('os');
244
+ const tmpDir = os.tmpdir();
245
+ return path.join(tmpDir, `openchamber-${port}.pid`);
246
+ }
247
+
248
+ function readPidFile(pidFilePath) {
249
+ try {
250
+ const content = fs.readFileSync(pidFilePath, 'utf8').trim();
251
+ const pid = parseInt(content);
252
+ if (isNaN(pid)) {
253
+ return null;
254
+ }
255
+ return pid;
256
+ } catch (error) {
257
+ return null;
258
+ }
259
+ }
260
+
261
+ function writePidFile(pidFilePath, pid) {
262
+ try {
263
+ fs.writeFileSync(pidFilePath, pid.toString());
264
+ } catch (error) {
265
+ console.warn(`Warning: Could not write PID file: ${error.message}`);
266
+ }
267
+ }
268
+
269
+ function removePidFile(pidFilePath) {
270
+ try {
271
+ if (fs.existsSync(pidFilePath)) {
272
+ fs.unlinkSync(pidFilePath);
273
+ }
274
+ } catch (error) {
275
+ console.warn(`Warning: Could not remove PID file: ${error.message}`);
276
+ }
277
+ }
278
+
279
+ function isProcessRunning(pid) {
280
+ try {
281
+ process.kill(pid, 0);
282
+ return true;
283
+ } catch (error) {
284
+ return false;
285
+ }
286
+ }
287
+
288
+ const commands = {
289
+ async serve(options) {
290
+ const pidFilePath = await getPidFilePath(options.port);
291
+
292
+ const existingPid = readPidFile(pidFilePath);
293
+ if (existingPid && isProcessRunning(existingPid)) {
294
+ console.error(`Error: OpenChamber is already running on port ${options.port} (PID: ${existingPid})`);
295
+ console.error('Use "openchamber stop" to stop the existing instance');
296
+ process.exit(1);
297
+ }
298
+
299
+ const opencodeBinary = await checkOpenCodeCLI();
300
+
301
+ const serverPath = path.join(__dirname, '..', 'server', 'index.js');
302
+
303
+ const serverArgs = [serverPath, '--port', options.port.toString()];
304
+ if (typeof options.uiPassword === 'string') {
305
+ serverArgs.push('--ui-password', options.uiPassword);
306
+ }
307
+
308
+ if (options.daemon) {
309
+
310
+ const child = spawn(process.execPath, serverArgs, {
311
+ detached: true,
312
+ stdio: 'ignore',
313
+ env: {
314
+ ...process.env,
315
+ OPENCHAMBER_PORT: options.port.toString(),
316
+ OPENCODE_BINARY: opencodeBinary,
317
+ ...(typeof options.uiPassword === 'string' ? { OPENCHAMBER_UI_PASSWORD: options.uiPassword } : {})
318
+ }
319
+ });
320
+
321
+ child.unref();
322
+
323
+ setTimeout(() => {
324
+ if (isProcessRunning(child.pid)) {
325
+ writePidFile(pidFilePath, child.pid);
326
+ console.log(`OpenChamber started in daemon mode on port ${options.port}`);
327
+ console.log(`PID: ${child.pid}`);
328
+ console.log(`Visit: http://localhost:${options.port}`);
329
+ } else {
330
+ console.error('Failed to start server in daemon mode');
331
+ process.exit(1);
332
+ }
333
+ }, 1000);
334
+
335
+ } else {
336
+
337
+ process.env.OPENCODE_BINARY = opencodeBinary;
338
+ if (typeof options.uiPassword === 'string') {
339
+ process.env.OPENCHAMBER_UI_PASSWORD = options.uiPassword;
340
+ }
341
+ const { startWebUiServer } = await import(serverPath);
342
+ await startWebUiServer({
343
+ port: options.port,
344
+ attachSignals: true,
345
+ exitOnShutdown: true,
346
+ uiPassword: typeof options.uiPassword === 'string' ? options.uiPassword : null
347
+ });
348
+ }
349
+ },
350
+
351
+ async stop(options) {
352
+ const os = await import('os');
353
+ const tmpDir = os.tmpdir();
354
+
355
+ let runningInstances = [];
356
+
357
+ try {
358
+ const files = fs.readdirSync(tmpDir);
359
+ const pidFiles = files.filter(file => file.startsWith('openchamber-') && file.endsWith('.pid'));
360
+
361
+ for (const file of pidFiles) {
362
+ const port = parseInt(file.replace('openchamber-', '').replace('.pid', ''));
363
+ if (!isNaN(port)) {
364
+ const pidFilePath = path.join(tmpDir, file);
365
+ const pid = readPidFile(pidFilePath);
366
+
367
+ if (pid && isProcessRunning(pid)) {
368
+ runningInstances.push({ port, pid, pidFilePath });
369
+ } else {
370
+
371
+ removePidFile(pidFilePath);
372
+ }
373
+ }
374
+ }
375
+ } catch (error) {
376
+
377
+ }
378
+
379
+ if (runningInstances.length === 0) {
380
+ console.log('No running OpenChamber instances found');
381
+ return;
382
+ }
383
+
384
+ const portWasSpecified = process.argv.includes('--port') || process.argv.includes('-p');
385
+
386
+ if (portWasSpecified) {
387
+ const targetInstance = runningInstances.find(inst => inst.port === options.port);
388
+
389
+ if (!targetInstance) {
390
+ console.log(`No OpenChamber instance found running on port ${options.port}`);
391
+ return;
392
+ }
393
+
394
+ console.log(`Stopping OpenChamber (PID: ${targetInstance.pid}, Port: ${targetInstance.port})...`);
395
+
396
+ try {
397
+ process.kill(targetInstance.pid, 'SIGTERM');
398
+
399
+ let attempts = 0;
400
+ const maxAttempts = 10;
401
+
402
+ const checkShutdown = setInterval(() => {
403
+ attempts++;
404
+ if (!isProcessRunning(targetInstance.pid)) {
405
+ clearInterval(checkShutdown);
406
+ removePidFile(targetInstance.pidFilePath);
407
+ console.log('OpenChamber stopped successfully');
408
+ } else if (attempts >= maxAttempts) {
409
+ clearInterval(checkShutdown);
410
+ console.log('Force killing process...');
411
+ process.kill(targetInstance.pid, 'SIGKILL');
412
+ removePidFile(targetInstance.pidFilePath);
413
+ console.log('OpenChamber force stopped');
414
+ }
415
+ }, 500);
416
+
417
+ } catch (error) {
418
+ console.error(`Error stopping process: ${error.message}`);
419
+ process.exit(1);
420
+ }
421
+ } else {
422
+
423
+ console.log(`Stopping all OpenChamber instances (${runningInstances.length} found)...`);
424
+
425
+ for (const instance of runningInstances) {
426
+ console.log(` Stopping instance on port ${instance.port} (PID: ${instance.pid})...`);
427
+
428
+ try {
429
+ process.kill(instance.pid, 'SIGTERM');
430
+
431
+ let attempts = 0;
432
+ const maxAttempts = 10;
433
+
434
+ await new Promise((resolve) => {
435
+ const checkShutdown = setInterval(() => {
436
+ attempts++;
437
+ if (!isProcessRunning(instance.pid)) {
438
+ clearInterval(checkShutdown);
439
+ removePidFile(instance.pidFilePath);
440
+ console.log(` Port ${instance.port} stopped successfully`);
441
+ resolve(true);
442
+ } else if (attempts >= maxAttempts) {
443
+ clearInterval(checkShutdown);
444
+ console.log(` Force killing port ${instance.port}...`);
445
+ try {
446
+ process.kill(instance.pid, 'SIGKILL');
447
+ removePidFile(instance.pidFilePath);
448
+ console.log(` Port ${instance.port} force stopped`);
449
+ } catch (e) {
450
+
451
+ }
452
+ resolve(true);
453
+ }
454
+ }, 500);
455
+ });
456
+
457
+ } catch (error) {
458
+ console.error(` Error stopping port ${instance.port}: ${error.message}`);
459
+ }
460
+ }
461
+
462
+ console.log('\nAll OpenChamber instances stopped');
463
+ }
464
+ },
465
+
466
+ async restart(options) {
467
+ await commands.stop(options);
468
+ await commands.serve(options);
469
+ },
470
+
471
+ async status(options) {
472
+ const os = await import('os');
473
+ const tmpDir = os.tmpdir();
474
+
475
+ let runningInstances = [];
476
+ let stoppedInstances = [];
477
+
478
+ try {
479
+ const files = fs.readdirSync(tmpDir);
480
+ const pidFiles = files.filter(file => file.startsWith('openchamber-') && file.endsWith('.pid'));
481
+
482
+ for (const file of pidFiles) {
483
+ const port = parseInt(file.replace('openchamber-', '').replace('.pid', ''));
484
+ if (!isNaN(port)) {
485
+ const pidFilePath = path.join(tmpDir, file);
486
+ const pid = readPidFile(pidFilePath);
487
+
488
+ if (pid && isProcessRunning(pid)) {
489
+ runningInstances.push({ port, pid, pidFilePath });
490
+ } else {
491
+
492
+ removePidFile(pidFilePath);
493
+ stoppedInstances.push({ port });
494
+ }
495
+ }
496
+ }
497
+ } catch (error) {
498
+
499
+ }
500
+
501
+ if (runningInstances.length === 0) {
502
+ console.log('OpenChamber Status:');
503
+ console.log(' Status: Stopped');
504
+ if (stoppedInstances.length > 0) {
505
+ console.log(` Previously used ports: ${stoppedInstances.map(s => s.port).join(', ')}`);
506
+ }
507
+ return;
508
+ }
509
+
510
+ console.log('OpenChamber Status:');
511
+ for (const [index, instance] of runningInstances.entries()) {
512
+ if (runningInstances.length > 1) {
513
+ console.log(`\nInstance ${index + 1}:`);
514
+ }
515
+ console.log(' Status: Running');
516
+ console.log(` PID: ${instance.pid}`);
517
+ console.log(` Port: ${instance.port}`);
518
+ console.log(` Visit: http://localhost:${instance.port}`);
519
+
520
+ try {
521
+ const { execSync } = await import('child_process');
522
+ const startTime = execSync(`ps -o lstart= -p ${instance.pid}`, { encoding: 'utf8' }).trim();
523
+ console.log(` Start Time: ${startTime}`);
524
+ } catch (error) {
525
+
526
+ }
527
+ }
528
+ },
529
+
530
+ };
531
+
532
+ async function main() {
533
+ const { command, options } = parseArgs();
534
+
535
+ if (!commands[command]) {
536
+ console.error(`Error: Unknown command '${command}'`);
537
+ console.error('Use --help to see available commands');
538
+ process.exit(1);
539
+ }
540
+
541
+ try {
542
+ await commands[command](options);
543
+ } catch (error) {
544
+ console.error(`Error executing command '${command}': ${error.message}`);
545
+ process.exit(1);
546
+ }
547
+ }
548
+
549
+ process.on('unhandledRejection', (reason, promise) => {
550
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
551
+ process.exit(1);
552
+ });
553
+
554
+ process.on('uncaughtException', (error) => {
555
+ console.error('Uncaught Exception:', error);
556
+ process.exit(1);
557
+ });
558
+
559
+ main();
560
+
561
+ export { commands, parseArgs, getPidFilePath };
Binary file
@@ -0,0 +1,18 @@
1
+ <svg width="180" height="180" viewBox="0 0 70 70" xmlns="http://www.w3.org/2000/svg">
2
+ <g transform="translate(8.75, 8.75) scale(0.75)">
3
+ <!-- Letter O with white fill and thin black stroke -->
4
+ <path fill-rule="evenodd" clip-rule="evenodd"
5
+ d="M0 13H35V58H0V13ZM26.25 22.1957H8.75V48.701H26.25V22.1957Z"
6
+ fill="white"
7
+ stroke="black"
8
+ stroke-width="1.1"
9
+ stroke-linejoin="round"/>
10
+
11
+ <!-- Letter C with white fill and thin black stroke -->
12
+ <path d="M43.75 13H70V22.1957H52.5V48.701H70V57.8967H43.75V13Z"
13
+ fill="white"
14
+ stroke="black"
15
+ stroke-width="1.1"
16
+ stroke-linejoin="round"/>
17
+ </g>
18
+ </svg>