@openchamber/web 1.11.6 → 1.11.7

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 (47) hide show
  1. package/README.md +6 -0
  2. package/bin/cli.js +443 -2
  3. package/dist/assets/{MarkdownRendererImpl-COdbjw73.js → MarkdownRendererImpl-DaF15QNC.js} +3 -3
  4. package/dist/assets/{MultiRunWindow-BKSHxjMq.js → MultiRunWindow-Cl7wS_CB.js} +1 -1
  5. package/dist/assets/{OnboardingScreen-Chjg337p.js → OnboardingScreen-DTv6YJI1.js} +2 -2
  6. package/dist/assets/{SettingsWindow-C0lRRW8M.js → SettingsWindow-_c3TTL2z.js} +1 -1
  7. package/dist/assets/{TerminalView-Bvil3j1u.js → TerminalView-CuXkDROt.js} +3 -3
  8. package/dist/assets/es-CYoUf2D-.js +15 -0
  9. package/dist/assets/{index-B9LvUHdG.js → index-3WXrN3AX.js} +1 -1
  10. package/dist/assets/index-BREIbhcb.css +1 -0
  11. package/dist/assets/ko-2tM0fIna.js +15 -0
  12. package/dist/assets/main-BF3kWAJ9.js +239 -0
  13. package/dist/assets/{main-Blhx9Fp5.js → main-o8ZERrmU.js} +2 -2
  14. package/dist/assets/miniChat-BZQjpK23.js +2 -0
  15. package/dist/assets/{modelPrefsAutoSave-DRJSYigo.js → modelPrefsAutoSave-wwnbqBk7.js} +110 -108
  16. package/dist/assets/pl-Dq8uAotM.js +15 -0
  17. package/dist/assets/pt-BR-nh9s9DFT.js +15 -0
  18. package/dist/assets/{renderElectronMiniChatApp-BxZRI73j.js → renderElectronMiniChatApp-C-Ezew9P.js} +2 -2
  19. package/dist/assets/uk-BZtz0wUV.js +15 -0
  20. package/dist/assets/{vendor-.bun-Bum-iBXX.js → vendor-.bun-CV3tusA8.js} +1 -1
  21. package/dist/assets/zh-CN-j_nYMchE.js +15 -0
  22. package/dist/assets/zh-TW-B11UpkDJ.js +15 -0
  23. package/dist/index.html +11 -28
  24. package/dist/mini-chat.html +4 -4
  25. package/package.json +1 -1
  26. package/server/lib/fs/routes.js +5 -0
  27. package/server/lib/fs/routes.test.js +61 -1
  28. package/server/lib/git/DOCUMENTATION.md +1 -0
  29. package/server/lib/git/routes.js +82 -1
  30. package/server/lib/git/service.js +338 -19
  31. package/server/lib/git/service.test.js +414 -8
  32. package/server/lib/opencode/env-runtime.js +52 -4
  33. package/server/lib/opencode/env-runtime.test.js +82 -6
  34. package/server/lib/opencode/openchamber-routes.js +9 -7
  35. package/server/lib/opencode/settings-helpers.js +3 -0
  36. package/server/lib/opencode/settings-runtime.js +39 -1
  37. package/server/lib/opencode/settings-runtime.test.js +39 -0
  38. package/server/lib/skills-catalog/source.js +1 -1
  39. package/dist/assets/es-BZIAUghG.js +0 -15
  40. package/dist/assets/index-UcCH2KN9.css +0 -1
  41. package/dist/assets/ko-DU9l-zox.js +0 -15
  42. package/dist/assets/main-d2-dY4er.js +0 -232
  43. package/dist/assets/miniChat-CJ7-rZFl.js +0 -2
  44. package/dist/assets/pl-CdqzokG-.js +0 -15
  45. package/dist/assets/pt-BR-Bknbr_Y3.js +0 -15
  46. package/dist/assets/uk-Be4E8ZNO.js +0 -15
  47. package/dist/assets/zh-CN-qpPiaZMg.js +0 -15
