buninu 0.4.0 → 0.4.2
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/CHANGELOG.md +56 -0
- package/README.md +86 -5
- package/apps/bunmsh/CHANGELOG.md +176 -0
- package/apps/bunmsh/LICENSE-MICRO +58 -0
- package/apps/bunmsh/README.md +243 -2
- package/apps/bunmsh/help/README.md +292 -0
- package/apps/bunmsh/help/curl.md +181 -0
- package/apps/bunmsh/help/kill.md +16 -0
- package/apps/bunmsh/help/pspa.md +38 -0
- package/apps/bunmsh/help/pspac.md +57 -0
- package/apps/bunmsh/package.json +6 -2
- package/apps/bunmsh/src/completion.js +116 -0
- package/apps/bunmsh/src/curl.js +1191 -0
- package/apps/bunmsh/src/main.js +74 -0
- package/apps/bunmsh/src/shell.js +346 -5
- package/apps/bunmsh/test/curl.test.js +335 -0
- package/apps/bunmsh/test/reference.test.js +127 -1
- package/apps/bunmsh/test/shell.test.js +316 -2
- package/bin/install.js +41 -12
- package/package.json +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
## curl
|
|
2
|
+
|
|
3
|
+
Transfer a URL over HTTP or HTTPS, built on Bun's own `fetch`.
|
|
4
|
+
|
|
5
|
+
This is a **fallback builtin**: a real `curl` found on `PATH` always wins, so
|
|
6
|
+
scripts keep the system binary where there is one and still run on a device
|
|
7
|
+
that has none. `builtin curl` selects this implementation explicitly.
|
|
8
|
+
|
|
9
|
+
### Usage
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
curl [OPTIONS] URL...
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A URL with no scheme gets one: `http://` by default, `https://` when the port
|
|
16
|
+
is `443`, or whatever `--proto-default` says. So `curl localhost:8080` and
|
|
17
|
+
`curl example.com/page` work the way they do with the real curl. Only `http`
|
|
18
|
+
and `https` are supported; anything else fails with exit code `1`.
|
|
19
|
+
|
|
20
|
+
### Options and forms
|
|
21
|
+
|
|
22
|
+
#### Output and files
|
|
23
|
+
|
|
24
|
+
- `-o, --output FILE`: Write the body to `FILE` instead of stdout. `-o -`
|
|
25
|
+
means stdout. Repeat it to pair one target with each URL, in order.
|
|
26
|
+
- `-O, --remote-name`: Save under the filename from the URL path.
|
|
27
|
+
- `--remote-name-all`: Treat every URL as if it had its own `-O`.
|
|
28
|
+
- `-J, --remote-header-name`: With `-O`, prefer the name in the
|
|
29
|
+
`Content-Disposition` header.
|
|
30
|
+
- `--output-dir DIR`: Write `-o`/`-O` files under `DIR`.
|
|
31
|
+
- `--create-dirs`: Create missing parent directories for the output file.
|
|
32
|
+
- `-a, --append`: Append to the output file instead of truncating it.
|
|
33
|
+
- `-C, --continue-at OFFSET|-`: Resume a partial download. `-` sizes the
|
|
34
|
+
existing file and asks for the rest with a `Range` header; the file is
|
|
35
|
+
appended to when the server answers `206`, and rewritten when it answers
|
|
36
|
+
`200` and ignored the range. A `416` answer means the local file is already
|
|
37
|
+
complete: nothing is written and the exit status is `0`.
|
|
38
|
+
- `-D, --dump-header FILE`: Write the response headers to `FILE` (`-` for
|
|
39
|
+
stdout).
|
|
40
|
+
- `-i, --include`: Print the response headers before the body. With `-L`,
|
|
41
|
+
every hop's headers are printed, as the real curl does.
|
|
42
|
+
- `-I, --head`: Send `HEAD` and print the response headers.
|
|
43
|
+
- `-w, --write-out FORMAT`: Print `FORMAT` after the transfer, with
|
|
44
|
+
`%{variable}` substitutions and `\n`, `\t`, `\r` escapes. `@FILE` (or `@-`)
|
|
45
|
+
reads the format from a file. Supported variables: `url`, `url_effective`,
|
|
46
|
+
`method`, `scheme`, `http_code`, `response_code`, `http_version`,
|
|
47
|
+
`num_redirects`, `num_headers`, `redirect_url`, `size_download`,
|
|
48
|
+
`size_upload`, `size_header`, `size_request`, `speed_download`,
|
|
49
|
+
`speed_upload`, `content_type`, `filename_effective`, `remote_ip`,
|
|
50
|
+
`remote_port`, `ssl_verify_result`, `exitcode`, `errormsg`, `time_total`,
|
|
51
|
+
`time_namelookup`, `time_connect`, `time_appconnect`, `time_pretransfer`,
|
|
52
|
+
`time_starttransfer`, `time_redirect`, `header_json`, `json`, `stdout`,
|
|
53
|
+
`stderr`.
|
|
54
|
+
|
|
55
|
+
#### Request shaping
|
|
56
|
+
|
|
57
|
+
- `-X, --request METHOD`: Set the request method.
|
|
58
|
+
- `-H, --header 'Name: value'`: Add or replace a header. `'Name;'` sends an
|
|
59
|
+
empty value; `'Name:'` removes a header that would otherwise be sent.
|
|
60
|
+
- `-d, --data DATA`, `--data-ascii DATA`: `POST` body; `@file` reads a file
|
|
61
|
+
(newlines stripped) and `@-` reads stdin. Repeats join with `&`.
|
|
62
|
+
- `--data-raw DATA`: Same, but `@` has no special meaning.
|
|
63
|
+
- `--data-binary DATA`: Same, but a `@file` is sent byte for byte.
|
|
64
|
+
- `--data-urlencode DATA`: Percent-encode before sending. Accepts `content`,
|
|
65
|
+
`=content`, `name=content`, `@file`, and `name@file`.
|
|
66
|
+
- `--json DATA`: `--data-binary` plus `Content-Type: application/json` and
|
|
67
|
+
`Accept: application/json`. `@file` and `@-` work here too.
|
|
68
|
+
- `-G, --get`: Move the data onto the query string and send `GET`.
|
|
69
|
+
- `-F, --form 'name=value'`, `--form-string`: `multipart/form-data`. `@file`
|
|
70
|
+
attaches a file, `<file` sends its contents as the field value, and
|
|
71
|
+
`;type=` / `;filename=` set the part's type and name.
|
|
72
|
+
- `-T, --upload-file FILE`: `PUT` the file (`-` reads stdin).
|
|
73
|
+
- `-u, --user USER[:PASSWORD]`: HTTP basic authentication.
|
|
74
|
+
- `--oauth2-bearer TOKEN`: Send `Authorization: Bearer TOKEN`.
|
|
75
|
+
- `-A, --user-agent NAME`: Override the default `curl/8.14.1`.
|
|
76
|
+
- `-e, --referer URL`: Send a `Referer` header; a trailing `;auto` is ignored.
|
|
77
|
+
- `-b, --cookie 'k=v'`: Send a `Cookie` header.
|
|
78
|
+
- `-r, --range RANGE`: Send `Range: bytes=RANGE`.
|
|
79
|
+
- `--compressed`: Ask for and transparently decode a compressed response.
|
|
80
|
+
Without it, no `Accept-Encoding` is negotiated, matching curl's default.
|
|
81
|
+
- `-x, --proxy URL`: Send the request through a proxy.
|
|
82
|
+
|
|
83
|
+
#### Redirects, failure, and timing
|
|
84
|
+
|
|
85
|
+
- `-L, --location`: Follow redirects. `301`/`302`/`303` turn a `POST` into a
|
|
86
|
+
`GET`; `307`/`308` keep the method and body. `Authorization` is dropped when
|
|
87
|
+
the host changes, unless `--location-trusted` is given.
|
|
88
|
+
- `--max-redirs N`: Cap the redirect chain (default 50, `-1` for unlimited).
|
|
89
|
+
Exceeding it fails with exit code `47`.
|
|
90
|
+
- `-f, --fail`: No body output on HTTP errors; exit `22` instead.
|
|
91
|
+
- `--fail-with-body`: Print the body and still exit `22`.
|
|
92
|
+
- `-k, --insecure`: Do not verify the server certificate.
|
|
93
|
+
- `-m, --max-time SECONDS`, `--connect-timeout SECONDS`: Abort a slow
|
|
94
|
+
transfer with exit code `28`.
|
|
95
|
+
- `--retry N`, `--retry-delay SECONDS`, `--retry-all-errors`,
|
|
96
|
+
`--retry-connrefused`: Retry connection failures, timeouts, and the
|
|
97
|
+
transient statuses `408`, `429`, `500`, `502`, `503`, and `504`.
|
|
98
|
+
|
|
99
|
+
#### Reporting
|
|
100
|
+
|
|
101
|
+
- `-s, --silent`: No progress meter and no error messages.
|
|
102
|
+
- `-S, --show-error`: Keep error messages while `-s` is in effect.
|
|
103
|
+
- `-#, --progress-bar`: A single-bar progress display instead of the meter.
|
|
104
|
+
- `--no-progress-meter`: Keep errors, drop the meter.
|
|
105
|
+
- `-v, --verbose`: Trace the request and response headers on stderr.
|
|
106
|
+
- `-V, --version`: Print the version banner.
|
|
107
|
+
|
|
108
|
+
Like the real curl, the progress meter is shown only when the body is *not*
|
|
109
|
+
being painted on the terminal — a `-o`/`-O` download or a redirected stdout
|
|
110
|
+
gets a meter, `curl URL` at a prompt does not.
|
|
111
|
+
|
|
112
|
+
#### Accepted and ignored
|
|
113
|
+
|
|
114
|
+
These are parsed so a script does not die on them, and then have no effect,
|
|
115
|
+
because `fetch` either handles that part itself or does not expose it:
|
|
116
|
+
`--cacert`, `--capath`, `-E/--cert`, `--key`, `--interface`, `--limit-rate`,
|
|
117
|
+
`--resolve`, `--connect-to`, `--unix-socket`, `--noproxy`, `--proxy-user`,
|
|
118
|
+
`--proxy-header`, `-n/--netrc`, `-c/--cookie-jar`, `-z/--time-cond`,
|
|
119
|
+
`-R/--remote-time`, `-N/--no-buffer` (output is always streamed anyway),
|
|
120
|
+
`-g/--globoff` (URL globbing is not performed, so `{}` and `[]` are already
|
|
121
|
+
literal), `-q/--disable`, `-4`, `-6`, `--http1.0`, `--http1.1`, `--http2`,
|
|
122
|
+
`--http3`, `--tlsv1.x`, `--tcp-nodelay`, `--keepalive`, `--anyauth`,
|
|
123
|
+
`--basic`, `--digest`, `-Z/--parallel`, `--max-filesize`, `--path-as-is`.
|
|
124
|
+
|
|
125
|
+
### Where it differs from the system curl
|
|
126
|
+
|
|
127
|
+
- `fetch` reports header names in lower case, so `-i`, `-I`, `-D`, and `-v`
|
|
128
|
+
rebuild the conventional capitalisation (`Content-Type`, `ETag`) and list
|
|
129
|
+
the headers in `fetch`'s order rather than the server's wire order.
|
|
130
|
+
- The negotiated HTTP version is not exposed, so status lines are written as
|
|
131
|
+
`HTTP/1.1`.
|
|
132
|
+
- Bun adds a `Connection: keep-alive` request header of its own.
|
|
133
|
+
- The progress meter is a look-alike: same columns, not the same arithmetic.
|
|
134
|
+
- URL globbing (`{a,b}`, `[1-10]`) is not implemented.
|
|
135
|
+
- `fetch` forbids a body on `GET` and `HEAD`, so `-X GET -d ...` sends the
|
|
136
|
+
request without the data; use `-G` to put it on the query string instead.
|
|
137
|
+
|
|
138
|
+
### Exit status
|
|
139
|
+
|
|
140
|
+
`0` on success, `1` unsupported protocol, `2` bad usage, `3` malformed URL,
|
|
141
|
+
`6` host not resolved, `7` connection failed, `22` HTTP error with `-f`,
|
|
142
|
+
`23` write error, `26` local file could not be read, `28` timeout,
|
|
143
|
+
`47` too many redirects, `56` connection reset, `60` certificate problem.
|
|
144
|
+
|
|
145
|
+
### Example
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' example.com
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Output:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
200 text/html
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The invocations a package script tends to use:
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
curl -kLO https://example.com/pkg.tar.gz
|
|
161
|
+
curl -C - -kLO https://example.com/pkg.tar.gz
|
|
162
|
+
curl -fsSL https://example.com/install.sh | sh
|
|
163
|
+
curl -#k https://example.com/pkg.tar.gz.sha256
|
|
164
|
+
curl -kfsS https://example.com/probe >/dev/null 2>/dev/null
|
|
165
|
+
curl localhost:8080
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Calling a JSON API — enough for the OpenAI chat completions endpoint:
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
curl -sS https://api.openai.com/v1/chat/completions \
|
|
172
|
+
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
|
173
|
+
-H 'Content-Type: application/json' \
|
|
174
|
+
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`--json` shortens that, and `-N` streaming responses arrive chunk by chunk:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
curl -sN --json @request.json https://api.openai.com/v1/chat/completions
|
|
181
|
+
```
|
package/apps/bunmsh/help/kill.md
CHANGED
|
@@ -12,6 +12,22 @@ kill [-SIGNAL] PID ...
|
|
|
12
12
|
|
|
13
13
|
- `-SIGNAL`: Select a signal by name or number; the default is SIGTERM.
|
|
14
14
|
- `-0`: Check whether a process can be signalled without sending a signal.
|
|
15
|
+
- `-l`: List the signal names this runtime knows.
|
|
16
|
+
|
|
17
|
+
### Windows
|
|
18
|
+
|
|
19
|
+
Windows has no signals to deliver. The runtime turns SIGTERM, SIGINT, and
|
|
20
|
+
SIGKILL into an unconditional `TerminateProcess`, refuses every other name,
|
|
21
|
+
and in no case touches the target's children. So on Windows every terminating
|
|
22
|
+
signal is sent as `taskkill /PID PID /T /F` instead: `/T` reaches the children
|
|
23
|
+
the runtime would leave running, and `/F` is what makes it work on a console
|
|
24
|
+
process that has no message loop to close.
|
|
25
|
+
|
|
26
|
+
The consequence is that the signal name is accepted but not honoured there —
|
|
27
|
+
`kill -HUP PID` terminates the process rather than failing, and nothing gets
|
|
28
|
+
the chance to shut down gracefully. `-0` is the exception: it asks whether the
|
|
29
|
+
PID exists without touching it, which `taskkill` cannot express, so it still
|
|
30
|
+
goes through the runtime.
|
|
15
31
|
|
|
16
32
|
### Example
|
|
17
33
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## pspa
|
|
2
|
+
|
|
3
|
+
List every process as a PID and its full command line.
|
|
4
|
+
|
|
5
|
+
### Usage
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pspa
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It is a PATH-fallback builtin, so an executable named `pspa` found in `PATH`
|
|
12
|
+
wins; `builtin pspa` selects this implementation. It takes no options — pipe
|
|
13
|
+
it into `grep` to narrow the listing, and into `kill` to act on what you find.
|
|
14
|
+
|
|
15
|
+
[`pspac`](#pspac) prints the same listing with the PID and the command line
|
|
16
|
+
coloured.
|
|
17
|
+
|
|
18
|
+
On POSIX systems this is `ps -eo pid,args`, and its output is passed through
|
|
19
|
+
untouched. Reading `ps` over a pipe is also what keeps the command lines
|
|
20
|
+
whole: procps truncates them at the terminal width when it is writing straight
|
|
21
|
+
to a terminal.
|
|
22
|
+
|
|
23
|
+
Windows has no `ps`, so the same two columns are queried from
|
|
24
|
+
`Win32_Process` through PowerShell and laid out the same way, with the image
|
|
25
|
+
name standing in for a system process that reports no command line.
|
|
26
|
+
|
|
27
|
+
### Example
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pspa | grep bun
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Output:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
1004 bun ./src/main.js
|
|
37
|
+
1220 bun run dev
|
|
38
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
## pspac
|
|
2
|
+
|
|
3
|
+
The `pspa` process listing, coloured.
|
|
4
|
+
|
|
5
|
+
### Usage
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pspac
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It is a PATH-fallback builtin taking no options, and it lists exactly what
|
|
12
|
+
[`pspa`](#pspa) lists — same processes, same two columns, same order. Strip
|
|
13
|
+
the colour off its output and you have `pspa`'s byte for byte.
|
|
14
|
+
|
|
15
|
+
Like `catfancy`, it always colours rather than sniffing for a terminal, so the
|
|
16
|
+
plain listing is always one command away: use `pspa` when the output is going
|
|
17
|
+
somewhere that wants plain text.
|
|
18
|
+
|
|
19
|
+
### What gets coloured
|
|
20
|
+
|
|
21
|
+
The PID is painted as the number it is. The COMMAND column is a shell command
|
|
22
|
+
line, so it is highlighted as one, using micro's `syntax/sh.yaml` rules and
|
|
23
|
+
its `colorschemes/monokai.micro` colours — the same palette `catfancy` uses,
|
|
24
|
+
so a command line reads the same here as in a previewed script:
|
|
25
|
+
|
|
26
|
+
- Command names, shell keywords, flags, numbers, variables, quoted strings,
|
|
27
|
+
and comments each take their own colour.
|
|
28
|
+
- Quoted strings and comments are regions, so a `#` inside an argument does
|
|
29
|
+
not turn the rest of the line into a comment.
|
|
30
|
+
- Later rules win, exactly as in the editor: `--cat` is a flag, not the
|
|
31
|
+
coreutils `cat`.
|
|
32
|
+
|
|
33
|
+
Two things a process listing needs that a script does not:
|
|
34
|
+
|
|
35
|
+
- The directory in front of the program is dimmed. It is the part an absolute
|
|
36
|
+
path buries the answer in, and `ps` prints a lot of it.
|
|
37
|
+
- The program's own name is coloured as a command whether or not sh.yaml's
|
|
38
|
+
word lists have heard of it — the file has to guess a command from a list,
|
|
39
|
+
while here the first token is known to be one.
|
|
40
|
+
|
|
41
|
+
A kernel thread such as `[kworker/0:1]` has no program, path, or arguments to
|
|
42
|
+
take apart, so it recedes as a whole.
|
|
43
|
+
|
|
44
|
+
### Example
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
pspac
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Output:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
PID COMMAND
|
|
54
|
+
1 /init
|
|
55
|
+
8824 /data/data/com.termux/files/usr/bin/bash -l
|
|
56
|
+
8955 bun run dev
|
|
57
|
+
```
|
package/apps/bunmsh/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunmsh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Cross-platform command shell for Bun JavaScript",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"start": "bun ./src/main.js",
|
|
13
|
-
"test": "bun test"
|
|
13
|
+
"test": "bun test",
|
|
14
|
+
"genallhelp": "bun ./src/main.js -c 'builtin cat --exclude help/README.md help/*.md' > help/README.md"
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
16
17
|
"bun",
|
|
@@ -21,6 +22,9 @@
|
|
|
21
22
|
"assets": [
|
|
22
23
|
"README.md",
|
|
23
24
|
"CHANGELOG.md",
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"LICENSE-MKSH",
|
|
27
|
+
"LICENSE-MICRO",
|
|
24
28
|
"help"
|
|
25
29
|
]
|
|
26
30
|
}
|
|
@@ -75,6 +75,21 @@ export function completionContext(line, cursor = line.length) {
|
|
|
75
75
|
quote = ch;
|
|
76
76
|
continue;
|
|
77
77
|
}
|
|
78
|
+
// `$(` opens a nested command list, so what follows it is a command
|
|
79
|
+
// name again rather than a continuation of the word being typed.
|
|
80
|
+
if (ch === "$" && source[i + 1] === "(") {
|
|
81
|
+
finishWord();
|
|
82
|
+
command = true;
|
|
83
|
+
token = "";
|
|
84
|
+
i++;
|
|
85
|
+
wordStart = i + 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ch === ")") {
|
|
89
|
+
finishWord();
|
|
90
|
+
wordStart = i + 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
78
93
|
if (ch === ";" || ch === "|" || ch === "&" || ch === "\n") {
|
|
79
94
|
finishWord();
|
|
80
95
|
command = true;
|
|
@@ -96,9 +111,38 @@ export function completionContext(line, cursor = line.length) {
|
|
|
96
111
|
command: command && !assignment && quote === null,
|
|
97
112
|
prefix: token,
|
|
98
113
|
start: wordStart,
|
|
114
|
+
quote,
|
|
99
115
|
};
|
|
100
116
|
}
|
|
101
117
|
|
|
118
|
+
// A `$` with a name being typed after it: `$NAME`, `${NAME`, `${#NAME`.
|
|
119
|
+
// Everything else a `$` can begin is left alone, because none of it is a
|
|
120
|
+
// name in progress: `$(` opens a command substitution, `$?`/`$1`/`$$` are
|
|
121
|
+
// already complete, and a backslash-escaped `\$` is a literal dollar sign.
|
|
122
|
+
export function variableContext(line, cursor = line.length) {
|
|
123
|
+
const source = line.slice(0, cursor);
|
|
124
|
+
const match = /\$(\{#?)?([A-Za-z_][A-Za-z0-9_]*)?$/.exec(source);
|
|
125
|
+
if (!match) return null;
|
|
126
|
+
if (source[match.index - 1] === "$") return null;
|
|
127
|
+
let backslashes = 0;
|
|
128
|
+
for (let i = match.index - 1; i >= 0 && source[i] === "\\"; i--) backslashes++;
|
|
129
|
+
if (backslashes % 2) return null;
|
|
130
|
+
const name = match[2] ?? "";
|
|
131
|
+
return {
|
|
132
|
+
start: match.index,
|
|
133
|
+
text: match[0],
|
|
134
|
+
lead: match[0].slice(0, match[0].length - name.length),
|
|
135
|
+
brace: Boolean(match[1]),
|
|
136
|
+
prefix: name,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// `${NAME` completes to `${NAME}`: the brace it opened is part of the name
|
|
141
|
+
// being written, so finishing the name finishes the brace too.
|
|
142
|
+
export function variableCompletion(context, name) {
|
|
143
|
+
return `${context.lead}${name}${context.brace ? "}" : ""}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
102
146
|
export function historyGhost(history, line) {
|
|
103
147
|
if (!line) return null;
|
|
104
148
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
@@ -199,6 +243,78 @@ export class CommandIndex {
|
|
|
199
243
|
}
|
|
200
244
|
}
|
|
201
245
|
|
|
246
|
+
// A JavaScript line is not shell text, so the shell's own word-splitting says
|
|
247
|
+
// nothing useful about it: `Bun.file("/tm` is one long word to a tokenizer
|
|
248
|
+
// that only breaks on whitespace. This reads it as JavaScript instead, and
|
|
249
|
+
// answers the two questions worth asking there — is the cursor inside a
|
|
250
|
+
// string literal, where a path is being typed, or on a `$.` property, where
|
|
251
|
+
// a shell variable name is.
|
|
252
|
+
//
|
|
253
|
+
// Strings are tracked with a stack so that an interpolation is code again:
|
|
254
|
+
// inside `\`${...}\`` a `$.` completes, and the template only resumes after
|
|
255
|
+
// the closing brace.
|
|
256
|
+
export function javascriptContext(line, cursor = line.length) {
|
|
257
|
+
const source = line.slice(0, cursor);
|
|
258
|
+
const stack = [];
|
|
259
|
+
for (let i = 0; i < source.length; i++) {
|
|
260
|
+
const ch = source[i];
|
|
261
|
+
const top = stack.at(-1);
|
|
262
|
+
if (top?.kind === "string") {
|
|
263
|
+
if (ch === "\\") { i++; continue; }
|
|
264
|
+
if (top.quote === "`" && ch === "$" && source[i + 1] === "{") {
|
|
265
|
+
stack.push({ kind: "code" });
|
|
266
|
+
i++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (ch === top.quote) stack.pop();
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
273
|
+
stack.push({ kind: "string", quote: ch, start: i + 1 });
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (ch === "}" && top?.kind === "code") stack.pop();
|
|
277
|
+
}
|
|
278
|
+
const top = stack.at(-1);
|
|
279
|
+
// An unclosed string is a path being typed, whatever else is on the line.
|
|
280
|
+
if (top?.kind === "string")
|
|
281
|
+
return { kind: "path", prefix: source.slice(top.start), quote: top.quote, text: source.slice(top.start) };
|
|
282
|
+
// `$` alone is the table object itself, so only the property form
|
|
283
|
+
// completes: a name is being written only after the dot.
|
|
284
|
+
const match = /\$\.([A-Za-z_][A-Za-z0-9_]*)?$/.exec(source);
|
|
285
|
+
if (!match) return null;
|
|
286
|
+
const name = match[1] ?? "";
|
|
287
|
+
return { kind: "variable", prefix: name, text: match[0], lead: "$.", brace: false };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Names come from the shell's own variable table, so completion offers what
|
|
291
|
+
// expansion would actually find — exported or not, and including anything
|
|
292
|
+
// JavaScript mode wrote through `$`.
|
|
293
|
+
export class VariableIndex {
|
|
294
|
+
constructor() {
|
|
295
|
+
this.names = [];
|
|
296
|
+
this.signature = null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
sync(state) {
|
|
300
|
+
const keys = Object.keys(state.env);
|
|
301
|
+
const signature = keys.join("\0");
|
|
302
|
+
if (signature === this.signature) return;
|
|
303
|
+
this.signature = signature;
|
|
304
|
+
this.names = keys.sort();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
matches(state, prefix) {
|
|
308
|
+
this.sync(state);
|
|
309
|
+
return prefix ? prefixMatches(this.names, prefix) : [...this.names];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
first(state, prefix) {
|
|
313
|
+
this.sync(state);
|
|
314
|
+
return prefix ? firstPrefixMatch(this.names, prefix) : null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
202
318
|
export class FileIndex {
|
|
203
319
|
constructor(ttl = 10_000) {
|
|
204
320
|
this.ttl = ttl;
|