fauxnix-cli 0.1.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,7 @@
1
+ /**
2
+ * Decode process output: UTF-8 first (we force UTF-8 in the wrapper),
3
+ * with a GBK fallback for legacy native tools that ignore the codepage.
4
+ */
5
+ export declare function decodeOutput(buf: Buffer): string;
6
+ /** Encode a PowerShell script for -EncodedCommand (UTF-16LE base64). */
7
+ export declare function encodeCommand(script: string): string;
@@ -0,0 +1,29 @@
1
+ import iconv from 'iconv-lite';
2
+ const strictUtf8 = new TextDecoder('utf-8', { fatal: true });
3
+ /**
4
+ * Decode process output: UTF-8 first (we force UTF-8 in the wrapper),
5
+ * with a GBK fallback for legacy native tools that ignore the codepage.
6
+ */
7
+ export function decodeOutput(buf) {
8
+ if (buf.length === 0)
9
+ return '';
10
+ try {
11
+ let s = strictUtf8.decode(buf);
12
+ if (s.charCodeAt(0) === 0xfeff)
13
+ s = s.slice(1);
14
+ return s;
15
+ }
16
+ catch {
17
+ // not valid UTF-8 — assume the console ANSI codepage (GBK on zh-CN)
18
+ try {
19
+ return iconv.decode(buf, 'gbk');
20
+ }
21
+ catch {
22
+ return buf.toString('utf8');
23
+ }
24
+ }
25
+ }
26
+ /** Encode a PowerShell script for -EncodedCommand (UTF-16LE base64). */
27
+ export function encodeCommand(script) {
28
+ return Buffer.from(script, 'utf16le').toString('base64');
29
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Error normalization — make PowerShell failures look like bash failures
3
+ * so agents can pattern-match on familiar Linux error styles.
4
+ */
5
+ export declare function normalizeStderr(stderr: string): string;
package/dist/errors.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Error normalization — make PowerShell failures look like bash failures
3
+ * so agents can pattern-match on familiar Linux error styles.
4
+ */
5
+ /** Lines produced by PowerShell error formatting that bash would never show. */
6
+ const PS_NOISE = [
7
+ /^\s*\+ CategoryInfo\s*:/,
8
+ /^\s*\+ FullyQualifiedErrorId\s*:/,
9
+ /^\s*\+ .*\.ps1:? line \d+/,
10
+ /^At line:\d+ char:\d+/,
11
+ /^所在位置 行:\d+ 字符: \d+/,
12
+ /^\s*\+ ~+/,
13
+ /^\s+at [\w.]+, .+ line \d+/,
14
+ /^#< CLIXML/,
15
+ /^<Objs /,
16
+ ];
17
+ const CLIXML_MARKER = '#< CLIXML';
18
+ /** Undo .NET's CLIXML string escaping inside serialized records. */
19
+ function unescapeClixml(t) {
20
+ return t
21
+ .replace(/_x000D__x000A_/g, '\n')
22
+ .replace(/_x000A_/g, '\n')
23
+ .replace(/_x000D_/g, '')
24
+ .replace(/_x0009_/g, '\t')
25
+ .replace(/&lt;/g, '<')
26
+ .replace(/&gt;/g, '>')
27
+ .replace(/&quot;/g, '"')
28
+ .replace(/&apos;/g, "'")
29
+ .replace(/&amp;/g, '&')
30
+ .trim();
31
+ }
32
+ /**
33
+ * When stderr is redirected, powershell.exe serializes error records as
34
+ * CLIXML (`#< CLIXML` + XML). Unwrap the real message lines and drop
35
+ * progress records, so agents see plain bash-style text.
36
+ */
37
+ function extractClixml(s) {
38
+ const idx = s.indexOf(CLIXML_MARKER);
39
+ if (idx < 0)
40
+ return s;
41
+ const plain = s.slice(0, idx).trim();
42
+ const xml = s.slice(idx);
43
+ const messages = [];
44
+ // each serialized record looks like: <S S="Error">message text</S>
45
+ for (const chunk of xml.split('<S ')) {
46
+ const close = chunk.indexOf('</S>');
47
+ if (close < 0)
48
+ continue;
49
+ const open = chunk.indexOf('>');
50
+ if (open < 0 || open >= close)
51
+ continue;
52
+ const text = unescapeClixml(chunk.slice(open + 1, close));
53
+ if (text)
54
+ messages.push(text);
55
+ }
56
+ return plain ? plain + '\n' + messages.join('\n') : messages.join('\n');
57
+ }
58
+ export function normalizeStderr(stderr) {
59
+ const unwrapped = extractClixml(stderr);
60
+ const lines = unwrapped.split(/\r?\n/).filter((l) => !PS_NOISE.some((re) => re.test(l)));
61
+ const out = lines.map((line) => {
62
+ // "The term 'x' is not recognized as a name of a cmdlet, function, ..."
63
+ let m = line.match(/^The term '(.+?)' is not recognized/);
64
+ if (m)
65
+ return 'bash: ' + m[1] + ': command not found';
66
+ // zh-CN: 无法将"x"项识别为 cmdlet、函数、脚本文件或可运行程序的名称
67
+ m = line.match(/^无法将["'”]?([^"'”]+)["'”]?项识别为/);
68
+ if (m)
69
+ return 'bash: ' + m[1] + ': command not found';
70
+ // "x : The term 'y' is not recognized ..." (with source prefix)
71
+ m = line.match(/^(\S+)\s*:\s*The term '(.+?)' is not recognized/);
72
+ if (m)
73
+ return 'bash: ' + m[2] + ': command not found';
74
+ // "cat : Cannot find path 'D:\x' because it does not exist."
75
+ m = line.match(/^(\S+)\s*:\s*Cannot find path '(.+?)' because it does not exist\.?$/);
76
+ if (m) {
77
+ const cmd = m[1].toLowerCase();
78
+ return (cmd +
79
+ ': ' +
80
+ m[2].replace(/\\/g, '/') +
81
+ ': No such file or directory');
82
+ }
83
+ // zh-CN: "Get-Content : 找不到路径“X”,因为该路径不存在。"
84
+ m = line.match(/^(\S+)\s*:\s*找不到路径[“'"](.+?)[”'"],因为该路径不存在\.?$/);
85
+ if (m) {
86
+ return m[1].toLowerCase() + ': ' + m[2].replace(/\\/g, '/') + ': No such file or directory';
87
+ }
88
+ // "cat : Cannot find drive. A drive with the name 'z' does not exist."
89
+ m = line.match(/^(\S+)\s*:\s*Cannot find drive\..*name '(.+?)'.*$/);
90
+ if (m)
91
+ return m[1].toLowerCase() + ': ' + m[2] + ': No such file or directory';
92
+ // "rm : Cannot remove item ... Access is denied"
93
+ m = line.match(/^(\S+)\s*:\s*(.*)Access to the path '(.+?)' is denied\.?$/);
94
+ if (m)
95
+ return m[1].toLowerCase() + ': cannot remove \'' + m[3] + '\': Permission denied';
96
+ // helpful hint for bash scripts
97
+ if (/\.sh'?/.test(line) && /is not recognized/.test(line)) {
98
+ return line + ' (fauxnix: .sh scripts cannot run natively on Windows)';
99
+ }
100
+ return line;
101
+ });
102
+ return out.join('\n').replace(/\n{3,}/g, '\n\n').trim();
103
+ }
@@ -0,0 +1,27 @@
1
+ import { SegmentPlan } from './translator.js';
2
+ export interface ExecResult {
3
+ stdout: string;
4
+ stderr: string;
5
+ exitCode: number;
6
+ }
7
+ export interface ExecOptions {
8
+ timeoutMs?: number;
9
+ /** Extra environment layered over the session (used by MCP per-call cwd). */
10
+ cwd?: string;
11
+ }
12
+ /** Session persists cwd and env across segments, like a real shell. */
13
+ export declare class FauxnixSession {
14
+ cwd: string | null;
15
+ env: Record<string, string>;
16
+ /** Exit code of the previous segment — powers bash's `$?`. */
17
+ prevExit: number | null;
18
+ private cwdFile;
19
+ private envFile;
20
+ private scriptFile;
21
+ constructor();
22
+ private syncFromDisk;
23
+ dispose(): Promise<void>;
24
+ /** env for the child powershell process. */
25
+ childEnv(cwdOverride?: string, stdinFile?: string | null): NodeJS.ProcessEnv;
26
+ run(plans: SegmentPlan[], opts?: ExecOptions): Promise<ExecResult>;
27
+ }
@@ -0,0 +1,299 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { promises as fs, readFileSync, writeFileSync, existsSync } from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { normalizeLiteralPath } from './translator.js';
7
+ import { decodeOutput, encodeCommand } from './encoding.js';
8
+ import { normalizeStderr } from './errors.js';
9
+ const DEFAULT_TIMEOUT_MS = 120_000;
10
+ const PS_ARGS = ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass'];
11
+ /** Resolve /dev/null and POSIX-ish literal targets to real Windows paths. */
12
+ function winTarget(target) {
13
+ const p = normalizeLiteralPath(target);
14
+ if (p === '$env:TEMP')
15
+ return os.tmpdir();
16
+ if (p.startsWith('$env:TEMP\\'))
17
+ return path.join(os.tmpdir(), p.slice('$env:TEMP\\'.length));
18
+ return p;
19
+ }
20
+ function planRedirects(redirects) {
21
+ const r = {
22
+ stdinFile: null,
23
+ stdoutFile: null,
24
+ appendStdout: false,
25
+ stderrFile: null,
26
+ appendStderr: false,
27
+ mergeStderr: false,
28
+ devNull: false,
29
+ };
30
+ for (const red of redirects) {
31
+ const target = winTarget(red.target);
32
+ switch (red.op) {
33
+ case '<':
34
+ r.stdinFile = target;
35
+ break;
36
+ case '>':
37
+ case '&>':
38
+ if (target === 'NUL')
39
+ r.devNull = true;
40
+ else {
41
+ r.stdoutFile = target;
42
+ r.appendStdout = false;
43
+ if (red.op === '&>')
44
+ r.stderrFile = target;
45
+ }
46
+ break;
47
+ case '>>':
48
+ case '&>>':
49
+ if (target === 'NUL')
50
+ r.devNull = true;
51
+ else {
52
+ r.stdoutFile = target;
53
+ r.appendStdout = true;
54
+ if (red.op === '&>>') {
55
+ r.stderrFile = target;
56
+ r.appendStderr = true;
57
+ }
58
+ }
59
+ break;
60
+ case '2>':
61
+ if (target === 'NUL') {
62
+ // 2>/dev/null swallows stderr only
63
+ r.stderrFile = null;
64
+ r.devNull = false;
65
+ r.swallowStderr = true;
66
+ }
67
+ else {
68
+ r.stderrFile = target;
69
+ r.appendStderr = false;
70
+ }
71
+ break;
72
+ case '2>>':
73
+ if (target !== 'NUL') {
74
+ r.stderrFile = target;
75
+ r.appendStderr = true;
76
+ }
77
+ break;
78
+ case '2>&1':
79
+ r.mergeStderr = true;
80
+ break;
81
+ case '1>&2':
82
+ r.mergeStderr = false;
83
+ r.stdoutToStderr = true;
84
+ break;
85
+ }
86
+ }
87
+ return r;
88
+ }
89
+ /** Session persists cwd and env across segments, like a real shell. */
90
+ export class FauxnixSession {
91
+ cwd = null;
92
+ env = {};
93
+ /** Exit code of the previous segment — powers bash's `$?`. */
94
+ prevExit = null;
95
+ cwdFile;
96
+ envFile;
97
+ scriptFile;
98
+ constructor() {
99
+ const id = randomUUID().slice(0, 8);
100
+ this.cwdFile = path.join(os.tmpdir(), 'fauxnix-' + id + '-cwd.txt');
101
+ this.envFile = path.join(os.tmpdir(), 'fauxnix-' + id + '-env.json');
102
+ this.scriptFile = path.join(os.tmpdir(), 'fauxnix-' + id + '-script.ps1');
103
+ }
104
+ syncFromDisk() {
105
+ try {
106
+ if (existsSync(this.cwdFile)) {
107
+ const c = readFileSync(this.cwdFile, 'utf8').trim();
108
+ if (c)
109
+ this.cwd = c;
110
+ }
111
+ }
112
+ catch {
113
+ /* ignore */
114
+ }
115
+ try {
116
+ if (existsSync(this.envFile)) {
117
+ const raw = readFileSync(this.envFile, 'utf8');
118
+ if (raw.trim())
119
+ this.env = JSON.parse(raw);
120
+ }
121
+ }
122
+ catch {
123
+ /* ignore */
124
+ }
125
+ }
126
+ async dispose() {
127
+ await Promise.allSettled([
128
+ fs.rm(this.cwdFile, { force: true }),
129
+ fs.rm(this.envFile, { force: true }),
130
+ fs.rm(this.scriptFile, { force: true }),
131
+ ]);
132
+ }
133
+ /** env for the child powershell process. */
134
+ childEnv(cwdOverride, stdinFile) {
135
+ const env = { ...process.env };
136
+ for (const [k, v] of Object.entries(this.env)) {
137
+ if (v === undefined)
138
+ delete env[k];
139
+ else
140
+ env[k] = v;
141
+ }
142
+ env.FAUXNIX_CWD_FILE = this.cwdFile;
143
+ env.FAUXNIX_ENV_FILE = this.envFile;
144
+ if (stdinFile)
145
+ env.FAUXNIX_STDIN_FILE = stdinFile;
146
+ else
147
+ delete env.FAUXNIX_STDIN_FILE;
148
+ if (this.prevExit !== null)
149
+ env.FAUXNIX_PREV_EXIT = String(this.prevExit);
150
+ else
151
+ delete env.FAUXNIX_PREV_EXIT;
152
+ const cwd = cwdOverride ?? this.cwd;
153
+ if (cwd)
154
+ env.FAUXNIX_CWD = cwd;
155
+ else
156
+ delete env.FAUXNIX_CWD;
157
+ return env;
158
+ }
159
+ run(plans, opts = {}) {
160
+ return runPlans(plans, this, opts, () => this.syncFromDisk(), this.scriptFile);
161
+ }
162
+ }
163
+ function killTree(pid) {
164
+ if (!pid)
165
+ return;
166
+ try {
167
+ spawn('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });
168
+ }
169
+ catch {
170
+ /* best effort */
171
+ }
172
+ }
173
+ async function runPlans(plans, session, opts, afterSegment, scriptFile) {
174
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
175
+ let stdout = '';
176
+ let stderr = '';
177
+ let exitCode = 0;
178
+ // bash list semantics: `a && b ; c` runs c regardless of a; `a && b && c`
179
+ // skips b AND c when a fails. chainOk models the value of the current
180
+ // &&/|| chain; `;` segments always run and restart the chain.
181
+ let chainOk = true;
182
+ // redirect targets are relative to the session cwd, not this node process
183
+ const baseDir = opts.cwd ?? session.cwd ?? process.cwd();
184
+ const resolveTarget = (t) => path.isAbsolute(t) || /^[A-Za-z]:[\\/]/.test(t) ? t : path.resolve(baseDir, t);
185
+ for (const plan of plans) {
186
+ if (plan.op === '&&' && !chainOk)
187
+ continue;
188
+ if (plan.op === '||' && chainOk)
189
+ continue;
190
+ const red = planRedirects(plan.redirects);
191
+ red.stdinFile = red.stdinFile ? resolveTarget(red.stdinFile) : null;
192
+ red.stdoutFile = red.stdoutFile ? resolveTarget(red.stdoutFile) : null;
193
+ red.stderrFile = red.stderrFile ? resolveTarget(red.stderrFile) : null;
194
+ // bash: a missing `< file` target aborts the segment before running it
195
+ if (red.stdinFile && !existsSync(red.stdinFile)) {
196
+ stderr += 'bash: ' + red.stdinFile + ': No such file or directory\n';
197
+ exitCode = 1;
198
+ session.prevExit = exitCode;
199
+ chainOk = false;
200
+ continue;
201
+ }
202
+ const encoded = encodeCommand(plan.script);
203
+ // -EncodedCommand is capped by the ~32K command-line limit; heavy
204
+ // pipelines fall back to a UTF-8 BOM temp script (PS 5.1 honors the BOM
205
+ // regardless of the console codepage, so non-ASCII stays intact).
206
+ let psArgs;
207
+ if (encoded.length > 28000) {
208
+ writeFileSync(scriptFile, '\ufeff' + plan.script, 'utf8');
209
+ psArgs = [...PS_ARGS, '-File', scriptFile];
210
+ }
211
+ else {
212
+ psArgs = [...PS_ARGS, '-EncodedCommand', encoded];
213
+ }
214
+ const child = spawn('powershell.exe', psArgs, {
215
+ env: session.childEnv(opts.cwd, red.stdinFile),
216
+ stdio: ['pipe', 'pipe', 'pipe'],
217
+ windowsHide: true,
218
+ });
219
+ const running = { proc: child, killed: false };
220
+ const outBufs = [];
221
+ const errBufs = [];
222
+ child.stdout.on('data', (d) => outBufs.push(d));
223
+ child.stderr.on('data', (d) => errBufs.push(d));
224
+ child.stdin.end();
225
+ const timer = setTimeout(() => {
226
+ running.killed = true;
227
+ killTree(child.pid);
228
+ }, timeoutMs);
229
+ const code = await new Promise((resolve) => {
230
+ child.on('error', (e) => {
231
+ stderr += 'fauxnix: failed to start powershell.exe: ' + e.message + '\n';
232
+ resolve(127);
233
+ });
234
+ child.on('close', (c) => resolve(running.killed ? 124 : (c ?? 0)));
235
+ });
236
+ clearTimeout(timer);
237
+ afterSegment();
238
+ let segOut = decodeOutput(Buffer.concat(outBufs));
239
+ let segErr = normalizeStderr(decodeOutput(Buffer.concat(errBufs)));
240
+ if (running.killed) {
241
+ segErr += '\nbash: command timed out after ' + Math.round(timeoutMs / 1000) + 's';
242
+ }
243
+ if (red.mergeStderr) {
244
+ segOut += (segOut && !segOut.endsWith('\n') && segErr ? '\n' : '') + segErr;
245
+ segErr = '';
246
+ }
247
+ const stdoutToStderr = red.stdoutToStderr;
248
+ if (stdoutToStderr) {
249
+ segErr += segOut;
250
+ segOut = '';
251
+ }
252
+ const swallowStderr = red.swallowStderr;
253
+ if (swallowStderr)
254
+ segErr = '';
255
+ // redirect stdout to file instead of the result stream
256
+ if (red.stdoutFile) {
257
+ try {
258
+ if (red.appendStdout) {
259
+ const prev = existsSync(red.stdoutFile) ? readFileSync(red.stdoutFile) : Buffer.alloc(0);
260
+ writeFileSync(red.stdoutFile, Buffer.concat([prev, Buffer.from(segOut, 'utf8')]));
261
+ }
262
+ else {
263
+ writeFileSync(red.stdoutFile, segOut, 'utf8');
264
+ }
265
+ segOut = '';
266
+ }
267
+ catch (e) {
268
+ segErr += 'bash: ' + red.stdoutFile + ': cannot create: ' + e.message + '\n';
269
+ exitCode = 1;
270
+ }
271
+ }
272
+ if (red.stderrFile) {
273
+ try {
274
+ const body = red.stderrFile === red.stdoutFile ? segOut + segErr : segErr;
275
+ if (red.appendStderr && existsSync(red.stderrFile)) {
276
+ const prev = readFileSync(red.stderrFile);
277
+ writeFileSync(red.stderrFile, Buffer.concat([prev, Buffer.from(body, 'utf8')]));
278
+ }
279
+ else if (red.stderrFile === red.stdoutFile && existsSync(red.stderrFile)) {
280
+ const prev = readFileSync(red.stderrFile);
281
+ writeFileSync(red.stderrFile, Buffer.concat([prev, Buffer.from(body, 'utf8')]));
282
+ }
283
+ else {
284
+ writeFileSync(red.stderrFile, body, 'utf8');
285
+ }
286
+ segErr = '';
287
+ }
288
+ catch {
289
+ /* best effort */
290
+ }
291
+ }
292
+ stdout += segOut;
293
+ stderr += segErr;
294
+ exitCode = code ?? 0;
295
+ session.prevExit = exitCode;
296
+ chainOk = exitCode === 0;
297
+ }
298
+ return { stdout, stderr, exitCode };
299
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from './cli.js';
3
+ runCli(process.argv.slice(2)).catch((e) => {
4
+ console.error(e instanceof Error ? e.message : String(e));
5
+ process.exit(1);
6
+ });
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { translatePipelineBody } from './translator.js';
2
+ import './commands/install-all.js';
3
+ export declare function startMcpServer(): Promise<void>;
4
+ export { translatePipelineBody };
package/dist/mcp.js ADDED
@@ -0,0 +1,79 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { FauxnixSession } from './executor.js';
5
+ import { parseCommand } from './parser.js';
6
+ import { translateCommandList, wrapScript, translatePipelineBody } from './translator.js';
7
+ import { registeredNames } from './registry.js';
8
+ import './commands/install-all.js';
9
+ const TOOL_NAME = process.env.FAUXNIX_TOOL_NAME || 'bash';
10
+ const TOOL_DESCRIPTION = `Execute a Linux/bash-style command on this Windows machine.
11
+
12
+ Commands are deterministically translated to PowerShell and executed natively — no WSL or VM.
13
+ 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.
14
+
15
+ Supported: pipes (|), && / || / ;, redirections (> >> 2> 2>&1 < /dev/null), variables ($VAR $HOME ~), command substitution $(...), and ${registeredNames().length}+ coreutils-style commands (${registeredNames().slice(0, 18).join(', ')}...).
16
+ Unknown commands (git, node, npm, python, cargo...) are passed through and executed natively with argv-style quoting.
17
+ Not supported: heredocs, backticks, control flow (if/for/while), background jobs.
18
+
19
+ CWD, environment variables, export/unset and cd persist across calls within this session.
20
+ Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command not found, 124 timeout).`;
21
+ export async function startMcpServer() {
22
+ const server = new McpServer({ name: 'fauxnix', version: '0.1.0' }, { capabilities: { tools: {} } });
23
+ const session = new FauxnixSession();
24
+ server.tool(TOOL_NAME, TOOL_DESCRIPTION, {
25
+ command: z.string().describe('The bash-style command line to run'),
26
+ timeout_ms: z
27
+ .number()
28
+ .int()
29
+ .min(1000)
30
+ .max(600_000)
31
+ .optional()
32
+ .describe('Timeout in milliseconds (default 120000)'),
33
+ }, async ({ command, timeout_ms }) => {
34
+ try {
35
+ const plans = translateCommandList(parseCommand(command));
36
+ const result = await session.run(plans, { timeoutMs: timeout_ms });
37
+ const parts = [];
38
+ if (result.stdout.trim())
39
+ parts.push(result.stdout.replace(/\n$/, ''));
40
+ if (result.stderr.trim())
41
+ parts.push(result.stderr.replace(/\n$/, ''));
42
+ if (result.exitCode !== 0)
43
+ parts.push('Exit code: ' + result.exitCode);
44
+ const text = parts.length ? parts.join('\n') : '(no output)';
45
+ return { content: [{ type: 'text', text }] };
46
+ }
47
+ catch (e) {
48
+ const msg = e instanceof Error ? e.message : String(e);
49
+ return { content: [{ type: 'text', text: msg }], isError: true };
50
+ }
51
+ });
52
+ server.tool('fauxnix_translate', 'Translate a bash-style command into the equivalent PowerShell script WITHOUT executing it. Useful for learning/debugging what fauxnix does under the hood.', { command: z.string() }, async ({ command }) => {
53
+ try {
54
+ const list = parseCommand(command);
55
+ const plans = translateCommandList(list);
56
+ const script = wrapScript(plans.map((p) => p.script).join('\n# ---- next segment ----\n'));
57
+ return { content: [{ type: 'text', text: script }] };
58
+ }
59
+ catch (e) {
60
+ const msg = e instanceof Error ? e.message : String(e);
61
+ return { content: [{ type: 'text', text: msg }], isError: true };
62
+ }
63
+ });
64
+ server.tool('fauxnix_session', 'Inspect or reset the persistent fauxnix shell session (current directory, environment, session id). Actions: "status" (default) or "reset".', { action: z.enum(['status', 'reset']).default('status') }, async ({ action }) => {
65
+ if (action === 'reset') {
66
+ await session.dispose();
67
+ return { content: [{ type: 'text', text: 'fauxnix: session reset' }] };
68
+ }
69
+ const envKeys = Object.keys(session.env).sort();
70
+ const text = 'cwd: ' + (session.cwd ?? '(inherit from server start)') +
71
+ '\nenv keys: ' + (envKeys.length ? envKeys.join(', ') : '(none tracked)') +
72
+ '\ncommands registered: ' + registeredNames().length;
73
+ return { content: [{ type: 'text', text }] };
74
+ });
75
+ const transport = new StdioServerTransport();
76
+ await server.connect(transport);
77
+ }
78
+ // keep referenced for tree-shaking clarity
79
+ export { translatePipelineBody };
@@ -0,0 +1,11 @@
1
+ import { CommandList, WordPart } from './ast.js';
2
+ type TokType = 'WORD' | 'OP' | 'EOF';
3
+ interface Token {
4
+ type: TokType;
5
+ /** For WORD: the parsed parts. For OP: the operator text. */
6
+ op?: string;
7
+ parts?: WordPart[];
8
+ }
9
+ export declare function tokenize(input: string): Token[];
10
+ export declare function parseCommand(input: string): CommandList;
11
+ export {};