oysterun 0.1.1-beta.2 → 0.1.1-beta.5
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 +67 -12
- package/docs/routec/npmjs_publish.md +37 -2
- package/host-service/package.json +1 -1
- package/host-service/server.mjs +178 -0
- package/host-service/setup.mjs +159 -35
- package/package.json +1 -1
- package/tool_scripts/oysterun_release_service.sh +27 -2
|
@@ -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.
|
|
@@ -72,25 +74,34 @@ separate.
|
|
|
72
74
|
9. Explain: "You need the Oysterun phone app to scan the login QR."
|
|
73
75
|
Ask: "Show phone app download link and QR code?"
|
|
74
76
|
- yes: show the phone app download link and QR.
|
|
75
|
-
- no: continue to
|
|
77
|
+
- no: continue to service start.
|
|
76
78
|
- first version download URL: `https://oysterun.com`.
|
|
77
79
|
- later production URL should be the App Store link.
|
|
78
80
|
|
|
79
81
|
[6/7] Login QR
|
|
80
|
-
10.
|
|
81
|
-
11.
|
|
82
|
+
10. Do not print the login QR before Host start.
|
|
83
|
+
11. Explain that a fresh login QR will be shown after the Host service starts.
|
|
84
|
+
If the user chooses not to start now, setup must not print a login QR.
|
|
82
85
|
|
|
83
86
|
[7/7] Start Host
|
|
84
87
|
12. Install the launchd service.
|
|
85
|
-
13.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
13. If no managed Oysterun service is running, ask whether to start Host now,
|
|
89
|
+
default yes.
|
|
90
|
+
14. If a managed Oysterun service is already running, ask whether to restart
|
|
91
|
+
Host now, default yes.
|
|
92
|
+
- warn that existing live runtime sessions and running loops will close.
|
|
93
|
+
- explain that chat history remains and supported sessions can be resumed
|
|
94
|
+
from Sessions after restart.
|
|
95
|
+
15. yes:
|
|
96
|
+
launchd start or restart
|
|
88
97
|
health check
|
|
89
98
|
display Web URL
|
|
90
99
|
on interactive macOS setup, open `<Host URL>/app/sessions` in the browser
|
|
91
100
|
after health check succeeds
|
|
101
|
+
create and print a fresh 15-minute one-time Host login QR
|
|
92
102
|
no:
|
|
93
103
|
print `oysterun` as the start command.
|
|
104
|
+
explain that the start command will print a fresh login QR.
|
|
94
105
|
```
|
|
95
106
|
|
|
96
107
|
Setup must not ask for macOS Full Disk Access during first-run installation.
|
|
@@ -113,6 +124,11 @@ If the Host is already running and setup changes runtime connection settings,
|
|
|
113
124
|
setup should tell the user restart is required and offer to restart through the
|
|
114
125
|
service manager.
|
|
115
126
|
|
|
127
|
+
Setup rerun must still go through the normal setup questions. A running Host
|
|
128
|
+
does not skip port selection, password update, login QR readiness explanation,
|
|
129
|
+
or the final service action. The login QR itself is only printed after the
|
|
130
|
+
service starts or restarts successfully.
|
|
131
|
+
|
|
116
132
|
## Service Contract
|
|
117
133
|
|
|
118
134
|
Release Host should run as a local managed service, not as a foreground
|
|
@@ -138,21 +154,24 @@ Release service behavior:
|
|
|
138
154
|
- does not require the user to remember a raw server command
|
|
139
155
|
```
|
|
140
156
|
|
|
157
|
+
`oysterun service:start` and `oysterun service:restart` must print a fresh
|
|
158
|
+
15-minute one-time Host login QR at the end after the Host start/restart
|
|
159
|
+
finishes successfully.
|
|
160
|
+
|
|
141
161
|
## QR Login Contract
|
|
142
162
|
|
|
143
163
|
The setup QR is a convenience login/bootstrap path. It should contain enough
|
|
144
164
|
information for the app to connect and log in without typing IP:port/password:
|
|
145
165
|
|
|
146
166
|
```text
|
|
147
|
-
host_id
|
|
148
167
|
direct_host_url
|
|
149
168
|
one-time login/bootstrap token
|
|
150
|
-
expiry
|
|
151
|
-
Cloud API URL when Cloud direct mode is enabled
|
|
152
|
-
display name
|
|
153
169
|
```
|
|
154
170
|
|
|
155
|
-
The
|
|
171
|
+
The terminal QR should use the smallest app-compatible payload. In Stage 1 this
|
|
172
|
+
is `u` for Host URL and `b` for the one-time login/bootstrap token. Host ID,
|
|
173
|
+
Host display name, and expiry are product state shown or fetched separately, not
|
|
174
|
+
required QR fields. The one-time token should expire after 15 minutes.
|
|
156
175
|
|
|
157
176
|
Host Preferences must provide a "generate login QR" action for later pairing or
|
|
158
177
|
re-login. It creates a new short-lived token and displays the QR.
|
|
@@ -240,6 +259,42 @@ settings. For settings that require process restart:
|
|
|
240
259
|
Runtime edits must not require rebuilding the phone app. Capacitor sync/build
|
|
241
260
|
is only for app code changes, not day-to-day Host/IP changes.
|
|
242
261
|
|
|
262
|
+
## Host Update Control
|
|
263
|
+
|
|
264
|
+
Host Preferences may expose package update controls for npm installs:
|
|
265
|
+
|
|
266
|
+
```text
|
|
267
|
+
current Oysterun version
|
|
268
|
+
update channel, such as latest or beta
|
|
269
|
+
latest version from npm registry after manual check
|
|
270
|
+
Check for updates
|
|
271
|
+
Update now
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Update now is explicit. It must not run automatically on startup.
|
|
275
|
+
|
|
276
|
+
First implementation:
|
|
277
|
+
|
|
278
|
+
```text
|
|
279
|
+
1. Owner clicks Check for updates.
|
|
280
|
+
2. Host queries npm registry for the selected channel.
|
|
281
|
+
3. If a newer version exists, Host enables Update now.
|
|
282
|
+
4. Owner clicks Update now and confirms the restart warning.
|
|
283
|
+
5. Host runs npm install -g oysterun@<channel>.
|
|
284
|
+
6. Host restarts through the same service restart path.
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Update now is supported only on the production npm Host stack. Dev/test stacks
|
|
288
|
+
may show version and check status, but must not mutate the global npm package.
|
|
289
|
+
|
|
290
|
+
Until runtime restore is implemented, update/restart UI must warn:
|
|
291
|
+
|
|
292
|
+
```text
|
|
293
|
+
Updating restarts Oysterun Host. Existing live runtime sessions and running
|
|
294
|
+
loops will close; chat history remains and supported sessions can be resumed
|
|
295
|
+
from Sessions after restart.
|
|
296
|
+
```
|
|
297
|
+
|
|
243
298
|
## Stage 2 Direction
|
|
244
299
|
|
|
245
300
|
Stage 2 may add:
|
|
@@ -117,12 +117,12 @@ write ~/.oysterun/config.json
|
|
|
117
117
|
create dashboard credential hash
|
|
118
118
|
register Cloud direct mode when enabled by release setup
|
|
119
119
|
write Host Cloud identity state outside user-editable config
|
|
120
|
-
create 15-minute one-time QR/bootstrap token
|
|
121
120
|
show phone app download/QR step
|
|
122
|
-
|
|
121
|
+
explain that Host login QR is shown after service start
|
|
123
122
|
install service manager integration
|
|
124
123
|
start Host by default after explicit setup flow
|
|
125
124
|
wait for /health
|
|
125
|
+
create and show a fresh 15-minute Host login QR only when Host starts
|
|
126
126
|
show/open /app/sessions when browser open is available
|
|
127
127
|
```
|
|
128
128
|
|
|
@@ -153,6 +153,41 @@ 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
|
+
`oysterun service:start` and `oysterun service:restart` print a fresh
|
|
157
|
+
15-minute one-time Host login QR after the Host start/restart succeeds.
|
|
158
|
+
|
|
159
|
+
## Update Control
|
|
160
|
+
|
|
161
|
+
The npm package must support owner-visible update awareness after install.
|
|
162
|
+
|
|
163
|
+
Host Preferences may show:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
current_version
|
|
167
|
+
channel: latest or beta
|
|
168
|
+
latest_version after manual npm registry check
|
|
169
|
+
Check for updates
|
|
170
|
+
Update now
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Update now must be explicit and restart-aware:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
1. Owner clicks Update now.
|
|
177
|
+
2. Host confirms that updating restarts Oysterun Host.
|
|
178
|
+
3. Host runs npm install -g oysterun@<channel>.
|
|
179
|
+
4. Host restarts through the release service path.
|
|
180
|
+
5. Host keeps config.json, cloud_identity.json, Matrix history, and agent
|
|
181
|
+
folders untouched.
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Update now is production-stack only. Dev/test stacks may check npm but must not
|
|
185
|
+
mutate the global npm installation from Host Preferences.
|
|
186
|
+
|
|
187
|
+
Until runtime restore is implemented, the update confirmation must warn that
|
|
188
|
+
live runtime sessions and running loops will close, while durable chat history
|
|
189
|
+
remains and supported sessions can be resumed from Sessions.
|
|
190
|
+
|
|
156
191
|
## Cloud Endpoint
|
|
157
192
|
|
|
158
193
|
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 {
|
|
@@ -289,37 +361,54 @@ async function maybeOpenSetupBrowser(url) {
|
|
|
289
361
|
|
|
290
362
|
async function maybeInstallOrStartReleaseService(opts, { hostUrl }) {
|
|
291
363
|
if (!opts.installService && !opts.startService && !opts.noStartService) {
|
|
292
|
-
return false;
|
|
364
|
+
return { handled: false, started: false, serviceAlreadyRunning: false };
|
|
293
365
|
}
|
|
294
366
|
if (process.platform !== "darwin") {
|
|
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`);
|
|
400
|
+
return { handled: true, started: true, serviceAlreadyRunning };
|
|
313
401
|
} else {
|
|
314
402
|
console.log(
|
|
315
403
|
` Installing LaunchAgent without starting Oysterun Host (${inferReleaseServiceStack(opts)})...`
|
|
316
404
|
);
|
|
317
|
-
runReleaseService("install", ["--no-start"], opts);
|
|
405
|
+
runReleaseService("install", ["--no-start"], opts, { quiet: true });
|
|
318
406
|
console.log("\n Host service was installed but not started.");
|
|
319
407
|
console.log(" Start later with:");
|
|
320
408
|
console.log(" oysterun");
|
|
409
|
+
console.log(" A fresh login QR will be shown after the Host starts.");
|
|
410
|
+
return { handled: true, started: false, serviceAlreadyRunning };
|
|
321
411
|
}
|
|
322
|
-
return true;
|
|
323
412
|
}
|
|
324
413
|
|
|
325
414
|
function requireNonEmptyPassword(value, label) {
|
|
@@ -390,10 +479,16 @@ async function findAvailablePort(startPort) {
|
|
|
390
479
|
return null;
|
|
391
480
|
}
|
|
392
481
|
|
|
393
|
-
async function promptHostPort({ defaultPort, optsPort }) {
|
|
482
|
+
async function promptHostPort({ defaultPort, optsPort, opts = {} }) {
|
|
394
483
|
if (optsPort !== undefined) {
|
|
395
484
|
const port = parsePortInput(optsPort);
|
|
396
485
|
if (port === null) throw new Error(`Invalid port: ${optsPort}`);
|
|
486
|
+
if (
|
|
487
|
+
!(await isPortAvailable(port)) &&
|
|
488
|
+
!isPortOccupiedByManagedReleaseService(port, opts)
|
|
489
|
+
) {
|
|
490
|
+
throw new Error(`Port ${port} is already in use`);
|
|
491
|
+
}
|
|
397
492
|
return port;
|
|
398
493
|
}
|
|
399
494
|
|
|
@@ -407,6 +502,12 @@ async function promptHostPort({ defaultPort, optsPort }) {
|
|
|
407
502
|
continue;
|
|
408
503
|
}
|
|
409
504
|
if (await isPortAvailable(port)) return port;
|
|
505
|
+
if (isPortOccupiedByManagedReleaseService(port, opts)) {
|
|
506
|
+
console.log(
|
|
507
|
+
` Port ${port} is used by the current Oysterun Host service and will be reused after restart.`
|
|
508
|
+
);
|
|
509
|
+
return port;
|
|
510
|
+
}
|
|
410
511
|
|
|
411
512
|
const fallback = await findAvailablePort(port + 1);
|
|
412
513
|
if (fallback && isInteractiveSetup()) {
|
|
@@ -625,6 +726,29 @@ function createDirectHostLoginQrPayload({ hostId, directHostUrl }) {
|
|
|
625
726
|
};
|
|
626
727
|
}
|
|
627
728
|
|
|
729
|
+
function buildTerminalDirectHostLoginQrPayload(qrPayload) {
|
|
730
|
+
return {
|
|
731
|
+
u: qrPayload.verbose.direct_host_url,
|
|
732
|
+
b: qrPayload.verbose.bootstrap_token,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function printDirectHostLoginQr({ hostId, directHostUrl }) {
|
|
737
|
+
const qrPayload = createDirectHostLoginQrPayload({
|
|
738
|
+
hostId,
|
|
739
|
+
directHostUrl,
|
|
740
|
+
});
|
|
741
|
+
console.log("\n Direct Host connection QR:\n");
|
|
742
|
+
qrcode.generate(
|
|
743
|
+
JSON.stringify(buildTerminalDirectHostLoginQrPayload(qrPayload)),
|
|
744
|
+
{ small: true }
|
|
745
|
+
);
|
|
746
|
+
console.log(`\n Host ID: ${hostId}`);
|
|
747
|
+
console.log(` Direct Host URL: ${directHostUrl}`);
|
|
748
|
+
console.log(` QR expires at: ${qrPayload.verbose.expires_at}`);
|
|
749
|
+
return qrPayload;
|
|
750
|
+
}
|
|
751
|
+
|
|
628
752
|
async function configureDirectMode(opts, config) {
|
|
629
753
|
const detectedCommands = detectProviderCommands();
|
|
630
754
|
const directSetupDefaults = getDirectSetupDefaults();
|
|
@@ -646,6 +770,7 @@ async function configureDirectMode(opts, config) {
|
|
|
646
770
|
const port = await promptHostPort({
|
|
647
771
|
defaultPort: parsePortInput(currentPort) || defaultConfig.port,
|
|
648
772
|
optsPort: opts.port,
|
|
773
|
+
opts,
|
|
649
774
|
});
|
|
650
775
|
|
|
651
776
|
const currentPublicBaseUrl = config.public_base_url || "";
|
|
@@ -691,10 +816,10 @@ async function configureDirectMode(opts, config) {
|
|
|
691
816
|
console.log(` Local login username: ${dashboardUser}`);
|
|
692
817
|
printDetectedProviderStatus(detectedCommands);
|
|
693
818
|
console.log(` Config saved to ${getConfigPath()}`);
|
|
694
|
-
const
|
|
819
|
+
const serviceResult = await maybeInstallOrStartReleaseService(opts, {
|
|
695
820
|
hostUrl: publicBaseUrl || `http://localhost:${port}`,
|
|
696
821
|
});
|
|
697
|
-
if (!
|
|
822
|
+
if (!serviceResult.handled) {
|
|
698
823
|
console.log(`\n Next steps:`);
|
|
699
824
|
console.log(` 1. Run: oysterun`);
|
|
700
825
|
console.log(` 2. Open the Host URL directly from your client app`);
|
|
@@ -727,6 +852,7 @@ async function configureCloudDirectMode(opts, config, backendUrl, backendStage)
|
|
|
727
852
|
const port = await promptHostPort({
|
|
728
853
|
defaultPort: parsePortInput(portValue) || defaultConfig.port,
|
|
729
854
|
optsPort: opts.port,
|
|
855
|
+
opts,
|
|
730
856
|
});
|
|
731
857
|
|
|
732
858
|
const directHostUrl = await promptDirectHostUrl({
|
|
@@ -821,11 +947,6 @@ async function configureCloudDirectMode(opts, config, backendUrl, backendStage)
|
|
|
821
947
|
codex_command: detectedCommands.codex_command,
|
|
822
948
|
});
|
|
823
949
|
|
|
824
|
-
const qrPayload = createDirectHostLoginQrPayload({
|
|
825
|
-
hostId,
|
|
826
|
-
directHostUrl,
|
|
827
|
-
});
|
|
828
|
-
|
|
829
950
|
console.log(`\n ✓ Direct-IP Cloud Host registration complete`);
|
|
830
951
|
console.log(` Host ID: ${hostId}`);
|
|
831
952
|
console.log(` Direct Host URL: ${directHostUrl}`);
|
|
@@ -835,27 +956,34 @@ async function configureCloudDirectMode(opts, config, backendUrl, backendStage)
|
|
|
835
956
|
printDetectedProviderStatus(detectedCommands);
|
|
836
957
|
await maybeShowPhoneAppDownloadStep();
|
|
837
958
|
printSetupStep(6, "Login QR");
|
|
838
|
-
console.log("
|
|
839
|
-
|
|
840
|
-
console.log(`\n QR expires at: ${qrPayload.verbose.expires_at}`);
|
|
959
|
+
console.log(" A fresh login QR will be shown after the Host service starts.");
|
|
960
|
+
console.log(" If you choose not to start now, run `oysterun` later to start and print a QR.");
|
|
841
961
|
console.log(`\n Config saved to ${getConfigPath()}`);
|
|
842
962
|
console.log(` Cloud identity saved to ${getCloudIdentityPath()}`);
|
|
843
|
-
const
|
|
963
|
+
const serviceResult = await maybeInstallOrStartReleaseService(opts, {
|
|
844
964
|
hostUrl: directHostUrl,
|
|
845
965
|
});
|
|
846
|
-
if (
|
|
966
|
+
if (serviceResult.started) {
|
|
967
|
+
printDirectHostLoginQr({ hostId, directHostUrl });
|
|
847
968
|
console.log("\n Oysterun is ready.");
|
|
848
969
|
console.log(` Web: ${directHostUrl}/app/sessions`);
|
|
849
|
-
console.log(" iPhone: scan the QR above
|
|
970
|
+
console.log(" iPhone: scan the QR above.");
|
|
850
971
|
console.log(" Later: run `oysterun`.");
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (serviceResult.handled) {
|
|
975
|
+
console.log("\n Host service was not started.");
|
|
976
|
+
console.log(" Start later with: oysterun");
|
|
977
|
+
console.log(" The start command will print a fresh login QR.");
|
|
978
|
+
return;
|
|
851
979
|
}
|
|
852
|
-
|
|
980
|
+
|
|
981
|
+
if (!serviceResult.handled) {
|
|
853
982
|
console.log(`\n Next steps:`);
|
|
854
983
|
console.log(` 1. Run: oysterun`);
|
|
855
984
|
console.log(` 2. Connect the app to ${directHostUrl}`);
|
|
856
|
-
console.log(
|
|
857
|
-
|
|
858
|
-
);
|
|
985
|
+
console.log(` 3. Run: oysterun show-qr`);
|
|
986
|
+
console.log(` 4. Scan the QR, or log in with the Host password`);
|
|
859
987
|
console.log();
|
|
860
988
|
}
|
|
861
989
|
}
|
|
@@ -990,15 +1118,11 @@ async function main() {
|
|
|
990
1118
|
// ── --show-qr: Re-display existing QR ──────────────────
|
|
991
1119
|
if (opts.showQr) {
|
|
992
1120
|
if (config.connection_mode === "direct" && config.host_id && config.direct_host_url) {
|
|
993
|
-
|
|
1121
|
+
printDirectHostLoginQr({
|
|
994
1122
|
hostId: config.host_id,
|
|
995
1123
|
directHostUrl: config.direct_host_url,
|
|
996
1124
|
});
|
|
997
|
-
console.log(
|
|
998
|
-
qrcode.generate(JSON.stringify(qrPayload.compact), { small: true });
|
|
999
|
-
console.log(`\n Host ID: ${config.host_id}`);
|
|
1000
|
-
console.log(` Direct Host URL: ${config.direct_host_url}`);
|
|
1001
|
-
console.log(` QR expires at: ${qrPayload.verbose.expires_at}\n`);
|
|
1125
|
+
console.log();
|
|
1002
1126
|
return;
|
|
1003
1127
|
}
|
|
1004
1128
|
if (config.connection_mode !== "cloud" || !config.onboarding_url) {
|
package/package.json
CHANGED
|
@@ -26,18 +26,43 @@ for development with OYSTERUN_RELEASE_STACK=<stack>.
|
|
|
26
26
|
EOF
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
print_login_qr_after_service_ready() {
|
|
30
|
+
local service_args=("$@")
|
|
31
|
+
local target_stack="${STACK_NAME}"
|
|
32
|
+
(
|
|
33
|
+
source "${SCRIPT_DIR}/oysterun_service_common.sh"
|
|
34
|
+
STACK_NAME="${target_stack}"
|
|
35
|
+
parse_common_args "${service_args[@]}"
|
|
36
|
+
configure_stack_runtime
|
|
37
|
+
local node_bin
|
|
38
|
+
node_bin="$(resolve_command_path node)"
|
|
39
|
+
if [[ -z "${node_bin}" ]]; then
|
|
40
|
+
echo "[oysterun-service] node not found; cannot print login QR" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
cd "${ROOT_DIR}"
|
|
44
|
+
OYSTERUN_CONFIG_DIR="${CONFIG_DIR}" \
|
|
45
|
+
OYSTERUN_PORT="${HOST_PORT}" \
|
|
46
|
+
"${node_bin}" host-service/setup.mjs --show-qr
|
|
47
|
+
) || {
|
|
48
|
+
echo "[oysterun-service] Login QR unavailable. Run 'oysterun show-qr' after setup." >&2
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
case "${COMMAND}" in
|
|
30
53
|
install)
|
|
31
54
|
exec "${SCRIPT_DIR}/install_oysterun_launch_agents.sh" --stack "${STACK_NAME}" "$@"
|
|
32
55
|
;;
|
|
33
56
|
start)
|
|
34
|
-
|
|
57
|
+
"${SCRIPT_DIR}/start_oysterun.sh" --stack "${STACK_NAME}" "$@"
|
|
58
|
+
print_login_qr_after_service_ready "$@"
|
|
35
59
|
;;
|
|
36
60
|
stop)
|
|
37
61
|
exec "${SCRIPT_DIR}/stop_oysterun.sh" --stack "${STACK_NAME}" "$@"
|
|
38
62
|
;;
|
|
39
63
|
restart)
|
|
40
|
-
|
|
64
|
+
"${SCRIPT_DIR}/restart_oysterun.sh" --stack "${STACK_NAME}" "$@"
|
|
65
|
+
print_login_qr_after_service_ready "$@"
|
|
41
66
|
;;
|
|
42
67
|
status)
|
|
43
68
|
exec "${SCRIPT_DIR}/status_oysterun_launch_agents.sh" --stack "${STACK_NAME}" "$@"
|