fauxnix-cli 0.9.3 → 0.12.0

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,129 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { win32 } from 'node:path';
3
+ export const POWERSHELL_ARGS = [
4
+ '-NoProfile',
5
+ '-NonInteractive',
6
+ '-ExecutionPolicy',
7
+ 'Bypass',
8
+ ];
9
+ function envValue(env, name) {
10
+ const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
11
+ return key === undefined ? undefined : env[key];
12
+ }
13
+ function comparableWindowsPath(value) {
14
+ return win32.normalize(value).replace(/[\\/]+$/, '').toLowerCase();
15
+ }
16
+ function isFullyQualifiedWindowsPath(value) {
17
+ return /^[a-z]:[\\/]/i.test(value) || /^\\\\[^\\/]+[\\/][^\\/]+/.test(value);
18
+ }
19
+ function unquotePathEntry(value) {
20
+ const trimmed = value.trim();
21
+ if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
22
+ return trimmed.slice(1, -1).trim();
23
+ }
24
+ return trimmed;
25
+ }
26
+ function desktopSelection(env, configured, requested, exists) {
27
+ const systemRoot = envValue(env, 'SystemRoot')?.trim();
28
+ if (!systemRoot || !/^[a-z]:[\\/]/i.test(systemRoot)) {
29
+ return {
30
+ executable: '',
31
+ expectedEdition: 'Desktop',
32
+ configured,
33
+ requested,
34
+ error: 'fauxnix: cannot resolve Windows PowerShell safely because SystemRoot is missing or not drive-absolute.',
35
+ };
36
+ }
37
+ const executable = win32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
38
+ if (!exists(executable)) {
39
+ return {
40
+ executable,
41
+ expectedEdition: 'Desktop',
42
+ configured,
43
+ requested,
44
+ error: `fauxnix: Windows PowerShell not found at trusted system path ${executable}.`,
45
+ };
46
+ }
47
+ return { executable, expectedEdition: 'Desktop', configured, requested };
48
+ }
49
+ function coreSelection(env, requested, cwd, exists) {
50
+ const cwdKey = win32.isAbsolute(cwd) ? comparableWindowsPath(cwd) : '';
51
+ const rawPath = envValue(env, 'PATH') ?? '';
52
+ const seen = new Set();
53
+ for (const rawEntry of rawPath.split(';')) {
54
+ const directory = unquotePathEntry(rawEntry);
55
+ if (!directory || !isFullyQualifiedWindowsPath(directory))
56
+ continue;
57
+ const directoryKey = comparableWindowsPath(directory);
58
+ if (!directoryKey || directoryKey === cwdKey || seen.has(directoryKey))
59
+ continue;
60
+ seen.add(directoryKey);
61
+ const candidate = win32.join(directory, 'pwsh.exe');
62
+ if (exists(candidate)) {
63
+ return {
64
+ executable: candidate,
65
+ expectedEdition: 'Core',
66
+ configured: true,
67
+ requested,
68
+ };
69
+ }
70
+ }
71
+ return {
72
+ executable: '',
73
+ expectedEdition: 'Core',
74
+ configured: true,
75
+ requested,
76
+ error: 'fauxnix: pwsh.exe not found in eligible PATH entries ' +
77
+ '(absolute directories only; the current directory is excluded).',
78
+ };
79
+ }
80
+ /**
81
+ * Select the process-wide PowerShell host. FAUXNIX_PS is intentionally a
82
+ * small enum, not a command line: spawn() receives one executable and the
83
+ * fixed fauxnix arguments separately.
84
+ */
85
+ export function resolvePowerShell(env = process.env, options = {}) {
86
+ const requested = envValue(env, 'FAUXNIX_PS')?.trim();
87
+ const exists = options.exists ?? existsSync;
88
+ const cwd = options.cwd ?? process.cwd();
89
+ if (!requested) {
90
+ return desktopSelection(env, false, undefined, exists);
91
+ }
92
+ switch (requested.toLowerCase()) {
93
+ case 'powershell':
94
+ case 'powershell.exe':
95
+ return desktopSelection(env, true, requested, exists);
96
+ case 'pwsh':
97
+ case 'pwsh.exe':
98
+ return coreSelection(env, requested, cwd, exists);
99
+ default:
100
+ return {
101
+ executable: '',
102
+ expectedEdition: 'Desktop',
103
+ configured: true,
104
+ requested,
105
+ error: `fauxnix: invalid FAUXNIX_PS=${JSON.stringify(requested)}; ` +
106
+ 'expected "powershell" or "pwsh". Unset it to use Windows PowerShell 5.1.',
107
+ };
108
+ }
109
+ }
110
+ export function powerShellDisplay(selection) {
111
+ if (selection.error) {
112
+ if (selection.requested === undefined)
113
+ return 'unresolved default Windows PowerShell';
114
+ return `unresolved selection (FAUXNIX_PS=${JSON.stringify(selection.requested)})`;
115
+ }
116
+ if (!selection.configured)
117
+ return `${selection.executable} (default, trusted system path)`;
118
+ return `${selection.executable} (FAUXNIX_PS=${selection.requested}, resolved once)`;
119
+ }
120
+ export function powerShellMissingMessage(selection) {
121
+ if (selection.expectedEdition === 'Core') {
122
+ return ('fauxnix: pwsh.exe not found in eligible absolute PATH entries — FAUXNIX_PS=pwsh selects PowerShell 7.\n' +
123
+ 'Install PowerShell 7 in an absolute PATH directory outside the current working directory, ' +
124
+ 'or unset FAUXNIX_PS ' +
125
+ 'to use Windows PowerShell 5.1.\n');
126
+ }
127
+ return (`fauxnix: Windows PowerShell not found at ${selection.executable || 'the trusted SystemRoot path'}.\n` +
128
+ 'Run fauxnix on Windows, or set FAUXNIX_PS=pwsh after installing PowerShell 7 in an eligible absolute PATH directory.\n');
129
+ }
package/dist/ps-host.d.ts CHANGED
@@ -1,24 +1,43 @@
1
- export declare const PS_MISSING_MESSAGE: string;
1
+ import { PowerShellSelection } from './powershell.js';
2
+ type NativeSpoolWriter = (fd: number, buffer: Buffer, offset: number, length: number) => number;
2
3
  export declare const DEFAULT_STDOUT_LIMIT = 8388608;
