@dimina-kit/devtools 0.3.2-dev.20260525152421 → 0.3.2-dev.20260610082053
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 +43 -40
- package/dist/main/api.d.ts +0 -1
- package/dist/main/api.js +5 -1
- package/dist/main/app/app.d.ts +15 -5
- package/dist/main/app/app.js +247 -110
- package/dist/main/app/bootstrap.d.ts +16 -0
- package/dist/main/app/bootstrap.js +20 -0
- package/dist/main/app/launch.d.ts +8 -4
- package/dist/main/app/launch.js +9 -4
- package/dist/main/index.bundle.js +6607 -2102
- package/dist/main/index.js +9 -1
- package/dist/main/ipc/app.d.ts +1 -1
- package/dist/main/ipc/bridge-router.d.ts +61 -0
- package/dist/main/ipc/bridge-router.js +1300 -0
- package/dist/main/ipc/panels.d.ts +1 -1
- package/dist/main/ipc/panels.js +6 -2
- package/dist/main/ipc/popover.d.ts +1 -1
- package/dist/main/ipc/project-fs.d.ts +57 -0
- package/dist/main/ipc/project-fs.js +448 -0
- package/dist/main/ipc/projects.d.ts +1 -1
- package/dist/main/ipc/session.d.ts +1 -1
- package/dist/main/ipc/settings.d.ts +1 -1
- package/dist/main/ipc/simulator-module.d.ts +6 -0
- package/dist/main/ipc/simulator-module.js +11 -1
- package/dist/main/ipc/simulator.d.ts +2 -2
- package/dist/main/ipc/simulator.js +49 -5
- package/dist/main/ipc/toolbar.d.ts +1 -1
- package/dist/main/ipc/views.d.ts +19 -0
- package/dist/main/ipc/views.js +65 -0
- package/dist/main/runtime/devtools-backend.d.ts +17 -0
- package/dist/main/runtime/devtools-backend.js +49 -0
- package/dist/main/runtime/miniapp-runtime.d.ts +39 -0
- package/dist/main/runtime/miniapp-runtime.js +17 -0
- package/dist/main/services/automation/exec.d.ts +27 -2
- package/dist/main/services/automation/exec.js +37 -3
- package/dist/main/services/automation/handlers/app.js +127 -11
- package/dist/main/services/automation/handlers/page.js +36 -19
- package/dist/main/services/automation/index.js +40 -13
- package/dist/main/services/automation/wait-active-page.d.ts +56 -0
- package/dist/main/services/automation/wait-active-page.js +71 -0
- package/dist/main/services/console-forward/index.d.ts +36 -0
- package/dist/main/services/console-forward/index.js +81 -0
- package/dist/main/services/dimina-resource-server.d.ts +7 -0
- package/dist/main/services/dimina-resource-server.js +99 -0
- package/dist/main/services/elements-forward/index.d.ts +66 -0
- package/dist/main/services/elements-forward/index.js +583 -0
- package/dist/main/services/layout/index.d.ts +23 -1
- package/dist/main/services/layout/index.js +28 -3
- package/dist/main/services/mcp/index.d.ts +1 -0
- package/dist/main/services/mcp/index.js +3 -0
- package/dist/main/services/mcp/server.d.ts +1 -1
- package/dist/main/services/mcp/server.js +1 -1
- package/dist/main/services/mcp/target-manager.d.ts +35 -0
- package/dist/main/services/mcp/target-manager.js +63 -1
- package/dist/main/services/mcp/tools/context-tools.js +15 -1
- package/dist/main/services/module.d.ts +1 -1
- package/dist/main/services/network-forward/index.d.ts +190 -0
- package/dist/main/services/network-forward/index.js +765 -0
- package/dist/main/services/notifications/renderer-notifier.d.ts +11 -0
- package/dist/main/services/notifications/renderer-notifier.js +7 -1
- package/dist/main/services/render-inspect/index.d.ts +41 -0
- package/dist/main/services/render-inspect/index.js +88 -0
- package/dist/main/services/safe-area/index.d.ts +17 -0
- package/dist/main/services/safe-area/index.js +59 -0
- package/dist/main/services/service-host-pool/pool.d.ts +197 -0
- package/dist/main/services/service-host-pool/pool.js +364 -0
- package/dist/main/services/simulator/custom-apis.d.ts +34 -0
- package/dist/main/services/simulator/custom-apis.js +22 -0
- package/dist/main/services/simulator-appdata/index.d.ts +35 -0
- package/dist/main/services/simulator-appdata/index.js +71 -0
- package/dist/main/services/simulator-current-page/index.d.ts +24 -0
- package/dist/main/services/simulator-current-page/index.js +19 -0
- package/dist/main/services/simulator-storage/index.d.ts +37 -2
- package/dist/main/services/simulator-storage/index.js +209 -32
- package/dist/main/services/simulator-storage/service-storage-ops.d.ts +40 -0
- package/dist/main/services/simulator-storage/service-storage-ops.js +67 -0
- package/dist/main/services/simulator-temp-files/fs-channels.js +1 -1
- package/dist/main/services/simulator-temp-files/index.d.ts +1 -1
- package/dist/main/services/simulator-temp-files/index.js +1 -1
- package/dist/main/services/simulator-temp-files/request-handler.js +1 -1
- package/dist/main/services/simulator-wxml/index.d.ts +29 -0
- package/dist/main/services/simulator-wxml/index.js +28 -0
- package/dist/main/services/update/update-manager.d.ts +10 -0
- package/dist/main/services/update/update-manager.js +16 -3
- package/dist/main/services/views/devtools-tabs.d.ts +47 -0
- package/dist/main/services/views/devtools-tabs.js +95 -0
- package/dist/main/services/views/view-manager.d.ts +142 -4
- package/dist/main/services/views/view-manager.js +909 -17
- package/dist/main/services/workbench-context.d.ts +66 -1
- package/dist/main/services/workbench-context.js +6 -1
- package/dist/main/services/workspace/workspace-service.d.ts +8 -0
- package/dist/main/services/workspace/workspace-service.js +12 -0
- package/dist/main/utils/ipc-registry.d.ts +11 -2
- package/dist/main/utils/ipc-registry.js +63 -3
- package/dist/main/utils/paths.d.ts +15 -0
- package/dist/main/utils/paths.js +19 -0
- package/dist/main/utils/sender-policy.d.ts +9 -3
- package/dist/main/utils/sender-policy.js +18 -4
- package/dist/main/utils/theme.d.ts +1 -1
- package/dist/main/utils/theme.js +1 -1
- package/dist/main/windows/main-window/create.d.ts +0 -6
- package/dist/main/windows/main-window/create.js +11 -59
- package/dist/main/windows/main-window/events.d.ts +1 -1
- package/dist/main/windows/main-window/events.js +1 -1
- package/dist/main/windows/service-host-window/create.d.ts +52 -0
- package/dist/main/windows/service-host-window/create.js +112 -0
- package/dist/native-host/common/common.js +368 -0
- package/dist/native-host/container/pageFrame.css +1 -0
- package/dist/native-host/render/render.js +9334 -0
- package/dist/native-host/service/service.js +6 -0
- package/dist/preload/index.d.ts +2 -0
- package/dist/preload/index.js +6 -0
- package/dist/preload/instrumentation/app-data.d.ts +18 -20
- package/dist/preload/instrumentation/app-data.js +23 -160
- package/dist/preload/instrumentation/wxml-extract.d.ts +1 -1
- package/dist/preload/instrumentation/wxml-extract.js +1 -1
- package/dist/preload/runtime/bridge.d.ts +3 -8
- package/dist/preload/runtime/bridge.js +2 -32
- package/dist/preload/runtime/custom-apis.js +20 -17
- package/dist/preload/runtime/host-toolbar-advertiser.d.ts +27 -0
- package/dist/preload/runtime/host-toolbar-advertiser.js +55 -0
- package/dist/preload/runtime/main-api-runner.d.ts +9 -0
- package/dist/preload/runtime/main-api-runner.js +85 -0
- package/dist/preload/runtime/native-host.d.ts +43 -0
- package/dist/preload/runtime/native-host.js +83 -0
- package/dist/preload/shared/expose.d.ts +10 -5
- package/dist/preload/shared/expose.js +29 -11
- package/dist/preload/shared/sid-registry.d.ts +11 -0
- package/dist/preload/shared/sid-registry.js +38 -0
- package/dist/preload/windows/host-toolbar.cjs +161 -0
- package/dist/preload/windows/host-toolbar.cjs.map +7 -0
- package/dist/preload/windows/host-toolbar.d.ts +2 -0
- package/dist/preload/windows/host-toolbar.js +12 -0
- package/dist/preload/windows/main.cjs +1939 -0
- package/dist/preload/windows/main.cjs.map +4 -4
- package/dist/preload/windows/simulator.cjs +925 -0
- package/dist/preload/windows/simulator.cjs.map +7 -0
- package/dist/preload/windows/simulator.js +243 -573
- package/dist/render-host/pageFrame.html +29 -0
- package/dist/render-host/preload.cjs +154 -0
- package/dist/render-host/render-inspect.js +295 -0
- package/dist/renderer/assets/abap-DiwvWnMr.js +2 -0
- package/dist/renderer/assets/apex-CmtZjKlf.js +2 -0
- package/dist/renderer/assets/azcli-DL2My_i-.js +2 -0
- package/dist/renderer/assets/bat-B-nC98wG.js +2 -0
- package/dist/renderer/assets/bicep-Ju5MwOgh.js +3 -0
- package/dist/renderer/assets/cameligo-8Eu1TyBr.js +2 -0
- package/dist/renderer/assets/chunk-DPI0Y_ll.js +1 -0
- package/dist/renderer/assets/clojure-u-RpMkH3.js +2 -0
- package/dist/renderer/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/renderer/assets/coffee-CdA7bbTe.js +2 -0
- package/dist/renderer/assets/constants-CLAQX6VZ.js +2 -0
- package/dist/renderer/assets/cpp-CzNFP8ks.js +2 -0
- package/dist/renderer/assets/csharp-j1LThmcE.js +2 -0
- package/dist/renderer/assets/csp-CLRC61y6.js +2 -0
- package/dist/renderer/assets/css-r6rC_7P2.js +4 -0
- package/dist/renderer/assets/css.worker-DyDu5ynT.js +90 -0
- package/dist/renderer/assets/cssMode-BCBgxrnA.js +2 -0
- package/dist/renderer/assets/cypher-CW08XVUh.js +2 -0
- package/dist/renderer/assets/dart-Cs9aL5T_.js +2 -0
- package/dist/renderer/assets/dockerfile-BWM0M184.js +2 -0
- package/dist/renderer/assets/ecl-MJJuer5P.js +2 -0
- package/dist/renderer/assets/editor-CWI9TnMM.css +1 -0
- package/dist/renderer/assets/editor.api2-BTaYmRq7.js +873 -0
- package/dist/renderer/assets/editor.worker-Dpt_xPrs.js +27 -0
- package/dist/renderer/assets/elixir-D2AIuXqn.js +2 -0
- package/dist/renderer/assets/flow9-B2H24giC.js +2 -0
- package/dist/renderer/assets/freemarker2-DGDGzimW.js +4 -0
- package/dist/renderer/assets/fsharp-CMk2OIJN.js +2 -0
- package/dist/renderer/assets/go-BrMkuJg0.js +2 -0
- package/dist/renderer/assets/graphql-PSR1UKGv.js +2 -0
- package/dist/renderer/assets/handlebars-bOHhw3EG.js +2 -0
- package/dist/renderer/assets/hcl-DAQrbDOW.js +2 -0
- package/dist/renderer/assets/html-BMyuX0_c.js +2 -0
- package/dist/renderer/assets/html.worker-IWNXhnIC.js +503 -0
- package/dist/renderer/assets/htmlMode-DR4kXJ7l.js +2 -0
- package/dist/renderer/assets/index-CcJKzatF.js +50 -0
- package/dist/renderer/assets/ini-0TG5BxW0.js +2 -0
- package/dist/renderer/assets/input-QGx6FFX1.js +2 -0
- package/dist/renderer/assets/ipc-transport-BT4QYQqu.css +1 -0
- package/dist/renderer/assets/{ipc-transport-BzPzxPwf.js → ipc-transport-DvZvwK-Z.js} +5 -5
- package/dist/renderer/assets/java-rgorz17v.js +2 -0
- package/dist/renderer/assets/javascript-Bu-tl1P7.js +2 -0
- package/dist/renderer/assets/json.worker-c4h5s80L.js +59 -0
- package/dist/renderer/assets/jsonMode-MQV_ffLR.js +8 -0
- package/dist/renderer/assets/julia-C8VMdHm8.js +2 -0
- package/dist/renderer/assets/kotlin-AhhC4Qhm.js +2 -0
- package/dist/renderer/assets/less-Cgca25AP.js +3 -0
- package/dist/renderer/assets/lexon-D0GHdBaw.js +2 -0
- package/dist/renderer/assets/liquid-ClkfOvhB.js +2 -0
- package/dist/renderer/assets/lspLanguageFeatures-Bke_4Ie9.js +5 -0
- package/dist/renderer/assets/lua-DmRsNG-P.js +2 -0
- package/dist/renderer/assets/m3-BgL5dNKT.js +2 -0
- package/dist/renderer/assets/markdown-BuJfycGS.js +2 -0
- package/dist/renderer/assets/mdx-RZdUskPu.js +2 -0
- package/dist/renderer/assets/mips-C9m_93PR.js +2 -0
- package/dist/renderer/assets/monaco.contribution-B7rUMaEv.js +3 -0
- package/dist/renderer/assets/msdax-CpFHC9OI.js +2 -0
- package/dist/renderer/assets/mysql-qFvltsqN.js +2 -0
- package/dist/renderer/assets/objective-c-Bnmr858J.js +2 -0
- package/dist/renderer/assets/pascal-WP0_D5AO.js +2 -0
- package/dist/renderer/assets/pascaligo-Blom4Rij.js +2 -0
- package/dist/renderer/assets/perl-B-vk8g64.js +2 -0
- package/dist/renderer/assets/pgsql-Cgvz6v67.js +2 -0
- package/dist/renderer/assets/php-8a3Lrw9m.js +2 -0
- package/dist/renderer/assets/pla-DuFqEZ8V.js +2 -0
- package/dist/renderer/assets/popover-BOH1If1J.js +2 -0
- package/dist/renderer/assets/postiats-DkLtSgkp.js +2 -0
- package/dist/renderer/assets/powerquery-BJ1aNepW.js +2 -0
- package/dist/renderer/assets/powershell-rE98k687.js +2 -0
- package/dist/renderer/assets/protobuf-CUheFacr.js +3 -0
- package/dist/renderer/assets/pug-LDcAMD8w.js +2 -0
- package/dist/renderer/assets/python-Cobrm_-8.js +2 -0
- package/dist/renderer/assets/qsharp-DUKSQoR1.js +2 -0
- package/dist/renderer/assets/r-D-QApv87.js +2 -0
- package/dist/renderer/assets/razor-BEUIQFNJ.js +2 -0
- package/dist/renderer/assets/redis-SXdDyWR9.js +2 -0
- package/dist/renderer/assets/redshift-Y6lsCryn.js +2 -0
- package/dist/renderer/assets/restructuredtext-edObr9a8.js +2 -0
- package/dist/renderer/assets/ruby-CNnUfF-8.js +2 -0
- package/dist/renderer/assets/rust-IHUZWzBr.js +2 -0
- package/dist/renderer/assets/sb-DrUvY44N.js +2 -0
- package/dist/renderer/assets/scala-B4hbXGLM.js +2 -0
- package/dist/renderer/assets/scheme-BGrd12j3.js +2 -0
- package/dist/renderer/assets/scss-x5G1ES4U.js +4 -0
- package/dist/renderer/assets/select-C7JQ8G5N.js +2 -0
- package/dist/renderer/assets/settings-DpAbTJyF.js +2 -0
- package/dist/renderer/assets/{settings-api-BXCw9vOZ.js → settings-api-RD2IZF53.js} +2 -2
- package/dist/renderer/assets/shell-DOehe2Y8.js +2 -0
- package/dist/renderer/assets/solidity-BeRvcwWV.js +2 -0
- package/dist/renderer/assets/sophia-DZbkUNjy.js +2 -0
- package/dist/renderer/assets/sparql-B7_oi5-h.js +2 -0
- package/dist/renderer/assets/sql-CTlsFWVE.js +2 -0
- package/dist/renderer/assets/st-DJVEJdPE.js +2 -0
- package/dist/renderer/assets/swift-CwhT3fYa.js +2 -0
- package/dist/renderer/assets/systemverilog-BQN63pkN.js +2 -0
- package/dist/renderer/assets/tcl-DqwfpskA.js +2 -0
- package/dist/renderer/assets/ts.worker-BwM5Ha3L.js +67720 -0
- package/dist/renderer/assets/tsMode-BHa7dl9v.js +12 -0
- package/dist/renderer/assets/twig-BiyenUgc.js +2 -0
- package/dist/renderer/assets/typescript-15aT0Yj8.js +2 -0
- package/dist/renderer/assets/typespec-CWOJribt.js +2 -0
- package/dist/renderer/assets/vb-Cq5F87m3.js +2 -0
- package/dist/renderer/assets/wgsl-BAvW2lVr.js +299 -0
- package/dist/renderer/assets/workbenchSettings-CNJX4Qna.js +8 -0
- package/dist/renderer/assets/workers-CRBS0W-v.js +2 -0
- package/dist/renderer/assets/xml-bR2Zh-HV.js +2 -0
- package/dist/renderer/assets/yaml-Bdk54Qrb.js +2 -0
- package/dist/renderer/entries/main/index.html +29 -6
- package/dist/renderer/entries/popover/index.html +7 -6
- package/dist/renderer/entries/settings/index.html +6 -5
- package/dist/renderer/entries/workbench-settings/index.html +6 -5
- package/dist/service-host/preload.cjs +254 -0
- package/dist/service-host/service.html +10 -0
- package/dist/service-host/sourcemap-rewrite.cjs +58 -0
- package/dist/service-host/sync-api-patch.js +20 -0
- package/dist/service-host/sync-impls/menu-button.js +16 -0
- package/dist/service-host/sync-impls/storage.js +50 -0
- package/dist/service-host/sync-impls/system-info.js +54 -0
- package/dist/shared/appdata-accumulator.d.ts +95 -0
- package/dist/shared/appdata-accumulator.js +213 -0
- package/dist/shared/bridge-channels.d.ts +364 -0
- package/dist/shared/bridge-channels.js +54 -0
- package/dist/shared/constants.d.ts +0 -4
- package/dist/shared/constants.js +0 -4
- package/dist/shared/ipc-channels.d.ts +108 -1
- package/dist/shared/ipc-channels.js +127 -10
- package/dist/shared/ipc-schemas.d.ts +65 -2
- package/dist/shared/ipc-schemas.js +79 -3
- package/dist/shared/open-in-editor.d.ts +71 -0
- package/dist/shared/open-in-editor.js +138 -0
- package/dist/shared/simulator-api-metadata.d.ts +21 -0
- package/dist/shared/simulator-api-metadata.js +23 -0
- package/dist/shared/types.d.ts +2 -2
- package/dist/shared/vpath.d.ts +42 -0
- package/dist/shared/vpath.js +105 -0
- package/dist/simulator/assets/device-shell-DsdcNEkt.css +1 -0
- package/dist/simulator/assets/device-shell-vwYSOVrh.js +2 -0
- package/dist/simulator/assets/jsx-runtime-P6ZegaOf.js +2 -0
- package/dist/simulator/assets/simulator-DddliUL5.js +10 -0
- package/dist/simulator/assets/simulator-mini-app-BDNu5n26.js +2 -0
- package/dist/simulator/device-shell/menu-button-geometry.js +8 -0
- package/dist/simulator/simulator.html +2 -112
- package/package.json +9 -9
- package/dist/main/utils/disposable.d.ts +0 -18
- package/dist/main/utils/disposable.js +0 -67
- package/dist/renderer/assets/constants-DI382mTP.js +0 -2
- package/dist/renderer/assets/index-4lkyF644.js +0 -46
- package/dist/renderer/assets/input-D4byFOLS.js +0 -2
- package/dist/renderer/assets/ipc-transport-DqbRdZNO.css +0 -1
- package/dist/renderer/assets/popover-cxKrg_os.js +0 -2
- package/dist/renderer/assets/select-BgSkkdHW.js +0 -2
- package/dist/renderer/assets/settings-oD2K1jdT.js +0 -2
- package/dist/renderer/assets/workbenchSettings-CTNBpTft.js +0 -8
- package/dist/simulator/assets/simulator-CVOFTpSj.js +0 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { WorkbenchContext } from '../services/workbench-context.js';
|
|
2
|
-
import type { Disposable } from '
|
|
2
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
3
3
|
export declare function registerPanelsIpc(ctx: Pick<WorkbenchContext, 'panels' | 'views' | 'workspace' | 'senderPolicy'>): Disposable;
|
|
4
4
|
//# sourceMappingURL=panels.d.ts.map
|
package/dist/main/ipc/panels.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PanelChannel } from '../../shared/ipc-channels.js';
|
|
1
|
+
import { PanelChannel, WorkbenchRuntimeChannel } from '../../shared/ipc-channels.js';
|
|
2
2
|
import { PanelEvalSchema, PanelSelectSchema, } from '../../shared/ipc-schemas.js';
|
|
3
3
|
import { validate } from '../utils/ipc-schema.js';
|
|
4
4
|
import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
@@ -37,6 +37,10 @@ export function registerPanelsIpc(ctx) {
|
|
|
37
37
|
if (ctx.views.hasSimulatorView() && !ctx.views.isSimulatorAdded()) {
|
|
38
38
|
ctx.views.showSimulator(ctx.views.getLastSimWidth());
|
|
39
39
|
}
|
|
40
|
-
})
|
|
40
|
+
})
|
|
41
|
+
// Renderer reads this once to pick panel data sources: native-host is the
|
|
42
|
+
// sole runtime, so WXML + AppData always come from the main process (the
|
|
43
|
+
// page/service logic live in separate webContents).
|
|
44
|
+
.handle(WorkbenchRuntimeChannel.GetNativeHost, () => true);
|
|
41
45
|
}
|
|
42
46
|
//# sourceMappingURL=panels.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WorkbenchContext } from '../services/workbench-context.js';
|
|
2
2
|
import type { WorkbenchModule } from '../services/module.js';
|
|
3
|
-
import type { Disposable } from '
|
|
3
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
4
4
|
export declare function registerPopoverIpc(ctx: Pick<WorkbenchContext, 'views' | 'notify' | 'senderPolicy'>): Disposable;
|
|
5
5
|
export declare const popoverModule: WorkbenchModule;
|
|
6
6
|
//# sourceMappingURL=popover.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { WorkbenchContext } from '../services/workbench-context.js';
|
|
2
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
3
|
+
/**
|
|
4
|
+
* Synchronous, string-level guard: reject empty root (ENOACTIVE), NUL bytes
|
|
5
|
+
* and empty paths (EINVAL), and lexical `..` escapes (EACCES) by resolving
|
|
6
|
+
* both sides with `path.resolve` and a `path.sep`-aware containment check
|
|
7
|
+
* (so `/foo/bar` ⊄ `/foo/bar2`). This is the cheap first pass; it does NOT
|
|
8
|
+
* resolve symlinks — that is done by `resolveWithinProjectRoot` below.
|
|
9
|
+
*/
|
|
10
|
+
export declare function enforceWithinProjectRoot(abs: string, root: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Pick the project root a write should be validated against: the CURRENT root
|
|
13
|
+
* or the LAST-CLOSED one (recorded by workspace-service in `closeProject`,
|
|
14
|
+
* reset in `openProject`). This rescues an in-flight teardown/debounced write
|
|
15
|
+
* whose target lives under the project that was just closed — `closeProject`
|
|
16
|
+
* clears `currentProjectPath` to '' BEFORE the renderer navigates away, so a
|
|
17
|
+
* write resolving its root via the live root alone would be rejected with
|
|
18
|
+
* ENOACTIVE and the developer's last edit lost.
|
|
19
|
+
*
|
|
20
|
+
* Selection is purely LEXICAL (same `path.resolve` + `path.sep`-aware
|
|
21
|
+
* containment as `enforceWithinProjectRoot`, no fs I/O): symlink/realpath
|
|
22
|
+
* hardening stays in the downstream `resolveWithinProjectRoot` /
|
|
23
|
+
* `assertWritableAncestor` defences, applied by the caller against the root
|
|
24
|
+
* this returns — so accepting the last-closed root never relaxes the sandbox.
|
|
25
|
+
*
|
|
26
|
+
* - Returns whichever root lexically contains `absPath`, preferring `current`.
|
|
27
|
+
* - An empty-string root ('') means "no such root" and is never selected.
|
|
28
|
+
* - Throws when `absPath` is inside NEITHER root: ENOACTIVE when both roots
|
|
29
|
+
* are empty (no project context at all), otherwise EACCES (the path escapes
|
|
30
|
+
* every known sandbox root).
|
|
31
|
+
*/
|
|
32
|
+
export declare function pickWriteRoot(absPath: string, roots: {
|
|
33
|
+
current: string;
|
|
34
|
+
lastClosed: string;
|
|
35
|
+
}): string;
|
|
36
|
+
declare function readFile(root: string, p: string): Promise<string>;
|
|
37
|
+
declare function writeFile(root: string, p: string, content: string): Promise<void>;
|
|
38
|
+
/** Synchronous twin of {@link writeFile} — identical sandbox, blocking I/O. */
|
|
39
|
+
declare function writeFileSync(root: string, p: string, content: string): void;
|
|
40
|
+
declare function listFiles(root: string, rootArg: string): Promise<string[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Register the project-fs IPC channels. Call once during workbench setup;
|
|
43
|
+
* the returned Disposable removes every channel (park on `ctx.registry`).
|
|
44
|
+
*/
|
|
45
|
+
export declare function registerProjectFsIpc(ctx: Pick<WorkbenchContext, 'workspace' | 'senderPolicy'>): Disposable;
|
|
46
|
+
/** Test seams — pure handlers, no Electron needed. */
|
|
47
|
+
export declare const __testing: {
|
|
48
|
+
readFile: typeof readFile;
|
|
49
|
+
writeFile: typeof writeFile;
|
|
50
|
+
writeFileSync: typeof writeFileSync;
|
|
51
|
+
listFiles: typeof listFiles;
|
|
52
|
+
pickWriteRoot: typeof pickWriteRoot;
|
|
53
|
+
SKIP_DIRS: ReadonlySet<string>;
|
|
54
|
+
LIST_FILE_LIMIT: number;
|
|
55
|
+
};
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=project-fs.d.ts.map
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandboxed project file-system IPC for the in-renderer Monaco editor.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the OpenSumi editor's `editor:fs:*` bridge (which was gated to a
|
|
5
|
+
* separate WebContentsView via an allow-list). The Monaco editor now lives
|
|
6
|
+
* in the MAIN renderer, so these channels run under the standard workbench
|
|
7
|
+
* sender policy (`ctx.senderPolicy`) — the main window is already trusted.
|
|
8
|
+
*
|
|
9
|
+
* Sandbox: every path is resolved and verified against the live active
|
|
10
|
+
* project root (`ctx.workspace.getProjectPath()`, read per-call so project
|
|
11
|
+
* switches take effect immediately). `..`/escapes throw `EACCES`; no open
|
|
12
|
+
* project throws `ENOACTIVE`. Symlinks ARE resolved: both the root and the
|
|
13
|
+
* target go through `fs.realpath` before the containment check, so a symlink
|
|
14
|
+
* inside the root that points outside it (`proj/link -> ../secret`) cannot be
|
|
15
|
+
* used to read or write past the sandbox. Writes additionally verify the
|
|
16
|
+
* deepest existing ancestor directory with `fs.realpath` BEFORE any `mkdir`,
|
|
17
|
+
* so a symlinked-out parent cannot leak a `mkdir -p` side-effect (a directory
|
|
18
|
+
* created outside the sandbox) ahead of the containment check.
|
|
19
|
+
*
|
|
20
|
+
* TOCTOU hardening (two layers):
|
|
21
|
+
* 1. The final `open` uses `O_NOFOLLOW`, so a FINAL path component swapped to
|
|
22
|
+
* a symlink between the realpath check and the open is refused instead of
|
|
23
|
+
* followed out of the sandbox.
|
|
24
|
+
* 2. After the open, the resolved path is re-`realpath`'d and re-checked
|
|
25
|
+
* against the root BEFORE any bytes are returned (read) or written (write).
|
|
26
|
+
* This catches a MID-PATH directory component swapped to a symlink in the
|
|
27
|
+
* same window — which `O_NOFOLLOW` does NOT guard. Writes open with
|
|
28
|
+
* `O_CREAT` but WITHOUT `O_TRUNC`, and only truncate+write once this
|
|
29
|
+
* re-check passes, so a detected mid-path race never writes the user's
|
|
30
|
+
* content to an out-of-sandbox target (at worst it leaves a zero-byte file
|
|
31
|
+
* if `O_CREAT` created one at the swapped location).
|
|
32
|
+
*
|
|
33
|
+
* Residual: a perfectly-timed swap-and-restore that puts the mid-path symlink
|
|
34
|
+
* in place only for the open and reverts it before the post-open realpath would
|
|
35
|
+
* evade detection. Provably closing that needs per-component `openat` /
|
|
36
|
+
* `F_GETPATH`-style resolution, which portable Node does not expose — an
|
|
37
|
+
* accepted residual under this dev-tool threat model (the main window is
|
|
38
|
+
* already trusted and there is no adversary racing the local filesystem).
|
|
39
|
+
*
|
|
40
|
+
* The same defences are mirrored synchronously in `writeFileSync` for the
|
|
41
|
+
* editor's `beforeunload` flush (`ProjectFsChannel.WriteFileSync`).
|
|
42
|
+
*/
|
|
43
|
+
import fs from 'node:fs/promises';
|
|
44
|
+
import nodeFs from 'node:fs';
|
|
45
|
+
import path from 'node:path';
|
|
46
|
+
import { z } from 'zod';
|
|
47
|
+
import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
48
|
+
import { validate } from '../utils/ipc-schema.js';
|
|
49
|
+
import { ProjectFsChannel } from '../../shared/ipc-channels.js';
|
|
50
|
+
/** Cap for `listFiles` — huge monorepos would otherwise stall the renderer. */
|
|
51
|
+
const LIST_FILE_LIMIT = 5000;
|
|
52
|
+
const SKIP_DIRS = new Set([
|
|
53
|
+
'node_modules', '.git', '.svn', '.hg', 'dist', 'build',
|
|
54
|
+
'.next', '.cache', '.turbo', '.parcel-cache', '.nuxt', '.output', '.vite',
|
|
55
|
+
]);
|
|
56
|
+
const PathArg = z.string().min(1);
|
|
57
|
+
const ReadFileSchema = z.tuple([PathArg]);
|
|
58
|
+
const WriteFileSchema = z.tuple([PathArg, z.string()]);
|
|
59
|
+
const ListFilesSchema = z.tuple([PathArg]);
|
|
60
|
+
function makeError(code, message) {
|
|
61
|
+
const err = new Error(message);
|
|
62
|
+
err.code = code;
|
|
63
|
+
return err;
|
|
64
|
+
}
|
|
65
|
+
/** `true` if `child` is `parent` itself or lives strictly under it. */
|
|
66
|
+
function isWithin(parent, child) {
|
|
67
|
+
if (child === parent)
|
|
68
|
+
return true;
|
|
69
|
+
const rel = path.relative(parent, child);
|
|
70
|
+
// `rel` empty == same dir; `..`-prefixed or absolute == outside.
|
|
71
|
+
return rel.length > 0 && !rel.startsWith('..' + path.sep) && rel !== '..' && !path.isAbsolute(rel);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Synchronous, string-level guard: reject empty root (ENOACTIVE), NUL bytes
|
|
75
|
+
* and empty paths (EINVAL), and lexical `..` escapes (EACCES) by resolving
|
|
76
|
+
* both sides with `path.resolve` and a `path.sep`-aware containment check
|
|
77
|
+
* (so `/foo/bar` ⊄ `/foo/bar2`). This is the cheap first pass; it does NOT
|
|
78
|
+
* resolve symlinks — that is done by `resolveWithinProjectRoot` below.
|
|
79
|
+
*/
|
|
80
|
+
export function enforceWithinProjectRoot(abs, root) {
|
|
81
|
+
if (root === '') {
|
|
82
|
+
throw makeError('ENOACTIVE', 'No active project — open a project before using the editor file system');
|
|
83
|
+
}
|
|
84
|
+
if (typeof abs !== 'string' || abs.length === 0) {
|
|
85
|
+
throw makeError('EINVAL', 'path must be a non-empty string');
|
|
86
|
+
}
|
|
87
|
+
if (abs.includes('\0') || root.includes('\0')) {
|
|
88
|
+
throw makeError('EINVAL', 'path must not contain NUL bytes');
|
|
89
|
+
}
|
|
90
|
+
const resolvedRoot = path.resolve(root);
|
|
91
|
+
const resolved = path.resolve(abs);
|
|
92
|
+
if (!isWithin(resolvedRoot, resolved)) {
|
|
93
|
+
throw makeError('EACCES', `Path escapes project root: ${resolved}`);
|
|
94
|
+
}
|
|
95
|
+
return resolved;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Pick the project root a write should be validated against: the CURRENT root
|
|
99
|
+
* or the LAST-CLOSED one (recorded by workspace-service in `closeProject`,
|
|
100
|
+
* reset in `openProject`). This rescues an in-flight teardown/debounced write
|
|
101
|
+
* whose target lives under the project that was just closed — `closeProject`
|
|
102
|
+
* clears `currentProjectPath` to '' BEFORE the renderer navigates away, so a
|
|
103
|
+
* write resolving its root via the live root alone would be rejected with
|
|
104
|
+
* ENOACTIVE and the developer's last edit lost.
|
|
105
|
+
*
|
|
106
|
+
* Selection is purely LEXICAL (same `path.resolve` + `path.sep`-aware
|
|
107
|
+
* containment as `enforceWithinProjectRoot`, no fs I/O): symlink/realpath
|
|
108
|
+
* hardening stays in the downstream `resolveWithinProjectRoot` /
|
|
109
|
+
* `assertWritableAncestor` defences, applied by the caller against the root
|
|
110
|
+
* this returns — so accepting the last-closed root never relaxes the sandbox.
|
|
111
|
+
*
|
|
112
|
+
* - Returns whichever root lexically contains `absPath`, preferring `current`.
|
|
113
|
+
* - An empty-string root ('') means "no such root" and is never selected.
|
|
114
|
+
* - Throws when `absPath` is inside NEITHER root: ENOACTIVE when both roots
|
|
115
|
+
* are empty (no project context at all), otherwise EACCES (the path escapes
|
|
116
|
+
* every known sandbox root).
|
|
117
|
+
*/
|
|
118
|
+
export function pickWriteRoot(absPath, roots) {
|
|
119
|
+
const resolved = path.resolve(absPath);
|
|
120
|
+
if (roots.current !== '' && isWithin(path.resolve(roots.current), resolved)) {
|
|
121
|
+
return roots.current;
|
|
122
|
+
}
|
|
123
|
+
if (roots.lastClosed !== '' && isWithin(path.resolve(roots.lastClosed), resolved)) {
|
|
124
|
+
return roots.lastClosed;
|
|
125
|
+
}
|
|
126
|
+
if (roots.current === '' && roots.lastClosed === '') {
|
|
127
|
+
throw makeError('ENOACTIVE', 'No active project — open a project before using the editor file system');
|
|
128
|
+
}
|
|
129
|
+
throw makeError('EACCES', `Path escapes project root: ${resolved}`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Symlink-aware resolution: run the cheap lexical guard, then `fs.realpath`
|
|
133
|
+
* BOTH the root and the target before re-checking containment. This closes
|
|
134
|
+
* the symlink-escape hole — a link inside the root that points outside it
|
|
135
|
+
* resolves to an out-of-root real path and is rejected with EACCES.
|
|
136
|
+
*
|
|
137
|
+
* Writing a not-yet-existing file would make `realpath(target)` throw ENOENT,
|
|
138
|
+
* so when the target is missing we realpath its PARENT directory and re-join
|
|
139
|
+
* the basename: the file's containing directory must itself be inside the
|
|
140
|
+
* (realpath'd) root.
|
|
141
|
+
*/
|
|
142
|
+
async function resolveWithinProjectRoot(abs, root) {
|
|
143
|
+
// Lexical guard first (handles ENOACTIVE / EINVAL / `..` before any fs I/O).
|
|
144
|
+
const lexical = enforceWithinProjectRoot(abs, root);
|
|
145
|
+
const realRoot = await fs.realpath(path.resolve(root));
|
|
146
|
+
let realTarget;
|
|
147
|
+
try {
|
|
148
|
+
realTarget = await fs.realpath(lexical);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
if (err.code !== 'ENOENT')
|
|
152
|
+
throw err;
|
|
153
|
+
// Target does not exist yet (e.g. first write): the parent dir must.
|
|
154
|
+
const parent = path.dirname(lexical);
|
|
155
|
+
const realParent = await fs.realpath(parent);
|
|
156
|
+
if (!isWithin(realRoot, realParent)) {
|
|
157
|
+
throw makeError('EACCES', `Path escapes project root: ${lexical}`);
|
|
158
|
+
}
|
|
159
|
+
return path.join(realParent, path.basename(lexical));
|
|
160
|
+
}
|
|
161
|
+
if (!isWithin(realRoot, realTarget)) {
|
|
162
|
+
throw makeError('EACCES', `Path escapes project root: ${realTarget}`);
|
|
163
|
+
}
|
|
164
|
+
return realTarget;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Post-open TOCTOU re-verification: re-`realpath` the already-opened path and
|
|
168
|
+
* re-check containment. `O_NOFOLLOW` guards only the FINAL component, so a
|
|
169
|
+
* MID-PATH directory component swapped to a symlink in the realpath→open window
|
|
170
|
+
* would still be followed out of the sandbox. Re-resolving here AFTER the open
|
|
171
|
+
* catches that swap — callers run it before returning/writing bytes and reject
|
|
172
|
+
* on failure. (Residual: a swap reverted before this re-resolve; see file head.)
|
|
173
|
+
*/
|
|
174
|
+
async function assertOpenedWithinRoot(safe, root) {
|
|
175
|
+
const realRoot = await fs.realpath(path.resolve(root));
|
|
176
|
+
const real = await fs.realpath(safe);
|
|
177
|
+
if (!isWithin(realRoot, real)) {
|
|
178
|
+
throw makeError('EACCES', `Path escapes project root (mid-path race): ${real}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function readFile(root, p) {
|
|
182
|
+
const safe = await resolveWithinProjectRoot(p, root);
|
|
183
|
+
// Open with O_NOFOLLOW so a final-component symlink swapped in AFTER the
|
|
184
|
+
// realpath containment check (the TOCTOU race) is not followed out of the
|
|
185
|
+
// sandbox. Operate on the FileHandle, never a second path-based open (which
|
|
186
|
+
// would re-follow the swapped symlink).
|
|
187
|
+
const fh = await fs.open(safe, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
188
|
+
try {
|
|
189
|
+
// Re-verify post-open: catches a mid-path component swapped to a symlink in
|
|
190
|
+
// the realpath→open window (which O_NOFOLLOW does not guard). Reject before
|
|
191
|
+
// returning any bytes, so an escaped read leaks nothing.
|
|
192
|
+
await assertOpenedWithinRoot(safe, root);
|
|
193
|
+
return await fh.readFile('utf8');
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
await fh.close();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Symlink-aware ancestor guard, run BEFORE any `mkdir`. Walks up from the
|
|
201
|
+
* write target to the DEEPEST already-existing ancestor directory, then
|
|
202
|
+
* `fs.realpath`s it and checks it is still inside the (realpath'd) root.
|
|
203
|
+
*
|
|
204
|
+
* This closes the `mkdir -p` write side-channel: with `proj/escape ->
|
|
205
|
+
* /outside`, the lexical guard alone would let `mkdir -p proj/escape/new`
|
|
206
|
+
* follow the symlink and create `/outside/new` (an out-of-sandbox side-effect)
|
|
207
|
+
* before the post-mkdir realpath check could reject it. By realpath'ing the
|
|
208
|
+
* deepest existing ancestor first, we reject such writes with zero mkdir.
|
|
209
|
+
*
|
|
210
|
+
* Legitimate deep writes (`proj/a/b/c.txt` where `a`/`b` don't exist yet)
|
|
211
|
+
* walk up to the root itself — `realpath(root)` is inside the root, so they
|
|
212
|
+
* pass and `mkdir -p` is allowed to create the in-root intermediate dirs.
|
|
213
|
+
*/
|
|
214
|
+
async function assertWritableAncestor(lexical, root) {
|
|
215
|
+
const realRoot = await fs.realpath(path.resolve(root));
|
|
216
|
+
// Climb to the deepest ancestor that actually exists on disk.
|
|
217
|
+
let ancestor = path.dirname(lexical);
|
|
218
|
+
// Guard against an unbounded loop: `path.dirname` is a fixed point at the
|
|
219
|
+
// filesystem root ('/' -> '/'), so stop once it stops changing.
|
|
220
|
+
for (;;) {
|
|
221
|
+
let realAncestor;
|
|
222
|
+
try {
|
|
223
|
+
realAncestor = await fs.realpath(ancestor);
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
if (err.code !== 'ENOENT')
|
|
227
|
+
throw err;
|
|
228
|
+
const next = path.dirname(ancestor);
|
|
229
|
+
if (next === ancestor) {
|
|
230
|
+
// Reached the filesystem root without finding an existing ancestor.
|
|
231
|
+
throw makeError('EACCES', `Path escapes project root: ${lexical}`);
|
|
232
|
+
}
|
|
233
|
+
ancestor = next;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
// Deepest existing ancestor found — it must resolve inside the root.
|
|
237
|
+
if (!isWithin(realRoot, realAncestor)) {
|
|
238
|
+
throw makeError('EACCES', `Path escapes project root: ${lexical}`);
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function writeFile(root, p, content) {
|
|
244
|
+
// First lexical guard so a `..` escape is rejected before any fs I/O.
|
|
245
|
+
const lexical = enforceWithinProjectRoot(p, root);
|
|
246
|
+
// Symlink-aware ancestor check BEFORE any mkdir, so a symlinked-out parent
|
|
247
|
+
// (`proj/escape -> /outside`) cannot leak a `mkdir -p` side-effect outside
|
|
248
|
+
// the sandbox. Rejected writes create nothing, in or out of the root.
|
|
249
|
+
await assertWritableAncestor(lexical, root);
|
|
250
|
+
await fs.mkdir(path.dirname(lexical), { recursive: true });
|
|
251
|
+
// Re-resolve through realpath now that the parent dir exists; final check
|
|
252
|
+
// for any boundary case introduced while creating the parent dirs.
|
|
253
|
+
const safe = await resolveWithinProjectRoot(p, root);
|
|
254
|
+
// Open with O_NOFOLLOW (final-component symlink guard) and O_CREAT but NOT
|
|
255
|
+
// O_TRUNC: truncating at open would destroy an out-of-sandbox file's contents
|
|
256
|
+
// before any mid-path re-check could run. We re-verify containment first, and
|
|
257
|
+
// only then truncate+write — so a detected mid-path race never mutates the
|
|
258
|
+
// user's content out of the sandbox.
|
|
259
|
+
const fh = await fs.open(safe, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_NOFOLLOW, 0o666);
|
|
260
|
+
try {
|
|
261
|
+
// Re-verify post-open: catches a mid-path component swapped to a symlink in
|
|
262
|
+
// the realpath→open window (which O_NOFOLLOW does not guard). Reject BEFORE
|
|
263
|
+
// writing, so an escaped write lands none of the user's content.
|
|
264
|
+
await assertOpenedWithinRoot(safe, root);
|
|
265
|
+
const buf = Buffer.from(content, 'utf8');
|
|
266
|
+
// Write the whole buffer at absolute offset 0, looping over any short write
|
|
267
|
+
// (a single `write` may persist fewer than requested bytes), then truncate
|
|
268
|
+
// to the exact length — the O_TRUNC-free replacement of prior contents.
|
|
269
|
+
let off = 0;
|
|
270
|
+
while (off < buf.length) {
|
|
271
|
+
const { bytesWritten } = await fh.write(buf, off, buf.length - off, off);
|
|
272
|
+
if (bytesWritten <= 0)
|
|
273
|
+
break;
|
|
274
|
+
off += bytesWritten;
|
|
275
|
+
}
|
|
276
|
+
await fh.truncate(buf.length);
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
await fh.close();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// ── Synchronous mirror (editor beforeunload flush) ──────────────────────────
|
|
283
|
+
// The sync write path exists so the editor can flush a pending edit inside
|
|
284
|
+
// `beforeunload` and BLOCK until it lands (an async write loses the race with a
|
|
285
|
+
// hard window/app close). These functions mirror the async guards above EXACTLY
|
|
286
|
+
// and MUST stay in lockstep — any divergence reopens a sandbox escape on the
|
|
287
|
+
// sync path. They reuse the pure lexical helpers (`enforceWithinProjectRoot`,
|
|
288
|
+
// `isWithin`, `pickWriteRoot`, `makeError`) so the boundary is single-sourced;
|
|
289
|
+
// only sync-vs-async fs calls differ. All `node:fs` access goes through the
|
|
290
|
+
// default `nodeFs` object so the same realpath/open surface is exercised.
|
|
291
|
+
/** Synchronous twin of {@link resolveWithinProjectRoot}. */
|
|
292
|
+
function resolveWithinProjectRootSync(abs, root) {
|
|
293
|
+
const lexical = enforceWithinProjectRoot(abs, root);
|
|
294
|
+
const realRoot = nodeFs.realpathSync(path.resolve(root));
|
|
295
|
+
let realTarget;
|
|
296
|
+
try {
|
|
297
|
+
realTarget = nodeFs.realpathSync(lexical);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
if (err.code !== 'ENOENT')
|
|
301
|
+
throw err;
|
|
302
|
+
const parent = path.dirname(lexical);
|
|
303
|
+
const realParent = nodeFs.realpathSync(parent);
|
|
304
|
+
if (!isWithin(realRoot, realParent)) {
|
|
305
|
+
throw makeError('EACCES', `Path escapes project root: ${lexical}`);
|
|
306
|
+
}
|
|
307
|
+
return path.join(realParent, path.basename(lexical));
|
|
308
|
+
}
|
|
309
|
+
if (!isWithin(realRoot, realTarget)) {
|
|
310
|
+
throw makeError('EACCES', `Path escapes project root: ${realTarget}`);
|
|
311
|
+
}
|
|
312
|
+
return realTarget;
|
|
313
|
+
}
|
|
314
|
+
/** Synchronous twin of {@link assertWritableAncestor}. */
|
|
315
|
+
function assertWritableAncestorSync(lexical, root) {
|
|
316
|
+
const realRoot = nodeFs.realpathSync(path.resolve(root));
|
|
317
|
+
let ancestor = path.dirname(lexical);
|
|
318
|
+
for (;;) {
|
|
319
|
+
let realAncestor;
|
|
320
|
+
try {
|
|
321
|
+
realAncestor = nodeFs.realpathSync(ancestor);
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
if (err.code !== 'ENOENT')
|
|
325
|
+
throw err;
|
|
326
|
+
const next = path.dirname(ancestor);
|
|
327
|
+
if (next === ancestor) {
|
|
328
|
+
throw makeError('EACCES', `Path escapes project root: ${lexical}`);
|
|
329
|
+
}
|
|
330
|
+
ancestor = next;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (!isWithin(realRoot, realAncestor)) {
|
|
334
|
+
throw makeError('EACCES', `Path escapes project root: ${lexical}`);
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** Synchronous twin of {@link assertOpenedWithinRoot}. */
|
|
340
|
+
function assertOpenedWithinRootSync(safe, root) {
|
|
341
|
+
const realRoot = nodeFs.realpathSync(path.resolve(root));
|
|
342
|
+
const real = nodeFs.realpathSync(safe);
|
|
343
|
+
if (!isWithin(realRoot, real)) {
|
|
344
|
+
throw makeError('EACCES', `Path escapes project root (mid-path race): ${real}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/** Synchronous twin of {@link writeFile} — identical sandbox, blocking I/O. */
|
|
348
|
+
function writeFileSync(root, p, content) {
|
|
349
|
+
const lexical = enforceWithinProjectRoot(p, root);
|
|
350
|
+
assertWritableAncestorSync(lexical, root);
|
|
351
|
+
nodeFs.mkdirSync(path.dirname(lexical), { recursive: true });
|
|
352
|
+
const safe = resolveWithinProjectRootSync(p, root);
|
|
353
|
+
const fd = nodeFs.openSync(safe, nodeFs.constants.O_WRONLY | nodeFs.constants.O_CREAT | nodeFs.constants.O_NOFOLLOW, 0o666);
|
|
354
|
+
try {
|
|
355
|
+
assertOpenedWithinRootSync(safe, root);
|
|
356
|
+
const buf = Buffer.from(content, 'utf8');
|
|
357
|
+
// Loop over short writes (see the async twin), then truncate to length.
|
|
358
|
+
let off = 0;
|
|
359
|
+
while (off < buf.length) {
|
|
360
|
+
const bytesWritten = nodeFs.writeSync(fd, buf, off, buf.length - off, off);
|
|
361
|
+
if (bytesWritten <= 0)
|
|
362
|
+
break;
|
|
363
|
+
off += bytesWritten;
|
|
364
|
+
}
|
|
365
|
+
nodeFs.ftruncateSync(fd, buf.length);
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
nodeFs.closeSync(fd);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async function listFiles(root, rootArg) {
|
|
372
|
+
const safeRoot = await resolveWithinProjectRoot(rootArg, root);
|
|
373
|
+
const out = [];
|
|
374
|
+
const queue = [safeRoot];
|
|
375
|
+
while (queue.length > 0) {
|
|
376
|
+
if (out.length >= LIST_FILE_LIMIT)
|
|
377
|
+
break;
|
|
378
|
+
const dir = queue.shift();
|
|
379
|
+
let entries;
|
|
380
|
+
try {
|
|
381
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
const code = err.code;
|
|
385
|
+
if (code === 'EACCES' || code === 'ENOENT' || code === 'ENOTDIR')
|
|
386
|
+
continue;
|
|
387
|
+
throw err;
|
|
388
|
+
}
|
|
389
|
+
for (const e of entries) {
|
|
390
|
+
if (out.length >= LIST_FILE_LIMIT)
|
|
391
|
+
break;
|
|
392
|
+
const full = path.join(dir, e.name);
|
|
393
|
+
if (e.isDirectory()) {
|
|
394
|
+
if (SKIP_DIRS.has(e.name))
|
|
395
|
+
continue;
|
|
396
|
+
queue.push(full);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (!e.isFile())
|
|
400
|
+
continue;
|
|
401
|
+
out.push(path.relative(safeRoot, full).split(path.sep).join('/'));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Register the project-fs IPC channels. Call once during workbench setup;
|
|
408
|
+
* the returned Disposable removes every channel (park on `ctx.registry`).
|
|
409
|
+
*/
|
|
410
|
+
export function registerProjectFsIpc(ctx) {
|
|
411
|
+
const getRoot = () => ctx.workspace.getProjectPath();
|
|
412
|
+
const getLastClosedRoot = () => ctx.workspace.getLastClosedProjectPath();
|
|
413
|
+
return new IpcRegistry(ctx.senderPolicy)
|
|
414
|
+
.handle(ProjectFsChannel.GetRoot, () => getRoot())
|
|
415
|
+
.handle(ProjectFsChannel.ReadFile, (_event, ...args) => {
|
|
416
|
+
const [p] = validate(ProjectFsChannel.ReadFile, ReadFileSchema, args);
|
|
417
|
+
// Reads stay scoped to the CURRENT root only — never widen the read
|
|
418
|
+
// surface to the last-closed project.
|
|
419
|
+
return readFile(getRoot(), p);
|
|
420
|
+
})
|
|
421
|
+
.handle(ProjectFsChannel.WriteFile, (_event, ...args) => {
|
|
422
|
+
const [p, content] = validate(ProjectFsChannel.WriteFile, WriteFileSchema, args);
|
|
423
|
+
// Accept a write under the current OR the just-closed project root (the
|
|
424
|
+
// latter rescues an in-flight teardown flush after `closeProject`). The
|
|
425
|
+
// selected root then runs the FULL sandbox (realpath containment +
|
|
426
|
+
// ancestor guard + O_NOFOLLOW) inside `writeFile`, so the boundary is
|
|
427
|
+
// never relaxed — only WHICH root is considered.
|
|
428
|
+
const root = pickWriteRoot(p, { current: getRoot(), lastClosed: getLastClosedRoot() });
|
|
429
|
+
return writeFile(root, p, content);
|
|
430
|
+
})
|
|
431
|
+
.handleSync(ProjectFsChannel.WriteFileSync, (_event, ...args) => {
|
|
432
|
+
// Synchronous twin of WriteFile for the editor's beforeunload flush. Same
|
|
433
|
+
// root selection + same sandbox (`writeFileSync` mirrors `writeFile`); the
|
|
434
|
+
// only difference is the blocking transport. Returns `{ ok: true }`; any
|
|
435
|
+
// throw is converted to `{ ok: false, code, message }` by `handleSync`.
|
|
436
|
+
const [p, content] = validate(ProjectFsChannel.WriteFileSync, WriteFileSchema, args);
|
|
437
|
+
const root = pickWriteRoot(p, { current: getRoot(), lastClosed: getLastClosedRoot() });
|
|
438
|
+
writeFileSync(root, p, content);
|
|
439
|
+
return { ok: true };
|
|
440
|
+
})
|
|
441
|
+
.handle(ProjectFsChannel.ListFiles, (_event, ...args) => {
|
|
442
|
+
const [p] = validate(ProjectFsChannel.ListFiles, ListFilesSchema, args);
|
|
443
|
+
return listFiles(getRoot(), p);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
/** Test seams — pure handlers, no Electron needed. */
|
|
447
|
+
export const __testing = { readFile, writeFile, writeFileSync, listFiles, pickWriteRoot, SKIP_DIRS, LIST_FILE_LIMIT };
|
|
448
|
+
//# sourceMappingURL=project-fs.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WorkbenchContext } from '../services/workbench-context.js';
|
|
2
2
|
import type { WorkbenchModule } from '../services/module.js';
|
|
3
|
-
import type { Disposable } from '
|
|
3
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
4
4
|
type ProjectsIpcCtx = Pick<WorkbenchContext, 'workspace' | 'windows' | 'senderPolicy' | 'projectsProvider' | 'projectTemplates' | 'customCreateProjectDialog'>;
|
|
5
5
|
export declare function registerProjectsIpc(ctx: ProjectsIpcCtx): Disposable;
|
|
6
6
|
export declare const projectsModule: WorkbenchModule;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WorkbenchContext } from '../services/workbench-context.js';
|
|
2
2
|
import type { WorkbenchModule } from '../services/module.js';
|
|
3
|
-
import type { Disposable } from '
|
|
3
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
4
4
|
export declare function registerSessionIpc(ctx: Pick<WorkbenchContext, 'workspace' | 'senderPolicy'>): Disposable;
|
|
5
5
|
export declare const sessionModule: WorkbenchModule;
|
|
6
6
|
//# sourceMappingURL=session.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WorkbenchContext } from '../services/workbench-context.js';
|
|
2
|
-
import type { Disposable } from '
|
|
2
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
3
3
|
import type { WorkbenchModule } from '../services/module.js';
|
|
4
4
|
export declare function registerSettingsIpc(ctx: Pick<WorkbenchContext, 'views' | 'notify' | 'workspace' | 'rendererDir' | 'senderPolicy' | 'windows'>): Disposable;
|
|
5
5
|
export declare const settingsModule: WorkbenchModule;
|
|
@@ -3,6 +3,12 @@ import type { WorkbenchModule } from '../services/module.js';
|
|
|
3
3
|
* The 'simulator' built-in module fans out into three IPC registrars
|
|
4
4
|
* (core simulator, panels, toolbar). Bundle them under a single module
|
|
5
5
|
* so app.ts only sees one entry per BuiltinModuleId.
|
|
6
|
+
*
|
|
7
|
+
* Bridge router (native-host PAGE_OPEN / NAV_ACTION / TAB_ACTION etc.) hooks
|
|
8
|
+
* up unconditionally — the `__diminaNativeHost.enabled` flag in the simulator
|
|
9
|
+
* window decides whether to actually call dmb:* channels, but the main-side
|
|
10
|
+
* handlers must always be ready or `ipcRenderer.invoke('dmb:spawn')` fails
|
|
11
|
+
* with `No handler registered`.
|
|
6
12
|
*/
|
|
7
13
|
export declare const simulatorModule: WorkbenchModule;
|
|
8
14
|
//# sourceMappingURL=simulator-module.d.ts.map
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import { DisposableRegistry } from '
|
|
1
|
+
import { DisposableRegistry } from '@dimina-kit/electron-deck/main';
|
|
2
2
|
import { registerSimulatorIpc } from './simulator.js';
|
|
3
3
|
import { registerPanelsIpc } from './panels.js';
|
|
4
4
|
import { registerToolbarIpc } from './toolbar.js';
|
|
5
|
+
import { installBridgeRouter } from './bridge-router.js';
|
|
6
|
+
import { registerViewsIpc } from './views.js';
|
|
5
7
|
/**
|
|
6
8
|
* The 'simulator' built-in module fans out into three IPC registrars
|
|
7
9
|
* (core simulator, panels, toolbar). Bundle them under a single module
|
|
8
10
|
* so app.ts only sees one entry per BuiltinModuleId.
|
|
11
|
+
*
|
|
12
|
+
* Bridge router (native-host PAGE_OPEN / NAV_ACTION / TAB_ACTION etc.) hooks
|
|
13
|
+
* up unconditionally — the `__diminaNativeHost.enabled` flag in the simulator
|
|
14
|
+
* window decides whether to actually call dmb:* channels, but the main-side
|
|
15
|
+
* handlers must always be ready or `ipcRenderer.invoke('dmb:spawn')` fails
|
|
16
|
+
* with `No handler registered`.
|
|
9
17
|
*/
|
|
10
18
|
export const simulatorModule = {
|
|
11
19
|
setup: (ctx) => {
|
|
@@ -13,6 +21,8 @@ export const simulatorModule = {
|
|
|
13
21
|
reg.add(registerSimulatorIpc(ctx));
|
|
14
22
|
reg.add(registerPanelsIpc(ctx));
|
|
15
23
|
reg.add(registerToolbarIpc(ctx));
|
|
24
|
+
reg.add(registerViewsIpc(ctx));
|
|
25
|
+
installBridgeRouter(ctx);
|
|
16
26
|
return reg;
|
|
17
27
|
},
|
|
18
28
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { WorkbenchContext } from '../services/workbench-context.js';
|
|
2
|
-
import type { Disposable } from '
|
|
3
|
-
export declare function registerSimulatorIpc(ctx: Pick<WorkbenchContext, 'views' | 'notify' | 'senderPolicy' | 'simulatorApis'>): Disposable;
|
|
2
|
+
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
3
|
+
export declare function registerSimulatorIpc(ctx: Pick<WorkbenchContext, 'views' | 'notify' | 'senderPolicy' | 'simulatorApis' | 'bridge'>): Disposable;
|
|
4
4
|
//# sourceMappingURL=simulator.d.ts.map
|