@dimina-kit/devtools 0.3.2-dev.20260524104239 → 0.3.2-dev.20260601115343

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.
Files changed (174) hide show
  1. package/README.md +20 -7
  2. package/dist/main/api.d.ts +1 -1
  3. package/dist/main/app/app.js +30 -2
  4. package/dist/main/app/bootstrap.d.ts +9 -0
  5. package/dist/main/app/bootstrap.js +28 -1
  6. package/dist/main/index.bundle.js +1468 -496
  7. package/dist/main/ipc/project-fs.d.ts +57 -0
  8. package/dist/main/ipc/project-fs.js +448 -0
  9. package/dist/main/ipc/simulator-module.js +2 -0
  10. package/dist/main/ipc/views.d.ts +19 -0
  11. package/dist/main/ipc/views.js +27 -0
  12. package/dist/main/services/layout/index.d.ts +1 -1
  13. package/dist/main/services/layout/index.js +3 -3
  14. package/dist/main/services/simulator-temp-files/disk.d.ts +55 -0
  15. package/dist/main/services/simulator-temp-files/disk.js +215 -0
  16. package/dist/main/services/simulator-temp-files/fs-channels.d.ts +69 -0
  17. package/dist/main/services/simulator-temp-files/fs-channels.js +108 -0
  18. package/dist/main/services/simulator-temp-files/index.d.ts +33 -0
  19. package/dist/main/services/simulator-temp-files/index.js +160 -0
  20. package/dist/main/services/simulator-temp-files/request-handler.d.ts +30 -0
  21. package/dist/main/services/simulator-temp-files/request-handler.js +178 -0
  22. package/dist/main/services/simulator-temp-files/resolver.d.ts +22 -0
  23. package/dist/main/services/simulator-temp-files/resolver.js +22 -0
  24. package/dist/main/services/simulator-temp-files/store.d.ts +14 -0
  25. package/dist/main/services/simulator-temp-files/store.js +24 -0
  26. package/dist/main/services/toolbar/toolbar-store.d.ts +3 -3
  27. package/dist/main/services/toolbar/toolbar-store.js +18 -1
  28. package/dist/main/services/update/update-manager.d.ts +10 -0
  29. package/dist/main/services/update/update-manager.js +16 -3
  30. package/dist/main/services/views/view-manager.d.ts +17 -4
  31. package/dist/main/services/views/view-manager.js +71 -14
  32. package/dist/main/services/workbench-context.d.ts +6 -1
  33. package/dist/main/services/workspace/workspace-service.d.ts +8 -0
  34. package/dist/main/services/workspace/workspace-service.js +22 -4
  35. package/dist/main/utils/ipc-registry.d.ts +10 -1
  36. package/dist/main/utils/ipc-registry.js +43 -0
  37. package/dist/preload/runtime/temp-files.d.ts +8 -0
  38. package/dist/preload/runtime/temp-files.js +34 -0
  39. package/dist/preload/shared/expose.js +2 -2
  40. package/dist/preload/windows/main.cjs +4 -0
  41. package/dist/preload/windows/main.cjs.map +2 -2
  42. package/dist/preload/windows/simulator.js +40 -8
  43. package/dist/renderer/assets/abap-DiwvWnMr.js +2 -0
  44. package/dist/renderer/assets/apex-CmtZjKlf.js +2 -0
  45. package/dist/renderer/assets/azcli-DL2My_i-.js +2 -0
  46. package/dist/renderer/assets/bat-B-nC98wG.js +2 -0
  47. package/dist/renderer/assets/bicep-Ju5MwOgh.js +3 -0
  48. package/dist/renderer/assets/cameligo-8Eu1TyBr.js +2 -0
  49. package/dist/renderer/assets/chunk-DPI0Y_ll.js +1 -0
  50. package/dist/renderer/assets/clojure-u-RpMkH3.js +2 -0
  51. package/dist/renderer/assets/codicon-ngg6Pgfi.ttf +0 -0
  52. package/dist/renderer/assets/coffee-CdA7bbTe.js +2 -0
  53. package/dist/renderer/assets/constants-CLAQX6VZ.js +2 -0
  54. package/dist/renderer/assets/cpp-CzNFP8ks.js +2 -0
  55. package/dist/renderer/assets/csharp-j1LThmcE.js +2 -0
  56. package/dist/renderer/assets/csp-CLRC61y6.js +2 -0
  57. package/dist/renderer/assets/css-r6rC_7P2.js +4 -0
  58. package/dist/renderer/assets/css.worker-DyDu5ynT.js +90 -0
  59. package/dist/renderer/assets/cssMode-BCBgxrnA.js +2 -0
  60. package/dist/renderer/assets/cypher-CW08XVUh.js +2 -0
  61. package/dist/renderer/assets/dart-Cs9aL5T_.js +2 -0
  62. package/dist/renderer/assets/dockerfile-BWM0M184.js +2 -0
  63. package/dist/renderer/assets/ecl-MJJuer5P.js +2 -0
  64. package/dist/renderer/assets/editor-CWI9TnMM.css +1 -0
  65. package/dist/renderer/assets/editor.api2-BTaYmRq7.js +873 -0
  66. package/dist/renderer/assets/editor.worker-Dpt_xPrs.js +27 -0
  67. package/dist/renderer/assets/elixir-D2AIuXqn.js +2 -0
  68. package/dist/renderer/assets/flow9-B2H24giC.js +2 -0
  69. package/dist/renderer/assets/freemarker2-DGDGzimW.js +4 -0
  70. package/dist/renderer/assets/fsharp-CMk2OIJN.js +2 -0
  71. package/dist/renderer/assets/go-BrMkuJg0.js +2 -0
  72. package/dist/renderer/assets/graphql-PSR1UKGv.js +2 -0
  73. package/dist/renderer/assets/handlebars-bOHhw3EG.js +2 -0
  74. package/dist/renderer/assets/hcl-DAQrbDOW.js +2 -0
  75. package/dist/renderer/assets/html-BMyuX0_c.js +2 -0
  76. package/dist/renderer/assets/html.worker-IWNXhnIC.js +503 -0
  77. package/dist/renderer/assets/htmlMode-DR4kXJ7l.js +2 -0
  78. package/dist/renderer/assets/index-BDMxXUfn.js +50 -0
  79. package/dist/renderer/assets/ini-0TG5BxW0.js +2 -0
  80. package/dist/renderer/assets/input-CCYS8Ave.js +2 -0
  81. package/dist/renderer/assets/{ipc-transport-BzPzxPwf.js → ipc-transport-Bn4KXvhu.js} +5 -5
  82. package/dist/renderer/assets/ipc-transport-Cu7tubyg.css +1 -0
  83. package/dist/renderer/assets/java-rgorz17v.js +2 -0
  84. package/dist/renderer/assets/javascript-Bu-tl1P7.js +2 -0
  85. package/dist/renderer/assets/json.worker-c4h5s80L.js +59 -0
  86. package/dist/renderer/assets/jsonMode-MQV_ffLR.js +8 -0
  87. package/dist/renderer/assets/julia-C8VMdHm8.js +2 -0
  88. package/dist/renderer/assets/kotlin-AhhC4Qhm.js +2 -0
  89. package/dist/renderer/assets/less-Cgca25AP.js +3 -0
  90. package/dist/renderer/assets/lexon-D0GHdBaw.js +2 -0
  91. package/dist/renderer/assets/liquid-ClkfOvhB.js +2 -0
  92. package/dist/renderer/assets/lspLanguageFeatures-Bke_4Ie9.js +5 -0
  93. package/dist/renderer/assets/lua-DmRsNG-P.js +2 -0
  94. package/dist/renderer/assets/m3-BgL5dNKT.js +2 -0
  95. package/dist/renderer/assets/markdown-BuJfycGS.js +2 -0
  96. package/dist/renderer/assets/mdx-RZdUskPu.js +2 -0
  97. package/dist/renderer/assets/mips-C9m_93PR.js +2 -0
  98. package/dist/renderer/assets/monaco.contribution-B7rUMaEv.js +3 -0
  99. package/dist/renderer/assets/msdax-CpFHC9OI.js +2 -0
  100. package/dist/renderer/assets/mysql-qFvltsqN.js +2 -0
  101. package/dist/renderer/assets/objective-c-Bnmr858J.js +2 -0
  102. package/dist/renderer/assets/pascal-WP0_D5AO.js +2 -0
  103. package/dist/renderer/assets/pascaligo-Blom4Rij.js +2 -0
  104. package/dist/renderer/assets/perl-B-vk8g64.js +2 -0
  105. package/dist/renderer/assets/pgsql-Cgvz6v67.js +2 -0
  106. package/dist/renderer/assets/php-8a3Lrw9m.js +2 -0
  107. package/dist/renderer/assets/pla-DuFqEZ8V.js +2 -0
  108. package/dist/renderer/assets/popover-CH4rwpdt.js +2 -0
  109. package/dist/renderer/assets/postiats-DkLtSgkp.js +2 -0
  110. package/dist/renderer/assets/powerquery-BJ1aNepW.js +2 -0
  111. package/dist/renderer/assets/powershell-rE98k687.js +2 -0
  112. package/dist/renderer/assets/protobuf-CUheFacr.js +3 -0
  113. package/dist/renderer/assets/pug-LDcAMD8w.js +2 -0
  114. package/dist/renderer/assets/python-Cobrm_-8.js +2 -0
  115. package/dist/renderer/assets/qsharp-DUKSQoR1.js +2 -0
  116. package/dist/renderer/assets/r-D-QApv87.js +2 -0
  117. package/dist/renderer/assets/razor-BEUIQFNJ.js +2 -0
  118. package/dist/renderer/assets/redis-SXdDyWR9.js +2 -0
  119. package/dist/renderer/assets/redshift-Y6lsCryn.js +2 -0
  120. package/dist/renderer/assets/restructuredtext-edObr9a8.js +2 -0
  121. package/dist/renderer/assets/ruby-CNnUfF-8.js +2 -0
  122. package/dist/renderer/assets/rust-IHUZWzBr.js +2 -0
  123. package/dist/renderer/assets/sb-DrUvY44N.js +2 -0
  124. package/dist/renderer/assets/scala-B4hbXGLM.js +2 -0
  125. package/dist/renderer/assets/scheme-BGrd12j3.js +2 -0
  126. package/dist/renderer/assets/scss-x5G1ES4U.js +4 -0
  127. package/dist/renderer/assets/select-Ct4-sijm.js +2 -0
  128. package/dist/renderer/assets/settings-CMTB-t0x.js +2 -0
  129. package/dist/renderer/assets/{settings-api-BXCw9vOZ.js → settings-api-Csdngf9Z.js} +2 -2
  130. package/dist/renderer/assets/shell-DOehe2Y8.js +2 -0
  131. package/dist/renderer/assets/solidity-BeRvcwWV.js +2 -0
  132. package/dist/renderer/assets/sophia-DZbkUNjy.js +2 -0
  133. package/dist/renderer/assets/sparql-B7_oi5-h.js +2 -0
  134. package/dist/renderer/assets/sql-CTlsFWVE.js +2 -0
  135. package/dist/renderer/assets/st-DJVEJdPE.js +2 -0
  136. package/dist/renderer/assets/swift-CwhT3fYa.js +2 -0
  137. package/dist/renderer/assets/systemverilog-BQN63pkN.js +2 -0
  138. package/dist/renderer/assets/tcl-DqwfpskA.js +2 -0
  139. package/dist/renderer/assets/ts.worker-BwM5Ha3L.js +67720 -0
  140. package/dist/renderer/assets/tsMode-BHa7dl9v.js +12 -0
  141. package/dist/renderer/assets/twig-BiyenUgc.js +2 -0
  142. package/dist/renderer/assets/typescript-15aT0Yj8.js +2 -0
  143. package/dist/renderer/assets/typespec-CWOJribt.js +2 -0
  144. package/dist/renderer/assets/vb-Cq5F87m3.js +2 -0
  145. package/dist/renderer/assets/wgsl-BAvW2lVr.js +299 -0
  146. package/dist/renderer/assets/workbenchSettings-CODFCzOg.js +8 -0
  147. package/dist/renderer/assets/workers-CRBS0W-v.js +2 -0
  148. package/dist/renderer/assets/xml-bR2Zh-HV.js +2 -0
  149. package/dist/renderer/assets/yaml-Bdk54Qrb.js +2 -0
  150. package/dist/renderer/entries/main/index.html +29 -6
  151. package/dist/renderer/entries/popover/index.html +7 -6
  152. package/dist/renderer/entries/settings/index.html +6 -5
  153. package/dist/renderer/entries/workbench-settings/index.html +6 -5
  154. package/dist/shared/constants.d.ts +0 -4
  155. package/dist/shared/constants.js +0 -4
  156. package/dist/shared/ipc-channels.d.ts +24 -0
  157. package/dist/shared/ipc-channels.js +37 -0
  158. package/dist/shared/ipc-schemas.d.ts +6 -0
  159. package/dist/shared/ipc-schemas.js +15 -0
  160. package/dist/shared/types.d.ts +37 -0
  161. package/dist/shared/vpath.d.ts +42 -0
  162. package/dist/shared/vpath.js +105 -0
  163. package/dist/simulator/assets/simulator-B4e3c6_O.js +9 -0
  164. package/dist/simulator/simulator.html +1 -1
  165. package/package.json +5 -4
  166. package/dist/renderer/assets/constants-DI382mTP.js +0 -2
  167. package/dist/renderer/assets/index-4lkyF644.js +0 -46
  168. package/dist/renderer/assets/input-D4byFOLS.js +0 -2
  169. package/dist/renderer/assets/ipc-transport-DqbRdZNO.css +0 -1
  170. package/dist/renderer/assets/popover-cxKrg_os.js +0 -2
  171. package/dist/renderer/assets/select-BgSkkdHW.js +0 -2
  172. package/dist/renderer/assets/settings-oD2K1jdT.js +0 -2
  173. package/dist/renderer/assets/workbenchSettings-CTNBpTft.js +0 -8
  174. package/dist/simulator/assets/simulator-DDRBJ6Wa.js +0 -9
