fauxnix-cli 0.5.0 → 0.6.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.
- package/README.md +2 -1
- package/dist/ast.d.ts +2 -1
- package/dist/ast.js +2 -1
- package/dist/executor.d.ts +5 -0
- package/dist/executor.js +46 -62
- package/dist/mcp.js +14 -13
- package/dist/parser.js +6 -0
- package/dist/ps-host.d.ts +51 -0
- package/dist/ps-host.js +310 -0
- package/dist/translator.d.ts +16 -2
- package/dist/translator.js +367 -162
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,7 +193,8 @@ fauxnix optimizes for the commands agents actually run. Documented deviations:
|
|
|
193
193
|
word expansion precedes the temporary environment).
|
|
194
194
|
- `yes` is capped at 65,536 lines — PS 5.1 pipelines cannot signal upstream producers to stop, so
|
|
195
195
|
an unbounded `yes | head` would hang.
|
|
196
|
-
- `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case
|
|
196
|
+
- `tail -f`, `eval`, `alias`, heredocs, `while`/`until`/`case`, word-level
|
|
197
|
+
`$((...))` arithmetic expansion
|
|
197
198
|
and background `&` are rejected with actionable error messages instead of misbehaving.
|
|
198
199
|
(`if/then/else/fi`, `for x in ...`, backtick substitution, `command -v`, pipeline `read`,
|
|
199
200
|
and dotenv-style `source` are supported.)
|
package/dist/ast.d.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Explicitly unsupported (parser throws a helpful FauxnixError):
|
|
14
14
|
* heredocs, subshells (...), background &, while/until/case,
|
|
15
|
-
* globs inside quotes,
|
|
15
|
+
* word-level $((...)) arithmetic expansion, globs inside quotes,
|
|
16
|
+
* process substitution <(...).
|
|
16
17
|
*/
|
|
17
18
|
export interface CommandList {
|
|
18
19
|
kind: 'CommandList';
|
package/dist/ast.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Explicitly unsupported (parser throws a helpful FauxnixError):
|
|
14
14
|
* heredocs, subshells (...), background &, while/until/case,
|
|
15
|
-
* globs inside quotes,
|
|
15
|
+
* word-level $((...)) arithmetic expansion, globs inside quotes,
|
|
16
|
+
* process substitution <(...).
|
|
16
17
|
*/
|
|
17
18
|
export function wordToString(w) {
|
|
18
19
|
return w.map(partToString).join('');
|
package/dist/executor.d.ts
CHANGED
|
@@ -18,8 +18,13 @@ export declare class FauxnixSession {
|
|
|
18
18
|
private cwdFile;
|
|
19
19
|
private envFile;
|
|
20
20
|
private scriptFile;
|
|
21
|
+
private hostFile;
|
|
22
|
+
private host;
|
|
23
|
+
private runLock;
|
|
21
24
|
constructor();
|
|
25
|
+
private bindFiles;
|
|
22
26
|
private syncFromDisk;
|
|
27
|
+
private ensureHost;
|
|
23
28
|
dispose(): Promise<void>;
|
|
24
29
|
/** env for the child powershell process. */
|
|
25
30
|
childEnv(cwdOverride?: string, stdinFile?: string | null): NodeJS.ProcessEnv;
|
package/dist/executor.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { promises as fs, readFileSync,
|
|
2
|
+
import { promises as fs, readFileSync, existsSync, openSync, closeSync, writeSync } from 'node:fs';
|
|
4
3
|
import os from 'node:os';
|
|
5
4
|
import path from 'node:path';
|
|
6
|
-
import { normalizeLiteralPath } from './translator.js';
|
|
7
|
-
import { decodeOutput,
|
|
5
|
+
import { normalizeLiteralPath, wrapScript } from './translator.js';
|
|
6
|
+
import { decodeOutput, normalizeHostNewlines, resolveNativePref } from './encoding.js';
|
|
8
7
|
import { normalizeStderr } from './errors.js';
|
|
8
|
+
import { PowerShellHost, PS_MISSING_MESSAGE } from './ps-host.js';
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
10
|
-
const PS_ARGS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass'];
|
|
11
10
|
/** Resolve /dev/null and POSIX-ish literal targets to real Windows paths. */
|
|
12
11
|
function winTarget(target) {
|
|
13
12
|
const p = normalizeLiteralPath(target);
|
|
@@ -156,11 +155,17 @@ export class FauxnixSession {
|
|
|
156
155
|
cwdFile;
|
|
157
156
|
envFile;
|
|
158
157
|
scriptFile;
|
|
158
|
+
hostFile;
|
|
159
|
+
host = null;
|
|
160
|
+
runLock = Promise.resolve();
|
|
159
161
|
constructor() {
|
|
160
|
-
|
|
162
|
+
this.bindFiles(randomUUID().slice(0, 8));
|
|
163
|
+
}
|
|
164
|
+
bindFiles(id) {
|
|
161
165
|
this.cwdFile = path.join(os.tmpdir(), 'fauxnix-' + id + '-cwd.txt');
|
|
162
166
|
this.envFile = path.join(os.tmpdir(), 'fauxnix-' + id + '-env.json');
|
|
163
167
|
this.scriptFile = path.join(os.tmpdir(), 'fauxnix-' + id + '-script.ps1');
|
|
168
|
+
this.hostFile = path.join(os.tmpdir(), 'fauxnix-' + id + '-host.ps1');
|
|
164
169
|
}
|
|
165
170
|
syncFromDisk() {
|
|
166
171
|
try {
|
|
@@ -184,12 +189,27 @@ export class FauxnixSession {
|
|
|
184
189
|
/* ignore */
|
|
185
190
|
}
|
|
186
191
|
}
|
|
192
|
+
ensureHost() {
|
|
193
|
+
if (!this.host) {
|
|
194
|
+
this.host = new PowerShellHost(this.hostFile, () => this.childEnv());
|
|
195
|
+
}
|
|
196
|
+
return this.host;
|
|
197
|
+
}
|
|
187
198
|
async dispose() {
|
|
199
|
+
if (this.host) {
|
|
200
|
+
await this.host.stop();
|
|
201
|
+
this.host = null;
|
|
202
|
+
}
|
|
203
|
+
this.cwd = null;
|
|
204
|
+
this.env = {};
|
|
205
|
+
this.prevExit = null;
|
|
188
206
|
await Promise.allSettled([
|
|
189
207
|
fs.rm(this.cwdFile, { force: true }),
|
|
190
208
|
fs.rm(this.envFile, { force: true }),
|
|
191
209
|
fs.rm(this.scriptFile, { force: true }),
|
|
210
|
+
fs.rm(this.hostFile, { force: true }),
|
|
192
211
|
]);
|
|
212
|
+
this.bindFiles(randomUUID().slice(0, 8));
|
|
193
213
|
}
|
|
194
214
|
/** env for the child powershell process. */
|
|
195
215
|
childEnv(cwdOverride, stdinFile) {
|
|
@@ -218,10 +238,12 @@ export class FauxnixSession {
|
|
|
218
238
|
return env;
|
|
219
239
|
}
|
|
220
240
|
run(plans, opts = {}) {
|
|
221
|
-
|
|
241
|
+
const done = this.runLock.then(() => runPlans(plans, this, opts, () => this.syncFromDisk(), () => this.ensureHost()));
|
|
242
|
+
this.runLock = done.then(() => undefined, () => undefined);
|
|
243
|
+
return done;
|
|
222
244
|
}
|
|
223
245
|
}
|
|
224
|
-
async function runPlans(plans, session, opts, afterSegment,
|
|
246
|
+
async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
225
247
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
226
248
|
let stdout = '';
|
|
227
249
|
let stderr = '';
|
|
@@ -313,65 +335,27 @@ async function runPlans(plans, session, opts, afterSegment, scriptFile) {
|
|
|
313
335
|
chainOk = false;
|
|
314
336
|
continue;
|
|
315
337
|
}
|
|
316
|
-
const encoded =
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (encoded.length > 28000) {
|
|
322
|
-
writeFileSync(scriptFile, '\ufeff' + plan.script, 'utf8');
|
|
323
|
-
psArgs = [...PS_ARGS, '-File', scriptFile];
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
psArgs = [...PS_ARGS, '-EncodedCommand', encoded];
|
|
327
|
-
}
|
|
328
|
-
const child = spawn('powershell.exe', psArgs, {
|
|
329
|
-
env: session.childEnv(currentDir, red.stdinFile),
|
|
330
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
331
|
-
windowsHide: true,
|
|
332
|
-
});
|
|
333
|
-
const running = { proc: child, killed: false };
|
|
334
|
-
const outBufs = [];
|
|
335
|
-
const errBufs = [];
|
|
336
|
-
child.stdout.on('data', (d) => outBufs.push(d));
|
|
337
|
-
child.stderr.on('data', (d) => errBufs.push(d));
|
|
338
|
-
child.stdin.end();
|
|
339
|
-
const timer = setTimeout(() => {
|
|
340
|
-
running.killed = true;
|
|
341
|
-
// Node-native termination — no external kill process, nothing injectable.
|
|
342
|
-
// Grandchildren of a timed-out script may survive; the `kill -9`/`pkill`
|
|
343
|
-
// builtins remain available for explicit Windows tree kills.
|
|
344
|
-
try {
|
|
345
|
-
child.kill();
|
|
346
|
-
}
|
|
347
|
-
catch {
|
|
348
|
-
/* best effort */
|
|
349
|
-
}
|
|
338
|
+
const encoded = wrapScript(plan.body, { mode: 'host' });
|
|
339
|
+
const inv = await ensureHost().invoke(encoded, {
|
|
340
|
+
FAUXNIX_CWD: currentDir,
|
|
341
|
+
FAUXNIX_PREV_EXIT: session.prevExit === null ? '' : String(session.prevExit),
|
|
342
|
+
FAUXNIX_STDIN_FILE: red.stdinFile || '',
|
|
350
343
|
}, timeoutMs);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
stderr += 'fauxnix: failed to start powershell.exe: ' + e.message + '\n';
|
|
361
|
-
}
|
|
362
|
-
resolve(127);
|
|
363
|
-
});
|
|
364
|
-
child.on('close', (c) => resolve(running.killed ? 124 : (c ?? 0)));
|
|
365
|
-
});
|
|
366
|
-
clearTimeout(timer);
|
|
344
|
+
if (inv.spawnError === 'ENOENT') {
|
|
345
|
+
stderr += inv.stderr.toString('utf8') || PS_MISSING_MESSAGE;
|
|
346
|
+
exitCode = 127;
|
|
347
|
+
session.prevExit = exitCode;
|
|
348
|
+
chainOk = false;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
367
351
|
afterSegment();
|
|
368
352
|
const decodePref = resolveNativePref();
|
|
369
353
|
// GNU line discipline: the PS host terminates Write-Output lines with
|
|
370
354
|
// CRLF. Exact writers (fx-write / printf / echo -n) must keep embedded
|
|
371
355
|
// CR so `printf 'a\r\nb' > out` stays 4 bytes.
|
|
372
|
-
let segOut = normalizeHostNewlines(decodeOutput(
|
|
373
|
-
let segErr = normalizeHostNewlines(normalizeStderr(decodeOutput(
|
|
374
|
-
if (
|
|
356
|
+
let segOut = normalizeHostNewlines(decodeOutput(inv.stdout, decodePref));
|
|
357
|
+
let segErr = normalizeHostNewlines(normalizeStderr(decodeOutput(inv.stderr, decodePref)));
|
|
358
|
+
if (inv.timedOut) {
|
|
375
359
|
segErr += '\nbash: command timed out after ' + Math.round(timeoutMs / 1000) + 's';
|
|
376
360
|
}
|
|
377
361
|
if (red.mergeStderr) {
|
|
@@ -414,7 +398,7 @@ async function runPlans(plans, session, opts, afterSegment, scriptFile) {
|
|
|
414
398
|
}
|
|
415
399
|
stdout += segOut;
|
|
416
400
|
stderr += segErr;
|
|
417
|
-
exitCode =
|
|
401
|
+
exitCode = inv.timedOut ? 124 : inv.exitCode;
|
|
418
402
|
session.prevExit = exitCode;
|
|
419
403
|
chainOk = exitCode === 0;
|
|
420
404
|
// Only inherit cwd from a segment that actually ran and whose
|
package/dist/mcp.js
CHANGED
|
@@ -30,22 +30,22 @@ const SESSION_ANNOTATIONS = {
|
|
|
30
30
|
idempotentHint: true,
|
|
31
31
|
openWorldHint: false,
|
|
32
32
|
};
|
|
33
|
-
const TOOL_DESCRIPTION = `Execute a Linux/bash-style command on this Windows machine.
|
|
34
|
-
|
|
35
|
-
Commands are deterministically translated to PowerShell and executed natively — no WSL or VM.
|
|
36
|
-
Output is formatted to look like GNU/Linux tooling (ls -l, ps aux, df -h ...), errors look like bash errors, and text encoding (UTF-8/GBK) is handled automatically.
|
|
37
|
-
|
|
38
|
-
Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME ~), command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
|
|
39
|
-
Unknown commands (git, node, npm, python, cargo...) are passed through and executed natively with argv-style quoting.
|
|
40
|
-
Not supported: heredocs, while/until/case, background jobs. if/then/else/fi and for-in loops are supported.
|
|
41
|
-
|
|
42
|
-
CWD, environment variables, export/unset and cd persist across calls within this session —
|
|
43
|
-
Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout).
|
|
44
|
-
|
|
33
|
+
const TOOL_DESCRIPTION = `Execute a Linux/bash-style command on this Windows machine.
|
|
34
|
+
|
|
35
|
+
Commands are deterministically translated to PowerShell and executed natively — no WSL or VM.
|
|
36
|
+
Output is formatted to look like GNU/Linux tooling (ls -l, ps aux, df -h ...), errors look like bash errors, and text encoding (UTF-8/GBK) is handled automatically.
|
|
37
|
+
|
|
38
|
+
Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME ~), command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
|
|
39
|
+
Unknown commands (git, node, npm, python, cargo...) are passed through and executed natively with argv-style quoting.
|
|
40
|
+
Not supported: heredocs, while/until/case, word-level \$((...)) arithmetic expansion, background jobs. if/then/else/fi and for-in loops are supported.
|
|
41
|
+
|
|
42
|
+
CWD, environment variables, export/unset and cd persist across calls within this session — a resident PowerShell 5.1 host is reused, so batching with ; or && is still nicer but many tiny calls no longer each pay a powershell.exe spawn.
|
|
43
|
+
Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout).
|
|
44
|
+
|
|
45
45
|
Platform requirement: the execution backend is native Windows PowerShell 5.1+. On hosts without PowerShell on PATH (e.g. Linux containers/sandboxes), the bash tool returns exit code 127 with an actionable error instead of running the command.`;
|
|
46
46
|
export async function startMcpServer() {
|
|
47
47
|
const server = new McpServer({ name: 'fauxnix', version: pkgVersion }, { capabilities: { tools: {} } });
|
|
48
|
-
|
|
48
|
+
let session = new FauxnixSession();
|
|
49
49
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, {
|
|
50
50
|
command: z.string().describe('The bash-style command line to run'),
|
|
51
51
|
timeout_ms: z
|
|
@@ -94,6 +94,7 @@ export async function startMcpServer() {
|
|
|
94
94
|
}, SESSION_ANNOTATIONS, async ({ action }) => {
|
|
95
95
|
if (action === 'reset') {
|
|
96
96
|
await session.dispose();
|
|
97
|
+
session = new FauxnixSession();
|
|
97
98
|
return { content: [{ type: 'text', text: 'fauxnix: session reset' }] };
|
|
98
99
|
}
|
|
99
100
|
const envKeys = Object.keys(session.env).sort();
|
package/dist/parser.js
CHANGED
|
@@ -256,6 +256,12 @@ function readDollar(input, i) {
|
|
|
256
256
|
}
|
|
257
257
|
return { part: { kind: 'Var', name }, next: end + 1 };
|
|
258
258
|
}
|
|
259
|
+
// $((...)) is arithmetic expansion, not command substitution of a
|
|
260
|
+
// parenthesized body. Today it was parsed as $( (expr) ) and became an
|
|
261
|
+
// empty/confusing command. Reject loudly until word-level arith lands.
|
|
262
|
+
if (input[j] === '(' && j + 1 < n && input[j + 1] === '(') {
|
|
263
|
+
throw new FauxnixParseError('fauxnix: $((...)) arithmetic expansion is not supported; compute the value in the agent or compare with [[ $n -eq m ]]');
|
|
264
|
+
}
|
|
259
265
|
// $(cmd substitution) — captured with balanced parens; the translator
|
|
260
266
|
// recursively translates this text before embedding it.
|
|
261
267
|
if (input[j] === '(') {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const PS_MISSING_MESSAGE: string;
|
|
2
|
+
export interface HostInvokeResult {
|
|
3
|
+
stdout: Buffer;
|
|
4
|
+
stderr: Buffer;
|
|
5
|
+
exitCode: number;
|
|
6
|
+
timedOut: boolean;
|
|
7
|
+
spawnError?: 'ENOENT' | 'START';
|
|
8
|
+
spawnMessage?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface HostRequestEnv {
|
|
11
|
+
[key: string]: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function encodeHostRequest(id: string, script: string, env: HostRequestEnv): string;
|
|
14
|
+
export declare function decodeHostResponse(line: string): {
|
|
15
|
+
id: string;
|
|
16
|
+
stdout: Buffer;
|
|
17
|
+
stderr: Buffer;
|
|
18
|
+
exitCode: number;
|
|
19
|
+
ready?: boolean;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* One resident powershell.exe 5.1 process. Frames are UTF-8 JSON lines;
|
|
23
|
+
* command stdout/stderr come back as base64 so PS 5.1's UTF-16LE pipe
|
|
24
|
+
* encoding cannot scramble the payload.
|
|
25
|
+
*/
|
|
26
|
+
export declare class PowerShellHost {
|
|
27
|
+
private readonly hostFile;
|
|
28
|
+
private readonly envFn;
|
|
29
|
+
private proc;
|
|
30
|
+
private stdoutBuf;
|
|
31
|
+
private queuedLines;
|
|
32
|
+
private waiters;
|
|
33
|
+
private stderrChunks;
|
|
34
|
+
private closeCode;
|
|
35
|
+
private closeErr;
|
|
36
|
+
private closed;
|
|
37
|
+
private startLock;
|
|
38
|
+
private invokeLock;
|
|
39
|
+
constructor(hostFile: string, envFn: () => NodeJS.ProcessEnv);
|
|
40
|
+
invoke(script: string, env: HostRequestEnv, timeoutMs: number): Promise<HostInvokeResult>;
|
|
41
|
+
stop(): Promise<void>;
|
|
42
|
+
private invokeSerial;
|
|
43
|
+
private ensureStarted;
|
|
44
|
+
private deadRestart;
|
|
45
|
+
private start;
|
|
46
|
+
private onStdout;
|
|
47
|
+
private nextLine;
|
|
48
|
+
private nextReadyLine;
|
|
49
|
+
private nextJsonLine;
|
|
50
|
+
private failWaiters;
|
|
51
|
+
}
|
package/dist/ps-host.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { hostBootstrapScript } from './translator.js';
|
|
4
|
+
const PS_ARGS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass'];
|
|
5
|
+
const READY_TIMEOUT_MS = 30_000;
|
|
6
|
+
export const PS_MISSING_MESSAGE = 'fauxnix: powershell.exe not found — fauxnix executes bash via native Windows PowerShell 5.1+.\n' +
|
|
7
|
+
'This host has no PowerShell on PATH (typical for Linux containers/sandboxes).\n' +
|
|
8
|
+
'Run fauxnix on Windows, or install PowerShell and make powershell.exe reachable on PATH.\n';
|
|
9
|
+
export function encodeHostRequest(id, script, env) {
|
|
10
|
+
return JSON.stringify({
|
|
11
|
+
id,
|
|
12
|
+
scriptB64: Buffer.from(script, 'utf8').toString('base64'),
|
|
13
|
+
env,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export function decodeHostResponse(line) {
|
|
17
|
+
const j = JSON.parse(line);
|
|
18
|
+
const n = Number(j.exitCode);
|
|
19
|
+
return {
|
|
20
|
+
id: j.id ?? '',
|
|
21
|
+
stdout: Buffer.from(j.stdoutB64 ?? '', 'base64'),
|
|
22
|
+
stderr: Buffer.from(j.stderrB64 ?? '', 'base64'),
|
|
23
|
+
exitCode: Number.isFinite(n) ? n : 0,
|
|
24
|
+
ready: j.ready === true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* One resident powershell.exe 5.1 process. Frames are UTF-8 JSON lines;
|
|
29
|
+
* command stdout/stderr come back as base64 so PS 5.1's UTF-16LE pipe
|
|
30
|
+
* encoding cannot scramble the payload.
|
|
31
|
+
*/
|
|
32
|
+
export class PowerShellHost {
|
|
33
|
+
hostFile;
|
|
34
|
+
envFn;
|
|
35
|
+
proc = null;
|
|
36
|
+
stdoutBuf = Buffer.alloc(0);
|
|
37
|
+
queuedLines = [];
|
|
38
|
+
waiters = [];
|
|
39
|
+
stderrChunks = [];
|
|
40
|
+
closeCode;
|
|
41
|
+
closeErr = null;
|
|
42
|
+
closed = false;
|
|
43
|
+
startLock = null;
|
|
44
|
+
invokeLock = Promise.resolve();
|
|
45
|
+
constructor(hostFile, envFn) {
|
|
46
|
+
this.hostFile = hostFile;
|
|
47
|
+
this.envFn = envFn;
|
|
48
|
+
}
|
|
49
|
+
async invoke(script, env, timeoutMs) {
|
|
50
|
+
const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs));
|
|
51
|
+
this.invokeLock = run.then(() => undefined, () => undefined);
|
|
52
|
+
return run;
|
|
53
|
+
}
|
|
54
|
+
async stop() {
|
|
55
|
+
const proc = this.proc;
|
|
56
|
+
this.proc = null;
|
|
57
|
+
this.closed = true;
|
|
58
|
+
this.failWaiters(new Error('fauxnix: powershell host stopped'));
|
|
59
|
+
if (!proc)
|
|
60
|
+
return;
|
|
61
|
+
try {
|
|
62
|
+
proc.stdin?.end();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* ignore */
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
proc.kill();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* ignore */
|
|
72
|
+
}
|
|
73
|
+
await new Promise((resolve) => {
|
|
74
|
+
const t = setTimeout(resolve, 2000);
|
|
75
|
+
proc.once('close', () => {
|
|
76
|
+
clearTimeout(t);
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async invokeSerial(script, env, timeoutMs) {
|
|
82
|
+
const started = await this.ensureStarted();
|
|
83
|
+
if (started)
|
|
84
|
+
return started;
|
|
85
|
+
const id = 'f' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
86
|
+
const line = encodeHostRequest(id, script, env);
|
|
87
|
+
try {
|
|
88
|
+
this.proc.stdin.write(line + '\n');
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
await this.deadRestart();
|
|
92
|
+
return {
|
|
93
|
+
stdout: Buffer.alloc(0),
|
|
94
|
+
stderr: Buffer.from('fauxnix: powershell host exited unexpectedly\n', 'utf8'),
|
|
95
|
+
exitCode: 1,
|
|
96
|
+
timedOut: false,
|
|
97
|
+
spawnMessage: e.message,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const raw = await this.nextJsonLine(timeoutMs, id);
|
|
102
|
+
const msg = decodeHostResponse(raw);
|
|
103
|
+
return {
|
|
104
|
+
stdout: msg.stdout,
|
|
105
|
+
stderr: msg.stderr,
|
|
106
|
+
exitCode: msg.exitCode,
|
|
107
|
+
timedOut: false,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
const timedOut = e.timedOut === true;
|
|
112
|
+
await this.stop();
|
|
113
|
+
if (timedOut) {
|
|
114
|
+
return { stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), exitCode: 124, timedOut: true };
|
|
115
|
+
}
|
|
116
|
+
if (this.closeErr && this.closeErr.code === 'ENOENT') {
|
|
117
|
+
return {
|
|
118
|
+
stdout: Buffer.alloc(0),
|
|
119
|
+
stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
|
|
120
|
+
exitCode: 127,
|
|
121
|
+
timedOut: false,
|
|
122
|
+
spawnError: 'ENOENT',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const code = this.closeCode ?? 1;
|
|
126
|
+
return {
|
|
127
|
+
stdout: Buffer.alloc(0),
|
|
128
|
+
stderr: Buffer.from('fauxnix: powershell host exited unexpectedly' +
|
|
129
|
+
(code !== 1 ? ' (exit ' + String(code) + ')' : '') +
|
|
130
|
+
'\n', 'utf8'),
|
|
131
|
+
exitCode: code === 0 ? 1 : code,
|
|
132
|
+
timedOut: false,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async ensureStarted() {
|
|
137
|
+
if (this.proc && !this.closed)
|
|
138
|
+
return null;
|
|
139
|
+
if (!this.startLock)
|
|
140
|
+
this.startLock = this.start();
|
|
141
|
+
try {
|
|
142
|
+
await this.startLock;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
const err = e;
|
|
147
|
+
if (err.code === 'ENOENT') {
|
|
148
|
+
return {
|
|
149
|
+
stdout: Buffer.alloc(0),
|
|
150
|
+
stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
|
|
151
|
+
exitCode: 127,
|
|
152
|
+
timedOut: false,
|
|
153
|
+
spawnError: 'ENOENT',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
stdout: Buffer.alloc(0),
|
|
158
|
+
stderr: Buffer.from('fauxnix: failed to start powershell.exe: ' + err.message + '\n', 'utf8'),
|
|
159
|
+
exitCode: 127,
|
|
160
|
+
timedOut: false,
|
|
161
|
+
spawnError: 'START',
|
|
162
|
+
spawnMessage: err.message,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
this.startLock = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async deadRestart() {
|
|
170
|
+
await this.stop();
|
|
171
|
+
this.closed = false;
|
|
172
|
+
this.closeCode = undefined;
|
|
173
|
+
this.closeErr = null;
|
|
174
|
+
this.stdoutBuf = Buffer.alloc(0);
|
|
175
|
+
this.queuedLines = [];
|
|
176
|
+
this.stderrChunks = [];
|
|
177
|
+
}
|
|
178
|
+
async start() {
|
|
179
|
+
await this.deadRestart();
|
|
180
|
+
this.closed = false;
|
|
181
|
+
writeFileSync(this.hostFile, '\ufeff' + hostBootstrapScript(), 'utf8');
|
|
182
|
+
const child = spawn('powershell.exe', [...PS_ARGS, '-File', this.hostFile], {
|
|
183
|
+
env: this.envFn(),
|
|
184
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
185
|
+
windowsHide: true,
|
|
186
|
+
});
|
|
187
|
+
this.proc = child;
|
|
188
|
+
child.stdout.on('data', (d) => this.onStdout(d));
|
|
189
|
+
child.stderr.on('data', (d) => this.stderrChunks.push(d));
|
|
190
|
+
child.on('error', (e) => {
|
|
191
|
+
this.closeErr = e;
|
|
192
|
+
this.closed = true;
|
|
193
|
+
this.failWaiters(e);
|
|
194
|
+
});
|
|
195
|
+
child.on('close', (c) => {
|
|
196
|
+
this.closeCode = c;
|
|
197
|
+
this.closed = true;
|
|
198
|
+
this.proc = null;
|
|
199
|
+
this.failWaiters(new Error('fauxnix: powershell host closed'));
|
|
200
|
+
});
|
|
201
|
+
try {
|
|
202
|
+
const readyLine = await this.nextReadyLine(READY_TIMEOUT_MS);
|
|
203
|
+
const msg = decodeHostResponse(readyLine);
|
|
204
|
+
if (!msg.ready) {
|
|
205
|
+
throw new Error('fauxnix: powershell host handshake failed');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
await this.stop();
|
|
210
|
+
throw e;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
onStdout(chunk) {
|
|
214
|
+
this.stdoutBuf = Buffer.concat([this.stdoutBuf, chunk]);
|
|
215
|
+
while (true) {
|
|
216
|
+
const i = this.stdoutBuf.indexOf(0x0a);
|
|
217
|
+
if (i < 0)
|
|
218
|
+
break;
|
|
219
|
+
let line = this.stdoutBuf.subarray(0, i);
|
|
220
|
+
this.stdoutBuf = this.stdoutBuf.subarray(i + 1);
|
|
221
|
+
if (line.length && line[line.length - 1] === 0x0d)
|
|
222
|
+
line = line.subarray(0, line.length - 1);
|
|
223
|
+
const s = line.toString('utf8').replace(/^\uFEFF/, '');
|
|
224
|
+
const w = this.waiters.shift();
|
|
225
|
+
if (w)
|
|
226
|
+
w.resolve(s);
|
|
227
|
+
else
|
|
228
|
+
this.queuedLines.push(s);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
nextLine(timeoutMs) {
|
|
232
|
+
if (this.queuedLines.length)
|
|
233
|
+
return Promise.resolve(this.queuedLines.shift());
|
|
234
|
+
if (this.closed) {
|
|
235
|
+
return Promise.reject(this.closeErr ?? new Error('fauxnix: powershell host closed'));
|
|
236
|
+
}
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
const waiter = {
|
|
239
|
+
resolve: (line) => {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
resolve(line);
|
|
242
|
+
},
|
|
243
|
+
reject: (err) => {
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
reject(err);
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
const timer = setTimeout(() => {
|
|
249
|
+
const idx = this.waiters.indexOf(waiter);
|
|
250
|
+
if (idx >= 0)
|
|
251
|
+
this.waiters.splice(idx, 1);
|
|
252
|
+
const err = new Error('fauxnix: powershell host timed out');
|
|
253
|
+
err.timedOut = true;
|
|
254
|
+
reject(err);
|
|
255
|
+
}, timeoutMs);
|
|
256
|
+
this.waiters.push(waiter);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async nextReadyLine(timeoutMs) {
|
|
260
|
+
const deadline = Date.now() + timeoutMs;
|
|
261
|
+
while (Date.now() < deadline) {
|
|
262
|
+
const line = await this.nextLine(Math.max(1, deadline - Date.now()));
|
|
263
|
+
if (!line.trim())
|
|
264
|
+
continue;
|
|
265
|
+
try {
|
|
266
|
+
const msg = decodeHostResponse(line);
|
|
267
|
+
if (msg.ready)
|
|
268
|
+
return line;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
/* skip PS boot noise */
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const err = new Error('fauxnix: powershell host handshake timed out');
|
|
275
|
+
err.timedOut = true;
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
async nextJsonLine(timeoutMs, id) {
|
|
279
|
+
const deadline = Date.now() + timeoutMs;
|
|
280
|
+
while (Date.now() < deadline) {
|
|
281
|
+
const line = await this.nextLine(Math.max(1, deadline - Date.now()));
|
|
282
|
+
if (!line.trim())
|
|
283
|
+
continue;
|
|
284
|
+
try {
|
|
285
|
+
const msg = decodeHostResponse(line);
|
|
286
|
+
if (msg.ready)
|
|
287
|
+
continue;
|
|
288
|
+
if (msg.id === id)
|
|
289
|
+
return line;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
/* skip noise */
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const err = new Error('fauxnix: powershell host timed out');
|
|
296
|
+
err.timedOut = true;
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
299
|
+
failWaiters(err) {
|
|
300
|
+
const ws = this.waiters.splice(0);
|
|
301
|
+
for (const w of ws) {
|
|
302
|
+
try {
|
|
303
|
+
w.reject(err);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
/* ignore */
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
package/dist/translator.d.ts
CHANGED
|
@@ -76,14 +76,28 @@ export declare function translatePipelineBody(p: {
|
|
|
76
76
|
}): PipelineParts;
|
|
77
77
|
export interface SegmentPlan {
|
|
78
78
|
op: ';' | '&&' | '||';
|
|
79
|
-
/**
|
|
79
|
+
/** Spawn-mode wrapScript (CLI/MCP `translate`, one-shot powershell.exe). */
|
|
80
80
|
script: string;
|
|
81
|
+
/** Pipeline body before wrapScript — executor host mode re-wraps this. */
|
|
82
|
+
body: string;
|
|
81
83
|
/** All redirects collected from this segment (executor handles them). */
|
|
82
84
|
redirects: Redirect[];
|
|
83
85
|
}
|
|
84
86
|
export declare function translateCommandList(list: CommandList): SegmentPlan[];
|
|
87
|
+
export type WrapMode = 'spawn' | 'host';
|
|
88
|
+
export interface WrapScriptOptions {
|
|
89
|
+
/** spawn (default): one-shot process, `exit` at the end. host: no `exit`, no helper re-emit. */
|
|
90
|
+
mode?: WrapMode;
|
|
91
|
+
}
|
|
85
92
|
/**
|
|
86
93
|
* Wrap a pipeline body with the Fauxnix executor contract:
|
|
87
94
|
* UTF-8 everywhere, bash-style exit codes, cwd/env persistence channels.
|
|
95
|
+
* Spawn mode emits only the fx- helpers the body actually calls. Host mode
|
|
96
|
+
* assumes the resident process already loaded the catalog and must not `exit`.
|
|
97
|
+
*/
|
|
98
|
+
export declare function wrapScript(body: string, opts?: WrapScriptOptions): string;
|
|
99
|
+
/**
|
|
100
|
+
* Resident-host bootstrap: encoding + full fx-* catalog + JSON-line RPC loop.
|
|
101
|
+
* Loaded once via `powershell.exe -File`. Must never `exit` a successful frame.
|
|
88
102
|
*/
|
|
89
|
-
export declare function
|
|
103
|
+
export declare function hostBootstrapScript(): string;
|
package/dist/translator.js
CHANGED
|
@@ -771,21 +771,72 @@ export function translateCommandList(list) {
|
|
|
771
771
|
call +
|
|
772
772
|
' }';
|
|
773
773
|
}
|
|
774
|
-
plans.push({ op: seg.op, script: wrapScript(body), redirects });
|
|
774
|
+
plans.push({ op: seg.op, script: wrapScript(body), body, redirects });
|
|
775
775
|
}
|
|
776
776
|
return plans;
|
|
777
777
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
778
|
+
const WRAP_HELPER_ORDER = [
|
|
779
|
+
'fx-readlines',
|
|
780
|
+
'fx-csub',
|
|
781
|
+
'fx-svenc',
|
|
782
|
+
'fx-svdec',
|
|
783
|
+
'fx-arrload',
|
|
784
|
+
'fx-scalar0',
|
|
785
|
+
'fx-ifs1',
|
|
786
|
+
'fx-arrdrop',
|
|
787
|
+
'fx-arrhas',
|
|
788
|
+
'fx-arrpackget',
|
|
789
|
+
'fx-arrpackset',
|
|
790
|
+
'fx-arrput',
|
|
791
|
+
'fx-arrclr',
|
|
792
|
+
'fx-subget',
|
|
793
|
+
];
|
|
794
|
+
const WRAP_HELPER_DEPS = {
|
|
795
|
+
'fx-readlines': [],
|
|
796
|
+
'fx-csub': [],
|
|
797
|
+
'fx-svenc': [],
|
|
798
|
+
'fx-svdec': [],
|
|
799
|
+
'fx-arrload': ['fx-scalar0', 'fx-svdec'],
|
|
800
|
+
'fx-scalar0': ['fx-svdec'],
|
|
801
|
+
'fx-ifs1': ['fx-scalar0'],
|
|
802
|
+
'fx-arrdrop': [],
|
|
803
|
+
'fx-arrhas': [],
|
|
804
|
+
'fx-arrpackget': [],
|
|
805
|
+
'fx-arrpackset': ['fx-arrdrop'],
|
|
806
|
+
'fx-arrput': ['fx-arrdrop', 'fx-svenc'],
|
|
807
|
+
'fx-arrclr': ['fx-arrdrop'],
|
|
808
|
+
'fx-subget': ['fx-arrload', 'fx-ifs1'],
|
|
809
|
+
};
|
|
810
|
+
/** Helpers the body calls that wrapScript still has to emit (not already defined there). */
|
|
811
|
+
function wrapHelpersNeeded(body) {
|
|
812
|
+
const defined = new Set();
|
|
813
|
+
const defRe = /function\s+(fx-[A-Za-z0-9]+)/g;
|
|
814
|
+
let m;
|
|
815
|
+
while ((m = defRe.exec(body)))
|
|
816
|
+
defined.add(m[1]);
|
|
817
|
+
const seeds = [];
|
|
818
|
+
const callRe = /\b(fx-[A-Za-z0-9]+)\b/g;
|
|
819
|
+
while ((m = callRe.exec(body))) {
|
|
820
|
+
const n = m[1];
|
|
821
|
+
if (!defined.has(n) && n in WRAP_HELPER_DEPS)
|
|
822
|
+
seeds.push(n);
|
|
823
|
+
}
|
|
824
|
+
const needed = new Set();
|
|
825
|
+
const stack = seeds.slice();
|
|
826
|
+
while (stack.length) {
|
|
827
|
+
const n = stack.pop();
|
|
828
|
+
if (needed.has(n) || defined.has(n))
|
|
829
|
+
continue;
|
|
830
|
+
needed.add(n);
|
|
831
|
+
for (const d of WRAP_HELPER_DEPS[n])
|
|
832
|
+
stack.push(d);
|
|
833
|
+
}
|
|
834
|
+
return needed;
|
|
835
|
+
}
|
|
836
|
+
function wrapEncodingPreamble() {
|
|
837
|
+
return [
|
|
784
838
|
'$ErrorActionPreference = "Continue"',
|
|
785
839
|
"$ProgressPreference = 'SilentlyContinue'",
|
|
786
|
-
'$fx_exit = 0',
|
|
787
|
-
'$fx_prev = 0',
|
|
788
|
-
'if ($env:FAUXNIX_PREV_EXIT) { try { $fx_prev = [int]$env:FAUXNIX_PREV_EXIT } catch { $fx_prev = 0 } }',
|
|
789
840
|
// single console-encoding knob in PS 5.1: ansi mode decodes GBK-native
|
|
790
841
|
// admin tools correctly, utf8 mode decodes UTF-8-native dev tools
|
|
791
842
|
// (see encoding.ts — file reads sniff per file and are always right)
|
|
@@ -796,6 +847,13 @@ export function wrapScript(body) {
|
|
|
796
847
|
' try { chcp 65001 > $null } catch {}',
|
|
797
848
|
'}',
|
|
798
849
|
'$OutputEncoding = [System.Text.Encoding]::UTF8',
|
|
850
|
+
];
|
|
851
|
+
}
|
|
852
|
+
function wrapCwdPreamble() {
|
|
853
|
+
return [
|
|
854
|
+
'$script:fx_exit = 0',
|
|
855
|
+
'$fx_prev = 0',
|
|
856
|
+
'if ($env:FAUXNIX_PREV_EXIT) { try { $fx_prev = [int]$env:FAUXNIX_PREV_EXIT } catch { $fx_prev = 0 } }',
|
|
799
857
|
'if ($env:FAUXNIX_CWD) { try { Set-Location -LiteralPath $env:FAUXNIX_CWD } catch {} }',
|
|
800
858
|
// capture AFTER the session cwd is applied — OLDPWD must refer to the
|
|
801
859
|
// shell's previous directory, not the host process' startup directory
|
|
@@ -803,157 +861,10 @@ export function wrapScript(body) {
|
|
|
803
861
|
// .NET APIs (ReadAllBytes & friends) resolve relative paths against the
|
|
804
862
|
// process working directory, NOT the PS location — keep them in sync.
|
|
805
863
|
'try { [Environment]::CurrentDirectory = (Get-Location).ProviderPath } catch {}',
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
' try { $t = (New-Object System.Text.UTF8Encoding($false, $true)).GetString($b) } catch {}',
|
|
811
|
-
" if ($null -eq $t) { try { $t = [System.Text.Encoding]::GetEncoding(936).GetString($b) } catch { $t = [System.Text.Encoding]::ASCII.GetString($b) } }",
|
|
812
|
-
' $t = $t -replace "`r`n", "`n"',
|
|
813
|
-
' $t = $t -replace "`r", "`n"',
|
|
814
|
-
' $parts = @($t.Split("`n"))',
|
|
815
|
-
" if ($parts.Count -eq 1 -and $parts[0] -eq '') { return @() }",
|
|
816
|
-
" if ($parts[$parts.Count - 1] -eq '') { $parts = $parts[0..($parts.Count - 2)] }",
|
|
817
|
-
' return $parts',
|
|
818
|
-
'}',
|
|
819
|
-
'function fx-csub([scriptblock]$b) {',
|
|
820
|
-
' $fx_prevcs = $script:fx_csub',
|
|
821
|
-
' $script:fx_csub = $true',
|
|
822
|
-
' try { $fx_o = @(& $b | ForEach-Object { [string]$_ }) }',
|
|
823
|
-
' finally { $script:fx_csub = $fx_prevcs }',
|
|
824
|
-
' $fx_s = ($fx_o -join [string][char]10)',
|
|
825
|
-
' while ($fx_s.Length -gt 0 -and $fx_s[$fx_s.Length - 1] -eq [char]10) {',
|
|
826
|
-
' $fx_s = $fx_s.Substring(0, $fx_s.Length - 1)',
|
|
827
|
-
' }',
|
|
828
|
-
' return $fx_s',
|
|
829
|
-
'}',
|
|
830
|
-
'function fx-svenc($s) {',
|
|
831
|
-
' return ([string]$s).Replace([string][char]92, ([string][char]92 + [string][char]92)).Replace([string][char]13, ([string][char]92 + [char]114)).Replace([string][char]10, ([string][char]92 + [char]110))',
|
|
832
|
-
'}',
|
|
833
|
-
'function fx-svdec($s) {',
|
|
834
|
-
' $s = [string]$s',
|
|
835
|
-
' $sb = New-Object System.Text.StringBuilder',
|
|
836
|
-
' $i = 0',
|
|
837
|
-
' while ($i -lt $s.Length) {',
|
|
838
|
-
' $c = $s[$i]',
|
|
839
|
-
' if ($c -eq [char]92 -and ($i + 1) -lt $s.Length) {',
|
|
840
|
-
' $n2 = $s[$i + 1]',
|
|
841
|
-
' if ($n2 -eq [char]110) { [void]$sb.Append([char]10); $i += 2; continue }',
|
|
842
|
-
' if ($n2 -eq [char]114) { [void]$sb.Append([char]13); $i += 2; continue }',
|
|
843
|
-
' if ($n2 -eq [char]92) { [void]$sb.Append([char]92); $i += 2; continue }',
|
|
844
|
-
' }',
|
|
845
|
-
' [void]$sb.Append($c)',
|
|
846
|
-
' $i++',
|
|
847
|
-
' }',
|
|
848
|
-
' return [string]$sb',
|
|
849
|
-
'}',
|
|
850
|
-
'function fx-arrload($n) {',
|
|
851
|
-
' $n = [string]$n',
|
|
852
|
-
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
853
|
-
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
854
|
-
' if ($fx_eq -lt 1) { continue }',
|
|
855
|
-
' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { continue }',
|
|
856
|
-
' $out = @()',
|
|
857
|
-
' foreach ($el in @($fx_pair.Substring($fx_eq + 1) -split [string][char]30)) { $out += ,(fx-svdec $el) }',
|
|
858
|
-
' return $out',
|
|
859
|
-
' }',
|
|
860
|
-
' $s0 = fx-scalar0 $n',
|
|
861
|
-
' if ($null -eq $s0) { return @() }',
|
|
862
|
-
' return @([string]$s0)',
|
|
863
|
-
'}',
|
|
864
|
-
'function fx-scalar0($n) {',
|
|
865
|
-
' $n = [string]$n',
|
|
866
|
-
" if (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return $null }",
|
|
867
|
-
' foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
|
|
868
|
-
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
869
|
-
' if ($fx_eq -lt 1) { continue }',
|
|
870
|
-
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return (fx-svdec $fx_pair.Substring($fx_eq + 1)) }',
|
|
871
|
-
' }',
|
|
872
|
-
" if ($n -ceq 'HOME') { return [string]$HOME }",
|
|
873
|
-
" if ($n -ceq 'PWD') { return [string]$PWD.Path }",
|
|
874
|
-
" if ($n -ceq 'USER' -or $n -ceq 'LOGNAME') { return [string]$env:USERNAME }",
|
|
875
|
-
" if ($n -ceq 'PATH') { return [string]$env:PATH }",
|
|
876
|
-
" if ($n -ceq 'SHELL') { return 'powershell' }",
|
|
877
|
-
" if ($n -ceq 'TERM') { return 'xterm-256color' }",
|
|
878
|
-
" if ($n -ceq 'OLDPWD') { return $(if ($env:FAUXNIX_OLDPWD) { [string]$env:FAUXNIX_OLDPWD } else { $null }) }",
|
|
879
|
-
" if ($n -ceq 'HOSTNAME') { return [string]$env:COMPUTERNAME }",
|
|
880
|
-
' $ev = Get-ChildItem Env: | Where-Object { $_.Name -ceq $n } | Select-Object -First 1',
|
|
881
|
-
' if ($ev) { return [string]$ev.Value }',
|
|
882
|
-
' return $null',
|
|
883
|
-
'}',
|
|
884
|
-
'function fx-ifs1 {',
|
|
885
|
-
" $s = fx-scalar0 'IFS'",
|
|
886
|
-
" if ($null -eq $s) { return ' ' }",
|
|
887
|
-
" if ([string]$s -eq '') { return '' }",
|
|
888
|
-
' return [string]$s[0]',
|
|
889
|
-
'}',
|
|
890
|
-
'function fx-arrdrop($n) {',
|
|
891
|
-
' $n = [string]$n',
|
|
892
|
-
' $fx_sm = @()',
|
|
893
|
-
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
894
|
-
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
895
|
-
' if ($fx_eq -lt 1) { continue }',
|
|
896
|
-
' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sm += $fx_pair }',
|
|
897
|
-
' }',
|
|
898
|
-
' $env:FAUXNIX_ARRS = ($fx_sm -join [string][char]10)',
|
|
899
|
-
'}',
|
|
900
|
-
'function fx-arrhas($n) {',
|
|
901
|
-
' $n = [string]$n',
|
|
902
|
-
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
903
|
-
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
904
|
-
' if ($fx_eq -lt 1) { continue }',
|
|
905
|
-
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $true }',
|
|
906
|
-
' }',
|
|
907
|
-
' return $false',
|
|
908
|
-
'}',
|
|
909
|
-
'function fx-arrpackget($n) {',
|
|
910
|
-
' $n = [string]$n',
|
|
911
|
-
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
912
|
-
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
913
|
-
' if ($fx_eq -lt 1) { continue }',
|
|
914
|
-
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $fx_pair.Substring($fx_eq + 1) }',
|
|
915
|
-
' }',
|
|
916
|
-
' return $null',
|
|
917
|
-
'}',
|
|
918
|
-
'function fx-arrpackset($n, $pay) {',
|
|
919
|
-
' fx-arrdrop $n',
|
|
920
|
-
' if ($null -eq $pay) { return }',
|
|
921
|
-
" $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ([string]$n + [string][char]61 + [string]$pay)) -join [string][char]10)",
|
|
922
|
-
'}',
|
|
923
|
-
'function fx-arrput($n, $vals) {',
|
|
924
|
-
' $n = [string]$n',
|
|
925
|
-
' $vals = @($vals)',
|
|
926
|
-
' fx-arrdrop $n',
|
|
927
|
-
' if ($vals.Count -eq 0) { } else {',
|
|
928
|
-
' $encs = @(); foreach ($v in $vals) { $encs += (fx-svenc $v) }',
|
|
929
|
-
" $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ($n + [string][char]61 + ($encs -join [string][char]30))) -join [string][char]10)",
|
|
930
|
-
' }',
|
|
931
|
-
" $fx_0 = $(if ($vals.Count -gt 0) { [string]$vals[0] } else { '' })",
|
|
932
|
-
' Set-Item -LiteralPath (\'Env:\\\' + $n) -Value $fx_0',
|
|
933
|
-
" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
|
|
934
|
-
" $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
|
|
935
|
-
' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }',
|
|
936
|
-
" $fx_sv += ($n + [string][char]61 + (fx-svenc $fx_0)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)",
|
|
937
|
-
'}',
|
|
938
|
-
'function fx-arrclr($n) {',
|
|
939
|
-
' $n = [string]$n',
|
|
940
|
-
' fx-arrdrop $n',
|
|
941
|
-
" Remove-Item -LiteralPath ('Env:\\' + $n) -ErrorAction SilentlyContinue",
|
|
942
|
-
" $env:FAUXNIX_SETVARS = (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
|
|
943
|
-
" $env:FAUXNIX_UNSETVARS = ((@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
|
|
944
|
-
' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }; $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
|
|
945
|
-
'}',
|
|
946
|
-
'function fx-subget($n, $ix) {',
|
|
947
|
-
' $arr = @(fx-arrload $n)',
|
|
948
|
-
' $ix = [string]$ix',
|
|
949
|
-
// argv-level `@` is expanded by argListExpr; this is the scalar/quoted-* join.
|
|
950
|
-
" if ($ix -eq '*') { return ($arr -join (fx-ifs1)) }",
|
|
951
|
-
" if ($ix -eq '@') { return ($arr -join (fx-ifs1)) }",
|
|
952
|
-
' $i = 0',
|
|
953
|
-
' if (-not [int]::TryParse($ix, [ref]$i)) { return \'\' }',
|
|
954
|
-
" if ($i -lt 0 -or $i -ge $arr.Count) { return '' }",
|
|
955
|
-
' return [string]$arr[$i]',
|
|
956
|
-
'}',
|
|
864
|
+
];
|
|
865
|
+
}
|
|
866
|
+
function wrapBodyAndPersist(body, exitProcess) {
|
|
867
|
+
const lines = [
|
|
957
868
|
'try {',
|
|
958
869
|
...body.split('\n').map((l) => ' ' + l),
|
|
959
870
|
'} catch [System.Management.Automation.CommandNotFoundException] {',
|
|
@@ -971,7 +882,301 @@ export function wrapScript(body) {
|
|
|
971
882
|
' Get-ChildItem Env: | ForEach-Object { $envObj[$_.Name] = $_.Value }',
|
|
972
883
|
' [IO.File]::WriteAllText($env:FAUXNIX_ENV_FILE, (ConvertTo-Json $envObj -Compress))',
|
|
973
884
|
'} catch {}',
|
|
974
|
-
'exit $script:fx_exit',
|
|
975
885
|
];
|
|
886
|
+
if (exitProcess)
|
|
887
|
+
lines.push('exit $script:fx_exit');
|
|
888
|
+
return lines;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Wrap a pipeline body with the Fauxnix executor contract:
|
|
892
|
+
* UTF-8 everywhere, bash-style exit codes, cwd/env persistence channels.
|
|
893
|
+
* Spawn mode emits only the fx- helpers the body actually calls. Host mode
|
|
894
|
+
* assumes the resident process already loaded the catalog and must not `exit`.
|
|
895
|
+
*/
|
|
896
|
+
export function wrapScript(body, opts = {}) {
|
|
897
|
+
const mode = opts.mode ?? 'spawn';
|
|
898
|
+
const needed = mode === 'host' ? new Set() : wrapHelpersNeeded(body);
|
|
899
|
+
const lines = mode === 'host'
|
|
900
|
+
? wrapCwdPreamble()
|
|
901
|
+
: [...wrapEncodingPreamble(), ...wrapCwdPreamble()];
|
|
902
|
+
const helpers = {
|
|
903
|
+
'fx-readlines': [
|
|
904
|
+
'function fx-readlines($p) {',
|
|
905
|
+
' $b = [IO.File]::ReadAllBytes($p)',
|
|
906
|
+
' $t = $null',
|
|
907
|
+
' try { $t = (New-Object System.Text.UTF8Encoding($false, $true)).GetString($b) } catch {}',
|
|
908
|
+
" if ($null -eq $t) { try { $t = [System.Text.Encoding]::GetEncoding(936).GetString($b) } catch { $t = [System.Text.Encoding]::ASCII.GetString($b) } }",
|
|
909
|
+
' $t = $t -replace "`r`n", "`n"',
|
|
910
|
+
' $t = $t -replace "`r", "`n"',
|
|
911
|
+
' $parts = @($t.Split("`n"))',
|
|
912
|
+
" if ($parts.Count -eq 1 -and $parts[0] -eq '') { return @() }",
|
|
913
|
+
" if ($parts[$parts.Count - 1] -eq '') { $parts = $parts[0..($parts.Count - 2)] }",
|
|
914
|
+
' return $parts',
|
|
915
|
+
'}',
|
|
916
|
+
],
|
|
917
|
+
'fx-csub': [
|
|
918
|
+
'function fx-csub([scriptblock]$b) {',
|
|
919
|
+
' $fx_prevcs = $script:fx_csub',
|
|
920
|
+
' $script:fx_csub = $true',
|
|
921
|
+
' try { $fx_o = @(& $b | ForEach-Object { [string]$_ }) }',
|
|
922
|
+
' finally { $script:fx_csub = $fx_prevcs }',
|
|
923
|
+
' $fx_s = ($fx_o -join [string][char]10)',
|
|
924
|
+
' while ($fx_s.Length -gt 0 -and $fx_s[$fx_s.Length - 1] -eq [char]10) {',
|
|
925
|
+
' $fx_s = $fx_s.Substring(0, $fx_s.Length - 1)',
|
|
926
|
+
' }',
|
|
927
|
+
' return $fx_s',
|
|
928
|
+
'}',
|
|
929
|
+
],
|
|
930
|
+
'fx-svenc': [
|
|
931
|
+
'function fx-svenc($s) {',
|
|
932
|
+
' return ([string]$s).Replace([string][char]92, ([string][char]92 + [string][char]92)).Replace([string][char]13, ([string][char]92 + [char]114)).Replace([string][char]10, ([string][char]92 + [char]110))',
|
|
933
|
+
'}',
|
|
934
|
+
],
|
|
935
|
+
'fx-svdec': [
|
|
936
|
+
'function fx-svdec($s) {',
|
|
937
|
+
' $s = [string]$s',
|
|
938
|
+
' $sb = New-Object System.Text.StringBuilder',
|
|
939
|
+
' $i = 0',
|
|
940
|
+
' while ($i -lt $s.Length) {',
|
|
941
|
+
' $c = $s[$i]',
|
|
942
|
+
' if ($c -eq [char]92 -and ($i + 1) -lt $s.Length) {',
|
|
943
|
+
' $n2 = $s[$i + 1]',
|
|
944
|
+
' if ($n2 -eq [char]110) { [void]$sb.Append([char]10); $i += 2; continue }',
|
|
945
|
+
' if ($n2 -eq [char]114) { [void]$sb.Append([char]13); $i += 2; continue }',
|
|
946
|
+
' if ($n2 -eq [char]92) { [void]$sb.Append([char]92); $i += 2; continue }',
|
|
947
|
+
' }',
|
|
948
|
+
' [void]$sb.Append($c)',
|
|
949
|
+
' $i++',
|
|
950
|
+
' }',
|
|
951
|
+
' return [string]$sb',
|
|
952
|
+
'}',
|
|
953
|
+
],
|
|
954
|
+
'fx-arrload': [
|
|
955
|
+
'function fx-arrload($n) {',
|
|
956
|
+
' $n = [string]$n',
|
|
957
|
+
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
958
|
+
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
959
|
+
' if ($fx_eq -lt 1) { continue }',
|
|
960
|
+
' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { continue }',
|
|
961
|
+
' $out = @()',
|
|
962
|
+
' foreach ($el in @($fx_pair.Substring($fx_eq + 1) -split [string][char]30)) { $out += ,(fx-svdec $el) }',
|
|
963
|
+
' return $out',
|
|
964
|
+
' }',
|
|
965
|
+
' $s0 = fx-scalar0 $n',
|
|
966
|
+
' if ($null -eq $s0) { return @() }',
|
|
967
|
+
' return @([string]$s0)',
|
|
968
|
+
'}',
|
|
969
|
+
],
|
|
970
|
+
'fx-scalar0': [
|
|
971
|
+
'function fx-scalar0($n) {',
|
|
972
|
+
' $n = [string]$n',
|
|
973
|
+
" if (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return $null }",
|
|
974
|
+
' foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
|
|
975
|
+
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
976
|
+
' if ($fx_eq -lt 1) { continue }',
|
|
977
|
+
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return (fx-svdec $fx_pair.Substring($fx_eq + 1)) }',
|
|
978
|
+
' }',
|
|
979
|
+
" if ($n -ceq 'HOME') { return [string]$HOME }",
|
|
980
|
+
" if ($n -ceq 'PWD') { return [string]$PWD.Path }",
|
|
981
|
+
" if ($n -ceq 'USER' -or $n -ceq 'LOGNAME') { return [string]$env:USERNAME }",
|
|
982
|
+
" if ($n -ceq 'PATH') { return [string]$env:PATH }",
|
|
983
|
+
" if ($n -ceq 'SHELL') { return 'powershell' }",
|
|
984
|
+
" if ($n -ceq 'TERM') { return 'xterm-256color' }",
|
|
985
|
+
" if ($n -ceq 'OLDPWD') { return $(if ($env:FAUXNIX_OLDPWD) { [string]$env:FAUXNIX_OLDPWD } else { $null }) }",
|
|
986
|
+
" if ($n -ceq 'HOSTNAME') { return [string]$env:COMPUTERNAME }",
|
|
987
|
+
' $ev = Get-ChildItem Env: | Where-Object { $_.Name -ceq $n } | Select-Object -First 1',
|
|
988
|
+
' if ($ev) { return [string]$ev.Value }',
|
|
989
|
+
' return $null',
|
|
990
|
+
'}',
|
|
991
|
+
],
|
|
992
|
+
'fx-ifs1': [
|
|
993
|
+
'function fx-ifs1 {',
|
|
994
|
+
" $s = fx-scalar0 'IFS'",
|
|
995
|
+
" if ($null -eq $s) { return ' ' }",
|
|
996
|
+
" if ([string]$s -eq '') { return '' }",
|
|
997
|
+
' return [string]$s[0]',
|
|
998
|
+
'}',
|
|
999
|
+
],
|
|
1000
|
+
'fx-arrdrop': [
|
|
1001
|
+
'function fx-arrdrop($n) {',
|
|
1002
|
+
' $n = [string]$n',
|
|
1003
|
+
' $fx_sm = @()',
|
|
1004
|
+
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
1005
|
+
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
1006
|
+
' if ($fx_eq -lt 1) { continue }',
|
|
1007
|
+
' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sm += $fx_pair }',
|
|
1008
|
+
' }',
|
|
1009
|
+
' $env:FAUXNIX_ARRS = ($fx_sm -join [string][char]10)',
|
|
1010
|
+
'}',
|
|
1011
|
+
],
|
|
1012
|
+
'fx-arrhas': [
|
|
1013
|
+
'function fx-arrhas($n) {',
|
|
1014
|
+
' $n = [string]$n',
|
|
1015
|
+
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
1016
|
+
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
1017
|
+
' if ($fx_eq -lt 1) { continue }',
|
|
1018
|
+
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $true }',
|
|
1019
|
+
' }',
|
|
1020
|
+
' return $false',
|
|
1021
|
+
'}',
|
|
1022
|
+
],
|
|
1023
|
+
'fx-arrpackget': [
|
|
1024
|
+
'function fx-arrpackget($n) {',
|
|
1025
|
+
' $n = [string]$n',
|
|
1026
|
+
' foreach ($fx_pair in @($env:FAUXNIX_ARRS -split [string][char]10)) {',
|
|
1027
|
+
' $fx_eq = $fx_pair.IndexOf([char]61)',
|
|
1028
|
+
' if ($fx_eq -lt 1) { continue }',
|
|
1029
|
+
' if ($fx_pair.Substring(0, $fx_eq) -ceq $n) { return $fx_pair.Substring($fx_eq + 1) }',
|
|
1030
|
+
' }',
|
|
1031
|
+
' return $null',
|
|
1032
|
+
'}',
|
|
1033
|
+
],
|
|
1034
|
+
'fx-arrpackset': [
|
|
1035
|
+
'function fx-arrpackset($n, $pay) {',
|
|
1036
|
+
' fx-arrdrop $n',
|
|
1037
|
+
' if ($null -eq $pay) { return }',
|
|
1038
|
+
" $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ([string]$n + [string][char]61 + [string]$pay)) -join [string][char]10)",
|
|
1039
|
+
'}',
|
|
1040
|
+
],
|
|
1041
|
+
'fx-arrput': [
|
|
1042
|
+
'function fx-arrput($n, $vals) {',
|
|
1043
|
+
' $n = [string]$n',
|
|
1044
|
+
' $vals = @($vals)',
|
|
1045
|
+
' fx-arrdrop $n',
|
|
1046
|
+
' if ($vals.Count -eq 0) { } else {',
|
|
1047
|
+
' $encs = @(); foreach ($v in $vals) { $encs += (fx-svenc $v) }',
|
|
1048
|
+
" $env:FAUXNIX_ARRS = ((@($env:FAUXNIX_ARRS -split [string][char]10 | Where-Object { $_ -ne '' }) + ($n + [string][char]61 + ($encs -join [string][char]30))) -join [string][char]10)",
|
|
1049
|
+
' }',
|
|
1050
|
+
" $fx_0 = $(if ($vals.Count -gt 0) { [string]$vals[0] } else { '' })",
|
|
1051
|
+
' Set-Item -LiteralPath (\'Env:\\\' + $n) -Value $fx_0',
|
|
1052
|
+
" $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
|
|
1053
|
+
" $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
|
|
1054
|
+
' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }',
|
|
1055
|
+
" $fx_sv += ($n + [string][char]61 + (fx-svenc $fx_0)); $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)",
|
|
1056
|
+
'}',
|
|
1057
|
+
],
|
|
1058
|
+
'fx-arrclr': [
|
|
1059
|
+
'function fx-arrclr($n) {',
|
|
1060
|
+
' $n = [string]$n',
|
|
1061
|
+
' fx-arrdrop $n',
|
|
1062
|
+
" Remove-Item -LiteralPath ('Env:\\' + $n) -ErrorAction SilentlyContinue",
|
|
1063
|
+
" $env:FAUXNIX_SETVARS = (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
|
|
1064
|
+
" $env:FAUXNIX_UNSETVARS = ((@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
|
|
1065
|
+
' $fx_sv = @(); foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) { $fx_eq = $fx_pair.IndexOf([char]61); if ($fx_eq -lt 1) { continue }; if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair } }; $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
|
|
1066
|
+
'}',
|
|
1067
|
+
],
|
|
1068
|
+
'fx-subget': [
|
|
1069
|
+
'function fx-subget($n, $ix) {',
|
|
1070
|
+
' $arr = @(fx-arrload $n)',
|
|
1071
|
+
' $ix = [string]$ix',
|
|
1072
|
+
// argv-level `@` is expanded by argListExpr; this is the scalar/quoted-* join.
|
|
1073
|
+
" if ($ix -eq '*') { return ($arr -join (fx-ifs1)) }",
|
|
1074
|
+
" if ($ix -eq '@') { return ($arr -join (fx-ifs1)) }",
|
|
1075
|
+
' $i = 0',
|
|
1076
|
+
' if (-not [int]::TryParse($ix, [ref]$i)) { return \'\' }',
|
|
1077
|
+
" if ($i -lt 0 -or $i -ge $arr.Count) { return '' }",
|
|
1078
|
+
' return [string]$arr[$i]',
|
|
1079
|
+
'}',
|
|
1080
|
+
],
|
|
1081
|
+
};
|
|
1082
|
+
cachedWrapHelpers = helpers;
|
|
1083
|
+
for (const name of WRAP_HELPER_ORDER) {
|
|
1084
|
+
if (needed.has(name))
|
|
1085
|
+
lines.push(...helpers[name]);
|
|
1086
|
+
}
|
|
1087
|
+
lines.push(...wrapBodyAndPersist(body, mode === 'spawn'));
|
|
976
1088
|
return lines.join('\n');
|
|
977
1089
|
}
|
|
1090
|
+
let cachedWrapHelpers = null;
|
|
1091
|
+
function wrapHelperCatalog() {
|
|
1092
|
+
if (!cachedWrapHelpers)
|
|
1093
|
+
wrapScript('');
|
|
1094
|
+
return cachedWrapHelpers;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Resident-host bootstrap: encoding + full fx-* catalog + JSON-line RPC loop.
|
|
1098
|
+
* Loaded once via `powershell.exe -File`. Must never `exit` a successful frame.
|
|
1099
|
+
*/
|
|
1100
|
+
export function hostBootstrapScript() {
|
|
1101
|
+
const helpers = wrapHelperCatalog();
|
|
1102
|
+
const helperLines = [];
|
|
1103
|
+
for (const name of WRAP_HELPER_ORDER)
|
|
1104
|
+
helperLines.push(...helpers[name]);
|
|
1105
|
+
return [
|
|
1106
|
+
...wrapEncodingPreamble(),
|
|
1107
|
+
'$script:fx_exit = 0',
|
|
1108
|
+
...helperLines,
|
|
1109
|
+
HOST_RPC_LOOP,
|
|
1110
|
+
].join('\n');
|
|
1111
|
+
}
|
|
1112
|
+
/** Raw UTF-8 JSON lines on stdin/stdout; command streams captured per frame. */
|
|
1113
|
+
const HOST_RPC_LOOP = `
|
|
1114
|
+
$fx_utf8 = New-Object System.Text.UTF8Encoding $false
|
|
1115
|
+
$fx_in = [Console]::OpenStandardInput()
|
|
1116
|
+
$fx_out = [Console]::OpenStandardOutput()
|
|
1117
|
+
$fx_reader = New-Object System.IO.StreamReader($fx_in, $fx_utf8, $true, 8192, $true)
|
|
1118
|
+
$fx_proto = New-Object System.IO.StreamWriter($fx_out, $fx_utf8, 8192, $true)
|
|
1119
|
+
$fx_proto.NewLine = [string][char]10
|
|
1120
|
+
$fx_proto.AutoFlush = $true
|
|
1121
|
+
$fx_proto.WriteLine('{"ready":true}')
|
|
1122
|
+
while ($true) {
|
|
1123
|
+
$fx_line = $fx_reader.ReadLine()
|
|
1124
|
+
if ($null -eq $fx_line) { break }
|
|
1125
|
+
if ($fx_line -eq '') { continue }
|
|
1126
|
+
$fx_id = ''
|
|
1127
|
+
$fx_msOut = $null
|
|
1128
|
+
$fx_msErr = $null
|
|
1129
|
+
$fx_outW = $null
|
|
1130
|
+
$fx_errW = $null
|
|
1131
|
+
$fx_oldOut = [Console]::Out
|
|
1132
|
+
$fx_oldErr = [Console]::Error
|
|
1133
|
+
try {
|
|
1134
|
+
$fx_req = $fx_line | ConvertFrom-Json
|
|
1135
|
+
$fx_id = [string]$fx_req.id
|
|
1136
|
+
if ($fx_req.env) {
|
|
1137
|
+
foreach ($fx_p in $fx_req.env.PSObject.Properties) {
|
|
1138
|
+
$fx_en = [string]$fx_p.Name
|
|
1139
|
+
$fx_ev = [string]$fx_p.Value
|
|
1140
|
+
if ($fx_ev -eq '') {
|
|
1141
|
+
Remove-Item -LiteralPath ('Env:\\' + $fx_en) -ErrorAction SilentlyContinue
|
|
1142
|
+
} else {
|
|
1143
|
+
Set-Item -LiteralPath ('Env:\\' + $fx_en) -Value $fx_ev
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
$fx_script = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String([string]$fx_req.scriptB64))
|
|
1148
|
+
$fx_msOut = New-Object System.IO.MemoryStream
|
|
1149
|
+
$fx_msErr = New-Object System.IO.MemoryStream
|
|
1150
|
+
$fx_outW = New-Object System.IO.StreamWriter($fx_msOut, $fx_utf8, 1024, $true)
|
|
1151
|
+
$fx_errW = New-Object System.IO.StreamWriter($fx_msErr, $fx_utf8, 1024, $true)
|
|
1152
|
+
$fx_outW.NewLine = [string][char]13 + [string][char]10
|
|
1153
|
+
$fx_errW.NewLine = [string][char]13 + [string][char]10
|
|
1154
|
+
$fx_outW.AutoFlush = $true
|
|
1155
|
+
$fx_errW.AutoFlush = $true
|
|
1156
|
+
[Console]::SetOut($fx_outW)
|
|
1157
|
+
[Console]::SetError($fx_errW)
|
|
1158
|
+
$script:fx_exit = 0
|
|
1159
|
+
$fx_sb = [scriptblock]::Create($fx_script)
|
|
1160
|
+
& $fx_sb | ForEach-Object { [Console]::Out.WriteLine([string]$_) }
|
|
1161
|
+
} catch [System.Management.Automation.CommandNotFoundException] {
|
|
1162
|
+
[Console]::Error.WriteLine('bash: ' + $_.Exception.TargetName + ': command not found')
|
|
1163
|
+
$script:fx_exit = 127
|
|
1164
|
+
} catch {
|
|
1165
|
+
[Console]::Error.WriteLine(($_.Exception.Message).Split([string][char]10)[0])
|
|
1166
|
+
$script:fx_exit = 1
|
|
1167
|
+
} finally {
|
|
1168
|
+
try { if ($null -ne $fx_outW) { $fx_outW.Flush() } } catch {}
|
|
1169
|
+
try { if ($null -ne $fx_errW) { $fx_errW.Flush() } } catch {}
|
|
1170
|
+
try { [Console]::SetOut($fx_oldOut) } catch {}
|
|
1171
|
+
try { [Console]::SetError($fx_oldErr) } catch {}
|
|
1172
|
+
}
|
|
1173
|
+
$fx_outB64 = ''
|
|
1174
|
+
$fx_errB64 = ''
|
|
1175
|
+
if ($null -ne $fx_msOut) { $fx_outB64 = [Convert]::ToBase64String($fx_msOut.ToArray()) }
|
|
1176
|
+
if ($null -ne $fx_msErr) { $fx_errB64 = [Convert]::ToBase64String($fx_msErr.ToArray()) }
|
|
1177
|
+
$fx_code = 0
|
|
1178
|
+
try { $fx_code = [int]$script:fx_exit } catch { $fx_code = 1 }
|
|
1179
|
+
$fx_res = @{ id = $fx_id; stdoutB64 = $fx_outB64; stderrB64 = $fx_errB64; exitCode = $fx_code }
|
|
1180
|
+
$fx_proto.WriteLine(($fx_res | ConvertTo-Json -Compress))
|
|
1181
|
+
}
|
|
1182
|
+
`.trim();
|
package/package.json
CHANGED