@makerbi/remodex 1.5.3 → 1.5.8
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/bin/remodex-jsonl-diagnose.js +0 -0
- package/bin/remodex.js +42 -10
- package/package.json +1 -1
- package/src/account-status.js +7 -1
- package/src/apply-patch-changes.js +185 -0
- package/src/bootstrap-codex-cli.js +1 -1
- package/src/bridge-status.js +122 -0
- package/src/bridge.js +823 -157
- package/src/codex-transport.js +38 -11
- package/src/desktop-handler.js +14 -1
- package/src/desktop-ipc-action-follower.js +173 -1
- package/src/index.js +4 -2
- package/src/macos-launch-agent.js +87 -2
- package/src/project-handler.js +162 -1
- package/src/push-notification-service-client.js +85 -37
- package/src/push-notification-tracker.js +15 -0
- package/src/rollout-live-mirror.js +331 -20
- package/src/rollout-watch.js +5 -1
- package/src/secure-device-state.js +26 -1
- package/src/secure-transport.js +40 -12
- package/src/session-jsonl-history.js +860 -14
- package/src/voice-handler.js +129 -62
- package/src/workspace-handler.js +327 -14
- package/src/private-defaults.json +0 -4
package/src/workspace-handler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// FILE: workspace-handler.js
|
|
2
|
-
// Purpose: Executes workspace-scoped
|
|
2
|
+
// Purpose: Executes workspace-scoped previews, reads, and patch operations without touching unrelated repo changes.
|
|
3
3
|
// Layer: Bridge handler
|
|
4
4
|
// Exports: handleWorkspaceRequest
|
|
5
5
|
// Depends on: child_process, fs, os, path, ./codex-home, ./git-handler
|
|
@@ -8,7 +8,7 @@ const { execFile } = require("child_process");
|
|
|
8
8
|
const fs = require("fs");
|
|
9
9
|
const os = require("os");
|
|
10
10
|
const path = require("path");
|
|
11
|
-
const { promisify } = require("util");
|
|
11
|
+
const { promisify, TextDecoder } = require("util");
|
|
12
12
|
const { resolveCodexGeneratedImagesRoot } = require("./codex-home");
|
|
13
13
|
const { gitStatus } = require("./git-handler");
|
|
14
14
|
const {
|
|
@@ -23,6 +23,9 @@ const execFileAsync = promisify(execFile);
|
|
|
23
23
|
const GIT_TIMEOUT_MS = 30_000;
|
|
24
24
|
const MAX_IMAGE_READ_BYTES = 8 * 1024 * 1024;
|
|
25
25
|
const MAX_IMAGE_PREVIEW_READ_BYTES = 2 * 1024 * 1024;
|
|
26
|
+
const MAX_TEXT_FILE_READ_BYTES = 2 * 1024 * 1024;
|
|
27
|
+
const BINARY_SNIFF_BYTES = 8 * 1024;
|
|
28
|
+
const MAX_BASENAME_FALLBACK_MATCHES = 2;
|
|
26
29
|
const MIN_IMAGE_PREVIEW_PIXEL_DIMENSION = 128;
|
|
27
30
|
const MAX_IMAGE_PREVIEW_PIXEL_DIMENSION = 3_200;
|
|
28
31
|
const IMAGE_PREVIEW_RETRY_SCALE = 0.75;
|
|
@@ -32,6 +35,7 @@ const IMAGE_MIME_TYPES_BY_EXTENSION = new Map([
|
|
|
32
35
|
[".jpg", "image/jpeg"],
|
|
33
36
|
[".jpeg", "image/jpeg"],
|
|
34
37
|
[".png", "image/png"],
|
|
38
|
+
[".svg", "image/svg+xml"],
|
|
35
39
|
[".gif", "image/gif"],
|
|
36
40
|
[".webp", "image/webp"],
|
|
37
41
|
[".heic", "image/heic"],
|
|
@@ -83,6 +87,9 @@ async function handleWorkspaceMethod(method, params) {
|
|
|
83
87
|
if (method === "workspace/readImage") {
|
|
84
88
|
return workspaceReadImage(params);
|
|
85
89
|
}
|
|
90
|
+
if (method === "workspace/readFile") {
|
|
91
|
+
return workspaceReadFile(params);
|
|
92
|
+
}
|
|
86
93
|
|
|
87
94
|
const cwd = await resolveWorkspaceCwd(params);
|
|
88
95
|
const repoRoot = await resolveRepoRoot(cwd);
|
|
@@ -111,6 +118,60 @@ async function handleWorkspaceMethod(method, params) {
|
|
|
111
118
|
}
|
|
112
119
|
}
|
|
113
120
|
|
|
121
|
+
// Reads a UTF-8 text file from the active workspace for the phone-side read-only viewer.
|
|
122
|
+
async function workspaceReadFile(params) {
|
|
123
|
+
const requestedPath = firstNonEmptyString([params.path, params.filePath, params.localPath]);
|
|
124
|
+
if (!requestedPath) {
|
|
125
|
+
throw workspaceError("missing_file_path", "The request must include a file path.");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const cwd = await resolveWorkspaceCwd(params);
|
|
129
|
+
const realWorkspaceRoot = await resolveReadableWorkspaceRoot(cwd);
|
|
130
|
+
const realFilePath = await resolveWorkspaceTextFilePath(cwd, requestedPath, realWorkspaceRoot);
|
|
131
|
+
if (!realFilePath) {
|
|
132
|
+
throw workspaceError("file_not_found", "The file no longer exists on this Mac.");
|
|
133
|
+
}
|
|
134
|
+
if (!realWorkspaceRoot || !isPathInside(realFilePath, realWorkspaceRoot)) {
|
|
135
|
+
throw workspaceError("file_path_not_allowed", "Only files in the current workspace can be viewed.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const stat = await fs.promises.stat(realFilePath);
|
|
139
|
+
if (!stat.isFile()) {
|
|
140
|
+
throw workspaceError("file_not_found", "The path is not a file.");
|
|
141
|
+
}
|
|
142
|
+
if (stat.size > MAX_TEXT_FILE_READ_BYTES) {
|
|
143
|
+
throw workspaceError(
|
|
144
|
+
"file_too_large",
|
|
145
|
+
"This file is too large to send to the phone. Open it on the Mac or ask for a smaller section."
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const result = {
|
|
150
|
+
path: realFilePath,
|
|
151
|
+
fileName: path.basename(realFilePath),
|
|
152
|
+
byteLength: stat.size,
|
|
153
|
+
mtimeMs: stat.mtimeMs,
|
|
154
|
+
encoding: "utf-8",
|
|
155
|
+
};
|
|
156
|
+
if (params.includeContent === false || params.metadataOnly === true) {
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
if (isUnchangedTextFileRead(params, stat)) {
|
|
160
|
+
return {
|
|
161
|
+
...result,
|
|
162
|
+
notModified: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const data = await fs.promises.readFile(realFilePath);
|
|
167
|
+
const content = decodeUtf8TextFile(data);
|
|
168
|
+
return {
|
|
169
|
+
...result,
|
|
170
|
+
content,
|
|
171
|
+
lineCount: countLines(content),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
114
175
|
// Reads recognized local image files from the bound repo, Codex image cache, or host temp screenshot folders.
|
|
115
176
|
async function workspaceReadImage(params) {
|
|
116
177
|
const requestedPath = firstNonEmptyString([params.path, params.filePath, params.localPath]);
|
|
@@ -125,10 +186,7 @@ async function workspaceReadImage(params) {
|
|
|
125
186
|
? path.resolve(requestedPath)
|
|
126
187
|
: path.resolve(cwd || process.cwd(), requestedPath);
|
|
127
188
|
const extension = path.extname(imagePath).toLowerCase();
|
|
128
|
-
|
|
129
|
-
if (!mimeType) {
|
|
130
|
-
throw workspaceError("unsupported_image_type", "Only local image files can be previewed.");
|
|
131
|
-
}
|
|
189
|
+
let mimeType = IMAGE_MIME_TYPES_BY_EXTENSION.get(extension);
|
|
132
190
|
|
|
133
191
|
const [realImagePath, realGeneratedImagesRoot] = await Promise.all([
|
|
134
192
|
realpathOrNull(imagePath),
|
|
@@ -154,6 +212,12 @@ async function workspaceReadImage(params) {
|
|
|
154
212
|
if (!stat.isFile()) {
|
|
155
213
|
throw workspaceError("image_not_found", "The image path is not a file.");
|
|
156
214
|
}
|
|
215
|
+
if (!mimeType) {
|
|
216
|
+
mimeType = await sniffImageMimeType(realImagePath);
|
|
217
|
+
}
|
|
218
|
+
if (!mimeType) {
|
|
219
|
+
throw workspaceError("unsupported_image_type", "Only local image files can be previewed.");
|
|
220
|
+
}
|
|
157
221
|
const includeData = params.includeData !== false && params.metadataOnly !== true;
|
|
158
222
|
const maxPixelDimension = normalizedPreviewPixelDimension(params);
|
|
159
223
|
if (stat.size > MAX_IMAGE_READ_BYTES && !maxPixelDimension) {
|
|
@@ -181,7 +245,9 @@ async function workspaceReadImage(params) {
|
|
|
181
245
|
};
|
|
182
246
|
}
|
|
183
247
|
|
|
184
|
-
const data =
|
|
248
|
+
const data = mimeType === "image/svg+xml"
|
|
249
|
+
? await readSVGPreviewData(realImagePath, stat.size)
|
|
250
|
+
: maxPixelDimension
|
|
185
251
|
? await readPreviewImageData(realImagePath, maxPixelDimension, stat.size)
|
|
186
252
|
: await fs.promises.readFile(realImagePath);
|
|
187
253
|
return {
|
|
@@ -202,6 +268,45 @@ function normalizedPreviewPixelDimension(params) {
|
|
|
202
268
|
);
|
|
203
269
|
}
|
|
204
270
|
|
|
271
|
+
async function sniffImageMimeType(filePath) {
|
|
272
|
+
let header;
|
|
273
|
+
try {
|
|
274
|
+
const handle = await fs.promises.open(filePath, "r");
|
|
275
|
+
try {
|
|
276
|
+
header = Buffer.alloc(512);
|
|
277
|
+
const read = await handle.read(header, 0, header.length, 0);
|
|
278
|
+
header = header.subarray(0, read.bytesRead);
|
|
279
|
+
} finally {
|
|
280
|
+
await handle.close();
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (header.length >= 8 && header.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
|
|
287
|
+
return "image/png";
|
|
288
|
+
}
|
|
289
|
+
if (header.length >= 3 && header[0] === 0xff && header[1] === 0xd8 && header[2] === 0xff) {
|
|
290
|
+
return "image/jpeg";
|
|
291
|
+
}
|
|
292
|
+
if (header.length >= 6 && (header.subarray(0, 6).toString("ascii") === "GIF87a" || header.subarray(0, 6).toString("ascii") === "GIF89a")) {
|
|
293
|
+
return "image/gif";
|
|
294
|
+
}
|
|
295
|
+
if (header.length >= 12 && header.subarray(0, 4).toString("ascii") === "RIFF" && header.subarray(8, 12).toString("ascii") === "WEBP") {
|
|
296
|
+
return "image/webp";
|
|
297
|
+
}
|
|
298
|
+
if (looksLikeSVGHeader(header)) {
|
|
299
|
+
return "image/svg+xml";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function looksLikeSVGHeader(header) {
|
|
306
|
+
const sample = header.toString("utf8").replace(/^\uFEFF/, "").trimStart().toLowerCase();
|
|
307
|
+
return sample.startsWith("<svg") || (sample.startsWith("<?xml") && sample.includes("<svg"));
|
|
308
|
+
}
|
|
309
|
+
|
|
205
310
|
async function realTemporaryImageRoots() {
|
|
206
311
|
const candidates = [
|
|
207
312
|
os.tmpdir(),
|
|
@@ -210,6 +315,9 @@ async function realTemporaryImageRoots() {
|
|
|
210
315
|
|
|
211
316
|
if (process.platform === "darwin") {
|
|
212
317
|
candidates.push("/tmp");
|
|
318
|
+
candidates.push(path.join(os.homedir(), "Library", "Caches", "com.raycast-x.macos", "clipboard"));
|
|
319
|
+
candidates.push(path.join(os.homedir(), "Library", "Application Support", "CleanShot", "media"));
|
|
320
|
+
candidates.push(path.join(os.homedir(), "Library", "Application Support", "CleanShot X", "media"));
|
|
213
321
|
}
|
|
214
322
|
|
|
215
323
|
const roots = await Promise.all(
|
|
@@ -218,8 +326,8 @@ async function realTemporaryImageRoots() {
|
|
|
218
326
|
return Array.from(new Set(roots.filter(Boolean)));
|
|
219
327
|
}
|
|
220
328
|
|
|
221
|
-
//
|
|
222
|
-
async function
|
|
329
|
+
// Read-only previews can scope non-git Codex scratch folders to cwd while rejecting broad roots.
|
|
330
|
+
async function resolveReadableWorkspaceRoot(cwd) {
|
|
223
331
|
const realRepoRoot = await resolveRepoRoot(cwd).then(realpathOrNull).catch(() => null);
|
|
224
332
|
if (realRepoRoot) {
|
|
225
333
|
return realRepoRoot;
|
|
@@ -232,14 +340,99 @@ async function resolveImageWorkspaceRoot(cwd) {
|
|
|
232
340
|
return realCwd;
|
|
233
341
|
}
|
|
234
342
|
|
|
343
|
+
async function resolveImageWorkspaceRoot(cwd) {
|
|
344
|
+
return resolveReadableWorkspaceRoot(cwd);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Handles assistant links that only include a filename by finding one unique workspace match.
|
|
348
|
+
async function resolveWorkspaceTextFilePath(cwd, requestedPath, realWorkspaceRoot) {
|
|
349
|
+
const filePath = path.isAbsolute(requestedPath)
|
|
350
|
+
? path.resolve(requestedPath)
|
|
351
|
+
: path.resolve(cwd, requestedPath);
|
|
352
|
+
const realFilePath = await realpathOrNull(filePath);
|
|
353
|
+
if (realFilePath || path.isAbsolute(requestedPath) || !realWorkspaceRoot) {
|
|
354
|
+
return realFilePath;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!isBareFileName(requestedPath)) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return resolveUniqueWorkspaceBasenameMatch(realWorkspaceRoot, requestedPath);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function resolveUniqueWorkspaceBasenameMatch(realWorkspaceRoot, requestedFileName) {
|
|
365
|
+
const matches = await findWorkspaceBasenameMatches(realWorkspaceRoot, requestedFileName);
|
|
366
|
+
if (matches.length === 0) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
if (matches.length > 1) {
|
|
370
|
+
throw workspaceError(
|
|
371
|
+
"file_path_ambiguous",
|
|
372
|
+
`Multiple files named "${requestedFileName}" exist in this workspace. Use a path with folders.`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return matches[0];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function findWorkspaceBasenameMatches(realWorkspaceRoot, requestedFileName) {
|
|
379
|
+
const matches = [];
|
|
380
|
+
const output = await git(realWorkspaceRoot, "ls-files", "-co", "--exclude-standard").catch(() => "");
|
|
381
|
+
const relativePaths = output
|
|
382
|
+
.split("\n")
|
|
383
|
+
.map((line) => line.trim())
|
|
384
|
+
.filter(Boolean);
|
|
385
|
+
|
|
386
|
+
for (const relativePath of relativePaths) {
|
|
387
|
+
if (path.basename(relativePath) !== requestedFileName) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const realCandidate = await realpathOrNull(path.resolve(realWorkspaceRoot, relativePath));
|
|
391
|
+
if (realCandidate && isPathInside(realCandidate, realWorkspaceRoot)) {
|
|
392
|
+
matches.push(realCandidate);
|
|
393
|
+
if (matches.length >= MAX_BASENAME_FALLBACK_MATCHES) {
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return matches;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function isBareFileName(candidatePath) {
|
|
402
|
+
return candidatePath === path.basename(candidatePath)
|
|
403
|
+
&& candidatePath !== "."
|
|
404
|
+
&& candidatePath !== "..";
|
|
405
|
+
}
|
|
406
|
+
|
|
235
407
|
function isBroadWorkspaceRoot(candidatePath) {
|
|
236
408
|
const normalized = path.resolve(candidatePath);
|
|
237
409
|
return normalized === path.parse(normalized).root
|
|
238
410
|
|| normalized === path.resolve(os.homedir());
|
|
239
411
|
}
|
|
240
412
|
|
|
413
|
+
function decodeUtf8TextFile(data) {
|
|
414
|
+
const sample = data.subarray(0, Math.min(data.length, BINARY_SNIFF_BYTES));
|
|
415
|
+
if (sample.includes(0)) {
|
|
416
|
+
throw workspaceError("binary_file", "This file looks binary, so it cannot be shown as text.");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
421
|
+
} catch {
|
|
422
|
+
throw workspaceError("unsupported_text_encoding", "Only UTF-8 text files can be viewed.");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function countLines(content) {
|
|
427
|
+
if (!content) {
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
const newlineCount = (content.match(/\n/g) || []).length;
|
|
431
|
+
return content.endsWith("\n") ? newlineCount : newlineCount + 1;
|
|
432
|
+
}
|
|
433
|
+
|
|
241
434
|
async function readPreviewImageData(imagePath, maxPixelDimension, originalByteLength) {
|
|
242
|
-
if (!
|
|
435
|
+
if (!usesNativeImagePreview()) {
|
|
243
436
|
if (originalByteLength <= MAX_IMAGE_PREVIEW_READ_BYTES) {
|
|
244
437
|
return fs.promises.readFile(imagePath);
|
|
245
438
|
}
|
|
@@ -262,7 +455,7 @@ async function readPreviewImageData(imagePath, maxPixelDimension, originalByteLe
|
|
|
262
455
|
}
|
|
263
456
|
|
|
264
457
|
try {
|
|
265
|
-
const previewData = await
|
|
458
|
+
const previewData = await downsampleImageNative(
|
|
266
459
|
imagePath,
|
|
267
460
|
candidateDimension,
|
|
268
461
|
Math.min(IMAGE_PREVIEW_TOOL_TIMEOUT_MS, remainingTimeoutMs)
|
|
@@ -294,6 +487,17 @@ async function readPreviewImageData(imagePath, maxPixelDimension, originalByteLe
|
|
|
294
487
|
);
|
|
295
488
|
}
|
|
296
489
|
|
|
490
|
+
// SVGs are already compact vector source; send them as-is so the phone can render them in WebKit.
|
|
491
|
+
async function readSVGPreviewData(imagePath, originalByteLength) {
|
|
492
|
+
if (originalByteLength > MAX_IMAGE_PREVIEW_READ_BYTES) {
|
|
493
|
+
throw workspaceError(
|
|
494
|
+
"image_too_large",
|
|
495
|
+
"This SVG is too large to send to the phone. Open it on the Mac or move a smaller preview into the workspace."
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return fs.promises.readFile(imagePath);
|
|
499
|
+
}
|
|
500
|
+
|
|
297
501
|
function previewPixelDimensionCandidates(maxPixelDimension) {
|
|
298
502
|
const dimensions = [];
|
|
299
503
|
let next = maxPixelDimension;
|
|
@@ -318,18 +522,118 @@ function previewPixelDimensionCandidates(maxPixelDimension) {
|
|
|
318
522
|
return Array.from(new Set(dimensions)).sort((a, b) => b - a);
|
|
319
523
|
}
|
|
320
524
|
|
|
321
|
-
function
|
|
525
|
+
function usesNativeImagePreview() {
|
|
526
|
+
const normalizedPlatform = String(process.platform || "").trim().toLowerCase();
|
|
527
|
+
return normalizedPlatform === "darwin"
|
|
528
|
+
|| normalizedPlatform === "macos"
|
|
529
|
+
|| normalizedPlatform === "mac"
|
|
530
|
+
|| normalizedPlatform === "win32"
|
|
531
|
+
|| normalizedPlatform === "windows";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function downsampleImageNative(imagePath, maxPixelDimension, timeoutMs = IMAGE_PREVIEW_TOOL_TIMEOUT_MS) {
|
|
535
|
+
return usesWindowsImagePreview()
|
|
536
|
+
? downsampleImageWithPowerShell(imagePath, maxPixelDimension, timeoutMs)
|
|
537
|
+
: downsampleImageWithSips(imagePath, maxPixelDimension, timeoutMs);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function usesWindowsImagePreview() {
|
|
322
541
|
const normalizedPlatform = String(process.platform || "").trim().toLowerCase();
|
|
323
|
-
return normalizedPlatform === "
|
|
542
|
+
return normalizedPlatform === "win32" || normalizedPlatform === "windows";
|
|
324
543
|
}
|
|
325
544
|
|
|
326
545
|
async function downsampleImageWithSips(imagePath, maxPixelDimension, timeoutMs = IMAGE_PREVIEW_TOOL_TIMEOUT_MS) {
|
|
327
546
|
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "remodex-image-preview-"));
|
|
328
547
|
const outputPath = path.join(tempDir, `preview${path.extname(imagePath) || ".png"}`);
|
|
548
|
+
const command = resolveHostCommand("sips");
|
|
549
|
+
try {
|
|
550
|
+
await execFileAsync(command.file, [
|
|
551
|
+
...command.args,
|
|
552
|
+
"-Z",
|
|
553
|
+
String(maxPixelDimension),
|
|
554
|
+
imagePath,
|
|
555
|
+
"--out",
|
|
556
|
+
outputPath,
|
|
557
|
+
], {
|
|
558
|
+
timeout: Math.max(1, Math.floor(timeoutMs)),
|
|
559
|
+
maxBuffer: 1024 * 1024,
|
|
560
|
+
});
|
|
561
|
+
return await fs.promises.readFile(outputPath);
|
|
562
|
+
} finally {
|
|
563
|
+
await fs.promises.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function resolveHostCommand(commandName) {
|
|
568
|
+
if (path.delimiter !== ";") {
|
|
569
|
+
return { file: commandName, args: [] };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const pathEntries = String(process.env.PATH || "")
|
|
573
|
+
.split(path.delimiter)
|
|
574
|
+
.filter(Boolean);
|
|
575
|
+
for (const entry of pathEntries) {
|
|
576
|
+
const jsShim = path.join(entry, `${commandName}.js`);
|
|
577
|
+
if (fs.existsSync(jsShim)) {
|
|
578
|
+
return { file: process.execPath, args: [jsShim] };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return { file: commandName, args: [] };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function downsampleImageWithPowerShell(imagePath, maxPixelDimension, timeoutMs = IMAGE_PREVIEW_TOOL_TIMEOUT_MS) {
|
|
585
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "remodex-image-preview-"));
|
|
586
|
+
const outputPath = path.join(tempDir, `preview${path.extname(imagePath) || ".png"}`);
|
|
587
|
+
const scriptPath = path.join(tempDir, "resize-preview.ps1");
|
|
588
|
+
const script = `
|
|
589
|
+
param(
|
|
590
|
+
[Parameter(Mandatory=$true)][string]$InputPath,
|
|
591
|
+
[Parameter(Mandatory=$true)][string]$OutputPath,
|
|
592
|
+
[Parameter(Mandatory=$true)][int]$MaxDimension
|
|
593
|
+
)
|
|
594
|
+
Add-Type -AssemblyName System.Drawing
|
|
595
|
+
$image = [System.Drawing.Image]::FromFile($InputPath)
|
|
596
|
+
try {
|
|
597
|
+
$longest = [Math]::Max($image.Width, $image.Height)
|
|
598
|
+
if ($longest -le 0) { throw "Invalid image dimensions." }
|
|
599
|
+
$scale = [Math]::Min(1.0, [double]$MaxDimension / [double]$longest)
|
|
600
|
+
$targetWidth = [Math]::Max(1, [int][Math]::Round($image.Width * $scale))
|
|
601
|
+
$targetHeight = [Math]::Max(1, [int][Math]::Round($image.Height * $scale))
|
|
602
|
+
$bitmap = New-Object System.Drawing.Bitmap $targetWidth, $targetHeight
|
|
603
|
+
try {
|
|
604
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
605
|
+
try {
|
|
606
|
+
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
607
|
+
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
|
|
608
|
+
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
|
609
|
+
$graphics.DrawImage($image, 0, 0, $targetWidth, $targetHeight)
|
|
610
|
+
} finally {
|
|
611
|
+
$graphics.Dispose()
|
|
612
|
+
}
|
|
613
|
+
$bitmap.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
614
|
+
} finally {
|
|
615
|
+
if ($bitmap) { $bitmap.Dispose() }
|
|
616
|
+
}
|
|
617
|
+
} finally {
|
|
618
|
+
$image.Dispose()
|
|
619
|
+
}
|
|
620
|
+
`;
|
|
329
621
|
try {
|
|
330
|
-
await
|
|
622
|
+
await fs.promises.writeFile(scriptPath, script, "utf8");
|
|
623
|
+
await execFileAsync("powershell.exe", [
|
|
624
|
+
"-NoProfile",
|
|
625
|
+
"-NonInteractive",
|
|
626
|
+
"-ExecutionPolicy",
|
|
627
|
+
"Bypass",
|
|
628
|
+
"-File",
|
|
629
|
+
scriptPath,
|
|
630
|
+
imagePath,
|
|
631
|
+
outputPath,
|
|
632
|
+
String(maxPixelDimension),
|
|
633
|
+
], {
|
|
331
634
|
timeout: Math.max(1, Math.floor(timeoutMs)),
|
|
332
635
|
maxBuffer: 1024 * 1024,
|
|
636
|
+
windowsHide: true,
|
|
333
637
|
});
|
|
334
638
|
return await fs.promises.readFile(outputPath);
|
|
335
639
|
} finally {
|
|
@@ -357,6 +661,15 @@ function isUnchangedImageRead(params, stat, maxPixelDimension) {
|
|
|
357
661
|
&& cachedMtimeMs === stat.mtimeMs;
|
|
358
662
|
}
|
|
359
663
|
|
|
664
|
+
function isUnchangedTextFileRead(params, stat) {
|
|
665
|
+
const cachedByteLength = Number(params.ifByteLength);
|
|
666
|
+
const cachedMtimeMs = Number(params.ifMtimeMs);
|
|
667
|
+
return Number.isFinite(cachedByteLength)
|
|
668
|
+
&& Number.isFinite(cachedMtimeMs)
|
|
669
|
+
&& cachedByteLength === stat.size
|
|
670
|
+
&& cachedMtimeMs === stat.mtimeMs;
|
|
671
|
+
}
|
|
672
|
+
|
|
360
673
|
// Validates the reverse patch against the current tree without writing repo files.
|
|
361
674
|
async function workspaceRevertPatchPreview(repoRoot, params) {
|
|
362
675
|
const forwardPatch = resolveForwardPatch(params);
|