@opentig/cli 0.2.3 → 0.2.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/LICENSE +1 -1
- package/README.md +14 -14
- package/dist/bin.mjs +25 -10
- package/dist/chunks/{cli-L6lvCZdX.mjs → cli-gyjD1t0O.mjs} +611 -268
- package/dist/chunks/network-settings-CdFeKFRB.mjs +142 -0
- package/dist/chunks/{server-Cz0gvydn.mjs → server-DMk8-Xxt.mjs} +557 -105
- package/dist/chunks/{service-manager-Jyx-MWo-.mjs → service-manager-BxSmV_8Z.mjs} +88 -14
- package/dist/client/assets/App-Bfw_TX8q.js +132 -0
- package/dist/client/assets/CreatePullRequestDialog-DyPd6SKH.js +2 -0
- package/dist/client/assets/DiffWorkspace-DCo89dF1.js +7 -0
- package/dist/client/assets/EditableFileViewer-Dotntfij.js +1 -0
- package/dist/client/assets/EditableFileViewer-svutjHZv.css +1 -0
- package/dist/client/assets/ExternalLink-jdtpN_yL.js +1 -0
- package/dist/client/assets/{File-BPxtFxHC.js → File-DHnVtIhS.js} +1 -1
- package/dist/client/assets/FilesView-D6f1hPVy.js +1 -0
- package/dist/client/assets/GitHubAvatar-B6vyoVlh.js +1 -0
- package/dist/client/assets/HistoryView-D9RhzCPY.js +1 -0
- package/dist/client/assets/HtmlFileViewer-Bwmj5umg.js +2 -0
- package/dist/client/assets/{IconAlertTriangle-C1OX2bTH.js → IconAlertTriangle-B9iDJKvP.js} +1 -1
- package/dist/client/assets/IconCheck-C-G-IA13.js +1 -0
- package/dist/client/assets/IconChevronDown-DU5i40lD.js +1 -0
- package/dist/client/assets/{IconEdit-BP5Emwzw.js → IconEdit-CZe-EJxX.js} +1 -1
- package/dist/client/assets/{IconFolderSymlink-BNWI0eXB.js → IconFolderSymlink-DkLEuUb6.js} +1 -1
- package/dist/client/assets/IconGitMerge-D9KEj1cE.js +1 -0
- package/dist/client/assets/IconGitPullRequest-ClaUqyVa.js +1 -0
- package/dist/client/assets/{IconLoader4-Bk6G4Bls.js → IconLoader4-D2VN7u1V.js} +1 -1
- package/dist/client/assets/{IconPlus-DUFNSK_i.js → IconPlus-DJCoD3wt.js} +1 -1
- package/dist/client/assets/{IconSearch-ZL3YVslZ.js → IconSearch-1VQ4P4kW.js} +1 -1
- package/dist/client/assets/{IconSelector-BGwY84R8.js → IconSelector-BVMU1PEt.js} +1 -1
- package/dist/client/assets/ImageFileViewer-3izbFVTw.css +1 -0
- package/dist/client/assets/ImageFileViewer-CdFLl_in.js +1 -0
- package/dist/client/assets/LinkedMarkdown-Bnc2OA63.js +1 -0
- package/dist/client/assets/LinkedText-BD6K3d1t.js +1 -0
- package/dist/client/assets/LocalRefsDialog-Bl-fddap.js +1 -0
- package/dist/client/assets/MarkdownFileViewer-NUAoQmv5.js +2 -0
- package/dist/client/assets/PairingPage-CqrxpC3_.js +1 -0
- package/dist/client/assets/PierreDiffViewer-CDfQ32e0.js +1 -0
- package/dist/client/assets/{PierreEditBoundary-Bt0q7SXc.js → PierreEditBoundary-DAdYNYZ3.js} +1 -1
- package/dist/client/assets/{PierreWorkerPool-DMFigp0u.js → PierreWorkerPool-mIDzTMB9.js} +3 -3
- package/dist/client/assets/PullRequestDiff-n1NZFHKN.js +1 -0
- package/dist/client/assets/PullRequestViewer-u89zl169.js +2 -0
- package/dist/client/assets/PullRequestsView-D6BqvB4E.js +1 -0
- package/dist/client/assets/QuickOpenDialog-BbEifGRi.js +1 -0
- package/dist/client/assets/RepositoryProjectsDialog-EGAsXL-Z.js +1 -0
- package/dist/client/assets/{SearchView-BYz1K1Ta.js → SearchView-BMz7qOdc.js} +1 -1
- package/dist/client/assets/SettingsDialog-DmD7HvhX.js +9 -0
- package/dist/client/assets/Viewer-CDj_vo6w.js +3 -0
- package/dist/client/assets/{abnfDiagram-YUKMZFIV-CpKAN_8n.js → abnfDiagram-YUKMZFIV-It6tW-4v.js} +1 -1
- package/dist/client/assets/{architecture-WOLXFQ4H-DrizVopY.js → architecture-WOLXFQ4H-CaOaChND.js} +1 -1
- package/dist/client/assets/{architectureDiagram-47P4ROYG-gvUijB3H.js → architectureDiagram-47P4ROYG-COtncecA.js} +1 -1
- package/dist/client/assets/{blockDiagram-E7TT5TSB-CdcXmnf_.js → blockDiagram-E7TT5TSB-NU4wFrfP.js} +1 -1
- package/dist/client/assets/{c4Diagram-CRA5TL53-DtoD1jKt.js → c4Diagram-CRA5TL53-0xzM4S8q.js} +1 -1
- package/dist/client/assets/channel-QED-4ACa.js +1 -0
- package/dist/client/assets/checkbox-Dh4T0bCe.js +1 -0
- package/dist/client/assets/{chunk-2BW5OAIV-Bxqf2dFQ.js → chunk-2BW5OAIV-BQMxlSNI.js} +1 -1
- package/dist/client/assets/{chunk-3YJQHVM4-BhzXJiq0.js → chunk-3YJQHVM4-CdP15KtA.js} +1 -1
- package/dist/client/assets/{chunk-4EA7E6EY-CvEILf8c.js → chunk-4EA7E6EY-xRPCCZLU.js} +1 -1
- package/dist/client/assets/{chunk-7TKQ45FW-pTEdzVz3.js → chunk-7TKQ45FW-Cr_UAObV.js} +1 -1
- package/dist/client/assets/{chunk-AW2ZBBNX-LVROkcXk.js → chunk-AW2ZBBNX-DBqqxF2h.js} +1 -1
- package/dist/client/assets/{chunk-DBDB3WZW-FCGrB-q8.js → chunk-DBDB3WZW-BonEIRP2.js} +1 -1
- package/dist/client/assets/{chunk-E2ZNV5FY-B3rGXPn-.js → chunk-E2ZNV5FY-DnKvxSiA.js} +1 -1
- package/dist/client/assets/{chunk-EU5HNXII-D6a_Ly_W.js → chunk-EU5HNXII-DLK0K3sR.js} +1 -1
- package/dist/client/assets/{chunk-FVRAUYC3-B5hy0Ket.js → chunk-FVRAUYC3-DmjKoYAh.js} +1 -1
- package/dist/client/assets/{chunk-HJ2JQQFS-CKHFz1Wl.js → chunk-HJ2JQQFS-AF4phse0.js} +1 -1
- package/dist/client/assets/{chunk-HTAEGDNF-CtwDuO27.js → chunk-HTAEGDNF-CG4pBnUZ.js} +1 -1
- package/dist/client/assets/{chunk-KQW6MTUR-BN3uxLYf.js → chunk-KQW6MTUR-Dd87yj5r.js} +1 -1
- package/dist/client/assets/{chunk-NTY3LDVX-CfXSvyIC.js → chunk-NTY3LDVX-OolU2PaP.js} +1 -1
- package/dist/client/assets/{chunk-VPRB5NB3-LAggcSji.js → chunk-VPRB5NB3-Dnr3edxV.js} +3 -3
- package/dist/client/assets/{chunk-XC4XBNZT-BksIPWQU.js → chunk-XC4XBNZT-CIqGRxkD.js} +1 -1
- package/dist/client/assets/{classDiagram-v2-K4WV4PDN-DH78Maqm.js → classDiagram-v2-K4WV4PDN-CPGawm8n.js} +1 -1
- package/dist/client/assets/createReactComponent-Ctub7J3a.js +1 -0
- package/dist/client/assets/{cynefin-EF2NZ3EQ-DozKlDV1.js → cynefin-EF2NZ3EQ-OA2R9WCH.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-3GCD6N5R-A1yS7NKN.js → cynefinDiagram-3GCD6N5R-DqCVtoKt.js} +1 -1
- package/dist/client/assets/{dagre-W4DXFKR2-DRJ1mmbe.js → dagre-W4DXFKR2-kc3-tkAQ.js} +1 -1
- package/dist/client/assets/{diagram-2UJZ2QOL-B1sI2b2o.js → diagram-2UJZ2QOL-DoBZKT3s.js} +1 -1
- package/dist/client/assets/{diagram-OVF4WLC6-DLE1o4-P.js → diagram-OVF4WLC6-DGkyha_F.js} +1 -1
- package/dist/client/assets/{diagram-PFPMY2P6-UtIEsv14.js → diagram-PFPMY2P6-DpRDDgdV.js} +1 -1
- package/dist/client/assets/{diagram-UMYRVEAY-BwTyzVap.js → diagram-UMYRVEAY-DlsfhYv3.js} +1 -1
- package/dist/client/assets/{diagram-YEKJPTXX-DNPUQqu0.js → diagram-YEKJPTXX-BIhRZzBM.js} +1 -1
- package/dist/client/assets/{diagram-ZIFT7M5P-ClXykQ3I.js → diagram-ZIFT7M5P-mweJcgBZ.js} +1 -1
- package/dist/client/assets/diff-parse.worker-C1IFRYY9.js +13 -0
- package/dist/client/assets/{ebnfDiagram-VR2GEFS7-Dm8VAviu.js → ebnfDiagram-VR2GEFS7-Dn5zyIbZ.js} +1 -1
- package/dist/client/assets/{elk-IJKZMXRS-CEEoPARn.js → elk-IJKZMXRS-eBPlf1-o.js} +1 -1
- package/dist/client/assets/{erDiagram-O2IAPWRE-CaDE4obX.js → erDiagram-O2IAPWRE-WEE519yZ.js} +1 -1
- package/dist/client/assets/{eventmodeling-K75KTNOO-PjYgMkPC.js → eventmodeling-K75KTNOO-BgE_VmNy.js} +1 -1
- package/dist/client/assets/external-url-IUS7RmYD.js +1 -0
- package/dist/client/assets/flowDiagram-OXPTDLAJ-CHQYfkMg.js +1 -0
- package/dist/client/assets/{ganttDiagram-R7TSEDQI-CmkBarBi.js → ganttDiagram-R7TSEDQI-D_1KyJZU.js} +1 -1
- package/dist/client/assets/{gitGraph-VSP46ZUC-DeotTdHj.js → gitGraph-VSP46ZUC-CEbT7DA4.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-XJZIOB7I-uV1hPaZT.js → gitGraphDiagram-XJZIOB7I-QQyYc1gM.js} +1 -1
- package/dist/client/assets/index-DsKvWsTs.js +47 -0
- package/dist/client/assets/index-kyWCnbkb.css +2 -0
- package/dist/client/assets/{info-OHQRW6UA-BQDsGvSl.js → info-OHQRW6UA-ChHAnveo.js} +1 -1
- package/dist/client/assets/{infoDiagram-5W2HQ5XZ-CDfEPsgZ.js → infoDiagram-5W2HQ5XZ-BaxAE7UA.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-K3B6WC7H-Bb7PQGe6.js → ishikawaDiagram-K3B6WC7H-vhUflBE_.js} +1 -1
- package/dist/client/assets/{journeyDiagram-COXZFDF6-BDythglw.js → journeyDiagram-COXZFDF6-C8WWosVa.js} +1 -1
- package/dist/client/assets/{kanban-definition-P3RFRI5V-CvY5kXhh.js → kanban-definition-P3RFRI5V-CU6W9Yi3.js} +1 -1
- package/dist/client/assets/katex-CQfTPFh0.js +1 -0
- package/dist/client/assets/{line-99p2yFnB.js → line-Axq4vVX-.js} +1 -1
- package/dist/client/assets/{mermaid-parser.core-BjZYtoJ8.js → mermaid-parser.core-DNeaCX12.js} +3 -3
- package/dist/client/assets/{mermaid.core-DRcXnUox.js → mermaid.core-Dtt7rXw0.js} +4 -4
- package/dist/client/assets/{mindmap-definition-LZFPQGKD-t9LoCpnW.js → mindmap-definition-LZFPQGKD-D_8ZqQot.js} +1 -1
- package/dist/client/assets/{packet-JDAUHWVQ-BlR-5V7K.js → packet-JDAUHWVQ-6z7ujmGF.js} +1 -1
- package/dist/client/assets/{pegDiagram-BGZESJAR-CWeGI-T9.js → pegDiagram-BGZESJAR-3ujyk-bt.js} +1 -1
- package/dist/client/assets/{pie-XZMESJXO-BkvYHWgl.js → pie-XZMESJXO-DvpLy2DB.js} +1 -1
- package/dist/client/assets/{pieDiagram-CAPJLFHJ-CY5PPIr3.js → pieDiagram-CAPJLFHJ-BPMPWyc7.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-GDMTTRAM-kqj-z271.js → quadrantDiagram-GDMTTRAM-BVUCR_lf.js} +1 -1
- package/dist/client/assets/{radar-ABXABTNO-DuEtYatv.js → radar-ABXABTNO-lu14X99R.js} +1 -1
- package/dist/client/assets/{railroad-I3PHUGI6-CVlnj2j9.js → railroad-I3PHUGI6-DI_WtDmt.js} +1 -1
- package/dist/client/assets/{railroad-abnf-LNEFI6M7-9Qiczw4d.js → railroad-abnf-LNEFI6M7-CmWAKZ2T.js} +1 -1
- package/dist/client/assets/{railroad-ebnf-DWYD2UWJ-BNL7TMEg.js → railroad-ebnf-DWYD2UWJ-BWKFU_UH.js} +1 -1
- package/dist/client/assets/{railroad-peg-JR7BVNR7-DdpRxgk6.js → railroad-peg-JR7BVNR7-9wkgzxMB.js} +1 -1
- package/dist/client/assets/{railroadDiagram-SM67HX2B-CDXvYNqW.js → railroadDiagram-SM67HX2B-4JnRbxl_.js} +1 -1
- package/dist/client/assets/{render-markdown-DF9Fy7MD.js → render-markdown-CQi2yKYQ.js} +2 -2
- package/dist/client/assets/{requirementDiagram-X7JNWC4A-BDdhqnZA.js → requirementDiagram-X7JNWC4A-CVJiZZsi.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-UM26HJRW-Rkyk5oEf.js → sankeyDiagram-UM26HJRW-BmlWQdkv.js} +1 -1
- package/dist/client/assets/select-Dq3KDX2O.js +1 -0
- package/dist/client/assets/{sequenceDiagram-ZO4K6R2Y-Coj9ap7h.js → sequenceDiagram-ZO4K6R2Y-zUx3LmPj.js} +1 -1
- package/dist/client/assets/{stateDiagram-v2-TBUQTH76-BucWlahR.js → stateDiagram-v2-TBUQTH76-o_ci-2bu.js} +1 -1
- package/dist/client/assets/{swimlanes-N4OXWK64-tEECcFP9.js → swimlanes-N4OXWK64-BlI0GC5-.js} +1 -1
- package/dist/client/assets/swimlanesDiagram-K3J5GTZL-BEeJLN8d.js +8 -0
- package/dist/client/assets/tanstack-CVAt_k_T.js +1 -0
- package/dist/client/assets/{timeline-definition-YOQAKGHF-XvIbGO4d.js → timeline-definition-YOQAKGHF-DLnvF__o.js} +1 -1
- package/dist/client/assets/{treeView-D4JQ5SDB-DuhJpfXe.js → treeView-D4JQ5SDB-DPuMLQhB.js} +1 -1
- package/dist/client/assets/{treemap-SAJKECNS-CLiUWfZh.js → treemap-SAJKECNS-DjxqxJ8-.js} +1 -1
- package/dist/client/assets/{usecaseDiagram-VIAY4XPW-BuwkpV9w.js → usecaseDiagram-VIAY4XPW-BtnFNltN.js} +1 -1
- package/dist/client/assets/utils-D0T3A-kS.js +1 -0
- package/dist/client/assets/{vennDiagram-BLWOH2XV-kfza5Rxq.js → vennDiagram-BLWOH2XV-CAiF2FKz.js} +1 -1
- package/dist/client/assets/viewer-tabs-DkkqN9MD.js +1 -0
- package/dist/client/assets/{wardley-7MLQ67FV-CcbU_Ykg.js → wardley-7MLQ67FV-DAWySamq.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YMQ3BMBF-DnxlEg3a.js → wardleyDiagram-YMQ3BMBF-DiBCclEL.js} +1 -1
- package/dist/client/assets/{xychartDiagram-TAQBALBS-BzgtwtvK.js → xychartDiagram-TAQBALBS-B0NgfXz6.js} +1 -1
- package/dist/client/index.html +6 -4
- package/dist/dev.mjs +6 -6
- package/dist/manifest.json +1 -1
- package/dist/server.mjs +1 -1
- package/dist/service-update.mjs +11 -3
- package/dist/utility.mjs +25 -4
- package/package.json +4 -4
- package/dist/client/assets/App-DEIZhWJG.js +0 -132
- package/dist/client/assets/CreatePullRequestDialog-BceOUbYK.js +0 -2
- package/dist/client/assets/DiffWorkspace-BMtV4MMc.js +0 -6
- package/dist/client/assets/EditableFileViewer-B44TSuFu.css +0 -1
- package/dist/client/assets/EditableFileViewer-BE3AsnVa.js +0 -1
- package/dist/client/assets/FilesView-DoVhRfi1.js +0 -1
- package/dist/client/assets/GitHubAvatar-RiePq1Yf.js +0 -1
- package/dist/client/assets/HistoryView-Dl8uqmKe.js +0 -1
- package/dist/client/assets/HtmlFileViewer-xfjY1Swc.js +0 -2
- package/dist/client/assets/IconCheck-BPhxw_LS.js +0 -1
- package/dist/client/assets/IconChevronDown-C3FplJL8.js +0 -1
- package/dist/client/assets/ImageFileViewer-BJBOmpsf.css +0 -1
- package/dist/client/assets/ImageFileViewer-DXaIo7_d.js +0 -1
- package/dist/client/assets/LocalRefsDialog-C5J3kgTe.js +0 -1
- package/dist/client/assets/MarkdownFileViewer-18mvm0f2.js +0 -2
- package/dist/client/assets/PairingPage-BEUj3t0V.js +0 -1
- package/dist/client/assets/PierreDiffViewer-p29h7lW_.js +0 -1
- package/dist/client/assets/PullRequestDiff-C17JA4ko.js +0 -1
- package/dist/client/assets/PullRequestViewer-DaMtkESD.js +0 -2
- package/dist/client/assets/PullRequestsView-44f03VLC.js +0 -1
- package/dist/client/assets/QuickOpenDialog-Bi-L68PU.js +0 -1
- package/dist/client/assets/RepositoryProjectsDialog-DFGn74fe.js +0 -1
- package/dist/client/assets/SettingsDialog-CruoIiyN.js +0 -9
- package/dist/client/assets/Viewer-3WKpwRzo.js +0 -3
- package/dist/client/assets/channel-BqKQxg-1.js +0 -1
- package/dist/client/assets/checkbox-MDl2t9_3.js +0 -1
- package/dist/client/assets/createReactComponent-BfrD_oQK.js +0 -1
- package/dist/client/assets/flowDiagram-OXPTDLAJ-DSokwYKA.js +0 -1
- package/dist/client/assets/index-CoZCJqTQ.js +0 -47
- package/dist/client/assets/index-DUERTTRS.css +0 -2
- package/dist/client/assets/katex-C6e6EqXB.js +0 -1
- package/dist/client/assets/select-pZpCy7qn.js +0 -1
- package/dist/client/assets/swimlanesDiagram-K3J5GTZL-DEFKR6Vf.js +0 -8
- package/dist/client/assets/tanstack-CYi5knVJ.js +0 -1
- package/dist/client/assets/viewer-tabs-MMNrJqGE.js +0 -1
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { i as applicationName, n as runOpenTigServer, o as normalizeRuntimePlatform } from "./server-DMk8-Xxt.mjs";
|
|
2
|
+
import { c as redactSensitiveText, i as loadStartupNetwork, s as writeFileAtomically } from "./network-settings-CdFeKFRB.mjs";
|
|
3
|
+
import { a as newerVersion, c as saveInstallation, d as cliCommand, f as diagnostic, h as serviceError, i as execute, l as stableVersion, m as operationError, n as createServiceManager, o as packageDirectory, p as errorCode, r as atomicWrite, s as readInstallation, t as assertManager, u as CliError } from "./service-manager-BxSmV_8Z.mjs";
|
|
3
4
|
import process$1 from "node:process";
|
|
4
|
-
import { execFile, spawn } from "node:child_process";
|
|
5
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
6
5
|
import { access, appendFile, chmod, cp, mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
6
|
import path from "node:path";
|
|
7
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { execFile, spawn } from "node:child_process";
|
|
10
11
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
12
|
+
import { createServer } from "node:net";
|
|
13
|
+
import { lookup } from "node:dns/promises";
|
|
11
14
|
//#region ../../src/shared/date-format.ts
|
|
12
15
|
var pad = (value, width = 2) => String(value).padStart(width, "0");
|
|
13
16
|
function localDate(value) {
|
|
@@ -908,6 +911,354 @@ function renderUnicodeCompact(data, options = {}) {
|
|
|
908
911
|
return lines.join("\n");
|
|
909
912
|
}
|
|
910
913
|
//#endregion
|
|
914
|
+
//#region src/cli-network.ts
|
|
915
|
+
async function cliNetwork(config) {
|
|
916
|
+
try {
|
|
917
|
+
const { network, savedListener } = await loadStartupNetwork({
|
|
918
|
+
settingsPath: path.join(config.home, "settings.json"),
|
|
919
|
+
serverDataPath: path.join(config.home, "server"),
|
|
920
|
+
mode: "web-access",
|
|
921
|
+
host: config.host,
|
|
922
|
+
port: config.port
|
|
923
|
+
});
|
|
924
|
+
return {
|
|
925
|
+
...network,
|
|
926
|
+
savedListener
|
|
927
|
+
};
|
|
928
|
+
} catch (cause) {
|
|
929
|
+
throw new CliError("ACCESS_SETTINGS_INVALID", "Could not load the saved web access settings.", [
|
|
930
|
+
`Check ${path.join(config.home, "server/web-access.json")} and its permissions.`,
|
|
931
|
+
`To restore startup address and browser access, run ${cliCommand(config.home, "reset-access")} and restart this instance.`,
|
|
932
|
+
"Resetting access preserves paired browsers but removes the saved listener and access switches."
|
|
933
|
+
], { cause });
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function listenerError(error, config, network, command) {
|
|
937
|
+
const code = errorCode(error);
|
|
938
|
+
const address = `${network.listenerHost.includes(":") ? `[${network.listenerHost}]` : network.listenerHost}:${network.listenerPort}`;
|
|
939
|
+
const context = network.savedListener || network.listenerHost !== config.host || network.listenerPort !== config.port ? [
|
|
940
|
+
"This address is affected by saved Settings → Web access. Saved listener fields override the corresponding startup options.",
|
|
941
|
+
"Change the saved listener in that instance before retrying. If access recovery is needed:",
|
|
942
|
+
`${cliCommand(config.home, "reset-access")} removes saved access switches and listener; restart with the desired options.`
|
|
943
|
+
] : [
|
|
944
|
+
"Listener selected by startup options, OPENTIG_HOST/OPENTIG_PORT, or defaults.",
|
|
945
|
+
`Choose another port, for example: ${cliCommand(config.home, ...command.split(" "), "--host", config.host, "--port", String(network.listenerPort === 65535 ? 6767 : network.listenerPort + 1))}`,
|
|
946
|
+
"The example port has not been checked for availability."
|
|
947
|
+
];
|
|
948
|
+
if (code === "EADDRINUSE") return new CliError("PORT_IN_USE", `Port ${network.listenerPort} is already in use (${address}); cannot run ${command}.`, [
|
|
949
|
+
"OpenTig Desktop or another process may be using this address. Close it and retry, or use a different port.",
|
|
950
|
+
...context,
|
|
951
|
+
"No other application was stopped."
|
|
952
|
+
], { cause: error });
|
|
953
|
+
if (code === "EADDRNOTAVAIL" || code === "ENOTFOUND" || code === "EAI_AGAIN") return new CliError("HOST_UNAVAILABLE", `Cannot listen on ${address}: this host is not available on this machine.`, ["Use a local listening address, such as 127.0.0.1 for access from this computer.", ...context], { cause: error });
|
|
954
|
+
if (code === "EACCES" || code === "EPERM") return new CliError("LISTENER_PERMISSION_DENIED", `Permission denied when listening on ${address}.`, ["Check OS port restrictions and choose an allowed port. Do not run the server as administrator.", ...context], { cause: error });
|
|
955
|
+
return error;
|
|
956
|
+
}
|
|
957
|
+
/** Same bind semantics as OpenTigServer; advisory only, the real bind still handles races. */
|
|
958
|
+
async function assertListenerAvailable(config, network, command) {
|
|
959
|
+
const socket = createServer();
|
|
960
|
+
try {
|
|
961
|
+
await new Promise((resolve, reject) => {
|
|
962
|
+
socket.once("error", reject);
|
|
963
|
+
socket.listen(network.listenerPort, network.listenerHost, resolve);
|
|
964
|
+
});
|
|
965
|
+
} catch (error) {
|
|
966
|
+
throw listenerError(error, config, network, command);
|
|
967
|
+
} finally {
|
|
968
|
+
if (socket.listening) await new Promise((resolve, reject) => socket.close((error) => error ? reject(error) : resolve()));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
async function sameListener(network, runtime) {
|
|
972
|
+
if (network.listenerPort !== runtime.port) return false;
|
|
973
|
+
if (network.listenerHost === runtime.host) return true;
|
|
974
|
+
try {
|
|
975
|
+
return (await lookup(network.listenerHost)).address === runtime.host;
|
|
976
|
+
} catch {
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
//#endregion
|
|
981
|
+
//#region src/cli-home.ts
|
|
982
|
+
var RUNTIME_STATE_VERSION = 1;
|
|
983
|
+
function resolveCliPaths(home) {
|
|
984
|
+
const resolved = path.resolve(home);
|
|
985
|
+
const serverData = path.join(resolved, "server");
|
|
986
|
+
const logs = path.join(resolved, "logs");
|
|
987
|
+
return {
|
|
988
|
+
home: resolved,
|
|
989
|
+
settings: path.join(resolved, "settings.json"),
|
|
990
|
+
aiLog: path.join(resolved, "ai-log.jsonl"),
|
|
991
|
+
problems: path.join(resolved, "problems.jsonl"),
|
|
992
|
+
serverData,
|
|
993
|
+
logs,
|
|
994
|
+
serverLog: path.join(logs, "server.log"),
|
|
995
|
+
runtimeState: path.join(resolved, "runtime.json"),
|
|
996
|
+
adminToken: path.join(serverData, "admin-token")
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
async function prepareCliHome(paths) {
|
|
1000
|
+
await mkdir(paths.home, {
|
|
1001
|
+
recursive: true,
|
|
1002
|
+
mode: 448
|
|
1003
|
+
});
|
|
1004
|
+
await chmod(paths.home, 448);
|
|
1005
|
+
await Promise.all([mkdir(paths.serverData, {
|
|
1006
|
+
recursive: true,
|
|
1007
|
+
mode: 448
|
|
1008
|
+
}), mkdir(paths.logs, {
|
|
1009
|
+
recursive: true,
|
|
1010
|
+
mode: 448
|
|
1011
|
+
})]);
|
|
1012
|
+
await Promise.all([chmod(paths.serverData, 448), chmod(paths.logs, 448)]);
|
|
1013
|
+
}
|
|
1014
|
+
async function loadOrCreateAdminToken(filePath) {
|
|
1015
|
+
try {
|
|
1016
|
+
return await readAdminToken(filePath);
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
if (error.code !== "ENOENT") throw error;
|
|
1019
|
+
}
|
|
1020
|
+
const token = randomBytes(32).toString("base64url");
|
|
1021
|
+
await writeFileAtomically(filePath, `${token}\n`, { directoryMode: 448 });
|
|
1022
|
+
return token;
|
|
1023
|
+
}
|
|
1024
|
+
async function readAdminToken(filePath) {
|
|
1025
|
+
const token = (await readFile(filePath, "utf8")).trim();
|
|
1026
|
+
if (!isToken(token)) throw new CliError("ADMIN_CREDENTIAL_INVALID", "OpenTig local admin credential is corrupt.", ["Check the private home permissions and restore this credential from a trusted backup; do not delete the home or copy a credential from another instance."]);
|
|
1027
|
+
await chmod(filePath, 384);
|
|
1028
|
+
return token;
|
|
1029
|
+
}
|
|
1030
|
+
async function writeRuntimeState(filePath, state) {
|
|
1031
|
+
await writeFileAtomically(filePath, `${JSON.stringify({
|
|
1032
|
+
version: RUNTIME_STATE_VERSION,
|
|
1033
|
+
...state
|
|
1034
|
+
}, null, 2)}\n`, {
|
|
1035
|
+
parseJson: true,
|
|
1036
|
+
directoryMode: 448
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
async function readRuntimeState(filePath) {
|
|
1040
|
+
let value;
|
|
1041
|
+
try {
|
|
1042
|
+
value = JSON.parse(await readFile(filePath, "utf8"));
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
if (error.code === "ENOENT") throw new CliError("RUNTIME_MISSING", "No running OpenTig server was found for this home.", [`Start it with ${cliCommand(path.dirname(filePath), "serve")}`, `Or check ${cliCommand(path.dirname(filePath), "service", "status")}`], { cause: error });
|
|
1045
|
+
throw new CliError("RUNTIME_INVALID", "OpenTig runtime state is unreadable.", [`Check permissions and server logs in ${path.dirname(filePath)}. Do not delete settings or credentials.`], { cause: error });
|
|
1046
|
+
}
|
|
1047
|
+
if (!isRuntimeState(value)) throw new CliError("RUNTIME_INVALID", "OpenTig runtime state is invalid.", [`Inspect ${filePath} and the service status before restarting; do not delete the home.`]);
|
|
1048
|
+
await chmod(filePath, 384);
|
|
1049
|
+
return value;
|
|
1050
|
+
}
|
|
1051
|
+
async function clearRuntimeState(filePath, instanceId) {
|
|
1052
|
+
try {
|
|
1053
|
+
if ((await readRuntimeState(filePath)).instanceId === instanceId) await rm(filePath, { force: true });
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
if (!(error instanceof CliError && error.code === "RUNTIME_MISSING")) throw error;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function newInstanceId() {
|
|
1059
|
+
return randomBytes(24).toString("base64url");
|
|
1060
|
+
}
|
|
1061
|
+
function isToken(value) {
|
|
1062
|
+
return /^[A-Za-z0-9_-]{43}$/.test(value);
|
|
1063
|
+
}
|
|
1064
|
+
function isRuntimeState(value) {
|
|
1065
|
+
const state = value;
|
|
1066
|
+
return Boolean(state && state.version === RUNTIME_STATE_VERSION && Number.isSafeInteger(state.pid) && Number(state.pid) > 0 && typeof state.host === "string" && state.host.length > 0 && state.host.length <= 255 && Number.isSafeInteger(state.port) && Number(state.port) > 0 && Number(state.port) <= 65535 && Number.isSafeInteger(state.protocolVersion) && Number(state.protocolVersion) > 0 && typeof state.appVersion === "string" && state.appVersion.length > 0 && state.appVersion.length <= 128 && typeof state.instanceId === "string" && /^[A-Za-z0-9_-]{32}$/.test(state.instanceId) && typeof state.startedAt === "string" && !Number.isNaN(Date.parse(state.startedAt)));
|
|
1067
|
+
}
|
|
1068
|
+
//#endregion
|
|
1069
|
+
//#region src/cli-runtime.ts
|
|
1070
|
+
function publicOrigin(host, port) {
|
|
1071
|
+
const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host === "::" ? "::1" : host;
|
|
1072
|
+
return `http://${displayHost.includes(":") ? `[${displayHost}]` : displayHost}:${port}`;
|
|
1073
|
+
}
|
|
1074
|
+
function isProcessRunning(pid) {
|
|
1075
|
+
try {
|
|
1076
|
+
process.kill(pid, 0);
|
|
1077
|
+
return true;
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
return error.code === "EPERM";
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/** The admin endpoint proves the listener owns both this home's token and instance ID. */
|
|
1083
|
+
async function probeRuntime(home, runtime, allowLegacy = false) {
|
|
1084
|
+
const paths = resolveCliPaths(home);
|
|
1085
|
+
const state = runtime ?? await readRuntimeState(paths.runtimeState);
|
|
1086
|
+
if (!isProcessRunning(state.pid)) throw new CliError("RUNTIME_STALE", "OpenTig runtime state is stale; its process is not running.", [`Start this instance: ${cliCommand(home, "serve")}`, `Or restart its service: ${cliCommand(home, "service", "restart")}`]);
|
|
1087
|
+
const token = await readAdminToken(paths.adminToken).catch((cause) => {
|
|
1088
|
+
throw new CliError("ADMIN_CREDENTIAL_INVALID", "OpenTig local admin credential is missing or unreadable.", [`Check the private credential and permissions in ${paths.serverData}; do not delete the home or copy another instance's credential.`], { cause });
|
|
1089
|
+
});
|
|
1090
|
+
const response = await fetch(`${publicOrigin(state.host, state.port)}/api/admin/status`, {
|
|
1091
|
+
method: "POST",
|
|
1092
|
+
headers: {
|
|
1093
|
+
"Content-Type": "application/json",
|
|
1094
|
+
"X-OpenTig-Admin": token
|
|
1095
|
+
},
|
|
1096
|
+
body: JSON.stringify({ instanceId: state.instanceId }),
|
|
1097
|
+
signal: AbortSignal.timeout(1e3)
|
|
1098
|
+
});
|
|
1099
|
+
if (response.status === 404 && allowLegacy) {
|
|
1100
|
+
const check = await fetch(`${publicOrigin(state.host, state.port)}/api/admin/pair`, {
|
|
1101
|
+
method: "POST",
|
|
1102
|
+
headers: {
|
|
1103
|
+
"Content-Type": "application/json",
|
|
1104
|
+
"X-OpenTig-Admin": token
|
|
1105
|
+
},
|
|
1106
|
+
body: JSON.stringify({
|
|
1107
|
+
instanceId: state.instanceId,
|
|
1108
|
+
publicOrigin: "invalid-origin"
|
|
1109
|
+
}),
|
|
1110
|
+
signal: AbortSignal.timeout(1e3)
|
|
1111
|
+
});
|
|
1112
|
+
if (check.status === 400 && (await check.json()).error === "Invalid public origin.") {
|
|
1113
|
+
const ready = await fetch(`${publicOrigin(state.host, state.port)}/readyz`, { signal: AbortSignal.timeout(1e3) });
|
|
1114
|
+
const identity = await ready.json();
|
|
1115
|
+
if (ready.ok && identity.status === "ready" && identity.appVersion === state.appVersion && identity.protocolVersion === state.protocolVersion) return {
|
|
1116
|
+
...state,
|
|
1117
|
+
legacy: true
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (!response.ok) throw new CliError("RUNTIME_MISMATCH", "OpenTig runtime state does not match the running server.", [`Inspect this instance: ${cliCommand(home, "service", "status")}`, "Do not stop an unknown process or remove credentials to work around an identity mismatch."]);
|
|
1122
|
+
const identity = await response.json();
|
|
1123
|
+
if (identity.instanceId !== state.instanceId || identity.pid !== state.pid || identity.appVersion !== state.appVersion || identity.protocolVersion !== state.protocolVersion || identity.status !== "ready") throw new CliError("RUNTIME_MISMATCH", "The running server identity or readiness does not match this home.", [`Check ${path.join(home, "logs/server.log")} before restarting this instance.`]);
|
|
1124
|
+
return state;
|
|
1125
|
+
}
|
|
1126
|
+
function assertGitAvailable() {
|
|
1127
|
+
return new Promise((resolve, reject) => {
|
|
1128
|
+
execFile("git", ["--version"], {
|
|
1129
|
+
timeout: 1e4,
|
|
1130
|
+
windowsHide: true
|
|
1131
|
+
}, (error, stdout) => {
|
|
1132
|
+
if (error || !stdout.trim().startsWith("git version ")) reject(new CliError("GIT_UNAVAILABLE", "Git is required and could not be executed from PATH.", ["Install Git or correct PATH in this terminal, then retry.", "If tool locations changed, reinstall the service from this terminal to capture the updated PATH."], { cause: error }));
|
|
1133
|
+
else resolve();
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
//#endregion
|
|
1138
|
+
//#region src/cli-log.ts
|
|
1139
|
+
var MAX_BYTES = 1048576;
|
|
1140
|
+
var BACKUPS = 3;
|
|
1141
|
+
var CliServerLog = class {
|
|
1142
|
+
filePath;
|
|
1143
|
+
queue = Promise.resolve();
|
|
1144
|
+
constructor(filePath) {
|
|
1145
|
+
this.filePath = filePath;
|
|
1146
|
+
}
|
|
1147
|
+
logger = (level, message) => {
|
|
1148
|
+
const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${redactSensitiveText(message)}\n`;
|
|
1149
|
+
this.queue = this.queue.then(async () => {
|
|
1150
|
+
await mkdir(path.dirname(this.filePath), {
|
|
1151
|
+
recursive: true,
|
|
1152
|
+
mode: 448
|
|
1153
|
+
});
|
|
1154
|
+
if (await wouldExceed(this.filePath, Buffer.byteLength(line))) await this.rotate();
|
|
1155
|
+
await appendFile(this.filePath, line, {
|
|
1156
|
+
encoding: "utf8",
|
|
1157
|
+
mode: 384
|
|
1158
|
+
});
|
|
1159
|
+
}).catch(() => void 0);
|
|
1160
|
+
};
|
|
1161
|
+
close() {
|
|
1162
|
+
return this.queue;
|
|
1163
|
+
}
|
|
1164
|
+
async rotate() {
|
|
1165
|
+
await rm(`${this.filePath}.${BACKUPS}`, { force: true });
|
|
1166
|
+
for (let index = 2; index >= 1; index -= 1) await moveIfPresent(`${this.filePath}.${index}`, `${this.filePath}.${index + 1}`);
|
|
1167
|
+
await moveIfPresent(this.filePath, `${this.filePath}.1`);
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
async function wouldExceed(filePath, incomingBytes) {
|
|
1171
|
+
try {
|
|
1172
|
+
return (await stat(filePath)).size + incomingBytes > MAX_BYTES;
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
if (error.code === "ENOENT") return false;
|
|
1175
|
+
throw error;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
async function moveIfPresent(source, target) {
|
|
1179
|
+
try {
|
|
1180
|
+
await rename(source, target);
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
if (error.code !== "ENOENT") throw error;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
//#endregion
|
|
1186
|
+
//#region src/cli-startup.ts
|
|
1187
|
+
var file = (home) => path.join(home, "service/startup.json");
|
|
1188
|
+
async function readStartup(home) {
|
|
1189
|
+
try {
|
|
1190
|
+
const value = JSON.parse(await readFile(file(home), "utf8"));
|
|
1191
|
+
return value && typeof value.attempt === "string" && typeof value.requestedAt === "string" && [
|
|
1192
|
+
"pending",
|
|
1193
|
+
"starting",
|
|
1194
|
+
"ready",
|
|
1195
|
+
"failed"
|
|
1196
|
+
].includes(value.phase) ? value : null;
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
if (errorCode(error) === "ENOENT" || error instanceof SyntaxError) return null;
|
|
1199
|
+
throw error;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
/** One private bounded record, replaced for each installation/restart attempt. */
|
|
1203
|
+
async function requestStartup(home) {
|
|
1204
|
+
const attempt = randomUUID();
|
|
1205
|
+
await atomicWrite(file(home), JSON.stringify({
|
|
1206
|
+
attempt,
|
|
1207
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1208
|
+
phase: "pending"
|
|
1209
|
+
}));
|
|
1210
|
+
return attempt;
|
|
1211
|
+
}
|
|
1212
|
+
async function startupReporter(home) {
|
|
1213
|
+
if (!process.env.OPENTIG_SERVICE_HOME || path.resolve(process.env.OPENTIG_SERVICE_HOME) !== path.resolve(home)) return null;
|
|
1214
|
+
const previous = await readStartup(home);
|
|
1215
|
+
const result = previous?.phase === "pending" ? previous : {
|
|
1216
|
+
attempt: randomUUID(),
|
|
1217
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1218
|
+
phase: "pending"
|
|
1219
|
+
};
|
|
1220
|
+
const save = async (patch, initial = false) => {
|
|
1221
|
+
const current = await readStartup(home);
|
|
1222
|
+
if (current && current.attempt !== result.attempt && (!initial || current.attempt !== previous?.attempt)) return;
|
|
1223
|
+
await atomicWrite(file(home), JSON.stringify({
|
|
1224
|
+
...result,
|
|
1225
|
+
...patch,
|
|
1226
|
+
pid: process.pid
|
|
1227
|
+
}));
|
|
1228
|
+
};
|
|
1229
|
+
await save({ phase: "starting" }, true);
|
|
1230
|
+
return {
|
|
1231
|
+
ready: (instanceId) => save({
|
|
1232
|
+
phase: "ready",
|
|
1233
|
+
instanceId
|
|
1234
|
+
}),
|
|
1235
|
+
failed: (error) => save({
|
|
1236
|
+
phase: "failed",
|
|
1237
|
+
code: error instanceof CliError ? error.code : "STARTUP_FAILED",
|
|
1238
|
+
message: diagnostic(error)
|
|
1239
|
+
})
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
async function recordCliFailure(home, error) {
|
|
1243
|
+
const log = new CliServerLog(path.join(home, "logs/server.log"));
|
|
1244
|
+
log.logger("error", diagnostic(error));
|
|
1245
|
+
let cause = error instanceof Error ? error.cause : void 0;
|
|
1246
|
+
for (let depth = 0; cause && depth < 4; depth++) {
|
|
1247
|
+
log.logger("error", `Cause: ${cause instanceof Error ? cause.message : "Unknown failure"}`);
|
|
1248
|
+
if (cause instanceof AggregateError) for (const item of cause.errors) log.logger("error", `Recovery detail: ${diagnostic(item)}`);
|
|
1249
|
+
cause = cause instanceof Error ? cause.cause : void 0;
|
|
1250
|
+
}
|
|
1251
|
+
await log.close();
|
|
1252
|
+
}
|
|
1253
|
+
function startupTimeout(home) {
|
|
1254
|
+
return new CliError("STARTUP_TIMEOUT", "The managed service did not become ready before the startup timeout.", [
|
|
1255
|
+
"The cause could not be determined automatically.",
|
|
1256
|
+
`Check: ${cliCommand(home, "service", "status")}`,
|
|
1257
|
+
`Server log: ${path.join(home, "logs/server.log")}`,
|
|
1258
|
+
`Windows startup output: ${path.join(home, "logs/service-stderr.log")}; on Linux/macOS inspect the user supervisor's logs.`
|
|
1259
|
+
]);
|
|
1260
|
+
}
|
|
1261
|
+
//#endregion
|
|
911
1262
|
//#region src/browser.ts
|
|
912
1263
|
function browserCommand(url, platform = process.platform) {
|
|
913
1264
|
if (platform === "win32") return {
|
|
@@ -981,8 +1332,13 @@ function parseCliArguments(args, environment = process.env, currentDirectory = p
|
|
|
981
1332
|
}
|
|
982
1333
|
if (!optionsEnded && argument.startsWith("--")) {
|
|
983
1334
|
const [name, inline] = splitOption(argument);
|
|
1335
|
+
if (![
|
|
1336
|
+
"--host",
|
|
1337
|
+
"--port",
|
|
1338
|
+
"--home"
|
|
1339
|
+
].includes(name)) throw new CliUsageError(`Unknown option: ${name}. Use --host, --port, or --home.`);
|
|
984
1340
|
const value = inline ?? args[++index];
|
|
985
|
-
if (value === void 0 || value.startsWith("
|
|
1341
|
+
if (value === void 0 || value === "" || value.startsWith("-")) throw new CliUsageError(`Missing value for ${name}.`);
|
|
986
1342
|
if (name === "--host") hostValue = value;
|
|
987
1343
|
else if (name === "--port") portValue = value;
|
|
988
1344
|
else if (name === "--home") homeValue = value;
|
|
@@ -999,14 +1355,16 @@ function parseCliArguments(args, environment = process.env, currentDirectory = p
|
|
|
999
1355
|
serviceAction = argument;
|
|
1000
1356
|
continue;
|
|
1001
1357
|
}
|
|
1002
|
-
throw new CliUsageError(`
|
|
1358
|
+
if (command === "service" && serviceAction === null) throw new CliUsageError(`Unknown service action: ${argument}. Use install, status, restart, or uninstall.`);
|
|
1359
|
+
throw new CliUsageError(`Unexpected argument: ${argument}. Check the command spelling with opentig --help. Add and select repositories from the OpenTig web UI.`);
|
|
1003
1360
|
}
|
|
1004
|
-
if (command
|
|
1361
|
+
if (noBrowser && command !== "start") throw new CliUsageError("--no-browser is only supported by start; serve and services already run without opening a browser.");
|
|
1362
|
+
if ((command === "pair" || command === "reset-access") && (hostValue !== void 0 || portValue !== void 0 || noBrowser)) throw new CliUsageError(`The ${command} command only accepts --home.`);
|
|
1005
1363
|
if (command === "service" && serviceAction === null) throw new CliUsageError("The service command requires install, status, restart, or uninstall.");
|
|
1006
1364
|
if (command === "service" && serviceAction !== "install" && (hostValue !== void 0 || portValue !== void 0 || noBrowser)) throw new CliUsageError(`The service ${serviceAction} command only accepts --home.`);
|
|
1007
|
-
if (command === "pair") return {
|
|
1365
|
+
if (command === "pair" || command === "reset-access" || command === "service" && serviceAction !== "install") return {
|
|
1008
1366
|
command,
|
|
1009
|
-
serviceAction
|
|
1367
|
+
serviceAction,
|
|
1010
1368
|
host: DEFAULT_SERVER_HOST,
|
|
1011
1369
|
port: DEFAULT_SERVER_PORT,
|
|
1012
1370
|
home: resolvePath(homeValue ?? environment.OPENTIG_HOME ?? path.join(homeDirectory, ".opentig"), currentDirectory, "home"),
|
|
@@ -1033,6 +1391,7 @@ Usage:
|
|
|
1033
1391
|
opentig start [options]
|
|
1034
1392
|
opentig serve [options]
|
|
1035
1393
|
opentig pair [--home <path>]
|
|
1394
|
+
opentig reset-access [--home <path>]
|
|
1036
1395
|
opentig service <install|status|restart|uninstall> [options]
|
|
1037
1396
|
opentig help
|
|
1038
1397
|
opentig version
|
|
@@ -1041,6 +1400,7 @@ Commands:
|
|
|
1041
1400
|
start Start OpenTig and open the one-use pairing link (default)
|
|
1042
1401
|
serve Start OpenTig without opening a browser; ideal for servers
|
|
1043
1402
|
pair Print a fresh five-minute code/link for a running server
|
|
1403
|
+
reset-access Restore browser access and startup listener settings on next restart
|
|
1044
1404
|
service Manage a background server (Linux, macOS, Windows)
|
|
1045
1405
|
help Show this help
|
|
1046
1406
|
version Show the OpenTig version
|
|
@@ -1073,8 +1433,13 @@ Pairing and remote access:
|
|
|
1073
1433
|
a trusted LAN/VPN; pairing is still required.
|
|
1074
1434
|
|
|
1075
1435
|
Runtime behavior:
|
|
1436
|
+
serve runs in this terminal and stops when it closes. service install starts
|
|
1437
|
+
a background server and installs startup persistence; do not run both for
|
|
1438
|
+
the same home. Desktop runs its own server and needs a different port.
|
|
1076
1439
|
The configured port is never changed silently. If it is occupied, OpenTig
|
|
1077
|
-
exits and asks you to stop that process or choose another --port.
|
|
1440
|
+
exits and asks you to stop that process or choose another --port. Saved
|
|
1441
|
+
Settings → Web access take precedence over startup host/port options.
|
|
1442
|
+
Service installation checks the effective address before registering a service. SIGINT and
|
|
1078
1443
|
SIGTERM shut the server down cleanly. npx and bunx never install a service;
|
|
1079
1444
|
service installation is explicit. Linux uses systemd with lingering; macOS
|
|
1080
1445
|
and Windows start at login and stop at logout.
|
|
@@ -1097,7 +1462,7 @@ function splitOption(argument) {
|
|
|
1097
1462
|
return separator < 0 ? [argument, void 0] : [argument.slice(0, separator), argument.slice(separator + 1)];
|
|
1098
1463
|
}
|
|
1099
1464
|
function isCommand(value) {
|
|
1100
|
-
return value === "start" || value === "serve" || value === "pair" || value === "service" || value === "help" || value === "version";
|
|
1465
|
+
return value === "start" || value === "serve" || value === "pair" || value === "reset-access" || value === "service" || value === "help" || value === "version";
|
|
1101
1466
|
}
|
|
1102
1467
|
function isServiceAction(value) {
|
|
1103
1468
|
return value === "install" || value === "status" || value === "restart" || value === "uninstall";
|
|
@@ -1114,15 +1479,15 @@ function validatePort(value) {
|
|
|
1114
1479
|
return port;
|
|
1115
1480
|
}
|
|
1116
1481
|
function resolvePath(value, currentDirectory, label) {
|
|
1117
|
-
if (!value ||
|
|
1482
|
+
if (!value || /[\0\r\n]/.test(value)) throw new CliUsageError(`Invalid ${label}.`);
|
|
1118
1483
|
return path.resolve(currentDirectory, value);
|
|
1119
1484
|
}
|
|
1120
1485
|
//#endregion
|
|
1121
1486
|
//#region src/version.ts
|
|
1122
1487
|
var OPEN_TIG_APP_VERSION = {
|
|
1123
1488
|
name: "@opentig/cli",
|
|
1124
|
-
version: "0.2.
|
|
1125
|
-
description: "Run
|
|
1489
|
+
version: "0.2.4",
|
|
1490
|
+
description: "Run OpenTig, a free, open-source Git client, on your repository host and access it through a paired browser.",
|
|
1126
1491
|
type: "module",
|
|
1127
1492
|
main: "dist/server.mjs",
|
|
1128
1493
|
bin: { "opentig": "dist/bin.mjs" },
|
|
@@ -1154,7 +1519,7 @@ var OPEN_TIG_APP_VERSION = {
|
|
|
1154
1519
|
"cli",
|
|
1155
1520
|
"headless"
|
|
1156
1521
|
],
|
|
1157
|
-
author: "OpenTig
|
|
1522
|
+
author: "OpenTig",
|
|
1158
1523
|
license: "MIT",
|
|
1159
1524
|
repository: {
|
|
1160
1525
|
"type": "git",
|
|
@@ -1162,7 +1527,7 @@ var OPEN_TIG_APP_VERSION = {
|
|
|
1162
1527
|
"directory": "packages/server"
|
|
1163
1528
|
},
|
|
1164
1529
|
bugs: { "url": "https://github.com/jorgefl8/opentig/issues" },
|
|
1165
|
-
homepage: "https://
|
|
1530
|
+
homepage: "https://opentig.com/",
|
|
1166
1531
|
publishConfig: {
|
|
1167
1532
|
"access": "public",
|
|
1168
1533
|
"provenance": true
|
|
@@ -1176,6 +1541,31 @@ var OPEN_TIG_APP_VERSION = {
|
|
|
1176
1541
|
//#region src/service-npm.ts
|
|
1177
1542
|
/** Run npm's JS entry with Node, never a .cmd file or interpolated shell command. */
|
|
1178
1543
|
async function runServiceNpm(args, cwd) {
|
|
1544
|
+
const entry = await resolveServiceNpm();
|
|
1545
|
+
try {
|
|
1546
|
+
await execute(process.execPath, [entry, ...args], {
|
|
1547
|
+
cwd,
|
|
1548
|
+
timeout: 18e4,
|
|
1549
|
+
maxBuffer: 2e6,
|
|
1550
|
+
windowsHide: true
|
|
1551
|
+
});
|
|
1552
|
+
} catch (cause) {
|
|
1553
|
+
const code = (cause && typeof cause === "object" && "stderr" in cause && typeof cause.stderr === "string" ? cause.stderr : "").match(/npm (?:error|ERR!) code (E[A-Z0-9]+)/)?.[1];
|
|
1554
|
+
const action = code && [
|
|
1555
|
+
"ENOTFOUND",
|
|
1556
|
+
"EAI_AGAIN",
|
|
1557
|
+
"ETIMEDOUT",
|
|
1558
|
+
"ECONNREFUSED",
|
|
1559
|
+
"ECONNRESET"
|
|
1560
|
+
].includes(code) ? "npm could not reach the package registry. Check the network and npm proxy/registry configuration." : code === "E401" || code === "E403" ? "npm registry access was denied. Check npm authentication and registry policies." : code === "ENOSPC" ? "npm ran out of disk space. Free space on the service volume and npm cache volume." : code === "EACCES" || code === "EPERM" ? "npm could not write the dependencies. Check permissions for this user and the npm cache." : "Check npm registry connectivity, proxy configuration, available disk space, and permissions.";
|
|
1561
|
+
throw new CliError("DEPENDENCIES_FAILED", `Could not prepare the managed service dependencies with npm${code ? ` (${code})` : ""}.`, [
|
|
1562
|
+
action,
|
|
1563
|
+
"The existing service has not been replaced.",
|
|
1564
|
+
"Correct the npm error recorded in the server log, then retry the installation."
|
|
1565
|
+
], { cause });
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
async function resolveServiceNpm() {
|
|
1179
1569
|
const candidates = [
|
|
1180
1570
|
process.env.npm_execpath,
|
|
1181
1571
|
path.join(path.dirname(process.execPath), "node_modules/npm/bin/npm-cli.js"),
|
|
@@ -1195,37 +1585,47 @@ async function runServiceNpm(args, cwd) {
|
|
|
1195
1585
|
} catch {
|
|
1196
1586
|
continue;
|
|
1197
1587
|
}
|
|
1198
|
-
|
|
1199
|
-
cwd,
|
|
1200
|
-
timeout: 18e4,
|
|
1201
|
-
maxBuffer: 2e6,
|
|
1202
|
-
windowsHide: true
|
|
1203
|
-
});
|
|
1204
|
-
return;
|
|
1588
|
+
return candidate;
|
|
1205
1589
|
}
|
|
1206
|
-
throw new
|
|
1590
|
+
throw new CliError("NPM_UNAVAILABLE", "npm could not be located.", ["Install Node.js with npm and run service install from that terminal."]);
|
|
1207
1591
|
}
|
|
1208
1592
|
//#endregion
|
|
1209
1593
|
//#region src/cli-service.ts
|
|
1210
1594
|
async function manageCliService(config, io, managerOverride) {
|
|
1211
|
-
|
|
1595
|
+
try {
|
|
1596
|
+
return await manage(config, io, managerOverride ?? await createServiceManager(config.home));
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
throw serviceError(error, config.home, config.serviceAction ?? "manage");
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
async function manage(config, io, manager) {
|
|
1212
1602
|
const action = config.serviceAction;
|
|
1213
|
-
if (!action) throw new
|
|
1603
|
+
if (!action) throw new CliError("SERVICE_ACTION_REQUIRED", "A service action is required.", ["Use install, status, restart, or uninstall."]);
|
|
1214
1604
|
const installed = await readInstallation(config.home);
|
|
1215
|
-
if (action !== "status" && installed && newerVersion(installed.version, OPEN_TIG_APP_VERSION)) throw new
|
|
1605
|
+
if (action !== "status" && installed && newerVersion(installed.version, OPEN_TIG_APP_VERSION)) throw new CliError("SERVICE_VERSION_NEWER", "A newer managed service is installed; downgrades are not automatic.", ["Update the global CLI to that version or later before administering this service.", `Check versions: ${cliCommand(config.home, "--version")}`]);
|
|
1216
1606
|
const definition = await manager.read();
|
|
1217
|
-
if (definition && !manager.owns(definition)) throw new
|
|
1607
|
+
if (definition && !manager.owns(definition)) throw new CliError("SERVICE_HOME_MISMATCH", "Another OpenTig home owns this service.", ["Use the original installation's --home for status, restart, or uninstall. No service was changed.", `Service definition: ${manager.location}`]);
|
|
1218
1608
|
if (installed) assertManager(installed, manager.kind);
|
|
1219
1609
|
if (action === "status") {
|
|
1220
1610
|
if (!definition) {
|
|
1221
1611
|
io.out("OpenTig service is not installed.");
|
|
1612
|
+
io.out(`Install: ${cliCommand(config.home, "service", "install")}`);
|
|
1222
1613
|
return 1;
|
|
1223
1614
|
}
|
|
1224
1615
|
const state = await manager.status();
|
|
1225
|
-
|
|
1616
|
+
let status = state.starting ? "starting" : state.running ? "running" : "stopped";
|
|
1617
|
+
let ready = false;
|
|
1618
|
+
if (state.running) try {
|
|
1619
|
+
await verifiedServiceRuntime(config.home, manager);
|
|
1620
|
+
ready = true;
|
|
1621
|
+
} catch {
|
|
1622
|
+
status = "running, backend not ready or identity not verified";
|
|
1623
|
+
}
|
|
1624
|
+
io.out(`OpenTig service: ${status} (${state.enabled ? "enabled" : "disabled"}).`);
|
|
1226
1625
|
io.out(manager.location);
|
|
1227
1626
|
io.out(manager.startup);
|
|
1228
|
-
|
|
1627
|
+
if (!ready) io.out(`Inspect ${path.join(config.home, "logs/server.log")}; restart with ${cliCommand(config.home, "service", "restart")}`);
|
|
1628
|
+
return ready && state.enabled ? 0 : 1;
|
|
1229
1629
|
}
|
|
1230
1630
|
if (action === "uninstall" && !definition && !installed) {
|
|
1231
1631
|
io.out("OpenTig service is not installed.");
|
|
@@ -1238,16 +1638,18 @@ async function manageCliService(config, io, managerOverride) {
|
|
|
1238
1638
|
const lock = path.join(config.home, "service/update-lock");
|
|
1239
1639
|
try {
|
|
1240
1640
|
await mkdir(lock, { mode: 448 });
|
|
1241
|
-
} catch (
|
|
1242
|
-
if (
|
|
1243
|
-
throw
|
|
1641
|
+
} catch (cause) {
|
|
1642
|
+
if (errorCode(cause) === "EEXIST") throw new CliError("SERVICE_BUSY", "A service installation or update is already in progress.", [`Check ${cliCommand(config.home, "service", "status")} and wait for the operation to finish.`, "Do not delete its lock until you have verified that no installation or update is active."], { cause });
|
|
1643
|
+
throw cause;
|
|
1244
1644
|
}
|
|
1245
|
-
|
|
1645
|
+
return withServiceLock(config.home, lock, async () => {
|
|
1246
1646
|
if (action === "restart") {
|
|
1247
|
-
if (!definition || !installed) throw new
|
|
1647
|
+
if (!definition || !installed) throw new CliError("SERVICE_MISSING", "OpenTig service is not installed.", [`Install: ${cliCommand(config.home, "service", "install")}`]);
|
|
1648
|
+
const attempt = await requestStartup(config.home);
|
|
1649
|
+
io.out("Restarting the OpenTig service...");
|
|
1248
1650
|
await manager.restart();
|
|
1249
|
-
await waitServiceReady(config.home, installed, manager);
|
|
1250
|
-
io.out(
|
|
1651
|
+
const runtime = await waitServiceReady(config.home, installed, manager, attempt);
|
|
1652
|
+
io.out(`OpenTig service restarted. Connection URL: ${publicOrigin(runtime.host, runtime.port)}`);
|
|
1251
1653
|
return 0;
|
|
1252
1654
|
}
|
|
1253
1655
|
if (action === "uninstall") {
|
|
@@ -1262,14 +1664,53 @@ async function manageCliService(config, io, managerOverride) {
|
|
|
1262
1664
|
io.out("OpenTig service removed. Repository settings and paired sessions were kept.");
|
|
1263
1665
|
return 0;
|
|
1264
1666
|
}
|
|
1667
|
+
io.out("Checking service configuration and listening address...");
|
|
1668
|
+
const network = await cliNetwork(config);
|
|
1669
|
+
let ownRuntime = null;
|
|
1670
|
+
try {
|
|
1671
|
+
if (isProcessRunning((await readRuntimeState(resolveCliPaths(config.home).runtimeState)).pid)) {
|
|
1672
|
+
if (definition && installed) ownRuntime = await verifiedServiceRuntime(config.home, manager);
|
|
1673
|
+
else throw new CliError("HOME_ACTIVE", "OpenTig is already running for this home.", [
|
|
1674
|
+
`Pair a browser: ${cliCommand(config.home, "pair")}`,
|
|
1675
|
+
"To install a service for this home, stop the foreground instance in its terminal and retry.",
|
|
1676
|
+
"No service was installed and the existing process was left running."
|
|
1677
|
+
]);
|
|
1678
|
+
}
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
if (!(error instanceof CliError && error.code === "RUNTIME_MISSING")) throw error;
|
|
1681
|
+
}
|
|
1682
|
+
if (!ownRuntime || !await sameListener(network, ownRuntime)) try {
|
|
1683
|
+
await assertListenerAvailable(config, network, "service install");
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
if (error instanceof CliError) throw new CliError(error.code, error.message, [...error.actions, "No service was installed or replaced."], { cause: error });
|
|
1686
|
+
throw error;
|
|
1687
|
+
}
|
|
1688
|
+
await assertGitAvailable();
|
|
1689
|
+
await resolveServiceNpm();
|
|
1265
1690
|
await manager.preflight();
|
|
1266
1691
|
return await installService(config, manager, installed, definition, io);
|
|
1267
|
-
}
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
async function withServiceLock(home, lock, run) {
|
|
1695
|
+
let result = 1;
|
|
1696
|
+
let failed = false;
|
|
1697
|
+
let failure;
|
|
1698
|
+
try {
|
|
1699
|
+
result = await run();
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
failed = true;
|
|
1702
|
+
failure = error;
|
|
1703
|
+
}
|
|
1704
|
+
try {
|
|
1268
1705
|
await rm(lock, {
|
|
1269
1706
|
recursive: true,
|
|
1270
1707
|
force: true
|
|
1271
1708
|
});
|
|
1709
|
+
} catch (cleanup) {
|
|
1710
|
+
throw new CliError("SERVICE_LOCK_CLEANUP_FAILED", failed ? diagnostic(serviceError(failure, home, "complete the operation")) : "The service operation completed, but its lock could not be removed.", [`Lock cleanup also failed: check permissions on ${lock}.`, `Inspect ${cliCommand(home, "service", "status")} and verify no operation is active before repairing the lock.`], { cause: failed ? new AggregateError([failure, cleanup], "Service operation and lock cleanup failed.") : cleanup });
|
|
1272
1711
|
}
|
|
1712
|
+
if (failed) throw failure;
|
|
1713
|
+
return result;
|
|
1273
1714
|
}
|
|
1274
1715
|
async function installService(config, manager, installed, previousDefinition, io) {
|
|
1275
1716
|
const packageRoot = await findPackageRoot(fileURLToPath(import.meta.url));
|
|
@@ -1278,6 +1719,7 @@ async function installService(config, manager, installed, previousDefinition, io
|
|
|
1278
1719
|
const reuse = installed?.version === OPEN_TIG_APP_VERSION;
|
|
1279
1720
|
const target = reuse ? packageDirectory(config.home, installed) : path.join(serviceRoot, `app-${OPEN_TIG_APP_VERSION}`);
|
|
1280
1721
|
const temporary = path.join(serviceRoot, `.install-${process.pid}-${Date.now()}`);
|
|
1722
|
+
io.out("Preparing the managed CLI and dependencies...");
|
|
1281
1723
|
try {
|
|
1282
1724
|
if (!reuse) {
|
|
1283
1725
|
await mkdir(temporary, {
|
|
@@ -1329,65 +1771,83 @@ async function installService(config, manager, installed, previousDefinition, io
|
|
|
1329
1771
|
port: next.port,
|
|
1330
1772
|
environmentPath: next.environmentPath
|
|
1331
1773
|
});
|
|
1774
|
+
let runtime;
|
|
1332
1775
|
try {
|
|
1776
|
+
io.out("Registering the OpenTig user service...");
|
|
1333
1777
|
await manager.write(definition);
|
|
1334
1778
|
await saveInstallation(config.home, next);
|
|
1779
|
+
const attempt = await requestStartup(config.home);
|
|
1780
|
+
io.out("Starting the service and verifying its identity...");
|
|
1335
1781
|
await manager.restart();
|
|
1336
|
-
await waitServiceReady(config.home, next, manager);
|
|
1782
|
+
runtime = await waitServiceReady(config.home, next, manager, attempt);
|
|
1337
1783
|
} catch (error) {
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
if (
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1784
|
+
const original = serviceError(error, config.home, "install");
|
|
1785
|
+
try {
|
|
1786
|
+
if (previousDefinition) {
|
|
1787
|
+
await manager.write(previousDefinition);
|
|
1788
|
+
if (installed) await saveInstallation(config.home, installed);
|
|
1789
|
+
else await rm(path.join(serviceRoot, "installation.json"), { force: true });
|
|
1790
|
+
const attempt = await requestStartup(config.home);
|
|
1791
|
+
await manager.restart();
|
|
1792
|
+
if (!installed) throw new CliError("RECOVERY_UNVERIFIED", "The previous service definition has no installation metadata.");
|
|
1793
|
+
await waitServiceReady(config.home, installed, manager, attempt);
|
|
1794
|
+
} else {
|
|
1795
|
+
await manager.stop();
|
|
1796
|
+
await manager.remove();
|
|
1797
|
+
await rm(path.join(serviceRoot, "installation.json"), { force: true });
|
|
1798
|
+
}
|
|
1799
|
+
} catch (recovery) {
|
|
1800
|
+
throw new CliError("SERVICE_RECOVERY_FAILED", diagnostic(original), [`Recovery also failed: ${diagnostic(serviceError(recovery, config.home, "restore or remove"))}`, `The final service state is not verified. Inspect ${cliCommand(config.home, "service", "status")} before retrying.`], { cause: new AggregateError([original, recovery], "Installation and recovery both failed.") });
|
|
1347
1801
|
}
|
|
1348
|
-
throw
|
|
1802
|
+
throw new CliError(original.code, diagnostic(original), [previousDefinition ? "The previous service was restored and verified." : "The newly registered service was removed. Existing applications were left running."], { cause: original });
|
|
1349
1803
|
}
|
|
1350
|
-
io.out(`OpenTig service installed and started on ${
|
|
1804
|
+
io.out(`OpenTig service installed and started on ${runtime.host}:${runtime.port}.`);
|
|
1805
|
+
io.out(`Connection URL: ${publicOrigin(runtime.host, runtime.port)}`);
|
|
1351
1806
|
io.out(manager.location);
|
|
1352
1807
|
io.out(manager.startup);
|
|
1353
|
-
io.out(`Pair another browser with:
|
|
1808
|
+
io.out(`Pair another browser with: ${cliCommand(config.home, "pair")}`);
|
|
1354
1809
|
return 0;
|
|
1355
1810
|
}
|
|
1356
|
-
async function
|
|
1357
|
-
const
|
|
1811
|
+
async function verifiedServiceRuntime(home, manager) {
|
|
1812
|
+
const runtime = await readRuntimeState(resolveCliPaths(home).runtimeState);
|
|
1813
|
+
const state = await manager.status();
|
|
1814
|
+
const owned = state.pid ? state.pid === runtime.pid : await manager.ownsRuntime?.(runtime.pid);
|
|
1815
|
+
if (!state.running || !owned) throw new CliError("SERVICE_PROCESS_MISMATCH", "The running OpenTig instance is not verified as this managed service.", [`Check ${cliCommand(home, "service", "status")}. If a foreground instance uses this home, stop it in its terminal before installing the service.`, "No unknown process was stopped."]);
|
|
1816
|
+
return probeRuntime(home, runtime, true);
|
|
1817
|
+
}
|
|
1818
|
+
async function waitServiceReady(home, installation, manager, attempt) {
|
|
1358
1819
|
for (let i = 0; i < 30; i++) {
|
|
1820
|
+
const startup = await readStartup(home);
|
|
1821
|
+
if (attempt && startup?.attempt === attempt && startup.phase === "failed") throw new CliError(startup.code ?? "STARTUP_FAILED", startup.message ?? "The service failed during startup.");
|
|
1359
1822
|
try {
|
|
1360
|
-
const
|
|
1361
|
-
|
|
1362
|
-
const response = await fetch(`http://${host}:${installation.port}/readyz`, { signal: AbortSignal.timeout(1e3) });
|
|
1363
|
-
const value = await response.json();
|
|
1364
|
-
if (state.running && (!state.pid || state.pid === runtime.pid) && runtime.appVersion === installation.version && response.ok && value.status === "ready" && value.appVersion === installation.version) return;
|
|
1823
|
+
const runtime = await verifiedServiceRuntime(home, manager);
|
|
1824
|
+
if (runtime.appVersion === installation.version && (!attempt || startup?.attempt === attempt && startup.phase === "ready" && startup.pid === runtime.pid && startup.instanceId === runtime.instanceId || runtime.legacy && startup?.attempt === attempt && startup.phase === "pending" && Date.parse(runtime.startedAt) >= Date.parse(startup.requestedAt))) return runtime;
|
|
1365
1825
|
} catch {}
|
|
1366
1826
|
await setTimeout$1(1e3);
|
|
1367
1827
|
}
|
|
1368
|
-
throw
|
|
1828
|
+
throw startupTimeout(home);
|
|
1369
1829
|
}
|
|
1370
1830
|
async function assertPackageBuild(packageRoot) {
|
|
1371
1831
|
await Promise.all([
|
|
1372
|
-
access(path.join(packageRoot, "dist
|
|
1373
|
-
access(path.join(packageRoot, "dist
|
|
1832
|
+
access(path.join(packageRoot, "dist/bin.mjs")),
|
|
1833
|
+
access(path.join(packageRoot, "dist/client/index.html")),
|
|
1374
1834
|
readFile(path.join(packageRoot, "package.json"), "utf8")
|
|
1375
|
-
]).catch(() => {
|
|
1376
|
-
throw new
|
|
1835
|
+
]).catch((cause) => {
|
|
1836
|
+
throw new CliError("PACKAGE_INCOMPLETE", "The OpenTig package is incomplete and cannot be installed as a service.", ["Reinstall the same CLI version from npm, then retry."], { cause });
|
|
1377
1837
|
});
|
|
1378
1838
|
}
|
|
1379
|
-
async function findPackageRoot(
|
|
1380
|
-
let directory = path.dirname(
|
|
1381
|
-
for (let depth = 0; depth < 4; depth
|
|
1839
|
+
async function findPackageRoot(moduleUrl) {
|
|
1840
|
+
let directory = path.dirname(moduleUrl);
|
|
1841
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
1382
1842
|
try {
|
|
1383
|
-
await Promise.all([access(path.join(directory, "package.json")), access(path.join(directory, "dist
|
|
1843
|
+
await Promise.all([access(path.join(directory, "package.json")), access(path.join(directory, "dist/bin.mjs"))]);
|
|
1384
1844
|
return directory;
|
|
1385
1845
|
} catch {}
|
|
1386
1846
|
const parent = path.dirname(directory);
|
|
1387
1847
|
if (parent === directory) break;
|
|
1388
1848
|
directory = parent;
|
|
1389
1849
|
}
|
|
1390
|
-
throw new
|
|
1850
|
+
throw new CliError("PACKAGE_INCOMPLETE", "Could not locate the installed OpenTig package.", ["Reinstall the CLI from npm, then retry."]);
|
|
1391
1851
|
}
|
|
1392
1852
|
//#endregion
|
|
1393
1853
|
//#region ../../src/shared/desktop-updates.ts
|
|
@@ -1673,142 +2133,6 @@ async function createServiceUpdater(home, version, profile, address) {
|
|
|
1673
2133
|
}, failedUpdate);
|
|
1674
2134
|
}
|
|
1675
2135
|
//#endregion
|
|
1676
|
-
//#region src/cli-home.ts
|
|
1677
|
-
var RUNTIME_STATE_VERSION = 1;
|
|
1678
|
-
function resolveCliPaths(home) {
|
|
1679
|
-
const resolved = path.resolve(home);
|
|
1680
|
-
const serverData = path.join(resolved, "server");
|
|
1681
|
-
const logs = path.join(resolved, "logs");
|
|
1682
|
-
return {
|
|
1683
|
-
home: resolved,
|
|
1684
|
-
settings: path.join(resolved, "settings.json"),
|
|
1685
|
-
aiLog: path.join(resolved, "ai-log.jsonl"),
|
|
1686
|
-
problems: path.join(resolved, "problems.jsonl"),
|
|
1687
|
-
serverData,
|
|
1688
|
-
logs,
|
|
1689
|
-
serverLog: path.join(logs, "server.log"),
|
|
1690
|
-
runtimeState: path.join(resolved, "runtime.json"),
|
|
1691
|
-
adminToken: path.join(serverData, "admin-token")
|
|
1692
|
-
};
|
|
1693
|
-
}
|
|
1694
|
-
async function prepareCliHome(paths) {
|
|
1695
|
-
await mkdir(paths.home, {
|
|
1696
|
-
recursive: true,
|
|
1697
|
-
mode: 448
|
|
1698
|
-
});
|
|
1699
|
-
await chmod(paths.home, 448);
|
|
1700
|
-
await Promise.all([mkdir(paths.serverData, {
|
|
1701
|
-
recursive: true,
|
|
1702
|
-
mode: 448
|
|
1703
|
-
}), mkdir(paths.logs, {
|
|
1704
|
-
recursive: true,
|
|
1705
|
-
mode: 448
|
|
1706
|
-
})]);
|
|
1707
|
-
await Promise.all([chmod(paths.serverData, 448), chmod(paths.logs, 448)]);
|
|
1708
|
-
}
|
|
1709
|
-
async function loadOrCreateAdminToken(filePath) {
|
|
1710
|
-
try {
|
|
1711
|
-
return await readAdminToken(filePath);
|
|
1712
|
-
} catch (error) {
|
|
1713
|
-
if (error.code !== "ENOENT") throw error;
|
|
1714
|
-
}
|
|
1715
|
-
const token = randomBytes(32).toString("base64url");
|
|
1716
|
-
await writeFileAtomically(filePath, `${token}\n`, { directoryMode: 448 });
|
|
1717
|
-
return token;
|
|
1718
|
-
}
|
|
1719
|
-
async function readAdminToken(filePath) {
|
|
1720
|
-
const token = (await readFile(filePath, "utf8")).trim();
|
|
1721
|
-
if (!isToken(token)) throw new Error("OpenTig local admin credential is corrupt.");
|
|
1722
|
-
await chmod(filePath, 384);
|
|
1723
|
-
return token;
|
|
1724
|
-
}
|
|
1725
|
-
async function writeRuntimeState(filePath, state) {
|
|
1726
|
-
await writeFileAtomically(filePath, `${JSON.stringify({
|
|
1727
|
-
version: RUNTIME_STATE_VERSION,
|
|
1728
|
-
...state
|
|
1729
|
-
}, null, 2)}\n`, {
|
|
1730
|
-
parseJson: true,
|
|
1731
|
-
directoryMode: 448
|
|
1732
|
-
});
|
|
1733
|
-
}
|
|
1734
|
-
async function readRuntimeState(filePath) {
|
|
1735
|
-
let value;
|
|
1736
|
-
try {
|
|
1737
|
-
value = JSON.parse(await readFile(filePath, "utf8"));
|
|
1738
|
-
} catch (error) {
|
|
1739
|
-
if (error.code === "ENOENT") throw new Error("No running OpenTig server was found for this home.", { cause: error });
|
|
1740
|
-
throw new Error("OpenTig runtime state is unreadable.", { cause: error });
|
|
1741
|
-
}
|
|
1742
|
-
if (!isRuntimeState(value)) throw new Error("OpenTig runtime state is invalid.");
|
|
1743
|
-
await chmod(filePath, 384);
|
|
1744
|
-
return value;
|
|
1745
|
-
}
|
|
1746
|
-
async function clearRuntimeState(filePath, instanceId) {
|
|
1747
|
-
try {
|
|
1748
|
-
if ((await readRuntimeState(filePath)).instanceId === instanceId) await rm(filePath, { force: true });
|
|
1749
|
-
} catch (error) {
|
|
1750
|
-
if (!(error instanceof Error && error.message === "No running OpenTig server was found for this home.")) throw error;
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
function newInstanceId() {
|
|
1754
|
-
return randomBytes(24).toString("base64url");
|
|
1755
|
-
}
|
|
1756
|
-
function isToken(value) {
|
|
1757
|
-
return /^[A-Za-z0-9_-]{43}$/.test(value);
|
|
1758
|
-
}
|
|
1759
|
-
function isRuntimeState(value) {
|
|
1760
|
-
const state = value;
|
|
1761
|
-
return Boolean(state && state.version === RUNTIME_STATE_VERSION && Number.isSafeInteger(state.pid) && Number(state.pid) > 0 && typeof state.host === "string" && state.host.length > 0 && state.host.length <= 255 && Number.isSafeInteger(state.port) && Number(state.port) > 0 && Number(state.port) <= 65535 && Number.isSafeInteger(state.protocolVersion) && Number(state.protocolVersion) > 0 && typeof state.appVersion === "string" && state.appVersion.length > 0 && state.appVersion.length <= 128 && typeof state.instanceId === "string" && /^[A-Za-z0-9_-]{32}$/.test(state.instanceId) && typeof state.startedAt === "string" && !Number.isNaN(Date.parse(state.startedAt)));
|
|
1762
|
-
}
|
|
1763
|
-
//#endregion
|
|
1764
|
-
//#region src/cli-log.ts
|
|
1765
|
-
var MAX_BYTES = 1048576;
|
|
1766
|
-
var BACKUPS = 3;
|
|
1767
|
-
var CliServerLog = class {
|
|
1768
|
-
filePath;
|
|
1769
|
-
queue = Promise.resolve();
|
|
1770
|
-
constructor(filePath) {
|
|
1771
|
-
this.filePath = filePath;
|
|
1772
|
-
}
|
|
1773
|
-
logger = (level, message) => {
|
|
1774
|
-
const line = `${(/* @__PURE__ */ new Date()).toISOString()} [${level}] ${redactSensitiveText(message)}\n`;
|
|
1775
|
-
this.queue = this.queue.then(async () => {
|
|
1776
|
-
await mkdir(path.dirname(this.filePath), {
|
|
1777
|
-
recursive: true,
|
|
1778
|
-
mode: 448
|
|
1779
|
-
});
|
|
1780
|
-
if (await wouldExceed(this.filePath, Buffer.byteLength(line))) await this.rotate();
|
|
1781
|
-
await appendFile(this.filePath, line, {
|
|
1782
|
-
encoding: "utf8",
|
|
1783
|
-
mode: 384
|
|
1784
|
-
});
|
|
1785
|
-
}).catch(() => void 0);
|
|
1786
|
-
};
|
|
1787
|
-
close() {
|
|
1788
|
-
return this.queue;
|
|
1789
|
-
}
|
|
1790
|
-
async rotate() {
|
|
1791
|
-
await rm(`${this.filePath}.${BACKUPS}`, { force: true });
|
|
1792
|
-
for (let index = 2; index >= 1; index -= 1) await moveIfPresent(`${this.filePath}.${index}`, `${this.filePath}.${index + 1}`);
|
|
1793
|
-
await moveIfPresent(this.filePath, `${this.filePath}.1`);
|
|
1794
|
-
}
|
|
1795
|
-
};
|
|
1796
|
-
async function wouldExceed(filePath, incomingBytes) {
|
|
1797
|
-
try {
|
|
1798
|
-
return (await stat(filePath)).size + incomingBytes > MAX_BYTES;
|
|
1799
|
-
} catch (error) {
|
|
1800
|
-
if (error.code === "ENOENT") return false;
|
|
1801
|
-
throw error;
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
async function moveIfPresent(source, target) {
|
|
1805
|
-
try {
|
|
1806
|
-
await rename(source, target);
|
|
1807
|
-
} catch (error) {
|
|
1808
|
-
if (error.code !== "ENOENT") throw error;
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
//#endregion
|
|
1812
2136
|
//#region src/cli.ts
|
|
1813
2137
|
var SHUTDOWN_TIMEOUT_MS = 8e3;
|
|
1814
2138
|
var consoleIo = {
|
|
@@ -1816,9 +2140,10 @@ var consoleIo = {
|
|
|
1816
2140
|
error: (value) => process$1.stderr.write(`${value}\n`)
|
|
1817
2141
|
};
|
|
1818
2142
|
async function runCli(args, environment = process$1.env, io = consoleIo, profile = "production") {
|
|
2143
|
+
let config;
|
|
2144
|
+
let reporter = null;
|
|
1819
2145
|
try {
|
|
1820
|
-
|
|
1821
|
-
const config = parseCliArguments(args, environment);
|
|
2146
|
+
config = parseCliArguments(args, environment);
|
|
1822
2147
|
if (config.command === "help") {
|
|
1823
2148
|
io.out(cliHelp(OPEN_TIG_APP_VERSION));
|
|
1824
2149
|
return 0;
|
|
@@ -1827,21 +2152,35 @@ async function runCli(args, environment = process$1.env, io = consoleIo, profile
|
|
|
1827
2152
|
io.out(OPEN_TIG_APP_VERSION);
|
|
1828
2153
|
return 0;
|
|
1829
2154
|
}
|
|
2155
|
+
if (config.command === "serve" || config.command === "start") reporter = await startupReporter(config.home);
|
|
2156
|
+
assertNodeVersion();
|
|
1830
2157
|
if (config.command === "service") return await manageCliService(config, io);
|
|
2158
|
+
if (config.command === "reset-access") {
|
|
2159
|
+
await rm(path.join(resolveCliPaths(config.home).serverData, "web-access.json"), { force: true });
|
|
2160
|
+
io.out("Web access settings reset. Restart this OpenTig instance to enable browser access on its startup address and port. Paired devices are preserved.");
|
|
2161
|
+
io.out(`Managed service: ${cliCommand(config.home, "service", "restart")}`);
|
|
2162
|
+
io.out(`Foreground server: stop it in its terminal, then run ${cliCommand(config.home, "serve")}`);
|
|
2163
|
+
return 0;
|
|
2164
|
+
}
|
|
1831
2165
|
if (config.command === "pair") return await pairRunningServer(config, io, profile);
|
|
1832
|
-
return await startCliServer(config, io, profile);
|
|
2166
|
+
return await startCliServer(config, io, profile, reporter);
|
|
1833
2167
|
} catch (error) {
|
|
1834
|
-
const
|
|
1835
|
-
|
|
2168
|
+
const failure = error instanceof CliUsageError || !config ? error : operationError(error, config.home, config.command === "service" ? `run service ${config.serviceAction}` : `run ${config.command}`);
|
|
2169
|
+
if (config && !(error instanceof CliUsageError)) {
|
|
2170
|
+
await reporter?.failed(failure).catch(() => void 0);
|
|
2171
|
+
await recordCliFailure(config.home, failure).catch(() => void 0);
|
|
2172
|
+
}
|
|
2173
|
+
io.error(`OpenTig: ${diagnostic(failure)}`);
|
|
1836
2174
|
if (error instanceof CliUsageError) io.error("Run \"opentig --help\" for usage.");
|
|
1837
2175
|
return error instanceof CliUsageError ? error.exitCode : 1;
|
|
1838
2176
|
}
|
|
1839
2177
|
}
|
|
1840
|
-
async function startCliServer(config, io, profile) {
|
|
2178
|
+
async function startCliServer(config, io, profile, reporter) {
|
|
1841
2179
|
await assertGitAvailable();
|
|
1842
2180
|
const paths = resolveCliPaths(config.home);
|
|
1843
2181
|
await prepareCliHome(paths);
|
|
1844
|
-
await refuseActiveRuntime(
|
|
2182
|
+
await refuseActiveRuntime(config, profile);
|
|
2183
|
+
await assertListenerAvailable(config, await cliNetwork(config), config.command);
|
|
1845
2184
|
const adminToken = await loadOrCreateAdminToken(paths.adminToken);
|
|
1846
2185
|
const instanceId = newInstanceId();
|
|
1847
2186
|
const log = new CliServerLog(paths.serverLog);
|
|
@@ -1863,29 +2202,40 @@ async function startCliServer(config, io, profile) {
|
|
|
1863
2202
|
admin: {
|
|
1864
2203
|
token: adminToken,
|
|
1865
2204
|
instanceId
|
|
2205
|
+
},
|
|
2206
|
+
recoveryCommand: profile === "dev" ? "node packages/server/dist/dev.mjs reset-access" : "opentig reset-access",
|
|
2207
|
+
onNetworkChanged: async (network) => {
|
|
2208
|
+
const state = await readRuntimeState(paths.runtimeState);
|
|
2209
|
+
await writeRuntimeState(paths.runtimeState, {
|
|
2210
|
+
...state,
|
|
2211
|
+
host: network.listenerHost,
|
|
2212
|
+
port: network.listenerPort
|
|
2213
|
+
});
|
|
1866
2214
|
}
|
|
1867
|
-
}, config.port);
|
|
2215
|
+
}, config.port, config);
|
|
1868
2216
|
await writeRuntimeState(paths.runtimeState, {
|
|
1869
2217
|
pid: process$1.pid,
|
|
1870
|
-
host:
|
|
2218
|
+
host: server.host,
|
|
1871
2219
|
port: server.port,
|
|
1872
2220
|
protocolVersion: server.protocolVersion,
|
|
1873
2221
|
appVersion: server.appVersion,
|
|
1874
2222
|
instanceId,
|
|
1875
2223
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1876
2224
|
});
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
const
|
|
2225
|
+
await reporter?.ready(instanceId);
|
|
2226
|
+
log.logger("info", `OpenTig CLI ready on ${publicOrigin(server.host, server.port)}.`);
|
|
2227
|
+
const origin = publicOrigin(server.host, server.port);
|
|
2228
|
+
const pairing = server.getStatus().browserAccessEnabled ? rewritePairingOrigin(server.createPairingLink(), origin) : null;
|
|
1880
2229
|
io.out(`${applicationName(profile)} ${server.appVersion} is ready.`);
|
|
1881
2230
|
if (profile === "dev") io.out(`Dev data directory: ${paths.home}`);
|
|
1882
2231
|
io.out(`Connection URL: ${origin}`);
|
|
1883
|
-
printPairing(pairing, io, config.command === "serve");
|
|
1884
|
-
|
|
2232
|
+
if (pairing) printPairing(pairing, io, config.command === "serve");
|
|
2233
|
+
else io.out("Web access is paused. Use reset-access locally and restart to restore it.");
|
|
2234
|
+
if (!isLoopbackHost(server.host)) {
|
|
1885
2235
|
io.error("WARNING: This listener grants owner-level access as your OS user. Use only a trusted LAN/VPN or an HTTPS/SSH tunnel; never expose it directly to the public Internet.");
|
|
1886
|
-
if (
|
|
2236
|
+
if (server.host === "0.0.0.0" || server.host === "::") io.error("Replace the loopback host in the printed link with a trusted address of this machine for another device.");
|
|
1887
2237
|
}
|
|
1888
|
-
if (config.openBrowser) await openSystemBrowser(pairing.url).catch(() => {
|
|
2238
|
+
if (config.openBrowser && pairing) await openSystemBrowser(pairing.url).catch(() => {
|
|
1889
2239
|
io.error("Could not open the system browser. Open the one-time pairing link shown above manually.");
|
|
1890
2240
|
});
|
|
1891
2241
|
updates.start();
|
|
@@ -1895,7 +2245,7 @@ async function startCliServer(config, io, profile) {
|
|
|
1895
2245
|
await log.close();
|
|
1896
2246
|
}, io);
|
|
1897
2247
|
} catch (error) {
|
|
1898
|
-
await updates.stop();
|
|
2248
|
+
await updates.stop().catch(() => void 0);
|
|
1899
2249
|
if (server) await server.close().catch(() => void 0);
|
|
1900
2250
|
await clearRuntimeState(paths.runtimeState, instanceId).catch(() => void 0);
|
|
1901
2251
|
await log.close();
|
|
@@ -1905,15 +2255,15 @@ async function startCliServer(config, io, profile) {
|
|
|
1905
2255
|
async function pairRunningServer(config, io, profile) {
|
|
1906
2256
|
const paths = resolveCliPaths(config.home);
|
|
1907
2257
|
const state = await readRuntimeState(paths.runtimeState);
|
|
1908
|
-
if (!isProcessRunning(state.pid)) throw new
|
|
2258
|
+
if (!isProcessRunning(state.pid)) throw new CliError("RUNTIME_STALE", "OpenTig runtime state is stale; its process is not running.", [`Start ${cliCommand(config.home, "serve")} or restart its managed service.`]);
|
|
1909
2259
|
const origin = publicOrigin(state.host, state.port);
|
|
1910
|
-
const adminOrigin =
|
|
2260
|
+
const adminOrigin = publicOrigin(state.host, state.port);
|
|
1911
2261
|
const ready = await fetch(`${adminOrigin}/readyz`, { signal: AbortSignal.timeout(3e3) });
|
|
1912
|
-
if (!ready.ok) throw new
|
|
2262
|
+
if (!ready.ok) throw new CliError("SERVER_NOT_READY", "The running OpenTig server is not ready.", [`Check ${cliCommand(config.home, "service", "status")} and retry when startup completes.`]);
|
|
1913
2263
|
const identity = await ready.json();
|
|
1914
|
-
if (identity.status !== "ready" || identity.protocolVersion !== state.protocolVersion || identity.appVersion !== state.appVersion || profile === "dev" && identity.profile !== "dev") throw new
|
|
2264
|
+
if (identity.status !== "ready" || identity.protocolVersion !== state.protocolVersion || identity.appVersion !== state.appVersion || profile === "dev" && identity.profile !== "dev") throw new CliError("RUNTIME_MISMATCH", "OpenTig runtime state does not match the running server.", [`Inspect ${cliCommand(config.home, "service", "status")} before restarting this instance.`]);
|
|
1915
2265
|
const adminToken = await readAdminToken(paths.adminToken).catch(() => {
|
|
1916
|
-
throw new
|
|
2266
|
+
throw new CliError("ADMIN_CREDENTIAL_INVALID", "OpenTig local admin credential is missing or unreadable.", [`Check the private credential and permissions in ${paths.serverData}. Do not copy credentials from another instance.`]);
|
|
1917
2267
|
});
|
|
1918
2268
|
const response = await fetch(`${adminOrigin}/api/admin/pair`, {
|
|
1919
2269
|
method: "POST",
|
|
@@ -1927,32 +2277,50 @@ async function pairRunningServer(config, io, profile) {
|
|
|
1927
2277
|
}),
|
|
1928
2278
|
signal: AbortSignal.timeout(3e3)
|
|
1929
2279
|
});
|
|
1930
|
-
if (
|
|
1931
|
-
|
|
2280
|
+
if (response.status === 403) throw new CliError("WEB_ACCESS_DISABLED", "Web access is paused; no pairing link was created.", [`Enable browser access in Settings → Web access, or run ${cliCommand(config.home, "reset-access")} and restart this instance.`, "Resetting access restores startup listener settings and preserves paired browsers."]);
|
|
2281
|
+
if (!response.ok) throw new CliError("PAIRING_REFUSED", "The running server refused the local pairing request.", [`Check ${cliCommand(config.home, "service", "status")} and the selected home before retrying.`]);
|
|
2282
|
+
let pairing;
|
|
2283
|
+
try {
|
|
2284
|
+
pairing = validatePairing(await response.json(), origin);
|
|
2285
|
+
} catch (cause) {
|
|
2286
|
+
throw new CliError("PAIRING_INVALID", "The server returned an invalid pairing link.", [`No link was printed. Inspect ${cliCommand(config.home, "service", "status")} and the server log before retrying.`], { cause });
|
|
2287
|
+
}
|
|
2288
|
+
printPairing(pairing, io, true);
|
|
1932
2289
|
return 0;
|
|
1933
2290
|
}
|
|
1934
|
-
async function startOnConfiguredPort(baseConfig, port) {
|
|
2291
|
+
async function startOnConfiguredPort(baseConfig, port, cliConfig) {
|
|
1935
2292
|
try {
|
|
1936
2293
|
return await runOpenTigServer({
|
|
1937
2294
|
...baseConfig,
|
|
1938
2295
|
port
|
|
1939
2296
|
});
|
|
1940
2297
|
} catch (error) {
|
|
1941
|
-
|
|
1942
|
-
|
|
2298
|
+
const config = cliConfig ?? {
|
|
2299
|
+
home: path.dirname(baseConfig.settingsPath),
|
|
2300
|
+
host: baseConfig.host ?? "127.0.0.1",
|
|
2301
|
+
port
|
|
2302
|
+
};
|
|
2303
|
+
const { network, savedListener } = await loadStartupNetwork({
|
|
2304
|
+
...baseConfig,
|
|
2305
|
+
port
|
|
2306
|
+
});
|
|
2307
|
+
throw listenerError(error, config, {
|
|
2308
|
+
...network,
|
|
2309
|
+
savedListener
|
|
2310
|
+
}, cliConfig?.command ?? "serve");
|
|
1943
2311
|
}
|
|
1944
2312
|
}
|
|
1945
|
-
function
|
|
1946
|
-
const
|
|
1947
|
-
return `http://${displayHost.includes(":") ? `[${displayHost}]` : displayHost}:${port}`;
|
|
1948
|
-
}
|
|
1949
|
-
async function refuseActiveRuntime(runtimeStatePath, profile) {
|
|
2313
|
+
async function refuseActiveRuntime(config, profile) {
|
|
2314
|
+
const paths = resolveCliPaths(config.home);
|
|
1950
2315
|
try {
|
|
1951
|
-
const state = await readRuntimeState(
|
|
1952
|
-
if (isProcessRunning(state.pid))
|
|
1953
|
-
|
|
2316
|
+
const state = await readRuntimeState(paths.runtimeState);
|
|
2317
|
+
if (isProcessRunning(state.pid)) {
|
|
2318
|
+
await probeRuntime(config.home, state);
|
|
2319
|
+
throw new CliError("HOME_ACTIVE", "OpenTig is already running for this home.", [`Pair a browser with ${profile === "dev" ? "npm run pair:web:dev" : cliCommand(config.home, "pair")}`, "To switch a foreground instance to a service, stop it in its terminal first."]);
|
|
2320
|
+
}
|
|
2321
|
+
await clearRuntimeState(paths.runtimeState, state.instanceId);
|
|
1954
2322
|
} catch (error) {
|
|
1955
|
-
if (error instanceof
|
|
2323
|
+
if (error instanceof CliError && error.code === "RUNTIME_MISSING") return;
|
|
1956
2324
|
throw error;
|
|
1957
2325
|
}
|
|
1958
2326
|
}
|
|
@@ -2002,47 +2370,22 @@ function rewritePairingOrigin(pairing, origin) {
|
|
|
2002
2370
|
}
|
|
2003
2371
|
function validatePairing(value, origin) {
|
|
2004
2372
|
const pairing = value;
|
|
2005
|
-
if (!pairing || typeof pairing.url !== "string" || typeof pairing.expiresAt !== "string") throw new
|
|
2373
|
+
if (!pairing || typeof pairing.url !== "string" || typeof pairing.expiresAt !== "string") throw new CliError("PAIRING_INVALID", "The server returned an invalid pairing link.", ["No link was printed. Check the selected instance and server logs before retrying."]);
|
|
2006
2374
|
const url = new URL(pairing.url);
|
|
2007
2375
|
const token = new URLSearchParams(url.hash.slice(1)).get("token");
|
|
2008
2376
|
const expiresAt = Date.parse(pairing.expiresAt);
|
|
2009
|
-
if (url.origin !== origin || url.pathname !== "/pair" || url.search || !token || !/^[A-Za-z0-9_-]{43}$/.test(token) || expiresAt <= Date.now() || expiresAt > Date.now() + 9e5) throw new
|
|
2377
|
+
if (url.origin !== origin || url.username || url.password || url.pathname !== "/pair" || url.search || !token || !/^[A-Za-z0-9_-]{43}$/.test(token) || !Number.isFinite(expiresAt) || expiresAt <= Date.now() || expiresAt > Date.now() + 9e5) throw new CliError("PAIRING_INVALID", "The server returned an invalid pairing link.", ["No link was printed. Check the selected instance and server logs before retrying."]);
|
|
2010
2378
|
return {
|
|
2011
2379
|
url: url.href,
|
|
2012
2380
|
expiresAt: pairing.expiresAt
|
|
2013
2381
|
};
|
|
2014
2382
|
}
|
|
2015
|
-
function localAdminOrigin(state) {
|
|
2016
|
-
return publicOrigin(state.host, state.port);
|
|
2017
|
-
}
|
|
2018
2383
|
function isLoopbackHost(host) {
|
|
2019
2384
|
return host === "127.0.0.1" || host === "::1" || host.toLowerCase() === "localhost";
|
|
2020
2385
|
}
|
|
2021
|
-
function isAddressInUse(error) {
|
|
2022
|
-
return error instanceof Error && "code" in error && error.code === "EADDRINUSE";
|
|
2023
|
-
}
|
|
2024
|
-
function isProcessRunning(pid) {
|
|
2025
|
-
try {
|
|
2026
|
-
process$1.kill(pid, 0);
|
|
2027
|
-
return true;
|
|
2028
|
-
} catch (error) {
|
|
2029
|
-
return error.code === "EPERM";
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
2386
|
function assertNodeVersion() {
|
|
2033
2387
|
const major = Number(process$1.versions.node.split(".", 1)[0]);
|
|
2034
|
-
if (!Number.isSafeInteger(major) || major < 24) throw new
|
|
2035
|
-
}
|
|
2036
|
-
function assertGitAvailable() {
|
|
2037
|
-
return new Promise((resolve, reject) => {
|
|
2038
|
-
execFile("git", ["--version"], {
|
|
2039
|
-
timeout: 1e4,
|
|
2040
|
-
windowsHide: true
|
|
2041
|
-
}, (error, stdout) => {
|
|
2042
|
-
if (error || !stdout.trim().startsWith("git version ")) reject(/* @__PURE__ */ new Error("Git is required and was not found on PATH."));
|
|
2043
|
-
else resolve();
|
|
2044
|
-
});
|
|
2045
|
-
});
|
|
2388
|
+
if (!Number.isSafeInteger(major) || major < 24) throw new CliError("NODE_UNSUPPORTED", "Node.js 24 or later is required.", ["Install a supported Node.js version, then retry from that terminal."]);
|
|
2046
2389
|
}
|
|
2047
2390
|
//#endregion
|
|
2048
2391
|
export { parseCliArguments as i, OPEN_TIG_APP_VERSION as n, CliUsageError as r, runCli as t };
|