- skills/archify: vendored tt-a1i/archify 2.14.0 (MIT), pure Node 18+, zero deps - extensions/pi-mermaid: renders mermaid blocks as ASCII in TUI; deps beautiful-mermaid + mermaid - skills/project-diagramming-*: orchestration skills, per-project docs/ convention
87 lines
2.2 KiB
JavaScript
87 lines
2.2 KiB
JavaScript
import { spawnSync } from 'node:child_process';
|
|
import path from 'node:path';
|
|
|
|
const OPENERS = {
|
|
darwin: {
|
|
command: 'open',
|
|
method: 'open',
|
|
args: (target) => [target],
|
|
},
|
|
linux: {
|
|
command: 'xdg-open',
|
|
method: 'xdg-open',
|
|
args: (target) => [target],
|
|
},
|
|
win32: {
|
|
command: 'powershell.exe',
|
|
method: 'powershell',
|
|
// Keep the command constant and pass the target through PowerShell's
|
|
// argument array. Paths are never interpolated into executable source.
|
|
args: (target) => [
|
|
'-NoProfile',
|
|
'-NonInteractive',
|
|
'-Command',
|
|
'Start-Process -FilePath $args[0]',
|
|
target,
|
|
],
|
|
},
|
|
};
|
|
|
|
function launchTarget(target, options = {}) {
|
|
const platform = options.platform || process.platform;
|
|
const opener = OPENERS[platform];
|
|
if (!opener) {
|
|
return {
|
|
requested: true,
|
|
status: 'unsupported',
|
|
target,
|
|
method: null,
|
|
};
|
|
}
|
|
|
|
const spawn = options.spawn || spawnSync;
|
|
let result;
|
|
try {
|
|
result = spawn(opener.command, opener.args(target), {
|
|
encoding: 'utf8',
|
|
shell: false,
|
|
stdio: 'ignore',
|
|
timeout: options.timeoutMs || 5000,
|
|
windowsHide: true,
|
|
});
|
|
} catch {
|
|
result = { error: new Error('opener threw') };
|
|
}
|
|
|
|
let status = 'opened';
|
|
if (result?.error?.code === 'ENOENT') status = 'unsupported';
|
|
else if (result?.error || result?.signal || result?.status !== 0) status = 'failed';
|
|
|
|
return {
|
|
requested: true,
|
|
status,
|
|
target,
|
|
method: opener.method,
|
|
};
|
|
}
|
|
|
|
export function openArtifact(target, options = {}) {
|
|
return launchTarget(path.resolve(target), options);
|
|
}
|
|
|
|
export function openLoopbackUrl(target, options = {}) {
|
|
let url;
|
|
try {
|
|
url = new URL(target);
|
|
} catch {
|
|
throw new TypeError('Preview URL must be a valid loopback HTTP URL.');
|
|
}
|
|
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port) {
|
|
throw new TypeError('Preview URL must be a loopback URL using http://127.0.0.1:<port>.');
|
|
}
|
|
if (url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
|
|
throw new TypeError('Preview URL must target the loopback preview root.');
|
|
}
|
|
return launchTarget(url.href, options);
|
|
}
|