3
4
  export declare const DEFAULT_STDERR_LIMIT = 1048576;
4
5
  export interface HostInvokeResult {
5
6
  stdout: Buffer;
7
+ /** UTF-8 stderr captured inside the JSON-framed host protocol. */
6
8
  stderr: Buffer;
9
+ /** Bytes written directly to the PowerShell process stderr OS pipe. */
10
+ nativeStderr?: Buffer;
7
11
  exitCode: number;
8
12
  timedOut: boolean;
9
13
  cancelled: boolean;
10
14
  truncated: boolean;
15
+ /** The framed stdout source crossed its capture limit. */
16
+ stdoutTruncated?: boolean;
17
+ /** Framed or native stderr crossed the shared stderr capture limit. */
18
+ stderrTruncated?: boolean;
11
19
  spawnError?: 'ENOENT' | 'START';
12
20
  spawnMessage?: string;
21
+ stdoutSpool?: string;
22
+ stderrSpool?: string;
23
+ nativeStderrSpool?: string;
24
+ }
25
+ export type HostStreamMode = 'capture' | 'spool' | 'discard';
26
+ export interface HostOutputLimits {
27
+ stdoutLimit?: number;
28
+ stderrLimit?: number;
29
+ stdoutMode?: HostStreamMode;
30
+ stderrMode?: HostStreamMode;
31
+ stdoutSpoolPath?: string;
32
+ stderrSpoolPath?: string;
33
+ nativeStderrSpoolPath?: string;
13
34
  }
14
35
  export interface HostRequestEnv {
15
36
  [key: string]: string;
16
37
  }
17
38
  export declare function encodeHostRequest(id: string, script: string, env: HostRequestEnv, opts?: {
18
39
  v?: number;
19
- stdoutLimit?: number;
20
- stderrLimit?: number;
21
- }): string;
40
+ } & HostOutputLimits): string;
22
41
  export type HostV2Frame = {
23
42
  v: 2;
24
43
  type: 'ready';
@@ -41,6 +60,8 @@ export type HostV2Frame = {
41
60
  timedOut?: boolean;
42
61
  cancelled?: boolean;
43
62
  truncated?: boolean;
63
+ stdoutTruncated?: boolean;
64
+ stderrTruncated?: boolean;
44
65
  };
45
66
  export declare function parseHostLine(line: string): {
46
67
  v1Ready?: boolean;
@@ -55,31 +76,31 @@ export declare function decodeHostResponse(line: string): {
55
76
  ready?: boolean;
56
77
  };
57
78
  /**
58
- * One resident powershell.exe 5.1 process. Frames are UTF-8 JSON lines;
59
- * command stdout/stderr come back as base64 so PS 5.1's UTF-16LE pipe
60
- * encoding cannot scramble the payload.
79
+ * One resident selected PowerShell process. Frames are UTF-8 JSON lines;
80
+ * command stdout/stderr come back as base64 so Windows PowerShell 5.1's
81
+ * UTF-16LE pipe encoding cannot scramble the payload.
61
82
  */
62
83
  export declare class PowerShellHost {
63
84
  private readonly hostFile;
64
85
  private readonly envFn;
86
+ private readonly powerShell;
87
+ private readonly nativeSpoolWrite;
65
88
  private proc;
66
89
  private stdoutBuf;
67
90
  private queuedLines;
68
91
  private waiters;
69
92
  private stderrChunks;
93
+ private nativeCapture;
70
94
  private closeCode;
71
95
  private closeErr;
72
96
  private closed;
73
97
  private startLock;
74
98
  private invokeLock;
75
99
  protocol: 1 | 2;
76
- constructor(hostFile: string, envFn: () => NodeJS.ProcessEnv);
100
+ constructor(hostFile: string, envFn: () => NodeJS.ProcessEnv, powerShell?: PowerShellSelection, nativeSpoolWrite?: NativeSpoolWriter);
77
101
  /** Start the resident process and wait for the ready handshake (B1 prewarm). */
78
102
  ready(): Promise<HostInvokeResult | null>;
79
- invoke(script: string, env: HostRequestEnv, timeoutMs: number, signal?: AbortSignal, limits?: {
80
- stdoutLimit?: number;
81
- stderrLimit?: number;
82
- }): Promise<HostInvokeResult>;
103
+ invoke(script: string, env: HostRequestEnv, timeoutMs: number, signal?: AbortSignal, limits?: HostOutputLimits): Promise<HostInvokeResult>;
83
104
  drainNativeStderr(): Buffer;
84
105
  stop(): Promise<void>;
85
106
  private cancelledResult;
@@ -92,6 +113,13 @@ export declare class PowerShellHost {
92
113
  private nextReadyLine;
93
114
  private nextJsonLine;
94
115
  private collectV2;
95
- private waitNativeMarker;
116
+ private beginNativeCapture;
117
+ private appendNativePayload;
118
+ private onNativeStderr;
119
+ private cancelNativeCapture;
120
+ private closeNativeSpool;
121
+ private readUtf8Prefix;
122
+ private removeRequestSpools;
96
123
  private failWaiters;
97
124
  }
125
+ export {};