@@ -0,0 +1,57 @@
1
+ import type { WorkbenchContext } from '../services/workbench-context.js';
2
+ import type { Disposable } from '../utils/disposable.js';
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
@@ -2,6 +2,7 @@ import { DisposableRegistry } from '../utils/disposable.js';
2
2
  import { registerSimulatorIpc } from './simulator.js';
3
3
  import { registerPanelsIpc } from './panels.js';
4
4
  import { registerToolbarIpc } from './toolbar.js';
5
+ import { registerViewsIpc } from './views.js';
5
6
  /**
6
7
  * The 'simulator' built-in module fans out into three IPC registrars
7
8
  * (core simulator, panels, toolbar). Bundle them under a single module
@@ -13,6 +14,7 @@ export const simulatorModule = {
13
14
  reg.add(registerSimulatorIpc(ctx));
14
15
  reg.add(registerPanelsIpc(ctx));
15
16
  reg.add(registerToolbarIpc(ctx));
17
+ reg.add(registerViewsIpc(ctx));
16
18
  return reg;
17
19
  },
18
20
  };
@@ -0,0 +1,19 @@
1
+ import type { WorkbenchContext } from '../services/workbench-context.js';
2
+ import type { Disposable } from '../utils/disposable.js';
3
+ /**
4
+ * Renderer-driven overlay bounds.
5
+ *
6
+ * The main window's React layout is the source of truth for where the
7
+ * editor view and the simulator's Chromium DevTools overlay live on
8
+ * screen. A `ResizeObserver` in the renderer watches each placeholder
9
+ * `<div>` and publishes its client rect (in CSS pixels, relative to the
10
+ * window's content area) through these channels. The view manager caches
11
+ * the latest payload per kind and applies it to the WebContentsView.
12
+ *
13
+ * Width / height = 0 is the canonical "overlay hidden" signal — the React
14
+ * panel is collapsed or the tab is not selected. The view manager removes
15
+ * the child view from the contentView but keeps the WebContents alive so
16
+ * subsequent re-shows skip the OpenSumi DI bootstrap.
17
+ */
18
+ export declare function registerViewsIpc(ctx: Pick<WorkbenchContext, 'views' | 'senderPolicy'>): Disposable;
19
+ //# sourceMappingURL=views.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { ViewChannel } from '../../shared/ipc-channels.js';
2
+ import { ViewBoundsSchema } from '../../shared/ipc-schemas.js';
3
+ import { validate } from '../utils/ipc-schema.js';
4
+ import { IpcRegistry } from '../utils/ipc-registry.js';
5
+ /**
6
+ * Renderer-driven overlay bounds.
7
+ *
8
+ * The main window's React layout is the source of truth for where the
9
+ * editor view and the simulator's Chromium DevTools overlay live on
10
+ * screen. A `ResizeObserver` in the renderer watches each placeholder
11
+ * `<div>` and publishes its client rect (in CSS pixels, relative to the
12
+ * window's content area) through these channels. The view manager caches
13
+ * the latest payload per kind and applies it to the WebContentsView.
14
+ *
15
+ * Width / height = 0 is the canonical "overlay hidden" signal — the React
16
+ * panel is collapsed or the tab is not selected. The view manager removes
17
+ * the child view from the contentView but keeps the WebContents alive so
18
+ * subsequent re-shows skip the OpenSumi DI bootstrap.
19
+ */
20
+ export function registerViewsIpc(ctx) {
21
+ return new IpcRegistry(ctx.senderPolicy)
22
+ .handle(ViewChannel.SimulatorDevtoolsBounds, (_event, ...args) => {
23
+ const [bounds] = validate(ViewChannel.SimulatorDevtoolsBounds, ViewBoundsSchema, args);
24
+ ctx.views.setSimulatorDevtoolsBounds(bounds);
25
+ });
26
+ }
27
+ //# sourceMappingURL=views.js.map
@@ -1,7 +1,7 @@
1
1
  /** Layout constants (px) */