@@ -1,7 +1,6 @@
1
1
  export const registerOpenChamberRoutes = (app, dependencies) => {
2
2
  const {
3
3
  fs,
4
- os,
5
4
  path,
6
5
  process,
7
6
  server,
@@ -104,14 +103,15 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
104
103
  }
105
104
 
106
105
  const currentPort = server.address()?.port || 3000;
107
- const tmpDir = os.tmpdir();
108
- const instanceFilePath = path.join(tmpDir, `openchamber-${currentPort}.json`);
106
+ const instanceFilePath = path.join(openchamberDataDir, 'run', `openchamber-${currentPort}.json`);
109
107
  let storedOptions = { port: currentPort, daemon: true };
110
108
  try {
111
109
  const content = await fs.promises.readFile(instanceFilePath, 'utf8');
112
110
  storedOptions = JSON.parse(content);
113
111
  } catch {
114
112
  }
113
+ const launchMode = storedOptions.launchMode === 'foreground' ? 'foreground' : 'daemon';
114
+ const isForegroundService = launchMode === 'foreground';
115
115
 
116
116
  const isWindows = process.platform === 'win32';
117
117
  const quotePosix = (value) => `'${String(value).replace(/'/g, "'\\''")}'`;
@@ -152,7 +152,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
152
152
  restartCmdFallback += ` --ui-password '${escapedPw}'`;
153
153
  }
154
154
  }
155
- const restartCmd = `(${restartCmdPrimary}) || (${restartCmdFallback})`;
155
+ const restartCmd = isForegroundService ? '' : `(${restartCmdPrimary}) || (${restartCmdFallback})`;
156
156
  const updateLogPath = path.join(openchamberDataDir, 'update-install.log');
157
157
  const logPreamble = [
158
158
  '',
@@ -165,8 +165,9 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
165
165
  `packagePath=${pmDetails.packagePath || 'unknown'}`,
166
166
  `globalNodeModulesRoot=${pmDetails.globalNodeModulesRoot || 'unknown'}`,
167
167
  `mode=${isContainer ? 'container' : 'restart'}`,
168
+ `launchMode=${launchMode}`,
168
169
  `updateCommand=${updateCmd}`,
169
- `restartCommand=${restartCmd}`,
170
+ `restartCommand=${restartCmd || 'service-manager'}`,
170
171
  `logPath=${updateLogPath}`,
171
172
  ].join('\n');
172
173
 
@@ -176,6 +177,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
176
177
  version: updateInfo.version,
177
178
  packageManager: pm,
178
179
  autoRestart: true,
180
+ restartManager: isForegroundService ? 'service' : 'cli',
179
181
  });
180
182
 
