@vantran-se/openchamber-web 2.0.6 → 2.0.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/bin/lib/DOCUMENTATION.md +1 -0
- package/bin/lib/cli-args.js +3 -0
- package/bin/lib/cli-lifecycle.js +1 -0
- package/bin/lib/cli-process.js +1 -0
- package/bin/lib/cli-startup.js +53 -1
- package/bin/lib/cli-startup.test.js +37 -0
- package/bin/lib/commands-lifecycle.js +29 -13
- package/bin/lib/commands-startup.js +5 -2
- package/bin/lib/commands-startup.test.js +15 -0
- package/bin/lib/commands-update.js +40 -8
- package/bin/lib/commands-update.test.js +53 -6
- package/dist/assets/{CodeMirrorCommentWidgets-BlzB2e0C.js → CodeMirrorCommentWidgets--j227ElB.js} +1 -1
- package/dist/assets/{DiffView-D7JQzQVZ.js → DiffView-IFl6EaUT.js} +1 -1
- package/dist/assets/{FilesView-DC2gadXM.js → FilesView-2QJJIAmr.js} +1 -1
- package/dist/assets/{GitView-Gzl0wbdd.js → GitView-xpLPdSXl.js} +1 -1
- package/dist/assets/{GuestAttachDialog-CQdDIeco.js → GuestAttachDialog-CaRAB_pV.js} +1 -1
- package/dist/assets/{GuestSurfacePane-CrOhlIdt.js → GuestSurfacePane-CA1Pu0hE.js} +1 -1
- package/dist/assets/{JsonTreeView-C3QZifYe.js → JsonTreeView-RFvqdmCZ.js} +1 -1
- package/dist/assets/{LinearIssuesView-X27TCI3v.js → LinearIssuesView-CYI08VW3.js} +1 -1
- package/dist/assets/{MarkdownImageGallery-BlTWU9oH.js → MarkdownImageGallery-D_-yFGOt.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-Cno7QsJO.js → MarkdownRendererImpl-Dpb32U9p.js} +1 -1
- package/dist/assets/{OnboardingScreen-Wr_UzNtt.js → OnboardingScreen-B3B8bi91.js} +1 -1
- package/dist/assets/{PatchDiff-D6pWS7cD.js → PatchDiff-CdvF5h6a.js} +1 -1
- package/dist/assets/{PierreDiffViewer-DDljFbJJ.js → PierreDiffViewer-DLcUtI6Q.js} +1 -1
- package/dist/assets/{PlanView-LcfEbBPi.js → PlanView-Bvt1PdkO.js} +1 -1
- package/dist/assets/{PluginPane-DPWPLc2f.js → PluginPane-CaJz1LlW.js} +1 -1
- package/dist/assets/{SettingsView-CFe9O3EM.js → SettingsView-DL_ucsI6.js} +1 -1
- package/dist/assets/{SettingsWindow-DT1xhvgi.js → SettingsWindow-ilszvARl.js} +1 -1
- package/dist/assets/{SubmoduleDiffSummary-CBHXaIkB.js → SubmoduleDiffSummary-CeV_WX3U.js} +1 -1
- package/dist/assets/{ToolOutputDialog-B_ZX8ZbZ.js → ToolOutputDialog-vc1xXXMa.js} +1 -1
- package/dist/assets/{ToolPartDiffPreview-D_HQzkXU.js → ToolPartDiffPreview-B4EwqT8H.js} +1 -1
- package/dist/assets/{VirtualizedFileDiff-BUK1ujXg.js → VirtualizedFileDiff-DDgQH7-1.js} +1 -1
- package/dist/assets/{WalkthroughView-H-tYOvhO.js → WalkthroughView-Cf8EdtIU.js} +1 -1
- package/dist/assets/{appThemeRegistry-BoJGAwCP.js → appThemeRegistry-D2_fKsMa.js} +1 -1
- package/dist/assets/{commitAndPush-BzZrF8bK.js → commitAndPush-P6RxBC7G.js} +1 -1
- package/dist/assets/{computeVirtualFileMetrics-kYyVjXLu.js → computeVirtualFileMetrics-Bz7G81bS.js} +1 -1
- package/dist/assets/{de-ByfQDcPj.js → de-mIryiQXH.js} +1 -1
- package/dist/assets/{es-DzNbjsQS.js → es-CNRZG0Xh.js} +1 -1
- package/dist/assets/{fr-B8AVLrnX.js → fr-TQ28DG6e.js} +1 -1
- package/dist/assets/{index-0-ON1L7L.js → index-Dl3ruZe_.js} +1 -1
- package/dist/assets/{isDiffPlainText-Bgm_aLpu.js → isDiffPlainText-D2lANTsV.js} +1 -1
- package/dist/assets/{isFilePlainText-D5GMgURH.js → isFilePlainText-Cbv7v9Ro.js} +1 -1
- package/dist/assets/{ja-1g6S0G19.js → ja-eoTHPWE5.js} +1 -1
- package/dist/assets/{ko-CxplvYkJ.js → ko-B6kF-W3S.js} +1 -1
- package/dist/assets/{main-GjXmVNpD.js → main-BCP3yPm1.js} +4 -4
- package/dist/assets/{main--Y98ZLsD.js → main-yjCD5A_Y.js} +2 -2
- package/dist/assets/{markdownCore-CVdUcVCi.js → markdownCore-Bqfm6kwc.js} +1 -1
- package/dist/assets/{miniChat-tTx5mpbd.js → miniChat-fs-MMZEy.js} +2 -2
- package/dist/assets/{mobile-BmbiDWGb.js → mobile-DsTajahd.js} +2 -2
- package/dist/assets/{patchFileDiff-CV4iVQ8L.js → patchFileDiff-Cxi3zs93.js} +1 -1
- package/dist/assets/{pl-f3QyqvIB.js → pl-Cujf3IHL.js} +1 -1
- package/dist/assets/{pt-BR-BsaoaiCK.js → pt-BR-kre15SPf.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-BnCdnDhv.js → renderElectronMiniChatApp-Dpfh7kye.js} +2 -2
- package/dist/assets/{renderMobileApp-CKn8nbSW.js → renderMobileApp-BSo1S4UB.js} +3 -3
- package/dist/assets/{runtimeEndpointReset-sCi88eed.js → runtimeEndpointReset-3DNMw1le.js} +3 -3
- package/dist/assets/{shikiHighlight-O5_xLmWt.js → shikiHighlight-mGfGdiiG.js} +1 -1
- package/dist/assets/{textMateThemeFromAppTheme-XM4VefqN.js → textMateThemeFromAppTheme-SNTAfu1S.js} +1 -1
- package/dist/assets/{tr-BNFsa6nz.js → tr-DMmJYX6x.js} +1 -1
- package/dist/assets/{uk-CJ9y5ULA.js → uk-SB3DoBhl.js} +1 -1
- package/dist/assets/{useAppFontEffects-BN06mgHB.js → useAppFontEffects-DJZbUd4z.js} +10 -10
- package/dist/assets/{useInlineCommentController-Bb9Gjz1Q.js → useInlineCommentController-dDUqIMI2.js} +1 -1
- package/dist/assets/{useRootScrollLock-Chk1cFUw.js → useRootScrollLock-DAPXfqs1.js} +2 -2
- package/dist/assets/{vendor-codemirror-language-data-C2qMZiA7.js → vendor-codemirror-language-data-DeBrOhi1.js} +2 -2
- package/dist/assets/{web-components-BQWjCn_U.js → web-components-5hxxRZKN.js} +1 -1
- package/dist/assets/{zh-CN-U0QjVlAA.js → zh-CN-B-YkU2-B.js} +1 -1
- package/dist/assets/{zh-TW-J9PTJQJi.js → zh-TW-0ddAsFNT.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/mini-chat.html +1 -1
- package/dist/mobile.html +1 -1
- package/package.json +1 -1
package/bin/lib/DOCUMENTATION.md
CHANGED
|
@@ -127,6 +127,7 @@ These modules hold reusable, non-presentational logic for commands.
|
|
|
127
127
|
|
|
128
128
|
- `cli-startup.js`
|
|
129
129
|
- Native startup service detection, install/uninstall/status helpers, and platform-specific startup command execution.
|
|
130
|
+
- Installed startup services remain owned by their platform service manager. `startup start|stop|restart` controls that manager directly. Top-level lifecycle commands route the service's configured port through the same manager, and `update` restarts it after package installation instead of starting a duplicate daemon.
|
|
130
131
|
|
|
131
132
|
- `cli-tunnel-profiles.js`
|
|
132
133
|
- Tunnel profile normalization, token resolution/redaction, profile storage, migration, file-permission warnings, and managed-remote pair persistence.
|
package/bin/lib/cli-args.js
CHANGED
|
@@ -674,6 +674,9 @@ SUBCOMMANDS:
|
|
|
674
674
|
status Show startup integration status
|
|
675
675
|
enable Install and start native user startup integration
|
|
676
676
|
disable Stop and remove native user startup integration
|
|
677
|
+
start Start the installed startup service
|
|
678
|
+
stop Stop the installed startup service
|
|
679
|
+
restart Restart the installed startup service
|
|
677
680
|
|
|
678
681
|
OPTIONS:
|
|
679
682
|
-p, --port Web server port used by startup service
|
package/bin/lib/cli-lifecycle.js
CHANGED
|
@@ -234,6 +234,7 @@ async function discoverRunningInstances(options = {}) {
|
|
|
234
234
|
mtime,
|
|
235
235
|
startedAt,
|
|
236
236
|
launchMode,
|
|
237
|
+
startupService: storedOptions?.startupService === true,
|
|
237
238
|
runtime: liveInfo.runtime,
|
|
238
239
|
source: 'registry+probe',
|
|
239
240
|
host: typeof confirmedHost === 'string' && confirmedHost.length > 0
|
package/bin/lib/cli-process.js
CHANGED
|
@@ -60,6 +60,7 @@ function writeInstanceOptions(instanceFilePath, options, onNotice) {
|
|
|
60
60
|
uiPassword: typeof options.uiPassword === 'string' ? options.uiPassword : undefined,
|
|
61
61
|
hasUiPassword: typeof options.uiPassword === 'string',
|
|
62
62
|
apiOnly: options.apiOnly === true,
|
|
63
|
+
startupService: process.env.OPENCHAMBER_STARTUP_SERVICE === '1',
|
|
63
64
|
startedAt: Number.isFinite(options.startedAt) ? options.startedAt : Date.now(),
|
|
64
65
|
};
|
|
65
66
|
fs.writeFileSync(instanceFilePath, JSON.stringify(toStore, null, 2), { mode: 0o600 });
|
package/bin/lib/cli-startup.js
CHANGED
|
@@ -91,6 +91,7 @@ function collectStartupEnv(options = {}) {
|
|
|
91
91
|
env.OPENCODE_BINARY = opencodeBinary.trim();
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
env.OPENCHAMBER_STARTUP_SERVICE = '1';
|
|
94
95
|
const uiPassword = hasUiPasswordConfigured(options.uiPassword) ? options.uiPassword : undefined;
|
|
95
96
|
if (uiPassword) {
|
|
96
97
|
env.OPENCHAMBER_UI_PASSWORD = uiPassword;
|
|
@@ -329,6 +330,17 @@ function getUserLingerEnabled(user) {
|
|
|
329
330
|
return parseLingerState(result.stdout);
|
|
330
331
|
}
|
|
331
332
|
|
|
333
|
+
function startupServicePort(servicePath) {
|
|
334
|
+
try {
|
|
335
|
+
const content = fs.readFileSync(servicePath, 'utf8');
|
|
336
|
+
const match = content.match(/(?:--port(?:"|"|')?\s+(?:"|"|')?|<string>--port<\/string>\s*<string>)(\d+)/);
|
|
337
|
+
const port = Number.parseInt(match?.[1] || '', 10);
|
|
338
|
+
return Number.isInteger(port) && port > 0 ? port : DEFAULT_PORT;
|
|
339
|
+
} catch {
|
|
340
|
+
return DEFAULT_PORT;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
332
344
|
function getStartupStatus() {
|
|
333
345
|
const paths = getStartupServicePaths();
|
|
334
346
|
if (!paths.servicePath) {
|
|
@@ -336,7 +348,14 @@ function getStartupStatus() {
|
|
|
336
348
|
}
|
|
337
349
|
if (paths.platform === 'windows') {
|
|
338
350
|
const result = runStartupCommand('schtasks.exe', ['/Query', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
|
|
339
|
-
return {
|
|
351
|
+
return {
|
|
352
|
+
supported: true,
|
|
353
|
+
platform: paths.platform,
|
|
354
|
+
enabled: result.status === 0,
|
|
355
|
+
active: null,
|
|
356
|
+
servicePath: paths.servicePath,
|
|
357
|
+
port: startupServicePort(getWindowsStartupWrapperPath()),
|
|
358
|
+
};
|
|
340
359
|
}
|
|
341
360
|
if (paths.platform === 'linux') {
|
|
342
361
|
const enabledResult = runStartupCommand('systemctl', ['--user', 'is-enabled', 'openchamber.service'], { allowFailure: true });
|
|
@@ -350,6 +369,7 @@ function getStartupStatus() {
|
|
|
350
369
|
active: activeState === 'active',
|
|
351
370
|
activeState,
|
|
352
371
|
servicePath: paths.servicePath,
|
|
372
|
+
port: startupServicePort(paths.servicePath),
|
|
353
373
|
lingerEnabled: getUserLingerEnabled(lingerUser),
|
|
354
374
|
lingerUser: lingerUser || null,
|
|
355
375
|
};
|
|
@@ -360,6 +380,7 @@ function getStartupStatus() {
|
|
|
360
380
|
enabled: fs.existsSync(paths.servicePath),
|
|
361
381
|
active: null,
|
|
362
382
|
servicePath: paths.servicePath,
|
|
383
|
+
port: startupServicePort(paths.servicePath),
|
|
363
384
|
};
|
|
364
385
|
}
|
|
365
386
|
|
|
@@ -430,10 +451,41 @@ function disableStartupService() {
|
|
|
430
451
|
return getStartupStatus();
|
|
431
452
|
}
|
|
432
453
|
|
|
454
|
+
function controlStartupService(action) {
|
|
455
|
+
const status = getStartupStatus();
|
|
456
|
+
if (!status.supported || !status.enabled) {
|
|
457
|
+
throw new TunnelCliError('OpenChamber startup service is not installed.', EXIT_CODE.USAGE_ERROR);
|
|
458
|
+
}
|
|
459
|
+
if (!['start', 'stop', 'restart'].includes(action)) {
|
|
460
|
+
throw new TunnelCliError(`Unsupported startup service action: ${action}`, EXIT_CODE.USAGE_ERROR);
|
|
461
|
+
}
|
|
462
|
+
if (status.platform === 'linux') {
|
|
463
|
+
runStartupCommand('systemctl', ['--user', action, 'openchamber.service']);
|
|
464
|
+
} else if (status.platform === 'macos') {
|
|
465
|
+
const domain = `gui/${process.getuid()}`;
|
|
466
|
+
const target = `${domain}/${STARTUP_SERVICE_ID}`;
|
|
467
|
+
if (action === 'stop') {
|
|
468
|
+
runStartupCommand('/bin/launchctl', ['bootout', target], { allowFailure: true });
|
|
469
|
+
} else {
|
|
470
|
+
runStartupCommand('/bin/launchctl', ['bootout', target], { allowFailure: true });
|
|
471
|
+
runStartupCommand('/bin/launchctl', ['bootstrap', domain, status.servicePath]);
|
|
472
|
+
runStartupCommand('/bin/launchctl', ['kickstart', '-k', target]);
|
|
473
|
+
}
|
|
474
|
+
} else if (status.platform === 'windows') {
|
|
475
|
+
if (action !== 'start') runStartupCommand('schtasks.exe', ['/End', '/TN', STARTUP_SERVICE_ID], { allowFailure: true });
|
|
476
|
+
if (action !== 'stop') runStartupCommand('schtasks.exe', ['/Run', '/TN', STARTUP_SERVICE_ID]);
|
|
477
|
+
}
|
|
478
|
+
return getStartupStatus();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const restartStartupService = () => controlStartupService('restart');
|
|
433
482
|
|
|
434
483
|
export {
|
|
435
484
|
getStartupStatus,
|
|
485
|
+
startupServicePort,
|
|
436
486
|
enableStartupService,
|
|
437
487
|
disableStartupService,
|
|
488
|
+
controlStartupService,
|
|
489
|
+
restartStartupService,
|
|
438
490
|
buildWindowsStartupTaskCommand,
|
|
439
491
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { startupServicePort } from './cli-startup.js';
|
|
7
|
+
|
|
8
|
+
function withServiceFile(content, run) {
|
|
9
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-startup-test-'));
|
|
10
|
+
const servicePath = path.join(dir, 'service');
|
|
11
|
+
fs.writeFileSync(servicePath, content);
|
|
12
|
+
try {
|
|
13
|
+
return run(servicePath);
|
|
14
|
+
} finally {
|
|
15
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('startup service metadata', () => {
|
|
20
|
+
it('reads the configured port from a systemd unit', () => {
|
|
21
|
+
withServiceFile('ExecStart="/node" "/openchamber" "serve" "--foreground" "--port" "3069"\n', (servicePath) => {
|
|
22
|
+
expect(startupServicePort(servicePath)).toBe(3069);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('reads the configured port from a launchd plist', () => {
|
|
27
|
+
withServiceFile('<string>--port</string>\n<string>3070</string>\n', (servicePath) => {
|
|
28
|
+
expect(startupServicePort(servicePath)).toBe(3070);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('reads the configured port from the Windows startup wrapper', () => {
|
|
33
|
+
withServiceFile("& 'openchamber' 'serve' '--foreground' '--port' '3071'\n", (servicePath) => {
|
|
34
|
+
expect(startupServicePort(servicePath)).toBe(3071);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EXIT_CODE, TunnelCliError } from './cli-errors.js';
|
|
2
2
|
import { requestServerShutdown } from './cli-http.js';
|
|
3
3
|
import { isPortAvailable } from './cli-ports.js';
|
|
4
|
+
import { controlStartupService, getStartupStatus } from './cli-startup.js';
|
|
4
5
|
import {
|
|
5
6
|
discoverLifecycleInstances,
|
|
6
7
|
discoverUnconfirmedRegistryInstanceOnPort,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
} from '../cli-output.js';
|
|
25
26
|
|
|
26
27
|
async function stopCommand(options) {
|
|
28
|
+
const startupStatus = getStartupStatus();
|
|
27
29
|
const showOutput = shouldRenderHumanOutput(options);
|
|
28
30
|
const suppressQuietOutput = options?.suppressQuietOutput === true;
|
|
29
31
|
const jsonResults = [];
|
|
@@ -75,6 +77,17 @@ async function stopCommand(options) {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
const explicitInstance = runningInstances[0];
|
|
80
|
+
if (startupStatus.enabled && startupStatus.port === explicitInstance.port && explicitInstance.startupService === true) {
|
|
81
|
+
controlStartupService('stop');
|
|
82
|
+
jsonResults.push({ port: explicitInstance.port, stopped: true, manager: startupStatus.platform });
|
|
83
|
+
if (isJsonMode(options)) printJson({ stoppedCount: 1, results: jsonResults });
|
|
84
|
+
else if (showOutput) {
|
|
85
|
+
logStatus('success', `stopped startup service on port ${explicitInstance.port}`);
|
|
86
|
+
finish('stop complete');
|
|
87
|
+
}
|
|
88
|
+
printQuietStopResults();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
78
91
|
if (explicitInstance.runtime === 'desktop') {
|
|
79
92
|
jsonResults.push({ port: options.port, runtime: 'desktop', stopped: false, reason: 'desktop-managed' });
|
|
80
93
|
if (isJsonMode(options)) {
|
|
@@ -210,6 +223,11 @@ async function stopCommand(options) {
|
|
|
210
223
|
}
|
|
211
224
|
|
|
212
225
|
for (const instance of runningInstances) {
|
|
226
|
+
if (startupStatus.enabled && startupStatus.port === instance.port && instance.startupService === true) {
|
|
227
|
+
controlStartupService('stop');
|
|
228
|
+
jsonResults.push({ port: instance.port, stopped: true, manager: startupStatus.platform });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
213
231
|
const stopSpin = showOutput ? createSpinner(options) : null;
|
|
214
232
|
if (showOutput && !stopSpin) {
|
|
215
233
|
logStatus('info', `stopping port ${instance.port} (PID: ${instance.pid})`);
|
|
@@ -287,7 +305,13 @@ async function restartCommand(options, serveCommand) {
|
|
|
287
305
|
return;
|
|
288
306
|
}
|
|
289
307
|
|
|
308
|
+
const startupStatus = getStartupStatus();
|
|
290
309
|
for (const instance of runningInstances) {
|
|
310
|
+
if (startupStatus.enabled && startupStatus.port === instance.port && instance.startupService === true) {
|
|
311
|
+
controlStartupService('restart');
|
|
312
|
+
restarted.push({ fromPort: instance.port, toPort: instance.port, manager: startupStatus.platform });
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
291
315
|
if (instance.runtime === 'desktop') {
|
|
292
316
|
const message = `Port ${instance.port} is managed by OpenChamber Desktop and cannot be restarted with this command.`;
|
|
293
317
|
if (isJsonMode(options)) {
|
|
@@ -316,6 +340,11 @@ async function restartCommand(options, serveCommand) {
|
|
|
316
340
|
const isForeground = launchMode === 'foreground';
|
|
317
341
|
|
|
318
342
|
const restartPort = options.explicitPort ? options.port : instance.port;
|
|
343
|
+
if (isForeground) {
|
|
344
|
+
restarted.push({ fromPort: instance.port, toPort: restartPort, launchMode, ok: false, reason: 'manual-foreground' });
|
|
345
|
+
if (showOutput) logStatus('warning', `foreground OpenChamber on port ${instance.port} requires its owning terminal or process manager to restart it`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
319
348
|
|
|
320
349
|
const restartSpin = showOutput ? createSpinner(options) : null;
|
|
321
350
|
if (showOutput && !restartSpin) {
|
|
@@ -331,19 +360,6 @@ async function restartCommand(options, serveCommand) {
|
|
|
331
360
|
suppressQuietOutput: true,
|
|
332
361
|
});
|
|
333
362
|
|
|
334
|
-
// Foreground instances are managed by a process manager (systemd,
|
|
335
|
-
// Docker, etc.) that will restart them automatically after stop.
|
|
336
|
-
// Do not call serve() here — just record the stop as a successful
|
|
337
|
-
// restart and let the process manager handle the actual restart.
|
|
338
|
-
if (isForeground) {
|
|
339
|
-
restarted.push({ fromPort: instance.port, toPort: restartPort, launchMode, ok: true });
|
|
340
|
-
restartSpin?.stop(`Stopped foreground instance on port ${instance.port} (process manager will restart)`);
|
|
341
|
-
if (showOutput && !restartSpin) {
|
|
342
|
-
logStatus('success', `port ${instance.port} stopped`, 'process manager will restart');
|
|
343
|
-
}
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
363
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
348
364
|
|
|
349
365
|
const restartedPort = await runServe({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EXIT_CODE, TunnelCliError } from './cli-errors.js';
|
|
2
|
-
import { getStartupStatus, enableStartupService, disableStartupService } from './cli-startup.js';
|
|
2
|
+
import { getStartupStatus, enableStartupService, disableStartupService, controlStartupService } from './cli-startup.js';
|
|
3
3
|
import {
|
|
4
4
|
intro as clackIntro,
|
|
5
5
|
outro as clackOutro,
|
|
@@ -13,8 +13,9 @@ async function startupCommand(options, action = 'status', dependencies = {}) {
|
|
|
13
13
|
const getStatus = dependencies.getStartupStatus || getStartupStatus;
|
|
14
14
|
const enableService = dependencies.enableStartupService || enableStartupService;
|
|
15
15
|
const disableService = dependencies.disableStartupService || disableStartupService;
|
|
16
|
+
const controlService = dependencies.controlStartupService || controlStartupService;
|
|
16
17
|
const normalized = typeof action === 'string' ? action.trim().toLowerCase() : 'status';
|
|
17
|
-
if (!['status', 'enable', 'disable'].includes(normalized)) {
|
|
18
|
+
if (!['status', 'enable', 'disable', 'start', 'stop', 'restart'].includes(normalized)) {
|
|
18
19
|
throw new TunnelCliError(
|
|
19
20
|
`Unknown startup subcommand '${action}'. Use 'openchamber startup --help'.`,
|
|
20
21
|
EXIT_CODE.USAGE_ERROR
|
|
@@ -26,6 +27,8 @@ async function startupCommand(options, action = 'status', dependencies = {}) {
|
|
|
26
27
|
status = enableService(options);
|
|
27
28
|
} else if (normalized === 'disable') {
|
|
28
29
|
status = disableService();
|
|
30
|
+
} else if (normalized === 'start' || normalized === 'stop' || normalized === 'restart') {
|
|
31
|
+
status = controlService(normalized);
|
|
29
32
|
} else {
|
|
30
33
|
status = getStatus();
|
|
31
34
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { startupCommand } from './commands-startup.js';
|
|
4
|
+
|
|
5
|
+
const quiet = { quiet: true };
|
|
6
|
+
|
|
7
|
+
describe('startup command service controls', () => {
|
|
8
|
+
for (const action of ['start', 'stop', 'restart']) {
|
|
9
|
+
it(`${action}s the installed startup service without changing installation`, async () => {
|
|
10
|
+
const controlStartupService = vi.fn(() => ({ supported: true, enabled: true, active: action !== 'stop', platform: 'linux' }));
|
|
11
|
+
await startupCommand(quiet, action, { controlStartupService });
|
|
12
|
+
expect(controlStartupService).toHaveBeenCalledWith(action);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { requestServerShutdown } from './cli-http.js';
|
|
2
|
-
import { discoverRunningInstances } from './cli-lifecycle.js';
|
|
2
|
+
import { discoverRunningInstances as discoverRunningInstancesDefault } from './cli-lifecycle.js';
|
|
3
|
+
import { getStartupStatus as getStartupStatusDefault, restartStartupService as restartStartupServiceDefault } from './cli-startup.js';
|
|
3
4
|
import {
|
|
4
5
|
readInstanceOptions,
|
|
5
6
|
removePidFile,
|
|
@@ -16,7 +17,27 @@ import {
|
|
|
16
17
|
logStatus,
|
|
17
18
|
} from '../cli-output.js';
|
|
18
19
|
|
|
19
|
-
function
|
|
20
|
+
export function partitionUpdateInstances(instances, readOptions = readInstanceOptions, startupStatus = null) {
|
|
21
|
+
const managed = [];
|
|
22
|
+
const startup = [];
|
|
23
|
+
const foreground = [];
|
|
24
|
+
for (const instance of instances) {
|
|
25
|
+
const options = readOptions(instance.instanceFilePath) || {};
|
|
26
|
+
if (options.launchMode !== 'foreground') managed.push(instance);
|
|
27
|
+
else if (startupStatus?.enabled && startupStatus.port === instance.port && instance.startupService === true) startup.push(instance);
|
|
28
|
+
else foreground.push(instance);
|
|
29
|
+
}
|
|
30
|
+
return { managed, startup, foreground };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createUpdateCommand({
|
|
34
|
+
importFromFilePath,
|
|
35
|
+
packageManagerPath,
|
|
36
|
+
serveCommand,
|
|
37
|
+
discoverRunningInstances = discoverRunningInstancesDefault,
|
|
38
|
+
getStartupStatus = getStartupStatusDefault,
|
|
39
|
+
restartStartupService = restartStartupServiceDefault,
|
|
40
|
+
}) {
|
|
20
41
|
return async function updateCommand(options = {}) {
|
|
21
42
|
const showOutput = shouldRenderHumanOutput(options);
|
|
22
43
|
const updateSpin = createSpinner(options);
|
|
@@ -29,6 +50,12 @@ function createUpdateCommand({ importFromFilePath, packageManagerPath, serveComm
|
|
|
29
50
|
} = await importFromFilePath(packageManagerPath);
|
|
30
51
|
|
|
31
52
|
const runningInstances = await discoverRunningInstances();
|
|
53
|
+
const startupStatus = getStartupStatus();
|
|
54
|
+
const { managed: managedInstances, startup: startupInstances } = partitionUpdateInstances(
|
|
55
|
+
runningInstances,
|
|
56
|
+
readInstanceOptions,
|
|
57
|
+
startupStatus,
|
|
58
|
+
);
|
|
32
59
|
const currentVersion = getCurrentVersion();
|
|
33
60
|
|
|
34
61
|
if (showOutput) {
|
|
@@ -75,9 +102,9 @@ function createUpdateCommand({ importFromFilePath, packageManagerPath, serveComm
|
|
|
75
102
|
}
|
|
76
103
|
updateSpin?.message(`Updating to ${updateInfo.version || 'latest'}...`);
|
|
77
104
|
|
|
78
|
-
if (
|
|
79
|
-
updateSpin?.message(`Stopping ${
|
|
80
|
-
for (const instance of
|
|
105
|
+
if (managedInstances.length > 0) {
|
|
106
|
+
updateSpin?.message(`Stopping ${managedInstances.length} running instance(s)...`);
|
|
107
|
+
for (const instance of managedInstances) {
|
|
81
108
|
try {
|
|
82
109
|
const requested = await requestServerShutdown(instance.port, instance.host);
|
|
83
110
|
await stopInstanceProcess(instance.pid, {
|
|
@@ -101,9 +128,14 @@ function createUpdateCommand({ importFromFilePath, packageManagerPath, serveComm
|
|
|
101
128
|
throw new Error(`Update failed with exit code ${result.exitCode}`);
|
|
102
129
|
}
|
|
103
130
|
|
|
104
|
-
if (
|
|
105
|
-
updateSpin?.message(
|
|
106
|
-
|
|
131
|
+
if (startupInstances.length > 0) {
|
|
132
|
+
updateSpin?.message('Restarting startup service...');
|
|
133
|
+
restartStartupService();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (managedInstances.length > 0) {
|
|
137
|
+
updateSpin?.message(`Restarting ${managedInstances.length} instance(s)...`);
|
|
138
|
+
for (const instance of managedInstances) {
|
|
107
139
|
const storedOptions = readInstanceOptions(instance.instanceFilePath) || { port: instance.port };
|
|
108
140
|
await serveCommand({
|
|
109
141
|
port: storedOptions.port || instance.port,
|
|
@@ -3,7 +3,7 @@ import os from 'os';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
6
|
-
import { createUpdateCommand } from './commands-update.js';
|
|
6
|
+
import { createUpdateCommand, partitionUpdateInstances } from './commands-update.js';
|
|
7
7
|
|
|
8
8
|
async function withTempOpenChamberDataDir(fn) {
|
|
9
9
|
const previous = process.env.OPENCHAMBER_DATA_DIR;
|
|
@@ -12,16 +12,63 @@ async function withTempOpenChamberDataDir(fn) {
|
|
|
12
12
|
try {
|
|
13
13
|
return await fn(dir);
|
|
14
14
|
} finally {
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
} else {
|
|
18
|
-
delete process.env.OPENCHAMBER_DATA_DIR;
|
|
19
|
-
}
|
|
15
|
+
if (previous === undefined) delete process.env.OPENCHAMBER_DATA_DIR;
|
|
16
|
+
else process.env.OPENCHAMBER_DATA_DIR = previous;
|
|
20
17
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
21
18
|
}
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
describe('update command', () => {
|
|
22
|
+
it('separates CLI daemons from the installed startup service', () => {
|
|
23
|
+
const daemon = { port: 3000, instanceFilePath: '/daemon.json' };
|
|
24
|
+
const startup = { port: 3069, instanceFilePath: '/startup.json', startupService: true };
|
|
25
|
+
const manualForeground = { port: 3070, instanceFilePath: '/manual.json' };
|
|
26
|
+
const readOptions = vi.fn((filePath) => ({
|
|
27
|
+
launchMode: filePath === daemon.instanceFilePath ? 'daemon' : 'foreground',
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
expect(partitionUpdateInstances(
|
|
31
|
+
[daemon, startup, manualForeground],
|
|
32
|
+
readOptions,
|
|
33
|
+
{ enabled: true, active: true, port: 3069 },
|
|
34
|
+
)).toEqual({
|
|
35
|
+
managed: [daemon],
|
|
36
|
+
startup: [startup],
|
|
37
|
+
foreground: [manualForeground],
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('restarts the installed startup service after updating', async () => {
|
|
42
|
+
await withTempOpenChamberDataDir(async (dir) => {
|
|
43
|
+
const originalWrite = process.stdout.write;
|
|
44
|
+
process.stdout.write = vi.fn(() => true);
|
|
45
|
+
const instanceFilePath = path.join(dir, 'run', 'openchamber-3069.json');
|
|
46
|
+
fs.mkdirSync(path.dirname(instanceFilePath), { recursive: true });
|
|
47
|
+
fs.writeFileSync(instanceFilePath, JSON.stringify({ port: 3069, launchMode: 'foreground' }));
|
|
48
|
+
const restartStartupService = vi.fn();
|
|
49
|
+
const updateCommand = createUpdateCommand({
|
|
50
|
+
packageManagerPath: '/fake/package-manager.js',
|
|
51
|
+
serveCommand: vi.fn(),
|
|
52
|
+
discoverRunningInstances: vi.fn(async () => [{ port: 3069, instanceFilePath, startupService: true }]),
|
|
53
|
+
getStartupStatus: vi.fn(() => ({ supported: true, enabled: true, active: true, port: 3069 })),
|
|
54
|
+
restartStartupService,
|
|
55
|
+
importFromFilePath: vi.fn(async () => ({
|
|
56
|
+
checkForUpdates: vi.fn(async () => ({ available: true, version: '9.9.9' })),
|
|
57
|
+
detectPackageManager: vi.fn(() => 'npm'),
|
|
58
|
+
executeUpdate: vi.fn(() => ({ success: true, exitCode: 0 })),
|
|
59
|
+
getCurrentVersion: vi.fn(() => '1.0.0'),
|
|
60
|
+
})),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await updateCommand({ json: true });
|
|
65
|
+
expect(restartStartupService).toHaveBeenCalledTimes(1);
|
|
66
|
+
} finally {
|
|
67
|
+
process.stdout.write = originalWrite;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
25
72
|
it('uses the package-manager helpers on the update-available path', async () => {
|
|
26
73
|
await withTempOpenChamberDataDir(async () => {
|
|
27
74
|
const originalWrite = process.stdout.write;
|
package/dist/assets/{CodeMirrorCommentWidgets-BlzB2e0C.js → CodeMirrorCommentWidgets--j227ElB.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as a}from"./vendor-react-BUCujlKS.js";import{I as l,a as I}from"./useInlineCommentController-
|
|
1
|
+
import{j as a}from"./vendor-react-BUCujlKS.js";import{I as l,a as I}from"./useInlineCommentController-dDUqIMI2.js";function E(g){const{drafts:x,editingDraftId:d,commentText:o,onTextChange:r,selection:n,isDragging:u,fileLabel:s,newWidgetId:f,mapDraftToRange:p,onSave:m,onCancel:c,onEdit:C,onDelete:h}=g,t=[];for(const e of x){const i=p(e);if(e.id===d){t.push({afterLine:i.end,id:`edit-${e.id}`,content:a.jsx(l,{initialText:o,onTextChange:r,fileLabel:s,lineRange:i,isEditing:!0,onSave:m,onCancel:c},`edit-${e.id}`)});continue}t.push({afterLine:i.end,id:`card-${e.id}`,content:a.jsx(I,{draft:e,onEdit:()=>C(e),onDelete:()=>h(e)},`card-${e.id}`)})}if(n&&!d&&!u){const e={...n,start:Math.min(n.start,n.end),end:Math.max(n.start,n.end)};t.push({afterLine:e.end,id:f,content:a.jsx(l,{initialText:o,onTextChange:r,fileLabel:s,lineRange:e,isEditing:!1,onSave:m,onCancel:c},f)})}return t}export{E as b};
|