@forsion/unit 2.12.2 → 2.13.1
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 +61 -0
- package/backendWorker.mjs +6 -2
- package/main.mjs +991 -497
- package/package.json +1 -1
- package/release.json +5 -5
- package/web/assets/ArtificialView-XwHMLUFb.js +1 -0
- package/web/assets/{AutomationBuilder-sxWdUa0L.js → AutomationBuilder-hqs0QhMt.js} +1 -1
- package/web/assets/CalendarConfigView-DKib8Oed.js +1 -0
- package/web/assets/CalendarView-DJ5iOzvu.js +1 -0
- package/web/assets/CapabilityMenu-SwVlZ06x.js +1 -0
- package/web/assets/{CheckboxInput-BsaoUHZ0.js → CheckboxInput-BC-5a_C9.js} +1 -1
- package/web/assets/Chrome-HdfyYV3i.js +1 -0
- package/web/assets/{CodeView-j065KtVh.js → CodeView-CFL6yQ-6.js} +3 -3
- package/web/assets/DashboardGridView-B9DCWYVb.js +17 -0
- package/web/assets/{EventCard-BmDQkEj9.js → EventCard-B_ndpqH_.js} +1 -1
- package/web/assets/ExcalidrawCanvas-BQ4KKLJP.js +1 -0
- package/web/assets/{FloatingToc-CENybkAA.js → FloatingToc-Ce9GamvH.js} +1 -1
- package/web/assets/HomepageView-RCOfcVbE.js +1 -0
- package/web/assets/ImageStudioPanels-lkS_y2uM.js +3 -0
- package/web/assets/ImageStudioView-UPv52acl.js +1 -0
- package/web/assets/InlineFiles-CQbQumkf.js +1 -0
- package/web/assets/MarkdownBlock--__nPQGh.css +1 -0
- package/web/assets/MarkdownBlock-CkqiwPXa.js +58 -0
- package/web/assets/ModelPill-CY-rU1RU.js +2 -0
- package/web/assets/MuseLibraryView-CtTHlVnT.js +2 -0
- package/web/assets/{MuseView-d-iBk3sc.js → MuseView-CC2A7MBs.js} +2 -2
- package/web/assets/{PageHistoryPanel-CR-k1AM7.js → PageHistoryPanel-KhKT1DLy.js} +2 -2
- package/web/assets/PdfAnnotator-C_kglLCA.css +1 -0
- package/web/assets/PdfAnnotator-DUyrgY-t.js +505 -0
- package/web/assets/{ProductView-u2aezVsl.js → ProductView-CgsIYZqD.js} +1 -1
- package/web/assets/RightPanel-BHDgqTFs.js +4 -0
- package/web/assets/RunLocationPicker-BE5mxnab.css +1 -0
- package/web/assets/RunLocationPicker-DBnXYV5B.js +38 -0
- package/web/assets/{SidebarRow-BuSkKH5q.js → SidebarRow-BAJxbMcx.js} +1 -1
- package/web/assets/{SketchCard-D87GADyG.js → SketchCard-BGYz-bXP.js} +5 -5
- package/web/assets/{StudioGuestSurface-BijU_EEb.js → StudioGuestSurface-lt6znmkl.js} +1 -1
- package/web/assets/{TodoListView-DPPVpPUn.js → TodoListView-DPKhee6i.js} +1 -1
- package/web/assets/{Tooltip-DY4ISA-e.js → Tooltip-ZxrDqN4p.js} +1 -1
- package/web/assets/UnifiedPage-BX6ZQL0h.js +68 -0
- package/web/assets/UnifiedPage-CWXc_mo1.css +1 -0
- package/web/assets/{amadeus-manual.en-DWaN2fPs.js → amadeus-manual.en-DzRWr4BL.js} +18 -18
- package/web/assets/{amadeus-manual.zh--7D5TS8X.js → amadeus-manual.zh-DW32DkL7.js} +18 -18
- package/web/assets/{appStore-Dg9WALEA.js → appStore-DWP8UET9.js} +208 -122
- package/web/assets/appStore-wtOILggO.css +1 -0
- package/web/assets/{arrow-up-right-CpQs92mu.js → arrow-up-right-DrCXuWS7.js} +1 -1
- package/web/assets/{astryxBridge-BZhaZPs4.js → astryxBridge-D-m4_kYt.js} +3 -3
- package/web/assets/automationListSource-C8BNy6RY.js +1 -0
- package/web/assets/book-open-OLNm7OaB.js +1 -0
- package/web/assets/{calendarNavStore-C7fGRpoH.js → calendarNavStore-Cylfvv3s.js} +1 -1
- package/web/assets/{canvasPrefs-BeG4oEgP.js → canvasPrefs-8taQ42Cz.js} +1 -1
- package/web/assets/chatBoxSurface-CIlzs45H.js +1 -0
- package/web/assets/{circle-question-mark-WL3vbp-Z.js → circle-question-mark-XbAFplz1.js} +1 -1
- package/web/assets/{circle-x-D7s-M5sB.js → circle-x-fcReAZaE.js} +1 -1
- package/web/assets/{client-BSKpsyAw.js → client-0MPbodA6.js} +1 -1
- package/web/assets/{code-xml-BALV9u_c.js → code-xml-Cuna8Kh4.js} +1 -1
- package/web/assets/{codeStudioStore-C2mWfm32.js → codeStudioStore-BBU62GTy.js} +1 -1
- package/web/assets/codingNavigation-CZB4v1ZW.css +1 -0
- package/web/assets/codingNavigation-DDlPpPk5.js +47 -0
- package/web/assets/{create-4xUyKtAF.js → create-BGtEfCcg.js} +4 -4
- package/web/assets/dashboardSurface-BYM3C7WG.js +1 -0
- package/web/assets/{deskCapture-Df701U8L.js → deskCapture-BiUCmqfc.js} +1 -1
- package/web/assets/{diff2html-BV-zCH4d.js → diff2html-BNp2FTbk.js} +1 -1
- package/web/assets/{docx-preview-DW4IyU_Q.js → docx-preview-CFznGVOU.js} +1 -1
- package/web/assets/{emoji-DGaxpCIu.js → emoji-6zl4yfOS.js} +1 -1
- package/web/assets/floatingTocSurface-DreXPCa6.js +1 -0
- package/web/assets/{foldActions-B7KByFdN.js → foldActions-05QcQziQ.js} +1 -1
- package/web/assets/folder-BpsfOHD4.js +1 -0
- package/web/assets/{folder-plus-D1TDb7k9.js → folder-plus-BeQHr4AZ.js} +1 -1
- package/web/assets/gestures-DQBCcK4y.js +1 -0
- package/web/assets/{hand-Crrw-nSn.js → hand-Bko7ms15.js} +1 -1
- package/web/assets/{hard-drive-BbyVSbfP.js → hard-drive-BpuI5ebj.js} +1 -1
- package/web/assets/i18n-ecQQxiMQ.css +1 -0
- package/web/assets/i18n.generated-BuLjnmY8.js +15 -0
- package/web/assets/imageStudio--KF9gwHA.css +1 -0
- package/web/assets/imageStudio-BLVNKmEG.js +2 -0
- package/web/assets/{index-DKhyDkGB.js → index-BF10xp_K.js} +1 -1
- package/web/assets/{index-C7U6T-k8.js → index-BHm6K743.js} +1 -1
- package/web/assets/{index-CF4RV4mh.js → index-BM2o7PeV.js} +90 -90
- package/web/assets/{index-Brik2pf9.js → index-BgnNNv3e.js} +1 -1
- package/web/assets/{index--drOLtq7.js → index-C0Gy2NQ6.js} +1 -1
- package/web/assets/{index-CflzrdSt.js → index-C1doZp5H.js} +1 -1
- package/web/assets/{index-SZ5waGAq.js → index-C4L6vjd_.js} +1 -1
- package/web/assets/{index-Cmr7gVZQ.js → index-CD3yIkOA.js} +1 -1
- package/web/assets/{index-BYsdKR_1.js → index-CJeUgjPU.js} +1 -1
- package/web/assets/{index-CzM-C2Sd.js → index-CLHoq1w-.js} +1 -1
- package/web/assets/{index-tAXBa_fN.js → index-CUZI62qU.js} +1 -1
- package/web/assets/{index-C6GJulug.js → index-Cff42k7q.js} +1 -1
- package/web/assets/index-Ck_O3Jdd.js +1 -0
- package/web/assets/index-CuiYK18N.js +1 -0
- package/web/assets/{index-BRW82CsP.js → index-CwcOeZdT.js} +1 -1
- package/web/assets/{index-DUWR2nZi.js → index-D6AycQIz.js} +1 -1
- package/web/assets/{index-BCZZVJ_w.js → index-D7gVhvNY.js} +1 -1
- package/web/assets/{index-DjzSBM0U.js → index-D9UZQy42.js} +1 -1
- package/web/assets/{index-KL5Rj9kP.js → index-DuBQ0j2g.js} +1 -1
- package/web/assets/{index-Dg9oixs4.js → index-OD1gSX3h.js} +1 -1
- package/web/assets/{index-gPSbFSUn.js → index-Uo5tswys.js} +1 -1
- package/web/assets/{index-D1f_JwPH.js → index-o8cj0pRH.js} +1 -1
- package/web/assets/{index-OHi69jDq.js → index-rHjhbwvm.js} +1 -1
- package/web/assets/{index-CqyM6tQ0.js → index-rggq2czF.js} +1 -1
- package/web/assets/{index-Czu_bbDM.js → index-sCXThwqZ.js} +1 -1
- package/web/assets/{invitePage-lLQ-e0pj.js → invitePage-b0HGvMj6.js} +1 -1
- package/web/assets/{ipc-DQOD2UKk.js → ipc-CxAyeo8q.js} +1 -1
- package/web/assets/{jszip.min-DZxV9uZ_.js → jszip.min-B94KpE21.js} +1 -1
- package/web/assets/launchpad-C2c1MSVX.css +1 -0
- package/web/assets/{layout-grid-B_pjJwz4.js → layout-grid-BBVuEdWG.js} +1 -1
- package/web/assets/{linkFollow-C0_3k_PC.js → linkFollow-B7ivDZMM.js} +4 -4
- package/web/assets/{list-tree-DkAJYEXd.js → list-tree-4-0QDbLu.js} +1 -1
- package/web/assets/{listFold-BsOVn9UZ.js → listFold-DLuxXiIj.js} +1 -1
- package/web/assets/{lock-BnGHxjeE.js → lock-CnbZm4vo.js} +1 -1
- package/web/assets/{log-out-DBuMeqik.js → log-out-BEynWaHa.js} +1 -1
- package/web/assets/{main-NgUPPqDn.css → main-BiS_5wrB.css} +1 -1
- package/web/assets/main-sIq2N9Z5.js +20 -0
- package/web/assets/markdownEditorSurface-ZSJ3RHAv.js +3 -0
- package/web/assets/mdMarkStore-DQHDjG7s.js +2 -0
- package/web/assets/{mdMarks-Bq7Hl-4C.js → mdMarks-DbIcRa0O.js} +1 -1
- package/web/assets/{message-square-plus-BiAZAvcw.js → message-square-plus-CpiKDvle.js} +1 -1
- package/web/assets/{messages-BH5MfQdZ.js → messages-_jta1lSI.js} +1 -1
- package/web/assets/minus-C9ndHJ9O.js +1 -0
- package/web/assets/mobileEntry-CPmU_c4m.js +13 -0
- package/web/assets/nativeDownload-BYT082bY.js +1 -0
- package/web/assets/{pageChrome-xLNdFaSg.css → pageChrome-CovzZPdb.css} +1 -1
- package/web/assets/pageChrome-D3Niv1JM.js +1 -0
- package/web/assets/{pageTree-CZH2J6cc.js → pageTree-CgfvpzIj.js} +2 -2
- package/web/assets/{pen-tool-lkmBqliH.js → pen-tool-BXEFnU2i.js} +1 -1
- package/web/assets/{pencil-Cvk91P_Z.js → pencil-cqvCkekG.js} +1 -1
- package/web/assets/pluginChat-BbBukMuN.js +2 -0
- package/web/assets/{redo-2-DSi54z4e.js → redo-2-BO7wCDwy.js} +1 -1
- package/web/assets/{rotate-ccw-CUlnplr0.js → rotate-ccw-BvipPIjC.js} +1 -1
- package/web/assets/{sharePage-MM6UpPJ9.js → sharePage-XbN50US7.js} +2 -2
- package/web/assets/{shareViewer-CDZ8lTdy.js → shareViewer-BaYogetW.js} +1 -1
- package/web/assets/{star-CwDiGyUW.js → star-BQWMXnn7.js} +1 -1
- package/web/assets/tableSurface-BXqqidd1.js +1 -0
- package/web/assets/{terminal-DIXogYJY.js → terminal-l9k6RDrH.js} +1 -1
- package/web/assets/{terminalView-B3aZSsrQ.js → terminalView-Cpnt5_0P.js} +1 -1
- package/web/assets/theme-C6D3tMGV.css +1 -0
- package/web/assets/type-CFoaj2eD.js +1 -0
- package/web/assets/{undo-2-tUKCv5Gs.js → undo-2-DusxpdLa.js} +1 -1
- package/web/assets/{unitAccount-D8lVwaaT.js → unitAccount-DwutFPPw.js} +1 -1
- package/web/assets/{unitBridge-D547ublk.js → unitBridge-CcgQpkqV.js} +1 -1
- package/web/assets/{unitCloudTransport-BVF6lVp4.js → unitCloudTransport-vA7cLDts.js} +1 -1
- package/web/assets/{unitOwnerAccess-BfnEjsZb.js → unitOwnerAccess-LBlJ1YfA.js} +1 -1
- package/web/assets/{unitShim-DxetRgmP.js → unitShim-B_M9IrbC.js} +3 -3
- package/web/assets/{upload-BbeAsMuD.js → upload-ZrOj6tXA.js} +1 -1
- package/web/assets/userSpaces-uJOnsedR.js +1 -0
- package/web/assets/viewportLock-D3SNkwS2.js +631 -0
- package/web/assets/viewportLock-IP1zaVkz.css +1 -0
- package/web/assets/{workflow-DuoVRSSX.js → workflow-DPoG4QE2.js} +1 -1
- package/web/assets/{zoom-in-hr5bRpDL.js → zoom-in-lbTgzdQP.js} +1 -1
- package/web/index.html +232 -13
- package/web/assets/ArtificialView-B4ESD4iK.js +0 -1
- package/web/assets/CalendarConfigView-cCfo_EvZ.js +0 -1
- package/web/assets/CalendarView-wgLapctk.js +0 -1
- package/web/assets/CapabilityMenu-BQBsM638.js +0 -1
- package/web/assets/ChatView-BIUk0OJd.css +0 -1
- package/web/assets/ChatView-C-jCJnyS.js +0 -47
- package/web/assets/Chrome-CwFiDmre.js +0 -1
- package/web/assets/DashboardGridView-DXqe0vPd.js +0 -17
- package/web/assets/ExcalidrawCanvas-C3egkikA.js +0 -1
- package/web/assets/HomepageView-CRdhjLXu.js +0 -1
- package/web/assets/ImageStudioPanels-Dke7xf8E.js +0 -3
- package/web/assets/ImageStudioView-g38V8gaj.js +0 -1
- package/web/assets/InlineFiles-C_hjvn3u.js +0 -1
- package/web/assets/MarkdownBlock-DIjYEhTK.js +0 -56
- package/web/assets/ModelPill-BPlgovPo.js +0 -1
- package/web/assets/MuseLibraryView-CenyTTim.js +0 -2
- package/web/assets/PdfAnnotator-CGzC_o1Q.css +0 -1
- package/web/assets/PdfAnnotator-D79ip27w.js +0 -560
- package/web/assets/RightPanel-C3OJTIqT.js +0 -4
- package/web/assets/RunLocationPicker-BNcZ7KKF.css +0 -1
- package/web/assets/RunLocationPicker-CTYXUH2z.js +0 -34
- package/web/assets/UnifiedPage-CKNtVD_n.js +0 -57
- package/web/assets/UnifiedPage-DX_sTpyZ.css +0 -1
- package/web/assets/appStore-Di8LXuJ9.css +0 -1
- package/web/assets/automationListSource-DP5xZBxo.js +0 -1
- package/web/assets/chatBoxSurface-zvcyfrxE.js +0 -1
- package/web/assets/dashboardSurface-CFTH5z4c.js +0 -1
- package/web/assets/ellipsis-r1EcXsFq.js +0 -1
- package/web/assets/floatingTocSurface-C6RJ6i41.js +0 -1
- package/web/assets/folder-DlxnPmWY.js +0 -1
- package/web/assets/frame-_xBVYe3P.js +0 -1
- package/web/assets/gestures-CkW1Kfs9.js +0 -1
- package/web/assets/i18n-D-eU58uw.css +0 -1
- package/web/assets/i18n.generated-Bbsz16cl.js +0 -15
- package/web/assets/imageStudio-BqbNOvpu.css +0 -1
- package/web/assets/imageStudio-C_Rs630L.js +0 -2
- package/web/assets/index-BCubCwdT.js +0 -1
- package/web/assets/index-DZfZTksa.js +0 -1
- package/web/assets/main-Dki8NlsT.js +0 -21
- package/web/assets/markdownEditorSurface-B4PeUzed.js +0 -3
- package/web/assets/mdMarkStore-CsKl7C8O.js +0 -2
- package/web/assets/minus-Cyv1YzOU.js +0 -1
- package/web/assets/mobileEntry-Bd_nKr0D.js +0 -13
- package/web/assets/nativeDownload-D-f1_L2S.js +0 -1
- package/web/assets/pageChrome-C9_azUYW.js +0 -1
- package/web/assets/panel-left-rOc8As0s.js +0 -1
- package/web/assets/pen-line-m6_HvWZ9.js +0 -1
- package/web/assets/sticky-note-BIuPUSOU.js +0 -1
- package/web/assets/tableSurface-Y_JBQsuz.js +0 -1
- package/web/assets/theme-DfEi_DND.css +0 -1
- package/web/assets/type-Cqoabh7H.js +0 -1
- package/web/assets/userSpaces-DyyAn7vj.js +0 -1
- package/web/assets/viewportLock-C4SX-9PO.js +0 -103
- package/web/assets/viewportLock-DYSe7Jsn.css +0 -1
- package/web/assets/waves-horizontal-B8ZpHQ5H.js +0 -1
package/main.mjs
CHANGED
|
@@ -1,13 +1,494 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// desktop/shared/amadeus/pluginFiles.ts
|
|
12
|
+
function isSafePluginExt(extension) {
|
|
13
|
+
if (typeof extension !== "string") return false;
|
|
14
|
+
const value = extension.trim().toLowerCase();
|
|
15
|
+
if (!value.startsWith(".") || value.length < 2 || value.length > 40) return false;
|
|
16
|
+
if (value === ".md" || value === ".markdown" || value === ".txt") return false;
|
|
17
|
+
return value.endsWith(".md") ? /^\.[a-z0-9][a-z0-9-]*\.md$/.test(value) : /^\.[a-z0-9][a-z0-9.-]*$/.test(value);
|
|
18
|
+
}
|
|
19
|
+
var init_pluginFiles = __esm({
|
|
20
|
+
"desktop/shared/amadeus/pluginFiles.ts"() {
|
|
21
|
+
"use strict";
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// desktop/shared/nativeFeatures.ts
|
|
26
|
+
function nativeFeatureSpaceIds(features) {
|
|
27
|
+
return [...new Set(features.flatMap((feature) => NATIVE_FEATURE_SPACES[feature]))];
|
|
28
|
+
}
|
|
29
|
+
var NATIVE_FEATURE_IDS, NATIVE_FEATURE_SPACES, isNativeFeatureId;
|
|
30
|
+
var init_nativeFeatures = __esm({
|
|
31
|
+
"desktop/shared/nativeFeatures.ts"() {
|
|
32
|
+
"use strict";
|
|
33
|
+
NATIVE_FEATURE_IDS = ["amadeus", "tangu", "calendar", "automation", "public"];
|
|
34
|
+
NATIVE_FEATURE_SPACES = {
|
|
35
|
+
amadeus: ["amadeus"],
|
|
36
|
+
tangu: ["tangu"],
|
|
37
|
+
calendar: ["calendar"],
|
|
38
|
+
automation: ["automation"],
|
|
39
|
+
public: ["public"]
|
|
40
|
+
};
|
|
41
|
+
isNativeFeatureId = (value) => typeof value === "string" && NATIVE_FEATURE_IDS.includes(value);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// unit/packages.ts
|
|
46
|
+
import { readFile as readFile2, readdir, realpath as realpath2, stat } from "node:fs/promises";
|
|
47
|
+
import { isAbsolute, resolve, sep as sep2 } from "node:path";
|
|
48
|
+
function versionAtLeast(actual, minimum) {
|
|
49
|
+
const parse = (v) => /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(v) ? v.split(/[.+-]/).slice(0, 3).map(Number) : null;
|
|
50
|
+
const a = parse(actual), b = parse(minimum);
|
|
51
|
+
if (!a || !b) throw new Error("Invalid semantic version");
|
|
52
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
async function packagePath(root, relative2) {
|
|
56
|
+
if (typeof relative2 !== "string" || !relative2 || isAbsolute(relative2) || relative2.split(/[\\/]/).includes("..")) throw new Error("Plugin file escapes its package");
|
|
57
|
+
const file2 = await realpath2(resolve(root, relative2));
|
|
58
|
+
if (!file2.startsWith(root + sep2)) throw new Error("Plugin file escapes its package");
|
|
59
|
+
if (!(await stat(file2)).isFile()) throw new Error("Plugin entry must be a regular file");
|
|
60
|
+
return file2;
|
|
61
|
+
}
|
|
62
|
+
async function readPackage(dir, appVersion) {
|
|
63
|
+
const root = await realpath2(dir);
|
|
64
|
+
const manifest = JSON.parse(await readFile2(await packagePath(root, "manifest.json"), "utf8"));
|
|
65
|
+
if (!validId(manifest.id) || manifest.apiVersion !== 1) throw new Error("Invalid plugin identity or unsupported plugin API");
|
|
66
|
+
versionAtLeast(manifest.version, "0.0.0");
|
|
67
|
+
if (manifest.minAppVersion && appVersion && !versionAtLeast(appVersion, manifest.minAppVersion)) throw new Error(`Plugin ${manifest.id} requires Unit ${manifest.minAppVersion}`);
|
|
68
|
+
if (manifest.requires && (!Array.isArray(manifest.requires) || !manifest.requires.every(validId))) throw new Error("Invalid plugin dependencies");
|
|
69
|
+
const result = { root, manifest, spaces: [], spaceIds: /* @__PURE__ */ new Set() };
|
|
70
|
+
if (manifest.frontend) {
|
|
71
|
+
const { features, services = {} } = manifest.frontend;
|
|
72
|
+
if (!Array.isArray(features) || !features.length || !features.every(isNativeFeatureId) || new Set(features).size !== features.length) throw new Error("Invalid native frontend features");
|
|
73
|
+
for (const [id, service] of Object.entries(services)) {
|
|
74
|
+
if (!["amadeus", "tangu"].includes(id) || !features.includes(id) || !service || service.adapter !== "forsion-cloud-v1" || typeof service.apiBase !== "string" || !/^\/(?:[A-Za-z0-9_-]+\/?)+$/.test(service.apiBase) || id === "tangu" && service.execution !== "fleet" || id === "amadeus" && service.collaboration !== void 0 && typeof service.collaboration !== "boolean") {
|
|
75
|
+
throw new Error("Invalid frontend cloud service");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const id of nativeFeatureSpaceIds(features)) result.spaceIds.add(id);
|
|
79
|
+
}
|
|
80
|
+
if (manifest.main) {
|
|
81
|
+
const code = await readFile2(await packagePath(root, manifest.main), "utf8");
|
|
82
|
+
result.ui = {
|
|
83
|
+
id: manifest.id,
|
|
84
|
+
name: manifest.name,
|
|
85
|
+
nameEn: manifest.nameEn,
|
|
86
|
+
version: manifest.version,
|
|
87
|
+
apiVersion: manifest.apiVersion,
|
|
88
|
+
minAppVersion: manifest.minAppVersion,
|
|
89
|
+
description: manifest.description,
|
|
90
|
+
descriptionEn: manifest.descriptionEn,
|
|
91
|
+
onboarding: manifest.onboarding,
|
|
92
|
+
fileExtensions: Array.isArray(manifest.fileExtensions) ? manifest.fileExtensions.filter(isSafePluginExt).map((v) => v.trim().toLowerCase()).slice(0, 8) : [],
|
|
93
|
+
code
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (manifest.runtime) {
|
|
97
|
+
if (manifest.runtime.apiVersion !== 1) throw new Error("Unsupported local runtime API");
|
|
98
|
+
if (manifest.runtime.dependencies?.mode === "npm-ci") throw new Error("Local runtime dependencies must be bundled");
|
|
99
|
+
result.runtimeEntry = await packagePath(root, manifest.runtime.main);
|
|
100
|
+
}
|
|
101
|
+
if (manifest.backend) {
|
|
102
|
+
if (manifest.backend.apiVersion !== 1) throw new Error("Unsupported backend plugin API");
|
|
103
|
+
result.backendEntry = await packagePath(root, manifest.backend.main);
|
|
104
|
+
}
|
|
105
|
+
for (const deps of [manifest.backend?.dependencies, manifest.runtime?.dependencies]) {
|
|
106
|
+
if (deps && !["bundled", "npm-ci"].includes(deps.mode)) throw new Error("Unsupported dependency mode");
|
|
107
|
+
const libc = process.platform === "linux" ? process.report?.getReport()?.header?.glibcVersionRuntime ? "glibc" : "musl" : void 0;
|
|
108
|
+
if (deps?.mode === "bundled" && (deps.platform && deps.platform !== process.platform || deps.libc && deps.libc !== libc || deps.arch && deps.arch !== process.arch || deps.nodeAbi && deps.nodeAbi !== process.versions.modules)) {
|
|
109
|
+
throw new Error("Plugin dependencies were built for a different OS, architecture, Node ABI, or libc");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!result.ui && !result.backendEntry && !result.runtimeEntry && !manifest.frontend) throw new Error("Plugin has no UI or backend entry");
|
|
113
|
+
const entries = await readdir(resolve(root, "spaces"), { withFileTypes: true }).catch((error) => {
|
|
114
|
+
if (error.code === "ENOENT") return [];
|
|
115
|
+
throw error;
|
|
116
|
+
});
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (!entry.isDirectory()) continue;
|
|
119
|
+
const json2 = await readFile2(await packagePath(root, `spaces/${entry.name}/space.json`), "utf8");
|
|
120
|
+
const spec = JSON.parse(json2);
|
|
121
|
+
if (!validId(spec.id) || result.spaceIds.has(spec.id)) throw new Error("Invalid or duplicate Space id");
|
|
122
|
+
result.spaceIds.add(spec.id);
|
|
123
|
+
result.spaces.push({ slug: entry.name, json: json2, plugin: manifest.id });
|
|
124
|
+
}
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
var validId;
|
|
128
|
+
var init_packages = __esm({
|
|
129
|
+
"unit/packages.ts"() {
|
|
130
|
+
"use strict";
|
|
131
|
+
init_pluginFiles();
|
|
132
|
+
init_nativeFeatures();
|
|
133
|
+
validId = (id) => typeof id === "string" && /^[a-z][a-z0-9-]*$/.test(id);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// unit/config.ts
|
|
138
|
+
import { readFile as readFile5, writeFile as writeFile3, rename as rename3, mkdir as mkdir3 } from "node:fs/promises";
|
|
139
|
+
import { dirname, resolve as resolve3 } from "node:path";
|
|
140
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
141
|
+
async function readConfig(file2) {
|
|
142
|
+
const path = resolve3(file2), root = dirname(path);
|
|
143
|
+
const config = JSON.parse(await readFile5(path, "utf8"));
|
|
144
|
+
if (!Array.isArray(config.plugins)) throw new Error("unit.json plugins must be an array");
|
|
145
|
+
config.webDist = resolve3(root, config.webDist);
|
|
146
|
+
config.dataDir = resolve3(root, config.dataDir || "data");
|
|
147
|
+
if (config.workspace?.path) config.workspace.path = resolve3(root, config.workspace.path);
|
|
148
|
+
if (config.workerFile) config.workerFile = resolve3(root, config.workerFile);
|
|
149
|
+
config.plugins = config.plugins.map((p) => typeof p === "string" ? { path: resolve3(root, p) } : { ...p, path: resolve3(root, p.path) });
|
|
150
|
+
return config;
|
|
151
|
+
}
|
|
152
|
+
async function writeConfig(file2, config) {
|
|
153
|
+
await mkdir3(dirname(resolve3(file2)), { recursive: true, mode: 448 });
|
|
154
|
+
const temp = `${file2}.${randomUUID5()}.tmp`;
|
|
155
|
+
await writeFile3(temp, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
156
|
+
await rename3(temp, file2);
|
|
157
|
+
}
|
|
158
|
+
var installations;
|
|
159
|
+
var init_config = __esm({
|
|
160
|
+
"unit/config.ts"() {
|
|
161
|
+
"use strict";
|
|
162
|
+
installations = (config) => config.plugins;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// unit/control.ts
|
|
167
|
+
import net from "node:net";
|
|
168
|
+
import { chmod, rm as rm2, mkdir as mkdir4 } from "node:fs/promises";
|
|
169
|
+
import { resolve as resolve4, dirname as dirname2 } from "node:path";
|
|
170
|
+
import { tmpdir } from "node:os";
|
|
171
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
172
|
+
async function sendControl(dataDir, command) {
|
|
173
|
+
return new Promise((resolveReply, reject) => {
|
|
174
|
+
const socket = net.connect(controlAddress(dataDir));
|
|
175
|
+
let body = "";
|
|
176
|
+
socket.setTimeout(24e4, () => socket.destroy(new Error("Unit control timed out")));
|
|
177
|
+
socket.once("connect", () => socket.write(JSON.stringify(command) + "\n"));
|
|
178
|
+
socket.on("data", (data) => {
|
|
179
|
+
body += data;
|
|
180
|
+
if (body.length > 1e6) {
|
|
181
|
+
socket.destroy(new Error("Oversized control response"));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!body.includes("\n")) return;
|
|
185
|
+
try {
|
|
186
|
+
const reply = JSON.parse(body.split("\n")[0]);
|
|
187
|
+
socket.end();
|
|
188
|
+
if (reply.ok) resolveReply(reply.result);
|
|
189
|
+
else reject(new Error(reply.error || "Unit command failed"));
|
|
190
|
+
} catch (error) {
|
|
191
|
+
socket.destroy();
|
|
192
|
+
reject(error);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
socket.once("error", reject);
|
|
196
|
+
socket.once("end", () => {
|
|
197
|
+
if (!body.includes("\n")) reject(new Error("Unit control connection closed"));
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async function startControl(dataDir, dispatch) {
|
|
202
|
+
await mkdir4(dataDir, { recursive: true, mode: 448 });
|
|
203
|
+
try {
|
|
204
|
+
await sendControl(dataDir, { action: "status" });
|
|
205
|
+
throw new Error("This Unit is already running");
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (!unitIsOffline(error)) throw error;
|
|
208
|
+
}
|
|
209
|
+
const address = controlAddress(dataDir);
|
|
210
|
+
if (process.platform !== "win32") {
|
|
211
|
+
await mkdir4(dirname2(address), { recursive: true, mode: 448 });
|
|
212
|
+
await chmod(dirname2(address), 448);
|
|
213
|
+
}
|
|
214
|
+
if (process.platform !== "win32") await rm2(address, { force: true });
|
|
215
|
+
const clients = /* @__PURE__ */ new Set();
|
|
216
|
+
const server = net.createServer((socket) => {
|
|
217
|
+
clients.add(socket);
|
|
218
|
+
socket.once("close", () => clients.delete(socket));
|
|
219
|
+
let body = "", dispatched = false;
|
|
220
|
+
socket.setTimeout(24e4, () => socket.destroy());
|
|
221
|
+
socket.on("data", (data) => {
|
|
222
|
+
if (dispatched) return;
|
|
223
|
+
body += data;
|
|
224
|
+
if (body.length > 1e6) {
|
|
225
|
+
socket.destroy();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!body.includes("\n")) return;
|
|
229
|
+
dispatched = true;
|
|
230
|
+
void (async () => {
|
|
231
|
+
try {
|
|
232
|
+
socket.end(JSON.stringify({ ok: true, result: await dispatch(JSON.parse(body.split("\n")[0])) }) + "\n");
|
|
233
|
+
} catch (error) {
|
|
234
|
+
socket.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : "Unit command failed" }) + "\n");
|
|
235
|
+
}
|
|
236
|
+
})();
|
|
237
|
+
});
|
|
238
|
+
socket.on("error", () => {
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
await new Promise((ready, reject) => {
|
|
242
|
+
server.once("error", reject);
|
|
243
|
+
server.listen(address, ready);
|
|
244
|
+
});
|
|
245
|
+
if (process.platform !== "win32") await chmod(address, 384);
|
|
246
|
+
return { close: async () => {
|
|
247
|
+
await new Promise((done) => {
|
|
248
|
+
server.close(() => done());
|
|
249
|
+
for (const socket of clients) socket.destroy();
|
|
250
|
+
});
|
|
251
|
+
if (process.platform !== "win32") await rm2(address, { force: true });
|
|
252
|
+
} };
|
|
253
|
+
}
|
|
254
|
+
var controlAddress, unitIsOffline;
|
|
255
|
+
var init_control = __esm({
|
|
256
|
+
"unit/control.ts"() {
|
|
257
|
+
"use strict";
|
|
258
|
+
controlAddress = (dataDir) => process.platform === "win32" ? `\\\\.\\pipe\\forsion-unit-${createHash4("sha256").update(resolve4(dataDir)).digest("hex").slice(0, 24)}` : resolve4(tmpdir(), `forsion-unit-${process.getuid?.() ?? "user"}`, createHash4("sha256").update(resolve4(dataDir)).digest("hex").slice(0, 24) + ".sock");
|
|
259
|
+
unitIsOffline = (error) => ["ENOENT", "ECONNREFUSED"].includes(error?.code || "");
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// unit/install.ts
|
|
264
|
+
import { cp, mkdir as mkdir5, readdir as readdir3, realpath as realpath4, rm as rm3, lstat, open, readlink, symlink } from "node:fs/promises";
|
|
265
|
+
import { dirname as dirname3, resolve as resolve5, sep as sep4, relative, isAbsolute as isAbsolute3 } from "node:path";
|
|
266
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
267
|
+
import { spawn } from "node:child_process";
|
|
268
|
+
async function inspectTree(dir, root, seen = /* @__PURE__ */ new Set()) {
|
|
269
|
+
const actual = await realpath4(dir);
|
|
270
|
+
if (seen.has(actual)) return;
|
|
271
|
+
seen.add(actual);
|
|
272
|
+
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
273
|
+
const path = resolve5(dir, entry.name);
|
|
274
|
+
if (entry.name === ".env" || entry.name.startsWith(".env.") && !entry.name.endsWith(".example")) throw new Error("Package contains private environment files");
|
|
275
|
+
if (entry.isSymbolicLink()) {
|
|
276
|
+
const target = await realpath4(path);
|
|
277
|
+
if (!target.startsWith(root + sep4)) throw new Error("Package symlink escapes its directory");
|
|
278
|
+
if ((await lstat(target)).isDirectory()) await inspectTree(target, root, seen);
|
|
279
|
+
} else if (entry.isDirectory()) await inspectTree(path, root, seen);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function relocateLinks(dir, source, destination) {
|
|
283
|
+
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
284
|
+
const path = resolve5(dir, entry.name);
|
|
285
|
+
if (entry.isSymbolicLink()) {
|
|
286
|
+
const link = await readlink(path);
|
|
287
|
+
if (isAbsolute3(link)) {
|
|
288
|
+
const relocated = resolve5(destination, relative(source, await realpath4(link)));
|
|
289
|
+
await rm3(path);
|
|
290
|
+
await symlink(relative(dirname3(path), relocated), path);
|
|
291
|
+
}
|
|
292
|
+
} else if (entry.isDirectory()) await relocateLinks(path, source, destination);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function copyPackage(source, destination, version) {
|
|
296
|
+
const sourceRoot = await realpath4(source);
|
|
297
|
+
if (!(await lstat(sourceRoot)).isDirectory()) throw new Error("Expected an unpacked plugin directory");
|
|
298
|
+
await readPackage(sourceRoot, version);
|
|
299
|
+
await inspectTree(sourceRoot, sourceRoot);
|
|
300
|
+
await cp(sourceRoot, destination, { recursive: true, verbatimSymlinks: true });
|
|
301
|
+
await relocateLinks(destination, sourceRoot, destination);
|
|
302
|
+
return readPackage(destination, version);
|
|
303
|
+
}
|
|
304
|
+
async function installDependencies(root, backendMain) {
|
|
305
|
+
const backend = resolve5(root, backendMain.split("/")[0]);
|
|
306
|
+
await packagePath(root, backendMain.split("/")[0] + "/package-lock.json");
|
|
307
|
+
await new Promise((done, reject) => {
|
|
308
|
+
const win = process.platform === "win32";
|
|
309
|
+
const child = spawn(win ? "cmd.exe" : "npm", win ? ["/c", "npm", "ci", "--omit=dev"] : ["ci", "--omit=dev"], { cwd: backend, stdio: "inherit", env: { ...process.env, PATH: dirname3(process.execPath) + (win ? ";" : ":") + (process.env.PATH || "") } });
|
|
310
|
+
child.once("error", reject);
|
|
311
|
+
child.once("exit", (code) => code === 0 ? done() : reject(new Error(`Plugin dependency installation failed (${code})`)));
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async function installPackage(configPath, source, options = {}) {
|
|
315
|
+
const file2 = resolve5(configPath), root = dirname3(file2);
|
|
316
|
+
const lockPath = resolve5(root, ".install.lock");
|
|
317
|
+
const lock = await open(lockPath, "wx", 384).catch(() => {
|
|
318
|
+
throw new Error("Another install is in progress");
|
|
319
|
+
});
|
|
320
|
+
let stage, committed = false;
|
|
321
|
+
try {
|
|
322
|
+
const config = await readConfig(file2), sourceRoot = await realpath4(source);
|
|
323
|
+
if (!(await lstat(sourceRoot)).isDirectory()) throw new Error("Install expects an unpacked plugin directory");
|
|
324
|
+
const pack = await readPackage(sourceRoot, config.version);
|
|
325
|
+
const entries = installations(config);
|
|
326
|
+
const existing = (await Promise.all(entries.map(async (entry) => ({ entry, pack: await readPackage(entry.path) })))).find((p) => p.pack.manifest.id === pack.manifest.id);
|
|
327
|
+
if (options.updateId !== void 0) {
|
|
328
|
+
if (pack.manifest.id !== options.updateId) throw new Error("Update changes plugin identity");
|
|
329
|
+
if (!existing) throw new Error(`Plugin is not installed: ${options.updateId}`);
|
|
330
|
+
try {
|
|
331
|
+
await sendControl(config.dataDir, { action: "status" });
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (unitIsOffline(error)) throw new Error("Unit is not running; start it before updating");
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
await inspectTree(sourceRoot, sourceRoot);
|
|
338
|
+
stage = resolve5(root, "plugins", pack.manifest.id, `${pack.manifest.version}-${randomUUID6()}`);
|
|
339
|
+
await mkdir5(dirname3(stage), { recursive: true, mode: 448 });
|
|
340
|
+
await cp(sourceRoot, stage, { recursive: true, verbatimSymlinks: true });
|
|
341
|
+
await relocateLinks(stage, sourceRoot, stage);
|
|
342
|
+
if (options.releaseDigest) {
|
|
343
|
+
const { packageDigest: packageDigest2 } = await Promise.resolve().then(() => (init_releases(), releases_exports));
|
|
344
|
+
if (pack.manifest.backend?.dependencies?.mode === "npm-ci" || pack.manifest.runtime?.dependencies?.mode === "npm-ci" || await packageDigest2(stage) !== options.releaseDigest) throw new Error("Staged release changed during installation");
|
|
345
|
+
}
|
|
346
|
+
if (pack.manifest.backend?.dependencies?.mode === "npm-ci") await installDependencies(stage, pack.manifest.backend.main);
|
|
347
|
+
const ready = await readPackage(stage, config.version);
|
|
348
|
+
if (existing) {
|
|
349
|
+
try {
|
|
350
|
+
if (options.applyUpdate) await options.applyUpdate(pack.manifest.id, stage);
|
|
351
|
+
else await sendControl(config.dataDir, { action: "update", id: pack.manifest.id, path: stage });
|
|
352
|
+
committed = true;
|
|
353
|
+
return { id: ready.manifest.id, version: ready.manifest.version, path: stage };
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (!unitIsOffline(error)) {
|
|
356
|
+
committed = true;
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
if (options.updateId !== void 0) throw new Error("Unit is not running; start it before updating");
|
|
360
|
+
}
|
|
361
|
+
existing.entry.path = stage;
|
|
362
|
+
} else {
|
|
363
|
+
try {
|
|
364
|
+
await sendControl(config.dataDir, { action: "status" });
|
|
365
|
+
throw new Error("Stop the Unit before installing a new plugin; updates can run live");
|
|
366
|
+
} catch (error) {
|
|
367
|
+
if (!unitIsOffline(error)) throw error;
|
|
368
|
+
}
|
|
369
|
+
entries.push({ path: stage, enabled: true });
|
|
370
|
+
}
|
|
371
|
+
if (!config.defaultSpace && ready.spaceIds.size) config.defaultSpace = [...ready.spaceIds][0];
|
|
372
|
+
await writeConfig(file2, config);
|
|
373
|
+
committed = true;
|
|
374
|
+
return { id: ready.manifest.id, version: ready.manifest.version, path: stage };
|
|
375
|
+
} finally {
|
|
376
|
+
await lock.close();
|
|
377
|
+
await rm3(lockPath, { force: true });
|
|
378
|
+
if (!committed && stage) await rm3(stage, { recursive: true, force: true });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
var init_install = __esm({
|
|
382
|
+
"unit/install.ts"() {
|
|
383
|
+
"use strict";
|
|
384
|
+
init_packages();
|
|
385
|
+
init_config();
|
|
386
|
+
init_control();
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// unit/releases.ts
|
|
391
|
+
var releases_exports = {};
|
|
392
|
+
__export(releases_exports, {
|
|
393
|
+
listReleases: () => listReleases,
|
|
394
|
+
packageDigest: () => packageDigest,
|
|
395
|
+
resolveRelease: () => resolveRelease,
|
|
396
|
+
stageRelease: () => stageRelease,
|
|
397
|
+
validReleaseId: () => validReleaseId
|
|
398
|
+
});
|
|
399
|
+
import { createHash as createHash5, randomUUID as randomUUID7 } from "node:crypto";
|
|
400
|
+
import { createReadStream as createReadStream2 } from "node:fs";
|
|
401
|
+
import { mkdir as mkdir6, readdir as readdir4, readFile as readFile6, writeFile as writeFile4, rename as rename4, rm as rm4, readlink as readlink2 } from "node:fs/promises";
|
|
402
|
+
import { join as join2 } from "node:path";
|
|
403
|
+
async function packageDigest(root) {
|
|
404
|
+
const hash = createHash5("sha256");
|
|
405
|
+
async function visit(dir, prefix = "") {
|
|
406
|
+
const entries = (await readdir4(dir, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
407
|
+
for (const entry of entries) {
|
|
408
|
+
const name = prefix + entry.name, path = join2(dir, entry.name);
|
|
409
|
+
if (entry.isDirectory()) {
|
|
410
|
+
hash.update(JSON.stringify(["directory", name]));
|
|
411
|
+
await visit(path, name + "/");
|
|
412
|
+
} else if (entry.isSymbolicLink()) hash.update(JSON.stringify(["link", name, await readlink2(path)]));
|
|
413
|
+
else if (entry.isFile()) {
|
|
414
|
+
const file2 = createHash5("sha256");
|
|
415
|
+
for await (const chunk of createReadStream2(path)) file2.update(chunk);
|
|
416
|
+
hash.update(JSON.stringify(["file", name, file2.digest("hex")]));
|
|
417
|
+
} else throw new Error("Release contains a non-regular file");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
await visit(root);
|
|
421
|
+
return hash.digest("hex");
|
|
422
|
+
}
|
|
423
|
+
async function listReleases(dataDir) {
|
|
424
|
+
const root = join2(dataDir, "releases");
|
|
425
|
+
let names;
|
|
426
|
+
try {
|
|
427
|
+
names = await readdir4(root);
|
|
428
|
+
} catch (e) {
|
|
429
|
+
if (e.code === "ENOENT") return [];
|
|
430
|
+
throw e;
|
|
431
|
+
}
|
|
432
|
+
const result = [];
|
|
433
|
+
for (const name of names.filter(validReleaseId).slice(0, 200)) {
|
|
434
|
+
const item = JSON.parse(await readFile6(join2(root, name, "release.json"), "utf8"));
|
|
435
|
+
if (item.releaseId !== name) throw new Error("Invalid release record");
|
|
436
|
+
result.push({ releaseId: name, pluginId: item.pluginId, version: item.version, sha256: item.sha256, createdAt: item.createdAt });
|
|
437
|
+
}
|
|
438
|
+
return result.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
439
|
+
}
|
|
440
|
+
async function stageRelease(dataDir, source, version) {
|
|
441
|
+
const root = join2(dataDir, "releases"), id = randomUUID7(), temporary = join2(root, "." + id);
|
|
442
|
+
await mkdir6(root, { recursive: true, mode: 448 });
|
|
443
|
+
if ((await listReleases(dataDir)).length >= 200) throw new Error("Release storage is full; archive unused staged packages locally");
|
|
444
|
+
await mkdir6(temporary, { mode: 448 });
|
|
445
|
+
try {
|
|
446
|
+
const pack = await copyPackage(source, join2(temporary, "package"), version);
|
|
447
|
+
if (pack.manifest.backend?.dependencies?.mode === "npm-ci" || pack.manifest.runtime?.dependencies?.mode === "npm-ci") {
|
|
448
|
+
throw new Error("Stage requires a bundled package; install and seal dependencies on the target platform first");
|
|
449
|
+
}
|
|
450
|
+
const item = {
|
|
451
|
+
releaseId: id,
|
|
452
|
+
pluginId: pack.manifest.id,
|
|
453
|
+
version: pack.manifest.version,
|
|
454
|
+
sha256: await packageDigest(pack.root),
|
|
455
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
456
|
+
};
|
|
457
|
+
await writeFile4(join2(temporary, "release.json"), JSON.stringify(item), { mode: 384 });
|
|
458
|
+
await rename4(temporary, join2(root, id));
|
|
459
|
+
return item;
|
|
460
|
+
} finally {
|
|
461
|
+
await rm4(temporary, { recursive: true, force: true });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function resolveRelease(dataDir, id, pluginId, version, verifyContents = true) {
|
|
465
|
+
if (!validReleaseId(id)) throw new Error("Invalid release ID");
|
|
466
|
+
const root = join2(dataDir, "releases", id), path = join2(root, "package");
|
|
467
|
+
const item = JSON.parse(await readFile6(join2(root, "release.json"), "utf8"));
|
|
468
|
+
const pack = await readPackage(path, version);
|
|
469
|
+
if (item.releaseId !== id || item.pluginId !== pluginId || pack.manifest.id !== pluginId || item.version !== pack.manifest.version || pack.manifest.backend?.dependencies?.mode === "npm-ci" || pack.manifest.runtime?.dependencies?.mode === "npm-ci" || verifyContents && item.sha256 !== await packageDigest(path)) throw new Error("Staged release identity or content changed");
|
|
470
|
+
return { item, path };
|
|
471
|
+
}
|
|
472
|
+
var validReleaseId;
|
|
473
|
+
var init_releases = __esm({
|
|
474
|
+
"unit/releases.ts"() {
|
|
475
|
+
"use strict";
|
|
476
|
+
init_install();
|
|
477
|
+
init_packages();
|
|
478
|
+
validReleaseId = (id) => typeof id === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(id);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
1
482
|
// unit/main.ts
|
|
2
|
-
import { mkdir as
|
|
483
|
+
import { mkdir as mkdir9, readFile as readFile9, open as open2, rm as rm5 } from "node:fs/promises";
|
|
3
484
|
import { dirname as dirname4, resolve as resolve7 } from "node:path";
|
|
4
485
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5
|
-
import { randomUUID as
|
|
486
|
+
import { randomUUID as randomUUID10 } from "node:crypto";
|
|
6
487
|
|
|
7
488
|
// unit/host.ts
|
|
8
|
-
import { readFile as
|
|
9
|
-
import { resolve as
|
|
10
|
-
import { randomUUID as
|
|
489
|
+
import { readFile as readFile8, mkdir as mkdir8 } from "node:fs/promises";
|
|
490
|
+
import { resolve as resolve6 } from "node:path";
|
|
491
|
+
import { randomUUID as randomUUID9 } from "node:crypto";
|
|
11
492
|
import { homedir } from "node:os";
|
|
12
493
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
494
|
|
|
@@ -146,6 +627,7 @@ var ENGINE_ROUTES = [
|
|
|
146
627
|
{ method: "GET", path: "/agent/agents/:slug/avatar", access: "allow", src: "routes/agents.ts", why: "Agent avatar" },
|
|
147
628
|
{ method: "POST", path: "/agent/agents/:slug/avatar", access: "deny-remote", src: "routes/agents.ts", why: "update Agent avatar" },
|
|
148
629
|
{ method: "GET", path: "/agent/agents/:slug/harness", access: "allow", src: "routes/agents.ts", why: "read HARNESS" },
|
|
630
|
+
{ method: "POST", path: "/agent/agents/:slug/harness/candidate", access: "deny-remote", src: "routes/agents.ts", why: "adopt / dismiss one harness candidate (writes HARNESS.md as the user; the route itself also refuses remote origin)" },
|
|
149
631
|
{ method: "POST", path: "/agent/agents/:slug/harness/rollback", access: "deny-remote", src: "routes/agents.ts", why: "harness rollback (design appendix C)" },
|
|
150
632
|
{ method: "GET", path: "/agent/agents/:slug/library", access: "allow", src: "routes/agents.ts", why: "Agent library listing" },
|
|
151
633
|
{ method: "DELETE", path: "/agent/agents/:slug/library/file", access: "deny-remote", src: "routes/agents.ts", why: "deletes from the Agent library" },
|
|
@@ -237,6 +719,8 @@ var ENGINE_ROUTES = [
|
|
|
237
719
|
{ method: "GET", path: "/agent/plugins/:id/settings", access: "deny-remote", src: "routes/plugins.ts", why: "plugin settings may hold secrets" },
|
|
238
720
|
{ method: "PUT", path: "/agent/plugins/:id/settings", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
239
721
|
{ method: "GET", path: "/agent/plugins/:id/source", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
722
|
+
{ method: "GET", path: "/agent/plugins/external-tools", access: "deny-remote", src: "routes/plugins.ts", why: "plugin tools for the local Desktop MCP only; the handler also refuses remote callers" },
|
|
723
|
+
{ method: "POST", path: "/agent/plugins/external-tools/call", access: "deny-remote", src: "routes/plugins.ts", why: "runs a plugin tool; local Desktop MCP only" },
|
|
240
724
|
{ method: "POST", path: "/agent/plugins/install", access: "deny-remote", src: "routes/plugins.ts", why: "installs code (design appendix C)" },
|
|
241
725
|
{ method: "POST", path: "/agent/plugins/rescan", access: "deny-remote", src: "routes/plugins.ts", why: "engine plugins are local-only" },
|
|
242
726
|
{ method: "POST", path: "/agent/reply-segments", access: "allow", src: "routes/plugins.ts", why: "chat bubble segmentation helper (read-only)" },
|
|
@@ -255,6 +739,9 @@ var ENGINE_ROUTES = [
|
|
|
255
739
|
{ method: "GET", path: "/agent/project-context/icon", access: "allow", src: "routes/projectContext.ts", why: "project icon" },
|
|
256
740
|
{ method: "POST", path: "/agent/project-context/icon", access: "deny-remote", src: "routes/projectContext.ts", why: "project icon write" },
|
|
257
741
|
{ method: "POST", path: "/agent/project-context/init", access: "deny-remote", src: "routes/projectContext.ts", why: "project instruction write (design appendix C)" },
|
|
742
|
+
{ method: "DELETE", path: "/agent/project-context/memory", access: "deny-remote", src: "routes/projectContext.ts", why: "project memory delete (host-only store; the route itself also refuses remote origin)" },
|
|
743
|
+
{ method: "POST", path: "/agent/project-context/memory/candidate", access: "deny-remote", src: "routes/projectContext.ts", why: "adopt / dismiss one pending project-memory candidate (host-only store; the route itself also refuses remote origin)" },
|
|
744
|
+
{ method: "POST", path: "/agent/project-context/memory/restore", access: "deny-remote", src: "routes/projectContext.ts", why: "put back one sentence removed by a project-memory compaction (host-only store; the route itself also refuses remote origin)" },
|
|
258
745
|
{ method: "GET", path: "/agent/project-context/settings", access: "allow", src: "routes/projectContext.ts", why: "read project settings" },
|
|
259
746
|
{ method: "PUT", path: "/agent/project-context/settings", access: "deny-remote", src: "routes/projectContext.ts", why: "project settings write (design appendix C)" },
|
|
260
747
|
{ method: "POST", path: "/agent/project-context/skills", access: "deny-remote", src: "routes/projectContext.ts", why: "project skill write (design appendix C)" },
|
|
@@ -759,7 +1246,7 @@ function startUnitWeb(deps, opts) {
|
|
|
759
1246
|
return null;
|
|
760
1247
|
}
|
|
761
1248
|
};
|
|
762
|
-
const
|
|
1249
|
+
const json2 = (res, status, body) => {
|
|
763
1250
|
const buf = Buffer.from(JSON.stringify(body));
|
|
764
1251
|
res.writeHead(status, { "Content-Type": "application/json", "Content-Length": buf.length });
|
|
765
1252
|
res.end(buf);
|
|
@@ -790,7 +1277,7 @@ function startUnitWeb(deps, opts) {
|
|
|
790
1277
|
const proxyEngine = (req, res, path, via, caller) => {
|
|
791
1278
|
const engine = deps.getEngine();
|
|
792
1279
|
if (!engine.url) {
|
|
793
|
-
|
|
1280
|
+
json2(res, 503, { detail: "\u672C\u673A\u5F15\u64CE\u672A\u5C31\u7EEA", code: "ENGINE_NOT_READY" });
|
|
794
1281
|
return;
|
|
795
1282
|
}
|
|
796
1283
|
const target = new URL(engine.url);
|
|
@@ -822,7 +1309,7 @@ function startUnitWeb(deps, opts) {
|
|
|
822
1309
|
res.on("close", () => upRes.destroy());
|
|
823
1310
|
});
|
|
824
1311
|
up.on("error", (e) => {
|
|
825
|
-
if (!res.headersSent)
|
|
1312
|
+
if (!res.headersSent) json2(res, 502, { detail: `\u672C\u673A\u5F15\u64CE\u4E0D\u53EF\u8FBE: ${e.message}` });
|
|
826
1313
|
});
|
|
827
1314
|
req.pipe(up);
|
|
828
1315
|
};
|
|
@@ -837,13 +1324,13 @@ function startUnitWeb(deps, opts) {
|
|
|
837
1324
|
if (!extname(rel)) rel = "/index.html";
|
|
838
1325
|
const norm = normalize(rel).replace(/^([/\\])+/, "");
|
|
839
1326
|
if (norm.startsWith("..")) {
|
|
840
|
-
|
|
1327
|
+
json2(res, 400, { detail: "bad path" });
|
|
841
1328
|
return;
|
|
842
1329
|
}
|
|
843
1330
|
try {
|
|
844
1331
|
const [rootReal, fileReal] = await Promise.all([realpath(dist2), realpath(`${dist2}/${norm}`)]);
|
|
845
1332
|
if (!fileReal.startsWith(rootReal + sep)) {
|
|
846
|
-
|
|
1333
|
+
json2(res, 404, { detail: "not found" });
|
|
847
1334
|
return;
|
|
848
1335
|
}
|
|
849
1336
|
let buf = await readFile(fileReal);
|
|
@@ -866,31 +1353,31 @@ function startUnitWeb(deps, opts) {
|
|
|
866
1353
|
res.writeHead(200, h);
|
|
867
1354
|
res.end(buf);
|
|
868
1355
|
} catch {
|
|
869
|
-
|
|
1356
|
+
json2(res, 404, { detail: "not found" });
|
|
870
1357
|
}
|
|
871
1358
|
};
|
|
872
1359
|
const hostFileRequest = (req, res, url) => {
|
|
873
1360
|
const info = authInfo(req);
|
|
874
1361
|
if (!info.ok) {
|
|
875
|
-
|
|
1362
|
+
json2(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
876
1363
|
return null;
|
|
877
1364
|
}
|
|
878
1365
|
return { p: String(new URL(url, "http://x").searchParams.get("path") || ""), via: info.via };
|
|
879
1366
|
};
|
|
880
1367
|
const sendHostDownload = async (res, p) => {
|
|
881
1368
|
if (!deps.openHostFile) {
|
|
882
|
-
|
|
1369
|
+
json2(res, 501, { detail: "Host file download is not available on this device", code: "HOST_DOWNLOAD_UNSUPPORTED" });
|
|
883
1370
|
return;
|
|
884
1371
|
}
|
|
885
1372
|
const f = await deps.openHostFile(p);
|
|
886
1373
|
if (!f) {
|
|
887
|
-
|
|
1374
|
+
json2(res, 404, { detail: "not readable" });
|
|
888
1375
|
return;
|
|
889
1376
|
}
|
|
890
1377
|
if (f.size > HOST_DOWNLOAD_MAX_BYTES) {
|
|
891
1378
|
await f.fh.close().catch(() => {
|
|
892
1379
|
});
|
|
893
|
-
|
|
1380
|
+
json2(res, 413, { detail: `File is larger than the ${HOST_DOWNLOAD_MAX_BYTES / 1048576} MB remote download limit`, code: "HOST_DOWNLOAD_TOO_LARGE", size: f.size, limit: HOST_DOWNLOAD_MAX_BYTES });
|
|
894
1381
|
return;
|
|
895
1382
|
}
|
|
896
1383
|
const name = p.split(/[\\/]/).filter(Boolean).pop() || "download";
|
|
@@ -935,7 +1422,7 @@ function startUnitWeb(deps, opts) {
|
|
|
935
1422
|
if (!ownerProjection && lockedNow()) {
|
|
936
1423
|
const p = plainPath(req);
|
|
937
1424
|
if (!(p === "/engine" || p.startsWith("/engine/")) && p !== "/vault/rpc" && !lockedRequestAllowed(req.method || "GET", p)) {
|
|
938
|
-
|
|
1425
|
+
json2(res, 423, REMOTE_LOCKED_BODY);
|
|
939
1426
|
return;
|
|
940
1427
|
}
|
|
941
1428
|
}
|
|
@@ -956,50 +1443,50 @@ function startUnitWeb(deps, opts) {
|
|
|
956
1443
|
if (deps.projection?.mode === "public") {
|
|
957
1444
|
if (await deps.account?.handle(path, req, res)) return;
|
|
958
1445
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
959
|
-
|
|
1446
|
+
json2(res, 405, { detail: "Read-only projection" });
|
|
960
1447
|
return;
|
|
961
1448
|
}
|
|
962
1449
|
if (path === "/unit/whoami") {
|
|
963
|
-
|
|
1450
|
+
json2(res, 200, { ok: true, scope: "shell" });
|
|
964
1451
|
return;
|
|
965
1452
|
}
|
|
966
1453
|
if (path === "/unit/plugins") {
|
|
967
|
-
|
|
1454
|
+
json2(res, 200, { appVersion: deps.meta.version, plugins: await deps.readPlugins() });
|
|
968
1455
|
return;
|
|
969
1456
|
}
|
|
970
1457
|
if (path === "/unit/spaces") {
|
|
971
|
-
|
|
1458
|
+
json2(res, 200, { spaces: await deps.readSpaces() });
|
|
972
1459
|
return;
|
|
973
1460
|
}
|
|
974
1461
|
if (path === "/unit/config") {
|
|
975
|
-
|
|
1462
|
+
json2(res, 200, { config: {} });
|
|
976
1463
|
return;
|
|
977
1464
|
}
|
|
978
1465
|
if (path === "/unit/providers") {
|
|
979
|
-
|
|
1466
|
+
json2(res, 200, { providers: [] });
|
|
980
1467
|
return;
|
|
981
1468
|
}
|
|
982
1469
|
if (path === "/unit/meta") {
|
|
983
|
-
|
|
1470
|
+
json2(res, 200, { ...deps.meta, pair: false, projection: "public", account: deps.account?.metadata(), capabilities: deps.projection?.capabilities?.() });
|
|
984
1471
|
return;
|
|
985
1472
|
}
|
|
986
1473
|
if (/^\/(?:unit|vault|engine)(?:\/|$)/.test(path)) {
|
|
987
|
-
|
|
1474
|
+
json2(res, 403, { detail: "Host capability is not published" });
|
|
988
1475
|
return;
|
|
989
1476
|
}
|
|
990
1477
|
}
|
|
991
1478
|
if (path === "/unit/meta" && req.method === "GET") {
|
|
992
|
-
|
|
1479
|
+
json2(res, 200, { ...deps.meta, pair: !deps.projection, ...deps.projection?.mode === "local" ? { projection: "local", localCapabilities: deps.projection.localCapabilities?.() } : {} });
|
|
993
1480
|
return;
|
|
994
1481
|
}
|
|
995
1482
|
if (deps.projection?.mode === "local" && path.startsWith("/unit/pair/")) {
|
|
996
|
-
|
|
1483
|
+
json2(res, 403, { detail: "Use the workspace owner access key" });
|
|
997
1484
|
return;
|
|
998
1485
|
}
|
|
999
1486
|
if (path === "/unit/pair/request" && req.method === "POST") {
|
|
1000
1487
|
const prevId = pendingByIp.get(ip);
|
|
1001
1488
|
if (prevId && pending.get(prevId)?.status === "pending") {
|
|
1002
|
-
|
|
1489
|
+
json2(res, 429, { detail: "\u5DF2\u6709\u5F85\u786E\u8BA4\u7684\u914D\u5BF9\u8BF7\u6C42", code: "PAIR_PENDING" });
|
|
1003
1490
|
return;
|
|
1004
1491
|
}
|
|
1005
1492
|
let name = "\u672A\u547D\u540D\u8BBE\u5907";
|
|
@@ -1017,7 +1504,7 @@ function startUnitWeb(deps, opts) {
|
|
|
1017
1504
|
};
|
|
1018
1505
|
pending.set(p.id, p);
|
|
1019
1506
|
pendingByIp.set(ip, p.id);
|
|
1020
|
-
|
|
1507
|
+
json2(res, 200, { requestId: p.id, code: p.code, ttlMs: PAIR_TTL_MS });
|
|
1021
1508
|
void deps.confirmPair({ name: p.name, code: p.code, ip }).then(async (ok) => {
|
|
1022
1509
|
const live = pending.get(p.id);
|
|
1023
1510
|
if (!live || live.status !== "pending" || live.expires < Date.now()) return;
|
|
@@ -1037,34 +1524,34 @@ function startUnitWeb(deps, opts) {
|
|
|
1037
1524
|
const id = String(new URL(url, "http://x").searchParams.get("id") || "");
|
|
1038
1525
|
const p = pending.get(id);
|
|
1039
1526
|
if (!p || p.expires < Date.now()) {
|
|
1040
|
-
|
|
1527
|
+
json2(res, 200, { status: "expired" });
|
|
1041
1528
|
return;
|
|
1042
1529
|
}
|
|
1043
1530
|
if (p.status === "approved" && p.token) {
|
|
1044
1531
|
const token = p.token;
|
|
1045
1532
|
pending.delete(id);
|
|
1046
1533
|
pendingByIp.delete(p.ip);
|
|
1047
|
-
|
|
1534
|
+
json2(res, 200, { status: "approved", token });
|
|
1048
1535
|
return;
|
|
1049
1536
|
}
|
|
1050
|
-
|
|
1537
|
+
json2(res, 200, { status: p.status });
|
|
1051
1538
|
return;
|
|
1052
1539
|
}
|
|
1053
1540
|
if (path === "/unit/whoami" && req.method === "GET") {
|
|
1054
1541
|
if (!authed(req)) {
|
|
1055
|
-
|
|
1542
|
+
json2(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1056
1543
|
return;
|
|
1057
1544
|
}
|
|
1058
|
-
|
|
1545
|
+
json2(res, 200, { ok: true });
|
|
1059
1546
|
return;
|
|
1060
1547
|
}
|
|
1061
1548
|
if (path === "/unit/p2p/offer" && req.method === "POST") {
|
|
1062
1549
|
if (!authed(req)) {
|
|
1063
|
-
|
|
1550
|
+
json2(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1064
1551
|
return;
|
|
1065
1552
|
}
|
|
1066
1553
|
if (!deps.p2pAnswer) {
|
|
1067
|
-
|
|
1554
|
+
json2(res, 501, { detail: "\u672C\u7AEF\u4E0D\u652F\u6301 P2P \u76F4\u8FDE", code: "P2P_UNSUPPORTED" });
|
|
1068
1555
|
return;
|
|
1069
1556
|
}
|
|
1070
1557
|
let offer = "";
|
|
@@ -1073,38 +1560,38 @@ function startUnitWeb(deps, opts) {
|
|
|
1073
1560
|
} catch {
|
|
1074
1561
|
}
|
|
1075
1562
|
if (!offer) {
|
|
1076
|
-
|
|
1563
|
+
json2(res, 400, { detail: "\u7F3A offer SDP" });
|
|
1077
1564
|
return;
|
|
1078
1565
|
}
|
|
1079
1566
|
try {
|
|
1080
|
-
|
|
1567
|
+
json2(res, 200, { sdp: await deps.p2pAnswer(offer) });
|
|
1081
1568
|
} catch (e) {
|
|
1082
|
-
|
|
1569
|
+
json2(res, 500, { detail: `P2P \u5E94\u7B54\u5931\u8D25: ${e?.message || e}` });
|
|
1083
1570
|
}
|
|
1084
1571
|
return;
|
|
1085
1572
|
}
|
|
1086
1573
|
if (path === "/unit/plugins" && req.method === "GET") {
|
|
1087
1574
|
if (!authed(req)) {
|
|
1088
|
-
|
|
1575
|
+
json2(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1089
1576
|
return;
|
|
1090
1577
|
}
|
|
1091
|
-
|
|
1578
|
+
json2(res, 200, { appVersion: deps.meta.version, plugins: await deps.readPlugins() });
|
|
1092
1579
|
return;
|
|
1093
1580
|
}
|
|
1094
1581
|
if (path === "/unit/spaces" && req.method === "GET") {
|
|
1095
1582
|
if (!authed(req)) {
|
|
1096
|
-
|
|
1583
|
+
json2(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1097
1584
|
return;
|
|
1098
1585
|
}
|
|
1099
|
-
|
|
1586
|
+
json2(res, 200, { spaces: await deps.readSpaces() });
|
|
1100
1587
|
return;
|
|
1101
1588
|
}
|
|
1102
1589
|
if (path === "/unit/providers" && req.method === "GET") {
|
|
1103
1590
|
if (!authed(req)) {
|
|
1104
|
-
|
|
1591
|
+
json2(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1105
1592
|
return;
|
|
1106
1593
|
}
|
|
1107
|
-
|
|
1594
|
+
json2(res, 200, { providers: await deps.readProviders() });
|
|
1108
1595
|
return;
|
|
1109
1596
|
}
|
|
1110
1597
|
if ((path === "/unit/hostfile" || path === "/unit/hostfile/download") && req.method === "GET") {
|
|
@@ -1116,78 +1603,78 @@ function startUnitWeb(deps, opts) {
|
|
|
1116
1603
|
}
|
|
1117
1604
|
const f = await deps.readHostFile(q.p, q.via === "tunnel" ? 4 * 1024 * 1024 : void 0);
|
|
1118
1605
|
if (!f) {
|
|
1119
|
-
|
|
1606
|
+
json2(res, 404, { detail: "not readable" });
|
|
1120
1607
|
return;
|
|
1121
1608
|
}
|
|
1122
|
-
|
|
1609
|
+
json2(res, 200, f);
|
|
1123
1610
|
return;
|
|
1124
1611
|
}
|
|
1125
1612
|
if ((path === "/unit/hostdir" || path === "/unit/hoststat") && req.method === "GET") {
|
|
1126
1613
|
if (!authed(req)) {
|
|
1127
|
-
|
|
1614
|
+
json2(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1128
1615
|
return;
|
|
1129
1616
|
}
|
|
1130
1617
|
const p = String(new URL(url, "http://x").searchParams.get("path") || "");
|
|
1131
1618
|
const r = path === "/unit/hostdir" ? await deps.readHostDir(p) : await deps.readHostStat(p);
|
|
1132
1619
|
if (!r) {
|
|
1133
|
-
|
|
1620
|
+
json2(res, 404, { detail: "not readable" });
|
|
1134
1621
|
return;
|
|
1135
1622
|
}
|
|
1136
|
-
|
|
1623
|
+
json2(res, 200, path === "/unit/hostdir" ? { entries: r } : r);
|
|
1137
1624
|
return;
|
|
1138
1625
|
}
|
|
1139
1626
|
if (path === "/unit/config" && (req.method === "GET" || req.method === "PUT")) {
|
|
1140
1627
|
if (!authed(req)) {
|
|
1141
|
-
|
|
1628
|
+
json2(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1142
1629
|
return;
|
|
1143
1630
|
}
|
|
1144
1631
|
if (req.method === "GET") {
|
|
1145
|
-
|
|
1632
|
+
json2(res, 200, { config: await deps.readConfig() });
|
|
1146
1633
|
return;
|
|
1147
1634
|
}
|
|
1148
1635
|
let patch;
|
|
1149
1636
|
try {
|
|
1150
1637
|
patch = JSON.parse(await readBody(req));
|
|
1151
1638
|
} catch {
|
|
1152
|
-
|
|
1639
|
+
json2(res, 400, { detail: "bad body" });
|
|
1153
1640
|
return;
|
|
1154
1641
|
}
|
|
1155
|
-
|
|
1642
|
+
json2(res, 200, { config: await deps.writeConfig(patch && typeof patch === "object" ? patch : {}) });
|
|
1156
1643
|
return;
|
|
1157
1644
|
}
|
|
1158
1645
|
if (path === "/unit/remote-access" && req.method === "GET" || path === "/unit/remote-access/request" && req.method === "POST") {
|
|
1159
1646
|
const info = authInfo(req);
|
|
1160
1647
|
if (!info.ok) {
|
|
1161
|
-
|
|
1648
|
+
json2(res, 401, { detail: "Not paired", code: "UNPAIRED" });
|
|
1162
1649
|
return;
|
|
1163
1650
|
}
|
|
1164
1651
|
const rc = resolveCaller(req, info);
|
|
1165
1652
|
if (!rc.ok) {
|
|
1166
|
-
|
|
1653
|
+
json2(res, 403, BAD_CALLER_BODY);
|
|
1167
1654
|
return;
|
|
1168
1655
|
}
|
|
1169
1656
|
const caller = callerOf(info.via, pairInfo(info), rc.caller);
|
|
1170
1657
|
const ra = deps.remoteAccess;
|
|
1171
1658
|
if (!ra) {
|
|
1172
|
-
|
|
1659
|
+
json2(res, 200, { remoteSessions: false, principal: caller.kind === "paired" ? "lan" : caller.kind, caller: caller.kind === "paired" ? "paired" : "unconfirmed", maxApprovalMode: "auto-edit" });
|
|
1173
1660
|
return;
|
|
1174
1661
|
}
|
|
1175
|
-
|
|
1662
|
+
json2(res, 200, path === "/unit/remote-access" ? ra.status(caller) : await ra.request(caller));
|
|
1176
1663
|
return;
|
|
1177
1664
|
}
|
|
1178
1665
|
if (path === "/engine" || path.startsWith("/engine/")) {
|
|
1179
1666
|
const method = req.method || "GET";
|
|
1180
1667
|
const info = authInfo(req);
|
|
1181
1668
|
if (!info.ok) {
|
|
1182
|
-
|
|
1669
|
+
json2(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1183
1670
|
return;
|
|
1184
1671
|
}
|
|
1185
1672
|
if (isLocalOnlyEnginePath(url)) {
|
|
1186
1673
|
if (!engineTarget(url.slice("/engine".length))) {
|
|
1187
|
-
|
|
1674
|
+
json2(res, 400, { detail: "Ambiguous engine path", code: "BAD_PATH" });
|
|
1188
1675
|
return;
|
|
1189
1676
|
}
|
|
1190
|
-
|
|
1677
|
+
json2(res, 403, LOCAL_ONLY_BODY);
|
|
1191
1678
|
return;
|
|
1192
1679
|
}
|
|
1193
1680
|
if (ownerProjection) {
|
|
@@ -1196,26 +1683,26 @@ function startUnitWeb(deps, opts) {
|
|
|
1196
1683
|
}
|
|
1197
1684
|
const target = engineTarget(url.slice("/engine".length));
|
|
1198
1685
|
if (!target) {
|
|
1199
|
-
|
|
1686
|
+
json2(res, 400, { detail: "Ambiguous engine path", code: "BAD_PATH" });
|
|
1200
1687
|
return;
|
|
1201
1688
|
}
|
|
1202
1689
|
if (engineRouteAccess(method, target.path) !== "allow") {
|
|
1203
|
-
|
|
1690
|
+
json2(res, 403, LOCAL_ONLY_BODY);
|
|
1204
1691
|
return;
|
|
1205
1692
|
}
|
|
1206
1693
|
const rc = resolveCaller(req, info);
|
|
1207
1694
|
if (!rc.ok) {
|
|
1208
|
-
|
|
1695
|
+
json2(res, 403, BAD_CALLER_BODY);
|
|
1209
1696
|
return;
|
|
1210
1697
|
}
|
|
1211
1698
|
if (lockedNow() && !lockedEngineAllowed(method, target.path)) {
|
|
1212
|
-
|
|
1699
|
+
json2(res, 423, REMOTE_LOCKED_BODY);
|
|
1213
1700
|
return;
|
|
1214
1701
|
}
|
|
1215
1702
|
const caller = callerOf(info.via, pairInfo(info), rc.caller);
|
|
1216
1703
|
const g = deps.remoteAccess ? deps.remoteAccess.gateEngine({ method, path: target.path, via: info.via, caller }) : baseTierOnly(method, target.path);
|
|
1217
1704
|
if (!g.ok) {
|
|
1218
|
-
|
|
1705
|
+
json2(res, g.status, g.body);
|
|
1219
1706
|
return;
|
|
1220
1707
|
}
|
|
1221
1708
|
proxyEngine(req, res, target.path + target.query, info.via, rc.caller);
|
|
@@ -1224,7 +1711,7 @@ function startUnitWeb(deps, opts) {
|
|
|
1224
1711
|
if (path.startsWith("/vault/")) {
|
|
1225
1712
|
const vault = deps.vault();
|
|
1226
1713
|
if (!vault) {
|
|
1227
|
-
|
|
1714
|
+
json2(res, 503, { detail: "\u672C\u673A\u667A\u5E93\u9762\u672A\u5C31\u7EEA", code: "VAULT_NOT_READY" });
|
|
1228
1715
|
return;
|
|
1229
1716
|
}
|
|
1230
1717
|
const q = new URL(url, "http://x").searchParams;
|
|
@@ -1232,52 +1719,52 @@ function startUnitWeb(deps, opts) {
|
|
|
1232
1719
|
const assetAuthed = () => at !== "" && assetTokenLive(at) || authed(req);
|
|
1233
1720
|
if (path === "/vault/rpc" && req.method === "POST") {
|
|
1234
1721
|
if (!authed(req)) {
|
|
1235
|
-
|
|
1722
|
+
json2(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1236
1723
|
return;
|
|
1237
1724
|
}
|
|
1238
1725
|
let body;
|
|
1239
1726
|
try {
|
|
1240
1727
|
body = JSON.parse(await readBody(req, 32 * 1024 * 1024));
|
|
1241
1728
|
} catch {
|
|
1242
|
-
|
|
1729
|
+
json2(res, 400, { detail: "bad body" });
|
|
1243
1730
|
return;
|
|
1244
1731
|
}
|
|
1245
1732
|
const ch = String(body.ch || "");
|
|
1246
1733
|
if (!VAULT_RPC_ALLOW.has(ch)) {
|
|
1247
|
-
|
|
1734
|
+
json2(res, 400, { detail: `\u901A\u9053\u4E0D\u53EF\u8FDC\u7A0B\u8C03\u7528: ${ch}`, code: "VAULT_CH_DENIED" });
|
|
1248
1735
|
return;
|
|
1249
1736
|
}
|
|
1250
1737
|
if (!ownerProjection && VAULT_RPC_LOCAL_ONLY.has(ch)) {
|
|
1251
|
-
|
|
1738
|
+
json2(res, 403, LOCAL_ONLY_BODY);
|
|
1252
1739
|
return;
|
|
1253
1740
|
}
|
|
1254
1741
|
if (!ownerProjection && lockedNow() && !VAULT_RPC_READ.has(ch)) {
|
|
1255
|
-
|
|
1742
|
+
json2(res, 423, REMOTE_LOCKED_BODY);
|
|
1256
1743
|
return;
|
|
1257
1744
|
}
|
|
1258
1745
|
const origin = String(body.client || "").slice(0, 64) || null;
|
|
1259
1746
|
try {
|
|
1260
1747
|
const result = await vault.call(ch, decodeRpcArgs(Array.isArray(body.args) ? body.args : []), origin);
|
|
1261
|
-
|
|
1748
|
+
json2(res, 200, { ok: true, result: encodeRpcResult(result) ?? null });
|
|
1262
1749
|
} catch (e) {
|
|
1263
|
-
|
|
1750
|
+
json2(res, 200, { ok: false, error: String(e?.message || e) });
|
|
1264
1751
|
}
|
|
1265
1752
|
return;
|
|
1266
1753
|
}
|
|
1267
1754
|
if (path === "/vault/asset-token" && req.method === "POST") {
|
|
1268
1755
|
const info = authInfo(req);
|
|
1269
1756
|
if (!info.ok) {
|
|
1270
|
-
|
|
1757
|
+
json2(res, 401, { detail: "\u672A\u914D\u5BF9", code: "UNPAIRED" });
|
|
1271
1758
|
return;
|
|
1272
1759
|
}
|
|
1273
1760
|
const token = randomBytes(16).toString("hex");
|
|
1274
1761
|
assetTokens.set(token, { exp: Date.now() + ASSET_TTL_MS, pairHash: info.pairHash });
|
|
1275
|
-
|
|
1762
|
+
json2(res, 200, { token, ttlSec: ASSET_TTL_MS / 1e3 });
|
|
1276
1763
|
return;
|
|
1277
1764
|
}
|
|
1278
1765
|
if (path === "/vault/asset" && (req.method === "GET" || req.method === "HEAD")) {
|
|
1279
1766
|
if (!assetAuthed()) {
|
|
1280
|
-
|
|
1767
|
+
json2(res, 401, { detail: "\u8D44\u6E90\u4EE4\u724C\u65E0\u6548", code: "ASSET_TOKEN" });
|
|
1281
1768
|
return;
|
|
1282
1769
|
}
|
|
1283
1770
|
const exact = q.get("path");
|
|
@@ -1288,12 +1775,12 @@ function startUnitWeb(deps, opts) {
|
|
|
1288
1775
|
}
|
|
1289
1776
|
const real = abs ? await underVaultReal(abs, vault.root()) : null;
|
|
1290
1777
|
if (!real) {
|
|
1291
|
-
|
|
1778
|
+
json2(res, 404, { detail: "not found" });
|
|
1292
1779
|
return;
|
|
1293
1780
|
}
|
|
1294
1781
|
const stream = createReadStream(real);
|
|
1295
1782
|
stream.on("error", () => {
|
|
1296
|
-
if (!res.headersSent)
|
|
1783
|
+
if (!res.headersSent) json2(res, 404, { detail: "not found" });
|
|
1297
1784
|
else res.destroy();
|
|
1298
1785
|
});
|
|
1299
1786
|
stream.once("open", () => {
|
|
@@ -1308,7 +1795,7 @@ function startUnitWeb(deps, opts) {
|
|
|
1308
1795
|
}
|
|
1309
1796
|
if (path === "/vault/events" && req.method === "GET") {
|
|
1310
1797
|
if (!assetAuthed()) {
|
|
1311
|
-
|
|
1798
|
+
json2(res, 401, { detail: "\u8D44\u6E90\u4EE4\u724C\u65E0\u6548", code: "ASSET_TOKEN" });
|
|
1312
1799
|
return;
|
|
1313
1800
|
}
|
|
1314
1801
|
const viaAt = at !== "" && assetTokenLive(at);
|
|
@@ -1342,11 +1829,11 @@ function startUnitWeb(deps, opts) {
|
|
|
1342
1829
|
});
|
|
1343
1830
|
return;
|
|
1344
1831
|
}
|
|
1345
|
-
|
|
1832
|
+
json2(res, 404, { detail: "not found" });
|
|
1346
1833
|
return;
|
|
1347
1834
|
}
|
|
1348
1835
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1349
|
-
|
|
1836
|
+
json2(res, 405, { detail: "method not allowed" });
|
|
1350
1837
|
return;
|
|
1351
1838
|
}
|
|
1352
1839
|
await serveStatic(res, path);
|
|
@@ -1354,7 +1841,7 @@ function startUnitWeb(deps, opts) {
|
|
|
1354
1841
|
const server = http.createServer((req, res) => {
|
|
1355
1842
|
void handler(req, res).catch((e) => {
|
|
1356
1843
|
deps.log(`[unit-web] \u8BF7\u6C42\u5904\u7406\u5931\u8D25: ${e?.message || e}`);
|
|
1357
|
-
if (!res.headersSent)
|
|
1844
|
+
if (!res.headersSent) json2(res, 500, { detail: "internal error" });
|
|
1358
1845
|
});
|
|
1359
1846
|
});
|
|
1360
1847
|
server.requestTimeout = 60 * 60 * 1e3;
|
|
@@ -1398,114 +1885,8 @@ var basic_default = {
|
|
|
1398
1885
|
unit: true
|
|
1399
1886
|
};
|
|
1400
1887
|
|
|
1401
|
-
// unit/
|
|
1402
|
-
|
|
1403
|
-
import { isAbsolute, resolve, sep as sep2 } from "node:path";
|
|
1404
|
-
|
|
1405
|
-
// desktop/shared/amadeus/pluginFiles.ts
|
|
1406
|
-
function isSafePluginExt(extension) {
|
|
1407
|
-
if (typeof extension !== "string") return false;
|
|
1408
|
-
const value = extension.trim().toLowerCase();
|
|
1409
|
-
if (!value.startsWith(".") || value.length < 2 || value.length > 40) return false;
|
|
1410
|
-
if (value === ".md" || value === ".markdown" || value === ".txt") return false;
|
|
1411
|
-
return value.endsWith(".md") ? /^\.[a-z0-9][a-z0-9-]*\.md$/.test(value) : /^\.[a-z0-9][a-z0-9.-]*$/.test(value);
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// desktop/shared/nativeFeatures.ts
|
|
1415
|
-
var NATIVE_FEATURE_IDS = ["amadeus", "tangu", "calendar", "automation", "public"];
|
|
1416
|
-
var NATIVE_FEATURE_SPACES = {
|
|
1417
|
-
amadeus: ["amadeus"],
|
|
1418
|
-
tangu: ["tangu"],
|
|
1419
|
-
calendar: ["calendar"],
|
|
1420
|
-
automation: ["automation"],
|
|
1421
|
-
public: ["public"]
|
|
1422
|
-
};
|
|
1423
|
-
var isNativeFeatureId = (value) => typeof value === "string" && NATIVE_FEATURE_IDS.includes(value);
|
|
1424
|
-
function nativeFeatureSpaceIds(features) {
|
|
1425
|
-
return [...new Set(features.flatMap((feature) => NATIVE_FEATURE_SPACES[feature]))];
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
// unit/packages.ts
|
|
1429
|
-
var validId = (id) => typeof id === "string" && /^[a-z][a-z0-9-]*$/.test(id);
|
|
1430
|
-
function versionAtLeast(actual, minimum) {
|
|
1431
|
-
const parse = (v) => /^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(v) ? v.split(/[.+-]/).slice(0, 3).map(Number) : null;
|
|
1432
|
-
const a = parse(actual), b = parse(minimum);
|
|
1433
|
-
if (!a || !b) throw new Error("Invalid semantic version");
|
|
1434
|
-
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
|
|
1435
|
-
return true;
|
|
1436
|
-
}
|
|
1437
|
-
async function packagePath(root, relative2) {
|
|
1438
|
-
if (typeof relative2 !== "string" || !relative2 || isAbsolute(relative2) || relative2.split(/[\\/]/).includes("..")) throw new Error("Plugin file escapes its package");
|
|
1439
|
-
const file2 = await realpath2(resolve(root, relative2));
|
|
1440
|
-
if (!file2.startsWith(root + sep2)) throw new Error("Plugin file escapes its package");
|
|
1441
|
-
if (!(await stat(file2)).isFile()) throw new Error("Plugin entry must be a regular file");
|
|
1442
|
-
return file2;
|
|
1443
|
-
}
|
|
1444
|
-
async function readPackage(dir, appVersion) {
|
|
1445
|
-
const root = await realpath2(dir);
|
|
1446
|
-
const manifest = JSON.parse(await readFile2(await packagePath(root, "manifest.json"), "utf8"));
|
|
1447
|
-
if (!validId(manifest.id) || manifest.apiVersion !== 1) throw new Error("Invalid plugin identity or unsupported plugin API");
|
|
1448
|
-
versionAtLeast(manifest.version, "0.0.0");
|
|
1449
|
-
if (manifest.minAppVersion && appVersion && !versionAtLeast(appVersion, manifest.minAppVersion)) throw new Error(`Plugin ${manifest.id} requires Unit ${manifest.minAppVersion}`);
|
|
1450
|
-
if (manifest.requires && (!Array.isArray(manifest.requires) || !manifest.requires.every(validId))) throw new Error("Invalid plugin dependencies");
|
|
1451
|
-
const result = { root, manifest, spaces: [], spaceIds: /* @__PURE__ */ new Set() };
|
|
1452
|
-
if (manifest.frontend) {
|
|
1453
|
-
const { features, services = {} } = manifest.frontend;
|
|
1454
|
-
if (!Array.isArray(features) || !features.length || !features.every(isNativeFeatureId) || new Set(features).size !== features.length) throw new Error("Invalid native frontend features");
|
|
1455
|
-
for (const [id, service] of Object.entries(services)) {
|
|
1456
|
-
if (!["amadeus", "tangu"].includes(id) || !features.includes(id) || !service || service.adapter !== "forsion-cloud-v1" || typeof service.apiBase !== "string" || !/^\/(?:[A-Za-z0-9_-]+\/?)+$/.test(service.apiBase) || id === "tangu" && service.execution !== "fleet" || id === "amadeus" && service.collaboration !== void 0 && typeof service.collaboration !== "boolean") {
|
|
1457
|
-
throw new Error("Invalid frontend cloud service");
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
for (const id of nativeFeatureSpaceIds(features)) result.spaceIds.add(id);
|
|
1461
|
-
}
|
|
1462
|
-
if (manifest.main) {
|
|
1463
|
-
const code = await readFile2(await packagePath(root, manifest.main), "utf8");
|
|
1464
|
-
result.ui = {
|
|
1465
|
-
id: manifest.id,
|
|
1466
|
-
name: manifest.name,
|
|
1467
|
-
nameEn: manifest.nameEn,
|
|
1468
|
-
version: manifest.version,
|
|
1469
|
-
apiVersion: manifest.apiVersion,
|
|
1470
|
-
minAppVersion: manifest.minAppVersion,
|
|
1471
|
-
description: manifest.description,
|
|
1472
|
-
descriptionEn: manifest.descriptionEn,
|
|
1473
|
-
onboarding: manifest.onboarding,
|
|
1474
|
-
fileExtensions: Array.isArray(manifest.fileExtensions) ? manifest.fileExtensions.filter(isSafePluginExt).map((v) => v.trim().toLowerCase()).slice(0, 8) : [],
|
|
1475
|
-
code
|
|
1476
|
-
};
|
|
1477
|
-
}
|
|
1478
|
-
if (manifest.runtime) {
|
|
1479
|
-
if (manifest.runtime.apiVersion !== 1) throw new Error("Unsupported local runtime API");
|
|
1480
|
-
if (manifest.runtime.dependencies?.mode === "npm-ci") throw new Error("Local runtime dependencies must be bundled");
|
|
1481
|
-
result.runtimeEntry = await packagePath(root, manifest.runtime.main);
|
|
1482
|
-
}
|
|
1483
|
-
if (manifest.backend) {
|
|
1484
|
-
if (manifest.backend.apiVersion !== 1) throw new Error("Unsupported backend plugin API");
|
|
1485
|
-
result.backendEntry = await packagePath(root, manifest.backend.main);
|
|
1486
|
-
}
|
|
1487
|
-
for (const deps of [manifest.backend?.dependencies, manifest.runtime?.dependencies]) {
|
|
1488
|
-
if (deps && !["bundled", "npm-ci"].includes(deps.mode)) throw new Error("Unsupported dependency mode");
|
|
1489
|
-
const libc = process.platform === "linux" ? process.report?.getReport()?.header?.glibcVersionRuntime ? "glibc" : "musl" : void 0;
|
|
1490
|
-
if (deps?.mode === "bundled" && (deps.platform && deps.platform !== process.platform || deps.libc && deps.libc !== libc || deps.arch && deps.arch !== process.arch || deps.nodeAbi && deps.nodeAbi !== process.versions.modules)) {
|
|
1491
|
-
throw new Error("Plugin dependencies were built for a different OS, architecture, Node ABI, or libc");
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
if (!result.ui && !result.backendEntry && !result.runtimeEntry && !manifest.frontend) throw new Error("Plugin has no UI or backend entry");
|
|
1495
|
-
const entries = await readdir(resolve(root, "spaces"), { withFileTypes: true }).catch((error) => {
|
|
1496
|
-
if (error.code === "ENOENT") return [];
|
|
1497
|
-
throw error;
|
|
1498
|
-
});
|
|
1499
|
-
for (const entry of entries) {
|
|
1500
|
-
if (!entry.isDirectory()) continue;
|
|
1501
|
-
const json = await readFile2(await packagePath(root, `spaces/${entry.name}/space.json`), "utf8");
|
|
1502
|
-
const spec = JSON.parse(json);
|
|
1503
|
-
if (!validId(spec.id) || result.spaceIds.has(spec.id)) throw new Error("Invalid or duplicate Space id");
|
|
1504
|
-
result.spaceIds.add(spec.id);
|
|
1505
|
-
result.spaces.push({ slug: entry.name, json, plugin: manifest.id });
|
|
1506
|
-
}
|
|
1507
|
-
return result;
|
|
1508
|
-
}
|
|
1888
|
+
// unit/host.ts
|
|
1889
|
+
init_packages();
|
|
1509
1890
|
|
|
1510
1891
|
// unit/backendRunner.ts
|
|
1511
1892
|
import http2 from "node:http";
|
|
@@ -1884,7 +2265,7 @@ var BackendSession = class {
|
|
|
1884
2265
|
return;
|
|
1885
2266
|
}
|
|
1886
2267
|
try {
|
|
1887
|
-
this.worker.postMessage({ type: "account-resolve", requestId, token });
|
|
2268
|
+
this.worker.postMessage({ type: "account-resolve", requestId, token, purpose: options.purpose });
|
|
1888
2269
|
} catch {
|
|
1889
2270
|
finish(null, new Error("Account provider unavailable"));
|
|
1890
2271
|
}
|
|
@@ -2062,7 +2443,7 @@ var AccountChanged = class extends Error {
|
|
|
2062
2443
|
var preferences = new Set(UNIT_PREFERENCE_KEYS);
|
|
2063
2444
|
function createAccountHttp(options) {
|
|
2064
2445
|
const writes = /* @__PURE__ */ new Map();
|
|
2065
|
-
const
|
|
2446
|
+
const json2 = (res, status, body) => {
|
|
2066
2447
|
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
2067
2448
|
res.end(JSON.stringify(body));
|
|
2068
2449
|
};
|
|
@@ -2106,33 +2487,37 @@ function createAccountHttp(options) {
|
|
|
2106
2487
|
res.once("close", onClose);
|
|
2107
2488
|
try {
|
|
2108
2489
|
const bearer = /^Bearer (\S+)$/i.exec(String(req.headers.authorization || ""))?.[1];
|
|
2109
|
-
const identity = bearer ? await provider.account.resolve(bearer, { signal: controller.signal }) : null;
|
|
2490
|
+
const identity = bearer ? await provider.account.resolve(bearer, { signal: controller.signal, purpose: "administration" }) : null;
|
|
2110
2491
|
if (!identity) {
|
|
2111
|
-
|
|
2492
|
+
json2(res, 401, { error: "Authentication required" });
|
|
2112
2493
|
return true;
|
|
2113
2494
|
}
|
|
2114
2495
|
if (options.provider()?.account !== provider.account) {
|
|
2115
|
-
|
|
2496
|
+
json2(res, 503, { error: "Account provider changed" });
|
|
2116
2497
|
return true;
|
|
2117
2498
|
}
|
|
2118
2499
|
if (path === "/unit/account") {
|
|
2119
|
-
|
|
2500
|
+
json2(res, req.method === "GET" ? 200 : 405, req.method === "GET" ? identity : { error: "Method not allowed" });
|
|
2120
2501
|
return true;
|
|
2121
2502
|
}
|
|
2122
2503
|
const plugin = path.startsWith("/unit/plugin-data/") ? path.slice("/unit/plugin-data/".length) : null;
|
|
2123
2504
|
if (plugin !== null && (!/^[a-zA-Z0-9_-]+$/.test(plugin) || !options.pluginActive(plugin))) {
|
|
2124
|
-
|
|
2505
|
+
json2(res, 404, { error: "Plugin is not active" });
|
|
2125
2506
|
return true;
|
|
2126
2507
|
}
|
|
2127
2508
|
if (req.method !== "GET" && req.method !== "PUT") {
|
|
2128
|
-
|
|
2509
|
+
json2(res, 405, { error: "Method not allowed" });
|
|
2510
|
+
return true;
|
|
2511
|
+
}
|
|
2512
|
+
if (identity.role === "ADMIN_SCOPED") {
|
|
2513
|
+
json2(res, 403, { error: "MCP tokens cannot access personal storage" });
|
|
2129
2514
|
return true;
|
|
2130
2515
|
}
|
|
2131
2516
|
const dir = join(options.dataDir, "accounts", digest([provider.id, identity.userId, identity.tenantId, identity.workspaceId]));
|
|
2132
2517
|
const file2 = join(dir, plugin === null ? "preferences.json" : digest(plugin) + ".json");
|
|
2133
2518
|
if (req.method === "GET") {
|
|
2134
2519
|
const value2 = await read(file2);
|
|
2135
|
-
|
|
2520
|
+
json2(res, 200, plugin === null ? { config: value2 || {} } : { data: value2 });
|
|
2136
2521
|
return true;
|
|
2137
2522
|
}
|
|
2138
2523
|
let size = 0;
|
|
@@ -2140,7 +2525,7 @@ function createAccountHttp(options) {
|
|
|
2140
2525
|
for await (const chunk of req) {
|
|
2141
2526
|
size += Buffer.byteLength(chunk);
|
|
2142
2527
|
if (size > 1024 * 1024) {
|
|
2143
|
-
|
|
2528
|
+
json2(res, 413, { error: "Data exceeds 1 MB" });
|
|
2144
2529
|
return true;
|
|
2145
2530
|
}
|
|
2146
2531
|
chunks.push(Buffer.from(chunk));
|
|
@@ -2149,16 +2534,16 @@ function createAccountHttp(options) {
|
|
|
2149
2534
|
try {
|
|
2150
2535
|
input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
2151
2536
|
} catch {
|
|
2152
|
-
|
|
2537
|
+
json2(res, 400, { error: "Invalid JSON" });
|
|
2153
2538
|
return true;
|
|
2154
2539
|
}
|
|
2155
2540
|
if (!input || typeof input !== "object" || Array.isArray(input) || plugin !== null && typeof input.data !== "string") {
|
|
2156
|
-
|
|
2541
|
+
json2(res, 400, { error: "Invalid data" });
|
|
2157
2542
|
return true;
|
|
2158
2543
|
}
|
|
2159
2544
|
const guard = async () => {
|
|
2160
2545
|
controller.signal.throwIfAborted();
|
|
2161
|
-
const current = await provider.account.resolve(bearer, { signal: controller.signal });
|
|
2546
|
+
const current = await provider.account.resolve(bearer, { signal: controller.signal, purpose: "administration" });
|
|
2162
2547
|
if (!current || digest(current) !== digest(identity) || options.provider()?.account !== provider.account) {
|
|
2163
2548
|
throw new AccountChanged("Account changed");
|
|
2164
2549
|
}
|
|
@@ -2166,10 +2551,10 @@ function createAccountHttp(options) {
|
|
|
2166
2551
|
await guard();
|
|
2167
2552
|
await mkdir(dir, { recursive: true, mode: 448 });
|
|
2168
2553
|
const value = await write(file2, (previous) => plugin === null ? { ...previous, ...Object.fromEntries(Object.entries(input).filter(([key]) => preferences.has(key))) } : input.data, guard);
|
|
2169
|
-
|
|
2554
|
+
json2(res, 200, plugin === null ? { config: value } : { ok: true });
|
|
2170
2555
|
return true;
|
|
2171
2556
|
} catch (error) {
|
|
2172
|
-
if (!res.headersSent && !res.destroyed)
|
|
2557
|
+
if (!res.headersSent && !res.destroyed) json2(
|
|
2173
2558
|
res,
|
|
2174
2559
|
error instanceof AccountChanged ? 401 : 503,
|
|
2175
2560
|
{ error: error instanceof AccountChanged ? "Account changed" : "Account service unavailable" }
|
|
@@ -2218,75 +2603,303 @@ async function createLocalWorkspace(dataDir, workspaceDir, vaultRoot) {
|
|
|
2218
2603
|
const realRoot = await realpath3(root);
|
|
2219
2604
|
if (actual === realRoot || actual.startsWith(realRoot + sep3)) return actual;
|
|
2220
2605
|
}
|
|
2221
|
-
} catch {
|
|
2606
|
+
} catch {
|
|
2607
|
+
}
|
|
2608
|
+
return null;
|
|
2609
|
+
};
|
|
2610
|
+
return {
|
|
2611
|
+
token,
|
|
2612
|
+
device,
|
|
2613
|
+
readConfig: async () => ({ ...await read(), homeDir: workspaceDir, defaultWorkspaceDir: workspaceDir }),
|
|
2614
|
+
writeConfig: async (patch) => {
|
|
2615
|
+
const job = writes.then(async () => {
|
|
2616
|
+
const next = { ...await read(), ...pickUnitPreferences(patch) };
|
|
2617
|
+
const tmp = `${file2}.${randomUUID4()}.tmp`;
|
|
2618
|
+
await writeFile2(tmp, JSON.stringify(next), { mode: 384 });
|
|
2619
|
+
await rename2(tmp, file2);
|
|
2620
|
+
});
|
|
2621
|
+
writes = job.catch(() => {
|
|
2622
|
+
});
|
|
2623
|
+
await job;
|
|
2624
|
+
return { ...await read(), homeDir: workspaceDir, defaultWorkspaceDir: workspaceDir };
|
|
2625
|
+
},
|
|
2626
|
+
readHostFile: async (path, maxBytes = 50 * 1024 * 1024) => {
|
|
2627
|
+
const real = await scoped(path);
|
|
2628
|
+
if (!real) return null;
|
|
2629
|
+
const st = await stat2(real);
|
|
2630
|
+
if (!st.isFile()) return null;
|
|
2631
|
+
const types = { ".md": "text/markdown", ".txt": "text/plain", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml", ".pdf": "application/pdf", ".mp4": "video/mp4", ".mp3": "audio/mpeg", ".wav": "audio/wav" };
|
|
2632
|
+
const mimeType = types[extname2(real).toLowerCase()] || "application/octet-stream";
|
|
2633
|
+
if (st.size > Math.min(maxBytes, 50 * 1024 * 1024)) return { mimeType, content: "", size: st.size, mtimeMs: st.mtimeMs, tooLarge: true };
|
|
2634
|
+
return { mimeType, content: (await readFile4(real)).toString("base64"), size: st.size, mtimeMs: st.mtimeMs };
|
|
2635
|
+
},
|
|
2636
|
+
readHostDir: async (path) => {
|
|
2637
|
+
const real = await scoped(path);
|
|
2638
|
+
if (!real || !(await stat2(real)).isDirectory()) return null;
|
|
2639
|
+
const entries = await readdir2(real, { withFileTypes: true });
|
|
2640
|
+
const values = await Promise.all(entries.slice(0, 2e3).map(async (e) => {
|
|
2641
|
+
const child = await scoped(resolve2(real, e.name));
|
|
2642
|
+
if (!child) return null;
|
|
2643
|
+
const st = await stat2(child);
|
|
2644
|
+
return { name: e.name, isDir: st.isDirectory(), size: st.size, path: child };
|
|
2645
|
+
}));
|
|
2646
|
+
return values.filter((e) => !!e).sort((a, b) => Number(b.isDir) - Number(a.isDir) || a.name.localeCompare(b.name));
|
|
2647
|
+
},
|
|
2648
|
+
readHostStat: async (path) => {
|
|
2649
|
+
const real = await scoped(path);
|
|
2650
|
+
if (!real) return null;
|
|
2651
|
+
const st = await stat2(real);
|
|
2652
|
+
return { isDir: st.isDirectory(), mtimeMs: st.mtimeMs, birthtimeMs: st.birthtimeMs || null };
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// unit/management.ts
|
|
2658
|
+
init_packages();
|
|
2659
|
+
init_releases();
|
|
2660
|
+
import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile5, rename as rename5 } from "node:fs/promises";
|
|
2661
|
+
import { join as join3 } from "node:path";
|
|
2662
|
+
import { randomUUID as randomUUID8 } from "node:crypto";
|
|
2663
|
+
var MANAGEMENT_PATH = "/api/admin/unit";
|
|
2664
|
+
var actions = ["enable", "disable", "restart", "update", "migrate"];
|
|
2665
|
+
var HttpError = class extends Error {
|
|
2666
|
+
constructor(status, message) {
|
|
2667
|
+
super(message);
|
|
2668
|
+
this.status = status;
|
|
2669
|
+
}
|
|
2670
|
+
};
|
|
2671
|
+
var json = (res, status, value) => {
|
|
2672
|
+
if (res.destroyed || res.writableEnded) return;
|
|
2673
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
2674
|
+
res.end(JSON.stringify(value));
|
|
2675
|
+
};
|
|
2676
|
+
async function createManagement(options) {
|
|
2677
|
+
const file2 = join3(options.dataDir, "management-jobs.json");
|
|
2678
|
+
let jobs = [], saving = Promise.resolve(), busy = false, closing = false, pending;
|
|
2679
|
+
const requests = /* @__PURE__ */ new Set();
|
|
2680
|
+
if (options.enabled) {
|
|
2681
|
+
try {
|
|
2682
|
+
jobs = JSON.parse(await readFile7(file2, "utf8"));
|
|
2683
|
+
} catch (e) {
|
|
2684
|
+
if (e.code !== "ENOENT") throw e;
|
|
2685
|
+
}
|
|
2686
|
+
if (!Array.isArray(jobs) || jobs.length > 100) throw new Error("Invalid Unit job journal");
|
|
2687
|
+
for (const job of jobs) if (job.state === "queued" || job.state === "running") {
|
|
2688
|
+
job.state = "interrupted";
|
|
2689
|
+
job.error = "Unit stopped before completion; inspect current plugin state before retrying";
|
|
2690
|
+
job.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
const save = () => {
|
|
2694
|
+
const payload = JSON.stringify(jobs);
|
|
2695
|
+
const next = saving.then(async () => {
|
|
2696
|
+
await mkdir7(options.dataDir, { recursive: true, mode: 448 });
|
|
2697
|
+
const temporary = file2 + "." + randomUUID8();
|
|
2698
|
+
await writeFile5(temporary, payload, { mode: 384 });
|
|
2699
|
+
await rename5(temporary, file2);
|
|
2700
|
+
});
|
|
2701
|
+
saving = next.catch(() => {
|
|
2702
|
+
});
|
|
2703
|
+
return next;
|
|
2704
|
+
};
|
|
2705
|
+
if (options.enabled && jobs.length) await save();
|
|
2706
|
+
const handle = async (req, res) => {
|
|
2707
|
+
const controller = new AbortController();
|
|
2708
|
+
const cancel = () => {
|
|
2709
|
+
if (!res.writableEnded) controller.abort();
|
|
2710
|
+
};
|
|
2711
|
+
res.once("close", cancel);
|
|
2712
|
+
try {
|
|
2713
|
+
if (!options.enabled || !options.execute) throw new HttpError(503, "Unit management is disabled; enable management.enabled in private unit.json");
|
|
2714
|
+
if (closing) throw new HttpError(503, "Unit is stopping");
|
|
2715
|
+
const provider = options.provider();
|
|
2716
|
+
if (!provider) throw new HttpError(503, "Account authority is unavailable");
|
|
2717
|
+
const token = /^Bearer (\S+)$/i.exec(String(req.headers.authorization || ""))?.[1];
|
|
2718
|
+
if (!token || token.length > 16384) throw new HttpError(401, "Authentication required");
|
|
2719
|
+
let permission = "unit.read";
|
|
2720
|
+
const authorize = async () => {
|
|
2721
|
+
if (closing) throw new HttpError(503, "Unit is stopping");
|
|
2722
|
+
const identity = await provider.account.resolve(token, { signal: controller.signal, purpose: "administration" });
|
|
2723
|
+
if (!identity) throw new HttpError(401, "Invalid or expired account");
|
|
2724
|
+
if (identity.role !== "ADMIN" && !(identity.role === "ADMIN_SCOPED" && (identity.adminPermissions?.includes("*") || identity.adminPermissions?.includes(permission)))) throw new HttpError(403, "Administrator access required");
|
|
2725
|
+
if (options.provider()?.account !== provider.account) throw new HttpError(503, "Account authority changed");
|
|
2726
|
+
return identity;
|
|
2727
|
+
};
|
|
2728
|
+
const actor = await authorize();
|
|
2729
|
+
const path = new URL(req.url, "http://unit.invalid").pathname;
|
|
2730
|
+
if (req.method === "GET") {
|
|
2731
|
+
if (path === MANAGEMENT_PATH + "/status") json(res, 200, { ...options.status(), management: { enabled: true, actions, busy } });
|
|
2732
|
+
else if (path === MANAGEMENT_PATH + "/releases") json(res, 200, { releases: await listReleases(options.dataDir) });
|
|
2733
|
+
else if (path === MANAGEMENT_PATH + "/operations") json(res, 200, { operations: [...jobs].reverse() });
|
|
2734
|
+
else if (path.startsWith(MANAGEMENT_PATH + "/operations/")) {
|
|
2735
|
+
const job = jobs.find((entry) => entry.id === path.slice((MANAGEMENT_PATH + "/operations/").length));
|
|
2736
|
+
if (!job) throw new HttpError(404, "Operation not found");
|
|
2737
|
+
json(res, 200, job);
|
|
2738
|
+
} else throw new HttpError(404, "Unknown Unit management route");
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
if (req.method !== "POST" || path !== MANAGEMENT_PATH + "/operations") throw new HttpError(405, "Method not allowed");
|
|
2742
|
+
if (!/^application\/json(?:;|$)/i.test(String(req.headers["content-type"] || ""))) throw new HttpError(415, "JSON body required");
|
|
2743
|
+
let size = 0;
|
|
2744
|
+
const chunks = [];
|
|
2745
|
+
req.setTimeout(5e3, () => req.destroy());
|
|
2746
|
+
for await (const chunk of req) {
|
|
2747
|
+
size += Buffer.byteLength(chunk);
|
|
2748
|
+
if (size > 8192) throw new HttpError(413, "Operation body exceeds 8 KB");
|
|
2749
|
+
chunks.push(Buffer.from(chunk));
|
|
2750
|
+
}
|
|
2751
|
+
req.setTimeout(0);
|
|
2752
|
+
let input;
|
|
2753
|
+
try {
|
|
2754
|
+
input = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
2755
|
+
} catch {
|
|
2756
|
+
throw new HttpError(400, "Invalid JSON");
|
|
2757
|
+
}
|
|
2758
|
+
if (!input || Array.isArray(input) || typeof input !== "object" || Object.keys(input).some((key) => !["action", "pluginId", "releaseId", "requestId", "confirm"].includes(key)) || !actions.includes(input.action) || !validId(input.pluginId) || !validReleaseId(input.requestId) || (input.action === "update" ? !validReleaseId(input.releaseId) : input.releaseId !== void 0)) throw new HttpError(400, "Invalid operation arguments");
|
|
2759
|
+
if (input.confirm !== true) throw new HttpError(400, "confirm:true is required for Unit operations");
|
|
2760
|
+
permission = input.action === "update" ? "unit.update" : input.action === "migrate" ? "unit.migrate" : "unit.lifecycle";
|
|
2761
|
+
await authorize();
|
|
2762
|
+
const previous = jobs.find((job) => job.id === input.requestId);
|
|
2763
|
+
if (previous) {
|
|
2764
|
+
if (previous.actor.userId !== actor.userId || previous.action !== input.action || previous.pluginId !== input.pluginId || previous.releaseId !== input.releaseId) throw new HttpError(409, "Request ID is already used for a different operation");
|
|
2765
|
+
json(res, 200, previous);
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
if (busy) throw new HttpError(409, "Another Unit operation is running");
|
|
2769
|
+
if (provider.id === input.pluginId && ["disable", "migrate"].includes(input.action)) throw new HttpError(409, "Use local CLI for maintenance that stops the account authority");
|
|
2770
|
+
busy = true;
|
|
2771
|
+
try {
|
|
2772
|
+
const status = options.status();
|
|
2773
|
+
if (!status.plugins.some((plugin) => plugin.id === input.pluginId)) throw new HttpError(404, "Plugin is not installed");
|
|
2774
|
+
let release;
|
|
2775
|
+
if (input.action === "update") {
|
|
2776
|
+
try {
|
|
2777
|
+
release = await resolveRelease(options.dataDir, input.releaseId, input.pluginId, options.version, false);
|
|
2778
|
+
} catch {
|
|
2779
|
+
throw new HttpError(400, "Release is missing, incompatible, or its identity/content changed");
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
await authorize();
|
|
2783
|
+
const job = {
|
|
2784
|
+
id: input.requestId,
|
|
2785
|
+
action: input.action,
|
|
2786
|
+
pluginId: input.pluginId,
|
|
2787
|
+
...release ? { releaseId: input.releaseId } : {},
|
|
2788
|
+
actor: { userId: actor.userId, username: actor.username },
|
|
2789
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2790
|
+
state: "queued"
|
|
2791
|
+
};
|
|
2792
|
+
const priorJobs = jobs;
|
|
2793
|
+
jobs = [...jobs.slice(-99), job];
|
|
2794
|
+
try {
|
|
2795
|
+
await save();
|
|
2796
|
+
} catch (error) {
|
|
2797
|
+
jobs = priorJobs;
|
|
2798
|
+
throw error;
|
|
2799
|
+
}
|
|
2800
|
+
const run2 = async () => {
|
|
2801
|
+
try {
|
|
2802
|
+
await authorize();
|
|
2803
|
+
job.state = "running";
|
|
2804
|
+
await save();
|
|
2805
|
+
if (release) {
|
|
2806
|
+
job.phase = "verifying-release";
|
|
2807
|
+
await save();
|
|
2808
|
+
const verified = await resolveRelease(options.dataDir, input.releaseId, input.pluginId, options.version);
|
|
2809
|
+
if (verified.item.sha256 !== release.item.sha256) throw new Error("Staged release changed during installation");
|
|
2810
|
+
await authorize();
|
|
2811
|
+
}
|
|
2812
|
+
job.phase = "applying";
|
|
2813
|
+
await save();
|
|
2814
|
+
console.log(`[unit-management] ${job.id} ${job.action} ${job.pluginId} actor=${job.actor.userId}`);
|
|
2815
|
+
await options.execute({ action: job.action, id: job.pluginId, ...release ? { path: release.path, releaseDigest: release.item.sha256 } : {} }, async () => {
|
|
2816
|
+
await authorize();
|
|
2817
|
+
});
|
|
2818
|
+
job.state = "succeeded";
|
|
2819
|
+
} catch (error) {
|
|
2820
|
+
job.state = "failed";
|
|
2821
|
+
const reason = error instanceof Error ? error.message : "";
|
|
2822
|
+
job.error = error instanceof HttpError || [
|
|
2823
|
+
"Disable dependent plugins first",
|
|
2824
|
+
"Disable the plugin before migrating",
|
|
2825
|
+
"Unit is stopping",
|
|
2826
|
+
"Staged release changed during installation"
|
|
2827
|
+
].includes(reason) ? reason : "Operation failed; inspect plugin status. Updates attempt to restore the previous package.";
|
|
2828
|
+
} finally {
|
|
2829
|
+
job.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2830
|
+
busy = false;
|
|
2831
|
+
try {
|
|
2832
|
+
await save();
|
|
2833
|
+
} catch {
|
|
2834
|
+
console.error(`[unit-management] Could not persist result for ${job.id}`);
|
|
2835
|
+
}
|
|
2836
|
+
console.log(`[unit-management] ${job.id} ${job.state}`);
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2839
|
+
let launched = false;
|
|
2840
|
+
let done;
|
|
2841
|
+
pending = new Promise((resolve8) => {
|
|
2842
|
+
done = resolve8;
|
|
2843
|
+
});
|
|
2844
|
+
const launch = () => {
|
|
2845
|
+
if (launched) return;
|
|
2846
|
+
launched = true;
|
|
2847
|
+
setTimeout(() => {
|
|
2848
|
+
void run2().finally(done);
|
|
2849
|
+
}, 100);
|
|
2850
|
+
};
|
|
2851
|
+
res.once("finish", launch);
|
|
2852
|
+
res.once("close", launch);
|
|
2853
|
+
if (res.destroyed || res.writableEnded) launch();
|
|
2854
|
+
else json(res, 202, job);
|
|
2855
|
+
} catch (error) {
|
|
2856
|
+
busy = false;
|
|
2857
|
+
throw error;
|
|
2858
|
+
}
|
|
2859
|
+
} catch (error) {
|
|
2860
|
+
json(res, error instanceof HttpError ? error.status : 503, { error: error instanceof HttpError ? error.message : "Unit management is temporarily unavailable" });
|
|
2861
|
+
} finally {
|
|
2862
|
+
res.off("close", cancel);
|
|
2222
2863
|
}
|
|
2223
|
-
return null;
|
|
2224
2864
|
};
|
|
2225
2865
|
return {
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
await writeFile2(tmp, JSON.stringify(next), { mode: 384 });
|
|
2234
|
-
await rename2(tmp, file2);
|
|
2235
|
-
});
|
|
2236
|
-
writes = job.catch(() => {
|
|
2237
|
-
});
|
|
2238
|
-
await job;
|
|
2239
|
-
return { ...await read(), homeDir: workspaceDir, defaultWorkspaceDir: workspaceDir };
|
|
2240
|
-
},
|
|
2241
|
-
readHostFile: async (path, maxBytes = 50 * 1024 * 1024) => {
|
|
2242
|
-
const real = await scoped(path);
|
|
2243
|
-
if (!real) return null;
|
|
2244
|
-
const st = await stat2(real);
|
|
2245
|
-
if (!st.isFile()) return null;
|
|
2246
|
-
const types = { ".md": "text/markdown", ".txt": "text/plain", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml", ".pdf": "application/pdf", ".mp4": "video/mp4", ".mp3": "audio/mpeg", ".wav": "audio/wav" };
|
|
2247
|
-
const mimeType = types[extname2(real).toLowerCase()] || "application/octet-stream";
|
|
2248
|
-
if (st.size > Math.min(maxBytes, 50 * 1024 * 1024)) return { mimeType, content: "", size: st.size, mtimeMs: st.mtimeMs, tooLarge: true };
|
|
2249
|
-
return { mimeType, content: (await readFile4(real)).toString("base64"), size: st.size, mtimeMs: st.mtimeMs };
|
|
2250
|
-
},
|
|
2251
|
-
readHostDir: async (path) => {
|
|
2252
|
-
const real = await scoped(path);
|
|
2253
|
-
if (!real || !(await stat2(real)).isDirectory()) return null;
|
|
2254
|
-
const entries = await readdir2(real, { withFileTypes: true });
|
|
2255
|
-
const values = await Promise.all(entries.slice(0, 2e3).map(async (e) => {
|
|
2256
|
-
const child = await scoped(resolve2(real, e.name));
|
|
2257
|
-
if (!child) return null;
|
|
2258
|
-
const st = await stat2(child);
|
|
2259
|
-
return { name: e.name, isDir: st.isDirectory(), size: st.size, path: child };
|
|
2260
|
-
}));
|
|
2261
|
-
return values.filter((e) => !!e).sort((a, b) => Number(b.isDir) - Number(a.isDir) || a.name.localeCompare(b.name));
|
|
2866
|
+
route(req, res) {
|
|
2867
|
+
const path = (req.url || "/").split("?")[0];
|
|
2868
|
+
if (path !== MANAGEMENT_PATH && !path.startsWith(MANAGEMENT_PATH + "/")) return false;
|
|
2869
|
+
const task = handle(req, res);
|
|
2870
|
+
requests.add(task);
|
|
2871
|
+
void task.then(() => requests.delete(task), () => requests.delete(task));
|
|
2872
|
+
return true;
|
|
2262
2873
|
},
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2874
|
+
async close() {
|
|
2875
|
+
closing = true;
|
|
2876
|
+
await Promise.allSettled([...requests]);
|
|
2877
|
+
await pending;
|
|
2878
|
+
await saving;
|
|
2268
2879
|
}
|
|
2269
2880
|
};
|
|
2270
2881
|
}
|
|
2271
2882
|
|
|
2272
2883
|
// unit/host.ts
|
|
2884
|
+
init_packages();
|
|
2273
2885
|
var matches = (path, prefix) => prefix === "/" || path === prefix || path.startsWith(prefix + "/");
|
|
2274
2886
|
var pathname = (url) => (url || "/").split("?")[0];
|
|
2275
|
-
async function startBasicUnit(config) {
|
|
2887
|
+
async function startBasicUnit(config, managementOptions = {}) {
|
|
2276
2888
|
if (!config.instanceId || !config.name || !config.version) throw new Error("Unit identity is required");
|
|
2277
|
-
await
|
|
2278
|
-
const dataDir =
|
|
2279
|
-
await
|
|
2889
|
+
await readFile8(resolve6(config.webDist, "index.html"));
|
|
2890
|
+
const dataDir = resolve6(config.dataDir || resolve6(homedir(), ".forsion", "units", config.instanceId));
|
|
2891
|
+
await mkdir8(dataDir, { recursive: true, mode: 448 });
|
|
2280
2892
|
const workerFile = config.workerFile || fileURLToPath(new URL("./backendWorker.mjs", import.meta.url));
|
|
2281
2893
|
if (config.workspace && config.workspace.mode !== "local") throw new Error("Invalid workspace mode");
|
|
2282
2894
|
const local = config.workspace?.mode === "local";
|
|
2283
|
-
const workspaceDir =
|
|
2895
|
+
const workspaceDir = resolve6(config.workspace?.path || resolve6(dataDir, "workspace"));
|
|
2284
2896
|
const records = /* @__PURE__ */ new Map();
|
|
2285
2897
|
const runtimeVault = () => [...records.values()].find((r) => usable(r) && r.runtime?.vault)?.runtime?.vault || null;
|
|
2286
2898
|
const runtimeEngine = () => [...records.values()].find((r) => usable(r) && r.runtime?.engine)?.runtime?.engine;
|
|
2287
2899
|
const localWorkspace = local ? await createLocalWorkspace(dataDir, workspaceDir, () => runtimeVault()?.root() || null) : null;
|
|
2288
2900
|
const prefix = config.basePath.replace(/\/$/, "") || "/";
|
|
2289
2901
|
const protectedPaths = ["unit", "vault", "engine"].map((name) => (prefix === "/" ? "" : prefix) + "/" + name);
|
|
2902
|
+
protectedPaths.push(MANAGEMENT_PATH);
|
|
2290
2903
|
for (const entry of config.plugins) {
|
|
2291
2904
|
const installation = typeof entry === "string" ? { path: entry } : { ...entry };
|
|
2292
2905
|
const pack = await readPackage(installation.path, config.version);
|
|
@@ -2376,11 +2989,12 @@ async function startBasicUnit(config) {
|
|
|
2376
2989
|
id,
|
|
2377
2990
|
entry: rec.package.backendEntry,
|
|
2378
2991
|
packageDir: rec.package.root,
|
|
2379
|
-
dataDir:
|
|
2992
|
+
dataDir: resolve6(dataDir, "plugins", id),
|
|
2380
2993
|
config: rec.installation.config || {},
|
|
2381
2994
|
env: {
|
|
2382
2995
|
...rec.installation.config?.env || {},
|
|
2383
2996
|
...rec.installation.env,
|
|
2997
|
+
UNIT_DATA_DIR: dataDir,
|
|
2384
2998
|
PORT: String(web.port),
|
|
2385
2999
|
UNIT_HTTP_ORIGIN: `http://${localHost}:${web.port}`
|
|
2386
3000
|
},
|
|
@@ -2406,16 +3020,16 @@ async function startBasicUnit(config) {
|
|
|
2406
3020
|
let backend, runtime;
|
|
2407
3021
|
try {
|
|
2408
3022
|
if (local && rec.package.runtimeEntry) {
|
|
2409
|
-
const module = await import(pathToFileURL(rec.package.runtimeEntry).href + "?generation=" +
|
|
3023
|
+
const module = await import(pathToFileURL(rec.package.runtimeEntry).href + "?generation=" + randomUUID9());
|
|
2410
3024
|
const factory = module.createRuntime || module.default;
|
|
2411
3025
|
if (typeof factory !== "function") throw new Error("Local runtime has no factory");
|
|
2412
|
-
await
|
|
2413
|
-
runtime = await factory({ packageDir: rec.package.root, dataDir:
|
|
3026
|
+
await mkdir8(resolve6(dataDir, "plugins", id), { recursive: true, mode: 448 });
|
|
3027
|
+
runtime = await factory({ packageDir: rec.package.root, dataDir: resolve6(dataDir, "plugins", id), workspaceDir, config: rec.installation.config || {}, log: (m) => console.log(`[plugin:${id}] ${m}`) });
|
|
2414
3028
|
if (!runtime || typeof runtime.close !== "function") throw new Error("Invalid local runtime");
|
|
2415
3029
|
if (runtime.vault && runtimeVault() || runtime.engine && runtimeEngine()) throw new Error("Local capability already has a provider");
|
|
2416
3030
|
}
|
|
2417
3031
|
if (rec.package.backendEntry) {
|
|
2418
|
-
await
|
|
3032
|
+
await mkdir8(resolve6(dataDir, "plugins", id), { recursive: true, mode: 448 });
|
|
2419
3033
|
backend = await startBackend(options(id, rec));
|
|
2420
3034
|
if (backend.account && [...records.values()].some((r) => r !== rec && usable(r) && r.backend?.account)) {
|
|
2421
3035
|
throw new Error("A Unit can activate only one account authority");
|
|
@@ -2498,11 +3112,30 @@ async function startBasicUnit(config) {
|
|
|
2498
3112
|
return !!rec && usable(rec);
|
|
2499
3113
|
}
|
|
2500
3114
|
});
|
|
3115
|
+
const management = await createManagement({
|
|
3116
|
+
enabled: config.management?.enabled === true,
|
|
3117
|
+
dataDir,
|
|
3118
|
+
version: config.version,
|
|
3119
|
+
provider: accountProvider,
|
|
3120
|
+
execute: managementOptions.execute,
|
|
3121
|
+
status: () => ({
|
|
3122
|
+
instanceId: config.instanceId,
|
|
3123
|
+
version: config.version,
|
|
3124
|
+
pid: process.pid,
|
|
3125
|
+
plugins: [...records.values()].map((r) => ({
|
|
3126
|
+
id: r.package.manifest.id,
|
|
3127
|
+
version: r.package.manifest.version,
|
|
3128
|
+
state: r.state,
|
|
3129
|
+
requires: r.package.manifest.requires || []
|
|
3130
|
+
}))
|
|
3131
|
+
})
|
|
3132
|
+
});
|
|
2501
3133
|
const web = await startUnitWeb({
|
|
2502
3134
|
account: local ? void 0 : { metadata: () => accountProvider()?.account.metadata, handle: accountHttp },
|
|
2503
3135
|
meta: { instanceId: config.instanceId, name: config.name, version: config.version },
|
|
2504
3136
|
projection: { mode: local ? "local" : "public", basePath: config.basePath, product, capabilities, localCapabilities: () => ({ vault: !!runtimeVault(), engine: !!runtimeEngine()?.endpoint().url, host: !!runtimeEngine()?.endpoint().url }) },
|
|
2505
3137
|
routeRequest: (req, res) => {
|
|
3138
|
+
if (management.route(req, res)) return true;
|
|
2506
3139
|
const path = pathname(req.url), backend = select(path);
|
|
2507
3140
|
if (backend) {
|
|
2508
3141
|
backend.handle(req, res);
|
|
@@ -2566,6 +3199,7 @@ async function startBasicUnit(config) {
|
|
|
2566
3199
|
}
|
|
2567
3200
|
return {
|
|
2568
3201
|
...web,
|
|
3202
|
+
closeManagement: management.close,
|
|
2569
3203
|
status: () => [...records.values()].map((r) => ({ id: r.package.manifest.id, version: r.package.manifest.version, state: r.state })),
|
|
2570
3204
|
enable: (id) => serialize(() => activate(id)),
|
|
2571
3205
|
disable: (id) => serialize(() => deactivate(id)),
|
|
@@ -2602,237 +3236,36 @@ async function startBasicUnit(config) {
|
|
|
2602
3236
|
if (!rec.package.backendEntry) throw new Error("Plugin has no backend migration");
|
|
2603
3237
|
await migrateBackend(options(id, rec));
|
|
2604
3238
|
}),
|
|
2605
|
-
close:
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
3239
|
+
close: async () => {
|
|
3240
|
+
await management.close();
|
|
3241
|
+
return serialize(async () => {
|
|
3242
|
+
if (closing) return;
|
|
3243
|
+
closing = true;
|
|
3244
|
+
await web.close();
|
|
3245
|
+
const errors = [];
|
|
3246
|
+
for (const id of [...ordered].reverse()) {
|
|
3247
|
+
try {
|
|
3248
|
+
await deactivate(id, true);
|
|
3249
|
+
} catch (error) {
|
|
3250
|
+
errors.push(error);
|
|
3251
|
+
}
|
|
2615
3252
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
}
|
|
3253
|
+
if (errors.length) throw new AggregateError(errors, "Some plugins failed to stop cleanly");
|
|
3254
|
+
});
|
|
3255
|
+
}
|
|
2619
3256
|
};
|
|
2620
3257
|
}
|
|
2621
3258
|
|
|
2622
|
-
// unit/
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
const path = resolve4(file2), root = dirname(path);
|
|
2628
|
-
const config = JSON.parse(await readFile6(path, "utf8"));
|
|
2629
|
-
if (!Array.isArray(config.plugins)) throw new Error("unit.json plugins must be an array");
|
|
2630
|
-
config.webDist = resolve4(root, config.webDist);
|
|
2631
|
-
config.dataDir = resolve4(root, config.dataDir || "data");
|
|
2632
|
-
if (config.workspace?.path) config.workspace.path = resolve4(root, config.workspace.path);
|
|
2633
|
-
if (config.workerFile) config.workerFile = resolve4(root, config.workerFile);
|
|
2634
|
-
config.plugins = config.plugins.map((p) => typeof p === "string" ? { path: resolve4(root, p) } : { ...p, path: resolve4(root, p.path) });
|
|
2635
|
-
return config;
|
|
2636
|
-
}
|
|
2637
|
-
async function writeConfig(file2, config) {
|
|
2638
|
-
await mkdir4(dirname(resolve4(file2)), { recursive: true, mode: 448 });
|
|
2639
|
-
const temp = `${file2}.${randomUUID6()}.tmp`;
|
|
2640
|
-
await writeFile3(temp, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
2641
|
-
await rename3(temp, file2);
|
|
2642
|
-
}
|
|
2643
|
-
var installations = (config) => config.plugins;
|
|
2644
|
-
|
|
2645
|
-
// unit/install.ts
|
|
2646
|
-
import { cp, mkdir as mkdir6, readdir as readdir3, realpath as realpath4, rm as rm3, lstat, open, readlink, symlink } from "node:fs/promises";
|
|
2647
|
-
import { dirname as dirname3, resolve as resolve6, sep as sep4, relative, isAbsolute as isAbsolute3 } from "node:path";
|
|
2648
|
-
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
2649
|
-
import { spawn } from "node:child_process";
|
|
2650
|
-
|
|
2651
|
-
// unit/control.ts
|
|
2652
|
-
import net from "node:net";
|
|
2653
|
-
import { chmod, rm as rm2, mkdir as mkdir5 } from "node:fs/promises";
|
|
2654
|
-
import { resolve as resolve5, dirname as dirname2 } from "node:path";
|
|
2655
|
-
import { tmpdir } from "node:os";
|
|
2656
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
2657
|
-
var controlAddress = (dataDir) => process.platform === "win32" ? `\\\\.\\pipe\\forsion-unit-${createHash4("sha256").update(resolve5(dataDir)).digest("hex").slice(0, 24)}` : resolve5(tmpdir(), `forsion-unit-${process.getuid?.() ?? "user"}`, createHash4("sha256").update(resolve5(dataDir)).digest("hex").slice(0, 24) + ".sock");
|
|
2658
|
-
async function sendControl(dataDir, command) {
|
|
2659
|
-
return new Promise((resolveReply, reject) => {
|
|
2660
|
-
const socket = net.connect(controlAddress(dataDir));
|
|
2661
|
-
let body = "";
|
|
2662
|
-
socket.setTimeout(24e4, () => socket.destroy(new Error("Unit control timed out")));
|
|
2663
|
-
socket.once("connect", () => socket.write(JSON.stringify(command) + "\n"));
|
|
2664
|
-
socket.on("data", (data) => {
|
|
2665
|
-
body += data;
|
|
2666
|
-
if (body.length > 1e6) {
|
|
2667
|
-
socket.destroy(new Error("Oversized control response"));
|
|
2668
|
-
return;
|
|
2669
|
-
}
|
|
2670
|
-
if (!body.includes("\n")) return;
|
|
2671
|
-
try {
|
|
2672
|
-
const reply = JSON.parse(body.split("\n")[0]);
|
|
2673
|
-
socket.end();
|
|
2674
|
-
if (reply.ok) resolveReply(reply.result);
|
|
2675
|
-
else reject(new Error(reply.error || "Unit command failed"));
|
|
2676
|
-
} catch (error) {
|
|
2677
|
-
socket.destroy();
|
|
2678
|
-
reject(error);
|
|
2679
|
-
}
|
|
2680
|
-
});
|
|
2681
|
-
socket.once("error", reject);
|
|
2682
|
-
socket.once("end", () => {
|
|
2683
|
-
if (!body.includes("\n")) reject(new Error("Unit control connection closed"));
|
|
2684
|
-
});
|
|
2685
|
-
});
|
|
2686
|
-
}
|
|
2687
|
-
var unitIsOffline = (error) => ["ENOENT", "ECONNREFUSED"].includes(error?.code || "");
|
|
2688
|
-
async function startControl(dataDir, dispatch) {
|
|
2689
|
-
await mkdir5(dataDir, { recursive: true, mode: 448 });
|
|
2690
|
-
try {
|
|
2691
|
-
await sendControl(dataDir, { action: "status" });
|
|
2692
|
-
throw new Error("This Unit is already running");
|
|
2693
|
-
} catch (error) {
|
|
2694
|
-
if (!unitIsOffline(error)) throw error;
|
|
2695
|
-
}
|
|
2696
|
-
const address = controlAddress(dataDir);
|
|
2697
|
-
if (process.platform !== "win32") {
|
|
2698
|
-
await mkdir5(dirname2(address), { recursive: true, mode: 448 });
|
|
2699
|
-
await chmod(dirname2(address), 448);
|
|
2700
|
-
}
|
|
2701
|
-
if (process.platform !== "win32") await rm2(address, { force: true });
|
|
2702
|
-
const clients = /* @__PURE__ */ new Set();
|
|
2703
|
-
const server = net.createServer((socket) => {
|
|
2704
|
-
clients.add(socket);
|
|
2705
|
-
socket.once("close", () => clients.delete(socket));
|
|
2706
|
-
let body = "", dispatched = false;
|
|
2707
|
-
socket.setTimeout(24e4, () => socket.destroy());
|
|
2708
|
-
socket.on("data", (data) => {
|
|
2709
|
-
if (dispatched) return;
|
|
2710
|
-
body += data;
|
|
2711
|
-
if (body.length > 1e6) {
|
|
2712
|
-
socket.destroy();
|
|
2713
|
-
return;
|
|
2714
|
-
}
|
|
2715
|
-
if (!body.includes("\n")) return;
|
|
2716
|
-
dispatched = true;
|
|
2717
|
-
void (async () => {
|
|
2718
|
-
try {
|
|
2719
|
-
socket.end(JSON.stringify({ ok: true, result: await dispatch(JSON.parse(body.split("\n")[0])) }) + "\n");
|
|
2720
|
-
} catch (error) {
|
|
2721
|
-
socket.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : "Unit command failed" }) + "\n");
|
|
2722
|
-
}
|
|
2723
|
-
})();
|
|
2724
|
-
});
|
|
2725
|
-
socket.on("error", () => {
|
|
2726
|
-
});
|
|
2727
|
-
});
|
|
2728
|
-
await new Promise((ready, reject) => {
|
|
2729
|
-
server.once("error", reject);
|
|
2730
|
-
server.listen(address, ready);
|
|
2731
|
-
});
|
|
2732
|
-
if (process.platform !== "win32") await chmod(address, 384);
|
|
2733
|
-
return { close: async () => {
|
|
2734
|
-
await new Promise((done) => {
|
|
2735
|
-
server.close(() => done());
|
|
2736
|
-
for (const socket of clients) socket.destroy();
|
|
2737
|
-
});
|
|
2738
|
-
if (process.platform !== "win32") await rm2(address, { force: true });
|
|
2739
|
-
} };
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
// unit/install.ts
|
|
2743
|
-
async function inspectTree(dir, root, seen = /* @__PURE__ */ new Set()) {
|
|
2744
|
-
const actual = await realpath4(dir);
|
|
2745
|
-
if (seen.has(actual)) return;
|
|
2746
|
-
seen.add(actual);
|
|
2747
|
-
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
2748
|
-
const path = resolve6(dir, entry.name);
|
|
2749
|
-
if (entry.name === ".env" || entry.name.startsWith(".env.") && !entry.name.endsWith(".example")) throw new Error("Package contains private environment files");
|
|
2750
|
-
if (entry.isSymbolicLink()) {
|
|
2751
|
-
const target = await realpath4(path);
|
|
2752
|
-
if (!target.startsWith(root + sep4)) throw new Error("Package symlink escapes its directory");
|
|
2753
|
-
if ((await lstat(target)).isDirectory()) await inspectTree(target, root, seen);
|
|
2754
|
-
} else if (entry.isDirectory()) await inspectTree(path, root, seen);
|
|
2755
|
-
}
|
|
2756
|
-
}
|
|
2757
|
-
async function relocateLinks(dir, source, destination) {
|
|
2758
|
-
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
2759
|
-
const path = resolve6(dir, entry.name);
|
|
2760
|
-
if (entry.isSymbolicLink()) {
|
|
2761
|
-
const link = await readlink(path);
|
|
2762
|
-
if (isAbsolute3(link)) {
|
|
2763
|
-
const relocated = resolve6(destination, relative(source, await realpath4(link)));
|
|
2764
|
-
await rm3(path);
|
|
2765
|
-
await symlink(relative(dirname3(path), relocated), path);
|
|
2766
|
-
}
|
|
2767
|
-
} else if (entry.isDirectory()) await relocateLinks(path, source, destination);
|
|
2768
|
-
}
|
|
2769
|
-
}
|
|
2770
|
-
async function installDependencies(root, backendMain) {
|
|
2771
|
-
const backend = resolve6(root, backendMain.split("/")[0]);
|
|
2772
|
-
await packagePath(root, backendMain.split("/")[0] + "/package-lock.json");
|
|
2773
|
-
await new Promise((done, reject) => {
|
|
2774
|
-
const win = process.platform === "win32";
|
|
2775
|
-
const child = spawn(win ? "cmd.exe" : "npm", win ? ["/c", "npm", "ci", "--omit=dev"] : ["ci", "--omit=dev"], { cwd: backend, stdio: "inherit", env: { ...process.env, PATH: dirname3(process.execPath) + (win ? ";" : ":") + (process.env.PATH || "") } });
|
|
2776
|
-
child.once("error", reject);
|
|
2777
|
-
child.once("exit", (code) => code === 0 ? done() : reject(new Error(`Plugin dependency installation failed (${code})`)));
|
|
2778
|
-
});
|
|
2779
|
-
}
|
|
2780
|
-
async function installPackage(configPath, source) {
|
|
2781
|
-
const file2 = resolve6(configPath), root = dirname3(file2);
|
|
2782
|
-
const lockPath = resolve6(root, ".install.lock");
|
|
2783
|
-
const lock = await open(lockPath, "wx", 384).catch(() => {
|
|
2784
|
-
throw new Error("Another install is in progress");
|
|
2785
|
-
});
|
|
2786
|
-
let stage, committed = false;
|
|
2787
|
-
try {
|
|
2788
|
-
const config = await readConfig(file2), sourceRoot = await realpath4(source);
|
|
2789
|
-
if (!(await lstat(sourceRoot)).isDirectory()) throw new Error("Install expects an unpacked plugin directory");
|
|
2790
|
-
const pack = await readPackage(sourceRoot, config.version);
|
|
2791
|
-
await inspectTree(sourceRoot, sourceRoot);
|
|
2792
|
-
stage = resolve6(root, "plugins", pack.manifest.id, `${pack.manifest.version}-${randomUUID7()}`);
|
|
2793
|
-
await mkdir6(dirname3(stage), { recursive: true, mode: 448 });
|
|
2794
|
-
await cp(sourceRoot, stage, { recursive: true, verbatimSymlinks: true });
|
|
2795
|
-
await relocateLinks(stage, sourceRoot, stage);
|
|
2796
|
-
if (pack.manifest.backend?.dependencies?.mode === "npm-ci") await installDependencies(stage, pack.manifest.backend.main);
|
|
2797
|
-
const ready = await readPackage(stage, config.version);
|
|
2798
|
-
const entries = installations(config);
|
|
2799
|
-
const existing = (await Promise.all(entries.map(async (entry) => ({ entry, pack: await readPackage(entry.path) })))).find((p) => p.pack.manifest.id === pack.manifest.id);
|
|
2800
|
-
if (existing) {
|
|
2801
|
-
try {
|
|
2802
|
-
await sendControl(config.dataDir, { action: "update", id: pack.manifest.id, path: stage });
|
|
2803
|
-
committed = true;
|
|
2804
|
-
return { id: ready.manifest.id, version: ready.manifest.version, path: stage };
|
|
2805
|
-
} catch (error) {
|
|
2806
|
-
if (!unitIsOffline(error)) {
|
|
2807
|
-
committed = true;
|
|
2808
|
-
throw error;
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
existing.entry.path = stage;
|
|
2812
|
-
} else {
|
|
2813
|
-
try {
|
|
2814
|
-
await sendControl(config.dataDir, { action: "status" });
|
|
2815
|
-
throw new Error("Stop the Unit before installing a new plugin; updates can run live");
|
|
2816
|
-
} catch (error) {
|
|
2817
|
-
if (!unitIsOffline(error)) throw error;
|
|
2818
|
-
}
|
|
2819
|
-
entries.push({ path: stage, enabled: true });
|
|
2820
|
-
}
|
|
2821
|
-
if (!config.defaultSpace && ready.spaceIds.size) config.defaultSpace = [...ready.spaceIds][0];
|
|
2822
|
-
await writeConfig(file2, config);
|
|
2823
|
-
committed = true;
|
|
2824
|
-
return { id: ready.manifest.id, version: ready.manifest.version, path: stage };
|
|
2825
|
-
} finally {
|
|
2826
|
-
await lock.close();
|
|
2827
|
-
await rm3(lockPath, { force: true });
|
|
2828
|
-
if (!committed && stage) await rm3(stage, { recursive: true, force: true });
|
|
2829
|
-
}
|
|
2830
|
-
}
|
|
3259
|
+
// unit/main.ts
|
|
3260
|
+
init_config();
|
|
3261
|
+
init_packages();
|
|
3262
|
+
init_install();
|
|
3263
|
+
init_control();
|
|
2831
3264
|
|
|
2832
3265
|
// desktop/package.json
|
|
2833
3266
|
var package_default = {
|
|
2834
3267
|
name: "forsion-desktop",
|
|
2835
|
-
version: "2.
|
|
3268
|
+
version: "2.13.1",
|
|
2836
3269
|
description: "Forsion Desktop \u2014 AI \u9A71\u52A8\u7684\u5E94\u7528\u751F\u6001\u7CFB\u7EDF\u7684\u684C\u9762\u7AEF(Electron):Tangu Agent \u5F15\u64CE + LCL \u53EF\u505C\u9760\u5DE5\u4F5C\u533A\u6846\u67B6 + \u53EF\u63D2\u62D4\u529F\u80FD\u7A7A\u95F4(\u5BF9\u8BDD/\u7B14\u8BB0/\u65E5\u5386/\u6536\u4EF6\u7BB1/\u7F16\u7801),\u6309\u4EA7\u54C1\u6863\u6848\u88C1\u51FA\u5168\u5BB6\u6876\u4E0E\u5355\u54C1\u53D1\u884C\u7248",
|
|
2837
3270
|
author: "Forsion",
|
|
2838
3271
|
private: true,
|
|
@@ -2859,6 +3292,9 @@ var package_default = {
|
|
|
2859
3292
|
"e2e:dnd": "node scripts/block-dnd.e2e.cjs",
|
|
2860
3293
|
"e2e:sidebardrop": "node scripts/sidebar-drop.e2e.cjs",
|
|
2861
3294
|
"e2e:ribbon": "node scripts/ribbon-dnd.e2e.cjs",
|
|
3295
|
+
"e2e:spacewindow": "node scripts/space-window.e2e.cjs",
|
|
3296
|
+
"e2e:spacewindow-input": "node scripts/space-window-input.e2e.cjs",
|
|
3297
|
+
"e2e:ribbon-overflow": "node scripts/ribbon-overflow.e2e.cjs",
|
|
2862
3298
|
"e2e:mindmap": "node scripts/mindmap.e2e.cjs",
|
|
2863
3299
|
"e2e:canvas": "node scripts/canvas.e2e.cjs",
|
|
2864
3300
|
"e2e:daily": "node scripts/daily-template.e2e.cjs",
|
|
@@ -2870,6 +3306,7 @@ var package_default = {
|
|
|
2870
3306
|
"e2e:recall": "node scripts/recall-rewind.e2e.cjs",
|
|
2871
3307
|
"e2e:chatevents": "node scripts/chat-events.e2e.cjs",
|
|
2872
3308
|
"e2e:computerhistory": "node scripts/computer-history.e2e.cjs",
|
|
3309
|
+
"e2e:appdock": "node scripts/app-dock.e2e.cjs",
|
|
2873
3310
|
"e2e:remotesafety": "node scripts/remote-safety.e2e.cjs",
|
|
2874
3311
|
"e2e:chatshimmer": "node scripts/chat-shimmer.e2e.cjs",
|
|
2875
3312
|
"e2e:runstats": "node scripts/chat-runstats.e2e.cjs",
|
|
@@ -2881,6 +3318,7 @@ var package_default = {
|
|
|
2881
3318
|
"e2e:coreplugins": "node scripts/core-plugins.e2e.cjs",
|
|
2882
3319
|
"e2e:restart-update": "node scripts/restart-update.e2e.cjs",
|
|
2883
3320
|
"e2e:market-submissions": "node scripts/market-submissions.e2e.cjs",
|
|
3321
|
+
"e2e:storepages": "node scripts/store-pages.e2e.cjs",
|
|
2884
3322
|
"e2e:market-auto-update": "node scripts/market-auto-update.e2e.cjs",
|
|
2885
3323
|
"e2e:account": "node scripts/account-center.shot.cjs",
|
|
2886
3324
|
"e2e:accountpopups": "node scripts/account-popups.shot.cjs",
|
|
@@ -2893,6 +3331,7 @@ var package_default = {
|
|
|
2893
3331
|
"live:harness": "cd ../tangu-agent && npm run live:harness",
|
|
2894
3332
|
"probe:gzipupload": "node scripts/gzip-upload.probe.cjs",
|
|
2895
3333
|
"e2e:realtimevoice": "node scripts/realtime-voice.e2e.cjs",
|
|
3334
|
+
"e2e:voicestudio": "node scripts/voice-studio.e2e.cjs",
|
|
2896
3335
|
"e2e:dashdrag": "node scripts/dash3-drag.e2e.cjs",
|
|
2897
3336
|
"e2e:pathgone": "node scripts/vault-pathgone.e2e.cjs",
|
|
2898
3337
|
"check:pillbar": "node scripts/pillbar-overflow.check.cjs",
|
|
@@ -2907,6 +3346,7 @@ var package_default = {
|
|
|
2907
3346
|
"check:caret": "node scripts/smooth-caret.check.cjs",
|
|
2908
3347
|
"check:spacerestore": "node scripts/space-restore.check.cjs",
|
|
2909
3348
|
"check:spacerestart": "node scripts/space-restart.check.cjs",
|
|
3349
|
+
"check:spacefallback": "node scripts/space-fallback-persist.check.cjs",
|
|
2910
3350
|
"check:bluebirdspace": "node scripts/bluebird-space.check.cjs",
|
|
2911
3351
|
"check:bluebirdtimestamps": "node scripts/bluebird-space.check.cjs --timestamps",
|
|
2912
3352
|
"check:splitregions": "node scripts/split-regions.check.cjs",
|
|
@@ -2971,6 +3411,7 @@ var package_default = {
|
|
|
2971
3411
|
"check:hlband": "node scripts/pdf-highlight-geometry.check.cjs",
|
|
2972
3412
|
"e2e:filecite": "node scripts/file-citation.e2e.cjs",
|
|
2973
3413
|
"e2e:pathgonefiles": "node scripts/vault-pathgone-files.e2e.cjs",
|
|
3414
|
+
"e2e:ftoverride": "node scripts/filetype-override.e2e.cjs",
|
|
2974
3415
|
"e2e:pathgoneunmounted": "node scripts/vault-pathgone-unmounted.e2e.cjs",
|
|
2975
3416
|
"check:textfrag": "electron scripts/text-fragment.check.cjs",
|
|
2976
3417
|
"check:assetfetch": "electron scripts/asset-fetch.check.cjs",
|
|
@@ -2991,6 +3432,8 @@ var package_default = {
|
|
|
2991
3432
|
"check:settingsmode": "node scripts/settings-mode.check.cjs",
|
|
2992
3433
|
"check:themecontrast": "node scripts/theme-contrast.check.cjs",
|
|
2993
3434
|
"check:themevisual": "node scripts/theme-visual.check.cjs",
|
|
3435
|
+
"check:spaceappearance": "node scripts/space-appearance.e2e.cjs",
|
|
3436
|
+
"check:fps": "node scripts/check-fps.cjs",
|
|
2994
3437
|
"check:themeapp": "node scripts/theme-app.check.cjs",
|
|
2995
3438
|
"check:marketui": "node scripts/market-ui.check.cjs",
|
|
2996
3439
|
"probe:marketdl": "node scripts/market-download.probe.cjs",
|
|
@@ -3001,6 +3444,7 @@ var package_default = {
|
|
|
3001
3444
|
"check:floatingpanel": "node scripts/floating-panel.check.cjs",
|
|
3002
3445
|
"check:quietfocus": "node scripts/quiet-focus.check.cjs",
|
|
3003
3446
|
"check:persistignore": "node scripts/persist-ignore.check.cjs",
|
|
3447
|
+
"check:spaceicon": "node scripts/space-icon.check.cjs",
|
|
3004
3448
|
"check:onboarding": "node scripts/plugin-onboarding.check.cjs",
|
|
3005
3449
|
"check:plugindeps": "node scripts/plugin-deps.check.cjs",
|
|
3006
3450
|
"check:pluginsync": "node scripts/plugin-install-sync.check.cjs",
|
|
@@ -3011,18 +3455,22 @@ var package_default = {
|
|
|
3011
3455
|
"check:scrollbar": "node scripts/scrollbar-overlay.check.cjs",
|
|
3012
3456
|
"check:callout": "node scripts/callout-fold.check.cjs",
|
|
3013
3457
|
"check:toggle": "node scripts/e2e-editor.cjs --check=toggle-block",
|
|
3458
|
+
"check:togglecaret": "node scripts/e2e-editor.cjs --check=toggle-caret",
|
|
3014
3459
|
"check:togglecontent": "node scripts/e2e-editor.cjs --check=toggle-content",
|
|
3015
3460
|
"check:togglemotion": "node scripts/e2e-editor.cjs --check=toggle-motion",
|
|
3016
3461
|
"check:tabindent": "node scripts/e2e-editor.cjs --check=tab-indent",
|
|
3017
3462
|
"check:srcbtn": "node scripts/source-toggle.check.cjs",
|
|
3018
3463
|
"check:bsurface": "node scripts/e2e-editor.cjs --check=plugin-blocksurface",
|
|
3019
3464
|
"build:unitweb": "node scripts/build-unit-web.mjs",
|
|
3465
|
+
"check:unitweb": "node ../web/scripts/docker-context-build.mjs --unit-web",
|
|
3020
3466
|
pack: "npm run build:unitweb && electron-builder --config electron-builder.config.cjs --dir",
|
|
3021
3467
|
dist: "npm run build:unitweb && electron-builder --config electron-builder.config.cjs",
|
|
3022
3468
|
"fetch-python": "node build/fetch-python.cjs",
|
|
3023
3469
|
"fetch-node": "node build/fetch-node.cjs",
|
|
3024
3470
|
"fetch-office": "node build/fetch-office.cjs",
|
|
3025
3471
|
"fetch-git": "node build/fetch-git.cjs",
|
|
3472
|
+
"check:documentagents": "node scripts/e2e-editor.cjs --check=document-agents",
|
|
3473
|
+
"e2e:documentagents": "node scripts/document-agents.electron.cjs",
|
|
3026
3474
|
typecheck: "tsc --noEmit && node ../tangu-agent/scripts/sync-command-catalog.mjs --check",
|
|
3027
3475
|
"predev:amadeus": "node scripts/dev-version.check.cjs",
|
|
3028
3476
|
"dev:amadeus": "FORSION_PRODUCT=amadeus electron-vite dev --noSandbox",
|
|
@@ -3042,7 +3490,9 @@ var package_default = {
|
|
|
3042
3490
|
"check:hooks": 'eslint --config build/eslint.hooks.mjs "frontend/src/**/*.{ts,tsx}"',
|
|
3043
3491
|
"check:deskshot": "node scripts/desk-shot.check.cjs",
|
|
3044
3492
|
"check:splash": "node scripts/splash-loop.check.cjs",
|
|
3493
|
+
"probe:startup-windows": "node scripts/startup-windows.probe.cjs",
|
|
3045
3494
|
"check:parity": "node scripts/platform-parity.check.cjs",
|
|
3495
|
+
"check:vitecache": "node scripts/vite-cachedir.check.cjs",
|
|
3046
3496
|
"check:enginetarget": "node scripts/engine-target.check.cjs",
|
|
3047
3497
|
"check:remotechain": "node scripts/remote-chain.e2e.cjs",
|
|
3048
3498
|
"check:streamrenew": "node scripts/stream-renew.check.cjs",
|
|
@@ -3063,6 +3513,7 @@ var package_default = {
|
|
|
3063
3513
|
"check:focusring": "node scripts/focus-ring.check.cjs",
|
|
3064
3514
|
"check:layoutreset": "node scripts/layout-reset.check.cjs",
|
|
3065
3515
|
"check:statusbar": "node scripts/status-bar.check.cjs",
|
|
3516
|
+
"check:calm": "node scripts/calm.check.cjs",
|
|
3066
3517
|
"check:dropfeedback": "node scripts/drop-feedback.check.cjs",
|
|
3067
3518
|
"check:chatside": "node scripts/chat-sidepanel.check.cjs",
|
|
3068
3519
|
"check:facade": "node scripts/facade-scope.check.cjs",
|
|
@@ -3139,8 +3590,8 @@ var package_default = {
|
|
|
3139
3590
|
"@dnd-kit/sortable": "^10.0.0",
|
|
3140
3591
|
"@dnd-kit/utilities": "^3.2.2",
|
|
3141
3592
|
"@excalidraw/excalidraw": "npm:@zsviczian/excalidraw@0.18.122",
|
|
3142
|
-
"@forsion/extend": "0.
|
|
3143
|
-
"@forsion/tangu-computer-use": "0.6.
|
|
3593
|
+
"@forsion/extend": "0.8.1",
|
|
3594
|
+
"@forsion/tangu-computer-use": "0.6.2",
|
|
3144
3595
|
"@milkdown/kit": "^7.21.2",
|
|
3145
3596
|
"@milkdown/plugin-math": "^7.5.9",
|
|
3146
3597
|
"@milkdown/react": "^7.21.2",
|
|
@@ -3222,11 +3673,13 @@ var package_default = {
|
|
|
3222
3673
|
};
|
|
3223
3674
|
|
|
3224
3675
|
// unit/main.ts
|
|
3676
|
+
init_releases();
|
|
3225
3677
|
var dist = dirname4(fileURLToPath2(import.meta.url));
|
|
3226
3678
|
var args = process.argv.slice(2);
|
|
3227
|
-
var commands = ["init", "install", "run", "migrate", "enable", "disable", "restart", "status", "access"];
|
|
3679
|
+
var commands = ["init", "install", "stage", "update", "run", "migrate", "enable", "disable", "restart", "status", "access"];
|
|
3228
3680
|
var action = commands.includes(args[0]) ? args.shift() : "run";
|
|
3229
|
-
var
|
|
3681
|
+
var configArgument = args.shift();
|
|
3682
|
+
var file = resolve7(configArgument || (action === "init" ? "forsion-unit" : "unit.json"));
|
|
3230
3683
|
var print = (result) => console.log(JSON.stringify(result, null, 2));
|
|
3231
3684
|
async function findInstallation(config, id) {
|
|
3232
3685
|
if (!id) throw new Error("A plugin id is required");
|
|
@@ -3234,6 +3687,15 @@ async function findInstallation(config, id) {
|
|
|
3234
3687
|
throw new Error(`Plugin is not installed: ${id}`);
|
|
3235
3688
|
}
|
|
3236
3689
|
async function run() {
|
|
3690
|
+
if (action === "update") {
|
|
3691
|
+
if (!configArgument?.trim() || args.length !== 2 || args.some((arg) => !arg.trim())) {
|
|
3692
|
+
throw new Error("Usage: node main.mjs update <unit.json> <plugin-id> <package-path>");
|
|
3693
|
+
}
|
|
3694
|
+
if (!validId(args[0])) throw new Error("Invalid plugin id");
|
|
3695
|
+
}
|
|
3696
|
+
if (action === "stage" && (!configArgument?.trim() || args.length !== 1 || !args[0]?.trim())) {
|
|
3697
|
+
throw new Error("Usage: node main.mjs stage <unit.json> <bundled-package-path>");
|
|
3698
|
+
}
|
|
3237
3699
|
if (action === "init") {
|
|
3238
3700
|
const option = (key, fallback) => {
|
|
3239
3701
|
const i = args.indexOf(key);
|
|
@@ -3243,10 +3705,10 @@ async function run() {
|
|
|
3243
3705
|
if (!["public", "local"].includes(mode)) throw new Error("Mode must be public or local");
|
|
3244
3706
|
const port = Number(option("--port", "3001")), basePath = option("--base", mode === "local" ? "/" : "/admin/");
|
|
3245
3707
|
if (!Number.isInteger(port) || port < 0 || port > 65535 || !basePath?.startsWith("/")) throw new Error("Invalid port or projection base path");
|
|
3246
|
-
await
|
|
3708
|
+
await mkdir9(file, { recursive: true, mode: 448 });
|
|
3247
3709
|
const path = resolve7(file, "unit.json");
|
|
3248
3710
|
const config2 = {
|
|
3249
|
-
instanceId:
|
|
3711
|
+
instanceId: randomUUID10(),
|
|
3250
3712
|
name: "Forsion Unit",
|
|
3251
3713
|
version: package_default.version,
|
|
3252
3714
|
port,
|
|
@@ -3274,6 +3736,15 @@ async function run() {
|
|
|
3274
3736
|
}
|
|
3275
3737
|
const config = await readConfig(file);
|
|
3276
3738
|
config.workerFile ||= resolve7(dist, "backendWorker.mjs");
|
|
3739
|
+
if (action === "stage") {
|
|
3740
|
+
print(await stageRelease(config.dataDir, resolve7(args[0]), config.version));
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
if (action === "update") {
|
|
3744
|
+
await findInstallation(config, args[0]);
|
|
3745
|
+
print(await installPackage(file, resolve7(args[1]), { updateId: args[0] }));
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
3277
3748
|
if (action === "access") {
|
|
3278
3749
|
if (config.workspace?.mode !== "local") throw new Error("Public sites use their Account provider");
|
|
3279
3750
|
const token = await localOwnerToken(config.dataDir);
|
|
@@ -3302,7 +3773,7 @@ async function run() {
|
|
|
3302
3773
|
const pack = await readPackage(entry.path, config.version);
|
|
3303
3774
|
if (!pack.backendEntry) throw new Error("Plugin has no backend migration");
|
|
3304
3775
|
const dataDir = resolve7(config.dataDir, "plugins", pack.manifest.id);
|
|
3305
|
-
await
|
|
3776
|
+
await mkdir9(dataDir, { recursive: true, mode: 448 });
|
|
3306
3777
|
const migration = new AbortController();
|
|
3307
3778
|
const cancelMigration = () => migration.abort(new Error("Migration interrupted"));
|
|
3308
3779
|
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, cancelMigration);
|
|
@@ -3325,14 +3796,14 @@ async function run() {
|
|
|
3325
3796
|
return;
|
|
3326
3797
|
}
|
|
3327
3798
|
if (process.env.UNIT_PORT) config.port = Number(process.env.UNIT_PORT);
|
|
3328
|
-
await
|
|
3799
|
+
await mkdir9(config.dataDir, { recursive: true, mode: 448 });
|
|
3329
3800
|
const lockPath = resolve7(config.dataDir, "run.lock");
|
|
3330
3801
|
let lock;
|
|
3331
3802
|
try {
|
|
3332
3803
|
lock = await open2(lockPath, "wx", 384);
|
|
3333
3804
|
} catch (error) {
|
|
3334
3805
|
if (error.code !== "EEXIST") throw error;
|
|
3335
|
-
const pid = Number(await
|
|
3806
|
+
const pid = Number(await readFile9(lockPath, "utf8"));
|
|
3336
3807
|
if (!Number.isInteger(pid) || pid <= 0) throw new Error("Unit startup is locked; inspect data/run.lock");
|
|
3337
3808
|
try {
|
|
3338
3809
|
process.kill(pid, 0);
|
|
@@ -3340,13 +3811,14 @@ async function run() {
|
|
|
3340
3811
|
} catch (failure) {
|
|
3341
3812
|
if (failure.code !== "ESRCH") throw failure;
|
|
3342
3813
|
}
|
|
3343
|
-
await
|
|
3814
|
+
await rm5(lockPath);
|
|
3344
3815
|
lock = await open2(lockPath, "wx", 384);
|
|
3345
3816
|
}
|
|
3346
3817
|
await lock.writeFile(String(process.pid));
|
|
3347
3818
|
await lock.close();
|
|
3348
3819
|
let unit;
|
|
3349
3820
|
let control;
|
|
3821
|
+
let executeManagement;
|
|
3350
3822
|
let shutdownRequested = false, requestStop;
|
|
3351
3823
|
const onSignal = () => {
|
|
3352
3824
|
shutdownRequested = true;
|
|
@@ -3354,11 +3826,19 @@ async function run() {
|
|
|
3354
3826
|
};
|
|
3355
3827
|
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, onSignal);
|
|
3356
3828
|
try {
|
|
3357
|
-
unit = await startBasicUnit(config)
|
|
3829
|
+
unit = await startBasicUnit(config, { execute: async (command, authorize) => {
|
|
3830
|
+
if (!executeManagement) throw new Error("Unit control is not ready");
|
|
3831
|
+
return executeManagement(command, authorize);
|
|
3832
|
+
} });
|
|
3358
3833
|
let queue = Promise.resolve();
|
|
3359
3834
|
const status = () => ({ running: true, pid: process.pid, instanceId: config.instanceId, port: unit.port, plugins: unit.status() });
|
|
3360
3835
|
const dispatch = async (command) => {
|
|
3361
3836
|
if (command.action === "status") return status();
|
|
3837
|
+
if (command.action === "stage") {
|
|
3838
|
+
if (command.remote && config.management?.enabled !== true) throw new Error("Remote Unit management is disabled");
|
|
3839
|
+
if (!command.path) throw new Error("A prepared package is required");
|
|
3840
|
+
return stageRelease(config.dataDir, command.path, config.version);
|
|
3841
|
+
}
|
|
3362
3842
|
if (!["enable", "disable", "restart", "update", "migrate"].includes(command.action)) throw new Error("Unknown Unit command");
|
|
3363
3843
|
const entry = await findInstallation(config, command.id);
|
|
3364
3844
|
const previous = { ...entry };
|
|
@@ -3386,13 +3866,26 @@ async function run() {
|
|
|
3386
3866
|
}
|
|
3387
3867
|
return status();
|
|
3388
3868
|
};
|
|
3389
|
-
|
|
3390
|
-
const next = queue.then(
|
|
3869
|
+
const enqueue = (run2) => {
|
|
3870
|
+
const next = queue.then(run2);
|
|
3391
3871
|
queue = next.then(() => {
|
|
3392
3872
|
}, () => {
|
|
3393
3873
|
});
|
|
3394
3874
|
return next;
|
|
3395
|
-
}
|
|
3875
|
+
};
|
|
3876
|
+
executeManagement = async (command, authorize) => {
|
|
3877
|
+
const apply = (action2) => enqueue(async () => {
|
|
3878
|
+
await authorize();
|
|
3879
|
+
return dispatch(action2);
|
|
3880
|
+
});
|
|
3881
|
+
if (command.action === "update") return installPackage(file, command.path, {
|
|
3882
|
+
updateId: command.id,
|
|
3883
|
+
releaseDigest: command.releaseDigest,
|
|
3884
|
+
applyUpdate: (id, path) => apply({ action: "update", id, path })
|
|
3885
|
+
});
|
|
3886
|
+
return apply({ action: command.action, id: command.id });
|
|
3887
|
+
};
|
|
3888
|
+
control = await startControl(config.dataDir, (command) => enqueue(() => dispatch(command)));
|
|
3396
3889
|
print(status());
|
|
3397
3890
|
console.log(`[unit] ${config.name} http://${config.bindHost || "127.0.0.1"}:${unit.port}${config.basePath}`);
|
|
3398
3891
|
let stopping = false;
|
|
@@ -3401,6 +3894,7 @@ async function run() {
|
|
|
3401
3894
|
stopping = true;
|
|
3402
3895
|
let failure = false;
|
|
3403
3896
|
try {
|
|
3897
|
+
await unit.closeManagement();
|
|
3404
3898
|
await control.close();
|
|
3405
3899
|
await queue;
|
|
3406
3900
|
await unit.close();
|
|
@@ -3408,7 +3902,7 @@ async function run() {
|
|
|
3408
3902
|
console.error(error);
|
|
3409
3903
|
failure = true;
|
|
3410
3904
|
} finally {
|
|
3411
|
-
await
|
|
3905
|
+
await rm5(lockPath, { force: true });
|
|
3412
3906
|
process.exit(failure ? 1 : 0);
|
|
3413
3907
|
}
|
|
3414
3908
|
};
|
|
@@ -3422,7 +3916,7 @@ async function run() {
|
|
|
3422
3916
|
try {
|
|
3423
3917
|
await unit?.close();
|
|
3424
3918
|
} finally {
|
|
3425
|
-
await
|
|
3919
|
+
await rm5(lockPath, { force: true });
|
|
3426
3920
|
}
|
|
3427
3921
|
}
|
|
3428
3922
|
throw error;
|