@dimina-kit/devtools 0.4.0-dev.20260702175315 → 0.4.0-dev.20260703051110

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 (50) hide show
  1. package/dist/main/app/app.js +5 -25
  2. package/dist/main/app/native-overview.d.ts +27 -0
  3. package/dist/main/app/native-overview.js +44 -0
  4. package/dist/main/index.bundle.js +826 -690
  5. package/dist/main/ipc/bridge-router.js +180 -141
  6. package/dist/main/ipc/project-fs.js +39 -21
  7. package/dist/main/services/automation/handlers/app.js +143 -109
  8. package/dist/main/services/mcp/tools/simulator-tools.js +118 -97
  9. package/dist/main/services/network-forward/dispatch-batch.d.ts +22 -0
  10. package/dist/main/services/network-forward/dispatch-batch.js +21 -0
  11. package/dist/main/services/network-forward/index.js +5 -20
  12. package/dist/main/services/projects/create-project-service.js +54 -34
  13. package/dist/main/services/projects/types.d.ts +1 -32
  14. package/dist/main/services/service-console/console-api.js +36 -27
  15. package/dist/main/services/simulator-temp-files/disk.js +109 -86
  16. package/dist/main/services/workbench-coi-server.js +58 -45
  17. package/dist/main/utils/logger.d.ts +5 -12
  18. package/dist/main/utils/logger.js +5 -45
  19. package/dist/preload/instrumentation/wxml-extract.js +94 -62
  20. package/dist/preload/runtime/main-api-runner.d.ts +1 -1
  21. package/dist/preload/windows/main.cjs +284 -565
  22. package/dist/preload/windows/main.cjs.map +2 -2
  23. package/dist/preload/windows/simulator.cjs +37 -39
  24. package/dist/preload/windows/simulator.cjs.map +2 -2
  25. package/dist/preload/windows/simulator.js +37 -39
  26. package/dist/render-host/render-inspect.js +63 -52
  27. package/dist/renderer/assets/index-D-ksyN1p.js +49 -0
  28. package/dist/renderer/assets/workbenchSettings-COhuFF-i.js +8 -0
  29. package/dist/renderer/entries/main/index.html +1 -1
  30. package/dist/renderer/entries/workbench-settings/index.html +1 -1
  31. package/dist/shared/appdata-accumulator.js +53 -48
  32. package/dist/shared/open-in-editor-resource-path.d.ts +56 -0
  33. package/dist/shared/open-in-editor-resource-path.js +133 -0
  34. package/dist/shared/open-in-editor.d.ts +2 -9
  35. package/dist/shared/open-in-editor.js +8 -93
  36. package/dist/shared/types.d.ts +13 -0
  37. package/dist/simulator/assets/{device-shell-CUl0ILfn.js → device-shell-CiLAPa0I.js} +2 -2
  38. package/dist/simulator/assets/simulator-Dvnxry3y.js +10 -0
  39. package/dist/simulator/simulator.html +1 -1
  40. package/dist/vscode-workbench/assets/__vite-browser-external-CXi6Kz9W.js +1 -0
  41. package/dist/vscode-workbench/assets/{dist-CS7SQP3K.js → dist-TpGpmMGi.js} +3 -3
  42. package/dist/vscode-workbench/assets/{iconv-lite-umd-D3q-1-o6.js → iconv-lite-umd-C9tiCAJa.js} +1 -1
  43. package/dist/vscode-workbench/assets/{index-zjigpZ25.js → index-DJ1HyMZ7.js} +20 -22
  44. package/dist/vscode-workbench/assets/{jschardet-Cu4ufeHq.js → jschardet-DE1jV513.js} +1 -1
  45. package/dist/vscode-workbench/index.html +1 -1
  46. package/package.json +5 -5
  47. package/dist/renderer/assets/index-B-Dqb7G0.js +0 -49
  48. package/dist/renderer/assets/workbenchSettings-Bim9ol9R.js +0 -8
  49. package/dist/simulator/assets/simulator-Dz8iGcgy.js +0 -10
  50. package/dist/vscode-workbench/assets/__vite-browser-external-B0DTNerG.js +0 -1
@@ -15,55 +15,67 @@
15
15
  import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
  const DEFAULT_TEMPLATE_ID = 'blank';
