minecodex 0.1.2 → 0.1.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/README.md +5 -3
- package/features/images/codex-feature.json +1 -1
- package/features/model-slider/codex-feature.json +1 -1
- package/features/notes/codex-feature.json +1 -1
- package/package.json +1 -1
- package/packages/cli/src/commands.mjs +23 -27
- package/packages/cli/src/runtime-manager.mjs +4 -2
- package/packages/runtime-host/README.md +2 -3
- package/packages/runtime-host/src/codex-runtime.mjs +23 -101
- package/packages/runtime-host/src/main.mjs +2 -1
package/README.md
CHANGED
|
@@ -17,9 +17,11 @@ npm install -g minecodex
|
|
|
17
17
|
mcx install
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
`mcx install`
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
`mcx install` 不会关闭或启动 Codex,也不会改变当前窗口。安装完成后,终端会提示
|
|
21
|
+
MineCodex 将在下次重启 Codex 时启用,并询问是否现在重启;默认答案是 **No**。
|
|
22
|
+
只有用户明确确认,或之后主动运行 `mcx restart`,MineCodex 才会关闭并重新打开
|
|
23
|
+
Codex。安装后,macOS 的“登录项与扩展”中会显示名为 **MineCodex** 的后台项目;
|
|
24
|
+
这个后台项目负责插件与本地控制台,不会在用户退出 Codex 后自动将它重新打开。
|
|
23
25
|
|
|
24
26
|
安装后三个插件默认开启。打开本地控制台:
|
|
25
27
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, readFile, rm } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
3
4
|
import { assertNodeSupported, MacPlatformAdapter } from "./platform.mjs";
|
|
4
5
|
import { ensureConfig, ensureControlToken, readConfig } from "./config.mjs";
|
|
5
6
|
import { createControlServer } from "./control-server.mjs";
|
|
@@ -98,37 +99,32 @@ async function restoreService(platform, snapshot, { cliPath, paths }) {
|
|
|
98
99
|
return platform.restoreServiceState(snapshot, { cliPath, paths });
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
async function nativeSnapshot(platform) {
|
|
102
|
-
if (platform.snapshotNativeCodexState) return platform.snapshotNativeCodexState();
|
|
103
|
-
return { count: 0 };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function restoreNative(platform, snapshot) {
|
|
107
|
-
if (!snapshot?.count) return;
|
|
108
|
-
if (platform.restoreNativeCodexState) {
|
|
109
|
-
await platform.restoreNativeCodexState(snapshot);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
await platform.openNativeCodex?.();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
102
|
function appendRollbackError(error, rollbackError) {
|
|
116
103
|
if (!rollbackError) return error;
|
|
117
104
|
return new Error(`${error.message} Automatic rollback also failed: ${rollbackError.message}`, { cause: error });
|
|
118
105
|
}
|
|
119
106
|
|
|
120
|
-
async function
|
|
107
|
+
export async function confirmRestartNow({ input = process.stdin, terminalOutput = process.stdout } = {}) {
|
|
108
|
+
if (!input.isTTY || !terminalOutput.isTTY) return false;
|
|
109
|
+
const prompt = createInterface({ input, output: terminalOutput });
|
|
110
|
+
try {
|
|
111
|
+
const answer = await prompt.question("Restart Codex now? [y/N] ");
|
|
112
|
+
return /^(y|yes)$/i.test(answer.trim());
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
} finally {
|
|
116
|
+
prompt.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion = process.versions.node }) {
|
|
121
121
|
assertNodeSupported(nodeVersion);
|
|
122
122
|
platform.assertInstallSupported();
|
|
123
123
|
await platform.assertCodexInstalled();
|
|
124
124
|
const previousService = await serviceSnapshot(platform, paths);
|
|
125
|
-
const previousNative = await nativeSnapshot(platform);
|
|
126
125
|
await ensureConfig(paths.configPath);
|
|
127
126
|
await ensureControlToken(paths.tokenPath);
|
|
128
|
-
let nativeCount = 0;
|
|
129
127
|
try {
|
|
130
|
-
nativeCount = await platform.terminateNativeCodex();
|
|
131
|
-
if (nativeCount) output(`Restarting ${nativeCount} running Codex instance${nativeCount === 1 ? "" : "s"} with MineCodex enabled…`);
|
|
132
128
|
await platform.installService({ cliPath, paths, start: true });
|
|
133
129
|
const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
|
|
134
130
|
const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
|
|
@@ -143,15 +139,14 @@ async function installCommand({ paths, platform, cliPath, output, serviceProbe,
|
|
|
143
139
|
} catch (failure) {
|
|
144
140
|
rollbackError = failure;
|
|
145
141
|
}
|
|
146
|
-
try {
|
|
147
|
-
await restoreNative(platform, previousNative?.count ? previousNative : { count: nativeCount });
|
|
148
|
-
} catch (failure) {
|
|
149
|
-
rollbackError = rollbackError ?? failure;
|
|
150
|
-
}
|
|
151
142
|
throw appendRollbackError(error, rollbackError);
|
|
152
143
|
}
|
|
153
|
-
output("MineCodex installed.
|
|
154
|
-
|
|
144
|
+
output("MineCodex installed. It will be enabled the next time Codex restarts.");
|
|
145
|
+
if (await confirmRestart()) {
|
|
146
|
+
await restartCommand({ paths, output, fetchImpl });
|
|
147
|
+
} else {
|
|
148
|
+
output("Run `mcx restart` when you are ready.");
|
|
149
|
+
}
|
|
155
150
|
}
|
|
156
151
|
|
|
157
152
|
async function openCommand({ paths, platform, serviceProbe, fetchImpl }) {
|
|
@@ -377,12 +372,13 @@ export async function runCli(argv, {
|
|
|
377
372
|
npm = createNpmAdapter(),
|
|
378
373
|
serviceProbe,
|
|
379
374
|
fetchImpl = fetch,
|
|
375
|
+
confirmRestart = confirmRestartNow,
|
|
380
376
|
nodeVersion = process.versions.node,
|
|
381
377
|
} = {}) {
|
|
382
378
|
const [command = "--help", ...args] = argv;
|
|
383
379
|
if (["--help", "-h", "help"].includes(command)) return printHelp(output);
|
|
384
380
|
if (["--version", "-v"].includes(command)) return output(packageMetadata.version);
|
|
385
|
-
if (command === "install") return installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, nodeVersion });
|
|
381
|
+
if (command === "install") return installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion });
|
|
386
382
|
if (command === "open" || command === "gui") return openCommand({ paths, platform, serviceProbe, fetchImpl });
|
|
387
383
|
if (command === "status") return statusCommand({ paths, platform, jsonOutput: args.includes("--json"), output, fetchImpl });
|
|
388
384
|
if (command === "restart") return restartCommand({ paths, output, fetchImpl });
|
|
@@ -142,7 +142,7 @@ export class RuntimeManager {
|
|
|
142
142
|
return next;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
async startInternal() {
|
|
145
|
+
async startInternal({ launchCodex = false } = {}) {
|
|
146
146
|
if (this.child && !hasExited(this.child)) return this.status();
|
|
147
147
|
this.clearChildState();
|
|
148
148
|
this.stopping = false;
|
|
@@ -169,6 +169,7 @@ export class RuntimeManager {
|
|
|
169
169
|
MINECODEX_ENABLED_FEATURES: features.join(","),
|
|
170
170
|
MINECODEX_CODEX_PID_FILE: this.paths.codexPidPath,
|
|
171
171
|
MINECODEX_RUNTIME_READY_FILE: this.paths.runtimeReadyPath,
|
|
172
|
+
MINECODEX_LAUNCH_CODEX: launchCodex ? "1" : "0",
|
|
172
173
|
...(codexExecutable ? { MINECODEX_CODEX_EXECUTABLE: codexExecutable } : {}),
|
|
173
174
|
},
|
|
174
175
|
stdio: "inherit",
|
|
@@ -307,7 +308,8 @@ export class RuntimeManager {
|
|
|
307
308
|
const operation = this.enqueue(async () => {
|
|
308
309
|
await this.stopInternal();
|
|
309
310
|
await this.platform.terminateOwnedCodex(this.paths);
|
|
310
|
-
|
|
311
|
+
await this.platform.terminateNativeCodex?.();
|
|
312
|
+
return this.startInternal({ launchCodex: true });
|
|
311
313
|
});
|
|
312
314
|
this.restartInFlight = operation;
|
|
313
315
|
const shared = this.restartInFlight.finally(() => {
|
|
@@ -141,9 +141,8 @@ Host 会再次发送当前状态。功能页面可据此暂停隐藏状态下的
|
|
|
141
141
|
|
|
142
142
|
- 只在 top frame 安装 Runtime,绝不向业务 iframe 注入 binding token。
|
|
143
143
|
- Watch / refresh 串行;已连接 Renderer 不重复连接,关闭 target 会清理状态。
|
|
144
|
-
- 本地 Surface 注入前先启用 CSP bypass
|
|
145
|
-
|
|
146
|
-
替换旧 Runtime,不刷新当前 Codex 页面。
|
|
144
|
+
- 本地 Surface 注入前先启用 CSP bypass,并为后续 document 注册 document-start 脚本;
|
|
145
|
+
当前 Codex document 始终直接注入或替换 Runtime,绝不通过 CDP reload Codex 页面。
|
|
147
146
|
- 入口由 Renderer 内的 MutationObserver 幂等挂载,React 重绘不会产生重复入口。
|
|
148
147
|
- Summary 根据对话主区域宽度连续派生 `overlay / shift / gutter`:小于 1096px
|
|
149
148
|
使用临时 Popover;1096–1535px 预留 316px 并把对话内容左移 158px;更宽时
|
|
@@ -3570,10 +3570,6 @@ function createDocumentBootstrapSource(source) {
|
|
|
3570
3570
|
return `window[${JSON.stringify(CSP_BOOTSTRAP_KEY)}] = ${CSP_BOOTSTRAP_VERSION};\n${source}`;
|
|
3571
3571
|
}
|
|
3572
3572
|
|
|
3573
|
-
function documentBootstrapExpression() {
|
|
3574
|
-
return `window[${JSON.stringify(CSP_BOOTSTRAP_KEY)}] === ${CSP_BOOTSTRAP_VERSION}`;
|
|
3575
|
-
}
|
|
3576
|
-
|
|
3577
3573
|
async function connect(url) {
|
|
3578
3574
|
const socket = new WebSocket(url);
|
|
3579
3575
|
const pending = new Map();
|
|
@@ -3738,8 +3734,6 @@ export class CodexRuntime {
|
|
|
3738
3734
|
sleep = (timeoutMs) => new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
3739
3735
|
availabilityTimeoutMs = 20_000,
|
|
3740
3736
|
availabilityPollMs = 500,
|
|
3741
|
-
relaunchBaseDelayMs = 250,
|
|
3742
|
-
relaunchMaxDelayMs = 2_000,
|
|
3743
3737
|
monitorIntervalMs = 2_000,
|
|
3744
3738
|
runtimeSessionId = randomBytes(16).toString("hex"),
|
|
3745
3739
|
onStatusChange = null,
|
|
@@ -3756,8 +3750,6 @@ export class CodexRuntime {
|
|
|
3756
3750
|
this.sleep = sleep;
|
|
3757
3751
|
this.availabilityTimeoutMs = availabilityTimeoutMs;
|
|
3758
3752
|
this.availabilityPollMs = availabilityPollMs;
|
|
3759
|
-
this.relaunchBaseDelayMs = relaunchBaseDelayMs;
|
|
3760
|
-
this.relaunchMaxDelayMs = relaunchMaxDelayMs;
|
|
3761
3753
|
this.monitorIntervalMs = monitorIntervalMs;
|
|
3762
3754
|
this.runtimeSessionId = runtimeSessionId;
|
|
3763
3755
|
this.onStatusChange = onStatusChange;
|
|
@@ -3774,7 +3766,6 @@ export class CodexRuntime {
|
|
|
3774
3766
|
this.targetRefreshQueued = false;
|
|
3775
3767
|
this.appPid = null;
|
|
3776
3768
|
this.managedCodexChild = null;
|
|
3777
|
-
this.recoveryPromise = null;
|
|
3778
3769
|
this.monitorTimer = null;
|
|
3779
3770
|
this.monitorWake = null;
|
|
3780
3771
|
this.stopPromise = null;
|
|
@@ -3782,20 +3773,35 @@ export class CodexRuntime {
|
|
|
3782
3773
|
this.pidChangeQueue = null;
|
|
3783
3774
|
this.rendererDiscovered = false;
|
|
3784
3775
|
this.lastRendererStatus = null;
|
|
3785
|
-
this.codexLifecycleManaged = false;
|
|
3786
3776
|
}
|
|
3787
3777
|
|
|
3788
|
-
async start() {
|
|
3778
|
+
async start({ launchCodex = false } = {}) {
|
|
3789
3779
|
this.stopping = false;
|
|
3790
3780
|
await mkdir(this.profileDir, { recursive: true });
|
|
3791
|
-
|
|
3792
|
-
if (!
|
|
3793
|
-
this.launchManagedCodex();
|
|
3794
|
-
|
|
3781
|
+
const available = await this.isAvailable();
|
|
3782
|
+
if (!available && launchCodex) {
|
|
3783
|
+
const child = this.launchManagedCodex();
|
|
3784
|
+
try {
|
|
3785
|
+
await this.waitUntilAvailable();
|
|
3786
|
+
} catch (error) {
|
|
3787
|
+
if (this.managedCodexChild === child && !processChildHasExited(child)) {
|
|
3788
|
+
child.kill?.("SIGTERM");
|
|
3789
|
+
const exited = await waitForProcessChildExit(child, 500);
|
|
3790
|
+
if (!exited && !processChildHasExited(child)) {
|
|
3791
|
+
child.kill?.("SIGKILL");
|
|
3792
|
+
await waitForProcessChildExit(child, 500);
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
throw error;
|
|
3796
|
+
}
|
|
3795
3797
|
}
|
|
3796
3798
|
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
+
if (available || launchCodex) {
|
|
3800
|
+
await this.startTargetDiscovery();
|
|
3801
|
+
await this.refresh();
|
|
3802
|
+
} else {
|
|
3803
|
+
this.notifyStatusChange();
|
|
3804
|
+
}
|
|
3799
3805
|
await this.flushManagedCodexPidChange();
|
|
3800
3806
|
this.monitorPromise = this.monitor();
|
|
3801
3807
|
}
|
|
@@ -3869,14 +3875,10 @@ export class CodexRuntime {
|
|
|
3869
3875
|
|
|
3870
3876
|
markCodexUnavailable(error) {
|
|
3871
3877
|
this.clearRendererState();
|
|
3872
|
-
if (this.codexLifecycleManaged && !this.stopping) {
|
|
3873
|
-
this.scheduleCodexRecovery();
|
|
3874
|
-
}
|
|
3875
3878
|
return error;
|
|
3876
3879
|
}
|
|
3877
3880
|
|
|
3878
3881
|
launchManagedCodex() {
|
|
3879
|
-
this.codexLifecycleManaged = true;
|
|
3880
3882
|
const child = this.spawnProcess(
|
|
3881
3883
|
this.appPath,
|
|
3882
3884
|
[
|
|
@@ -3901,7 +3903,6 @@ export class CodexRuntime {
|
|
|
3901
3903
|
this.notifyManagedCodexPidChange(null);
|
|
3902
3904
|
if (this.stopping) return;
|
|
3903
3905
|
this.clearRendererState();
|
|
3904
|
-
this.scheduleCodexRecovery();
|
|
3905
3906
|
};
|
|
3906
3907
|
child.once?.("exit", onExit);
|
|
3907
3908
|
child.once?.("error", (error) => {
|
|
@@ -3912,73 +3913,6 @@ export class CodexRuntime {
|
|
|
3912
3913
|
return child;
|
|
3913
3914
|
}
|
|
3914
3915
|
|
|
3915
|
-
scheduleCodexRecovery() {
|
|
3916
|
-
if (this.stopping || this.recoveryPromise) return this.recoveryPromise;
|
|
3917
|
-
const recovery = this.recoverManagedCodex();
|
|
3918
|
-
let trackedRecovery;
|
|
3919
|
-
trackedRecovery = recovery.finally(() => {
|
|
3920
|
-
if (this.recoveryPromise === trackedRecovery) this.recoveryPromise = null;
|
|
3921
|
-
});
|
|
3922
|
-
this.recoveryPromise = trackedRecovery;
|
|
3923
|
-
return trackedRecovery;
|
|
3924
|
-
}
|
|
3925
|
-
|
|
3926
|
-
async recoverManagedCodex() {
|
|
3927
|
-
let attempt = 0;
|
|
3928
|
-
while (!this.stopping) {
|
|
3929
|
-
if (this.managedCodexChild && !processChildHasExited(this.managedCodexChild)) {
|
|
3930
|
-
await this.sleep(Math.max(1, Math.min(this.relaunchMaxDelayMs, this.relaunchBaseDelayMs || 1)));
|
|
3931
|
-
continue;
|
|
3932
|
-
}
|
|
3933
|
-
if (await this.isAvailable()) {
|
|
3934
|
-
try {
|
|
3935
|
-
await this.startTargetDiscovery();
|
|
3936
|
-
await this.refresh();
|
|
3937
|
-
await this.flushManagedCodexPidChange();
|
|
3938
|
-
return;
|
|
3939
|
-
} catch (error) {
|
|
3940
|
-
if (this.stopping) return;
|
|
3941
|
-
if (await this.isAvailable()) {
|
|
3942
|
-
this.logger.warn?.("Managed Codex CDP is still available; retrying discovery", error.message);
|
|
3943
|
-
await this.sleep(Math.max(1, Math.min(this.relaunchMaxDelayMs, this.relaunchBaseDelayMs || 1)));
|
|
3944
|
-
continue;
|
|
3945
|
-
}
|
|
3946
|
-
}
|
|
3947
|
-
}
|
|
3948
|
-
const delay = Math.min(
|
|
3949
|
-
this.relaunchBaseDelayMs * (2 ** attempt),
|
|
3950
|
-
this.relaunchMaxDelayMs,
|
|
3951
|
-
);
|
|
3952
|
-
if (delay > 0) await this.sleep(delay);
|
|
3953
|
-
if (this.stopping) return;
|
|
3954
|
-
let child;
|
|
3955
|
-
try {
|
|
3956
|
-
child = this.launchManagedCodex();
|
|
3957
|
-
await this.waitUntilAvailable();
|
|
3958
|
-
await this.startTargetDiscovery();
|
|
3959
|
-
await this.refresh();
|
|
3960
|
-
await this.flushManagedCodexPidChange();
|
|
3961
|
-
return;
|
|
3962
|
-
} catch (error) {
|
|
3963
|
-
this.logger.warn?.("Managed Codex recovery retry failed", error.message);
|
|
3964
|
-
if (
|
|
3965
|
-
child
|
|
3966
|
-
&& this.managedCodexChild === child
|
|
3967
|
-
&& child.exitCode === null
|
|
3968
|
-
&& child.signalCode === null
|
|
3969
|
-
) {
|
|
3970
|
-
child.kill?.("SIGTERM");
|
|
3971
|
-
const exited = await waitForProcessChildExit(child, 500);
|
|
3972
|
-
if (!exited && !processChildHasExited(child)) {
|
|
3973
|
-
child.kill?.("SIGKILL");
|
|
3974
|
-
await waitForProcessChildExit(child, 500);
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
attempt += 1;
|
|
3978
|
-
}
|
|
3979
|
-
}
|
|
3980
|
-
}
|
|
3981
|
-
|
|
3982
3916
|
queueTargetRefresh() {
|
|
3983
3917
|
if (this.targetRefreshQueued || this.stopping) return;
|
|
3984
3918
|
this.targetRefreshQueued = true;
|
|
@@ -4260,20 +4194,9 @@ export class CodexRuntime {
|
|
|
4260
4194
|
bindingToken,
|
|
4261
4195
|
runtimeSessionId: this.runtimeSessionId,
|
|
4262
4196
|
});
|
|
4263
|
-
const bootstrapExpression = documentBootstrapExpression();
|
|
4264
4197
|
const script = await client.send("Page.addScriptToEvaluateOnNewDocument", {
|
|
4265
4198
|
source: createDocumentBootstrapSource(source),
|
|
4266
4199
|
});
|
|
4267
|
-
const documentWasBootstrapped = Boolean(evaluationValue(await client.send("Runtime.evaluate", {
|
|
4268
|
-
expression: bootstrapExpression,
|
|
4269
|
-
returnByValue: true,
|
|
4270
|
-
})));
|
|
4271
|
-
if (!documentWasBootstrapped) {
|
|
4272
|
-
const pageLoaded = client.waitFor("Page.loadEventFired", 20_000);
|
|
4273
|
-
await client.send("Page.reload");
|
|
4274
|
-
await pageLoaded;
|
|
4275
|
-
await waitForExpression(client, bootstrapExpression);
|
|
4276
|
-
}
|
|
4277
4200
|
await waitForExpression(
|
|
4278
4201
|
client,
|
|
4279
4202
|
`document.readyState === "interactive" || document.readyState === "complete"`,
|
|
@@ -4303,7 +4226,6 @@ export class CodexRuntime {
|
|
|
4303
4226
|
this.stopping = true;
|
|
4304
4227
|
this.targetRefreshQueued = false;
|
|
4305
4228
|
this.monitorWake?.();
|
|
4306
|
-
await this.recoveryPromise?.catch(() => {});
|
|
4307
4229
|
await this.monitorPromise?.catch(() => {});
|
|
4308
4230
|
await Promise.all(Array.from(this.clients, async ([targetId, client]) => {
|
|
4309
4231
|
const identifier = this.scriptIds.get(targetId);
|
|
@@ -17,6 +17,7 @@ const featuresRoot = process.env.CODEX_FEATURES_ROOT ?? path.dirname(runtimeRoot
|
|
|
17
17
|
const legacyDataDir = process.env.CODEX_IMAGE_HOST_DATA_DIR ?? path.join(os.homedir(), ".codex-image-host");
|
|
18
18
|
const profileDir = process.env.CODEX_RUNTIME_PROFILE_DIR ?? path.join(legacyDataDir, "codex-profile");
|
|
19
19
|
const cdpPort = Number(process.env.CODEX_RUNTIME_CDP_PORT ?? 9231);
|
|
20
|
+
const launchCodex = process.env.MINECODEX_LAUNCH_CODEX === "1";
|
|
20
21
|
|
|
21
22
|
const discoveredFeatures = await discoverFeatures(featuresRoot);
|
|
22
23
|
if (!discoveredFeatures.length) throw new Error(`No codex-feature.json files found under ${featuresRoot}`);
|
|
@@ -70,7 +71,7 @@ runtime = new CodexRuntime({
|
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
try {
|
|
73
|
-
await runtime.start();
|
|
74
|
+
await runtime.start({ launchCodex });
|
|
74
75
|
runtimeReady = true;
|
|
75
76
|
await writeReadyFile();
|
|
76
77
|
} catch (error) {
|