@fitlab-ai/agent-infra 0.8.1 → 0.8.3

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 (131) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/bin/cli.ts +3 -3
  4. package/dist/bin/cli.js +4 -4
  5. package/dist/lib/cp.js +1 -1
  6. package/dist/lib/decide.js +2 -2
  7. package/dist/lib/defaults.json +2 -0
  8. package/dist/lib/init.js +7 -7
  9. package/dist/lib/merge.js +1 -1
  10. package/dist/lib/prompt.js +1 -1
  11. package/dist/lib/run/index.js +7 -7
  12. package/dist/lib/run/prompt.js +1 -1
  13. package/dist/lib/sandbox/capture.js +5 -5
  14. package/dist/lib/sandbox/clipboard/bridge.js +48 -13
  15. package/dist/lib/sandbox/clipboard/index.js +5 -6
  16. package/dist/lib/sandbox/clipboard/linux.js +91 -0
  17. package/dist/lib/sandbox/clipboard/paths.js +1 -1
  18. package/dist/lib/sandbox/clipboard/win32.js +144 -0
  19. package/dist/lib/sandbox/commands/create.js +101 -66
  20. package/dist/lib/sandbox/commands/enter.js +9 -9
  21. package/dist/lib/sandbox/commands/list-running.js +2 -2
  22. package/dist/lib/sandbox/commands/ls.js +7 -7
  23. package/dist/lib/sandbox/commands/prune.js +8 -8
  24. package/dist/lib/sandbox/commands/rebuild.js +38 -50
  25. package/dist/lib/sandbox/commands/refresh.js +2 -2
  26. package/dist/lib/sandbox/commands/rm.js +11 -11
  27. package/dist/lib/sandbox/commands/show.js +4 -4
  28. package/dist/lib/sandbox/commands/start.js +4 -4
  29. package/dist/lib/sandbox/commands/vm.js +4 -4
  30. package/dist/lib/sandbox/config.js +11 -5
  31. package/dist/lib/sandbox/constants.js +4 -1
  32. package/dist/lib/sandbox/credentials.js +1 -1
  33. package/dist/lib/sandbox/dotfiles.js +1 -1
  34. package/dist/lib/sandbox/engine.js +3 -3
  35. package/dist/lib/sandbox/engines/index.js +5 -5
  36. package/dist/lib/sandbox/engines/wsl2-paths.js +1 -1
  37. package/dist/lib/sandbox/image-build.js +80 -0
  38. package/dist/lib/sandbox/image-prune.js +2 -2
  39. package/dist/lib/sandbox/index.js +10 -10
  40. package/dist/lib/sandbox/managed-fs.js +1 -1
  41. package/dist/lib/sandbox/readme-scaffold.js +1 -1
  42. package/dist/lib/sandbox/runtimes/node20.dockerfile +1 -1
  43. package/dist/lib/sandbox/runtimes/node22.dockerfile +1 -1
  44. package/dist/lib/sandbox/tools.js +11 -2
  45. package/dist/lib/server/adapters/feishu/index.js +12 -3
  46. package/dist/lib/server/adapters/feishu/renderer.js +88 -0
  47. package/dist/lib/server/adapters/feishu/transport.js +9 -2
  48. package/dist/lib/server/daemon.js +64 -43
  49. package/dist/lib/server/display.js +83 -0
  50. package/dist/lib/server/index.js +2 -2
  51. package/dist/lib/server/process-control.js +2 -2
  52. package/dist/lib/server/protocol.js +22 -6
  53. package/dist/lib/server/streamer.js +5 -4
  54. package/dist/lib/task/commands/cat.js +2 -2
  55. package/dist/lib/task/commands/decisions.js +4 -4
  56. package/dist/lib/task/commands/files.js +3 -3
  57. package/dist/lib/task/commands/grep.js +3 -3
  58. package/dist/lib/task/commands/issue-body.js +4 -4
  59. package/dist/lib/task/commands/log.js +5 -5
  60. package/dist/lib/task/commands/ls.js +3 -3
  61. package/dist/lib/task/commands/show.js +1 -1
  62. package/dist/lib/task/commands/status.js +71 -25
  63. package/dist/lib/task/index.js +9 -9
  64. package/dist/lib/task/resolve-ref.js +1 -1
  65. package/dist/lib/task/workflow-warnings.js +94 -0
  66. package/dist/lib/update.js +35 -4
  67. package/lib/defaults.json +2 -0
  68. package/lib/sandbox/clipboard/bridge.ts +50 -8
  69. package/lib/sandbox/clipboard/index.ts +5 -6
  70. package/lib/sandbox/clipboard/linux.ts +124 -0
  71. package/lib/sandbox/clipboard/win32.ts +173 -0
  72. package/lib/sandbox/commands/create.ts +123 -65
  73. package/lib/sandbox/commands/rebuild.ts +42 -54
  74. package/lib/sandbox/config.ts +14 -2
  75. package/lib/sandbox/constants.ts +4 -0
  76. package/lib/sandbox/image-build.ts +134 -0
  77. package/lib/sandbox/runtimes/node20.dockerfile +1 -1
  78. package/lib/sandbox/runtimes/node22.dockerfile +1 -1
  79. package/lib/sandbox/tools.ts +9 -0
  80. package/lib/server/adapters/_contract.ts +3 -0
  81. package/lib/server/adapters/feishu/index.ts +12 -2
  82. package/lib/server/adapters/feishu/renderer.ts +99 -0
  83. package/lib/server/adapters/feishu/transport.ts +19 -3
  84. package/lib/server/daemon.ts +74 -38
  85. package/lib/server/display.ts +136 -0
  86. package/lib/server/protocol.ts +20 -4
  87. package/lib/server/streamer.ts +5 -4
  88. package/lib/task/commands/log.ts +3 -3
  89. package/lib/task/commands/status.ts +102 -21
  90. package/lib/task/workflow-warnings.ts +121 -0
  91. package/lib/update.ts +34 -0
  92. package/package.json +3 -3
  93. package/templates/.agents/README.en.md +3 -3
  94. package/templates/.agents/README.zh-CN.md +3 -3
  95. package/templates/.agents/rules/create-issue.github.en.md +3 -3
  96. package/templates/.agents/rules/create-issue.github.zh-CN.md +3 -3
  97. package/templates/.agents/rules/issue-pr-commands.github.en.md +7 -2
  98. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +7 -2
  99. package/templates/.agents/rules/issue-sync.github.en.md +13 -0
  100. package/templates/.agents/rules/issue-sync.github.zh-CN.md +13 -0
  101. package/templates/.agents/rules/next-step-output.en.md +15 -1
  102. package/templates/.agents/rules/next-step-output.zh-CN.md +15 -1
  103. package/templates/.agents/rules/pr-sync.github.en.md +17 -2
  104. package/templates/.agents/rules/pr-sync.github.zh-CN.md +17 -2
  105. package/templates/.agents/rules/task-management.en.md +1 -1
  106. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  107. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +57 -0
  108. package/templates/.agents/scripts/validate-artifact.js +120 -0
  109. package/templates/.agents/scripts/workflow-warnings.js +290 -0
  110. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +118 -0
  111. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +118 -0
  112. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +49 -0
  113. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +49 -0
  114. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +48 -0
  115. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +48 -0
  116. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +60 -0
  117. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +87 -0
  118. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  119. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -0
  120. package/templates/.agents/skills/create-task/SKILL.en.md +4 -1
  121. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -1
  122. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +33 -1
  123. package/templates/.agents/templates/task.en.md +7 -0
  124. package/templates/.agents/templates/task.zh-CN.md +7 -0
  125. package/templates/.claude/commands/complete-manual-validation.en.md +9 -0
  126. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +9 -0
  127. package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +8 -0
  128. package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +8 -0
  129. package/templates/.github/workflows/metadata-sync.yml +5 -0
  130. package/templates/.opencode/commands/complete-manual-validation.en.md +11 -0
  131. package/templates/.opencode/commands/complete-manual-validation.zh-CN.md +11 -0
