oysterun 0.1.1-beta.2 → 0.1.1-beta.4
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/dev/client/web/index.html +207 -0
- package/docs/routec/host_setup.md +52 -4
- package/docs/routec/npmjs_publish.md +32 -0
- package/host-service/package.json +1 -1
- package/host-service/server.mjs +178 -0
- package/host-service/setup.mjs +111 -10
- package/package.json +1 -1
|
@@ -7587,6 +7587,62 @@
|
|
|
7587
7587
|
</div>
|
|
7588
7588
|
</div>
|
|
7589
7589
|
</section>
|
|
7590
|
+
<section class="table-panel">
|
|
7591
|
+
<div class="table-title">
|
|
7592
|
+
<div class="table-title-main">
|
|
7593
|
+
<strong>Oysterun Update</strong>
|
|
7594
|
+
<div class="table-title-copy">
|
|
7595
|
+
Check and update the installed Host package.
|
|
7596
|
+
</div>
|
|
7597
|
+
</div>
|
|
7598
|
+
</div>
|
|
7599
|
+
<div class="kv-table">
|
|
7600
|
+
<div class="kv-label">Current Version</div>
|
|
7601
|
+
<div class="kv-value">
|
|
7602
|
+
<div class="mono-path" id="host-update-current">
|
|
7603
|
+
--
|
|
7604
|
+
</div>
|
|
7605
|
+
</div>
|
|
7606
|
+
<div class="kv-label">Channel</div>
|
|
7607
|
+
<div class="kv-value">
|
|
7608
|
+
<div class="mono-path" id="host-update-channel">
|
|
7609
|
+
--
|
|
7610
|
+
</div>
|
|
7611
|
+
</div>
|
|
7612
|
+
<div class="kv-label">Latest Version</div>
|
|
7613
|
+
<div class="kv-value">
|
|
7614
|
+
<div class="value-stack">
|
|
7615
|
+
<div class="mono-path" id="host-update-latest">
|
|
7616
|
+
--
|
|
7617
|
+
</div>
|
|
7618
|
+
<div class="field-hint" id="host-update-status">
|
|
7619
|
+
Click Check for updates to query npm.
|
|
7620
|
+
</div>
|
|
7621
|
+
</div>
|
|
7622
|
+
</div>
|
|
7623
|
+
<div class="kv-label">Action</div>
|
|
7624
|
+
<div class="kv-value">
|
|
7625
|
+
<div class="control-cell">
|
|
7626
|
+
<button
|
|
7627
|
+
class="btn-primary"
|
|
7628
|
+
type="button"
|
|
7629
|
+
id="host-update-check-btn"
|
|
7630
|
+
onClick=${call("checkOysterunUpdate")}
|
|
7631
|
+
>
|
|
7632
|
+
Check for updates
|
|
7633
|
+
</button>
|
|
7634
|
+
<button
|
|
7635
|
+
class="btn-primary"
|
|
7636
|
+
type="button"
|
|
7637
|
+
id="host-update-run-btn"
|
|
7638
|
+
onClick=${call("runOysterunUpdate")}
|
|
7639
|
+
>
|
|
7640
|
+
Update now
|
|
7641
|
+
</button>
|
|
7642
|
+
</div>
|
|
7643
|
+
</div>
|
|
7644
|
+
</div>
|
|
7645
|
+
</section>
|
|
7590
7646
|
<section
|
|
7591
7647
|
class="table-panel browser-notification-card"
|
|
7592
7648
|
id="host-notification-card"
|
|
@@ -11265,6 +11321,11 @@ Waiting for Claude login output…</pre
|
|
|
11265
11321
|
loginQrError: "",
|
|
11266
11322
|
restartLoading: false,
|
|
11267
11323
|
restartStatus: "",
|
|
11324
|
+
update: null,
|
|
11325
|
+
updateChecking: false,
|
|
11326
|
+
updateRunning: false,
|
|
11327
|
+
updateStatus: "",
|
|
11328
|
+
updateError: "",
|
|
11268
11329
|
localUrl: "",
|
|
11269
11330
|
defaultBrowsePath: "",
|
|
11270
11331
|
resolvedDefaultBrowsePath: "",
|
|
@@ -26515,6 +26576,14 @@ Waiting for Claude login output…</pre
|
|
|
26515
26576
|
loginQrLoading: false,
|
|
26516
26577
|
loginQrError: "",
|
|
26517
26578
|
restartLoading: false,
|
|
26579
|
+
update:
|
|
26580
|
+
data.update && typeof data.update === "object"
|
|
26581
|
+
? data.update
|
|
26582
|
+
: hostPreferencesState.update,
|
|
26583
|
+
updateChecking: false,
|
|
26584
|
+
updateRunning: false,
|
|
26585
|
+
updateStatus: "",
|
|
26586
|
+
updateError: "",
|
|
26518
26587
|
localUrl: data.local_url || "",
|
|
26519
26588
|
defaultBrowsePath: data.default_browse_path || "",
|
|
26520
26589
|
resolvedDefaultBrowsePath:
|
|
@@ -28099,6 +28168,50 @@ Waiting for Claude login output…</pre
|
|
|
28099
28168
|
? "Restarting..."
|
|
28100
28169
|
: "Restart Host";
|
|
28101
28170
|
}
|
|
28171
|
+
const update = hostPreferencesState.update || {};
|
|
28172
|
+
setText("host-update-current", update.current_version || "--");
|
|
28173
|
+
setText("host-update-channel", update.channel || "--");
|
|
28174
|
+
setText("host-update-latest", update.latest_version || "--");
|
|
28175
|
+
let updateStatus =
|
|
28176
|
+
hostPreferencesState.updateError ||
|
|
28177
|
+
hostPreferencesState.updateStatus ||
|
|
28178
|
+
update.error ||
|
|
28179
|
+
"";
|
|
28180
|
+
if (!updateStatus) {
|
|
28181
|
+
if (update.update_available === true) {
|
|
28182
|
+
updateStatus = `Update available: ${update.latest_version}.`;
|
|
28183
|
+
} else if (update.latest_version) {
|
|
28184
|
+
updateStatus = "Oysterun is up to date for this channel.";
|
|
28185
|
+
} else if (update.update_supported === false) {
|
|
28186
|
+
updateStatus =
|
|
28187
|
+
"Update now is only available on the production npm Host stack.";
|
|
28188
|
+
} else {
|
|
28189
|
+
updateStatus = "Click Check for updates to query npm.";
|
|
28190
|
+
}
|
|
28191
|
+
}
|
|
28192
|
+
setText("host-update-status", updateStatus);
|
|
28193
|
+
const updateCheckButton = $("host-update-check-btn");
|
|
28194
|
+
if (updateCheckButton) {
|
|
28195
|
+
updateCheckButton.disabled =
|
|
28196
|
+
disabled ||
|
|
28197
|
+
hostPreferencesState.updateChecking ||
|
|
28198
|
+
hostPreferencesState.updateRunning;
|
|
28199
|
+
updateCheckButton.textContent = hostPreferencesState.updateChecking
|
|
28200
|
+
? "Checking..."
|
|
28201
|
+
: "Check for updates";
|
|
28202
|
+
}
|
|
28203
|
+
const updateRunButton = $("host-update-run-btn");
|
|
28204
|
+
if (updateRunButton) {
|
|
28205
|
+
updateRunButton.disabled =
|
|
28206
|
+
disabled ||
|
|
28207
|
+
hostPreferencesState.updateChecking ||
|
|
28208
|
+
hostPreferencesState.updateRunning ||
|
|
28209
|
+
update.update_supported !== true ||
|
|
28210
|
+
update.update_available !== true;
|
|
28211
|
+
updateRunButton.textContent = hostPreferencesState.updateRunning
|
|
28212
|
+
? "Updating..."
|
|
28213
|
+
: "Update now";
|
|
28214
|
+
}
|
|
28102
28215
|
const qrPanel = $("host-login-qr-panel");
|
|
28103
28216
|
const qr = hostPreferencesState.loginQr;
|
|
28104
28217
|
if (qrPanel) {
|
|
@@ -28314,6 +28427,98 @@ Waiting for Claude login output…</pre
|
|
|
28314
28427
|
}
|
|
28315
28428
|
}
|
|
28316
28429
|
|
|
28430
|
+
async function checkOysterunUpdate() {
|
|
28431
|
+
if (
|
|
28432
|
+
hostPreferencesState.updateChecking ||
|
|
28433
|
+
hostPreferencesState.updateRunning
|
|
28434
|
+
) {
|
|
28435
|
+
return;
|
|
28436
|
+
}
|
|
28437
|
+
hostPreferencesState = {
|
|
28438
|
+
...hostPreferencesState,
|
|
28439
|
+
updateChecking: true,
|
|
28440
|
+
updateStatus: "Checking npm for updates...",
|
|
28441
|
+
updateError: "",
|
|
28442
|
+
};
|
|
28443
|
+
renderHostPreferences();
|
|
28444
|
+
try {
|
|
28445
|
+
const data = await apiWithAuthRetry("POST", "/admin/update-check", {
|
|
28446
|
+
channel: hostPreferencesState.update?.channel || null,
|
|
28447
|
+
});
|
|
28448
|
+
hostPreferencesState = {
|
|
28449
|
+
...hostPreferencesState,
|
|
28450
|
+
update: data,
|
|
28451
|
+
updateChecking: false,
|
|
28452
|
+
updateStatus:
|
|
28453
|
+
data.update_available === true
|
|
28454
|
+
? `Update available: ${data.latest_version}.`
|
|
28455
|
+
: "Oysterun is up to date for this channel.",
|
|
28456
|
+
updateError: "",
|
|
28457
|
+
};
|
|
28458
|
+
renderHostPreferences();
|
|
28459
|
+
} catch (err) {
|
|
28460
|
+
hostPreferencesState = {
|
|
28461
|
+
...hostPreferencesState,
|
|
28462
|
+
updateChecking: false,
|
|
28463
|
+
updateStatus: "",
|
|
28464
|
+
updateError: err.message || "Could not check for updates.",
|
|
28465
|
+
};
|
|
28466
|
+
renderHostPreferences();
|
|
28467
|
+
}
|
|
28468
|
+
}
|
|
28469
|
+
|
|
28470
|
+
async function runOysterunUpdate() {
|
|
28471
|
+
if (
|
|
28472
|
+
hostPreferencesState.updateRunning ||
|
|
28473
|
+
hostPreferencesState.updateChecking
|
|
28474
|
+
) {
|
|
28475
|
+
return;
|
|
28476
|
+
}
|
|
28477
|
+
const update = hostPreferencesState.update || {};
|
|
28478
|
+
if (
|
|
28479
|
+
update.update_supported !== true ||
|
|
28480
|
+
update.update_available !== true
|
|
28481
|
+
) {
|
|
28482
|
+
return;
|
|
28483
|
+
}
|
|
28484
|
+
const confirmed = window.confirm(
|
|
28485
|
+
"Updating Oysterun restarts the Host. Existing live runtime sessions and running loops will close; chat history remains and supported sessions can be resumed from Sessions after restart. Continue?"
|
|
28486
|
+
);
|
|
28487
|
+
if (!confirmed) return;
|
|
28488
|
+
hostPreferencesState = {
|
|
28489
|
+
...hostPreferencesState,
|
|
28490
|
+
updateRunning: true,
|
|
28491
|
+
updateStatus: "Scheduling Oysterun update...",
|
|
28492
|
+
updateError: "",
|
|
28493
|
+
};
|
|
28494
|
+
renderHostPreferences();
|
|
28495
|
+
try {
|
|
28496
|
+
const data = await apiWithAuthRetry("POST", "/admin/update-run", {
|
|
28497
|
+
channel: update.channel || null,
|
|
28498
|
+
});
|
|
28499
|
+
hostPreferencesState = {
|
|
28500
|
+
...hostPreferencesState,
|
|
28501
|
+
update: data,
|
|
28502
|
+
updateRunning: false,
|
|
28503
|
+
updateStatus:
|
|
28504
|
+
"Oysterun update scheduled. Reconnect after the service comes back.",
|
|
28505
|
+
updateError: "",
|
|
28506
|
+
};
|
|
28507
|
+
renderHostPreferences();
|
|
28508
|
+
window.setTimeout(() => {
|
|
28509
|
+
window.location.href = "/app/settings";
|
|
28510
|
+
}, 6000);
|
|
28511
|
+
} catch (err) {
|
|
28512
|
+
hostPreferencesState = {
|
|
28513
|
+
...hostPreferencesState,
|
|
28514
|
+
updateRunning: false,
|
|
28515
|
+
updateStatus: "",
|
|
28516
|
+
updateError: err.message || "Could not schedule update.",
|
|
28517
|
+
};
|
|
28518
|
+
renderHostPreferences();
|
|
28519
|
+
}
|
|
28520
|
+
}
|
|
28521
|
+
|
|
28317
28522
|
function clearHostPreferencesInput() {
|
|
28318
28523
|
discardUnsavedHostPreferencesChanges();
|
|
28319
28524
|
}
|
|
@@ -41361,6 +41566,8 @@ Waiting for Claude login output…</pre
|
|
|
41361
41566
|
window.saveHostPreferences = saveHostPreferences;
|
|
41362
41567
|
window.generateDirectHostLoginQr = generateDirectHostLoginQr;
|
|
41363
41568
|
window.restartHostFromPreferences = restartHostFromPreferences;
|
|
41569
|
+
window.checkOysterunUpdate = checkOysterunUpdate;
|
|
41570
|
+
window.runOysterunUpdate = runOysterunUpdate;
|
|
41364
41571
|
window.refreshWebNotificationPreferenceState =
|
|
41365
41572
|
refreshWebNotificationPreferenceState;
|
|
41366
41573
|
window.sendWebNotificationTest = sendWebNotificationTest;
|
|
@@ -48,7 +48,9 @@ separate.
|
|
|
48
48
|
[2/7] Host port
|
|
49
49
|
2. Ask for Host port.
|
|
50
50
|
- default production port is 8802.
|
|
51
|
-
- if occupied,
|
|
51
|
+
- if occupied by the same managed Oysterun service, allow the selected port
|
|
52
|
+
and restart that service at the final step.
|
|
53
|
+
- if occupied by another process, offer the next available port.
|
|
52
54
|
|
|
53
55
|
[3/7] iPhone connection URL
|
|
54
56
|
3. Ask how the iPhone will connect to this Mac.
|
|
@@ -82,9 +84,15 @@ separate.
|
|
|
82
84
|
|
|
83
85
|
[7/7] Start Host
|
|
84
86
|
12. Install the launchd service.
|
|
85
|
-
13.
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
13. If no managed Oysterun service is running, ask whether to start Host now,
|
|
88
|
+
default yes.
|
|
89
|
+
14. If a managed Oysterun service is already running, ask whether to restart
|
|
90
|
+
Host now, default yes.
|
|
91
|
+
- warn that existing live runtime sessions and running loops will close.
|
|
92
|
+
- explain that chat history remains and supported sessions can be resumed
|
|
93
|
+
from Sessions after restart.
|
|
94
|
+
15. yes:
|
|
95
|
+
launchd start or restart
|
|
88
96
|
health check
|
|
89
97
|
display Web URL
|
|
90
98
|
on interactive macOS setup, open `<Host URL>/app/sessions` in the browser
|
|
@@ -113,6 +121,10 @@ If the Host is already running and setup changes runtime connection settings,
|
|
|
113
121
|
setup should tell the user restart is required and offer to restart through the
|
|
114
122
|
service manager.
|
|
115
123
|
|
|
124
|
+
Setup rerun must still go through the normal setup questions. A running Host
|
|
125
|
+
does not skip port selection, password update, QR creation, or the final
|
|
126
|
+
service action.
|
|
127
|
+
|
|
116
128
|
## Service Contract
|
|
117
129
|
|
|
118
130
|
Release Host should run as a local managed service, not as a foreground
|
|
@@ -240,6 +252,42 @@ settings. For settings that require process restart:
|
|
|
240
252
|
Runtime edits must not require rebuilding the phone app. Capacitor sync/build
|
|
241
253
|
is only for app code changes, not day-to-day Host/IP changes.
|
|
242
254
|
|
|
255
|
+
## Host Update Control
|
|
256
|
+
|
|
257
|
+
Host Preferences may expose package update controls for npm installs:
|
|
258
|
+
|
|
259
|
+
```text
|
|
260
|
+
current Oysterun version
|
|
261
|
+
update channel, such as latest or beta
|
|
262
|
+
latest version from npm registry after manual check
|
|
263
|
+
Check for updates
|
|
264
|
+
Update now
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Update now is explicit. It must not run automatically on startup.
|
|
268
|
+
|
|
269
|
+
First implementation:
|
|
270
|
+
|
|
271
|
+
```text
|
|
272
|
+
1. Owner clicks Check for updates.
|
|
273
|
+
2. Host queries npm registry for the selected channel.
|
|
274
|
+
3. If a newer version exists, Host enables Update now.
|
|
275
|
+
4. Owner clicks Update now and confirms the restart warning.
|
|
276
|
+
5. Host runs npm install -g oysterun@<channel>.
|
|
277
|
+
6. Host restarts through the same service restart path.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Update now is supported only on the production npm Host stack. Dev/test stacks
|
|
281
|
+
may show version and check status, but must not mutate the global npm package.
|
|
282
|
+
|
|
283
|
+
Until runtime restore is implemented, update/restart UI must warn:
|
|
284
|
+
|
|
285
|
+
```text
|
|
286
|
+
Updating restarts Oysterun Host. Existing live runtime sessions and running
|
|
287
|
+
loops will close; chat history remains and supported sessions can be resumed
|
|
288
|
+
from Sessions after restart.
|
|
289
|
+
```
|
|
290
|
+
|
|
243
291
|
## Stage 2 Direction
|
|
244
292
|
|
|
245
293
|
Stage 2 may add:
|
|
@@ -153,6 +153,38 @@ oysterun service:logs
|
|
|
153
153
|
Explicit service commands should fail fast when setup has not completed. They
|
|
154
154
|
must not silently create config or run setup.
|
|
155
155
|
|
|
156
|
+
## Update Control
|
|
157
|
+
|
|
158
|
+
The npm package must support owner-visible update awareness after install.
|
|
159
|
+
|
|
160
|
+
Host Preferences may show:
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
current_version
|
|
164
|
+
channel: latest or beta
|
|
165
|
+
latest_version after manual npm registry check
|
|
166
|
+
Check for updates
|
|
167
|
+
Update now
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Update now must be explicit and restart-aware:
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
1. Owner clicks Update now.
|
|
174
|
+
2. Host confirms that updating restarts Oysterun Host.
|
|
175
|
+
3. Host runs npm install -g oysterun@<channel>.
|
|
176
|
+
4. Host restarts through the release service path.
|
|
177
|
+
5. Host keeps config.json, cloud_identity.json, Matrix history, and agent
|
|
178
|
+
folders untouched.
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Update now is production-stack only. Dev/test stacks may check npm but must not
|
|
182
|
+
mutate the global npm installation from Host Preferences.
|
|
183
|
+
|
|
184
|
+
Until runtime restore is implemented, the update confirmation must warn that
|
|
185
|
+
live runtime sessions and running loops will close, while durable chat history
|
|
186
|
+
remains and supported sessions can be resumed from Sessions.
|
|
187
|
+
|
|
156
188
|
## Cloud Endpoint
|
|
157
189
|
|
|
158
190
|
The production Cloud endpoint is a git-tracked product constant.
|
package/host-service/server.mjs
CHANGED
|
@@ -4842,6 +4842,144 @@ function getActiveStackName() {
|
|
|
4842
4842
|
);
|
|
4843
4843
|
}
|
|
4844
4844
|
|
|
4845
|
+
const OYSTERUN_NPM_PACKAGE_NAME = "oysterun";
|
|
4846
|
+
|
|
4847
|
+
function readOysterunPackageVersion() {
|
|
4848
|
+
for (const packagePath of [
|
|
4849
|
+
join(REPO_ROOT, "package.json"),
|
|
4850
|
+
join(REPO_ROOT, "host-service", "package.json"),
|
|
4851
|
+
]) {
|
|
4852
|
+
try {
|
|
4853
|
+
const parsed = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
4854
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
4855
|
+
return parsed.version.trim();
|
|
4856
|
+
}
|
|
4857
|
+
} catch {
|
|
4858
|
+
// Best-effort diagnostics must not break Host Preferences.
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
return "unknown";
|
|
4862
|
+
}
|
|
4863
|
+
|
|
4864
|
+
function inferOysterunUpdateChannel(version = readOysterunPackageVersion()) {
|
|
4865
|
+
return String(version || "").includes("-beta") ? "beta" : "latest";
|
|
4866
|
+
}
|
|
4867
|
+
|
|
4868
|
+
function normalizeOysterunUpdateChannel(value, fallback = null) {
|
|
4869
|
+
const normalized = normalizeString(value || fallback);
|
|
4870
|
+
if (normalized === "beta" || normalized === "latest") return normalized;
|
|
4871
|
+
return inferOysterunUpdateChannel();
|
|
4872
|
+
}
|
|
4873
|
+
|
|
4874
|
+
function compareVersionStrings(a, b) {
|
|
4875
|
+
const normalize = (value) =>
|
|
4876
|
+
String(value || "")
|
|
4877
|
+
.replace(/^v/, "")
|
|
4878
|
+
.split(/[-+.]/)
|
|
4879
|
+
.map((part) => {
|
|
4880
|
+
const number = Number.parseInt(part, 10);
|
|
4881
|
+
return Number.isNaN(number) ? part : number;
|
|
4882
|
+
});
|
|
4883
|
+
const left = normalize(a);
|
|
4884
|
+
const right = normalize(b);
|
|
4885
|
+
const length = Math.max(left.length, right.length);
|
|
4886
|
+
for (let index = 0; index < length; index += 1) {
|
|
4887
|
+
const l = left[index] ?? 0;
|
|
4888
|
+
const r = right[index] ?? 0;
|
|
4889
|
+
if (typeof l === "number" && typeof r === "number" && l !== r) {
|
|
4890
|
+
return l > r ? 1 : -1;
|
|
4891
|
+
}
|
|
4892
|
+
const ls = String(l);
|
|
4893
|
+
const rs = String(r);
|
|
4894
|
+
if (ls !== rs) return ls > rs ? 1 : -1;
|
|
4895
|
+
}
|
|
4896
|
+
return 0;
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
function buildOysterunUpdateStatusPayload(extra = {}) {
|
|
4900
|
+
const currentVersion = readOysterunPackageVersion();
|
|
4901
|
+
const channel = normalizeOysterunUpdateChannel(extra.channel, currentVersion);
|
|
4902
|
+
const latestVersion = normalizeString(extra.latest_version);
|
|
4903
|
+
return {
|
|
4904
|
+
status: extra.status || "update_status",
|
|
4905
|
+
package_name: OYSTERUN_NPM_PACKAGE_NAME,
|
|
4906
|
+
current_version: currentVersion,
|
|
4907
|
+
channel,
|
|
4908
|
+
latest_version: latestVersion || null,
|
|
4909
|
+
update_available: latestVersion
|
|
4910
|
+
? compareVersionStrings(latestVersion, currentVersion) > 0
|
|
4911
|
+
: false,
|
|
4912
|
+
update_supported: getActiveStackName() === "production",
|
|
4913
|
+
active_stack: getActiveStackName(),
|
|
4914
|
+
checked_at: extra.checked_at || null,
|
|
4915
|
+
error: extra.error || null,
|
|
4916
|
+
log_path: extra.log_path || null,
|
|
4917
|
+
};
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
async function checkOysterunNpmUpdate({ channel = null } = {}) {
|
|
4921
|
+
const resolvedChannel = normalizeOysterunUpdateChannel(channel);
|
|
4922
|
+
const resp = await fetch(
|
|
4923
|
+
`https://registry.npmjs.org/${encodeURIComponent(OYSTERUN_NPM_PACKAGE_NAME)}`,
|
|
4924
|
+
{ headers: { Accept: "application/json" } }
|
|
4925
|
+
);
|
|
4926
|
+
if (!resp.ok) {
|
|
4927
|
+
throw new Error(`npm registry returned HTTP ${resp.status}`);
|
|
4928
|
+
}
|
|
4929
|
+
const metadata = await resp.json();
|
|
4930
|
+
const latestVersion = normalizeString(metadata?.["dist-tags"]?.[resolvedChannel]);
|
|
4931
|
+
if (!latestVersion) {
|
|
4932
|
+
throw new Error(`npm dist-tag not found: ${resolvedChannel}`);
|
|
4933
|
+
}
|
|
4934
|
+
return buildOysterunUpdateStatusPayload({
|
|
4935
|
+
status: "update_check",
|
|
4936
|
+
channel: resolvedChannel,
|
|
4937
|
+
latest_version: latestVersion,
|
|
4938
|
+
checked_at: new Date().toISOString(),
|
|
4939
|
+
});
|
|
4940
|
+
}
|
|
4941
|
+
|
|
4942
|
+
function shellQuote(value) {
|
|
4943
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
function scheduleOysterunPackageUpdate({ channel = null } = {}) {
|
|
4947
|
+
const stackName = getActiveStackName();
|
|
4948
|
+
if (stackName !== "production") {
|
|
4949
|
+
throw new Error("Oysterun package update is only available on the production npm Host stack.");
|
|
4950
|
+
}
|
|
4951
|
+
const resolvedChannel = normalizeOysterunUpdateChannel(channel);
|
|
4952
|
+
const logDir = join(getConfigDir(), "operation_logs");
|
|
4953
|
+
mkdirSync(logDir, { recursive: true });
|
|
4954
|
+
const logPath = join(
|
|
4955
|
+
logDir,
|
|
4956
|
+
`oysterun-update-${new Date().toISOString().replace(/[:.]/g, "-")}.log`
|
|
4957
|
+
);
|
|
4958
|
+
const packageSpec = `${OYSTERUN_NPM_PACKAGE_NAME}@${resolvedChannel}`;
|
|
4959
|
+
const command = [
|
|
4960
|
+
"set -euo pipefail",
|
|
4961
|
+
'export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"',
|
|
4962
|
+
`echo "[oysterun-update] start $(date -Is)"`,
|
|
4963
|
+
`npm install -g ${shellQuote(packageSpec)}`,
|
|
4964
|
+
`echo "[oysterun-update] npm install complete $(date -Is)"`,
|
|
4965
|
+
"oysterun service:restart",
|
|
4966
|
+
].join("\n");
|
|
4967
|
+
setTimeout(() => {
|
|
4968
|
+
const child = spawn("bash", ["-lc", `${command} >> ${shellQuote(logPath)} 2>&1`], {
|
|
4969
|
+
cwd: REPO_ROOT,
|
|
4970
|
+
detached: true,
|
|
4971
|
+
env: { ...process.env, OYSTERUN_RELEASE_STACK: "production" },
|
|
4972
|
+
stdio: "ignore",
|
|
4973
|
+
});
|
|
4974
|
+
child.unref();
|
|
4975
|
+
}, 500).unref();
|
|
4976
|
+
return buildOysterunUpdateStatusPayload({
|
|
4977
|
+
status: "update_scheduled",
|
|
4978
|
+
channel: resolvedChannel,
|
|
4979
|
+
log_path: logPath,
|
|
4980
|
+
});
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4845
4983
|
function scheduleHostRestart({ redirectUrl = null } = {}) {
|
|
4846
4984
|
const stackName = getActiveStackName();
|
|
4847
4985
|
if (!stackName) {
|
|
@@ -4918,6 +5056,7 @@ function buildHostPreferencesPayload() {
|
|
|
4918
5056
|
config_path: getConfigPath(),
|
|
4919
5057
|
config_dir: getConfigDir(),
|
|
4920
5058
|
config_source: getConfigSource(),
|
|
5059
|
+
update: buildOysterunUpdateStatusPayload(),
|
|
4921
5060
|
ios_push: buildApnsPushStatusPayload("dashboard-admin"),
|
|
4922
5061
|
session_defaults: config.session_defaults,
|
|
4923
5062
|
error: null,
|
|
@@ -8146,6 +8285,45 @@ const server = createServer(async (req, res) => {
|
|
|
8146
8285
|
}
|
|
8147
8286
|
}
|
|
8148
8287
|
|
|
8288
|
+
// ── Oysterun npm package update status/control (dashboard-authenticated) ──
|
|
8289
|
+
if (req.method === "GET" && path === "/admin/update-status") {
|
|
8290
|
+
if (!requireDashboardMode(claims, res)) return;
|
|
8291
|
+
return respond(res, 200, buildOysterunUpdateStatusPayload());
|
|
8292
|
+
}
|
|
8293
|
+
|
|
8294
|
+
if (req.method === "POST" && path === "/admin/update-check") {
|
|
8295
|
+
if (!requireDashboardMode(claims, res)) return;
|
|
8296
|
+
try {
|
|
8297
|
+
const body = await readJsonBody(req);
|
|
8298
|
+
return respond(
|
|
8299
|
+
res,
|
|
8300
|
+
200,
|
|
8301
|
+
await checkOysterunNpmUpdate({ channel: body.channel })
|
|
8302
|
+
);
|
|
8303
|
+
} catch (err) {
|
|
8304
|
+
return respond(res, 409, {
|
|
8305
|
+
...buildOysterunUpdateStatusPayload({
|
|
8306
|
+
status: "update_check_failed",
|
|
8307
|
+
error: err.message,
|
|
8308
|
+
}),
|
|
8309
|
+
});
|
|
8310
|
+
}
|
|
8311
|
+
}
|
|
8312
|
+
|
|
8313
|
+
if (req.method === "POST" && path === "/admin/update-run") {
|
|
8314
|
+
if (!requireDashboardMode(claims, res)) return;
|
|
8315
|
+
try {
|
|
8316
|
+
const body = await readJsonBody(req);
|
|
8317
|
+
return respond(
|
|
8318
|
+
res,
|
|
8319
|
+
202,
|
|
8320
|
+
scheduleOysterunPackageUpdate({ channel: body.channel })
|
|
8321
|
+
);
|
|
8322
|
+
} catch (err) {
|
|
8323
|
+
return respond(res, 409, { error: err.message });
|
|
8324
|
+
}
|
|
8325
|
+
}
|
|
8326
|
+
|
|
8149
8327
|
// ── Authenticated transcript cleanup dry-run/status APIs (P39E source only) ──
|
|
8150
8328
|
if (
|
|
8151
8329
|
req.method === "GET" &&
|
package/host-service/setup.mjs
CHANGED
|
@@ -245,16 +245,39 @@ async function promptHidden(question) {
|
|
|
245
245
|
});
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
function runReleaseService(command, args = [], opts = {}) {
|
|
248
|
+
function runReleaseService(command, args = [], opts = {}, options = {}) {
|
|
249
249
|
const serviceStack = inferReleaseServiceStack(opts);
|
|
250
|
-
|
|
250
|
+
const execOptions = {
|
|
251
251
|
cwd: repoRoot,
|
|
252
|
-
stdio: "inherit",
|
|
253
252
|
env: {
|
|
254
253
|
...process.env,
|
|
255
254
|
OYSTERUN_RELEASE_STACK: serviceStack,
|
|
256
255
|
},
|
|
257
|
-
}
|
|
256
|
+
};
|
|
257
|
+
if (!options.quiet) {
|
|
258
|
+
execFileSync(releaseServiceScript, [command, ...args], {
|
|
259
|
+
...execOptions,
|
|
260
|
+
stdio: "inherit",
|
|
261
|
+
});
|
|
262
|
+
return "";
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const stdout = execFileSync(releaseServiceScript, [command, ...args], {
|
|
266
|
+
...execOptions,
|
|
267
|
+
encoding: "utf8",
|
|
268
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
269
|
+
});
|
|
270
|
+
return stdout || "";
|
|
271
|
+
} catch (err) {
|
|
272
|
+
const stdout = err.stdout ? String(err.stdout).trim() : "";
|
|
273
|
+
const stderr = err.stderr ? String(err.stderr).trim() : "";
|
|
274
|
+
if (stdout || stderr) {
|
|
275
|
+
console.error(" Host service diagnostic output:");
|
|
276
|
+
if (stdout) console.error(stdout);
|
|
277
|
+
if (stderr) console.error(stderr);
|
|
278
|
+
}
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
258
281
|
}
|
|
259
282
|
|
|
260
283
|
function inferReleaseServiceStack(opts = {}) {
|
|
@@ -275,6 +298,55 @@ function inferReleaseServiceStack(opts = {}) {
|
|
|
275
298
|
return "production";
|
|
276
299
|
}
|
|
277
300
|
|
|
301
|
+
function parseReleaseServiceStatusOutput(output) {
|
|
302
|
+
const status = {};
|
|
303
|
+
for (const line of String(output || "").split(/\r?\n/)) {
|
|
304
|
+
const match = line.match(/^([a-z_]+)=(.*)$/);
|
|
305
|
+
if (!match) continue;
|
|
306
|
+
status[match[1]] = match[2].trim();
|
|
307
|
+
}
|
|
308
|
+
return status;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function getReleaseServiceStatus(opts = {}) {
|
|
312
|
+
if (process.platform !== "darwin") {
|
|
313
|
+
return { available: false, running: false, status: {}, error: null };
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const output = runReleaseService("status", [], opts, { quiet: true });
|
|
317
|
+
const status = parseReleaseServiceStatusOutput(output);
|
|
318
|
+
const pid = status.pid && status.pid !== "-" ? status.pid : "";
|
|
319
|
+
const running =
|
|
320
|
+
status.loaded === "yes" || Boolean(pid) || status.health === "yes";
|
|
321
|
+
return { available: true, running, status, error: null };
|
|
322
|
+
} catch (err) {
|
|
323
|
+
return {
|
|
324
|
+
available: false,
|
|
325
|
+
running: false,
|
|
326
|
+
status: {},
|
|
327
|
+
error: err?.message || String(err),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getUrlPort(value) {
|
|
333
|
+
try {
|
|
334
|
+
const parsed = new URL(String(value || ""));
|
|
335
|
+
if (parsed.port) return Number.parseInt(parsed.port, 10);
|
|
336
|
+
if (parsed.protocol === "https:") return 443;
|
|
337
|
+
if (parsed.protocol === "http:") return 80;
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isPortOccupiedByManagedReleaseService(port, opts = {}) {
|
|
345
|
+
const releaseStatus = getReleaseServiceStatus(opts);
|
|
346
|
+
if (!releaseStatus.running) return false;
|
|
347
|
+
return getUrlPort(releaseStatus.status.url) === port;
|
|
348
|
+
}
|
|
349
|
+
|
|
278
350
|
async function maybeOpenSetupBrowser(url) {
|
|
279
351
|
if (!isInteractiveSetup() || process.platform !== "darwin" || !url) return;
|
|
280
352
|
try {
|
|
@@ -295,26 +367,41 @@ async function maybeInstallOrStartReleaseService(opts, { hostUrl }) {
|
|
|
295
367
|
throw new Error("release service install/start requires macOS launchd");
|
|
296
368
|
}
|
|
297
369
|
|
|
298
|
-
|
|
370
|
+
const serviceStatus = getReleaseServiceStatus(opts);
|
|
371
|
+
const serviceAlreadyRunning = serviceStatus.running === true;
|
|
372
|
+
printSetupStep(7, serviceAlreadyRunning ? "Restart Host" : "Start Host");
|
|
373
|
+
if (serviceAlreadyRunning && isInteractiveSetup()) {
|
|
374
|
+
console.log(" An Oysterun Host service is already running.");
|
|
375
|
+
console.log(
|
|
376
|
+
" Restarting will close existing live runtime sessions and running loops."
|
|
377
|
+
);
|
|
378
|
+
console.log(
|
|
379
|
+
" Chat history remains, and supported sessions can be resumed from Sessions after restart."
|
|
380
|
+
);
|
|
381
|
+
}
|
|
299
382
|
const shouldStart = opts.noStartService
|
|
300
383
|
? false
|
|
301
384
|
: opts.startService
|
|
302
385
|
? true
|
|
303
|
-
: await promptYesNoDefaultYes(
|
|
386
|
+
: await promptYesNoDefaultYes(
|
|
387
|
+
serviceAlreadyRunning
|
|
388
|
+
? "Restart Oysterun Host now?"
|
|
389
|
+
: "Start Oysterun Host now?"
|
|
390
|
+
);
|
|
304
391
|
|
|
305
392
|
console.log();
|
|
306
393
|
if (shouldStart) {
|
|
307
394
|
console.log(
|
|
308
|
-
` Installing LaunchAgent and starting Oysterun Host (${inferReleaseServiceStack(opts)})...`
|
|
395
|
+
` Installing LaunchAgent and ${serviceAlreadyRunning ? "restarting" : "starting"} Oysterun Host (${inferReleaseServiceStack(opts)})...`
|
|
309
396
|
);
|
|
310
|
-
runReleaseService("install", [], opts);
|
|
397
|
+
runReleaseService("install", [], opts, { quiet: true });
|
|
311
398
|
console.log(`\n ✓ Oysterun Host is running at ${hostUrl}`);
|
|
312
399
|
await maybeOpenSetupBrowser(`${hostUrl}/app/sessions`);
|
|
313
400
|
} else {
|
|
314
401
|
console.log(
|
|
315
402
|
` Installing LaunchAgent without starting Oysterun Host (${inferReleaseServiceStack(opts)})...`
|
|
316
403
|
);
|
|
317
|
-
runReleaseService("install", ["--no-start"], opts);
|
|
404
|
+
runReleaseService("install", ["--no-start"], opts, { quiet: true });
|
|
318
405
|
console.log("\n Host service was installed but not started.");
|
|
319
406
|
console.log(" Start later with:");
|
|
320
407
|
console.log(" oysterun");
|
|
@@ -390,10 +477,16 @@ async function findAvailablePort(startPort) {
|
|
|
390
477
|
return null;
|
|
391
478
|
}
|
|
392
479
|
|
|
393
|
-
async function promptHostPort({ defaultPort, optsPort }) {
|
|
480
|
+
async function promptHostPort({ defaultPort, optsPort, opts = {} }) {
|
|
394
481
|
if (optsPort !== undefined) {
|
|
395
482
|
const port = parsePortInput(optsPort);
|
|
396
483
|
if (port === null) throw new Error(`Invalid port: ${optsPort}`);
|
|
484
|
+
if (
|
|
485
|
+
!(await isPortAvailable(port)) &&
|
|
486
|
+
!isPortOccupiedByManagedReleaseService(port, opts)
|
|
487
|
+
) {
|
|
488
|
+
throw new Error(`Port ${port} is already in use`);
|
|
489
|
+
}
|
|
397
490
|
return port;
|
|
398
491
|
}
|
|
399
492
|
|
|
@@ -407,6 +500,12 @@ async function promptHostPort({ defaultPort, optsPort }) {
|
|
|
407
500
|
continue;
|
|
408
501
|
}
|
|
409
502
|
if (await isPortAvailable(port)) return port;
|
|
503
|
+
if (isPortOccupiedByManagedReleaseService(port, opts)) {
|
|
504
|
+
console.log(
|
|
505
|
+
` Port ${port} is used by the current Oysterun Host service and will be reused after restart.`
|
|
506
|
+
);
|
|
507
|
+
return port;
|
|
508
|
+
}
|
|
410
509
|
|
|
411
510
|
const fallback = await findAvailablePort(port + 1);
|
|
412
511
|
if (fallback && isInteractiveSetup()) {
|
|
@@ -646,6 +745,7 @@ async function configureDirectMode(opts, config) {
|
|
|
646
745
|
const port = await promptHostPort({
|
|
647
746
|
defaultPort: parsePortInput(currentPort) || defaultConfig.port,
|
|
648
747
|
optsPort: opts.port,
|
|
748
|
+
opts,
|
|
649
749
|
});
|
|
650
750
|
|
|
651
751
|
const currentPublicBaseUrl = config.public_base_url || "";
|
|
@@ -727,6 +827,7 @@ async function configureCloudDirectMode(opts, config, backendUrl, backendStage)
|
|
|
727
827
|
const port = await promptHostPort({
|
|
728
828
|
defaultPort: parsePortInput(portValue) || defaultConfig.port,
|
|
729
829
|
optsPort: opts.port,
|
|
830
|
+
opts,
|
|
730
831
|
});
|
|
731
832
|
|
|
732
833
|
const directHostUrl = await promptDirectHostUrl({
|