cc-viewer 1.9.16 → 1.9.18

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.
@@ -0,0 +1,66 @@
1
+ // Shared OS-integration helpers (open in file manager / terminal) used by both
2
+ // the project-scoped routes (files-fs.js) and the target-mode write routes
3
+ // (target-files-fs.js). Centralizing the platform branches keeps the two call
4
+ // surfaces from drifting apart (a Windows quoting or Linux terminal-fallback fix
5
+ // must apply to both). Pure argv spawning — no shell — so no metachar injection.
6
+ //
7
+ // L1 (lib): must NOT import from routes/ (verify:boundaries).
8
+ import { execFile, spawn } from 'node:child_process';
9
+ import { dirname } from 'node:path';
10
+ import { reportSwallowed } from './error-report.js';
11
+
12
+ // Reveal a file (select it) or open a directory in the OS file manager.
13
+ // - macOS: `open -R <path>` selects files AND opens directories in place.
14
+ // - Windows: `explorer /select,"<path>"` (verbatim args; a dir path opens it).
15
+ // - Linux: no select concept — open the path itself when it is a directory,
16
+ // otherwise its parent. (xdg-open on a FILE would launch the default app, so
17
+ // callers pass `isDir` to pick the directory to show.)
18
+ export function revealInOs(fullPath, { isDir = false } = {}) {
19
+ const plat = process.platform;
20
+ if (plat === 'darwin') {
21
+ execFile('open', ['-R', fullPath], () => {});
22
+ } else if (plat === 'win32') {
23
+ // windowsVerbatimArguments: Node would otherwise re-quote the spaced path into
24
+ // a form explorer cannot parse. No cmd.exe is involved, so no metachar risk;
25
+ // Windows filenames cannot contain `"`.
26
+ const child = spawn('explorer.exe', [`/select,"${fullPath}"`], { windowsVerbatimArguments: true, windowsHide: true });
27
+ child.on('error', (err) => { reportSwallowed('osOpen.reveal', err); });
28
+ } else {
29
+ execFile('xdg-open', [isDir ? fullPath : dirname(fullPath)], () => {});
30
+ }
31
+ }
32
+
33
+ // Open an OS terminal with cwd = dir. Linux probes common emulators with piped
34
+ // stdio so the next candidate is tried after ENOENT, falling back to xdg-open.
35
+ export function openTerminalAt(dir) {
36
+ const plat = process.platform;
37
+ if (plat === 'darwin') {
38
+ const child = spawn('open', ['-a', 'Terminal', dir], { stdio: 'ignore', detached: true });
39
+ child.on('error', (err) => { reportSwallowed('osOpen.terminal', err, { terminal: 'Terminal.app' }); });
40
+ child.unref();
41
+ } else if (plat === 'win32') {
42
+ const child = spawn('cmd.exe', ['/c', 'start', 'cmd.exe'], { cwd: dir, stdio: 'ignore', detached: true, windowsHide: true });
43
+ child.on('error', (err) => { reportSwallowed('osOpen.terminal', err, { terminal: 'cmd.exe' }); });
44
+ child.unref();
45
+ } else {
46
+ const terminals = ['gnome-terminal', 'konsole', 'xfce4-terminal', 'xterm'];
47
+ const tryTerminal = (index) => {
48
+ if (index >= terminals.length) {
49
+ const child = spawn('xdg-open', [dir], { stdio: 'ignore', detached: true });
50
+ child.on('error', (err) => { reportSwallowed('osOpen.terminal', err, { terminal: 'xdg-open' }); });
51
+ child.unref();
52
+ return;
53
+ }
54
+ const term = terminals[index];
55
+ let child;
56
+ try {
57
+ if (term === 'gnome-terminal') child = spawn(term, ['--working-directory=' + dir], { stdio: 'pipe' });
58
+ else if (term === 'konsole') child = spawn(term, ['--workdir', dir], { stdio: 'pipe' });
59
+ else child = spawn(term, [], { cwd: dir, stdio: 'pipe' });
60
+ } catch { tryTerminal(index + 1); return; }
61
+ child.once('error', (err) => { reportSwallowed('osOpen.terminal', err, { terminal: term }); tryTerminal(index + 1); });
62
+ child.once('spawn', () => { for (const s of [child.stdin, child.stdout, child.stderr]) s?.destroy(); child.unref(); });
63
+ };
64
+ tryTerminal(0);
65
+ }
66
+ }
@@ -33,6 +33,13 @@ const HOOK_TYPES = {
33
33
  onStreamChunk: 'parallel',
34
34
  };
35
35
 
36
+ // Approval hooks gate an interactive UI modal, so a hung plugin must not park the approval
37
+ // forever: each invocation times out and falls back to the normal approval modal.
38
+ // Non-approval waterfall hooks (beforeRequest/httpsOptions/localUrl) are startup/auth-critical
39
+ // and stay UNTIMED.
40
+ const APPROVAL_HOOK_TIMEOUT_MS = 15 * 1000;
41
+ const APPROVAL_HOOKS = new Set(['onPlanRequest', 'onAskRequest', 'onPermRequest']);
42
+
36
43
  let _plugins = [];
37
44
  // Full result of the last loadPlugins (directory service/panel plugins and failures included)
38
45
  let _entries = [];
@@ -312,14 +319,39 @@ export async function loadPlugins(opts = {}) {
312
319
 
313
320
  /**
314
321
  * waterfall hook:串行管道执行,前一个的返回值传给下一个
322
+ * `timeoutMsOverride` is test-only; approval hooks default to APPROVAL_HOOK_TIMEOUT_MS.
315
323
  */
316
- export async function runWaterfallHook(name, initialValue) {
324
+ export async function runWaterfallHook(name, initialValue, timeoutMsOverride) {
317
325
  let value = initialValue;
326
+ // Only approval hooks are timed out; the override is ignored for the other hooks.
327
+ const timeoutMs = APPROVAL_HOOKS.has(name)
328
+ ? (timeoutMsOverride ?? APPROVAL_HOOK_TIMEOUT_MS)
329
+ : undefined;
318
330
  for (const plugin of _plugins) {
319
331
  const hookFn = plugin.hooks[name];
320
332
  if (typeof hookFn !== 'function') continue;
321
333
  try {
322
- const result = await hookFn(value);
334
+ let result;
335
+ if (timeoutMs !== undefined) {
336
+ // A timed-out hook promise itself is not cancellable (bounded leak: it may keep
337
+ // running in the background); the timer is cleared as soon as the hook settles.
338
+ let timer;
339
+ try {
340
+ result = await Promise.race([
341
+ Promise.resolve().then(() => hookFn(value)),
342
+ new Promise((_, reject) => {
343
+ timer = setTimeout(
344
+ () => reject(new Error(`hook timed out after ${timeoutMs} ms`)),
345
+ timeoutMs
346
+ );
347
+ }),
348
+ ]);
349
+ } finally {
350
+ clearTimeout(timer);
351
+ }
352
+ } else {
353
+ result = await hookFn(value);
354
+ }
323
355
  if (result != null && typeof result === 'object') {
324
356
  value = { ...value, ...result };
325
357
  }