181
183
  setTimeout(() => {
@@ -192,7 +194,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
192
194
  ${updateCmd}
193
195
  if %ERRORLEVEL% EQU 0 (
194
196
  echo Update successful, restarting OpenChamber...
195
- ${restartCmd}
197
+ ${restartCmd || 'echo Service manager will restart OpenChamber.'}
196
198
  ) else (
197
199
  echo Update failed
198
200
  exit /b 1
@@ -204,7 +206,7 @@ export const registerOpenChamberRoutes = (app, dependencies) => {
204
206
  ${updateCmd}
205
207
  if [ $? -eq 0 ]; then
206
208
  echo "Update successful, restarting OpenChamber..."
207
- ${restartCmd}
209
+ ${restartCmd || 'echo "Service manager will restart OpenChamber."'}
208
210
  else
209
211
  echo "Update failed"
210
212
  exit 1
@@ -225,6 +225,9 @@ export const createSettingsHelpers = (dependencies) => {
225
225
  if (candidate.usageDisplayMode === 'usage' || candidate.usageDisplayMode === 'remaining') {
226
226
  result.usageDisplayMode = candidate.usageDisplayMode;
227
227
  }
228
+ if (typeof candidate.usageShowPredValues === 'boolean') {
229
+ result.usageShowPredValues = candidate.usageShowPredValues;
230
+ }
228
231
  if (Array.isArray(candidate.usageDropdownProviders)) {
229
232
  result.usageDropdownProviders = normalizeStringArray(candidate.usageDropdownProviders);
230
233
  }
@@ -438,6 +438,44 @@ export const createSettingsRuntime = (deps) => {
438
438
  }
439
439
  };
440
440
 
441
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
442
+
443
+ const isTransientWindowsReplaceError = (error) => {
444
+ if (process.platform !== 'win32' || !error || typeof error !== 'object') {
445
+ return false;
446
+ }
447
+ return error.code === 'EPERM' || error.code === 'EACCES' || error.code === 'EBUSY';
448
+ };
449
+
450
+ const replaceFile = async (tmp, target) => {
451
+ const maxAttempts = process.platform === 'win32' ? 6 : 1;
452
+ let lastError = null;
453
+
454
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
455
+ try {
456
+ await fsPromises.rename(tmp, target);
457
+ return;
458
+ } catch (error) {
459
+ lastError = error;
460
+ if (!isTransientWindowsReplaceError(error) || attempt === maxAttempts) {
461
+ break;
462
+ }
463
+ await sleep(25 * attempt);
464
+ }
465
+ }
466
+
467
+ if (!isTransientWindowsReplaceError(lastError)) {
468
+ throw lastError;
469
+ }
470
+
471
+ // Windows can transiently reject atomic replacement when another process
472
+ // briefly opens the target file. Preserve atomic rename everywhere it works,
473
+ // but fall back to a direct replacement so settings persistence does not
474
+ // get permanently wedged on Windows desktop installs.
475
+ await fsPromises.copyFile(tmp, target);
476
+ await fsPromises.rm(tmp, { force: true });
477
+ };
478
+
441
479
  const writeSettingsToDisk = async (settings) => {
442
480
  try {
443
481
  await fsPromises.mkdir(path.dirname(SETTINGS_FILE_PATH), { recursive: true });
@@ -447,7 +485,7 @@ export const createSettingsRuntime = (deps) => {
447
485
  // read-modify-write wipe the settings file.
448
486
  const tmp = `${SETTINGS_FILE_PATH}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
449
487
  await fsPromises.writeFile(tmp, JSON.stringify(settings, null, 2), 'utf8');
450
- await fsPromises.rename(tmp, SETTINGS_FILE_PATH);
488
+ await replaceFile(tmp, SETTINGS_FILE_PATH);
451
489
  } catch (error) {
452
490
  console.warn('Failed to write settings file:', error);
453
491
  throw error;
@@ -82,4 +82,43 @@ describe('settings runtime', () => {
82
82
  await cleanup();
83
83
  }
84
84
  });
85
+
86
+ it.skipIf(process.platform !== 'win32')('falls back when Windows blocks atomic settings replacement', async () => {
87
+ const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-settings-runtime-'));
88
+ const settingsFilePath = path.join(tempRoot, 'settings.json');
89
+ const wrappedFs = {
90
+ ...fsPromises,
91
+ rename: async () => {
92
+ const error = new Error('operation not permitted');
93
+ error.code = 'EPERM';
94
+ throw error;
95
+ },
96
+ };
97
+ const runtime = createSettingsRuntime({
98
+ fsPromises: wrappedFs,
99
+ path,
100
+ crypto,
101
+ SETTINGS_FILE_PATH: settingsFilePath,
102
+ sanitizeProjects: (projects) => Array.isArray(projects) ? projects : [],
103
+ sanitizeSettingsUpdate: (settings) => settings,
104
+ mergePersistedSettings: (_current, changes) => changes,
105
+ normalizeSettingsPaths: (settings) => ({ settings, changed: false }),
106
+ normalizeStringArray: (values) => Array.isArray(values) ? values.filter((value) => typeof value === 'string') : [],
107
+ formatSettingsResponse: (settings) => settings,
108
+ resolveDirectoryCandidate: (value) => value,
109
+ normalizeManagedRemoteTunnelHostname: (value) => value,
110
+ normalizeManagedRemoteTunnelPresets: (value) => value,
111
+ normalizeManagedRemoteTunnelPresetTokens: (value) => value,
112
+ syncManagedRemoteTunnelConfigWithPresets: async () => {},
113
+ upsertManagedRemoteTunnelToken: async () => {},
114
+ });
115
+
116
+ try {
117
+ await runtime.writeSettingsToDisk({ theme: 'dark' });
118
+
119
+ await expect(fsPromises.readFile(settingsFilePath, 'utf8')).resolves.toBe(JSON.stringify({ theme: 'dark' }, null, 2));
120
+ } finally {
121
+ await fsPromises.rm(tempRoot, { recursive: true, force: true });
122
+ }
123
+ });
85
124
  });
@@ -17,7 +17,7 @@ export function parseSkillRepoSource(input, options = {}) {
17
17
  return { ok: false, error: { kind: 'invalidSource', message: 'Repository source is required' } };
18
18
  }
19
19
  const explicitSubpath = typeof options.subpath === 'string' && options.subpath.trim() ? options.subpath.trim() : null;
20
-
20
+
21
21
  const urlFormat = raw.startsWith('https://') ? 'https' : raw.startsWith('git@') ? 'ssh' : 'shorthand';
22
22
  const gitHost = urlFormat === 'https' ? raw.split('/')[2] : urlFormat === 'ssh' ? raw.split('@')[1].split(':')[0] : null;
23
23