2
2
  export declare const SPLITTER_W = 4;
3
3
  /**
4
- * X coordinate where the right panel starts (simulator width + splitter).
4
+ * X coordinate where the legacy right-side overlay starts.
5
5
  */
6
6
  export declare function getRightX(simWidth: number): number;
7
7
  export interface Bounds {
@@ -1,7 +1,7 @@
1
1
  /** Layout constants (px) */
2
2
  export const SPLITTER_W = 4;
3
3
  /**
4
- * X coordinate where the right panel starts (simulator width + splitter).
4
+ * X coordinate where the legacy right-side overlay starts.
5
5
  */
6
6
  export function getRightX(simWidth) {
7
7
  return simWidth + SPLITTER_W;
@@ -26,7 +26,7 @@ export function computeSettingsBounds(contentWidth, contentHeight, headerHeight)
26
26
  return {
27
27
  x: Math.max(0, contentWidth - SETTINGS_W),
28
28
  y: headerHeight,
29
- width: SETTINGS_W,
29
+ width: Math.min(SETTINGS_W, Math.max(1, contentWidth)),
30
30
  height: Math.max(1, contentHeight - headerHeight),
31
31
  };
32
32
  }
@@ -34,7 +34,7 @@ export function computePopoverBounds(contentWidth, contentHeight, headerHeight)
34
34
  return {
35
35
  x: 0,
36
36
  y: headerHeight,
37
- width: contentWidth,
37
+ width: Math.max(1, contentWidth),
38
38
  height: contentHeight - headerHeight,
39
39
  };
40
40
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Main-process disk reader/writer backing `difile://_store/*` and
3
+ * `difile://<usr-rel>` URLs.
4
+ *
5
+ * Spec: `packages/devtools/docs/file-system.md` §6 P1-1 / P1-3.
6
+ *
7
+ * - `readDiskFile(realPath, opts?)`: read a (possibly ranged) slice of a
8
+ * file inside the USER_DATA_PATH sandbox. Returns `bytes` plus the
9
+ * content metadata callers need to assemble an HTTP response without a
10
+ * second `fs.stat` round-trip (`mime`, `etag`, `totalSize`).
11
+ * - `writeDiskFile(realPath, bytes)`: write bytes into the sandbox,
12
+ * creating parent directories on demand. Accepts both `Buffer` and
13
+ * `ArrayBuffer`.
14
+ * - `readDiskDir(realPath)`: list immediate children, no '.' / '..'.
15
+ * - `statDiskFile(realPath)`: minimal stat surface (size/mtime/mode/flags).
16
+ *
17
+ * The caller is responsible for canonicalizing `realPath` through
18
+ * `resolveVPath` *before* invoking these helpers — disk.ts assumes the
19
+ * argument is already known to be inside the sandbox base.
20
+ *
21
+ * MIME resolution prefers magic-byte sniffing (first ~12 bytes) over file
22
+ * extension, falling back to `application/octet-stream` for both unknown
23
+ * extensions and unknown magic.
24
+ *
25
+ * ETag shape is `W/"<mtime-ms>-<size>"` — weak validator, both axes folded
26
+ * into one token so byte-equal rewrites at the same timestamp still match.
27
+ */
28
+ export interface DiskReadResult {
29
+ bytes: Buffer;
30
+ mime: string;
31
+ /** Weak ETag of the form `W/"<mtime>-<size>"`. */
32
+ etag: string;
33
+ /** Full file size in bytes, regardless of whether a Range slice was returned. */
34
+ totalSize: number;
35
+ }
36
+ export interface DiskReadOptions {
37
+ /** Inclusive byte range `[start, end]`. Omit to read the whole file. */
38
+ range?: {
39
+ start: number;
40
+ end: number;
41
+ };
42
+ }
43
+ export interface DiskStat {
44
+ size: number;
45
+ /** Unix milliseconds. */
46
+ mtime: number;
47
+ mode: number;
48
+ isFile: boolean;
49
+ isDirectory: boolean;
50
+ }
51
+ export declare function readDiskFile(realPath: string, opts?: DiskReadOptions): Promise<DiskReadResult>;
52
+ export declare function writeDiskFile(realPath: string, bytes: Buffer | ArrayBuffer): Promise<void>;
53
+ export declare function readDiskDir(realPath: string): Promise<string[]>;
54
+ export declare function statDiskFile(realPath: string): Promise<DiskStat>;
55
+ //# sourceMappingURL=disk.d.ts.map