fauxnix-cli 0.8.0 → 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 +4 -3
- package/dist/cli.js +6 -1
- package/dist/commands/files.js +80 -20
- package/dist/commands/install-all.js +2 -1
- package/dist/commands/text-filters.d.ts +2 -1
- package/dist/commands/text-filters.js +67 -7
- package/dist/commands/text-io.js +81 -1
- package/dist/executor.js +1 -1
- package/dist/ps-host.d.ts +40 -2
- package/dist/ps-host.js +109 -12
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +49 -9
- package/dist/translator.js +64 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -154,9 +154,10 @@ development:
|
|
|
154
154
|
translate time (unsupported constructs throw named errors, never silently misbehave)
|
|
155
155
|
- **text I/O**: `echo printf cat head tail wc tee nl tac md5sum sha1sum sha256sum base64 seq yes xargs`
|
|
156
156
|
|
|
157
|
-
`cp` / `mv` / `rm` / `touch` / `tee` carry a `CommandSpec`: unknown
|
|
158
|
-
usage error instead of being ignored.
|
|
159
|
-
|
|
157
|
+
`cp` / `mv` / `rm` / `touch` / `tee` / `grep` / `head` / `du` carry a `CommandSpec`: unknown
|
|
158
|
+
options fail with a GNU-style usage error instead of being ignored. Implemented GNU holes:
|
|
159
|
+
`cp -n` / `mv -n` / `touch -c` / `tee --append` / `grep -m` / `head --lines` / `du --max-depth`.
|
|
160
|
+
`fauxnix list --json` and `docs/command-specs.md` dump the same metadata.
|
|
160
161
|
- **shell/system**: `cd pwd export unset env printenv ps kill pkill pgrep sleep which type whoami
|
|
161
162
|
id groups date uname hostname uptime free nproc clear true false test [ [[ : pushd popd dirs sudo
|
|
162
163
|
timeout man history less more source . eval exit alias set`
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { FauxnixSession } from './executor.js';
|
|
3
3
|
import { parseCommand } from './parser.js';
|
|
4
4
|
import { translateCommandList } from './translator.js';
|
|
5
|
-
import { listCommandsJson, registeredNames } from './registry.js';
|
|
5
|
+
import { listCommandsJson, registeredNames, specsMarkdown } from './registry.js';
|
|
6
6
|
import { encodeCommand } from './encoding.js';
|
|
7
7
|
import { startMcpServer } from './mcp.js';
|
|
8
8
|
import { packageVersion } from './version.js';
|
|
@@ -16,6 +16,7 @@ Usage:
|
|
|
16
16
|
fauxnix mcp start the MCP stdio server (for agent harnesses)
|
|
17
17
|
fauxnix list list translated commands
|
|
18
18
|
fauxnix list --json same list as machine-readable capability metadata
|
|
19
|
+
fauxnix list --markdown CommandSpec tables (same text as docs/command-specs.md)
|
|
19
20
|
fauxnix check verify the local PowerShell environment
|
|
20
21
|
fauxnix --version
|
|
21
22
|
|
|
@@ -36,6 +37,10 @@ export async function runCli(argv) {
|
|
|
36
37
|
console.log(JSON.stringify(listCommandsJson(), null, 2));
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
40
|
+
if (rest[0] === '--markdown') {
|
|
41
|
+
console.log(specsMarkdown());
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
39
44
|
const names = registeredNames();
|
|
40
45
|
console.log(names.length + ' translated commands:');
|
|
41
46
|
for (const n of names)
|
package/dist/commands/files.js
CHANGED
|
@@ -51,8 +51,8 @@ function psArray(words, fn = operandExpr) {
|
|
|
51
51
|
/* ls */
|
|
52
52
|
/* ------------------------------------------------------------------ */
|
|
53
53
|
const ls = (args) => {
|
|
54
|
-
const { flags, longs, operandWords } = parseWords(args);
|
|
55
|
-
const long = flags.has('l') || longs.has('--
|
|
54
|
+
const { flags, longs, values, operandWords } = parseWords(args, [], ['--format']);
|
|
55
|
+
const long = flags.has('l') || longs.has('--long') || values.get('--format') === 'long';
|
|
56
56
|
const all = flags.has('a') || longs.has('--all');
|
|
57
57
|
const almost = flags.has('A') || longs.has('--almost-all');
|
|
58
58
|
const dirOnly = flags.has('d') || longs.has('--directory');
|
|
@@ -208,7 +208,7 @@ const rm = (args) => {
|
|
|
208
208
|
const mkdir = (args) => {
|
|
209
209
|
const { flags, longs, operandWords } = parseWords(args);
|
|
210
210
|
const parents = flags.has('p') || longs.has('--parents');
|
|
211
|
-
const verbose = flags.has('v');
|
|
211
|
+
const verbose = flags.has('v') || longs.has('--verbose');
|
|
212
212
|
return [
|
|
213
213
|
'$fx_dirs = ' + psArray(operandWords),
|
|
214
214
|
"if ($fx_dirs.Count -eq 0) { [Console]::Error.WriteLine('mkdir: missing operand'); $script:fx_exit = 1 }",
|
|
@@ -254,8 +254,8 @@ const touch = (args) => {
|
|
|
254
254
|
].join('\n');
|
|
255
255
|
};
|
|
256
256
|
const mktemp = (args) => {
|
|
257
|
-
const { flags } = parseWords(args);
|
|
258
|
-
const dir = flags.has('d');
|
|
257
|
+
const { flags, longs } = parseWords(args);
|
|
258
|
+
const dir = flags.has('d') || longs.has('--directory');
|
|
259
259
|
return [
|
|
260
260
|
'try {',
|
|
261
261
|
' if (' + (dir ? '$true' : '$false') + ') {',
|
|
@@ -272,8 +272,8 @@ const mktemp = (args) => {
|
|
|
272
272
|
/* ln / readlink / realpath */
|
|
273
273
|
/* ------------------------------------------------------------------ */
|
|
274
274
|
const ln = (args) => {
|
|
275
|
-
const { flags, operandWords } = parseWords(args);
|
|
276
|
-
const sym = flags.has('s');
|
|
275
|
+
const { flags, longs, operandWords } = parseWords(args);
|
|
276
|
+
const sym = flags.has('s') || longs.has('--symbolic');
|
|
277
277
|
const kind = sym ? 'SymbolicLink' : 'HardLink';
|
|
278
278
|
const label = sym ? 'symbolic link' : 'hard link';
|
|
279
279
|
return [
|
|
@@ -288,8 +288,8 @@ const ln = (args) => {
|
|
|
288
288
|
].join('\n');
|
|
289
289
|
};
|
|
290
290
|
const readlink = (args) => {
|
|
291
|
-
const { flags, operandWords } = parseWords(args);
|
|
292
|
-
const canon = flags.has('f');
|
|
291
|
+
const { flags, longs, operandWords } = parseWords(args);
|
|
292
|
+
const canon = flags.has('f') || longs.has('--canonicalize');
|
|
293
293
|
return [
|
|
294
294
|
'$fx_p = ' + (operandWords.length ? operandExpr(operandWords[0]) : "''"),
|
|
295
295
|
"if ($fx_p -eq '') { [Console]::Error.WriteLine('readlink: missing operand'); $script:fx_exit = 1 }",
|
|
@@ -422,10 +422,31 @@ const file = (args) => {
|
|
|
422
422
|
/* du / df */
|
|
423
423
|
/* ------------------------------------------------------------------ */
|
|
424
424
|
const du = (args) => {
|
|
425
|
-
const { flags, longs, operandWords } = parseWords(args, [], ['--max-depth']);
|
|
425
|
+
const { flags, longs, values, missingValue, operandWords } = parseWords(args, ['d'], ['--max-depth']);
|
|
426
426
|
const sum = flags.has('s') || longs.has('--summarize');
|
|
427
427
|
const human = flags.has('h') || longs.has('--human-readable');
|
|
428
|
+
if (missingValue.includes('-d') || missingValue.includes('--max-depth')) {
|
|
429
|
+
return ('[Console]::Error.WriteLine(' +
|
|
430
|
+
psStr("du: option requires an argument -- 'max-depth'") +
|
|
431
|
+
'); $script:fx_exit = 1');
|
|
432
|
+
}
|
|
433
|
+
const maxDepthRaw = values.get('-d') ?? values.get('--max-depth');
|
|
434
|
+
let maxDepth = null;
|
|
435
|
+
if (maxDepthRaw !== undefined) {
|
|
436
|
+
if (!/^\d+$/.test(maxDepthRaw)) {
|
|
437
|
+
return ('[Console]::Error.WriteLine(' +
|
|
438
|
+
psStr("du: invalid maximum depth '" + maxDepthRaw + "'") +
|
|
439
|
+
'); $script:fx_exit = 1');
|
|
440
|
+
}
|
|
441
|
+
maxDepth = parseInt(maxDepthRaw, 10);
|
|
442
|
+
}
|
|
443
|
+
if (sum && maxDepth !== null && maxDepth !== 0) {
|
|
444
|
+
return ('[Console]::Error.WriteLine(' +
|
|
445
|
+
psStr('du: summarizing conflicts with --max-depth=' + String(maxDepth)) +
|
|
446
|
+
'); $script:fx_exit = 1');
|
|
447
|
+
}
|
|
428
448
|
const targets = operandWords.length ? argListExpr(operandWords) : "@('.')";
|
|
449
|
+
const onlyRoot = sum || maxDepth === 0;
|
|
429
450
|
return [
|
|
430
451
|
PS_HSIZE_FN,
|
|
431
452
|
'function fx-size($p) {',
|
|
@@ -436,7 +457,7 @@ const du = (args) => {
|
|
|
436
457
|
'$fx_ts = ' + targets,
|
|
437
458
|
'foreach ($fx_t in $fx_ts) {',
|
|
438
459
|
' if (-not (Test-Path -LiteralPath $fx_t)) { [Console]::Error.WriteLine("du: cannot access \'" + $fx_t + "\': No such file or directory"); $script:fx_exit = 1; continue }',
|
|
439
|
-
' if (' + (
|
|
460
|
+
' if (' + (onlyRoot ? '$true' : '$false') + ') {',
|
|
440
461
|
' $fx_kb = fx-size $fx_t',
|
|
441
462
|
' if (' + (human ? '$true' : '$false') + ') { "{0}`t{1}" -f (fx-hsize ($fx_kb * 1KB)), $fx_t } else { "{0}`t{1}" -f $fx_kb, $fx_t }',
|
|
442
463
|
' } else {',
|
|
@@ -444,6 +465,11 @@ const du = (args) => {
|
|
|
444
465
|
' foreach ($fx_d in @(Get-ChildItem -LiteralPath $fx_t -Recurse -Force -Directory -ErrorAction SilentlyContinue)) {',
|
|
445
466
|
' $fx_kb = fx-size $fx_d.FullName',
|
|
446
467
|
" $fx_rel = './' + $fx_d.FullName.Substring($fx_root.Length).TrimStart('\\').Replace('\\', '/')",
|
|
468
|
+
maxDepth !== null && maxDepth > 0
|
|
469
|
+
? " $fx_ddepth = @($fx_rel.ToCharArray() | Where-Object { $_ -eq '/' }).Count; if ($fx_ddepth -gt " +
|
|
470
|
+
maxDepth +
|
|
471
|
+
') { continue }'
|
|
472
|
+
: '',
|
|
447
473
|
' if (' + (human ? '$true' : '$false') + ') { "{0}`t{1}" -f (fx-hsize ($fx_kb * 1KB)), $fx_rel } else { "{0}`t{1}" -f $fx_kb, $fx_rel }',
|
|
448
474
|
' }',
|
|
449
475
|
' $fx_kb = fx-size $fx_t',
|
|
@@ -453,8 +479,8 @@ const du = (args) => {
|
|
|
453
479
|
].join('\n');
|
|
454
480
|
};
|
|
455
481
|
const df = (args) => {
|
|
456
|
-
const { flags } = parseWords(args);
|
|
457
|
-
const human = flags.has('h') || flags.has('H');
|
|
482
|
+
const { flags, longs } = parseWords(args);
|
|
483
|
+
const human = flags.has('h') || flags.has('H') || longs.has('--human-readable');
|
|
458
484
|
return [
|
|
459
485
|
PS_HSIZE_FN,
|
|
460
486
|
'"Filesystem Size Used Avail Use% Mounted on"',
|
|
@@ -801,9 +827,9 @@ const chown = () => {
|
|
|
801
827
|
/* diff — LCS-based, GNU normal format (+ -q, -u) */
|
|
802
828
|
/* ------------------------------------------------------------------ */
|
|
803
829
|
const diff = (args) => {
|
|
804
|
-
const { flags, operandWords } = parseWords(args);
|
|
805
|
-
const unified = flags.has('u') || flags.has('U');
|
|
806
|
-
const brief = flags.has('q') ||
|
|
830
|
+
const { flags, longs, operandWords } = parseWords(args);
|
|
831
|
+
const unified = flags.has('u') || flags.has('U') || longs.has('--unified');
|
|
832
|
+
const brief = flags.has('q') || longs.has('--brief');
|
|
807
833
|
void unified;
|
|
808
834
|
return [
|
|
809
835
|
PS_READTEXT_FN,
|
|
@@ -910,6 +936,43 @@ export const specs = [
|
|
|
910
936
|
opt('i', '--interactive', 'unsupported', INTERACTIVE),
|
|
911
937
|
], rm),
|
|
912
938
|
fileSpec(['touch'], ['write'], [opt('c', '--no-create')], touch),
|
|
939
|
+
fileSpec(['du'], ['read'], [
|
|
940
|
+
opt('s', '--summarize'),
|
|
941
|
+
opt('h', '--human-readable'),
|
|
942
|
+
opt('d', '--max-depth', 'implemented', { takesValue: true }),
|
|
943
|
+
], du),
|
|
944
|
+
fileSpec(['ls', 'll'], ['read'], [
|
|
945
|
+
opt('l', '--long'),
|
|
946
|
+
opt(undefined, '--format', 'implemented', { takesValue: true }),
|
|
947
|
+
opt('a', '--all'),
|
|
948
|
+
opt('A', '--almost-all'),
|
|
949
|
+
opt('d', '--directory'),
|
|
950
|
+
opt('h', '--human-readable'),
|
|
951
|
+
opt('F', '--classify'),
|
|
952
|
+
opt('p', undefined),
|
|
953
|
+
opt('t', undefined),
|
|
954
|
+
opt('S', undefined),
|
|
955
|
+
opt('r', undefined),
|
|
956
|
+
opt('R', '--recursive', 'unsupported', { reason: 'recursive listing' }),
|
|
957
|
+
], ls),
|
|
958
|
+
fileSpec(['mkdir'], ['write'], [opt('p', '--parents'), opt('v', '--verbose')], mkdir),
|
|
959
|
+
fileSpec(['rmdir'], ['delete'], [], rmdir),
|
|
960
|
+
fileSpec(['mktemp'], ['write'], [opt('d', '--directory')], mktemp),
|
|
961
|
+
fileSpec(['ln'], ['read', 'write'], [opt('s', '--symbolic')], ln),
|
|
962
|
+
fileSpec(['readlink'], ['read'], [opt('f', '--canonicalize')], readlink),
|
|
963
|
+
fileSpec(['realpath'], ['read'], [], realpath),
|
|
964
|
+
fileSpec(['basename'], ['read'], [], basename),
|
|
965
|
+
fileSpec(['dirname'], ['read'], [], dirname),
|
|
966
|
+
fileSpec(['stat'], ['read'], [
|
|
967
|
+
opt('c', undefined, 'implemented', { takesValue: true }),
|
|
968
|
+
opt(undefined, '--format', 'implemented', { takesValue: true }),
|
|
969
|
+
opt(undefined, '--printf', 'implemented', { takesValue: true }),
|
|
970
|
+
], stat),
|
|
971
|
+
fileSpec(['file'], ['read'], [], file),
|
|
972
|
+
fileSpec(['df'], ['read'], [opt('h', '--human-readable'), opt('H', undefined)], df),
|
|
973
|
+
fileSpec(['chmod'], ['write'], [opt('R', '--recursive', 'unsupported', { reason: 'recursive chmod' })], chmod),
|
|
974
|
+
fileSpec(['chown'], ['write'], [], chown),
|
|
975
|
+
fileSpec(['diff'], ['read'], [opt('q', '--brief'), opt('u', '--unified'), opt('U', undefined)], diff),
|
|
913
976
|
];
|
|
914
977
|
export const handlers = {
|
|
915
978
|
ls,
|
|
@@ -924,10 +987,7 @@ export const handlers = {
|
|
|
924
987
|
dirname,
|
|
925
988
|
stat,
|
|
926
989
|
file,
|
|
927
|
-
du,
|
|
928
990
|
df,
|
|
991
|
+
du,
|
|
929
992
|
find,
|
|
930
|
-
chmod,
|
|
931
|
-
chown,
|
|
932
|
-
diff,
|
|
933
993
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { registerAll, registerSpecs } from '../registry.js';
|
|
2
2
|
import { handlers as files, specs as fileSpecs } from './files.js';
|
|
3
|
-
import { handlers as textFilters } from './text-filters.js';
|
|
3
|
+
import { handlers as textFilters, specs as textFilterSpecs } from './text-filters.js';
|
|
4
4
|
import { handlers as textIo, specs as textIoSpecs } from './text-io.js';
|
|
5
5
|
import { handlers as sysinfo } from './sysinfo.js';
|
|
6
6
|
import { handlers as net } from './net.js';
|
|
@@ -15,5 +15,6 @@ export function installAll() {
|
|
|
15
15
|
registerAll(archive);
|
|
16
16
|
registerSpecs(fileSpecs);
|
|
17
17
|
registerSpecs(textIoSpecs);
|
|
18
|
+
registerSpecs(textFilterSpecs);
|
|
18
19
|
}
|
|
19
20
|
installAll();
|
|
@@ -268,13 +268,23 @@ const grep = (args) => {
|
|
|
268
268
|
const excludeDirGlobs = filterOptions
|
|
269
269
|
.filter((o) => o.name === '--exclude-dir')
|
|
270
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));
|
|
271
|
+
const { flags, operandWords, values, missingValue } = parseWords(args, ['A', 'B', 'C', 'm', 'e'], [...filterOptionNames, '--max-count', '--regexp']);
|
|
272
|
+
const missingFilterOption = missingValue.find((o) => [...filterOptionNames, '-m', '--max-count', '-e', '--regexp'].includes(o));
|
|
273
273
|
if (missingFilterOption) {
|
|
274
274
|
return ('[Console]::Error.WriteLine(' +
|
|
275
275
|
psStr("grep: option '" + missingFilterOption + "' requires an argument") +
|
|
276
276
|
'); $script:fx_exit = 2');
|
|
277
277
|
}
|
|
278
|
+
const maxCountRaw = values.get('-m') ?? values.get('--max-count');
|
|
279
|
+
let maxCount = null;
|
|
280
|
+
if (maxCountRaw !== undefined) {
|
|
281
|
+
if (!/^\d+$/.test(maxCountRaw)) {
|
|
282
|
+
return ('[Console]::Error.WriteLine(' +
|
|
283
|
+
psStr("grep: invalid max count '" + maxCountRaw + "'") +
|
|
284
|
+
'); $script:fx_exit = 2');
|
|
285
|
+
}
|
|
286
|
+
maxCount = parseInt(maxCountRaw, 10);
|
|
287
|
+
}
|
|
278
288
|
const ci = flags.has('i');
|
|
279
289
|
const inv = flags.has('v');
|
|
280
290
|
const num = flags.has('n');
|
|
@@ -294,11 +304,12 @@ const grep = (args) => {
|
|
|
294
304
|
};
|
|
295
305
|
const ctxA = Math.max(toInt(values.get('-A')), toInt(values.get('-C')));
|
|
296
306
|
const ctxB = Math.max(toInt(values.get('-B')), toInt(values.get('-C')));
|
|
297
|
-
|
|
307
|
+
const ePat = values.get('-e') ?? values.get('--regexp');
|
|
308
|
+
if (ePat === undefined && operandWords.length === 0) {
|
|
298
309
|
return ("[Console]::Error.WriteLine('usage: grep [OPTION]... PATTERN [FILE]...'); $script:fx_exit = 2");
|
|
299
310
|
}
|
|
300
|
-
const patternWord = operandWords[0];
|
|
301
|
-
const fileWords = operandWords.slice(1);
|
|
311
|
+
const patternWord = ePat !== undefined ? [{ kind: 'Text', text: ePat }] : operandWords[0];
|
|
312
|
+
const fileWords = ePat !== undefined ? operandWords : operandWords.slice(1);
|
|
302
313
|
const patLit = literalOfWord(patternWord);
|
|
303
314
|
let patExpr;
|
|
304
315
|
if (fixed || patLit === null) {
|
|
@@ -435,10 +446,16 @@ const grep = (args) => {
|
|
|
435
446
|
lines.push('}');
|
|
436
447
|
// --- per-source scan body ----------------------------------------------
|
|
437
448
|
const scan = [];
|
|
449
|
+
if (maxCount !== null)
|
|
450
|
+
scan.push('$fx_mleft = ' + maxCount);
|
|
438
451
|
if (cntMode) {
|
|
439
452
|
scan.push('$fx_c = 0');
|
|
440
453
|
scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
|
|
441
|
-
|
|
454
|
+
if (maxCount !== null)
|
|
455
|
+
scan.push(' if ($fx_mleft -le 0) { break }');
|
|
456
|
+
scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_c++' +
|
|
457
|
+
(maxCount !== null ? '; $fx_mleft--; if ($fx_mleft -le 0) { break }' : '') +
|
|
458
|
+
' }');
|
|
442
459
|
scan.push('}');
|
|
443
460
|
scan.push('if ($fx_c -gt 0) { $fx_any = $true }');
|
|
444
461
|
scan.push('if ($fx_pre) { $fx_disp + \':\' + [string]$fx_c } else { [string]$fx_c }');
|
|
@@ -446,21 +463,29 @@ const grep = (args) => {
|
|
|
446
463
|
else if (listMode) {
|
|
447
464
|
scan.push('$fx_hit1 = $false');
|
|
448
465
|
scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
|
|
466
|
+
if (maxCount !== null)
|
|
467
|
+
scan.push(' if ($fx_mleft -le 0) { break }');
|
|
449
468
|
scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_hit1 = $true; break }');
|
|
450
469
|
scan.push('}');
|
|
451
470
|
scan.push('if ($fx_hit1) { $fx_any = $true; $fx_disp }');
|
|
452
471
|
}
|
|
453
472
|
else if (quiet) {
|
|
454
473
|
scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
|
|
474
|
+
if (maxCount !== null)
|
|
475
|
+
scan.push(' if ($fx_mleft -le 0) { break }');
|
|
455
476
|
scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_any = $true; break }');
|
|
456
477
|
scan.push('}');
|
|
457
478
|
}
|
|
458
479
|
else {
|
|
459
480
|
scan.push('$fx_hits = @()');
|
|
460
481
|
scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
|
|
482
|
+
if (maxCount !== null)
|
|
483
|
+
scan.push(' if ($fx_mleft -le 0) { break }');
|
|
461
484
|
scan.push(' $fx_l = $fx_ls[$fx_i]');
|
|
462
485
|
scan.push(' if (fx-gmatch $fx_l) {');
|
|
463
486
|
scan.push(' $fx_any = $true');
|
|
487
|
+
if (maxCount !== null)
|
|
488
|
+
scan.push(' $fx_mleft--');
|
|
464
489
|
if (onlyMatch && !inv) {
|
|
465
490
|
if (fixed) {
|
|
466
491
|
if (ci) {
|
|
@@ -492,6 +517,8 @@ const grep = (args) => {
|
|
|
492
517
|
else if (!onlyMatch) {
|
|
493
518
|
scan.push(' $fx_hits += $fx_i');
|
|
494
519
|
}
|
|
520
|
+
if (maxCount !== null)
|
|
521
|
+
scan.push(' if ($fx_mleft -le 0) { break }');
|
|
495
522
|
scan.push(' }');
|
|
496
523
|
scan.push('}');
|
|
497
524
|
if (!onlyMatch) {
|
|
@@ -2347,8 +2374,41 @@ const tr = (args) => {
|
|
|
2347
2374
|
return lines.join('\n');
|
|
2348
2375
|
};
|
|
2349
2376
|
/* ------------------------------------------------------------------ */
|
|
2377
|
+
export const specs = [
|
|
2378
|
+
{
|
|
2379
|
+
names: ['grep'],
|
|
2380
|
+
options: [
|
|
2381
|
+
{ short: 'i', support: 'implemented' },
|
|
2382
|
+
{ short: 'v', support: 'implemented' },
|
|
2383
|
+
{ short: 'n', support: 'implemented' },
|
|
2384
|
+
{ short: 'c', support: 'implemented' },
|
|
2385
|
+
{ short: 'l', support: 'implemented' },
|
|
2386
|
+
{ short: 'r', support: 'implemented' },
|
|
2387
|
+
{ short: 'R', support: 'implemented' },
|
|
2388
|
+
{ short: 'E', support: 'implemented' },
|
|
2389
|
+
{ short: 'F', support: 'implemented' },
|
|
2390
|
+
{ short: 'w', support: 'implemented' },
|
|
2391
|
+
{ short: 'q', support: 'implemented' },
|
|
2392
|
+
{ short: 'o', support: 'implemented' },
|
|
2393
|
+
{ short: 'h', support: 'implemented' },
|
|
2394
|
+
{ short: 'H', support: 'implemented' },
|
|
2395
|
+
{ short: 'A', takesValue: true, support: 'implemented' },
|
|
2396
|
+
{ short: 'B', takesValue: true, support: 'implemented' },
|
|
2397
|
+
{ short: 'C', takesValue: true, support: 'implemented' },
|
|
2398
|
+
{ short: 'm', long: '--max-count', takesValue: true, support: 'implemented' },
|
|
2399
|
+
{ short: 'e', long: '--regexp', takesValue: true, support: 'implemented' },
|
|
2400
|
+
{ long: '--include', takesValue: true, support: 'implemented' },
|
|
2401
|
+
{ long: '--exclude', takesValue: true, support: 'implemented' },
|
|
2402
|
+
{ long: '--exclude-dir', takesValue: true, support: 'implemented' },
|
|
2403
|
+
],
|
|
2404
|
+
effects: ['read'],
|
|
2405
|
+
platform: 'windows-ps51',
|
|
2406
|
+
dispatch: 'translated',
|
|
2407
|
+
usageExit: 2,
|
|
2408
|
+
handler: grep,
|
|
2409
|
+
},
|
|
2410
|
+
];
|
|
2350
2411
|
export const handlers = {
|
|
2351
|
-
grep,
|
|
2352
2412
|
egrep: (args, ctx) => grep([[{ kind: 'Text', text: '-E' }], ...args], ctx), // egrep = grep -E
|
|
2353
2413
|
sed,
|
|
2354
2414
|
awk,
|
package/dist/commands/text-io.js
CHANGED
|
@@ -419,6 +419,37 @@ const head = (args, ctx) => {
|
|
|
419
419
|
continue;
|
|
420
420
|
}
|
|
421
421
|
if (t.startsWith('--')) {
|
|
422
|
+
const eq = t.indexOf('=');
|
|
423
|
+
const name = eq >= 0 ? t.slice(0, eq) : t;
|
|
424
|
+
const inline = eq >= 0 ? t.slice(eq + 1) : null;
|
|
425
|
+
if (name === '--lines' || name === '--bytes') {
|
|
426
|
+
let val = inline;
|
|
427
|
+
if (val === null) {
|
|
428
|
+
if (i + 1 >= args.length) {
|
|
429
|
+
return psErrExpr(psStr('head: option requires an argument -- ' + name.slice(2)));
|
|
430
|
+
}
|
|
431
|
+
val = wordToString(args[i + 1]);
|
|
432
|
+
i += 2;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
i++;
|
|
436
|
+
}
|
|
437
|
+
if (name === '--bytes')
|
|
438
|
+
nBytes = val;
|
|
439
|
+
else
|
|
440
|
+
nLines = val;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
if (name === '--quiet' || name === '--silent') {
|
|
444
|
+
quiet = true;
|
|
445
|
+
i++;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (name === '--verbose') {
|
|
449
|
+
verbose = true;
|
|
450
|
+
i++;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
422
453
|
i++;
|
|
423
454
|
continue;
|
|
424
455
|
}
|
|
@@ -464,6 +495,9 @@ const head = (args, ctx) => {
|
|
|
464
495
|
}
|
|
465
496
|
const bytesMode = nBytes !== null;
|
|
466
497
|
const countLit = bytesMode ? nBytes : nLines !== null ? nLines : '10';
|
|
498
|
+
if (!/^[+-]?\d+$/.test(countLit)) {
|
|
499
|
+
return psErrExpr(psStr("head: invalid number of " + (bytesMode ? 'bytes' : 'lines') + ": '" + countLit + "'"));
|
|
500
|
+
}
|
|
467
501
|
const lines = [
|
|
468
502
|
PS_GLOB_FN,
|
|
469
503
|
PS_READTEXT_FN,
|
|
@@ -516,6 +550,39 @@ const tail = (args, ctx) => {
|
|
|
516
550
|
continue;
|
|
517
551
|
}
|
|
518
552
|
if (t.startsWith('--')) {
|
|
553
|
+
const eq = t.indexOf('=');
|
|
554
|
+
const name = eq >= 0 ? t.slice(0, eq) : t;
|
|
555
|
+
const inline = eq >= 0 ? t.slice(eq + 1) : null;
|
|
556
|
+
if (name === '--lines' || name === '--bytes') {
|
|
557
|
+
let val = inline;
|
|
558
|
+
if (val === null) {
|
|
559
|
+
if (i + 1 >= args.length) {
|
|
560
|
+
return psErrExpr(psStr('tail: option requires an argument -- ' + name.slice(2)));
|
|
561
|
+
}
|
|
562
|
+
val = wordToString(args[i + 1]);
|
|
563
|
+
i += 2;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
i++;
|
|
567
|
+
}
|
|
568
|
+
if (name === '--bytes')
|
|
569
|
+
nBytes = val;
|
|
570
|
+
else if (val.startsWith('+'))
|
|
571
|
+
fromLine = val.slice(1);
|
|
572
|
+
else
|
|
573
|
+
nLines = val.replace(/^-/, '');
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (name === '--quiet' || name === '--silent') {
|
|
577
|
+
quiet = true;
|
|
578
|
+
i++;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (name === '--verbose') {
|
|
582
|
+
verbose = true;
|
|
583
|
+
i++;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
519
586
|
i++;
|
|
520
587
|
continue;
|
|
521
588
|
}
|
|
@@ -1153,12 +1220,25 @@ export const specs = [
|
|
|
1153
1220
|
dispatch: 'translated',
|
|
1154
1221
|
handler: tee,
|
|
1155
1222
|
},
|
|
1223
|
+
{
|
|
1224
|
+
names: ['head'],
|
|
1225
|
+
options: [
|
|
1226
|
+
{ short: 'n', long: '--lines', takesValue: true, support: 'implemented' },
|
|
1227
|
+
{ short: 'c', long: '--bytes', takesValue: true, support: 'implemented' },
|
|
1228
|
+
{ short: 'q', long: '--quiet', support: 'implemented' },
|
|
1229
|
+
{ long: '--silent', support: 'implemented' },
|
|
1230
|
+
{ short: 'v', long: '--verbose', support: 'implemented' },
|
|
1231
|
+
],
|
|
1232
|
+
effects: ['read'],
|
|
1233
|
+
platform: 'windows-ps51',
|
|
1234
|
+
dispatch: 'translated',
|
|
1235
|
+
handler: head,
|
|
1236
|
+
},
|
|
1156
1237
|
];
|
|
1157
1238
|
export const handlers = {
|
|
1158
1239
|
echo,
|
|
1159
1240
|
printf,
|
|
1160
1241
|
cat,
|
|
1161
|
-
head,
|
|
1162
1242
|
tail,
|
|
1163
1243
|
wc,
|
|
1164
1244
|
nl,
|
package/dist/executor.js
CHANGED
|
@@ -410,7 +410,7 @@ async function runPlans(plans, session, opts, afterSegment, ensureHost) {
|
|
|
410
410
|
FAUXNIX_CWD: currentDir,
|
|
411
411
|
FAUXNIX_PREV_EXIT: session.prevExit === null ? '' : String(session.prevExit),
|
|
412
412
|
FAUXNIX_STDIN_FILE: red.stdinFile || '',
|
|
413
|
-
}, remainingMs, opts.signal);
|
|
413
|
+
}, remainingMs, opts.signal, { stdoutLimit, stderrLimit });
|
|
414
414
|
if (inv.spawnError === 'ENOENT' || inv.spawnError === 'START') {
|
|
415
415
|
stderr += inv.stderr.toString('utf8') || PS_MISSING_MESSAGE;
|
|
416
416
|
exitCode = 127;
|
package/dist/ps-host.d.ts
CHANGED
|
@@ -14,7 +14,39 @@ export interface HostInvokeResult {
|
|
|
14
14
|
export interface HostRequestEnv {
|
|
15
15
|
[key: string]: string;
|
|
16
16
|
}
|
|
17
|
-
export declare function encodeHostRequest(id: string, script: string, env: HostRequestEnv
|
|
17
|
+
export declare function encodeHostRequest(id: string, script: string, env: HostRequestEnv, opts?: {
|
|
18
|
+
v?: number;
|
|
19
|
+
stdoutLimit?: number;
|
|
20
|
+
stderrLimit?: number;
|
|
21
|
+
}): string;
|
|
22
|
+
export type HostV2Frame = {
|
|
23
|
+
v: 2;
|
|
24
|
+
type: 'ready';
|
|
25
|
+
capabilities?: {
|
|
26
|
+
cancel?: boolean;
|
|
27
|
+
maxChunkBytes?: number;
|
|
28
|
+
stderrMarker?: boolean;
|
|
29
|
+
};
|
|
30
|
+
} | {
|
|
31
|
+
v: 2;
|
|
32
|
+
type: 'stdout' | 'stderr';
|
|
33
|
+
id: string;
|
|
34
|
+
seq: number;
|
|
35
|
+
dataB64: string;
|
|
36
|
+
} | {
|
|
37
|
+
v: 2;
|
|
38
|
+
type: 'end';
|
|
39
|
+
id: string;
|
|
40
|
+
exitCode: number;
|
|
41
|
+
timedOut?: boolean;
|
|
42
|
+
cancelled?: boolean;
|
|
43
|
+
truncated?: boolean;
|
|
44
|
+
};
|
|
45
|
+
export declare function parseHostLine(line: string): {
|
|
46
|
+
v1Ready?: boolean;
|
|
47
|
+
v2?: HostV2Frame;
|
|
48
|
+
v1?: ReturnType<typeof decodeHostResponse>;
|
|
49
|
+
};
|
|
18
50
|
export declare function decodeHostResponse(line: string): {
|
|
19
51
|
id: string;
|
|
20
52
|
stdout: Buffer;
|
|
@@ -40,10 +72,14 @@ export declare class PowerShellHost {
|
|
|
40
72
|
private closed;
|
|
41
73
|
private startLock;
|
|
42
74
|
private invokeLock;
|
|
75
|
+
protocol: 1 | 2;
|
|
43
76
|
constructor(hostFile: string, envFn: () => NodeJS.ProcessEnv);
|
|
44
77
|
/** Start the resident process and wait for the ready handshake (B1 prewarm). */
|
|
45
78
|
ready(): Promise<HostInvokeResult | null>;
|
|
46
|
-
invoke(script: string, env: HostRequestEnv, timeoutMs: number, signal?: AbortSignal
|
|
79
|
+
invoke(script: string, env: HostRequestEnv, timeoutMs: number, signal?: AbortSignal, limits?: {
|
|
80
|
+
stdoutLimit?: number;
|
|
81
|
+
stderrLimit?: number;
|
|
82
|
+
}): Promise<HostInvokeResult>;
|
|
47
83
|
drainNativeStderr(): Buffer;
|
|
48
84
|
stop(): Promise<void>;
|
|
49
85
|
private cancelledResult;
|
|
@@ -55,5 +91,7 @@ export declare class PowerShellHost {
|
|
|
55
91
|
private nextLine;
|
|
56
92
|
private nextReadyLine;
|
|
57
93
|
private nextJsonLine;
|
|
94
|
+
private collectV2;
|
|
95
|
+
private waitNativeMarker;
|
|
58
96
|
private failWaiters;
|
|
59
97
|
}
|
package/dist/ps-host.js
CHANGED
|
@@ -8,12 +8,27 @@ export const PS_MISSING_MESSAGE = 'fauxnix: powershell.exe not found — fauxnix
|
|
|
8
8
|
'Run fauxnix on Windows, or install PowerShell and make powershell.exe reachable on PATH.\n';
|
|
9
9
|
export const DEFAULT_STDOUT_LIMIT = 8_388_608;
|
|
10
10
|
export const DEFAULT_STDERR_LIMIT = 1_048_576;
|
|
11
|
-
export function encodeHostRequest(id, script, env) {
|
|
12
|
-
|
|
11
|
+
export function encodeHostRequest(id, script, env, opts) {
|
|
12
|
+
const body = {
|
|
13
13
|
id,
|
|
14
14
|
scriptB64: Buffer.from(script, 'utf8').toString('base64'),
|
|
15
15
|
env,
|
|
16
|
-
}
|
|
16
|
+
};
|
|
17
|
+
if (opts?.v === 2) {
|
|
18
|
+
body.v = 2;
|
|
19
|
+
body.type = 'run';
|
|
20
|
+
body.stdoutLimit = opts.stdoutLimit ?? DEFAULT_STDOUT_LIMIT;
|
|
21
|
+
body.stderrLimit = opts.stderrLimit ?? DEFAULT_STDERR_LIMIT;
|
|
22
|
+
}
|
|
23
|
+
return JSON.stringify(body);
|
|
24
|
+
}
|
|
25
|
+
export function parseHostLine(line) {
|
|
26
|
+
const j = JSON.parse(line);
|
|
27
|
+
if (j && j.v === 2 && typeof j.type === 'string')
|
|
28
|
+
return { v2: j };
|
|
29
|
+
if (j && j.ready === true)
|
|
30
|
+
return { v1Ready: true };
|
|
31
|
+
return { v1: decodeHostResponse(line) };
|
|
17
32
|
}
|
|
18
33
|
export function decodeHostResponse(line) {
|
|
19
34
|
const j = JSON.parse(line);
|
|
@@ -44,6 +59,7 @@ export class PowerShellHost {
|
|
|
44
59
|
closed = false;
|
|
45
60
|
startLock = null;
|
|
46
61
|
invokeLock = Promise.resolve();
|
|
62
|
+
protocol = 1;
|
|
47
63
|
constructor(hostFile, envFn) {
|
|
48
64
|
this.hostFile = hostFile;
|
|
49
65
|
this.envFn = envFn;
|
|
@@ -52,8 +68,8 @@ export class PowerShellHost {
|
|
|
52
68
|
async ready() {
|
|
53
69
|
return this.ensureStarted();
|
|
54
70
|
}
|
|
55
|
-
async invoke(script, env, timeoutMs, signal) {
|
|
56
|
-
const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs, signal));
|
|
71
|
+
async invoke(script, env, timeoutMs, signal, limits) {
|
|
72
|
+
const run = this.invokeLock.then(() => this.invokeSerial(script, env, timeoutMs, signal, limits));
|
|
57
73
|
this.invokeLock = run.then(() => undefined, () => undefined);
|
|
58
74
|
return run;
|
|
59
75
|
}
|
|
@@ -101,7 +117,7 @@ export class PowerShellHost {
|
|
|
101
117
|
truncated: false,
|
|
102
118
|
};
|
|
103
119
|
}
|
|
104
|
-
async invokeSerial(script, env, timeoutMs, signal) {
|
|
120
|
+
async invokeSerial(script, env, timeoutMs, signal, limits) {
|
|
105
121
|
if (signal?.aborted) {
|
|
106
122
|
await this.stop();
|
|
107
123
|
return this.cancelledResult();
|
|
@@ -110,7 +126,9 @@ export class PowerShellHost {
|
|
|
110
126
|
if (started)
|
|
111
127
|
return { ...started, cancelled: false, truncated: false };
|
|
112
128
|
const id = 'f' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
113
|
-
const line = encodeHostRequest(id, script, env
|
|
129
|
+
const line = encodeHostRequest(id, script, env, this.protocol === 2
|
|
130
|
+
? { v: 2, stdoutLimit: limits?.stdoutLimit, stderrLimit: limits?.stderrLimit }
|
|
131
|
+
: undefined);
|
|
114
132
|
try {
|
|
115
133
|
this.proc.stdin.write(line + '\n');
|
|
116
134
|
}
|
|
@@ -133,6 +151,9 @@ export class PowerShellHost {
|
|
|
133
151
|
};
|
|
134
152
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
135
153
|
try {
|
|
154
|
+
if (this.protocol === 2) {
|
|
155
|
+
return await this.collectV2(id, timeoutMs);
|
|
156
|
+
}
|
|
136
157
|
const raw = await this.nextJsonLine(timeoutMs, id);
|
|
137
158
|
const msg = decodeHostResponse(raw);
|
|
138
159
|
const native = this.drainNativeStderr();
|
|
@@ -271,10 +292,13 @@ export class PowerShellHost {
|
|
|
271
292
|
});
|
|
272
293
|
try {
|
|
273
294
|
const readyLine = await this.nextReadyLine(READY_TIMEOUT_MS);
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
295
|
+
const parsed = parseHostLine(readyLine);
|
|
296
|
+
if (parsed.v2?.type === 'ready')
|
|
297
|
+
this.protocol = 2;
|
|
298
|
+
else if (parsed.v1Ready || decodeHostResponse(readyLine).ready)
|
|
299
|
+
this.protocol = 1;
|
|
300
|
+
else
|
|
276
301
|
throw new Error('fauxnix: powershell host handshake failed');
|
|
277
|
-
}
|
|
278
302
|
}
|
|
279
303
|
catch (e) {
|
|
280
304
|
await this.stop();
|
|
@@ -334,8 +358,10 @@ export class PowerShellHost {
|
|
|
334
358
|
if (!line.trim())
|
|
335
359
|
continue;
|
|
336
360
|
try {
|
|
337
|
-
const
|
|
338
|
-
if (
|
|
361
|
+
const parsed = parseHostLine(line);
|
|
362
|
+
if (parsed.v2?.type === 'ready' || parsed.v1Ready)
|
|
363
|
+
return line;
|
|
364
|
+
if (parsed.v1?.ready)
|
|
339
365
|
return line;
|
|
340
366
|
}
|
|
341
367
|
catch {
|
|
@@ -367,6 +393,77 @@ export class PowerShellHost {
|
|
|
367
393
|
err.timedOut = true;
|
|
368
394
|
throw err;
|
|
369
395
|
}
|
|
396
|
+
async collectV2(id, timeoutMs) {
|
|
397
|
+
const deadline = Date.now() + timeoutMs;
|
|
398
|
+
const out = [];
|
|
399
|
+
const err = [];
|
|
400
|
+
let outSeq = 0;
|
|
401
|
+
let errSeq = 0;
|
|
402
|
+
let end = null;
|
|
403
|
+
while (!end) {
|
|
404
|
+
const line = await this.nextLine(Math.max(1, deadline - Date.now()));
|
|
405
|
+
if (!line.trim())
|
|
406
|
+
continue;
|
|
407
|
+
let parsed;
|
|
408
|
+
try {
|
|
409
|
+
parsed = parseHostLine(line);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const f = parsed.v2;
|
|
415
|
+
if (!f)
|
|
416
|
+
continue;
|
|
417
|
+
if (f.type === 'stdout' && f.id === id) {
|
|
418
|
+
if (f.seq !== outSeq)
|
|
419
|
+
throw new Error('fauxnix: host stdout seq gap');
|
|
420
|
+
out.push(Buffer.from(f.dataB64 ?? '', 'base64'));
|
|
421
|
+
outSeq++;
|
|
422
|
+
}
|
|
423
|
+
else if (f.type === 'stderr' && f.id === id) {
|
|
424
|
+
if (f.seq !== errSeq)
|
|
425
|
+
throw new Error('fauxnix: host stderr seq gap');
|
|
426
|
+
err.push(Buffer.from(f.dataB64 ?? '', 'base64'));
|
|
427
|
+
errSeq++;
|
|
428
|
+
}
|
|
429
|
+
else if (f.type === 'end' && f.id === id) {
|
|
430
|
+
end = f;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
let native = Buffer.alloc(0);
|
|
434
|
+
try {
|
|
435
|
+
native = Buffer.from(await this.waitNativeMarker(id, 2000));
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
native = Buffer.from(this.drainNativeStderr());
|
|
439
|
+
}
|
|
440
|
+
const capturedErr = Buffer.from(Buffer.concat(err));
|
|
441
|
+
const n = Number(end.exitCode);
|
|
442
|
+
return {
|
|
443
|
+
stdout: Buffer.from(Buffer.concat(out)),
|
|
444
|
+
stderr: native.length ? Buffer.from(Buffer.concat([capturedErr, native])) : capturedErr,
|
|
445
|
+
exitCode: Number.isFinite(n) ? n : 0,
|
|
446
|
+
timedOut: end.timedOut === true,
|
|
447
|
+
cancelled: end.cancelled === true,
|
|
448
|
+
truncated: end.truncated === true,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
async waitNativeMarker(id, timeoutMs) {
|
|
452
|
+
const needle = Buffer.from('FAUXNIX_ERR_END:' + id + '\n', 'utf8');
|
|
453
|
+
const deadline = Date.now() + timeoutMs;
|
|
454
|
+
while (Date.now() < deadline) {
|
|
455
|
+
const buf = Buffer.concat(this.stderrChunks);
|
|
456
|
+
const idx = buf.indexOf(needle);
|
|
457
|
+
if (idx >= 0) {
|
|
458
|
+
const before = buf.subarray(0, idx);
|
|
459
|
+
const after = buf.subarray(idx + needle.length);
|
|
460
|
+
this.stderrChunks = after.length ? [Buffer.from(after)] : [];
|
|
461
|
+
return Buffer.from(before);
|
|
462
|
+
}
|
|
463
|
+
await new Promise((r) => setTimeout(r, 15));
|
|
464
|
+
}
|
|
465
|
+
throw new Error('fauxnix: native stderr marker missing');
|
|
466
|
+
}
|
|
370
467
|
failWaiters(err) {
|
|
371
468
|
const ws = this.waiters.splice(0);
|
|
372
469
|
for (const w of ws) {
|
package/dist/registry.d.ts
CHANGED
|
@@ -97,6 +97,8 @@ export interface CommandSpec {
|
|
|
97
97
|
effects: CommandEffect[];
|
|
98
98
|
platform?: 'windows-ps51' | 'portable-translate';
|
|
99
99
|
dispatch?: 'translated' | 'native' | 'dynamic';
|
|
100
|
+
/** GNU usage/syntax exit (grep uses 2; cp/mv/rm use 1). */
|
|
101
|
+
usageExit?: number;
|
|
100
102
|
handler: Handler;
|
|
101
103
|
}
|
|
102
104
|
/** Register a spec'd command. Unknown/unsupported options become GNU-style usage errors. */
|
|
@@ -105,6 +107,8 @@ export declare function registerSpecs(list: CommandSpec[]): void;
|
|
|
105
107
|
export declare function lookupSpec(name: string): CommandSpec | undefined;
|
|
106
108
|
/** Unique specs in registration order. */
|
|
107
109
|
export declare function registeredSpecs(): CommandSpec[];
|
|
110
|
+
/** Markdown dump of every CommandSpec — source for docs/command-specs.md. */
|
|
111
|
+
export declare function specsMarkdown(): string;
|
|
108
112
|
export interface ListedCommand {
|
|
109
113
|
name: string;
|
|
110
114
|
spec: null | {
|
package/dist/registry.js
CHANGED
|
@@ -186,6 +186,43 @@ export function registeredSpecs() {
|
|
|
186
186
|
}
|
|
187
187
|
return out;
|
|
188
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
|
+
}
|
|
189
226
|
/** Capability dump for `fauxnix list --json` / MCP introspection. */
|
|
190
227
|
export function listCommandsJson() {
|
|
191
228
|
return registeredNames().map((name) => {
|
|
@@ -214,6 +251,8 @@ export function listCommandsJson() {
|
|
|
214
251
|
* null when every option is recognized and implemented.
|
|
215
252
|
*/
|
|
216
253
|
export function specOptionError(spec, args, cmdName) {
|
|
254
|
+
const usageExit = spec.usageExit ?? 1;
|
|
255
|
+
const fail = (msg) => optionFail(cmdName, msg, usageExit);
|
|
217
256
|
const shorts = new Map();
|
|
218
257
|
const longs = new Map();
|
|
219
258
|
for (const o of spec.options) {
|
|
@@ -240,18 +279,18 @@ export function specOptionError(spec, args, cmdName) {
|
|
|
240
279
|
const name = eq >= 0 ? t.slice(0, eq) : t;
|
|
241
280
|
const opt = longs.get(name);
|
|
242
281
|
if (!opt)
|
|
243
|
-
return
|
|
282
|
+
return fail("unrecognized option '" + name + "'");
|
|
244
283
|
if (opt.support === 'unsupported') {
|
|
245
|
-
return
|
|
284
|
+
return fail(unsupportedMsg(opt, name));
|
|
246
285
|
}
|
|
247
286
|
if (!opt.takesValue && eq >= 0) {
|
|
248
|
-
return
|
|
287
|
+
return fail("option '" + name + "' doesn't allow an argument");
|
|
249
288
|
}
|
|
250
289
|
if (opt.takesValue && eq < 0) {
|
|
251
290
|
if (i + 1 < args.length)
|
|
252
291
|
i++;
|
|
253
292
|
else
|
|
254
|
-
return
|
|
293
|
+
return fail("option '" + name + "' requires an argument");
|
|
255
294
|
}
|
|
256
295
|
i++;
|
|
257
296
|
continue;
|
|
@@ -262,9 +301,9 @@ export function specOptionError(spec, args, cmdName) {
|
|
|
262
301
|
const ch = body[c];
|
|
263
302
|
const opt = shorts.get(ch);
|
|
264
303
|
if (!opt)
|
|
265
|
-
return
|
|
304
|
+
return fail("invalid option -- '" + ch + "'");
|
|
266
305
|
if (opt.support === 'unsupported') {
|
|
267
|
-
return
|
|
306
|
+
return fail(unsupportedMsg(opt, '-' + ch));
|
|
268
307
|
}
|
|
269
308
|
if (opt.takesValue) {
|
|
270
309
|
const rest = body.slice(c + 1);
|
|
@@ -272,7 +311,7 @@ export function specOptionError(spec, args, cmdName) {
|
|
|
272
311
|
if (i + 1 < args.length)
|
|
273
312
|
i++;
|
|
274
313
|
else
|
|
275
|
-
return
|
|
314
|
+
return fail("option requires an argument -- '" + ch + "'");
|
|
276
315
|
}
|
|
277
316
|
break;
|
|
278
317
|
}
|
|
@@ -288,10 +327,11 @@ function unsupportedMsg(opt, shown) {
|
|
|
288
327
|
const reason = opt.reason ? ' (' + opt.reason + ')' : '';
|
|
289
328
|
return "option '" + shown + "' is not supported by fauxnix" + reason;
|
|
290
329
|
}
|
|
291
|
-
function optionFail(cmd, msg) {
|
|
330
|
+
function optionFail(cmd, msg, code = 1) {
|
|
292
331
|
return ('[Console]::Error.WriteLine(' +
|
|
293
332
|
psStr(cmd + ': ' + msg) +
|
|
294
333
|
'); [Console]::Error.WriteLine(' +
|
|
295
334
|
psStr("Try '" + cmd + " --help' for more information.") +
|
|
296
|
-
'); $script:fx_exit =
|
|
335
|
+
'); $script:fx_exit = ' +
|
|
336
|
+
String(code));
|
|
297
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,20 +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
|
-
|
|
1272
|
-
$
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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)
|
|
1277
1329
|
}
|
|
1278
|
-
$fx_proto.WriteLine($fx_json)
|
|
1279
1330
|
}
|
|
1280
1331
|
`.trim();
|
package/package.json
CHANGED