fauxnix-cli 0.7.1 → 0.9.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 +6 -0
- package/dist/cli.js +11 -1
- package/dist/commands/files.d.ts +4 -1
- package/dist/commands/files.js +356 -89
- package/dist/commands/install-all.js +7 -4
- package/dist/commands/sysinfo.js +3 -2
- package/dist/commands/text-filters.d.ts +2 -1
- package/dist/commands/text-filters.js +67 -7
- package/dist/commands/text-io.d.ts +2 -1
- package/dist/commands/text-io.js +95 -4
- package/dist/executor.d.ts +13 -1
- package/dist/executor.js +83 -13
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +79 -18
- package/dist/parser.js +55 -24
- package/dist/ps-host.d.ts +46 -2
- package/dist/ps-host.js +181 -17
- package/dist/registry.d.ts +54 -0
- package/dist/registry.js +182 -0
- package/dist/translator.js +65 -7
- package/package.json +1 -1
package/dist/registry.d.ts
CHANGED
|
@@ -77,3 +77,57 @@ 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
|
+
/** GNU usage/syntax exit (grep uses 2; cp/mv/rm use 1). */
|
|
101
|
+
usageExit?: number;
|
|
102
|
+
handler: Handler;
|
|
103
|
+
}
|
|
104
|
+
/** Register a spec'd command. Unknown/unsupported options become GNU-style usage errors. */
|
|
105
|
+
export declare function registerSpec(spec: CommandSpec): void;
|
|
106
|
+
export declare function registerSpecs(list: CommandSpec[]): void;
|
|
107
|
+
export declare function lookupSpec(name: string): CommandSpec | undefined;
|
|
108
|
+
/** Unique specs in registration order. */
|
|
109
|
+
export declare function registeredSpecs(): CommandSpec[];
|
|
110
|
+
/** Markdown dump of every CommandSpec — source for docs/command-specs.md. */
|
|
111
|
+
export declare function specsMarkdown(): string;
|
|
112
|
+
export interface ListedCommand {
|
|
113
|
+
name: string;
|
|
114
|
+
spec: null | {
|
|
115
|
+
options: Array<{
|
|
116
|
+
short?: string;
|
|
117
|
+
long?: string;
|
|
118
|
+
takesValue: boolean;
|
|
119
|
+
support: OptionSupport;
|
|
120
|
+
reason?: string;
|
|
121
|
+
}>;
|
|
122
|
+
effects: CommandEffect[];
|
|
123
|
+
platform: 'windows-ps51' | 'portable-translate';
|
|
124
|
+
dispatch: 'translated' | 'native' | 'dynamic';
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Capability dump for `fauxnix list --json` / MCP introspection. */
|
|
128
|
+
export declare function listCommandsJson(): ListedCommand[];
|
|
129
|
+
/**
|
|
130
|
+
* Walk argv against a CommandSpec. Returns a PowerShell error script, or
|
|
131
|
+
* null when every option is recognized and implemented.
|
|
132
|
+
*/
|
|
133
|
+
export declare function specOptionError(spec: CommandSpec, args: Word[], cmdName: string): string | null;
|
package/dist/registry.js
CHANGED
|
@@ -153,3 +153,185 @@ 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
|
+
/** Markdown dump of every CommandSpec — source for docs/command-specs.md. */
|
|
190
|
+
export function specsMarkdown() {
|
|
191
|
+
const lines = [
|
|
192
|
+
'# Command specs',
|
|
193
|
+
'',
|
|
194
|
+
'Generated from `CommandSpec`. Unlisted commands still use unchecked `parseWords` (unknown flags ignored). Spec\'d commands fail loud on unknown or unsupported options.',
|
|
195
|
+
'',
|
|
196
|
+
];
|
|
197
|
+
for (const spec of registeredSpecs()) {
|
|
198
|
+
lines.push('## `' + spec.names.join('` / `') + '`');
|
|
199
|
+
lines.push('');
|
|
200
|
+
lines.push('Effects: ' + spec.effects.map((e) => '`' + e + '`').join(', '));
|
|
201
|
+
lines.push('');
|
|
202
|
+
if (!spec.options.length) {
|
|
203
|
+
lines.push('No options declared.');
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
lines.push('| Option | Value | Support |');
|
|
207
|
+
lines.push('| --- | --- | --- |');
|
|
208
|
+
for (const o of spec.options) {
|
|
209
|
+
const names = [o.short ? '`-' + o.short + '`' : '', o.long ? '`' + o.long + '`' : '']
|
|
210
|
+
.filter((s) => s !== '')
|
|
211
|
+
.join(', ');
|
|
212
|
+
const val = o.takesValue ? 'required' : 'flag';
|
|
213
|
+
const extra = o.reason ? ' (' + o.reason + ')' : '';
|
|
214
|
+
lines.push('| ' + names + ' | ' + val + ' | ' + o.support + extra + ' |');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
lines.push('');
|
|
218
|
+
}
|
|
219
|
+
const unspec = registeredNames().filter((n) => !lookupSpec(n));
|
|
220
|
+
lines.push('## Unspec\'d commands');
|
|
221
|
+
lines.push('');
|
|
222
|
+
lines.push(unspec.map((n) => '`' + n + '`').join(', '));
|
|
223
|
+
lines.push('');
|
|
224
|
+
return lines.join('\n');
|
|
225
|
+
}
|
|
226
|
+
/** Capability dump for `fauxnix list --json` / MCP introspection. */
|
|
227
|
+
export function listCommandsJson() {
|
|
228
|
+
return registeredNames().map((name) => {
|
|
229
|
+
const spec = lookupSpec(name);
|
|
230
|
+
if (!spec)
|
|
231
|
+
return { name, spec: null };
|
|
232
|
+
return {
|
|
233
|
+
name,
|
|
234
|
+
spec: {
|
|
235
|
+
options: spec.options.map((o) => ({
|
|
236
|
+
...(o.short ? { short: o.short } : {}),
|
|
237
|
+
...(o.long ? { long: o.long } : {}),
|
|
238
|
+
takesValue: o.takesValue === true,
|
|
239
|
+
support: o.support,
|
|
240
|
+
...(o.reason ? { reason: o.reason } : {}),
|
|
241
|
+
})),
|
|
242
|
+
effects: spec.effects,
|
|
243
|
+
platform: spec.platform ?? 'windows-ps51',
|
|
244
|
+
dispatch: spec.dispatch ?? 'translated',
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Walk argv against a CommandSpec. Returns a PowerShell error script, or
|
|
251
|
+
* null when every option is recognized and implemented.
|
|
252
|
+
*/
|
|
253
|
+
export function specOptionError(spec, args, cmdName) {
|
|
254
|
+
const usageExit = spec.usageExit ?? 1;
|
|
255
|
+
const fail = (msg) => optionFail(cmdName, msg, usageExit);
|
|
256
|
+
const shorts = new Map();
|
|
257
|
+
const longs = new Map();
|
|
258
|
+
for (const o of spec.options) {
|
|
259
|
+
if (o.short)
|
|
260
|
+
shorts.set(o.short, o);
|
|
261
|
+
if (o.long)
|
|
262
|
+
longs.set(o.long, o);
|
|
263
|
+
}
|
|
264
|
+
let i = 0;
|
|
265
|
+
let onlyOperands = false;
|
|
266
|
+
while (i < args.length) {
|
|
267
|
+
const t = wordToString(args[i]);
|
|
268
|
+
if (onlyOperands) {
|
|
269
|
+
i++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (t === '--') {
|
|
273
|
+
onlyOperands = true;
|
|
274
|
+
i++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (t.startsWith('--')) {
|
|
278
|
+
const eq = t.indexOf('=');
|
|
279
|
+
const name = eq >= 0 ? t.slice(0, eq) : t;
|
|
280
|
+
const opt = longs.get(name);
|
|
281
|
+
if (!opt)
|
|
282
|
+
return fail("unrecognized option '" + name + "'");
|
|
283
|
+
if (opt.support === 'unsupported') {
|
|
284
|
+
return fail(unsupportedMsg(opt, name));
|
|
285
|
+
}
|
|
286
|
+
if (!opt.takesValue && eq >= 0) {
|
|
287
|
+
return fail("option '" + name + "' doesn't allow an argument");
|
|
288
|
+
}
|
|
289
|
+
if (opt.takesValue && eq < 0) {
|
|
290
|
+
if (i + 1 < args.length)
|
|
291
|
+
i++;
|
|
292
|
+
else
|
|
293
|
+
return fail("option '" + name + "' requires an argument");
|
|
294
|
+
}
|
|
295
|
+
i++;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (t.startsWith('-') && t.length > 1 && !/^-?\d/.test(t.slice(1, 2))) {
|
|
299
|
+
const body = t.slice(1);
|
|
300
|
+
for (let c = 0; c < body.length; c++) {
|
|
301
|
+
const ch = body[c];
|
|
302
|
+
const opt = shorts.get(ch);
|
|
303
|
+
if (!opt)
|
|
304
|
+
return fail("invalid option -- '" + ch + "'");
|
|
305
|
+
if (opt.support === 'unsupported') {
|
|
306
|
+
return fail(unsupportedMsg(opt, '-' + ch));
|
|
307
|
+
}
|
|
308
|
+
if (opt.takesValue) {
|
|
309
|
+
const rest = body.slice(c + 1);
|
|
310
|
+
if (!rest) {
|
|
311
|
+
if (i + 1 < args.length)
|
|
312
|
+
i++;
|
|
313
|
+
else
|
|
314
|
+
return fail("option requires an argument -- '" + ch + "'");
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
i++;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
i++;
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
function unsupportedMsg(opt, shown) {
|
|
327
|
+
const reason = opt.reason ? ' (' + opt.reason + ')' : '';
|
|
328
|
+
return "option '" + shown + "' is not supported by fauxnix" + reason;
|
|
329
|
+
}
|
|
330
|
+
function optionFail(cmd, msg, code = 1) {
|
|
331
|
+
return ('[Console]::Error.WriteLine(' +
|
|
332
|
+
psStr(cmd + ': ' + msg) +
|
|
333
|
+
'); [Console]::Error.WriteLine(' +
|
|
334
|
+
psStr("Try '" + cmd + " --help' for more information.") +
|
|
335
|
+
'); $script:fx_exit = ' +
|
|
336
|
+
String(code));
|
|
337
|
+
}
|
package/dist/translator.js
CHANGED
|
@@ -1209,12 +1209,36 @@ $fx_reader = New-Object System.IO.StreamReader($fx_in, $fx_utf8, $true, 8192, $t
|
|
|
1209
1209
|
$fx_proto = New-Object System.IO.StreamWriter($fx_out, $fx_utf8, 8192, $true)
|
|
1210
1210
|
$fx_proto.NewLine = [string][char]10
|
|
1211
1211
|
$fx_proto.AutoFlush = $true
|
|
1212
|
-
$fx_proto.WriteLine('{"ready":true}')
|
|
1212
|
+
$fx_proto.WriteLine('{"v":2,"type":"ready","capabilities":{"cancel":false,"maxChunkBytes":65536,"stderrMarker":true}}')
|
|
1213
|
+
function fx-b64([byte[]]$bytes, $off, $len) {
|
|
1214
|
+
if ($len -le 0) { return '' }
|
|
1215
|
+
$slice = New-Object byte[] $len
|
|
1216
|
+
[Array]::Copy($bytes, $off, $slice, 0, $len)
|
|
1217
|
+
return [Convert]::ToBase64String($slice)
|
|
1218
|
+
}
|
|
1219
|
+
function fx-emit-chunks($type, $id, [byte[]]$bytes, $limit, [ref]$seq) {
|
|
1220
|
+
$n = 0
|
|
1221
|
+
if ($null -ne $bytes) { $n = $bytes.Length }
|
|
1222
|
+
$use = $n
|
|
1223
|
+
$trunc = $false
|
|
1224
|
+
if ($use -gt $limit) { $use = $limit; $trunc = $true }
|
|
1225
|
+
$off = 0
|
|
1226
|
+
while ($off -lt $use) {
|
|
1227
|
+
$len = $use - $off
|
|
1228
|
+
if ($len -gt 65536) { $len = 65536 }
|
|
1229
|
+
$b64 = fx-b64 $bytes $off $len
|
|
1230
|
+
$fx_proto.WriteLine('{"v":2,"type":"' + $type + '","id":"' + $id + '","seq":' + $seq.Value + ',"dataB64":"' + $b64 + '"}')
|
|
1231
|
+
$seq.Value = $seq.Value + 1
|
|
1232
|
+
$off += $len
|
|
1233
|
+
}
|
|
1234
|
+
return $trunc
|
|
1235
|
+
}
|
|
1213
1236
|
while ($true) {
|
|
1214
1237
|
$fx_line = $fx_reader.ReadLine()
|
|
1215
1238
|
if ($null -eq $fx_line) { break }
|
|
1216
1239
|
if ($fx_line -eq '') { continue }
|
|
1217
1240
|
$fx_id = ''
|
|
1241
|
+
$fx_req = $null
|
|
1218
1242
|
$fx_msOut = $null
|
|
1219
1243
|
$fx_msErr = $null
|
|
1220
1244
|
$fx_outW = $null
|
|
@@ -1261,13 +1285,47 @@ while ($true) {
|
|
|
1261
1285
|
try { [Console]::SetOut($fx_oldOut) } catch {}
|
|
1262
1286
|
try { [Console]::SetError($fx_oldErr) } catch {}
|
|
1263
1287
|
}
|
|
1264
|
-
$fx_outB64 = ''
|
|
1265
|
-
$fx_errB64 = ''
|
|
1266
|
-
if ($null -ne $fx_msOut) { $fx_outB64 = [Convert]::ToBase64String($fx_msOut.ToArray()) }
|
|
1267
|
-
if ($null -ne $fx_msErr) { $fx_errB64 = [Convert]::ToBase64String($fx_msErr.ToArray()) }
|
|
1268
1288
|
$fx_code = 0
|
|
1269
1289
|
try { $fx_code = [int]$script:fx_exit } catch { $fx_code = 1 }
|
|
1270
|
-
$
|
|
1271
|
-
|
|
1290
|
+
$fx_v2 = $false
|
|
1291
|
+
if ($null -ne $fx_req -and $null -ne $fx_req.PSObject.Properties['v']) {
|
|
1292
|
+
if ([int]$fx_req.v -eq 2) { $fx_v2 = $true }
|
|
1293
|
+
}
|
|
1294
|
+
$fx_outBytes = New-Object byte[] 0
|
|
1295
|
+
$fx_errBytes = New-Object byte[] 0
|
|
1296
|
+
if ($null -ne $fx_msOut) { $fx_outBytes = $fx_msOut.ToArray() }
|
|
1297
|
+
if ($null -ne $fx_msErr) { $fx_errBytes = $fx_msErr.ToArray() }
|
|
1298
|
+
if ($fx_v2) {
|
|
1299
|
+
$fx_outLimit = 8388608
|
|
1300
|
+
$fx_errLimit = 1048576
|
|
1301
|
+
if ($null -ne $fx_req.PSObject.Properties['stdoutLimit']) { $fx_outLimit = [int]$fx_req.stdoutLimit }
|
|
1302
|
+
if ($null -ne $fx_req.PSObject.Properties['stderrLimit']) { $fx_errLimit = [int]$fx_req.stderrLimit }
|
|
1303
|
+
$fx_outSeq = 0
|
|
1304
|
+
$fx_errSeq = 0
|
|
1305
|
+
$fx_trunc = $false
|
|
1306
|
+
if (fx-emit-chunks 'stdout' $fx_id $fx_outBytes $fx_outLimit ([ref]$fx_outSeq)) { $fx_trunc = $true }
|
|
1307
|
+
if (fx-emit-chunks 'stderr' $fx_id $fx_errBytes $fx_errLimit ([ref]$fx_errSeq)) { $fx_trunc = $true }
|
|
1308
|
+
$fx_nativeErr = [Console]::OpenStandardError()
|
|
1309
|
+
$fx_mark = $fx_utf8.GetBytes(('FAUXNIX_ERR_END:' + $fx_id + [char]10))
|
|
1310
|
+
$fx_nativeErr.Write($fx_mark, 0, $fx_mark.Length)
|
|
1311
|
+
$fx_nativeErr.Flush()
|
|
1312
|
+
$fx_end = '{"v":2,"type":"end","id":"' + $fx_id + '","exitCode":' + $fx_code + ',"timedOut":false,"cancelled":false,"truncated":'
|
|
1313
|
+
if ($fx_trunc) { $fx_end = $fx_end + 'true}' } else { $fx_end = $fx_end + 'false}' }
|
|
1314
|
+
$fx_proto.WriteLine($fx_end)
|
|
1315
|
+
} else {
|
|
1316
|
+
$fx_outB64 = ''
|
|
1317
|
+
$fx_errB64 = ''
|
|
1318
|
+
if ($fx_outBytes.Length -gt 0) { $fx_outB64 = [Convert]::ToBase64String($fx_outBytes) }
|
|
1319
|
+
if ($fx_errBytes.Length -gt 0) { $fx_errB64 = [Convert]::ToBase64String($fx_errBytes) }
|
|
1320
|
+
$fx_res = @{ id = $fx_id; stdoutB64 = $fx_outB64; stderrB64 = $fx_errB64; exitCode = $fx_code }
|
|
1321
|
+
try {
|
|
1322
|
+
$fx_json = $fx_res | ConvertTo-Json -Compress
|
|
1323
|
+
} catch {
|
|
1324
|
+
$fx_msg = 'fauxnix: host result exceeded ConvertTo-Json MaxJsonLength (~2MB)'
|
|
1325
|
+
$fx_res = @{ id = $fx_id; stdoutB64 = ''; stderrB64 = [Convert]::ToBase64String($fx_utf8.GetBytes($fx_msg)); exitCode = 1 }
|
|
1326
|
+
$fx_json = $fx_res | ConvertTo-Json -Compress
|
|
1327
|
+
}
|
|
1328
|
+
$fx_proto.WriteLine($fx_json)
|
|
1329
|
+
}
|
|
1272
1330
|
}
|
|
1273
1331
|
`.trim();
|
package/package.json
CHANGED