@@ -30,8 +30,13 @@ type BridgeOptions = {
30
30
  createDetector?: () => CtrlVDetector;
31
31
  };
32
32
 
33
+ type TextImageClipboardAdapter = ClipboardAdapter & {
34
+ readImageFromText?: (text: string) => Buffer | null;
35
+ };
36
+
33
37
  const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
34
38
  const PARTIAL_ESCAPE_FLUSH_MS = 30;
39
+ const BRACKETED_PASTE_RE = /^\x1b\[200~([\s\S]*)\x1b\[201~$/u;
35
40
 
36
41
  // Node's stdin.setRawMode(true) uses libuv's RAW mode, which (unlike the
37
42
  // cfmakeraw that `docker exec -it` applies on the non-bridge path) keeps ONLCR
@@ -147,7 +152,7 @@ async function runBridge({
147
152
  clearFlushTimer();
148
153
  for (const token of detector.feed(inputDecoder.write(chunk))) {
149
154
  if (token.kind === 'text') {
150
- child.write(token.raw);
155
+ handleText(token.raw, child);
151
156
  } else {
152
157
  handleCtrlV(token, child);
153
158
  }
@@ -157,7 +162,7 @@ async function runBridge({
157
162
  flushTimer = null;
158
163
  for (const token of detector.flush()) {
159
164
  if (token.kind === 'text') {
160
- child.write(token.raw);
165
+ handleText(token.raw, child);
161
166
  } else {
162
167
  handleCtrlV(token, child);
163
168
  }
@@ -169,6 +174,37 @@ async function runBridge({
169
174
  const onSigint = () => child.kill('SIGINT');
170
175
  const onSigterm = () => child.kill('SIGTERM');
171
176
 
177
+ function writeImagePaste(png: Buffer, target: PtyProcess): void {
178
+ const filename = pngClipboardFilename(png);
179
+ writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
180
+ pruneClipboardDir(clipboardHostDir(home));
181
+ target.write(buildBracketedPaste(containerClipboardPath(filename)));
182
+ }
183
+
184
+ function handleText(raw: string, target: PtyProcess): void {
185
+ const textAdapter = adapter as TextImageClipboardAdapter;
186
+ const pastedText = extractSinglePathPaste(raw);
187
+ if (!textAdapter.readImageFromText || pastedText === null) {
188
+ target.write(raw);
189
+ return;
190
+ }
191
+
192
+ try {
193
+ const png = textAdapter.readImageFromText(pastedText);
194
+ if (!png) {
195
+ target.write(raw);
196
+ return;
197
+ }
198
+ writeImagePaste(png, target);
199
+ } catch (error) {
200
+ target.write(raw);
201
+ if (!warnedPasteFailure) {
202
+ warnedPasteFailure = true;
203
+ writeStderr(`Warning: clipboard image paste failed; forwarded pasted text (${error instanceof Error ? error.message : 'unknown error'})\n`);
204
+ }
205
+ }
206
+ }
207
+
172
208
  function handleCtrlV(match: CtrlVMatch, target: PtyProcess): void {
173
209
  try {
174
210
  // readImagePng returns null both for "no image on clipboard" and for
@@ -181,10 +217,7 @@ async function runBridge({
181
217
  target.write(match.raw);
182
218
  return;
183
219
  }
184
- const filename = pngClipboardFilename(png);
185
- writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
186
- pruneClipboardDir(clipboardHostDir(home));
187
- target.write(buildBracketedPaste(containerClipboardPath(filename)));
220
+ writeImagePaste(png, target);
188
221
  } catch (error) {
189
222
  target.write(match.raw);
190
223
  if (!warnedPasteFailure) {
@@ -219,14 +252,14 @@ async function runBridge({
219
252
  try {
220
253
  for (const token of detector.feed(inputDecoder.end())) {
221
254
  if (token.kind === 'text') {
222
- child.write(token.raw);
255
+ handleText(token.raw, child);
223
256
  } else {
224
257
  handleCtrlV(token, child);
225
258
  }
226
259
  }
227
260
  for (const token of detector.flush()) {
228
261
  if (token.kind === 'text') {
229
- child.write(token.raw);
262
+ handleText(token.raw, child);
230
263
  }
231
264
  }
232
265
  } catch {
@@ -244,6 +277,15 @@ async function runBridge({
244
277
  }
245
278
  }
246
279
 
280
+ function extractSinglePathPaste(raw: string): string | null {
281
+ const match = raw.match(BRACKETED_PASTE_RE);
282
+ const text = (match ? match[1] : raw)?.trim();
283
+ if (!text || /[\r\n]/u.test(text)) {
284
+ return null;
285
+ }
286
+ return text;
287
+ }
288
+
247
289
  function onceExit(
248
290
  child: PtyProcess,
249
291
  stdin: NodeJS.ReadStream
@@ -1,7 +1,9 @@
1
1
  import { platform } from 'node:os';
2
2
  import { createDarwinClipboardAdapter, type DarwinClipboardAdapter } from './darwin.ts';
3
+ import { createLinuxClipboardAdapter, type LinuxClipboardAdapter } from './linux.ts';
4
+ import { createWin32ClipboardAdapter, type Win32ClipboardAdapter } from './win32.ts';
3
5
 
4
- export type ClipboardAdapter = DarwinClipboardAdapter;
6
+ export type ClipboardAdapter = DarwinClipboardAdapter | LinuxClipboardAdapter | Win32ClipboardAdapter;
5
7
 
6
8
  export function createClipboardAdapter({
7
9
  platformName = platform()
@@ -10,12 +12,9 @@ export function createClipboardAdapter({
10
12
  case 'darwin':
11
13
  return createDarwinClipboardAdapter();
12
14
  case 'linux':
13
- // Future work: dispatch based on $WAYLAND_DISPLAY (wl-paste) or $DISPLAY (xclip);
14
- // see Issue #386 follow-up. Returning null disables the bridge for now.
15
- return null;
15
+ return createLinuxClipboardAdapter();
16
16
  case 'win32':
17
- // Future work: native Win32 clipboard reader. Returning null disables the bridge.
18
- return null;
17
+ return createWin32ClipboardAdapter();
19
18
  default:
20
19
  return null;
21
20
  }
@@ -0,0 +1,124 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import type { ExecFileSyncOptions } from 'node:child_process';
6
+
7
+ const PROBE_TIMEOUT_MS = 2_000;
8
+ const READ_IMAGE_TIMEOUT_MS = 5_000;
9
+ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
10
+
11
+ type ExecFn = (cmd: string, args: string[], options?: ExecFileSyncOptions) => Buffer | string;
12
+ type ExecToFileFn = (cmd: string, args: string[], outputPath: string, timeout: number) => void;
13
+
14
+ type LinuxBackend =
15
+ | { kind: 'wayland'; command: 'wl-paste' }
16
+ | { kind: 'x11'; command: 'xclip' };
17
+
18
+ export type LinuxClipboardAdapter = {
19
+ available(): { ok: true } | { ok: false; reason: string };
20
+ readImagePng(): Buffer | null;
21
+ };
22
+
23
+ export function createLinuxClipboardAdapter({
24
+ env = process.env,
25
+ execFn = execFileSync,
26
+ execToFileFn = execToFile,
27
+ mkdtempFn = fs.mkdtempSync,
28
+ readFileFn = fs.readFileSync,
29
+ rmFn = fs.rmSync
30
+ }: {
31
+ env?: NodeJS.ProcessEnv;
32
+ execFn?: ExecFn;
33
+ execToFileFn?: ExecToFileFn;
34
+ mkdtempFn?: typeof fs.mkdtempSync;
35
+ readFileFn?: typeof fs.readFileSync;
36
+ rmFn?: typeof fs.rmSync;
37
+ } = {}): LinuxClipboardAdapter {
38
+ return {
39
+ available() {
40
+ const backend = selectBackend(env);
41
+ try {
42
+ execFn(backend.command, versionArgs(backend), {
43
+ encoding: 'utf8',
44
+ timeout: PROBE_TIMEOUT_MS
45
+ });
46
+ return { ok: true };
47
+ } catch {
48
+ return { ok: false, reason: unavailableReason(backend) };
49
+ }
50
+ },
51
+ readImagePng() {
52
+ const backend = selectBackend(env);
53
+ try {
54
+ const mimeTypes = String(execFn(backend.command, mimeArgs(backend), {
55
+ encoding: 'utf8',
56
+ timeout: PROBE_TIMEOUT_MS
57
+ }));
58
+ if (!hasPngMime(mimeTypes)) {
59
+ return null;
60
+ }
61
+
62
+ const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
63
+ const outputPath = path.join(tmpDir, 'clipboard.png');
64
+ try {
65
+ execToFileFn(backend.command, imageArgs(backend), outputPath, READ_IMAGE_TIMEOUT_MS);
66
+ const png = Buffer.from(readFileFn(outputPath));
67
+ return isPng(png) ? png : null;
68
+ } finally {
69
+ rmFn(tmpDir, { recursive: true, force: true });
70
+ }
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ };
76
+ }
77
+
78
+ function selectBackend(env: NodeJS.ProcessEnv): LinuxBackend {
79
+ return env.WAYLAND_DISPLAY?.trim()
80
+ ? { kind: 'wayland', command: 'wl-paste' }
81
+ : { kind: 'x11', command: 'xclip' };
82
+ }
83
+
84
+ function versionArgs(backend: LinuxBackend): string[] {
85
+ return backend.kind === 'wayland' ? ['--version'] : ['-version'];
86
+ }
87
+
88
+ function mimeArgs(backend: LinuxBackend): string[] {
89
+ return backend.kind === 'wayland'
90
+ ? ['--list-types']
91
+ : ['-selection', 'clipboard', '-t', 'TARGETS', '-o'];
92
+ }
93
+
94
+ function imageArgs(backend: LinuxBackend): string[] {
95
+ return backend.kind === 'wayland'
96
+ ? ['-t', 'image/png']
97
+ : ['-selection', 'clipboard', '-t', 'image/png', '-o'];
98
+ }
99
+
100
+ function unavailableReason(backend: LinuxBackend): string {
101
+ return backend.kind === 'wayland'
102
+ ? 'Wayland clipboard tool wl-paste is unavailable; install wl-clipboard to enable image paste'
103
+ : 'X11 clipboard tool xclip is unavailable; install xclip to enable image paste';
104
+ }
105
+
106
+ function hasPngMime(output: string): boolean {
107
+ return output.split(/\s+/u).some((type) => type === 'image/png');
108
+ }
109
+
110
+ function execToFile(cmd: string, args: string[], outputPath: string, timeout: number): void {
111
+ const fd = fs.openSync(outputPath, 'w', 0o600);
112
+ try {
113
+ execFileSync(cmd, args, {
114
+ timeout,
115
+ stdio: ['ignore', fd, 'pipe']
116
+ });
117
+ } finally {
118
+ fs.closeSync(fd);
119
+ }
120
+ }
121
+
122
+ function isPng(buffer: Buffer): boolean {
123
+ return buffer.length >= PNG_MAGIC.length && PNG_MAGIC.every((byte, index) => buffer[index] === byte);
124
+ }
@@ -0,0 +1,173 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import type { ExecFileSyncOptions } from 'node:child_process';
6
+
7
+ const PROBE_TIMEOUT_MS = 2_000;
8
+ const READ_IMAGE_TIMEOUT_MS = 5_000;
9
+ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
10
+ const POWERSHELL_ARGS = ['-NoProfile', '-NonInteractive', '-STA', '-Command'];
11
+
12
+ type ExecFn = (cmd: string, args: string[], options?: ExecFileSyncOptions) => Buffer | string;
13
+ type MkdtempFn = (prefix: string) => string;
14
+ type ReadFileFn = (filePath: fs.PathOrFileDescriptor) => Buffer | string;
15
+ type RmFn = (filePath: fs.PathLike, options: { recursive?: boolean; force?: boolean }) => void;
16
+ type ExistsFn = (filePath: fs.PathLike) => boolean;
17
+
18
+ export type Win32ClipboardAdapter = {
19
+ available(): { ok: true } | { ok: false; reason: string };
20
+ readImagePng(): Buffer | null;
21
+ readImageFromPath(imagePath: string): Buffer | null;
22
+ readImageFromText(text: string): Buffer | null;
23
+ };
24
+
25
+ export function createWin32ClipboardAdapter({
26
+ execFn = execFileSync,
27
+ mkdtempFn = fs.mkdtempSync,
28
+ readFileFn = fs.readFileSync,
29
+ rmFn = fs.rmSync,
30
+ existsFn = fs.existsSync
31
+ }: {
32
+ execFn?: ExecFn;
33
+ mkdtempFn?: MkdtempFn;
34
+ readFileFn?: ReadFileFn;
35
+ rmFn?: RmFn;
36
+ existsFn?: ExistsFn;
37
+ } = {}): Win32ClipboardAdapter {
38
+ return {
39
+ available() {
40
+ try {
41
+ execFn('powershell.exe', [...POWERSHELL_ARGS, clipboardProbeScript()], {
42
+ encoding: 'utf8',
43
+ timeout: PROBE_TIMEOUT_MS
44
+ });
45
+ return { ok: true };
46
+ } catch {
47
+ return {
48
+ ok: false,
49
+ reason: 'Windows PowerShell Get-Clipboard is unavailable; install or enable powershell.exe to use image paste'
50
+ };
51
+ }
52
+ },
53
+ readImagePng() {
54
+ const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
55
+ const outputPath = path.join(tmpDir, 'clipboard.png');
56
+ try {
57
+ execFn('powershell.exe', [...POWERSHELL_ARGS, pngWriteScript(outputPath)], {
58
+ encoding: 'utf8',
59
+ timeout: READ_IMAGE_TIMEOUT_MS
60
+ });
61
+ const png = Buffer.from(readFileFn(outputPath));
62
+ return isPng(png) ? png : null;
63
+ } catch {
64
+ return null;
65
+ } finally {
66
+ rmFn(tmpDir, { recursive: true, force: true });
67
+ }
68
+ },
69
+ readImageFromPath(imagePath) {
70
+ const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
71
+ const outputPath = path.join(tmpDir, 'clipboard.png');
72
+ try {
73
+ execFn('powershell.exe', [...POWERSHELL_ARGS, pngFromPathScript(imagePath, outputPath)], {
74
+ encoding: 'utf8',
75
+ timeout: READ_IMAGE_TIMEOUT_MS
76
+ });
77
+ const png = Buffer.from(readFileFn(outputPath));
78
+ return isPng(png) ? png : null;
79
+ } catch {
80
+ return null;
81
+ } finally {
82
+ rmFn(tmpDir, { recursive: true, force: true });
83
+ }
84
+ },
85
+ readImageFromText(text) {
86
+ const trimmed = text.trim();
87
+ if (!isWindowsImagePath(trimmed)) {
88
+ return null;
89
+ }
90
+ if (!existsFn(trimmed)) {
91
+ return null;
92
+ }
93
+ return this.readImageFromPath(trimmed);
94
+ }
95
+ };
96
+ }
97
+
98
+ function clipboardProbeScript(): string {
99
+ return 'if ($null -eq (Get-Command Get-Clipboard -ErrorAction SilentlyContinue)) { exit 1 }';
100
+ }
101
+
102
+ // Embed a filesystem path in a PowerShell single-quoted string literal.
103
+ // Single-quoted strings in PowerShell are verbatim (no variable expansion,
104
+ // no escape sequences). The only special character is a single quote itself,
105
+ // which is escaped by doubling it.
106
+ function psLiteral(value: string): string {
107
+ return `'${value.replace(/'/g, "''")}'`;
108
+ }
109
+
110
+ function pngWriteScript(outputPath: string): string {
111
+ const psOutputPath = psLiteral(outputPath);
112
+ return [
113
+ '$ErrorActionPreference = "Stop"',
114
+ `$outputPath = ${psOutputPath}`,
115
+ '$image = $null',
116
+ 'try {',
117
+ ' Add-Type -AssemblyName System.Drawing',
118
+ ' $image = Get-Clipboard -Format Image -ErrorAction SilentlyContinue',
119
+ ' if ($null -ne $image) {',
120
+ ' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
121
+ ' return',
122
+ ' }',
123
+ ' $files = Get-Clipboard -Format FileDropList -ErrorAction SilentlyContinue',
124
+ ' if ($null -ne $files) {',
125
+ ' $allowed = @(".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff", ".tif", ".ico")',
126
+ ' foreach ($file in $files) {',
127
+ ' if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { continue }',
128
+ ' $ext = [System.IO.Path]::GetExtension($file)',
129
+ ' if ($allowed -notcontains $ext) { continue }',
130
+ ' try {',
131
+ ' $image = [System.Drawing.Image]::FromFile($file)',
132
+ ' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
133
+ ' return',
134
+ ' } catch {}',
135
+ ' }',
136
+ ' }',
137
+ ' throw "clipboard has no image"',
138
+ '} finally {',
139
+ ' if ($null -ne $image -and $image -is [System.IDisposable]) { $image.Dispose() }',
140
+ '}'
141
+ ].join('\n');
142
+ }
143
+
144
+ function pngFromPathScript(imagePath: string, outputPath: string): string {
145
+ const psImagePath = psLiteral(imagePath);
146
+ const psOutputPath = psLiteral(outputPath);
147
+ return [
148
+ '$ErrorActionPreference = "Stop"',
149
+ `$imagePath = ${psImagePath}`,
150
+ `$outputPath = ${psOutputPath}`,
151
+ '$image = $null',
152
+ 'try {',
153
+ ' Add-Type -AssemblyName System.Drawing',
154
+ ' if (-not (Test-Path -LiteralPath $imagePath -PathType Leaf)) { throw "file not found: $imagePath" }',
155
+ ' $image = [System.Drawing.Image]::FromFile($imagePath)',
156
+ ' $image.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)',
157
+ '} finally {',
158
+ ' if ($null -ne $image -and $image -is [System.IDisposable]) { $image.Dispose() }',
159
+ '}'
160
+ ].join('\n');
161
+ }
162
+
163
+ const IMAGE_EXTENSIONS_RE = /\.(png|jpg|jpeg|gif|bmp|webp|tiff?|ico)$/i;
164
+
165
+ function isWindowsImagePath(text: string): boolean {
166
+ // Must be a Windows absolute path: drive letter + colon + backslash/slash
167
+ // followed by a path ending with a known image extension.
168
+ return /^[A-Za-z]:[\\/].+$/.test(text) && IMAGE_EXTENSIONS_RE.test(text);
169
+ }
170
+
171
+ function isPng(buffer: Buffer): boolean {
172
+ return buffer.length >= PNG_MAGIC.length && PNG_MAGIC.every((byte, index) => buffer[index] === byte);
173
+ }