@gotgenes/pi-permission-system 4.6.0 → 4.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/CHANGELOG.md +31 -0
- package/README.md +46 -4
- package/config/config.example.json +4 -1
- package/package.json +1 -1
- package/schemas/permissions.schema.json +2 -2
- package/src/bash-arity.ts +185 -0
- package/src/expand-home.ts +28 -0
- package/src/pattern-suggest.ts +14 -9
- package/src/wildcard-matcher.ts +4 -1
- package/tests/bash-arity.test.ts +106 -0
- package/tests/expand-home.test.ts +93 -0
- package/tests/handlers/tool-call.test.ts +2 -1
- package/tests/pattern-suggest.test.ts +28 -9
- package/tests/permission-manager-unified.test.ts +74 -1
- package/tests/wildcard-matcher.test.ts +58 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.8.0](https://github.com/gotgenes/pi-permission-system/compare/v4.7.0...v4.8.0) (2026-05-05)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add expandHomePath utility for ~ and $HOME expansion ([18264e1](https://github.com/gotgenes/pi-permission-system/commit/18264e104f6aa12ca004a127d0f7b09b9e4fb740))
|
|
14
|
+
* expand ~ and $HOME in wildcard patterns at compile time ([3c7e0c2](https://github.com/gotgenes/pi-permission-system/commit/3c7e0c2ab92c1e6bb58fdab32cfd9ae2c72e100a))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* document ~/$HOME pattern expansion in schema, example config, and README ([8ad5190](https://github.com/gotgenes/pi-permission-system/commit/8ad51909512ca294c83876868407866290234882))
|
|
20
|
+
* plan home directory expansion in permission patterns ([#53](https://github.com/gotgenes/pi-permission-system/issues/53)) ([b5b77b6](https://github.com/gotgenes/pi-permission-system/commit/b5b77b640006b67b51420135be8fb78484c9d9a1))
|
|
21
|
+
* **retro:** add retro notes for issue [#52](https://github.com/gotgenes/pi-permission-system/issues/52) ([7fc8113](https://github.com/gotgenes/pi-permission-system/commit/7fc8113390fd1dd9cf09c05e903d597c16d80104))
|
|
22
|
+
* sleep before pulling release commit and tag ([af701b5](https://github.com/gotgenes/pi-permission-system/commit/af701b543b20f274ca9f8aa904af0a39bc232c26))
|
|
23
|
+
|
|
24
|
+
## [4.7.0](https://github.com/gotgenes/pi-permission-system/compare/v4.6.0...v4.7.0) (2026-05-05)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* add bash arity table with prefix lookup ([#52](https://github.com/gotgenes/pi-permission-system/issues/52)) ([56a8e81](https://github.com/gotgenes/pi-permission-system/commit/56a8e81911a5869bceabf9076bca6e1bb709814b))
|
|
30
|
+
* integrate arity table into suggestBashPattern ([#52](https://github.com/gotgenes/pi-permission-system/issues/52)) ([5a3c809](https://github.com/gotgenes/pi-permission-system/commit/5a3c8094319166a8f3bd7c97a68af6b8cd0d0205))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Documentation
|
|
34
|
+
|
|
35
|
+
* document bash arity table ([#52](https://github.com/gotgenes/pi-permission-system/issues/52)) ([376ae5c](https://github.com/gotgenes/pi-permission-system/commit/376ae5cdd9d3a9d28f0e26ec26455f44b45b56d7))
|
|
36
|
+
* plan bash arity table for smart approval patterns ([#52](https://github.com/gotgenes/pi-permission-system/issues/52)) ([6a78244](https://github.com/gotgenes/pi-permission-system/commit/6a78244b7552563e331bb2d426aa0abd35ef9065))
|
|
37
|
+
* **retro:** add retro notes for issue [#60](https://github.com/gotgenes/pi-permission-system/issues/60) ([54ed04a](https://github.com/gotgenes/pi-permission-system/commit/54ed04a85e47a5b1ceb9d496851474856fb0fa17))
|
|
38
|
+
|
|
8
39
|
## [4.6.0](https://github.com/gotgenes/pi-permission-system/compare/v4.5.0...v4.6.0) (2026-05-05)
|
|
9
40
|
|
|
10
41
|
|
package/README.md
CHANGED
|
@@ -156,7 +156,7 @@ The `permission` object maps surface names to actions:
|
|
|
156
156
|
| `bash` | string or object | Bash catch-all or `{ pattern: action }` map |
|
|
157
157
|
| `mcp` | string or object | MCP catch-all or `{ pattern: action }` map |
|
|
158
158
|
| `skill` | string or object | Skill catch-all or `{ pattern: action }` map |
|
|
159
|
-
| `external_directory` | string | Controls access to paths outside `cwd
|
|
159
|
+
| `external_directory` | string or object | Controls access to paths outside `cwd`; supports `~/` and `$HOME/` patterns |
|
|
160
160
|
|
|
161
161
|
> **Note:** Trailing commas are **not** supported. If parsing fails, the extension falls back to `ask` for all categories.
|
|
162
162
|
|
|
@@ -330,14 +330,36 @@ Skill name patterns use `*` wildcards (note: surface is `skill`, not `skills`):
|
|
|
330
330
|
}
|
|
331
331
|
```
|
|
332
332
|
|
|
333
|
+
### Home directory expansion in patterns
|
|
334
|
+
|
|
335
|
+
Pattern keys in any permission surface can start with `~/` or `$HOME/` (or be exactly `~` / `$HOME`).
|
|
336
|
+
They are expanded to the OS home directory at match time, so configs are portable across machines and users.
|
|
337
|
+
|
|
338
|
+
```jsonc
|
|
339
|
+
{
|
|
340
|
+
"permission": {
|
|
341
|
+
"external_directory": {
|
|
342
|
+
"*": "ask",
|
|
343
|
+
"~/development/*": "allow"
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
The pattern is stored and displayed as written (e.g. `~/development/*`) in logs and approval dialogs.
|
|
350
|
+
|
|
333
351
|
### `external_directory` surface
|
|
334
352
|
|
|
335
|
-
Controls access to paths outside the active working directory
|
|
353
|
+
Controls access to paths outside the active working directory.
|
|
354
|
+
Use a pattern map to allow specific directories without opening all external access:
|
|
336
355
|
|
|
337
356
|
```jsonc
|
|
338
357
|
{
|
|
339
358
|
"permission": {
|
|
340
|
-
"external_directory":
|
|
359
|
+
"external_directory": {
|
|
360
|
+
"*": "ask",
|
|
361
|
+
"~/development/*": "allow"
|
|
362
|
+
}
|
|
341
363
|
}
|
|
342
364
|
}
|
|
343
365
|
```
|
|
@@ -450,13 +472,32 @@ The suggested pattern is surface-specific:
|
|
|
450
472
|
|
|
451
473
|
|Surface|Example request|Suggested session pattern|
|
|
452
474
|
|---|---|---|
|
|
453
|
-
|bash|`git status --short`|`git *`|
|
|
475
|
+
|bash|`git status --short`|`git status *`|
|
|
454
476
|
|mcp (qualified)|`exa:search`|`exa:*`|
|
|
455
477
|
|mcp (munged)|`exa_search`|`exa_*`|
|
|
456
478
|
|skill|`librarian`|`librarian`|
|
|
457
479
|
|tool (read, write, …)|`read`|`*`|
|
|
458
480
|
|external_directory|`/other/project/src/foo.ts`|`/other/project/src/*`|
|
|
459
481
|
|
|
482
|
+
#### Bash arity table
|
|
483
|
+
|
|
484
|
+
Bash pattern suggestions use a curated arity dictionary (`src/bash-arity.ts`) to determine how many tokens define the "human-understandable subcommand."
|
|
485
|
+
Longest matching prefix wins, so `npm run` (arity 3) takes precedence over `npm` (arity 2).
|
|
486
|
+
Unknown commands default to arity 1 (first word only).
|
|
487
|
+
|
|
488
|
+
|Example command|Arity entry matched|Suggested pattern|
|
|
489
|
+
|---|---|---|
|
|
490
|
+
|`git checkout main`|`git` → 2|`git checkout *`|
|
|
491
|
+
|`npm run dev`|`npm run` → 3|`npm run dev*`|
|
|
492
|
+
|`npm install lodash`|`npm` → 2|`npm install *`|
|
|
493
|
+
|`docker compose up`|`docker compose` → 3|`docker compose up *`|
|
|
494
|
+
|`rm -rf node_modules`|`rm` → 1|`rm *`|
|
|
495
|
+
|`mytool --verbose`|(unknown) → 1|`mytool *`|
|
|
496
|
+
|
|
497
|
+
The arity table covers common CLI tools including git, npm/pnpm/yarn/bun, docker, cargo, go, kubectl, gh, and others.
|
|
498
|
+
To add an entry, open `src/bash-arity.ts` and add a key/arity pair to the `ARITY` object.
|
|
499
|
+
Put the most specific multi-word prefix first (e.g. `"npm run": 3`) before the shorter fallback (`"npm": 2`).
|
|
500
|
+
|
|
460
501
|
Session approvals are ephemeral — they are never persisted to disk and are cleared on `session_shutdown`.
|
|
461
502
|
The review log records these decisions: `resolution: "approved_for_session"` when the user approves, and `resolution: "session_approved"` when a later request is matched by an existing session rule.
|
|
462
503
|
|
|
@@ -505,6 +546,7 @@ index.ts → Root Pi entrypoint shim
|
|
|
505
546
|
src/
|
|
506
547
|
├── index.ts → Extension bootstrap, permission checks, readable prompts, review logging, reload handling, and subagent forwarding
|
|
507
548
|
├── pattern-suggest.ts → Per-surface session approval pattern suggestions
|
|
549
|
+
├── bash-arity.ts → Curated arity dictionary for smarter bash session-approval patterns
|
|
508
550
|
├── session-rules.ts → Ephemeral session-scoped approval rules (Ruleset-based, wildcard patterns across all surfaces)
|
|
509
551
|
├── config-loader.ts → Unified config loader, merger, and legacy-path detection
|
|
510
552
|
├── config-paths.ts → Path derivation for global, project, and legacy config locations
|
package/package.json
CHANGED
|
@@ -88,10 +88,10 @@
|
|
|
88
88
|
},
|
|
89
89
|
"permissionMap": {
|
|
90
90
|
"description": "A map of wildcard patterns to permission states. Last matching pattern wins.",
|
|
91
|
-
"markdownDescription": "A map of wildcard patterns to permission states.\n\nUse `*` for wildcard matching. When multiple patterns match, the **last matching rule wins** — put broad catch-alls first and specific overrides after them.",
|
|
91
|
+
"markdownDescription": "A map of wildcard patterns to permission states.\n\nUse `*` for wildcard matching. When multiple patterns match, the **last matching rule wins** — put broad catch-alls first and specific overrides after them.\n\nPattern keys support home directory expansion:\n- `~/path` or `$HOME/path` — expanded to the OS home directory at match time.\n- `~` or `$HOME` alone — expands to the home directory itself.\n\nThe stored pattern is always shown in logs and approval dialogs as written (e.g. `~/dev/*`).",
|
|
92
92
|
"type": "object",
|
|
93
93
|
"propertyNames": {
|
|
94
|
-
"description": "A non-empty pattern string. Use * for wildcard matching.",
|
|
94
|
+
"description": "A non-empty pattern string. Use * for wildcard matching. Prefix with ~/ or $HOME/ for home-relative paths.",
|
|
95
95
|
"type": "string",
|
|
96
96
|
"minLength": 1
|
|
97
97
|
},
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated arity dictionary for common CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Keys are lowercase, space-joined command prefixes.
|
|
5
|
+
* Values are the total token count that defines the "human-understandable
|
|
6
|
+
* subcommand" — i.e. how many tokens to include in a session-approval pattern.
|
|
7
|
+
*
|
|
8
|
+
* Multi-level entries (e.g. "npm run": 3) take precedence over shorter entries
|
|
9
|
+
* ("npm": 2) because `prefix()` uses longest-match-wins.
|
|
10
|
+
*
|
|
11
|
+
* Exported for testability.
|
|
12
|
+
*/
|
|
13
|
+
export const ARITY: Record<string, number> = {
|
|
14
|
+
// Version control
|
|
15
|
+
git: 2,
|
|
16
|
+
hg: 2,
|
|
17
|
+
svn: 2,
|
|
18
|
+
|
|
19
|
+
// Node.js package managers
|
|
20
|
+
npm: 2,
|
|
21
|
+
"npm run": 3,
|
|
22
|
+
"npm exec": 3,
|
|
23
|
+
npx: 2,
|
|
24
|
+
pnpm: 2,
|
|
25
|
+
"pnpm run": 3,
|
|
26
|
+
"pnpm exec": 3,
|
|
27
|
+
"pnpm dlx": 3,
|
|
28
|
+
yarn: 2,
|
|
29
|
+
"yarn run": 3,
|
|
30
|
+
bun: 2,
|
|
31
|
+
"bun run": 3,
|
|
32
|
+
"bun add": 2,
|
|
33
|
+
"bun x": 3,
|
|
34
|
+
|
|
35
|
+
// Runtimes
|
|
36
|
+
deno: 2,
|
|
37
|
+
"deno run": 3,
|
|
38
|
+
"deno task": 3,
|
|
39
|
+
"deno compile": 3,
|
|
40
|
+
|
|
41
|
+
// Python
|
|
42
|
+
pip: 2,
|
|
43
|
+
pip3: 2,
|
|
44
|
+
uv: 2,
|
|
45
|
+
"uv run": 3,
|
|
46
|
+
"uv pip": 3,
|
|
47
|
+
|
|
48
|
+
// Rust
|
|
49
|
+
cargo: 2,
|
|
50
|
+
|
|
51
|
+
// Go
|
|
52
|
+
go: 2,
|
|
53
|
+
"go run": 3,
|
|
54
|
+
|
|
55
|
+
// Ruby
|
|
56
|
+
bundle: 2,
|
|
57
|
+
"bundle exec": 3,
|
|
58
|
+
|
|
59
|
+
// Docker / container
|
|
60
|
+
docker: 2,
|
|
61
|
+
"docker compose": 3,
|
|
62
|
+
"docker container": 3,
|
|
63
|
+
"docker image": 3,
|
|
64
|
+
"docker network": 3,
|
|
65
|
+
"docker volume": 3,
|
|
66
|
+
podman: 2,
|
|
67
|
+
"podman compose": 3,
|
|
68
|
+
|
|
69
|
+
// Kubernetes
|
|
70
|
+
kubectl: 2,
|
|
71
|
+
helm: 2,
|
|
72
|
+
|
|
73
|
+
// Cloud CLIs
|
|
74
|
+
aws: 3,
|
|
75
|
+
az: 3,
|
|
76
|
+
gcloud: 3,
|
|
77
|
+
gh: 2,
|
|
78
|
+
"gh pr": 3,
|
|
79
|
+
"gh issue": 3,
|
|
80
|
+
"gh repo": 3,
|
|
81
|
+
fly: 2,
|
|
82
|
+
vercel: 2,
|
|
83
|
+
wrangler: 2,
|
|
84
|
+
|
|
85
|
+
// Build tools
|
|
86
|
+
make: 1,
|
|
87
|
+
bazel: 2,
|
|
88
|
+
|
|
89
|
+
// Infrastructure
|
|
90
|
+
terraform: 2,
|
|
91
|
+
tofu: 2,
|
|
92
|
+
pulumi: 2,
|
|
93
|
+
|
|
94
|
+
// System service management
|
|
95
|
+
systemctl: 2,
|
|
96
|
+
service: 2,
|
|
97
|
+
|
|
98
|
+
// Shell file-ops — args are paths/targets, not subcommands
|
|
99
|
+
ls: 1,
|
|
100
|
+
ll: 1,
|
|
101
|
+
la: 1,
|
|
102
|
+
cat: 1,
|
|
103
|
+
less: 1,
|
|
104
|
+
more: 1,
|
|
105
|
+
head: 1,
|
|
106
|
+
tail: 1,
|
|
107
|
+
grep: 1,
|
|
108
|
+
rg: 1,
|
|
109
|
+
ag: 1,
|
|
110
|
+
find: 1,
|
|
111
|
+
touch: 1,
|
|
112
|
+
mkdir: 1,
|
|
113
|
+
rm: 1,
|
|
114
|
+
cp: 1,
|
|
115
|
+
mv: 1,
|
|
116
|
+
ln: 1,
|
|
117
|
+
chmod: 1,
|
|
118
|
+
chown: 1,
|
|
119
|
+
du: 1,
|
|
120
|
+
df: 1,
|
|
121
|
+
echo: 1,
|
|
122
|
+
printf: 1,
|
|
123
|
+
diff: 1,
|
|
124
|
+
patch: 1,
|
|
125
|
+
wc: 1,
|
|
126
|
+
sort: 1,
|
|
127
|
+
uniq: 1,
|
|
128
|
+
awk: 1,
|
|
129
|
+
sed: 1,
|
|
130
|
+
tar: 1,
|
|
131
|
+
zip: 1,
|
|
132
|
+
unzip: 1,
|
|
133
|
+
|
|
134
|
+
// Network
|
|
135
|
+
curl: 1,
|
|
136
|
+
wget: 1,
|
|
137
|
+
ssh: 1,
|
|
138
|
+
scp: 1,
|
|
139
|
+
rsync: 1,
|
|
140
|
+
ping: 1,
|
|
141
|
+
|
|
142
|
+
// Process management
|
|
143
|
+
kill: 1,
|
|
144
|
+
killall: 1,
|
|
145
|
+
pkill: 1,
|
|
146
|
+
|
|
147
|
+
// Package managers (system)
|
|
148
|
+
brew: 2,
|
|
149
|
+
apt: 2,
|
|
150
|
+
"apt-get": 2,
|
|
151
|
+
yum: 2,
|
|
152
|
+
dnf: 2,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Return the semantically meaningful prefix tokens for a tokenized command.
|
|
157
|
+
*
|
|
158
|
+
* Performs a longest-match-wins lookup against the `ARITY` dictionary:
|
|
159
|
+
* iterates from the longest possible prefix down to a single token, returning
|
|
160
|
+
* the first (longest) match. Lookup is case-insensitive; the returned tokens
|
|
161
|
+
* preserve their original casing.
|
|
162
|
+
*
|
|
163
|
+
* When no entry matches, defaults to arity 1 (first token only).
|
|
164
|
+
* When the resolved arity exceeds the available tokens, it is clamped.
|
|
165
|
+
*
|
|
166
|
+
* @param tokens - The command split by whitespace (e.g. `["git", "checkout", "main"]`).
|
|
167
|
+
* @returns The prefix tokens defining the meaningful subcommand.
|
|
168
|
+
*/
|
|
169
|
+
export function prefix(tokens: string[]): string[] {
|
|
170
|
+
if (tokens.length === 0) return [];
|
|
171
|
+
|
|
172
|
+
for (let n = tokens.length; n >= 1; n--) {
|
|
173
|
+
const key = tokens
|
|
174
|
+
.slice(0, n)
|
|
175
|
+
.map((t) => t.toLowerCase())
|
|
176
|
+
.join(" ");
|
|
177
|
+
const arity = ARITY[key];
|
|
178
|
+
if (arity !== undefined) {
|
|
179
|
+
return tokens.slice(0, Math.min(arity, tokens.length));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Unknown command — default arity 1.
|
|
184
|
+
return [tokens[0]];
|
|
185
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Expand `~` and `$HOME` prefixes in a pattern to the OS home directory.
|
|
6
|
+
*
|
|
7
|
+
* Supported forms:
|
|
8
|
+
* - `~` → `homedir()`
|
|
9
|
+
* - `~/path` → `homedir()/path`
|
|
10
|
+
* - `~\path` → `homedir()\path` (Windows)
|
|
11
|
+
* - `$HOME` → `homedir()`
|
|
12
|
+
* - `$HOME/path` → `homedir()/path`
|
|
13
|
+
* - `$HOME\path` → `homedir()\path` (Windows)
|
|
14
|
+
*
|
|
15
|
+
* All other patterns are returned unchanged.
|
|
16
|
+
*/
|
|
17
|
+
export function expandHomePath(pattern: string): string {
|
|
18
|
+
if (pattern === "~" || pattern === "$HOME") {
|
|
19
|
+
return homedir();
|
|
20
|
+
}
|
|
21
|
+
if (pattern.startsWith("~/") || pattern.startsWith("~\\")) {
|
|
22
|
+
return join(homedir(), pattern.slice(2));
|
|
23
|
+
}
|
|
24
|
+
if (pattern.startsWith("$HOME/") || pattern.startsWith("$HOME\\")) {
|
|
25
|
+
return join(homedir(), pattern.slice(6));
|
|
26
|
+
}
|
|
27
|
+
return pattern;
|
|
28
|
+
}
|
package/src/pattern-suggest.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { prefix } from "./bash-arity";
|
|
1
2
|
import { deriveApprovalPattern } from "./session-rules";
|
|
2
3
|
|
|
3
4
|
/** The suggestion returned for a "Yes, for this session" dialog option. */
|
|
@@ -13,20 +14,24 @@ export interface SessionApprovalSuggestion {
|
|
|
13
14
|
/**
|
|
14
15
|
* Suggest a bash session-approval pattern from a command string.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Single-word commands → exact command (no wildcard).
|
|
17
|
+
* Uses the arity table (`src/bash-arity.ts`) to identify the semantically
|
|
18
|
+
* meaningful prefix tokens for the command, then produces a wildcard pattern:
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* - Single bare token (no args): exact command (`ls`).
|
|
21
|
+
* - Arity prefix covers all tokens: trailing wildcard (`npm run build*`).
|
|
22
|
+
* - Arity prefix shorter than token list: space + wildcard (`git checkout *`).
|
|
23
|
+
* - Unknown command: first token + space wildcard (`mytool *`).
|
|
22
24
|
*/
|
|
23
25
|
export function suggestBashPattern(command: string): string {
|
|
24
26
|
const trimmed = command.trim();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
if (!trimmed) return "";
|
|
28
|
+
const tokens = trimmed.split(/\s+/);
|
|
29
|
+
if (tokens.length === 1) return trimmed;
|
|
30
|
+
const meaningful = prefix(tokens);
|
|
31
|
+
if (meaningful.length >= tokens.length) {
|
|
32
|
+
return `${trimmed}*`;
|
|
28
33
|
}
|
|
29
|
-
return `${
|
|
34
|
+
return `${meaningful.join(" ")} *`;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
/**
|
package/src/wildcard-matcher.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { expandHomePath } from "./expand-home";
|
|
2
|
+
|
|
1
3
|
export type CompiledWildcardPattern<TState> = {
|
|
2
4
|
pattern: string;
|
|
3
5
|
state: TState;
|
|
@@ -18,7 +20,8 @@ export function compileWildcardPattern<TState>(
|
|
|
18
20
|
pattern: string,
|
|
19
21
|
state: TState,
|
|
20
22
|
): CompiledWildcardPattern<TState> {
|
|
21
|
-
const
|
|
23
|
+
const expanded = expandHomePath(pattern);
|
|
24
|
+
const escaped = expanded
|
|
22
25
|
.split("*")
|
|
23
26
|
.map((part) => escapeRegExp(part))
|
|
24
27
|
.join(".*");
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ARITY, prefix } from "../src/bash-arity";
|
|
3
|
+
|
|
4
|
+
describe("ARITY dictionary", () => {
|
|
5
|
+
it("is exported as a plain object", () => {
|
|
6
|
+
expect(typeof ARITY).toBe("object");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("maps 'git' to arity 2", () => {
|
|
10
|
+
expect(ARITY["git"]).toBe(2);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("maps 'npm run' to arity 3", () => {
|
|
14
|
+
expect(ARITY["npm run"]).toBe(3);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("maps 'npm' to arity 2 (fallback when 'npm run' does not match)", () => {
|
|
18
|
+
expect(ARITY["npm"]).toBe(2);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("maps 'docker compose' to arity 3", () => {
|
|
22
|
+
expect(ARITY["docker compose"]).toBe(3);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("maps 'docker' to arity 2 (fallback)", () => {
|
|
26
|
+
expect(ARITY["docker"]).toBe(2);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("prefix", () => {
|
|
31
|
+
it("returns empty array for empty input", () => {
|
|
32
|
+
expect(prefix([])).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns single-element array for a bare known command", () => {
|
|
36
|
+
// 'git' alone has arity 2 but only 1 token is available — clamp.
|
|
37
|
+
expect(prefix(["git"])).toEqual(["git"]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns arity-2 prefix for git subcommands", () => {
|
|
41
|
+
expect(prefix(["git", "checkout", "main"])).toEqual(["git", "checkout"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns arity-2 prefix for git status with flags", () => {
|
|
45
|
+
expect(prefix(["git", "status", "--short"])).toEqual(["git", "status"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns arity-3 prefix for npm run (longest match wins over npm arity-2)", () => {
|
|
49
|
+
expect(prefix(["npm", "run", "dev"])).toEqual(["npm", "run", "dev"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns arity-2 prefix for npm install (npm fallback, npm run does not match)", () => {
|
|
53
|
+
expect(prefix(["npm", "install", "lodash"])).toEqual(["npm", "install"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns arity-3 prefix for docker compose subcommands", () => {
|
|
57
|
+
expect(prefix(["docker", "compose", "up", "--build"])).toEqual([
|
|
58
|
+
"docker",
|
|
59
|
+
"compose",
|
|
60
|
+
"up",
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns arity-2 prefix for docker pull (docker fallback)", () => {
|
|
65
|
+
expect(prefix(["docker", "pull", "ubuntu"])).toEqual(["docker", "pull"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns arity-1 prefix for unknown commands", () => {
|
|
69
|
+
expect(prefix(["unknown-tool", "--flag"])).toEqual(["unknown-tool"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("returns arity-1 prefix for rm (args are targets, not subcommands)", () => {
|
|
73
|
+
expect(prefix(["rm", "-rf", "node_modules"])).toEqual(["rm"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns arity-1 prefix for cat", () => {
|
|
77
|
+
expect(prefix(["cat", "file.txt"])).toEqual(["cat"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("is case-insensitive: 'Git' looks up as 'git'", () => {
|
|
81
|
+
// Tokens are preserved as-is; only the lookup key is lowercased.
|
|
82
|
+
expect(prefix(["Git", "checkout", "main"])).toEqual(["Git", "checkout"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("clamps arity to available token count when command is shorter than arity", () => {
|
|
86
|
+
// npm run has arity 3; only ["npm", "run"] provided → return both.
|
|
87
|
+
expect(prefix(["npm", "run"])).toEqual(["npm", "run"]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns arity-2 prefix for pnpm run (longest match wins over pnpm)", () => {
|
|
91
|
+
// pnpm run <script> — arity 3 means include the script name.
|
|
92
|
+
expect(prefix(["pnpm", "run", "build"])).toEqual(["pnpm", "run", "build"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns arity-2 prefix for cargo subcommands", () => {
|
|
96
|
+
expect(prefix(["cargo", "build", "--release"])).toEqual(["cargo", "build"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns arity-2 prefix for kubectl subcommands", () => {
|
|
100
|
+
expect(prefix(["kubectl", "get", "pods"])).toEqual(["kubectl", "get"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns arity-1 for bare 'ls' (args are paths)", () => {
|
|
104
|
+
expect(prefix(["ls", "-la", "/tmp"])).toEqual(["ls"]);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const mockHomedir = vi.hoisted(() => vi.fn(() => "/home/testuser"));
|
|
5
|
+
|
|
6
|
+
vi.mock("node:os", () => ({
|
|
7
|
+
homedir: mockHomedir,
|
|
8
|
+
default: { homedir: mockHomedir },
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { expandHomePath } from "../src/expand-home";
|
|
12
|
+
|
|
13
|
+
const FAKE_HOME = "/home/testuser";
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
mockHomedir.mockClear();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("expandHomePath", () => {
|
|
20
|
+
describe("~ expansion", () => {
|
|
21
|
+
test("bare ~ expands to homedir()", () => {
|
|
22
|
+
expect(expandHomePath("~")).toBe(FAKE_HOME);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("~/path expands to homedir()/path", () => {
|
|
26
|
+
expect(expandHomePath("~/dev/project")).toBe(
|
|
27
|
+
join(FAKE_HOME, "dev/project"),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("~/path/* expands to homedir()/path/*", () => {
|
|
32
|
+
expect(expandHomePath("~/dev/*")).toBe(join(FAKE_HOME, "dev/*"));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("~\\ (Windows separator) expands to homedir() + rest", () => {
|
|
36
|
+
expect(expandHomePath("~\\dev\\project")).toBe(
|
|
37
|
+
join(FAKE_HOME, "dev\\project"),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("~username (no separator) is not expanded (no-op)", () => {
|
|
42
|
+
expect(expandHomePath("~username")).toBe("~username");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("$HOME expansion", () => {
|
|
47
|
+
test("bare $HOME expands to homedir()", () => {
|
|
48
|
+
expect(expandHomePath("$HOME")).toBe(FAKE_HOME);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("$HOME/path expands to homedir()/path", () => {
|
|
52
|
+
expect(expandHomePath("$HOME/dev/project")).toBe(
|
|
53
|
+
join(FAKE_HOME, "dev/project"),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("$HOME/path/* expands to homedir()/path/*", () => {
|
|
58
|
+
expect(expandHomePath("$HOME/dev/*")).toBe(join(FAKE_HOME, "dev/*"));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("$HOME\\ (Windows separator) expands to homedir() + rest", () => {
|
|
62
|
+
expect(expandHomePath("$HOME\\dev\\project")).toBe(
|
|
63
|
+
join(FAKE_HOME, "dev\\project"),
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("$HOMEDIR (no separator) is not expanded (no-op)", () => {
|
|
68
|
+
expect(expandHomePath("$HOMEDIR")).toBe("$HOMEDIR");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("no-op patterns", () => {
|
|
73
|
+
test("absolute path is unchanged", () => {
|
|
74
|
+
expect(expandHomePath("/usr/local/bin")).toBe("/usr/local/bin");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("relative path is unchanged", () => {
|
|
78
|
+
expect(expandHomePath("dev/project")).toBe("dev/project");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("glob-only pattern is unchanged", () => {
|
|
82
|
+
expect(expandHomePath("*")).toBe("*");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("empty string is unchanged", () => {
|
|
86
|
+
expect(expandHomePath("")).toBe("");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("bash command pattern starting with a word is unchanged", () => {
|
|
90
|
+
expect(expandHomePath("git push *")).toBe("git push *");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -568,7 +568,8 @@ describe("handleToolCall — session recording on approved_for_session", () => {
|
|
|
568
568
|
input: { command: "git status" },
|
|
569
569
|
});
|
|
570
570
|
await handleToolCall(deps, event, makeCtx());
|
|
571
|
-
|
|
571
|
+
// git arity=2: "git status" prefix covers all 2 tokens → trailing wildcard.
|
|
572
|
+
expect(sessionRules.approve).toHaveBeenCalledWith("bash", "git status*");
|
|
572
573
|
});
|
|
573
574
|
|
|
574
575
|
it("records mcp session approval with suggestMcpPattern result", async () => {
|
|
@@ -6,25 +6,42 @@ import {
|
|
|
6
6
|
} from "../src/pattern-suggest";
|
|
7
7
|
|
|
8
8
|
describe("suggestBashPattern", () => {
|
|
9
|
-
it("returns <command> *
|
|
10
|
-
|
|
9
|
+
it("returns <command> <subcommand> * using the arity table", () => {
|
|
10
|
+
// git arity=2: include the subcommand in the prefix.
|
|
11
|
+
expect(suggestBashPattern("git status --short")).toBe("git status *");
|
|
11
12
|
});
|
|
12
13
|
|
|
13
|
-
it("
|
|
14
|
-
|
|
14
|
+
it("appends trailing * when arity covers all tokens (multi-word script name)", () => {
|
|
15
|
+
// npm run arity=3: prefix covers all three tokens → trailing wildcard.
|
|
16
|
+
expect(suggestBashPattern("npm run build")).toBe("npm run build*");
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
it("returns the exact command when there are no arguments", () => {
|
|
18
20
|
expect(suggestBashPattern("ls")).toBe("ls");
|
|
19
21
|
});
|
|
20
22
|
|
|
21
|
-
it("trims leading and trailing whitespace", () => {
|
|
22
|
-
|
|
23
|
+
it("trims leading and trailing whitespace before lookup", () => {
|
|
24
|
+
// git arity=2, tokens=["git","log"], prefix covers all → trailing wildcard.
|
|
25
|
+
expect(suggestBashPattern(" git log ")).toBe("git log*");
|
|
23
26
|
});
|
|
24
27
|
|
|
25
28
|
it("handles empty string gracefully", () => {
|
|
26
29
|
expect(suggestBashPattern("")).toBe("");
|
|
27
30
|
});
|
|
31
|
+
|
|
32
|
+
it("falls back to first-word prefix for unknown commands", () => {
|
|
33
|
+
expect(suggestBashPattern("mytool --verbose run")).toBe("mytool *");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns first-word * for known arity-1 commands with args", () => {
|
|
37
|
+
expect(suggestBashPattern("rm -rf node_modules")).toBe("rm *");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("produces tighter pattern for docker compose than plain docker", () => {
|
|
41
|
+
expect(suggestBashPattern("docker compose up --build")).toBe(
|
|
42
|
+
"docker compose up *",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
28
45
|
});
|
|
29
46
|
|
|
30
47
|
describe("suggestMcpPattern", () => {
|
|
@@ -52,11 +69,12 @@ describe("suggestMcpPattern", () => {
|
|
|
52
69
|
|
|
53
70
|
describe("suggestSessionPattern", () => {
|
|
54
71
|
describe("bash surface", () => {
|
|
55
|
-
it("returns
|
|
72
|
+
it("returns arity-aware subcommand pattern for multi-word command", () => {
|
|
73
|
+
// git arity=2: include the subcommand token in the prefix.
|
|
56
74
|
const result = suggestSessionPattern("bash", "git status --short");
|
|
57
75
|
expect(result).toMatchObject({
|
|
58
76
|
surface: "bash",
|
|
59
|
-
pattern: "git *",
|
|
77
|
+
pattern: "git status *",
|
|
60
78
|
});
|
|
61
79
|
});
|
|
62
80
|
|
|
@@ -122,8 +140,9 @@ describe("suggestSessionPattern", () => {
|
|
|
122
140
|
|
|
123
141
|
describe("label field", () => {
|
|
124
142
|
it("includes the suggested pattern in the label", () => {
|
|
143
|
+
// git arity=2, "git status" has 2 tokens → trailing wildcard.
|
|
125
144
|
const result = suggestSessionPattern("bash", "git status");
|
|
126
|
-
expect(result.label).toContain("git *");
|
|
145
|
+
expect(result.label).toContain("git status*");
|
|
127
146
|
});
|
|
128
147
|
|
|
129
148
|
it("wraps the pattern in quotes in the label", () => {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Step 6: all five surfaces produce identical decisions to the old branching code.
|
|
6
6
|
*/
|
|
7
7
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { tmpdir } from "node:os";
|
|
8
|
+
import { homedir, tmpdir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { describe, expect, it } from "vitest";
|
|
11
11
|
import { PermissionManager } from "../src/permission-manager";
|
|
@@ -373,3 +373,76 @@ describe("checkPermission — source derivation and matchedPattern", () => {
|
|
|
373
373
|
});
|
|
374
374
|
});
|
|
375
375
|
});
|
|
376
|
+
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
// Home directory expansion in external_directory patterns
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
describe("checkPermission — home path expansion in external_directory rules", () => {
|
|
382
|
+
it("~/glob pattern allows a path under the real home directory", () => {
|
|
383
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
384
|
+
"*": "ask",
|
|
385
|
+
external_directory: { "~/trusted/*": "allow" },
|
|
386
|
+
});
|
|
387
|
+
try {
|
|
388
|
+
const result = manager.checkPermission("external_directory", {
|
|
389
|
+
path: join(homedir(), "trusted/repo"),
|
|
390
|
+
});
|
|
391
|
+
expect(result.state).toBe("allow");
|
|
392
|
+
expect(result.source).toBe("special");
|
|
393
|
+
expect(result.matchedPattern).toBe("~/trusted/*");
|
|
394
|
+
} finally {
|
|
395
|
+
cleanup();
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("$HOME/glob pattern allows a path under the real home directory", () => {
|
|
400
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
401
|
+
"*": "ask",
|
|
402
|
+
external_directory: { "$HOME/trusted/*": "allow" },
|
|
403
|
+
});
|
|
404
|
+
try {
|
|
405
|
+
const result = manager.checkPermission("external_directory", {
|
|
406
|
+
path: join(homedir(), "trusted/repo"),
|
|
407
|
+
});
|
|
408
|
+
expect(result.state).toBe("allow");
|
|
409
|
+
expect(result.source).toBe("special");
|
|
410
|
+
expect(result.matchedPattern).toBe("$HOME/trusted/*");
|
|
411
|
+
} finally {
|
|
412
|
+
cleanup();
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("~/glob deny rule blocks a path under home", () => {
|
|
417
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
418
|
+
"*": "allow",
|
|
419
|
+
external_directory: { "~/private/*": "deny" },
|
|
420
|
+
});
|
|
421
|
+
try {
|
|
422
|
+
const result = manager.checkPermission("external_directory", {
|
|
423
|
+
path: join(homedir(), "private/secrets.txt"),
|
|
424
|
+
});
|
|
425
|
+
expect(result.state).toBe("deny");
|
|
426
|
+
expect(result.matchedPattern).toBe("~/private/*");
|
|
427
|
+
} finally {
|
|
428
|
+
cleanup();
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("~/glob pattern does not match a path outside home", () => {
|
|
433
|
+
const { manager, cleanup } = makeManagerWithConfig({
|
|
434
|
+
"*": "ask",
|
|
435
|
+
external_directory: { "~/trusted/*": "allow" },
|
|
436
|
+
});
|
|
437
|
+
try {
|
|
438
|
+
const result = manager.checkPermission("external_directory", {
|
|
439
|
+
path: "/tmp/not-home/file",
|
|
440
|
+
});
|
|
441
|
+
// Falls back to the "*": "ask" default — no allow from the ~/trusted/* rule.
|
|
442
|
+
expect(result.state).toBe("ask");
|
|
443
|
+
expect(result.matchedPattern).toBeUndefined();
|
|
444
|
+
} finally {
|
|
445
|
+
cleanup();
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
});
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
3
|
|
|
4
|
+
const mockHomedir = vi.hoisted(() => vi.fn(() => "/home/testuser"));
|
|
5
|
+
|
|
6
|
+
vi.mock("node:os", () => ({
|
|
7
|
+
homedir: mockHomedir,
|
|
8
|
+
default: { homedir: mockHomedir },
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const FAKE_HOME = "/home/testuser";
|
|
12
|
+
|
|
3
13
|
import {
|
|
4
14
|
compileWildcardPattern,
|
|
5
15
|
compileWildcardPatternEntries,
|
|
@@ -9,6 +19,7 @@ import {
|
|
|
9
19
|
} from "../src/wildcard-matcher";
|
|
10
20
|
|
|
11
21
|
afterEach(() => {
|
|
22
|
+
mockHomedir.mockClear();
|
|
12
23
|
vi.restoreAllMocks();
|
|
13
24
|
});
|
|
14
25
|
|
|
@@ -233,3 +244,50 @@ describe("wildcardMatch", () => {
|
|
|
233
244
|
expect(wildcardMatch("tool.name", "toolXname")).toBe(false);
|
|
234
245
|
});
|
|
235
246
|
});
|
|
247
|
+
|
|
248
|
+
describe("home path expansion in patterns", () => {
|
|
249
|
+
test("wildcardMatch expands ~ prefix in pattern before matching", () => {
|
|
250
|
+
const expandedPath = join(FAKE_HOME, "dev/project");
|
|
251
|
+
expect(wildcardMatch("~/dev/project", expandedPath)).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("wildcardMatch expands ~/glob in pattern", () => {
|
|
255
|
+
const expandedFile = join(FAKE_HOME, "dev/project/file.ts");
|
|
256
|
+
expect(wildcardMatch("~/dev/*", expandedFile)).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("wildcardMatch ~/glob does not match a different home directory", () => {
|
|
260
|
+
expect(wildcardMatch("~/dev/*", "/other/user/dev/file.ts")).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("wildcardMatch expands $HOME prefix in pattern before matching", () => {
|
|
264
|
+
const expandedPath = join(FAKE_HOME, "dev/project");
|
|
265
|
+
expect(wildcardMatch("$HOME/dev/project", expandedPath)).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("wildcardMatch expands $HOME/glob in pattern", () => {
|
|
269
|
+
const expandedFile = join(FAKE_HOME, "work/file.ts");
|
|
270
|
+
expect(wildcardMatch("$HOME/work/*", expandedFile)).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("compileWildcardPattern retains original ~ pattern in .pattern field", () => {
|
|
274
|
+
const compiled = compileWildcardPattern("~/dev/*", "allow");
|
|
275
|
+
expect(compiled.pattern).toBe("~/dev/*");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("compileWildcardPattern retains original $HOME pattern in .pattern field", () => {
|
|
279
|
+
const compiled = compileWildcardPattern("$HOME/dev/*", "allow");
|
|
280
|
+
expect(compiled.pattern).toBe("$HOME/dev/*");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("compileWildcardPattern expanded regex matches the expanded path", () => {
|
|
284
|
+
const compiled = compileWildcardPattern("~/dev/*", "allow");
|
|
285
|
+
const expandedFile = join(FAKE_HOME, "dev/file.ts");
|
|
286
|
+
expect(compiled.regex.test(expandedFile)).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("non-home pattern is unaffected", () => {
|
|
290
|
+
expect(wildcardMatch("/absolute/path/*", "/absolute/path/file")).toBe(true);
|
|
291
|
+
expect(wildcardMatch("/absolute/path/*", "/other/file")).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
});
|