18
- export async function createProject(input, ctx) {
19
- // 1. name validation
20
- const name = (input.name ?? '').trim();
18
+ /** Non-empty-after-trim `input.name`, or throws. */
19
+ function resolveName(rawName) {
20
+ const name = (rawName ?? '').trim();
21
21
  if (name.length === 0) {
22
22
  throw new Error('Project name cannot be empty');
23
23
  }
24
- // 2. path validation: does-not-exist OR exists+empty.
25
- const target = input.path;
24
+ return name;
25
+ }
26
+ /** `input.path` does-not-exist OR exists+empty, or throws. */
27
+ function validateTargetPath(target) {
26
28
  if (!target || typeof target !== 'string') {
27
29
  throw new Error('Project path is required');
28
30
  }
29
- if (fs.existsSync(target)) {
30
- const stat = fs.statSync(target);
31
- if (!stat.isDirectory()) {
32
- throw new Error(`Project path exists and is not a directory: ${target}`);
33
- }
34
- const entries = fs.readdirSync(target);
35
- if (entries.length > 0) {
36
- throw new Error(`Project path is not empty (refusing to overwrite): ${target}`);
37
- }
31
+ if (!fs.existsSync(target))
32
+ return;
33
+ const stat = fs.statSync(target);
34
+ if (!stat.isDirectory()) {
35
+ throw new Error(`Project path exists and is not a directory: ${target}`);
36
+ }
37
+ const entries = fs.readdirSync(target);
38
+ if (entries.length > 0) {
39
+ throw new Error(`Project path is not empty (refusing to overwrite): ${target}`);
38
40
  }
39
- // 3. template lookup (default 'blank')
40
- const templateId = input.templateId ?? DEFAULT_TEMPLATE_ID;
41
- const template = ctx.templates.find((t) => t.id === templateId);
41
+ }
42
+ /** Look up `templateId` (default 'blank') and confirm it can materialise a project. */
43
+ function resolveTemplate(templates, templateId) {
44
+ const id = templateId ?? DEFAULT_TEMPLATE_ID;
45
+ const template = templates.find((t) => t.id === id);
42
46
  if (!template) {
43
- throw new Error(`Template not found: ${templateId}`);
47
+ throw new Error(`Template not found: ${id}`);
44
48
  }
45
49
  if (!template.source && !template.generate) {
46
- throw new Error(`Template '${templateId}' has neither a source directory nor a generate function`);
50
+ throw new Error(`Template '${id}' has neither a source directory nor a generate function`);
47
51
  }
48
- // 4. materialise: ensure target exists, then copy or generate.
52
+ return template;
53
+ }
54
+ /** Ensure `target` exists, then copy the template's `source` tree or run its `generate`. */
55
+ async function materializeTemplate(target, template, name) {
49
56
  fs.mkdirSync(target, { recursive: true });
50
57
  if (template.generate) {
51
58
  await template.generate(target, { name });
59
+ return;
52
60
  }
53
- else if (template.source) {
54
- if (!fs.existsSync(template.source.path)) {
55
- throw new Error(`Template source missing on disk: ${template.source.path}`);
56
- }
57
- // `recursive: true` makes cpSync mirror the entire tree (files,
58
- // subdirs, symlinks). `force: true` mirrors over an empty target.
59
- fs.cpSync(template.source.path, target, {
60
- recursive: true,
61
- force: true,
62
- });
61
+ if (!template.source)
62
+ return;
63
+ if (!fs.existsSync(template.source.path)) {
64
+ throw new Error(`Template source missing on disk: ${template.source.path}`);
63
65
  }
64
- // 5. rewrite project.config.json projectname (best-effort: create the file
65
- // if the template didn't ship one gives the user a working starting
66
- // point even for tiny generators).
66
+ // `recursive: true` makes cpSync mirror the entire tree (files,
67
+ // subdirs, symlinks). `force: true` mirrors over an empty target.
68
+ fs.cpSync(template.source.path, target, {
69
+ recursive: true,
70
+ force: true,
71
+ });
72
+ }
73
+ /**
74
+ * Rewrite `<target>/project.config.json`'s `projectname` (best-effort: create
75
+ * the file if the template didn't ship one — gives the user a working
76
+ * starting point even for tiny generators).
77
+ */
78
+ function writeProjectConfig(target, name) {
67
79
  const cfgPath = path.join(target, 'project.config.json');
68
80
  let cfg = {};
69
81
  if (fs.existsSync(cfgPath)) {
@@ -76,7 +88,15 @@ export async function createProject(input, ctx) {
76
88
  }
77
89
  cfg.projectname = name;
78
90
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
79
- // 6. register with provider — this is what makes the new project appear
91
+ }
92
+ export async function createProject(input, ctx) {
93
+ const name = resolveName(input.name);
94
+ const target = input.path;
95
+ validateTargetPath(target);
96
+ const template = resolveTemplate(ctx.templates, input.templateId);
97
+ await materializeTemplate(target, template, name);
98
+ writeProjectConfig(target, name);
99
+ // register with provider — this is what makes the new project appear
80
100
  // in the list immediately after the dialog closes.
81
101
  const created = await ctx.projectsProvider.addProject(target);
82
102
  return created;
@@ -7,6 +7,7 @@
7
7
  import type { CompileConfig } from '../../../shared/types.js';
8
8
  import type { Project } from './project-repository.js';
9
9
  export type { Project };
10
+ export type { ProjectTemplate, CreateProjectInput } from '../../../shared/types.js';
10
11
  /**
11
12
  * Default compile config returned by WorkspaceService when the injected
12
13
  * provider omits `getCompileConfig`, or when no record exists for a path.
@@ -81,38 +82,6 @@ export interface ProjectsProvider {
81
82
  */
82
83
  getThumbnail?(dirPath: string): string | null | Promise<string | null>;
83
84
  }
84
- /**
85
- * A template the user can pick from in the "新建项目" dialog. Exactly one of
86
- * `source` or `generate` should be supplied; if both are omitted, the
87
- * service refuses to materialise the template.
88
- */
89
- export interface ProjectTemplate {
90
- /** Stable identifier. Used by host whitelists and the `templateId` field of CreateProjectInput. */
91
- id: string;
92
- /** Human-readable label shown in the dialog. */
93
- name: string;
94
- description?: string;
95
- icon?: string;
96
- /** Copy-tree source. `path` must be absolute. */
97
- source?: {
98
- type: 'directory';
99
- path: string;
100
- };
101
- /** Programmatic generator. `target` is the absolute destination directory. */
102
- generate?: (target: string, opts: {
103
- name: string;
104
- }) => Promise<void>;
105
- }
106
- /**
107
- * Payload returned from the create-project dialog. The service uses this to
108
- * scaffold disk content and to register the project with the provider.
109
- */
110
- export interface CreateProjectInput {
111
- name: string;
112
- path: string;
113
- templateId?: string;
114
- extra?: Record<string, unknown>;
115
- }
116
85
  /** Built-in template policy. */
117
86
  export type BuiltinTemplatesMode = 'all' | 'none' | readonly string[];
118
87
  //# sourceMappingURL=types.d.ts.map
@@ -34,35 +34,25 @@ export function mapConsoleApiType(type) {
34
34
  }
35
35
  const BIGINT_LITERAL = /^-?\d+n$/;
36
36
  /**
37
- * A single RemoteObject a JSON-serializable value WITHOUT a CDP round-trip
38
- * (shallow). Objects that need their full contents are flagged by
39
- * {@link needsDeepFetch} and deep-serialized by the caller; this is the
40
- * inline/best-effort fallback.
37
+ * A CDP `unserializableValue` string (specials that can't ride in `value`,
38
+ * e.g. `Infinity`/`NaN`/bigint literals) the JS value it denotes. Unknown
39
+ * literals pass through as-is (e.g. a bigint literal stays a string).
41
40
  */
42
- export function remoteObjectToValue(ro) {
43
- if (!ro)
44
- return ro;
45
- // 1. An inlined value (CDP includes `value` for JSON-serializable primitives
46
- // and small arrays). Use `in` so falsy values (0, '', false) and an
47
- // explicit `null` are honoured rather than falling through.
48
- if ('value' in ro)
49
- return ro.value;
50
- // 2. Specials that can't ride in `value`.
51
- if (typeof ro.unserializableValue === 'string') {
52
- const u = ro.unserializableValue;
53
- if (u === 'Infinity')
54
- return Infinity;
55
- if (u === '-Infinity')
56
- return -Infinity;
57
- if (u === 'NaN')
58
- return NaN;
59
- if (u === '-0')
60
- return -0;
61
- if (BIGINT_LITERAL.test(u))
62
- return u;
41
+ function unserializableToValue(u) {
42
+ if (u === 'Infinity')
43
+ return Infinity;
44
+ if (u === '-Infinity')
45
+ return -Infinity;
46
+ if (u === 'NaN')
47
+ return NaN;
48
+ if (u === '-0')
49
+ return -0;
50
+ if (BIGINT_LITERAL.test(u))
63
51
  return u;
64
- }
65
- // 3. Fall back by type.
52
+ return u;
53
+ }
54
+ /** Fallback serialization by `RemoteObject.type` when neither `value` nor `unserializableValue` is present. */
55
+ function typeFallbackToValue(ro) {
66
56
  switch (ro.type) {
67
57
  case 'undefined':
68
58
  return undefined;
@@ -78,6 +68,25 @@ export function remoteObjectToValue(ro) {
78
68
  return ro.description ?? '[Unknown]';
79
69
  }
80
70
  }
71
+ /**
72
+ * A single RemoteObject → a JSON-serializable value WITHOUT a CDP round-trip
73
+ * (shallow). Objects that need their full contents are flagged by
74
+ * {@link needsDeepFetch} and deep-serialized by the caller; this is the
75
+ * inline/best-effort fallback.
76
+ */
77
+ export function remoteObjectToValue(ro) {
78
+ if (!ro)
79
+ return ro;
80
+ // An inlined value (CDP includes `value` for JSON-serializable primitives
81
+ // and small arrays). Use `in` so falsy values (0, '', false) and an
82
+ // explicit `null` are honoured rather than falling through.
83
+ if ('value' in ro)
84
+ return ro.value;
85
+ if (typeof ro.unserializableValue === 'string') {
86
+ return unserializableToValue(ro.unserializableValue);
87
+ }
88
+ return typeFallbackToValue(ro);
89
+ }
81
90
  /**
82
91
  * Whether this RemoteObject must be deep-fetched via `Runtime.callFunctionOn`
83
92
  * (returnByValue) to be fully serialized — i.e. a real object/array referenced
@@ -62,49 +62,63 @@ const EXT_MIME = {
62
62
  '.pdf': 'application/pdf',
63
63
  '.zip': 'application/zip',
64
64
  };
65
- function sniffMime(head) {
66
- if (head.length >= 8
65
+ function isPng(head) {
66
+ return head.length >= 8
67
67
  && head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4e && head[3] === 0x47
68
- && head[4] === 0x0d && head[5] === 0x0a && head[6] === 0x1a && head[7] === 0x0a) {
69
- return 'image/png';
70
- }
71
- if (head.length >= 3 && head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff) {
72
- return 'image/jpeg';
73
- }
74
- if (head.length >= 6
68
+ && head[4] === 0x0d && head[5] === 0x0a && head[6] === 0x1a && head[7] === 0x0a;
69
+ }
70
+ function isJpeg(head) {
71
+ return head.length >= 3 && head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff;
72
+ }
73
+ function isGif(head) {
74
+ return head.length >= 6
75
75
  && head[0] === 0x47 && head[1] === 0x49 && head[2] === 0x46
76
- && head[3] === 0x38 && (head[4] === 0x37 || head[4] === 0x39) && head[5] === 0x61) {
77
- return 'image/gif';
78
- }
79
- if (head.length >= 12
76
+ && head[3] === 0x38 && (head[4] === 0x37 || head[4] === 0x39) && head[5] === 0x61;
77
+ }
78
+ function isWebp(head) {
79
+ return head.length >= 12
80
80
  && head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46
81
- && head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50) {
82
- return 'image/webp';
83
- }
84
- if (head.length >= 12
85
- && head[4] === 0x66 && head[5] === 0x74 && head[6] === 0x79 && head[7] === 0x70) {
86
- // MP4 ftyp box: brand bytes vary, treat all as video/mp4 for now.
87
- return 'video/mp4';
88
- }
89
- if (head.length >= 4
90
- && head[0] === 0x25 && head[1] === 0x50 && head[2] === 0x44 && head[3] === 0x46) {
91
- return 'application/pdf';
92
- }
93
- if (head.length >= 2 && head[0] === 0x42 && head[1] === 0x4d) {
94
- return 'image/bmp';
95
- }
96
- if (head.length >= 4
97
- && head[0] === 0x50 && head[1] === 0x4b && head[2] === 0x03 && head[3] === 0x04) {
98
- return 'application/zip';
99
- }
100
- if (head.length >= 3 && head[0] === 0x49 && head[1] === 0x44 && head[2] === 0x33) {
101
- // ID3-tagged MP3.
102
- return 'audio/mpeg';
103
- }
104
- if (head.length >= 4 && head[0] === 0x4f && head[1] === 0x67 && head[2] === 0x67 && head[3] === 0x53) {
105
- return 'audio/ogg';
106
- }
107
- return null;
81
+ && head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50;
82
+ }
83
+ /** MP4 ftyp box: brand bytes vary, treat all as video/mp4 for now. */
84
+ function isMp4(head) {
85
+ return head.length >= 12
86
+ && head[4] === 0x66 && head[5] === 0x74 && head[6] === 0x79 && head[7] === 0x70;
87
+ }
88
+ function isPdf(head) {
89
+ return head.length >= 4
90
+ && head[0] === 0x25 && head[1] === 0x50 && head[2] === 0x44 && head[3] === 0x46;
91
+ }
92
+ function isBmp(head) {
93
+ return head.length >= 2 && head[0] === 0x42 && head[1] === 0x4d;
94
+ }
95
+ function isZip(head) {
96
+ return head.length >= 4
97
+ && head[0] === 0x50 && head[1] === 0x4b && head[2] === 0x03 && head[3] === 0x04;
98
+ }
99
+ /** ID3-tagged MP3. */
100
+ function isId3Mp3(head) {
101
+ return head.length >= 3 && head[0] === 0x49 && head[1] === 0x44 && head[2] === 0x33;
102
+ }
103
+ function isOgg(head) {
104
+ return head.length >= 4 && head[0] === 0x4f && head[1] === 0x67 && head[2] === 0x67 && head[3] === 0x53;
105
+ }
106
+ /** Magic-byte sniffers in priority order — first match wins. */
107
+ const MAGIC_SNIFFERS = [
108
+ { mime: 'image/png', match: isPng },
109
+ { mime: 'image/jpeg', match: isJpeg },
110
+ { mime: 'image/gif', match: isGif },
111
+ { mime: 'image/webp', match: isWebp },
112
+ { mime: 'video/mp4', match: isMp4 },
113
+ { mime: 'application/pdf', match: isPdf },
114
+ { mime: 'image/bmp', match: isBmp },
115
+ { mime: 'application/zip', match: isZip },
116
+ { mime: 'audio/mpeg', match: isId3Mp3 },
117
+ { mime: 'audio/ogg', match: isOgg },
118
+ ];
119
+ function sniffMime(head) {
120
+ const hit = MAGIC_SNIFFERS.find((sniffer) => sniffer.match(head));
121
+ return hit ? hit.mime : null;
108
122
  }
109
123
  function extMime(realPath) {
110
124
  const ext = path.extname(realPath).toLowerCase();
@@ -126,6 +140,54 @@ function etagOf(mtimeMs, size) {
126
140
  return `W/"${Math.floor(mtimeMs)}-${size}"`;
127
141
  }
128
142
  // -- read -------------------------------------------------------------------
143
+ /** Validate `range` against `totalSize` and resolve it to a byte slice, or throws. */
144
+ function resolveRange(range, totalSize) {
145
+ const { start, end } = range;
146
+ if (!Number.isFinite(start) || !Number.isFinite(end)) {
147
+ throw new RangeError(`invalid range: ${start}-${end}`);
148
+ }
149
+ if (start < 0)
150
+ throw new RangeError(`range start out of bounds: ${start}`);
151
+ if (start > end)
152
+ throw new RangeError(`range start > end: ${start} > ${end}`);
153
+ if (start >= totalSize) {
154
+ throw new RangeError(`range start beyond file size: ${start} >= ${totalSize}`);
155
+ }
156
+ const clampedEnd = Math.min(end, totalSize - 1);
157
+ return { sliceStart: start, sliceLen: clampedEnd - start + 1 };
158
+ }
159
+ /**
160
+ * Read at most the first 12 bytes for magic sniffing, regardless of whether
161
+ * the caller asked for a range. The head bytes from position 0 are needed to
162
+ * label Content-Type correctly even on a tail Range request.
163
+ */
164
+ async function readHeadBytes(handle, totalSize) {
165
+ const head = Buffer.alloc(Math.min(12, totalSize));
166
+ if (head.length > 0) {
167
+ await handle.read(head, 0, head.length, 0);
168
+ }
169
+ return head;
170
+ }
171
+ /** Full-file body, re-using `head` (already read) instead of a second read for the leading bytes. */
172
+ async function readFullBody(handle, head, totalSize) {
173
+ const bytes = Buffer.alloc(totalSize);
174
+ if (totalSize === 0)
175
+ return bytes;
176
+ if (totalSize <= head.length) {
177
+ head.copy(bytes, 0, 0, totalSize);
178
+ return bytes;
179
+ }
180
+ head.copy(bytes, 0, 0, head.length);
181
+ await handle.read(bytes, head.length, totalSize - head.length, head.length);
182
+ return bytes;
183
+ }
184
+ async function readRangeBody(handle, sliceStart, sliceLen) {
185
+ const bytes = Buffer.alloc(sliceLen);
186
+ if (sliceLen > 0) {
187
+ await handle.read(bytes, 0, sliceLen, sliceStart);
188
+ }
189
+ return bytes;
190
+ }
129
191
  export async function readDiskFile(realPath, opts) {
130
192
  const handle = await fs.open(realPath, 'r');
131
193
  try {
@@ -133,52 +195,13 @@ export async function readDiskFile(realPath, opts) {
133
195
  const totalSize = st.size;
134
196
  const mtimeMs = st.mtimeMs;
135
197
  const range = opts?.range;
136
- let sliceStart = 0;
137
- let sliceLen = totalSize;
138
- if (range) {
139
- const { start, end } = range;
140
- if (!Number.isFinite(start) || !Number.isFinite(end)) {
141
- throw new RangeError(`invalid range: ${start}-${end}`);
142
- }
143
- if (start < 0)
144
- throw new RangeError(`range start out of bounds: ${start}`);
145
- if (start > end)
146
- throw new RangeError(`range start > end: ${start} > ${end}`);
147
- if (start >= totalSize) {
148
- throw new RangeError(`range start beyond file size: ${start} >= ${totalSize}`);
149
- }
150
- const clampedEnd = Math.min(end, totalSize - 1);
151
- sliceStart = start;
152
- sliceLen = clampedEnd - start + 1;
153
- }
154
- // Read at most the first 12 bytes for magic sniffing, regardless of
155
- // whether the caller asked for a range. We need the head bytes from
156
- // position 0 to label Content-Type correctly even on a tail Range
157
- // request. For full reads the head is part of the body so we re-use
158
- // it without a second read.
159
- const head = Buffer.alloc(Math.min(12, totalSize));
160
- if (head.length > 0) {
161
- await handle.read(head, 0, head.length, 0);
162
- }
163
- let bytes;
164
- if (!range) {
165
- bytes = Buffer.alloc(totalSize);
166
- if (totalSize > 0) {
167
- if (totalSize <= head.length) {
168
- head.copy(bytes, 0, 0, totalSize);
169
- }
170
- else {
171
- head.copy(bytes, 0, 0, head.length);
172
- await handle.read(bytes, head.length, totalSize - head.length, head.length);
173
- }
174
- }
175
- }
176
- else {
177
- bytes = Buffer.alloc(sliceLen);
178
- if (sliceLen > 0) {
179
- await handle.read(bytes, 0, sliceLen, sliceStart);
180
- }
181
- }
198
+ const { sliceStart, sliceLen } = range
199
+ ? resolveRange(range, totalSize)
200
+ : { sliceStart: 0, sliceLen: totalSize };
201
+ const head = await readHeadBytes(handle, totalSize);
202
+ const bytes = range
203
+ ? await readRangeBody(handle, sliceStart, sliceLen)
204
+ : await readFullBody(handle, head, totalSize);
182
205
  return {
183
206
  bytes,
184
207
  mime: detectMime(realPath, head),
@@ -188,6 +188,59 @@ function fsErrorStatus(e) {
188
188
  return 404;
189
189
  return 500;
190
190
  }
191
+ async function fsStat(ctx) {
192
+ const st = await statWithin(ctx.projectRoot, ctx.rel);
193
+ jsonRes(ctx.res, 200, { type: st.isDirectory() ? 2 : 1, size: st.size, ctime: st.ctimeMs, mtime: st.mtimeMs });
194
+ }
195
+ async function fsReaddir(ctx) {
196
+ const entries = await readdirWithin(ctx.projectRoot, ctx.rel);
197
+ jsonRes(ctx.res, 200, entries.map((e) => [e.name, e.isDirectory() ? 2 : 1]));
198
+ }
199
+ async function fsRead(ctx) {
200
+ const buf = await readFileBufferWithin(ctx.projectRoot, ctx.rel);
201
+ ctx.res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Length': buf.length });
202
+ ctx.res.end(buf);
203
+ }
204
+ async function fsWrite(ctx) {
205
+ let buf;
206
+ try {
207
+ buf = await readBody(ctx.req);
208
+ }
209
+ catch (e) {
210
+ if (e instanceof BodyTooLargeError)
211
+ return jsonRes(ctx.res, 413, { error: e.message });
212
+ throw e;
213
+ }
214
+ await writeFileWithin(ctx.projectRoot, ctx.rel, buf);
215
+ ctx.res.writeHead(204);
216
+ ctx.res.end();
217
+ }
218
+ async function fsMkdir(ctx) {
219
+ await mkdirWithin(ctx.projectRoot, ctx.rel);
220
+ ctx.res.writeHead(204);
221
+ ctx.res.end();
222
+ }
223
+ async function fsDelete(ctx) {
224
+ await deleteWithin(ctx.projectRoot, ctx.rel);
225
+ ctx.res.writeHead(204);
226
+ ctx.res.end();
227
+ }
228
+ async function fsRename(ctx) {
229
+ const toRel = (ctx.url.searchParams.get('to') ?? '').replace(/^\/+/, '');
230
+ await renameWithin(ctx.projectRoot, ctx.rel, toRel);
231
+ ctx.res.writeHead(204);
232
+ ctx.res.end();
233
+ }
234
+ /** One handler per supported `/__fs/<action>` — keyed lookup replaces an if-chain. */
235
+ const FS_ACTIONS = {
236
+ stat: fsStat,
237
+ readdir: fsReaddir,
238
+ read: fsRead,
239
+ write: fsWrite,
240
+ mkdir: fsMkdir,
241
+ delete: fsDelete,
242
+ rename: fsRename,
243
+ };
191
244
  /**
192
245
  * `/__fs/<action>?p=<rel>` bridge onto the live active project root.
193
246
  * `p`/`to` are POSIX paths relative to the project root, resolved through the
@@ -205,54 +258,14 @@ async function handleFsRequest(req, res, projectRoot, url) {
205
258
  return jsonRes(res, rejectStatus, { error: 'mutating fs action requires a non-GET same-origin request' });
206
259
  }
207
260
  }
208
- try {
209
- if (action === 'stat') {
210
- const st = await statWithin(projectRoot, rel);
211
- return jsonRes(res, 200, { type: st.isDirectory() ? 2 : 1, size: st.size, ctime: st.ctimeMs, mtime: st.mtimeMs });
212
- }
213
- if (action === 'readdir') {
214
- const entries = await readdirWithin(projectRoot, rel);
215
- return jsonRes(res, 200, entries.map((e) => [e.name, e.isDirectory() ? 2 : 1]));
216
- }
217
- if (action === 'read') {
218
- const buf = await readFileBufferWithin(projectRoot, rel);
219
- res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Length': buf.length });
220
- return void res.end(buf);
221
- }
222
- if (action === 'write') {
223
- let buf;
224
- try {
225
- buf = await readBody(req);
226
- }
227
- catch (e) {
228
- if (e instanceof BodyTooLargeError)
229
- return jsonRes(res, 413, { error: e.message });
230
- throw e;
231
- }
232
- await writeFileWithin(projectRoot, rel, buf);
233
- res.writeHead(204);
234
- return void res.end();
235
- }
236
- if (action === 'mkdir') {
237
- await mkdirWithin(projectRoot, rel);
238
- res.writeHead(204);
239
- return void res.end();
240
- }
241
- if (action === 'delete') {
242
- await deleteWithin(projectRoot, rel);
243
- res.writeHead(204);
244
- return void res.end();
245
- }
246
- if (action === 'rename') {
247
- const toRel = (url.searchParams.get('to') ?? '').replace(/^\/+/, '');
248
- await renameWithin(projectRoot, rel, toRel);
249
- res.writeHead(204);
250
- return void res.end();
251
- }
261
+ const handler = FS_ACTIONS[action];
262
+ if (!handler)
252
263
  return jsonRes(res, 404, { error: 'unknown fs action: ' + action });
264
+ try {
265
+ await handler({ req, res, projectRoot, rel, url });
253
266
  }
254
267
  catch (e) {
255
- return jsonRes(res, fsErrorStatus(e), { error: String(e.message), code: e.code });
268
+ jsonRes(res, fsErrorStatus(e), { error: String(e.message), code: e.code });
256
269
  }
257
270
  }
258
271
  /**
@@ -1,15 +1,8 @@
1
- type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
- export interface Logger {
3
- debug(message: string, ...args: unknown[]): void;
4
- info(message: string, ...args: unknown[]): void;
5
- warn(message: string, ...args: unknown[]): void;
6
- error(message: string, ...args: unknown[]): void;
7
- }
8
1
  /**
9
- * Create a tagged logger for a specific module.
10
- * All output goes to console with structured prefix.
2
+ * Tagged console logger. The implementation lives in
3
+ * `@dimina-kit/electron-deck/main` (single source the two packages share one
4
+ * logger); this module re-exports it so devtools-internal imports keep their
5
+ * stable `utils/logger` path.
11
6
  */
12
- export declare function createLogger(tag: string): Logger;
13
- export declare function setLogLevel(level: LogLevel): void;
14
- export {};
7
+ export { createLogger, setLogLevel, type Logger } from '@dimina-kit/electron-deck/main';
15
8
  //# sourceMappingURL=logger.d.ts.map
@@ -1,48 +1,8 @@
1
- const LEVEL_PRIORITY = {
2
- debug: 0,
3
- info: 1,
4
- warn: 2,
5
- error: 3,
6
- };
7
- const LEVEL_PREFIX = {
8
- debug: '[DEBUG]',
9
- info: '[INFO]',
10
- warn: '[WARN]',
11
- error: '[ERROR]',
12
- };
13
- let currentLevel = 'debug';
14
- function shouldLog(level) {
15
- return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel];
16
- }
17
- function formatMessage(level, tag, message) {
18
- const ts = new Date().toISOString();
19
- return `${ts} ${LEVEL_PREFIX[level]} [${tag}] ${message}`;
20
- }
21
1
  /**
22
- * Create a tagged logger for a specific module.
23
- * All output goes to console with structured prefix.
2
+ * Tagged console logger. The implementation lives in
3
+ * `@dimina-kit/electron-deck/main` (single source the two packages share one
4
+ * logger); this module re-exports it so devtools-internal imports keep their
5
+ * stable `utils/logger` path.
24
6
  */
25
- export function createLogger(tag) {
26
- return {
27
- debug(message, ...args) {
28
- if (shouldLog('debug'))
29
- console.debug(formatMessage('debug', tag, message), ...args);
30
- },
31
- info(message, ...args) {
32
- if (shouldLog('info'))
33
- console.info(formatMessage('info', tag, message), ...args);
34
- },
35
- warn(message, ...args) {
36
- if (shouldLog('warn'))
37
- console.warn(formatMessage('warn', tag, message), ...args);
38
- },
39
- error(message, ...args) {
40
- if (shouldLog('error'))
41
- console.error(formatMessage('error', tag, message), ...args);
42
- },
43
- };
44
- }
45
- export function setLogLevel(level) {
46
- currentLevel = level;
47
- }
7
+ export { createLogger, setLogLevel } from '@dimina-kit/electron-deck/main';
48
8
  //# sourceMappingURL=logger.js.map