fauxnix-cli 0.7.0 → 0.8.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 +8 -1
- package/dist/cli.js +8 -2
- package/dist/commands/files.d.ts +4 -1
- package/dist/commands/files.js +277 -54
- package/dist/commands/install-all.js +5 -3
- package/dist/commands/sysinfo.js +3 -2
- package/dist/commands/text-filters.js +93 -13
- package/dist/commands/text-io.d.ts +2 -1
- package/dist/commands/text-io.js +14 -3
- package/dist/executor.d.ts +13 -1
- package/dist/executor.js +114 -14
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +81 -24
- package/dist/parser.js +55 -24
- package/dist/ps-host.d.ts +7 -1
- package/dist/ps-host.js +75 -8
- package/dist/registry.d.ts +50 -0
- package/dist/registry.js +142 -0
- package/dist/translator.js +38 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +8 -0
- package/package.json +3 -1
package/dist/parser.js
CHANGED
|
@@ -586,28 +586,62 @@ export function parseCommand(input) {
|
|
|
586
586
|
let pos = 0;
|
|
587
587
|
const peek = () => tokens[pos];
|
|
588
588
|
const next = () => tokens[pos++];
|
|
589
|
+
const isListSep = (o) => o === ';' || o === '\n';
|
|
590
|
+
/** Consume `;` / newline / `&&` / `||`. Trailing `&&`/`||` and `;;` fail loud (bash). */
|
|
591
|
+
const consumeListOp = (stops) => {
|
|
592
|
+
const t = peek();
|
|
593
|
+
if (t.type !== 'OP')
|
|
594
|
+
return null;
|
|
595
|
+
if (!(t.op === '&&' || t.op === '||' || isListSep(t.op)))
|
|
596
|
+
return null;
|
|
597
|
+
if (t.op === ';') {
|
|
598
|
+
next();
|
|
599
|
+
if (peek().type === 'OP' && peek().op === ';') {
|
|
600
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `;;'");
|
|
601
|
+
}
|
|
602
|
+
return ';';
|
|
603
|
+
}
|
|
604
|
+
if (t.op === '\n') {
|
|
605
|
+
next();
|
|
606
|
+
while (peek().type === 'OP' && peek().op === '\n')
|
|
607
|
+
next();
|
|
608
|
+
return ';';
|
|
609
|
+
}
|
|
610
|
+
const sep = t.op;
|
|
611
|
+
next();
|
|
612
|
+
while (peek().type === 'OP' && peek().op === '\n')
|
|
613
|
+
next();
|
|
614
|
+
const n = peek();
|
|
615
|
+
const kw = peekKw();
|
|
616
|
+
if (n.type === 'OP' && n.op === ';') {
|
|
617
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `" + sep + "'");
|
|
618
|
+
}
|
|
619
|
+
if (n.type === 'EOF' || (stops && kw && stops.has(kw))) {
|
|
620
|
+
throw new FauxnixParseError(n.type === 'EOF'
|
|
621
|
+
? 'fauxnix: syntax error: unexpected end of file after `' + sep + "'"
|
|
622
|
+
: "fauxnix: syntax error near unexpected token `" + sep + "'");
|
|
623
|
+
}
|
|
624
|
+
return sep;
|
|
625
|
+
};
|
|
589
626
|
const parseList = () => {
|
|
590
627
|
const segments = [];
|
|
591
628
|
let op = ';';
|
|
592
|
-
|
|
593
|
-
|
|
629
|
+
while (peek().type === 'OP' && isListSep(peek().op)) {
|
|
630
|
+
if (peek().op === ';' && tokens[pos + 1]?.type === 'OP' && tokens[pos + 1]?.op === ';') {
|
|
631
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `;;'");
|
|
632
|
+
}
|
|
594
633
|
next();
|
|
634
|
+
}
|
|
595
635
|
while (peek().type !== 'EOF') {
|
|
596
636
|
const pipeline = parsePipeline();
|
|
597
637
|
segments.push({ pipeline, op });
|
|
598
|
-
const
|
|
599
|
-
if (
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
while (peek().type === 'OP' && isListSep(peek().op))
|
|
603
|
-
next();
|
|
604
|
-
}
|
|
605
|
-
else if (t.type === 'EOF') {
|
|
606
|
-
break;
|
|
607
|
-
}
|
|
608
|
-
else {
|
|
638
|
+
const nextOp = consumeListOp();
|
|
639
|
+
if (nextOp === null) {
|
|
640
|
+
if (peek().type === 'EOF')
|
|
641
|
+
break;
|
|
609
642
|
throw new FauxnixParseError('fauxnix: unexpected token after pipeline');
|
|
610
643
|
}
|
|
644
|
+
op = nextOp;
|
|
611
645
|
}
|
|
612
646
|
if (segments.length === 0)
|
|
613
647
|
throw new FauxnixParseError('fauxnix: empty command');
|
|
@@ -672,25 +706,22 @@ export function parseCommand(input) {
|
|
|
672
706
|
const stop = new Set(stops);
|
|
673
707
|
const segments = [];
|
|
674
708
|
let op = ';';
|
|
675
|
-
|
|
676
|
-
|
|
709
|
+
while (peek().type === 'OP' && isListSep(peek().op)) {
|
|
710
|
+
if (peek().op === ';' && tokens[pos + 1]?.type === 'OP' && tokens[pos + 1]?.op === ';') {
|
|
711
|
+
throw new FauxnixParseError("fauxnix: syntax error near unexpected token `;;'");
|
|
712
|
+
}
|
|
677
713
|
next();
|
|
714
|
+
}
|
|
678
715
|
while (peek().type !== 'EOF') {
|
|
679
716
|
const kw = peekKw();
|
|
680
717
|
if (kw && stop.has(kw))
|
|
681
718
|
break;
|
|
682
719
|
const pipeline = parsePipeline();
|
|
683
720
|
segments.push({ pipeline, op });
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
686
|
-
op = t.op === '\n' ? ';' : t.op;
|
|
687
|
-
next();
|
|
688
|
-
while (peek().type === 'OP' && isListSep(peek().op))
|
|
689
|
-
next();
|
|
690
|
-
}
|
|
691
|
-
else {
|
|
721
|
+
const nextOp = consumeListOp(stop);
|
|
722
|
+
if (nextOp === null)
|
|
692
723
|
break;
|
|
693
|
-
|
|
724
|
+
op = nextOp;
|
|
694
725
|
}
|
|
695
726
|
if (segments.length === 0) {
|
|
696
727
|
throw new FauxnixParseError('fauxnix: empty command');
|
package/dist/ps-host.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export declare const PS_MISSING_MESSAGE: string;
|
|
2
|
+
export declare const DEFAULT_STDOUT_LIMIT = 8388608;
|
|
3
|
+
export declare const DEFAULT_STDERR_LIMIT = 1048576;
|
|
2
4
|
export interface HostInvokeResult {
|
|
3
5
|
stdout: Buffer;
|
|
4
6
|
stderr: Buffer;
|
|
5
7
|
exitCode: number;
|
|
6
8
|
timedOut: boolean;
|
|
9
|
+
cancelled: boolean;
|
|
10
|
+
truncated: boolean;
|
|
7
11
|
spawnError?: 'ENOENT' | 'START';
|
|
8
12
|
spawnMessage?: string;
|
|
9
13
|
}
|
|
@@ -39,8 +43,10 @@ export declare class PowerShellHost {
|
|
|
39
43
|
constructor(hostFile: string, envFn: () => NodeJS.ProcessEnv);
|
|
40
44
|
/** Start the resident process and wait for the ready handshake (B1 prewarm). */
|
|
41
45
|
ready(): Promise<HostInvokeResult | null>;
|
|
42
|
-
invoke(script: string, env: HostRequestEnv, timeoutMs: number): Promise<HostInvokeResult>;
|
|
46
|
+
invoke(script: string, env: HostRequestEnv, timeoutMs: number, signal?: AbortSignal): Promise<HostInvokeResult>;
|
|
47
|
+
drainNativeStderr(): Buffer;
|
|
43
48
|
stop(): Promise<void>;
|
|
49
|
+
private cancelledResult;
|
|
44
50
|
private invokeSerial;
|
|
45
51
|
private ensureStarted;
|
|
46
52
|
private deadRestart;
|
package/dist/ps-host.js
CHANGED
|
@@ -6,6 +6,8 @@ const READY_TIMEOUT_MS = 30_000;
|
|
|
6
6
|
export const PS_MISSING_MESSAGE = 'fauxnix: powershell.exe not found — fauxnix executes bash via native Windows PowerShell 5.1+.\n' +
|
|
7
7
|
'This host has no PowerShell on PATH (typical for Linux containers/sandboxes).\n' +
|
|
8
8
|
'Run fauxnix on Windows, or install PowerShell and make powershell.exe reachable on PATH.\n';
|
|
9
|
+
export const DEFAULT_STDOUT_LIMIT = 8_388_608;
|
|
10
|
+
export const DEFAULT_STDERR_LIMIT = 1_048_576;
|
|
9
11
|
export function encodeHostRequest(id, script, env) {
|
|
10
12
|
return JSON.stringify({
|
|
11
13
|
id,
|
|
@@ -50,11 +52,18 @@ export class PowerShellHost {
|
|
|
50
52
|
async ready() {
|
|
51
53
|
return this.ensureStarted();
|
|
52
54
|
}
|
|
53
|
-
async invoke(script, env, timeoutMs) {
|
|
54
|
-
const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs));
|
|
55
|
+
async invoke(script, env, timeoutMs, signal) {
|
|
56
|
+
const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs, signal));
|
|
55
57
|
this.invokeLock = run.then(() => undefined, () => undefined);
|
|
56
58
|
return run;
|
|
57
59
|
}
|
|
60
|
+
drainNativeStderr() {
|
|
61
|
+
if (!this.stderrChunks.length)
|
|
62
|
+
return Buffer.alloc(0);
|
|
63
|
+
const b = Buffer.concat(this.stderrChunks);
|
|
64
|
+
this.stderrChunks = [];
|
|
65
|
+
return b;
|
|
66
|
+
}
|
|
58
67
|
async stop() {
|
|
59
68
|
const proc = this.proc;
|
|
60
69
|
this.proc = null;
|
|
@@ -82,10 +91,24 @@ export class PowerShellHost {
|
|
|
82
91
|
});
|
|
83
92
|
});
|
|
84
93
|
}
|
|
85
|
-
|
|
94
|
+
cancelledResult() {
|
|
95
|
+
return {
|
|
96
|
+
stdout: Buffer.alloc(0),
|
|
97
|
+
stderr: Buffer.alloc(0),
|
|
98
|
+
exitCode: 130,
|
|
99
|
+
timedOut: false,
|
|
100
|
+
cancelled: true,
|
|
101
|
+
truncated: false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async invokeSerial(script, env, timeoutMs, signal) {
|
|
105
|
+
if (signal?.aborted) {
|
|
106
|
+
await this.stop();
|
|
107
|
+
return this.cancelledResult();
|
|
108
|
+
}
|
|
86
109
|
const started = await this.ensureStarted();
|
|
87
110
|
if (started)
|
|
88
|
-
return started;
|
|
111
|
+
return { ...started, cancelled: false, truncated: false };
|
|
89
112
|
const id = 'f' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
90
113
|
const line = encodeHostRequest(id, script, env);
|
|
91
114
|
try {
|
|
@@ -98,24 +121,45 @@ export class PowerShellHost {
|
|
|
98
121
|
stderr: Buffer.from('fauxnix: powershell host exited unexpectedly\n', 'utf8'),
|
|
99
122
|
exitCode: 1,
|
|
100
123
|
timedOut: false,
|
|
124
|
+
cancelled: false,
|
|
125
|
+
truncated: false,
|
|
101
126
|
spawnMessage: e.message,
|
|
102
127
|
};
|
|
103
128
|
}
|
|
129
|
+
let cancelled = false;
|
|
130
|
+
const onAbort = () => {
|
|
131
|
+
cancelled = true;
|
|
132
|
+
void this.stop();
|
|
133
|
+
};
|
|
134
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
104
135
|
try {
|
|
105
136
|
const raw = await this.nextJsonLine(timeoutMs, id);
|
|
106
137
|
const msg = decodeHostResponse(raw);
|
|
138
|
+
const native = this.drainNativeStderr();
|
|
107
139
|
return {
|
|
108
140
|
stdout: msg.stdout,
|
|
109
|
-
stderr: msg.stderr,
|
|
141
|
+
stderr: native.length ? Buffer.concat([msg.stderr, native]) : msg.stderr,
|
|
110
142
|
exitCode: msg.exitCode,
|
|
111
143
|
timedOut: false,
|
|
144
|
+
cancelled: false,
|
|
145
|
+
truncated: false,
|
|
112
146
|
};
|
|
113
147
|
}
|
|
114
148
|
catch (e) {
|
|
115
149
|
const timedOut = e.timedOut === true;
|
|
116
150
|
await this.stop();
|
|
151
|
+
this.drainNativeStderr();
|
|
152
|
+
if (cancelled || signal?.aborted)
|
|
153
|
+
return this.cancelledResult();
|
|
117
154
|
if (timedOut) {
|
|
118
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
stdout: Buffer.alloc(0),
|
|
157
|
+
stderr: Buffer.alloc(0),
|
|
158
|
+
exitCode: 124,
|
|
159
|
+
timedOut: true,
|
|
160
|
+
cancelled: false,
|
|
161
|
+
truncated: false,
|
|
162
|
+
};
|
|
119
163
|
}
|
|
120
164
|
if (this.closeErr && this.closeErr.code === 'ENOENT') {
|
|
121
165
|
return {
|
|
@@ -123,6 +167,8 @@ export class PowerShellHost {
|
|
|
123
167
|
stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
|
|
124
168
|
exitCode: 127,
|
|
125
169
|
timedOut: false,
|
|
170
|
+
cancelled: false,
|
|
171
|
+
truncated: false,
|
|
126
172
|
spawnError: 'ENOENT',
|
|
127
173
|
};
|
|
128
174
|
}
|
|
@@ -134,8 +180,13 @@ export class PowerShellHost {
|
|
|
134
180
|
'\n', 'utf8'),
|
|
135
181
|
exitCode: code === 0 ? 1 : code,
|
|
136
182
|
timedOut: false,
|
|
183
|
+
cancelled: false,
|
|
184
|
+
truncated: false,
|
|
137
185
|
};
|
|
138
186
|
}
|
|
187
|
+
finally {
|
|
188
|
+
signal?.removeEventListener('abort', onAbort);
|
|
189
|
+
}
|
|
139
190
|
}
|
|
140
191
|
async ensureStarted() {
|
|
141
192
|
if (this.proc && !this.closed)
|
|
@@ -154,6 +205,8 @@ export class PowerShellHost {
|
|
|
154
205
|
stderr: Buffer.from(PS_MISSING_MESSAGE, 'utf8'),
|
|
155
206
|
exitCode: 127,
|
|
156
207
|
timedOut: false,
|
|
208
|
+
cancelled: false,
|
|
209
|
+
truncated: false,
|
|
157
210
|
spawnError: 'ENOENT',
|
|
158
211
|
};
|
|
159
212
|
}
|
|
@@ -162,6 +215,8 @@ export class PowerShellHost {
|
|
|
162
215
|
stderr: Buffer.from('fauxnix: failed to start powershell.exe: ' + err.message + '\n', 'utf8'),
|
|
163
216
|
exitCode: 127,
|
|
164
217
|
timedOut: false,
|
|
218
|
+
cancelled: false,
|
|
219
|
+
truncated: false,
|
|
165
220
|
spawnError: 'START',
|
|
166
221
|
spawnMessage: err.message,
|
|
167
222
|
};
|
|
@@ -189,14 +244,26 @@ export class PowerShellHost {
|
|
|
189
244
|
windowsHide: true,
|
|
190
245
|
});
|
|
191
246
|
this.proc = child;
|
|
192
|
-
child.stdout.on('data', (d) =>
|
|
193
|
-
|
|
247
|
+
child.stdout.on('data', (d) => {
|
|
248
|
+
if (this.proc !== child)
|
|
249
|
+
return;
|
|
250
|
+
this.onStdout(d);
|
|
251
|
+
});
|
|
252
|
+
child.stderr.on('data', (d) => {
|
|
253
|
+
if (this.proc !== child)
|
|
254
|
+
return;
|
|
255
|
+
this.stderrChunks.push(d);
|
|
256
|
+
});
|
|
194
257
|
child.on('error', (e) => {
|
|
258
|
+
if (this.proc !== child)
|
|
259
|
+
return;
|
|
195
260
|
this.closeErr = e;
|
|
196
261
|
this.closed = true;
|
|
197
262
|
this.failWaiters(e);
|
|
198
263
|
});
|
|
199
264
|
child.on('close', (c) => {
|
|
265
|
+
if (this.proc !== child)
|
|
266
|
+
return;
|
|
200
267
|
this.closeCode = c;
|
|
201
268
|
this.closed = true;
|
|
202
269
|
this.proc = null;
|
package/dist/registry.d.ts
CHANGED
|
@@ -77,3 +77,53 @@ export interface WordArgs {
|
|
|
77
77
|
* shortValues: single-char options that consume a value (e.g. ['n']).
|
|
78
78
|
*/
|
|
79
79
|
export declare function parseWords(args: Word[], shortValues?: string[], longValues?: string[]): WordArgs;
|
|
80
|
+
export type CommandEffect = 'read' | 'write' | 'delete' | 'network' | 'process';
|
|
81
|
+
export type OptionSupport = 'implemented' | 'unsupported';
|
|
82
|
+
/** One short/long alias group. Unknown options on a spec'd command fail loud. */
|
|
83
|
+
export interface OptionSpec {
|
|
84
|
+
/** Short flag letter without dash (e.g. `'n'`). */
|
|
85
|
+
short?: string;
|
|
86
|
+
/** Long option including dashes (e.g. `'--no-clobber'`). */
|
|
87
|
+
long?: string;
|
|
88
|
+
/** Consumes a following argument (`-n 5`, `--lines=5`). */
|
|
89
|
+
takesValue?: boolean;
|
|
90
|
+
support: OptionSupport;
|
|
91
|
+
/** Extra phrase for unsupported options (`interactive prompt`). */
|
|
92
|
+
reason?: string;
|
|
93
|
+
}
|
|
94
|
+
export interface CommandSpec {
|
|
95
|
+
names: string[];
|
|
96
|
+
options: OptionSpec[];
|
|
97
|
+
effects: CommandEffect[];
|
|
98
|
+
platform?: 'windows-ps51' | 'portable-translate';
|
|
99
|
+
dispatch?: 'translated' | 'native' | 'dynamic';
|
|
100
|
+
handler: Handler;
|
|
101
|
+
}
|
|
102
|
+
/** Register a spec'd command. Unknown/unsupported options become GNU-style usage errors. */
|
|
103
|
+
export declare function registerSpec(spec: CommandSpec): void;
|
|
104
|
+
export declare function registerSpecs(list: CommandSpec[]): void;
|
|
105
|
+
export declare function lookupSpec(name: string): CommandSpec | undefined;
|
|
106
|
+
/** Unique specs in registration order. */
|
|
107
|
+
export declare function registeredSpecs(): CommandSpec[];
|
|
108
|
+
export interface ListedCommand {
|
|
109
|
+
name: string;
|
|
110
|
+
spec: null | {
|
|
111
|
+
options: Array<{
|
|
112
|
+
short?: string;
|
|
113
|
+
long?: string;
|
|
114
|
+
takesValue: boolean;
|
|
115
|
+
support: OptionSupport;
|
|
116
|
+
reason?: string;
|
|
117
|
+
}>;
|
|
118
|
+
effects: CommandEffect[];
|
|
119
|
+
platform: 'windows-ps51' | 'portable-translate';
|
|
120
|
+
dispatch: 'translated' | 'native' | 'dynamic';
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Capability dump for `fauxnix list --json` / MCP introspection. */
|
|
124
|
+
export declare function listCommandsJson(): ListedCommand[];
|
|
125
|
+
/**
|
|
126
|
+
* Walk argv against a CommandSpec. Returns a PowerShell error script, or
|
|
127
|
+
* null when every option is recognized and implemented.
|
|
128
|
+
*/
|
|
129
|
+
export declare function specOptionError(spec: CommandSpec, args: Word[], cmdName: string): string | null;
|
package/dist/registry.js
CHANGED
|
@@ -153,3 +153,145 @@ export function parseWords(args, shortValues = [], longValues = []) {
|
|
|
153
153
|
}
|
|
154
154
|
return { flags, longs, values, missingValue, operandWords };
|
|
155
155
|
}
|
|
156
|
+
const specs = new Map();
|
|
157
|
+
/** Register a spec'd command. Unknown/unsupported options become GNU-style usage errors. */
|
|
158
|
+
export function registerSpec(spec) {
|
|
159
|
+
for (const name of spec.names) {
|
|
160
|
+
const wrapped = (args, ctx) => {
|
|
161
|
+
const err = specOptionError(spec, args, name);
|
|
162
|
+
if (err)
|
|
163
|
+
return err;
|
|
164
|
+
return spec.handler(args, ctx);
|
|
165
|
+
};
|
|
166
|
+
registry.set(name, wrapped);
|
|
167
|
+
specs.set(name, spec);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function registerSpecs(list) {
|
|
171
|
+
for (const spec of list)
|
|
172
|
+
registerSpec(spec);
|
|
173
|
+
}
|
|
174
|
+
export function lookupSpec(name) {
|
|
175
|
+
return specs.get(name);
|
|
176
|
+
}
|
|
177
|
+
/** Unique specs in registration order. */
|
|
178
|
+
export function registeredSpecs() {
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const spec of specs.values()) {
|
|
182
|
+
if (seen.has(spec))
|
|
183
|
+
continue;
|
|
184
|
+
seen.add(spec);
|
|
185
|
+
out.push(spec);
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
/** Capability dump for `fauxnix list --json` / MCP introspection. */
|
|
190
|
+
export function listCommandsJson() {
|
|
191
|
+
return registeredNames().map((name) => {
|
|
192
|
+
const spec = lookupSpec(name);
|
|
193
|
+
if (!spec)
|
|
194
|
+
return { name, spec: null };
|
|
195
|
+
return {
|
|
196
|
+
name,
|
|
197
|
+
spec: {
|
|
198
|
+
options: spec.options.map((o) => ({
|
|
199
|
+
...(o.short ? { short: o.short } : {}),
|
|
200
|
+
...(o.long ? { long: o.long } : {}),
|
|
201
|
+
takesValue: o.takesValue === true,
|
|
202
|
+
support: o.support,
|
|
203
|
+
...(o.reason ? { reason: o.reason } : {}),
|
|
204
|
+
})),
|
|
205
|
+
effects: spec.effects,
|
|
206
|
+
platform: spec.platform ?? 'windows-ps51',
|
|
207
|
+
dispatch: spec.dispatch ?? 'translated',
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Walk argv against a CommandSpec. Returns a PowerShell error script, or
|
|
214
|
+
* null when every option is recognized and implemented.
|
|
215
|
+
*/
|
|
216
|
+
export function specOptionError(spec, args, cmdName) {
|
|
217
|
+
const shorts = new Map();
|
|
218
|
+
const longs = new Map();
|
|
219
|
+
for (const o of spec.options) {
|
|
220
|
+
if (o.short)
|
|
221
|
+
shorts.set(o.short, o);
|
|
222
|
+
if (o.long)
|
|
223
|
+
longs.set(o.long, o);
|
|
224
|
+
}
|
|
225
|
+
let i = 0;
|
|
226
|
+
let onlyOperands = false;
|
|
227
|
+
while (i < args.length) {
|
|
228
|
+
const t = wordToString(args[i]);
|
|
229
|
+
if (onlyOperands) {
|
|
230
|
+
i++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (t === '--') {
|
|
234
|
+
onlyOperands = true;
|
|
235
|
+
i++;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (t.startsWith('--')) {
|
|
239
|
+
const eq = t.indexOf('=');
|
|
240
|
+
const name = eq >= 0 ? t.slice(0, eq) : t;
|
|
241
|
+
const opt = longs.get(name);
|
|
242
|
+
if (!opt)
|
|
243
|
+
return optionFail(cmdName, "unrecognized option '" + name + "'");
|
|
244
|
+
if (opt.support === 'unsupported') {
|
|
245
|
+
return optionFail(cmdName, unsupportedMsg(opt, name));
|
|
246
|
+
}
|
|
247
|
+
if (!opt.takesValue && eq >= 0) {
|
|
248
|
+
return optionFail(cmdName, "option '" + name + "' doesn't allow an argument");
|
|
249
|
+
}
|
|
250
|
+
if (opt.takesValue && eq < 0) {
|
|
251
|
+
if (i + 1 < args.length)
|
|
252
|
+
i++;
|
|
253
|
+
else
|
|
254
|
+
return optionFail(cmdName, "option '" + name + "' requires an argument");
|
|
255
|
+
}
|
|
256
|
+
i++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (t.startsWith('-') && t.length > 1 && !/^-?\d/.test(t.slice(1, 2))) {
|
|
260
|
+
const body = t.slice(1);
|
|
261
|
+
for (let c = 0; c < body.length; c++) {
|
|
262
|
+
const ch = body[c];
|
|
263
|
+
const opt = shorts.get(ch);
|
|
264
|
+
if (!opt)
|
|
265
|
+
return optionFail(cmdName, "invalid option -- '" + ch + "'");
|
|
266
|
+
if (opt.support === 'unsupported') {
|
|
267
|
+
return optionFail(cmdName, unsupportedMsg(opt, '-' + ch));
|
|
268
|
+
}
|
|
269
|
+
if (opt.takesValue) {
|
|
270
|
+
const rest = body.slice(c + 1);
|
|
271
|
+
if (!rest) {
|
|
272
|
+
if (i + 1 < args.length)
|
|
273
|
+
i++;
|
|
274
|
+
else
|
|
275
|
+
return optionFail(cmdName, "option requires an argument -- '" + ch + "'");
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
i++;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
i++;
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
function unsupportedMsg(opt, shown) {
|
|
288
|
+
const reason = opt.reason ? ' (' + opt.reason + ')' : '';
|
|
289
|
+
return "option '" + shown + "' is not supported by fauxnix" + reason;
|
|
290
|
+
}
|
|
291
|
+
function optionFail(cmd, msg) {
|
|
292
|
+
return ('[Console]::Error.WriteLine(' +
|
|
293
|
+
psStr(cmd + ': ' + msg) +
|
|
294
|
+
'); [Console]::Error.WriteLine(' +
|
|
295
|
+
psStr("Try '" + cmd + " --help' for more information.") +
|
|
296
|
+
'); $script:fx_exit = 1');
|
|
297
|
+
}
|
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 = [];
|
|
@@ -1240,6 +1268,13 @@ while ($true) {
|
|
|
1240
1268
|
$fx_code = 0
|
|
1241
1269
|
try { $fx_code = [int]$script:fx_exit } catch { $fx_code = 1 }
|
|
1242
1270
|
$fx_res = @{ id = $fx_id; stdoutB64 = $fx_outB64; stderrB64 = $fx_errB64; exitCode = $fx_code }
|
|
1243
|
-
|
|
1271
|
+
try {
|
|
1272
|
+
$fx_json = $fx_res | ConvertTo-Json -Compress
|
|
1273
|
+
} catch {
|
|
1274
|
+
$fx_msg = 'fauxnix: host result exceeded ConvertTo-Json MaxJsonLength (~2MB)'
|
|
1275
|
+
$fx_res = @{ id = $fx_id; stdoutB64 = ''; stderrB64 = [Convert]::ToBase64String($fx_utf8.GetBytes($fx_msg)); exitCode = 1 }
|
|
1276
|
+
$fx_json = $fx_res | ConvertTo-Json -Compress
|
|
1277
|
+
}
|
|
1278
|
+
$fx_proto.WriteLine($fx_json)
|
|
1244
1279
|
}
|
|
1245
1280
|
`.trim();
|
|
@@ -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.
|
|
3
|
+
"version": "0.8.0",
|
|
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"
|