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 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 && npm install -g .
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('fauxnix 0.4.0');
30
+ console.log(`fauxnix ${packageVersion}`);
30
31
  return;
31
32
  }
32
33
  if (verb === 'list') {
@@ -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 && /^\d+$/.test(minDepthS) ? minDepthS : '0') + ') { continue }',
521
- maxDepthS && /^\d+$/.test(maxDepthS) ? ' if ($fx_depth -gt ' + maxDepthS + ') { continue }' : '',
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, longName) {
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 (longName && t.startsWith(longName + '='))
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 includeGlobs = collectShortValues(args, '', '--include');
242
- const { flags, operandWords, values } = parseWords(args, ['A', 'B', 'C']);
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('$fx_inc = @(' + includeGlobs.map((g) => psStr(g)).join(', ') + ')', '$fx_srcs = @()', '$fx_err = $false', '$fx_recd = $false');
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(' $fx_subs = @(Get-ChildItem -LiteralPath $fx_g -Recurse -Force -File -ErrorAction SilentlyContinue)');
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(' } else { $fx_srcs += $fx_g }');
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
- }, timeoutMs);
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 += '\nbash: command timed out after ' + Math.round(timeoutMs / 1000) + 's';
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: pkgVersion }, { capabilities: { tools: {} } });
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, {
@@ -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 indented = bodies[i]
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
- return { defs: defs.join('\n'), call: names.join(' | ') };
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;
@@ -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.0",
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"