fauxnix-cli 0.7.0 → 0.7.1
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 +3 -1
- package/dist/cli.js +2 -1
- package/dist/commands/files.js +19 -3
- package/dist/commands/text-filters.js +93 -13
- package/dist/executor.js +32 -2
- package/dist/mcp.js +2 -6
- package/dist/translator.js +30 -2
- package/dist/version.d.ts +1 -0
- package/dist/version.js +8 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -67,7 +67,9 @@ npm install -g fauxnix-cli
|
|
|
67
67
|
Or from source:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
git clone https://github.com/20000419/fauxnix && cd fauxnix
|
|
70
|
+
git clone https://github.com/20000419/fauxnix && cd fauxnix
|
|
71
|
+
npm ci
|
|
72
|
+
npm install -g .
|
|
71
73
|
```
|
|
72
74
|
|
|
73
75
|
> npm package name is `fauxnix-cli` (the `fauxnix` name on npm belongs to an
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { translateCommandList } from './translator.js';
|
|
|
5
5
|
import { registeredNames } from './registry.js';
|
|
6
6
|
import { encodeCommand } from './encoding.js';
|
|
7
7
|
import { startMcpServer } from './mcp.js';
|
|
8
|
+
import { packageVersion } from './version.js';
|
|
8
9
|
import './commands/install-all.js';
|
|
9
10
|
const USAGE = `fauxnix — run Linux-style commands on Windows via PowerShell translation
|
|
10
11
|
|
|
@@ -26,7 +27,7 @@ export async function runCli(argv) {
|
|
|
26
27
|
}
|
|
27
28
|
const [verb, ...rest] = argv;
|
|
28
29
|
if (verb === '--version' || verb === '-v') {
|
|
29
|
-
console.log(
|
|
30
|
+
console.log(`fauxnix ${packageVersion}`);
|
|
30
31
|
return;
|
|
31
32
|
}
|
|
32
33
|
if (verb === 'list') {
|
package/dist/commands/files.js
CHANGED
|
@@ -492,6 +492,22 @@ const find = (args) => {
|
|
|
492
492
|
const mtimeExpr = extractValue(preds, ['-mtime']);
|
|
493
493
|
const wantDelete = preds.includes('-delete');
|
|
494
494
|
const paths = pathWords.length ? argListExpr(pathWords) : "@('.')";
|
|
495
|
+
for (const option of ['-maxdepth', '-mindepth']) {
|
|
496
|
+
const optionIndex = preds.indexOf(option);
|
|
497
|
+
if (optionIndex < 0)
|
|
498
|
+
continue;
|
|
499
|
+
if (optionIndex + 1 >= preds.length) {
|
|
500
|
+
return ('[Console]::Error.WriteLine(' +
|
|
501
|
+
psStr(`find: missing argument to '${option}'`) +
|
|
502
|
+
'); $script:fx_exit = 1');
|
|
503
|
+
}
|
|
504
|
+
const value = preds[optionIndex + 1];
|
|
505
|
+
if (!/^\d+$/.test(value)) {
|
|
506
|
+
return ('[Console]::Error.WriteLine(' +
|
|
507
|
+
psStr(`find: expected a non-negative decimal integer argument to ${option}, but got '${value}'`) +
|
|
508
|
+
'); $script:fx_exit = 1');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
495
511
|
const conditions = [];
|
|
496
512
|
if (namePat !== null)
|
|
497
513
|
conditions.push("($fx_i.Name -like '" + likeOf(namePat) + "')");
|
|
@@ -516,9 +532,9 @@ const find = (args) => {
|
|
|
516
532
|
' foreach ($fx_i in $fx_all) {',
|
|
517
533
|
" $fx_rel = $fx_i.FullName.Substring($fx_root.Length).TrimStart('\\').Replace('\\', '/')",
|
|
518
534
|
" if ($fx_rel -eq '') { $fx_disp = $fx_p } else { $fx_disp = ($fx_p.TrimEnd('/') + '/' + $fx_rel) }",
|
|
519
|
-
' $fx_depth = 0; foreach ($fx_c in $fx_rel.ToCharArray()) { if ($fx_c -eq \'/\') { $fx_depth++ } }',
|
|
520
|
-
' if ($fx_depth -lt ' + (minDepthS
|
|
521
|
-
maxDepthS
|
|
535
|
+
' $fx_depth = 0; if ($fx_rel -ne \'\') { $fx_depth = 1; foreach ($fx_c in $fx_rel.ToCharArray()) { if ($fx_c -eq \'/\') { $fx_depth++ } } }',
|
|
536
|
+
' if ($fx_depth -lt ' + (minDepthS ?? '0') + ') { continue }',
|
|
537
|
+
maxDepthS !== null ? ' if ($fx_depth -gt ' + maxDepthS + ') { continue }' : '',
|
|
522
538
|
' if (-not (' + cond + ')) { continue }',
|
|
523
539
|
sizeCond ? ' $fx_sz = 0; if (-not $fx_i.PSIsContainer) { try { $fx_sz = $fx_i.Length } catch {} }' : '',
|
|
524
540
|
sizeCond ? ' if (-not (' + sizeCond + ')) { continue }' : '',
|
|
@@ -77,7 +77,7 @@ function textExpr(w) {
|
|
|
77
77
|
return exprOfWord(w);
|
|
78
78
|
}
|
|
79
79
|
/** Collect EVERY value of a short option (-kN, -k N) — parseWords keeps only the last. */
|
|
80
|
-
function collectShortValues(args, letter
|
|
80
|
+
function collectShortValues(args, letter) {
|
|
81
81
|
const out = [];
|
|
82
82
|
let onlyOps = false;
|
|
83
83
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -88,9 +88,7 @@ function collectShortValues(args, letter, longName) {
|
|
|
88
88
|
}
|
|
89
89
|
if (onlyOps)
|
|
90
90
|
continue;
|
|
91
|
-
if (
|
|
92
|
-
out.push(t.slice(longName.length + 1));
|
|
93
|
-
else if (t === '-' + letter) {
|
|
91
|
+
if (t === '-' + letter) {
|
|
94
92
|
if (i + 1 < args.length) {
|
|
95
93
|
out.push(wordToString(args[i + 1]));
|
|
96
94
|
i++;
|
|
@@ -102,6 +100,32 @@ function collectShortValues(args, letter, longName) {
|
|
|
102
100
|
}
|
|
103
101
|
return out;
|
|
104
102
|
}
|
|
103
|
+
/** Collect repeated value-taking long options without mistaking short bundles for values. */
|
|
104
|
+
function collectLongValues(args, names) {
|
|
105
|
+
const out = [];
|
|
106
|
+
let onlyOps = false;
|
|
107
|
+
for (let i = 0; i < args.length; i++) {
|
|
108
|
+
const t = wordToString(args[i]);
|
|
109
|
+
if (t === '--') {
|
|
110
|
+
onlyOps = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (onlyOps || !t.startsWith('--'))
|
|
114
|
+
continue;
|
|
115
|
+
const eq = t.indexOf('=');
|
|
116
|
+
const name = eq >= 0 ? t.slice(0, eq) : t;
|
|
117
|
+
if (!names.includes(name))
|
|
118
|
+
continue;
|
|
119
|
+
if (eq >= 0) {
|
|
120
|
+
out.push({ name, value: t.slice(eq + 1) });
|
|
121
|
+
}
|
|
122
|
+
else if (i + 1 < args.length) {
|
|
123
|
+
out.push({ name, value: wordToString(args[i + 1]) });
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
105
129
|
/** Build the "collect file operands through fx-glob" PS prologue. */
|
|
106
130
|
function psCollectSources(filesExpr, cmdErr, leafOnly) {
|
|
107
131
|
const test = leafOnly
|
|
@@ -238,8 +262,19 @@ function ereToDotNet(re) {
|
|
|
238
262
|
/* grep */
|
|
239
263
|
/* ------------------------------------------------------------------ */
|
|
240
264
|
const grep = (args) => {
|
|
241
|
-
const
|
|
242
|
-
const
|
|
265
|
+
const filterOptionNames = ['--include', '--exclude', '--exclude-dir'];
|
|
266
|
+
const filterOptions = collectLongValues(args, filterOptionNames);
|
|
267
|
+
const fileFilterOptions = filterOptions.filter((o) => o.name !== '--exclude-dir');
|
|
268
|
+
const excludeDirGlobs = filterOptions
|
|
269
|
+
.filter((o) => o.name === '--exclude-dir')
|
|
270
|
+
.map((o) => o.value.replace(/[\\/]+$/, ''));
|
|
271
|
+
const { flags, operandWords, values, missingValue } = parseWords(args, ['A', 'B', 'C'], filterOptionNames);
|
|
272
|
+
const missingFilterOption = missingValue.find((o) => filterOptionNames.includes(o));
|
|
273
|
+
if (missingFilterOption) {
|
|
274
|
+
return ('[Console]::Error.WriteLine(' +
|
|
275
|
+
psStr("grep: option '" + missingFilterOption + "' requires an argument") +
|
|
276
|
+
'); $script:fx_exit = 2');
|
|
277
|
+
}
|
|
243
278
|
const ci = flags.has('i');
|
|
244
279
|
const inv = flags.has('v');
|
|
245
280
|
const num = flags.has('n');
|
|
@@ -320,23 +355,68 @@ const grep = (args) => {
|
|
|
320
355
|
// --- source collection -------------------------------------------------
|
|
321
356
|
if (fileWords.length > 0) {
|
|
322
357
|
lines.push(PS_GLOB_FN);
|
|
323
|
-
lines.push('$
|
|
358
|
+
lines.push('$fx_fsel = @(' +
|
|
359
|
+
fileFilterOptions
|
|
360
|
+
.map((o) => '[pscustomobject]@{ Keep = ' +
|
|
361
|
+
pb(o.name === '--include') +
|
|
362
|
+
'; Glob = ' +
|
|
363
|
+
psStr(o.value) +
|
|
364
|
+
' }')
|
|
365
|
+
.join(', ') +
|
|
366
|
+
')', '$fx_excd = @(' + excludeDirGlobs.map((g) => psStr(g)).join(', ') + ')', '$fx_srcs = @()', '$fx_err = $false', '$fx_recd = $false');
|
|
367
|
+
lines.push('function fx-globmatch($fx_name, $fx_glob, $fx_suffix) {');
|
|
368
|
+
lines.push(" $fx_n = ([string]$fx_name).Replace('\\', '/')");
|
|
369
|
+
lines.push(" $fx_p = ([string]$fx_glob).Replace('\\', '/')");
|
|
370
|
+
lines.push(' if ($fx_n -like $fx_p) { return $true }');
|
|
371
|
+
lines.push(' if ($fx_suffix) {');
|
|
372
|
+
lines.push(" $fx_slash = $fx_n.IndexOf('/')");
|
|
373
|
+
lines.push(' while ($fx_slash -ge 0 -and $fx_slash + 1 -lt $fx_n.Length) {');
|
|
374
|
+
lines.push(' $fx_n = $fx_n.Substring($fx_slash + 1)');
|
|
375
|
+
lines.push(' if ($fx_n -like $fx_p) { return $true }');
|
|
376
|
+
lines.push(" $fx_slash = $fx_n.IndexOf('/')");
|
|
377
|
+
lines.push(' }');
|
|
378
|
+
lines.push(' }');
|
|
379
|
+
lines.push(' return $false');
|
|
380
|
+
lines.push('}');
|
|
381
|
+
lines.push('function fx-anyglob($fx_name, $fx_globs, $fx_suffix) {');
|
|
382
|
+
lines.push(' foreach ($fx_glob in $fx_globs) { if (fx-globmatch $fx_name $fx_glob $fx_suffix) { return $true } }');
|
|
383
|
+
lines.push(' return $false');
|
|
384
|
+
lines.push('}');
|
|
385
|
+
lines.push('function fx-filewanted($fx_path, $fx_suffix) {');
|
|
386
|
+
lines.push(' if ($fx_fsel.Count -eq 0) { return $true }');
|
|
387
|
+
lines.push(' $fx_name = if ($fx_suffix) { [string]$fx_path } else { [IO.Path]::GetFileName([string]$fx_path) }');
|
|
388
|
+
lines.push(' $fx_keep = -not [bool]$fx_fsel[0].Keep');
|
|
389
|
+
lines.push(' foreach ($fx_rule in $fx_fsel) { if (fx-globmatch $fx_name $fx_rule.Glob $fx_suffix) { $fx_keep = [bool]$fx_rule.Keep } }');
|
|
390
|
+
lines.push(' return $fx_keep');
|
|
391
|
+
lines.push('}');
|
|
392
|
+
if (rec) {
|
|
393
|
+
lines.push('function fx-walkfiles($fx_root) {');
|
|
394
|
+
lines.push(' $fx_dirs = New-Object System.Collections.Stack');
|
|
395
|
+
lines.push(' $fx_dirs.Push([string]$fx_root)');
|
|
396
|
+
lines.push(' while ($fx_dirs.Count -gt 0) {');
|
|
397
|
+
lines.push(' $fx_cur = [string]$fx_dirs.Pop()');
|
|
398
|
+
lines.push(' foreach ($fx_item in @(Get-ChildItem -LiteralPath $fx_cur -Force -ErrorAction SilentlyContinue)) {');
|
|
399
|
+
lines.push(' if ($fx_item.PSIsContainer) {');
|
|
400
|
+
lines.push(' if (($fx_item.Attributes -band [IO.FileAttributes]::ReparsePoint) -eq 0 -and -not (fx-anyglob $fx_item.Name $fx_excd $false)) { $fx_dirs.Push($fx_item.FullName) }');
|
|
401
|
+
lines.push(' } elseif (fx-filewanted $fx_item.FullName $false) { $fx_item.FullName }');
|
|
402
|
+
lines.push(' }');
|
|
403
|
+
lines.push(' }');
|
|
404
|
+
lines.push('}');
|
|
405
|
+
}
|
|
324
406
|
lines.push('foreach ($fx_o in ' + psArray(fileWords) + ') {');
|
|
325
407
|
lines.push(' foreach ($fx_g in (fx-glob $fx_o)) {');
|
|
326
408
|
lines.push(" if (-not (Test-Path -LiteralPath $fx_g)) { [Console]::Error.WriteLine('grep: ' + $fx_g + ': No such file or directory'); $fx_err = $true; continue }");
|
|
327
409
|
lines.push(' if (Test-Path -LiteralPath $fx_g -PathType Container) {');
|
|
328
410
|
if (rec) {
|
|
411
|
+
lines.push(' $fx_dir = Get-Item -LiteralPath $fx_g');
|
|
412
|
+
lines.push(' if (fx-anyglob $fx_g $fx_excd $true) { continue }');
|
|
329
413
|
lines.push(' $fx_recd = $true');
|
|
330
|
-
lines.push(' $
|
|
331
|
-
lines.push(' if ($fx_inc.Count -gt 0) {');
|
|
332
|
-
lines.push(" $fx_subs = @($fx_subs | Where-Object { $fx_ok = $false; foreach ($fx_gi in $fx_inc) { if ($_.Name -like $fx_gi) { $fx_ok = $true; break } }; $fx_ok })");
|
|
333
|
-
lines.push(' }');
|
|
334
|
-
lines.push(' foreach ($fx_s in $fx_subs) { $fx_srcs += $fx_s.FullName }');
|
|
414
|
+
lines.push(' $fx_srcs += @(fx-walkfiles $fx_dir.FullName)');
|
|
335
415
|
}
|
|
336
416
|
else {
|
|
337
417
|
lines.push(" [Console]::Error.WriteLine('grep: ' + $fx_g + ': Is a directory'); $fx_err = $true");
|
|
338
418
|
}
|
|
339
|
-
lines.push(' }
|
|
419
|
+
lines.push(' } elseif (fx-filewanted $fx_g $true) { $fx_srcs += $fx_g }');
|
|
340
420
|
lines.push(' }');
|
|
341
421
|
lines.push('}');
|
|
342
422
|
lines.push('$fx_pre = $false');
|
package/dist/executor.js
CHANGED
|
@@ -7,6 +7,11 @@ import { decodeOutput, normalizeHostNewlines, resolveNativePref } from './encodi
|
|
|
7
7
|
import { normalizeStderr } from './errors.js';
|
|
8
8
|
import { PowerShellHost, PS_MISSING_MESSAGE } from './ps-host.js';
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
10
|
+
const DEFAULT_WINDOWS_PATHEXT = '.COM;.EXE;.BAT;.CMD';
|
|
11
|
+
function hasEnvKey(env, name) {
|
|
12
|
+
const normalized = name.toUpperCase();
|
|
13
|
+
return Object.keys(env).some((key) => key.toUpperCase() === normalized);
|
|
14
|
+
}
|
|
10
15
|
/** Resolve /dev/null and POSIX-ish literal targets to real Windows paths. */
|
|
11
16
|
function winTarget(target) {
|
|
12
17
|
const p = normalizeLiteralPath(target);
|
|
@@ -224,6 +229,14 @@ export class FauxnixSession {
|
|
|
224
229
|
else
|
|
225
230
|
env[k] = v;
|
|
226
231
|
}
|
|
232
|
+
// The MCP SDK's safe Windows stdio environment omits PATHEXT. Without it,
|
|
233
|
+
// PowerShell cannot resolve extensionless native commands such as `node`.
|
|
234
|
+
// Windows environment names are case-insensitive, so preserve any explicit
|
|
235
|
+
// spelling/value supplied by the caller and only restore the OS default
|
|
236
|
+
// when no variant is present at all.
|
|
237
|
+
if (process.platform === 'win32' && !hasEnvKey(env, 'PATHEXT')) {
|
|
238
|
+
env.PATHEXT = DEFAULT_WINDOWS_PATHEXT;
|
|
239
|
+
}
|
|
227
240
|
env.FAUXNIX_CWD_FILE = this.cwdFile;
|
|
228
241
|
env.FAUXNIX_ENV_FILE = this.envFile;
|
|
229
242
|
if (stdinFile)
|
|
@@ -249,6 +262,8 @@ export class FauxnixSession {
|
|
|
249
262
|
}
|
|
250
263
|
async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
251
264
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
265
|
+
const deadline = Date.now() + timeoutMs;
|
|
266
|
+
const timeoutMessage = '\nbash: command timed out after ' + Math.round(timeoutMs / 1000) + 's';
|
|
252
267
|
let stdout = '';
|
|
253
268
|
let stderr = '';
|
|
254
269
|
let exitCode = 0;
|
|
@@ -267,6 +282,12 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
267
282
|
continue;
|
|
268
283
|
if (plan.op === '||' && chainOk)
|
|
269
284
|
continue;
|
|
285
|
+
if (Date.now() >= deadline) {
|
|
286
|
+
stderr += timeoutMessage;
|
|
287
|
+
exitCode = 124;
|
|
288
|
+
session.prevExit = exitCode;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
270
291
|
const red = planRedirects(plan.redirects);
|
|
271
292
|
red.stdinFile = red.stdinFile ? resolveTarget(red.stdinFile) : null;
|
|
272
293
|
red.stdoutFile = red.stdoutFile ? resolveTarget(red.stdoutFile) : null;
|
|
@@ -339,12 +360,19 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
339
360
|
chainOk = false;
|
|
340
361
|
continue;
|
|
341
362
|
}
|
|
363
|
+
const remainingMs = deadline - Date.now();
|
|
364
|
+
if (remainingMs <= 0) {
|
|
365
|
+
stderr += timeoutMessage;
|
|
366
|
+
exitCode = 124;
|
|
367
|
+
session.prevExit = exitCode;
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
342
370
|
const encoded = wrapScript(plan.body, { mode: 'host' });
|
|
343
371
|
const inv = await ensureHost().invoke(encoded, {
|
|
344
372
|
FAUXNIX_CWD: currentDir,
|
|
345
373
|
FAUXNIX_PREV_EXIT: session.prevExit === null ? '' : String(session.prevExit),
|
|
346
374
|
FAUXNIX_STDIN_FILE: red.stdinFile || '',
|
|
347
|
-
},
|
|
375
|
+
}, remainingMs);
|
|
348
376
|
if (inv.spawnError === 'ENOENT') {
|
|
349
377
|
stderr += inv.stderr.toString('utf8') || PS_MISSING_MESSAGE;
|
|
350
378
|
exitCode = 127;
|
|
@@ -360,7 +388,7 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
360
388
|
let segOut = normalizeHostNewlines(decodeOutput(inv.stdout, decodePref));
|
|
361
389
|
let segErr = normalizeHostNewlines(normalizeStderr(decodeOutput(inv.stderr, decodePref)));
|
|
362
390
|
if (inv.timedOut) {
|
|
363
|
-
segErr +=
|
|
391
|
+
segErr += timeoutMessage;
|
|
364
392
|
}
|
|
365
393
|
if (red.mergeStderr) {
|
|
366
394
|
segOut += (segOut && !segOut.endsWith('\n') && segErr ? '\n' : '') + segErr;
|
|
@@ -410,6 +438,8 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
410
438
|
// not move later relative redirects.
|
|
411
439
|
if (redirectOk && session.cwd)
|
|
412
440
|
currentDir = session.cwd;
|
|
441
|
+
if (inv.timedOut)
|
|
442
|
+
break;
|
|
413
443
|
}
|
|
414
444
|
finally {
|
|
415
445
|
closePrepFds(prepFds);
|
package/dist/mcp.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
3
|
import { z } from 'zod';
|
|
6
4
|
import { FauxnixSession } from './executor.js';
|
|
7
5
|
import { parseCommand } from './parser.js';
|
|
8
6
|
import { translateCommandList, wrapScript, translatePipelineBody } from './translator.js';
|
|
9
7
|
import { registeredNames } from './registry.js';
|
|
8
|
+
import { packageVersion } from './version.js';
|
|
10
9
|
import './commands/install-all.js';
|
|
11
|
-
// single source of truth: the npm package version in package.json
|
|
12
|
-
// (src/ and dist/ sit one level below the root, so the relative path holds in both)
|
|
13
|
-
const pkgVersion = JSON.parse(readFileSync(fileURLToPath(new URL('../package.json', import.meta.url)), 'utf8')).version;
|
|
14
10
|
const TOOL_NAME = process.env.FAUXNIX_TOOL_NAME || 'bash';
|
|
15
11
|
const EXEC_ANNOTATIONS = {
|
|
16
12
|
readOnlyHint: false,
|
|
@@ -44,7 +40,7 @@ Exit codes follow bash conventions (0 ok, 1 fail, 2 usage/serious, 127 command n
|
|
|
44
40
|
|
|
45
41
|
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
42
|
export async function startMcpServer() {
|
|
47
|
-
const server = new McpServer({ name: 'fauxnix', version:
|
|
43
|
+
const server = new McpServer({ name: 'fauxnix', version: packageVersion }, { capabilities: { tools: {} } });
|
|
48
44
|
let session = new FauxnixSession();
|
|
49
45
|
await session.prewarm();
|
|
50
46
|
server.tool(TOOL_NAME, TOOL_DESCRIPTION, {
|
package/dist/translator.js
CHANGED
|
@@ -719,6 +719,8 @@ export function wrapTempEnv(sets, body, extra) {
|
|
|
719
719
|
}
|
|
720
720
|
/** Unique suffix for generated stage functions (nested pipelines included). */
|
|
721
721
|
let stageSeq = 0;
|
|
722
|
+
/** Unique suffix for generated pipeline wrappers and their local status arrays. */
|
|
723
|
+
let pipelineSeq = 0;
|
|
722
724
|
/**
|
|
723
725
|
* Pipeline body. A lone command runs as a plain script-block expression;
|
|
724
726
|
* multi-command pipelines become generated functions chained with `|`
|
|
@@ -784,6 +786,12 @@ function translateFor(cmd) {
|
|
|
784
786
|
return lines.join('\n');
|
|
785
787
|
}
|
|
786
788
|
export function translatePipelineBody(p) {
|
|
789
|
+
// Every pipeline stage needs its own status slot. Handlers deliberately use
|
|
790
|
+
// `$script:fx_exit` because their helper functions run in child scopes; in a
|
|
791
|
+
// pipeline that shared flag lets an earlier failure leak into a successful
|
|
792
|
+
// last stage. Reserve the wrapper id before translating bodies so nested
|
|
793
|
+
// command substitutions cannot reuse it.
|
|
794
|
+
const pipelineId = p.commands.length > 1 ? pipelineSeq++ : -1;
|
|
787
795
|
const bodies = [];
|
|
788
796
|
for (let i = 0; i < p.commands.length; i++) {
|
|
789
797
|
const c = p.commands[i];
|
|
@@ -801,16 +809,36 @@ export function translatePipelineBody(p) {
|
|
|
801
809
|
}
|
|
802
810
|
const names = [];
|
|
803
811
|
const defs = [];
|
|
812
|
+
const statusVar = '$fx_pipe_status' + pipelineId;
|
|
804
813
|
for (let i = 0; i < bodies.length; i++) {
|
|
805
814
|
const name = '__fx_s' + stageSeq++;
|
|
806
815
|
names.push(name);
|
|
807
|
-
const
|
|
816
|
+
const isolatedBody = bodies[i].split('$script:fx_exit').join(statusVar + '[' + i + ']');
|
|
817
|
+
const indented = isolatedBody
|
|
808
818
|
.split('\n')
|
|
809
819
|
.map((l) => (l ? ' ' + l : l))
|
|
810
820
|
.join('\n');
|
|
811
821
|
defs.push('function ' + name + ' {\n' + indented + '\n}');
|
|
812
822
|
}
|
|
813
|
-
|
|
823
|
+
const pipelineName = '__fx_p' + pipelineId;
|
|
824
|
+
const statuses = bodies.map(() => '0').join(', ');
|
|
825
|
+
const pipelineCall = names.join(' | ');
|
|
826
|
+
defs.push([
|
|
827
|
+
'function ' + pipelineName + ' {',
|
|
828
|
+
' ' + statusVar + ' = @(' + statuses + ')',
|
|
829
|
+
' try {',
|
|
830
|
+
// The wrapper forwards redirect input to stage zero. With no input,
|
|
831
|
+
// PowerShell still invokes a regular function once, which preserves the
|
|
832
|
+
// existing no-stdin pipeline behavior on Windows PowerShell 5.1.
|
|
833
|
+
' $input | ' + pipelineCall,
|
|
834
|
+
' } finally {',
|
|
835
|
+
// Bash defaults to pipefail off: only the last stage controls the list
|
|
836
|
+
// status used by a following && / || segment.
|
|
837
|
+
' $script:fx_exit = [int]' + statusVar + '[' + (bodies.length - 1) + ']',
|
|
838
|
+
' }',
|
|
839
|
+
'}',
|
|
840
|
+
].join('\n'));
|
|
841
|
+
return { defs: defs.join('\n'), call: pipelineName };
|
|
814
842
|
}
|
|
815
843
|
export function translateCommandList(list) {
|
|
816
844
|
const plans = [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const packageVersion: string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
// src/ and dist/ are both one level below the package root, so package.json is
|
|
3
|
+
// the runtime source of truth in development and in the published tarball.
|
|
4
|
+
const metadata = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
5
|
+
if (typeof metadata.version !== 'string' || metadata.version.length === 0) {
|
|
6
|
+
throw new Error('fauxnix: package.json does not contain a valid version');
|
|
7
|
+
}
|
|
8
|
+
export const packageVersion = metadata.version;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fauxnix-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Fauxnix — run Linux-style commands on Windows via deterministic PowerShell translation. No VM, no WSL. MCP server + CLI for AI agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc",
|
|
16
|
+
"prepare": "npm run build",
|
|
16
17
|
"test": "vitest run",
|
|
18
|
+
"test:package": "node scripts/package-smoke.mjs",
|
|
17
19
|
"test:watch": "vitest",
|
|
18
20
|
"typecheck": "tsc --noEmit",
|
|
19
21
|
"dev": "tsx src/index.ts"
|