@dimina-kit/devtools 0.4.0-dev.20260702182435 → 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.
- package/dist/main/app/app.js +5 -25
- package/dist/main/app/native-overview.d.ts +27 -0
- package/dist/main/app/native-overview.js +44 -0
- package/dist/main/index.bundle.js +826 -690
- package/dist/main/ipc/bridge-router.js +180 -141
- package/dist/main/ipc/project-fs.js +39 -21
- package/dist/main/services/automation/handlers/app.js +143 -109
- package/dist/main/services/mcp/tools/simulator-tools.js +118 -97
- package/dist/main/services/network-forward/dispatch-batch.d.ts +22 -0
- package/dist/main/services/network-forward/dispatch-batch.js +21 -0
- package/dist/main/services/network-forward/index.js +5 -20
- package/dist/main/services/projects/create-project-service.js +54 -34
- package/dist/main/services/projects/types.d.ts +1 -32
- package/dist/main/services/service-console/console-api.js +36 -27
- package/dist/main/services/simulator-temp-files/disk.js +109 -86
- package/dist/main/services/workbench-coi-server.js +58 -45
- package/dist/main/utils/logger.d.ts +5 -12
- package/dist/main/utils/logger.js +5 -45
- package/dist/preload/instrumentation/wxml-extract.js +94 -62
- package/dist/preload/runtime/main-api-runner.d.ts +1 -1
- package/dist/preload/windows/main.cjs +284 -565
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs +37 -39
- package/dist/preload/windows/simulator.cjs.map +2 -2
- package/dist/preload/windows/simulator.js +37 -39
- package/dist/render-host/render-inspect.js +63 -52
- package/dist/renderer/assets/index-D-ksyN1p.js +49 -0
- package/dist/renderer/assets/workbenchSettings-COhuFF-i.js +8 -0
- package/dist/renderer/entries/main/index.html +1 -1
- package/dist/renderer/entries/workbench-settings/index.html +1 -1
- package/dist/shared/appdata-accumulator.js +53 -48
- package/dist/shared/open-in-editor-resource-path.d.ts +56 -0
- package/dist/shared/open-in-editor-resource-path.js +133 -0
- package/dist/shared/open-in-editor.d.ts +2 -9
- package/dist/shared/open-in-editor.js +8 -93
- package/dist/shared/types.d.ts +13 -0
- package/dist/simulator/assets/{device-shell-CUl0ILfn.js → device-shell-CiLAPa0I.js} +2 -2
- package/dist/simulator/assets/simulator-Dvnxry3y.js +10 -0
- package/dist/simulator/simulator.html +1 -1
- package/dist/vscode-workbench/assets/__vite-browser-external-CXi6Kz9W.js +1 -0
- package/dist/vscode-workbench/assets/{dist-CS7SQP3K.js → dist-TpGpmMGi.js} +3 -3
- package/dist/vscode-workbench/assets/{iconv-lite-umd-D3q-1-o6.js → iconv-lite-umd-C9tiCAJa.js} +1 -1
- package/dist/vscode-workbench/assets/{index-zjigpZ25.js → index-DJ1HyMZ7.js} +20 -22
- package/dist/vscode-workbench/assets/{jschardet-Cu4ufeHq.js → jschardet-DE1jV513.js} +1 -1
- package/dist/vscode-workbench/index.html +1 -1
- package/package.json +5 -5
- package/dist/renderer/assets/index-B-Dqb7G0.js +0 -49
- package/dist/renderer/assets/workbenchSettings-Bim9ol9R.js +0 -8
- package/dist/simulator/assets/simulator-Dz8iGcgy.js +0 -10
- 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
|
-
|
|
19
|
-
|
|
20
|
-
const name = (
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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: ${
|
|
47
|
+
throw new Error(`Template not found: ${id}`);
|
|
44
48
|
}
|
|
45
49
|
if (!template.source && !template.generate) {
|
|
46
|
-
throw new Error(`Template '${
|
|
50
|
+
throw new Error(`Template '${id}' has neither a source directory nor a generate function`);
|
|
47
51
|
}
|
|
48
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
|
|
43
|
-
if (
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
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
|
-
|
|
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
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
&& head[0] === 0x25 && head[1] === 0x50 && head[2] === 0x44 && head[3] === 0x46
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
&& head[0] === 0x50 && head[1] === 0x4b && head[2] === 0x03 && head[3] === 0x04
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
-
*
|
|
23
|
-
*
|
|
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
|
|
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
|