@openchamber/web 1.2.6 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -157,10 +157,10 @@
157
157
  pointer-events: none;
158
158
  }
159
159
  </style>
160
- <script type="module" crossorigin src="/assets/index-Ch3--lhG.js"></script>
161
- <link rel="modulepreload" crossorigin href="/assets/vendor-.pnpm-B7ko08Tb.js">
160
+ <script type="module" crossorigin src="/assets/index--hIzFLog.js"></script>
161
+ <link rel="modulepreload" crossorigin href="/assets/vendor-.pnpm-Buk6Khtx.js">
162
162
  <link rel="stylesheet" crossorigin href="/assets/vendor--B3aGWKBE.css">
163
- <link rel="stylesheet" crossorigin href="/assets/index-DR5HBLaO.css">
163
+ <link rel="stylesheet" crossorigin href="/assets/index-BSN-fGG5.css">
164
164
  </head>
165
165
  <body class="h-full bg-background text-foreground">
166
166
  <div id="root" class="h-full">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchamber/web",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
package/server/index.js CHANGED
@@ -1605,6 +1605,112 @@ async function main(options = {}) {
1605
1605
 
1606
1606
  app.use('/api', (req, res, next) => uiAuthController.requireAuth(req, res, next));
1607
1607
 
1608
+ app.get('/api/openchamber/update-check', async (_req, res) => {
1609
+ try {
1610
+ const { checkForUpdates } = await import('./lib/package-manager.js');
1611
+ const updateInfo = await checkForUpdates();
1612
+ res.json(updateInfo);
1613
+ } catch (error) {
1614
+ console.error('Failed to check for updates:', error);
1615
+ res.status(500).json({
1616
+ available: false,
1617
+ error: error instanceof Error ? error.message : 'Failed to check for updates',
1618
+ });
1619
+ }
1620
+ });
1621
+
1622
+ app.post('/api/openchamber/update-install', async (_req, res) => {
1623
+ try {
1624
+ const { spawn: spawnChild } = await import('child_process');
1625
+ const {
1626
+ checkForUpdates,
1627
+ getUpdateCommand,
1628
+ detectPackageManager,
1629
+ } = await import('./lib/package-manager.js');
1630
+
1631
+ // Verify update is available
1632
+ const updateInfo = await checkForUpdates();
1633
+ if (!updateInfo.available) {
1634
+ return res.status(400).json({ error: 'No update available' });
1635
+ }
1636
+
1637
+ const pm = detectPackageManager();
1638
+ const updateCmd = getUpdateCommand(pm);
1639
+
1640
+ // Get current server port for restart
1641
+ const currentPort = server.address()?.port || 3000;
1642
+
1643
+ // Try to read stored instance options for restart
1644
+ const tmpDir = os.tmpdir();
1645
+ const instanceFilePath = path.join(tmpDir, `openchamber-${currentPort}.json`);
1646
+ let storedOptions = { port: currentPort, daemon: true };
1647
+ try {
1648
+ const content = fs.readFileSync(instanceFilePath, 'utf8');
1649
+ storedOptions = JSON.parse(content);
1650
+ } catch {
1651
+ // Use defaults
1652
+ }
1653
+
1654
+ // Build restart command with stored options
1655
+ let restartCmd = `openchamber serve --port ${storedOptions.port} --daemon`;
1656
+ if (storedOptions.uiPassword) {
1657
+ // Escape password for shell
1658
+ const escapedPw = storedOptions.uiPassword.replace(/'/g, "'\\''");
1659
+ restartCmd += ` --ui-password '${escapedPw}'`;
1660
+ }
1661
+
1662
+ // Respond immediately - update will happen after response
1663
+ res.json({
1664
+ success: true,
1665
+ message: 'Update starting, server will restart shortly',
1666
+ version: updateInfo.version,
1667
+ packageManager: pm,
1668
+ });
1669
+
1670
+ // Give time for response to be sent
1671
+ setTimeout(() => {
1672
+ console.log(`\nInstalling update using ${pm}...`);
1673
+ console.log(`Running: ${updateCmd}`);
1674
+
1675
+ // Create a script that will:
1676
+ // 1. Wait for current process to exit
1677
+ // 2. Run the update
1678
+ // 3. Restart the server with original options
1679
+ const script = `
1680
+ sleep 2
1681
+ ${updateCmd}
1682
+ if [ $? -eq 0 ]; then
1683
+ echo "Update successful, restarting OpenChamber..."
1684
+ ${restartCmd}
1685
+ else
1686
+ echo "Update failed"
1687
+ exit 1
1688
+ fi
1689
+ `;
1690
+
1691
+ // Spawn detached shell to run update after we exit
1692
+ const child = spawnChild('sh', ['-c', script], {
1693
+ detached: true,
1694
+ stdio: 'ignore',
1695
+ env: process.env,
1696
+ });
1697
+ child.unref();
1698
+
1699
+ console.log('Update process spawned, shutting down server...');
1700
+
1701
+ // Give child process time to start, then exit
1702
+ setTimeout(() => {
1703
+ process.exit(0);
1704
+ }, 500);
1705
+ }, 500);
1706
+ } catch (error) {
1707
+ console.error('Failed to install update:', error);
1708
+ res.status(500).json({
1709
+ error: error instanceof Error ? error.message : 'Failed to install update',
1710
+ });
1711
+ }
1712
+ });
1713
+
1608
1714
  app.get('/api/openchamber/models-metadata', async (req, res) => {
1609
1715
  const now = Date.now();
1610
1716
 
@@ -0,0 +1,255 @@
1
+ import { spawnSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const PACKAGE_NAME = '@openchamber/web';
10
+ const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}`;
11
+ const CHANGELOG_URL = 'https://raw.githubusercontent.com/btriapitsyn/openchamber/main/CHANGELOG.md';
12
+
13
+ /**
14
+ * Detect which package manager was used to install this package.
15
+ * Strategy:
16
+ * 1. Check npm_config_user_agent (set during npm/pnpm/yarn/bun install)
17
+ * 2. Check npm_execpath for PM binary path
18
+ * 3. Analyze package location path for PM-specific patterns
19
+ * 4. Fall back to npm
20
+ */
21
+ export function detectPackageManager() {
22
+ // Strategy 1: Check user agent (most reliable during install)
23
+ const userAgent = process.env.npm_config_user_agent || '';
24
+ if (userAgent.startsWith('pnpm')) return 'pnpm';
25
+ if (userAgent.startsWith('yarn')) return 'yarn';
26
+ if (userAgent.startsWith('bun')) return 'bun';
27
+ if (userAgent.startsWith('npm')) return 'npm';
28
+
29
+ // Strategy 2: Check execpath
30
+ const execPath = process.env.npm_execpath || '';
31
+ if (execPath.includes('pnpm')) return 'pnpm';
32
+ if (execPath.includes('yarn')) return 'yarn';
33
+ if (execPath.includes('bun')) return 'bun';
34
+
35
+ // Strategy 3: Analyze package location for PM-specific patterns
36
+ try {
37
+ const pkgPath = path.resolve(__dirname, '..', '..');
38
+ if (pkgPath.includes('.pnpm')) return 'pnpm';
39
+ if (pkgPath.includes('/.yarn/') || pkgPath.includes('\\.yarn\\')) return 'yarn';
40
+ if (pkgPath.includes('/.bun/') || pkgPath.includes('\\.bun\\')) return 'bun';
41
+ } catch {
42
+ // Ignore path resolution errors
43
+ }
44
+
45
+ // Strategy 4: Check which PM binaries are available and preferred
46
+ const pmChecks = [
47
+ { name: 'pnpm', check: () => isCommandAvailable('pnpm') },
48
+ { name: 'yarn', check: () => isCommandAvailable('yarn') },
49
+ { name: 'bun', check: () => isCommandAvailable('bun') },
50
+ ];
51
+
52
+ for (const { name, check } of pmChecks) {
53
+ if (check()) {
54
+ // Verify this PM actually has the package installed globally
55
+ if (isPackageInstalledWith(name)) {
56
+ return name;
57
+ }
58
+ }
59
+ }
60
+
61
+ return 'npm';
62
+ }
63
+
64
+ function isCommandAvailable(command) {
65
+ try {
66
+ const result = spawnSync(command, ['--version'], {
67
+ encoding: 'utf8',
68
+ stdio: ['ignore', 'pipe', 'pipe'],
69
+ timeout: 5000,
70
+ });
71
+ return result.status === 0;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ function isPackageInstalledWith(pm) {
78
+ try {
79
+ let args;
80
+ switch (pm) {
81
+ case 'pnpm':
82
+ args = ['list', '-g', '--depth=0', PACKAGE_NAME];
83
+ break;
84
+ case 'yarn':
85
+ args = ['global', 'list', '--depth=0'];
86
+ break;
87
+ case 'bun':
88
+ args = ['pm', 'ls', '-g'];
89
+ break;
90
+ default:
91
+ args = ['list', '-g', '--depth=0', PACKAGE_NAME];
92
+ }
93
+
94
+ const result = spawnSync(pm, args, {
95
+ encoding: 'utf8',
96
+ stdio: ['ignore', 'pipe', 'pipe'],
97
+ timeout: 10000,
98
+ });
99
+
100
+ if (result.status !== 0) return false;
101
+ return result.stdout.includes(PACKAGE_NAME) || result.stdout.includes('openchamber');
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get the update command for the detected package manager
109
+ */
110
+ export function getUpdateCommand(pm = detectPackageManager()) {
111
+ switch (pm) {
112
+ case 'pnpm':
113
+ return `pnpm add -g ${PACKAGE_NAME}@latest`;
114
+ case 'yarn':
115
+ return `yarn global add ${PACKAGE_NAME}@latest`;
116
+ case 'bun':
117
+ return `bun add -g ${PACKAGE_NAME}@latest`;
118
+ default:
119
+ return `npm install -g ${PACKAGE_NAME}@latest`;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Get current installed version from package.json
125
+ */
126
+ export function getCurrentVersion() {
127
+ try {
128
+ const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
129
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
130
+ return pkg.version || 'unknown';
131
+ } catch {
132
+ return 'unknown';
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Fetch latest version from npm registry
138
+ */
139
+ export async function getLatestVersion() {
140
+ try {
141
+ const response = await fetch(NPM_REGISTRY_URL, {
142
+ headers: { Accept: 'application/json' },
143
+ signal: AbortSignal.timeout(10000),
144
+ });
145
+
146
+ if (!response.ok) {
147
+ throw new Error(`Registry responded with ${response.status}`);
148
+ }
149
+
150
+ const data = await response.json();
151
+ return data['dist-tags']?.latest || null;
152
+ } catch (error) {
153
+ console.warn('Failed to fetch latest version from npm:', error.message);
154
+ return null;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Parse semver version to numeric for comparison
160
+ */
161
+ function parseVersion(version) {
162
+ const parts = version.replace(/^v/, '').split('.').map(Number);
163
+ return (parts[0] || 0) * 10000 + (parts[1] || 0) * 100 + (parts[2] || 0);
164
+ }
165
+
166
+ /**
167
+ * Fetch changelog notes between versions
168
+ */
169
+ export async function fetchChangelogNotes(fromVersion, toVersion) {
170
+ try {
171
+ const response = await fetch(CHANGELOG_URL, {
172
+ signal: AbortSignal.timeout(10000),
173
+ });
174
+
175
+ if (!response.ok) return undefined;
176
+
177
+ const changelog = await response.text();
178
+ const sections = changelog.split(/^## /m).slice(1);
179
+
180
+ const fromNum = parseVersion(fromVersion);
181
+ const toNum = parseVersion(toVersion);
182
+
183
+ const relevantSections = sections.filter((section) => {
184
+ const match = section.match(/^\[(\d+\.\d+\.\d+)\]/);
185
+ if (!match) return false;
186
+ const ver = parseVersion(match[1]);
187
+ return ver > fromNum && ver <= toNum;
188
+ });
189
+
190
+ if (relevantSections.length === 0) return undefined;
191
+
192
+ return relevantSections
193
+ .map((s) => '## ' + s.trim())
194
+ .join('\n\n');
195
+ } catch {
196
+ return undefined;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Check for updates and return update info
202
+ */
203
+ export async function checkForUpdates() {
204
+ const currentVersion = getCurrentVersion();
205
+ const latestVersion = await getLatestVersion();
206
+
207
+ if (!latestVersion || currentVersion === 'unknown') {
208
+ return {
209
+ available: false,
210
+ currentVersion,
211
+ error: 'Unable to determine versions',
212
+ };
213
+ }
214
+
215
+ const currentNum = parseVersion(currentVersion);
216
+ const latestNum = parseVersion(latestVersion);
217
+ const available = latestNum > currentNum;
218
+
219
+ const pm = detectPackageManager();
220
+
221
+ let changelog;
222
+ if (available) {
223
+ changelog = await fetchChangelogNotes(currentVersion, latestVersion);
224
+ }
225
+
226
+ return {
227
+ available,
228
+ version: latestVersion,
229
+ currentVersion,
230
+ body: changelog,
231
+ packageManager: pm,
232
+ // Show our CLI command, not raw package manager command
233
+ updateCommand: 'openchamber update',
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Execute the update (used by CLI)
239
+ */
240
+ export function executeUpdate(pm = detectPackageManager()) {
241
+ const command = getUpdateCommand(pm);
242
+ console.log(`Updating ${PACKAGE_NAME} using ${pm}...`);
243
+ console.log(`Running: ${command}`);
244
+
245
+ const [cmd, ...args] = command.split(' ');
246
+ const result = spawnSync(cmd, args, {
247
+ stdio: 'inherit',
248
+ shell: true,
249
+ });
250
+
251
+ return {
252
+ success: result.status === 0,
253
+ exitCode: result.status,
254
+ };
255
+ }