pi-pkg-guard 0.1.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/LICENSE +21 -0
- package/README.md +158 -0
- package/biome.json +29 -0
- package/extensions/index.ts +269 -0
- package/justfile +57 -0
- package/package.json +55 -0
- package/test/analysis.test.ts +318 -0
- package/test/guards.test.ts +186 -0
- package/test/regex.test.ts +267 -0
- package/tsconfig.json +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Lee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# pi-pkg-guard
|
|
2
|
+
|
|
3
|
+
A lightweight pi extension that guards against the "orphaned package" trap where packages are installed via npm but not registered in pi's `settings.json`.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
When you install pi extensions via npm directly, they become "orphaned" - installed on your system but unknown to pi:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g pi-token-burden # Installs to npm global
|
|
11
|
+
# But pi doesn't know about it! Must also add to settings.json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This extension provides **passive guardrails** to prevent and fix this issue.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
| Feature | Behavior |
|
|
19
|
+
|---------|----------|
|
|
20
|
+
| **Startup Check** | Status line warning if orphaned packages detected (max once/hour) |
|
|
21
|
+
| **`/pi-pkg-guard`** | One-liner to auto-register orphaned packages |
|
|
22
|
+
| **npm Guard** | Warns when bash tool runs `npm install -g pi-*` |
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### Via pi (Recommended)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pi install npm:pi-pkg-guard
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Via npm
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g pi-pkg-guard
|
|
36
|
+
# Then add to ~/.pi/agent/settings.json:
|
|
37
|
+
# {
|
|
38
|
+
# "packages": ["npm:pi-pkg-guard"]
|
|
39
|
+
# }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Manual (Development)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Clone the repository
|
|
46
|
+
git clone https://github.com/alexleekt/pi-pkg-guard.git
|
|
47
|
+
|
|
48
|
+
# Symlink to pi extensions directory
|
|
49
|
+
ln -s $(pwd)/pi-pkg-guard ~/.pi/agent/extensions/pi-pkg-guard
|
|
50
|
+
|
|
51
|
+
# Or add to settings.json
|
|
52
|
+
# {
|
|
53
|
+
# "extensions": ["/path/to/pi-pkg-guard"]
|
|
54
|
+
# }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then `/reload` in pi.
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
### Check for Orphaned Packages
|
|
62
|
+
|
|
63
|
+
Run the slash command to check and fix:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
/pi-pkg-guard
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Outputs:**
|
|
70
|
+
- `✅ All pi packages registered` - No orphaned packages found
|
|
71
|
+
- `✅ Registered N package(s). Run /reload.` - Fixed! Run `/reload` to activate
|
|
72
|
+
|
|
73
|
+
### Automatic Startup Check
|
|
74
|
+
|
|
75
|
+
On pi startup, the extension checks for orphaned packages (max once per hour). If found, you'll see:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
⚠️ 3 orphaned pi package(s). Run /pi-pkg-guard
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### npm Install Guard
|
|
82
|
+
|
|
83
|
+
When a tool attempts `npm install -g pi-*`, you'll see:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
⚠️ Use 'pi install npm:pi-foo' instead of 'npm install -g'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## How It Works
|
|
90
|
+
|
|
91
|
+
1. **Detects** packages starting with `pi-` or scoped packages containing `/pi-`
|
|
92
|
+
2. **Compares** npm global packages against `settings.json` registered packages
|
|
93
|
+
3. **Normalizes** both `pi-foo` and `npm:pi-foo` formats
|
|
94
|
+
4. **Excludes** the core package `@mariozechner/pi-coding-agent`
|
|
95
|
+
|
|
96
|
+
## Compatibility
|
|
97
|
+
|
|
98
|
+
- **Node.js**: >= 18.0.0
|
|
99
|
+
- **pi**: Works with all versions supporting `ExtensionAPI`
|
|
100
|
+
|
|
101
|
+
## Related Extensions
|
|
102
|
+
|
|
103
|
+
- **[pi-extmgr](https://pi.dev/packages/pi-extmgr)** (`/extensions`) - Full package management UI. This extension provides passive guardrails that complement pi-extmgr.
|
|
104
|
+
- **[pi-extension-manager](https://pi.dev/packages/pi-extension-manager)** - Interactive extension manager
|
|
105
|
+
|
|
106
|
+
## Why Not Just Use pi-extmgr?
|
|
107
|
+
|
|
108
|
+
`pi-extmgr` provides a full UI for managing extensions. `pi-pkg-guard` provides **passive guardrails** - it watches and warns automatically without requiring you to open a UI. Use both together:
|
|
109
|
+
|
|
110
|
+
- `pi-pkg-guard` for automatic detection and warnings
|
|
111
|
+
- `pi-extmgr` for interactive browsing and management
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Clone
|
|
117
|
+
git clone https://github.com/alexleekt/pi-pkg-guard.git
|
|
118
|
+
cd pi-pkg-guard
|
|
119
|
+
|
|
120
|
+
# Install dependencies
|
|
121
|
+
npm install
|
|
122
|
+
|
|
123
|
+
# Run tests
|
|
124
|
+
just test
|
|
125
|
+
|
|
126
|
+
# Run all checks
|
|
127
|
+
just check
|
|
128
|
+
|
|
129
|
+
# Link for development
|
|
130
|
+
ln -s $(pwd) ~/.pi/agent/extensions/pi-pkg-guard
|
|
131
|
+
|
|
132
|
+
# Test in pi
|
|
133
|
+
pi
|
|
134
|
+
# Then /reload
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
See all available tasks: `just --list`
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Clone
|
|
141
|
+
git clone https://github.com/alexleekt/pi-pkg-guard.git
|
|
142
|
+
cd pi-pkg-guard
|
|
143
|
+
|
|
144
|
+
# Link for development
|
|
145
|
+
ln -s $(pwd) ~/.pi/agent/extensions/pi-pkg-guard
|
|
146
|
+
|
|
147
|
+
# Test in pi
|
|
148
|
+
pi
|
|
149
|
+
# Then /reload
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT © Alex Lee
|
|
155
|
+
|
|
156
|
+
## Contributing
|
|
157
|
+
|
|
158
|
+
Issues and PRs welcome at [github.com/alexleekt/pi-pkg-guard](https://github.com/alexleekt/pi-pkg-guard)
|
package/biome.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.4.11/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false
|
|
10
|
+
},
|
|
11
|
+
"organizeImports": {
|
|
12
|
+
"enabled": true
|
|
13
|
+
},
|
|
14
|
+
"formatter": {
|
|
15
|
+
"enabled": true,
|
|
16
|
+
"indentStyle": "tab"
|
|
17
|
+
},
|
|
18
|
+
"linter": {
|
|
19
|
+
"enabled": true,
|
|
20
|
+
"rules": {
|
|
21
|
+
"recommended": true
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"javascript": {
|
|
25
|
+
"formatter": {
|
|
26
|
+
"quoteStyle": "double"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// Constants
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
const SETTINGS_PATH = `${homedir()}/.pi/agent/settings.json`;
|
|
11
|
+
const NPM_PREFIX = "npm:";
|
|
12
|
+
const STATUS_KEY = "ext:pi-pkg-guard:v1";
|
|
13
|
+
const CORE_PACKAGE = "@mariozechner/pi-coding-agent";
|
|
14
|
+
const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
15
|
+
const STATUS_CLEAR_DELAY_MS = 3000;
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
interface PackageDiff {
|
|
22
|
+
orphaned: string[]; // In npm global, not in settings
|
|
23
|
+
hasOrphans: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface PiSettings {
|
|
27
|
+
packages?: string[];
|
|
28
|
+
extensions?: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Type Guards
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
function isPiSettings(value: unknown): value is PiSettings {
|
|
36
|
+
return typeof value === "object" && value !== null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isBashToolInput(input: unknown): input is { command?: string } {
|
|
40
|
+
return typeof input === "object" && input !== null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// NPM Operations
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get list of pi-* packages installed globally via npm.
|
|
49
|
+
* Returns empty array on error (non-critical operation).
|
|
50
|
+
*/
|
|
51
|
+
function getNpmGlobalPackages(): string[] {
|
|
52
|
+
try {
|
|
53
|
+
const output = execSync("npm list -g --json --depth=0", {
|
|
54
|
+
encoding: "utf-8",
|
|
55
|
+
timeout: 5000,
|
|
56
|
+
stdio: ["pipe", "pipe", "ignore"], // Ignore stderr
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const parsed = JSON.parse(output) as {
|
|
60
|
+
dependencies?: Record<string, unknown>;
|
|
61
|
+
};
|
|
62
|
+
const deps = parsed.dependencies || {};
|
|
63
|
+
|
|
64
|
+
return Object.keys(deps).filter(
|
|
65
|
+
(name) =>
|
|
66
|
+
(name.startsWith("pi-") || name.includes("/pi-")) &&
|
|
67
|
+
name !== CORE_PACKAGE,
|
|
68
|
+
);
|
|
69
|
+
} catch {
|
|
70
|
+
// Silently fail - this is a non-critical operation
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// Settings Operations
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read and parse pi settings.json.
|
|
81
|
+
* Returns empty settings on error (non-critical operation).
|
|
82
|
+
*/
|
|
83
|
+
function readPiSettings(): PiSettings {
|
|
84
|
+
try {
|
|
85
|
+
const content = readFileSync(SETTINGS_PATH, "utf-8");
|
|
86
|
+
const parsed = JSON.parse(content) as unknown;
|
|
87
|
+
|
|
88
|
+
if (!isPiSettings(parsed)) {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return parsed;
|
|
93
|
+
} catch {
|
|
94
|
+
// File doesn't exist or is invalid - return empty settings
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get list of packages registered in pi's settings.json.
|
|
101
|
+
* Normalizes both "npm:pi-foo" and "pi-foo" formats to "pi-foo".
|
|
102
|
+
*/
|
|
103
|
+
function getRegisteredPackages(): string[] {
|
|
104
|
+
const settings = readPiSettings();
|
|
105
|
+
const packages = settings.packages || [];
|
|
106
|
+
|
|
107
|
+
return packages.map((pkg: string) => {
|
|
108
|
+
// Handle both "npm:pi-foo" and "pi-foo" formats
|
|
109
|
+
return pkg.startsWith(NPM_PREFIX) ? pkg.slice(NPM_PREFIX.length) : pkg;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Write updated settings back to settings.json.
|
|
115
|
+
* Silently fails on error (non-critical operation).
|
|
116
|
+
*/
|
|
117
|
+
function writePiSettings(settings: PiSettings): void {
|
|
118
|
+
try {
|
|
119
|
+
writeFileSync(SETTINGS_PATH, `${JSON.stringify(settings, null, 2)}\n`);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// Log but don't throw - this is a non-critical operation
|
|
122
|
+
console.error("[pi-pkg-guard] Failed to write settings:", error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// Package Analysis
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compare npm global packages against registered packages.
|
|
132
|
+
* Returns diff showing orphaned packages (installed but not registered).
|
|
133
|
+
*/
|
|
134
|
+
function analyzePackages(): PackageDiff {
|
|
135
|
+
const npmPackages = new Set(getNpmGlobalPackages());
|
|
136
|
+
const registeredPackages = new Set(getRegisteredPackages());
|
|
137
|
+
|
|
138
|
+
const orphaned = [...npmPackages].filter(
|
|
139
|
+
(pkg) => !registeredPackages.has(pkg),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
orphaned,
|
|
144
|
+
hasOrphans: orphaned.length > 0,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sync orphaned packages to settings.json.
|
|
150
|
+
* Adds npm: prefix to each orphaned package.
|
|
151
|
+
*/
|
|
152
|
+
function syncOrphanedPackages(diff: PackageDiff): void {
|
|
153
|
+
if (diff.orphaned.length === 0) return;
|
|
154
|
+
|
|
155
|
+
const settings = readPiSettings();
|
|
156
|
+
settings.packages = settings.packages || [];
|
|
157
|
+
|
|
158
|
+
for (const pkg of diff.orphaned) {
|
|
159
|
+
const npmRef = `${NPM_PREFIX}${pkg}`;
|
|
160
|
+
if (!settings.packages.includes(npmRef)) {
|
|
161
|
+
settings.packages.push(npmRef);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
writePiSettings(settings);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// npm Install Guard
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
// Detect npm install with -g or --global and a pi-* package
|
|
173
|
+
const NPM_GLOBAL_PATTERN = /npm\s+(install|i)\s+.*(-g|--global)/;
|
|
174
|
+
const PI_PACKAGE_PATTERN = /pi-[\w-]+/;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if a bash command is a global npm install of a pi package.
|
|
178
|
+
*/
|
|
179
|
+
function isGlobalPiInstall(command: string): {
|
|
180
|
+
isMatch: boolean;
|
|
181
|
+
packageName?: string;
|
|
182
|
+
} {
|
|
183
|
+
if (!NPM_GLOBAL_PATTERN.test(command)) {
|
|
184
|
+
return { isMatch: false };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const match = command.match(PI_PACKAGE_PATTERN);
|
|
188
|
+
if (!match) {
|
|
189
|
+
return { isMatch: false };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { isMatch: true, packageName: match[0] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// =============================================================================
|
|
196
|
+
// Extension Entry Point
|
|
197
|
+
// =============================================================================
|
|
198
|
+
|
|
199
|
+
export default function (pi: ExtensionAPI) {
|
|
200
|
+
let lastCheckTime = 0;
|
|
201
|
+
|
|
202
|
+
// ===========================================================================
|
|
203
|
+
// Startup Guard: Check for orphaned packages (debounced to once/hour)
|
|
204
|
+
// ===========================================================================
|
|
205
|
+
|
|
206
|
+
pi.on("session_start", async (event, ctx) => {
|
|
207
|
+
if (event.reason !== "startup") return;
|
|
208
|
+
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
if (now - lastCheckTime < CHECK_INTERVAL_MS) return;
|
|
211
|
+
lastCheckTime = now;
|
|
212
|
+
|
|
213
|
+
const diff = analyzePackages();
|
|
214
|
+
|
|
215
|
+
if (diff.hasOrphans) {
|
|
216
|
+
ctx.ui.setStatus(
|
|
217
|
+
STATUS_KEY,
|
|
218
|
+
`⚠️ ${diff.orphaned.length} orphaned pi package(s). Run /pi-pkg-guard`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ===========================================================================
|
|
224
|
+
// Sync Command: Register orphaned packages
|
|
225
|
+
// ===========================================================================
|
|
226
|
+
|
|
227
|
+
pi.registerCommand("pi-pkg-guard", {
|
|
228
|
+
description: "Register orphaned pi packages in settings.json",
|
|
229
|
+
handler: async (_args, ctx) => {
|
|
230
|
+
const diff = analyzePackages();
|
|
231
|
+
|
|
232
|
+
if (!diff.hasOrphans) {
|
|
233
|
+
ctx.ui.setStatus(STATUS_KEY, "✅ All pi packages registered");
|
|
234
|
+
setTimeout(
|
|
235
|
+
() => ctx.ui.setStatus(STATUS_KEY, ""),
|
|
236
|
+
STATUS_CLEAR_DELAY_MS,
|
|
237
|
+
);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
syncOrphanedPackages(diff);
|
|
242
|
+
ctx.ui.setStatus(
|
|
243
|
+
STATUS_KEY,
|
|
244
|
+
`✅ Registered ${diff.orphaned.length} package(s). Run /reload.`,
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ===========================================================================
|
|
250
|
+
// npm Guard: Warn on direct npm install of pi packages
|
|
251
|
+
// ===========================================================================
|
|
252
|
+
|
|
253
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
254
|
+
if (event.toolName !== "bash") return;
|
|
255
|
+
|
|
256
|
+
const input = event.input;
|
|
257
|
+
if (!isBashToolInput(input)) return;
|
|
258
|
+
|
|
259
|
+
const command = input.command || "";
|
|
260
|
+
const { isMatch, packageName } = isGlobalPiInstall(command);
|
|
261
|
+
|
|
262
|
+
if (isMatch && packageName) {
|
|
263
|
+
ctx.ui.notify(
|
|
264
|
+
`⚠️ Use 'pi install npm:${packageName}' instead of 'npm install -g'`,
|
|
265
|
+
"warning",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
package/justfile
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# pi-pkg-guard task runner
|
|
2
|
+
# https://github.com/casey/just
|
|
3
|
+
|
|
4
|
+
# Default recipe - show available tasks
|
|
5
|
+
default:
|
|
6
|
+
@just --list
|
|
7
|
+
|
|
8
|
+
# Run all checks (format, lint, test, typecheck)
|
|
9
|
+
check: format lint test typecheck
|
|
10
|
+
@echo "✅ All checks passed!"
|
|
11
|
+
|
|
12
|
+
# Check code with biome (no fixes)
|
|
13
|
+
check-only:
|
|
14
|
+
npx @biomejs/biome check .
|
|
15
|
+
|
|
16
|
+
# Fix all auto-fixable biome issues
|
|
17
|
+
fix:
|
|
18
|
+
npx @biomejs/biome check --write .
|
|
19
|
+
|
|
20
|
+
# Format code with biome
|
|
21
|
+
format:
|
|
22
|
+
npx @biomejs/biome format --write .
|
|
23
|
+
|
|
24
|
+
# Lint code with biome
|
|
25
|
+
lint:
|
|
26
|
+
npx @biomejs/biome lint .
|
|
27
|
+
|
|
28
|
+
# Run all tests
|
|
29
|
+
test:
|
|
30
|
+
node --test test/**/*.test.ts
|
|
31
|
+
|
|
32
|
+
# Run tests in watch mode
|
|
33
|
+
test-watch:
|
|
34
|
+
node --test --watch test/**/*.test.ts
|
|
35
|
+
|
|
36
|
+
# TypeScript type checking
|
|
37
|
+
typecheck:
|
|
38
|
+
npx tsc --noEmit --skipLibCheck
|
|
39
|
+
|
|
40
|
+
# Clean build artifacts and dependencies
|
|
41
|
+
clean:
|
|
42
|
+
rm -rf node_modules dist coverage *.log
|
|
43
|
+
|
|
44
|
+
# Install dependencies
|
|
45
|
+
install:
|
|
46
|
+
npm install
|
|
47
|
+
|
|
48
|
+
# Dry run npm publish
|
|
49
|
+
dry-run:
|
|
50
|
+
npm publish --dry-run
|
|
51
|
+
|
|
52
|
+
# Publish to npm (requires auth)
|
|
53
|
+
publish:
|
|
54
|
+
npm publish --access public
|
|
55
|
+
|
|
56
|
+
# CI recipe - runs in CI/CD pipelines (no watch mode)
|
|
57
|
+
ci: check-only test typecheck
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-pkg-guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight pi extension that guards against the 'orphaned package' trap where packages are installed via npm but not registered in pi's settings.json",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Alex Lee <657215+alexleekt@users.noreply.github.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/alexleekt/pi-pkg-guard.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/alexleekt/pi-pkg-guard#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/alexleekt/pi-pkg-guard/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi-package",
|
|
18
|
+
"pi-extension",
|
|
19
|
+
"pi-coding-agent",
|
|
20
|
+
"package-manager",
|
|
21
|
+
"guardrails",
|
|
22
|
+
"npm",
|
|
23
|
+
"extensions"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"extensions/",
|
|
27
|
+
"test/",
|
|
28
|
+
"justfile",
|
|
29
|
+
"biome.json",
|
|
30
|
+
"tsconfig.json",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "node --test test/**/*.test.ts"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "^1.9.0",
|
|
39
|
+
"@types/node": "^20.0.0",
|
|
40
|
+
"typescript": "^5.0.0"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"pi": {
|
|
46
|
+
"extensions": ["./extensions/index.ts"]
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
50
|
+
"@mariozechner/pi-tui": "*"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// Test cases for package analysis logic
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
|
|
6
|
+
// Mock types and functions
|
|
7
|
+
interface PackageDiff {
|
|
8
|
+
orphaned: string[];
|
|
9
|
+
hasOrphans: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Simulated functions from extensions/index.ts
|
|
13
|
+
function analyzePackages(
|
|
14
|
+
npmPackages: string[],
|
|
15
|
+
registeredPackages: string[],
|
|
16
|
+
): PackageDiff {
|
|
17
|
+
const npmSet = new Set(npmPackages);
|
|
18
|
+
const registeredSet = new Set(registeredPackages);
|
|
19
|
+
|
|
20
|
+
const orphaned = [...npmSet].filter((pkg) => !registeredSet.has(pkg));
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
orphaned,
|
|
24
|
+
hasOrphans: orphaned.length > 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function syncOrphanedPackages(
|
|
29
|
+
orphaned: string[],
|
|
30
|
+
existingPackages: string[],
|
|
31
|
+
): string[] {
|
|
32
|
+
const NPM_PREFIX = "npm:";
|
|
33
|
+
const result = [...existingPackages];
|
|
34
|
+
|
|
35
|
+
for (const pkg of orphaned) {
|
|
36
|
+
const npmRef = `${NPM_PREFIX}${pkg}`;
|
|
37
|
+
if (!result.includes(npmRef) && !result.includes(pkg)) {
|
|
38
|
+
result.push(npmRef);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Helper to normalize package names
|
|
46
|
+
function normalizePackageName(pkg: string): string {
|
|
47
|
+
const NPM_PREFIX = "npm:";
|
|
48
|
+
return pkg.startsWith(NPM_PREFIX) ? pkg.slice(NPM_PREFIX.length) : pkg;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("analyzePackages - GOOD CASES", () => {
|
|
52
|
+
it("should return no orphans when all packages are registered", () => {
|
|
53
|
+
const npm = ["pi-foo", "pi-bar"];
|
|
54
|
+
const registered = ["pi-foo", "pi-bar"];
|
|
55
|
+
const result = analyzePackages(npm, registered);
|
|
56
|
+
|
|
57
|
+
assert.strictEqual(result.hasOrphans, false);
|
|
58
|
+
assert.deepStrictEqual(result.orphaned, []);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should detect orphaned packages not in registered", () => {
|
|
62
|
+
const npm = ["pi-foo", "pi-bar", "pi-baz"];
|
|
63
|
+
const registered = ["pi-foo"];
|
|
64
|
+
const result = analyzePackages(npm, registered);
|
|
65
|
+
|
|
66
|
+
assert.strictEqual(result.hasOrphans, true);
|
|
67
|
+
assert.deepStrictEqual(result.orphaned, ["pi-bar", "pi-baz"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should handle empty npm packages", () => {
|
|
71
|
+
const npm: string[] = [];
|
|
72
|
+
const registered = ["pi-foo"];
|
|
73
|
+
const result = analyzePackages(npm, registered);
|
|
74
|
+
|
|
75
|
+
assert.strictEqual(result.hasOrphans, false);
|
|
76
|
+
assert.deepStrictEqual(result.orphaned, []);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should handle empty registered packages", () => {
|
|
80
|
+
const npm = ["pi-foo", "pi-bar"];
|
|
81
|
+
const registered: string[] = [];
|
|
82
|
+
const result = analyzePackages(npm, registered);
|
|
83
|
+
|
|
84
|
+
assert.strictEqual(result.hasOrphans, true);
|
|
85
|
+
assert.deepStrictEqual(result.orphaned, ["pi-foo", "pi-bar"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should handle npm: prefix in registered", () => {
|
|
89
|
+
const npm = ["pi-foo", "pi-bar"];
|
|
90
|
+
const registered = ["npm:pi-foo", "npm:pi-bar"].map(normalizePackageName);
|
|
91
|
+
const result = analyzePackages(npm, registered);
|
|
92
|
+
|
|
93
|
+
assert.strictEqual(result.hasOrphans, false);
|
|
94
|
+
assert.deepStrictEqual(result.orphaned, []);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should handle mixed prefixes in registered", () => {
|
|
98
|
+
const npm = ["pi-foo", "pi-bar", "pi-baz"];
|
|
99
|
+
const registered = ["npm:pi-foo", "pi-bar"].map(normalizePackageName);
|
|
100
|
+
const result = analyzePackages(npm, registered);
|
|
101
|
+
|
|
102
|
+
assert.strictEqual(result.hasOrphans, true);
|
|
103
|
+
assert.deepStrictEqual(result.orphaned, ["pi-baz"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should exclude core package from orphaned", () => {
|
|
107
|
+
// Core package is filtered BEFORE analyzePackages is called
|
|
108
|
+
// So it shouldn't be in the npm list passed to analyzePackages
|
|
109
|
+
const npm = ["pi-foo", "@mariozechner/pi-coding-agent"];
|
|
110
|
+
// Core package should be filtered out by getNpmGlobalPackages
|
|
111
|
+
const filteredNpm = npm.filter(
|
|
112
|
+
(p) => p !== "@mariozechner/pi-coding-agent",
|
|
113
|
+
);
|
|
114
|
+
const registered: string[] = [];
|
|
115
|
+
const result = analyzePackages(filteredNpm, registered);
|
|
116
|
+
|
|
117
|
+
assert.strictEqual(result.hasOrphans, true);
|
|
118
|
+
assert.deepStrictEqual(result.orphaned, ["pi-foo"]);
|
|
119
|
+
// Core package should not appear
|
|
120
|
+
assert.strictEqual(
|
|
121
|
+
result.orphaned.includes("@mariozechner/pi-coding-agent"),
|
|
122
|
+
false,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("analyzePackages - EDGE CASES", () => {
|
|
128
|
+
it("should handle duplicates in npm list", () => {
|
|
129
|
+
// Set automatically deduplicates
|
|
130
|
+
const npm = ["pi-foo", "pi-foo", "pi-bar"];
|
|
131
|
+
const registered: string[] = [];
|
|
132
|
+
const result = analyzePackages(npm, registered);
|
|
133
|
+
|
|
134
|
+
// Should have pi-foo only once
|
|
135
|
+
assert.strictEqual(result.orphaned.length, 2);
|
|
136
|
+
assert.ok(result.orphaned.includes("pi-foo"));
|
|
137
|
+
assert.ok(result.orphaned.includes("pi-bar"));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should handle scoped packages in npm", () => {
|
|
141
|
+
const npm = ["pi-foo", "@scope/pi-bar"];
|
|
142
|
+
const registered = ["pi-foo"];
|
|
143
|
+
const result = analyzePackages(npm, registered);
|
|
144
|
+
|
|
145
|
+
assert.strictEqual(result.hasOrphans, true);
|
|
146
|
+
assert.deepStrictEqual(result.orphaned, ["@scope/pi-bar"]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should handle scoped packages in registered (normalized)", () => {
|
|
150
|
+
const npm = ["@scope/pi-bar"];
|
|
151
|
+
const registered = ["@scope/pi-bar"];
|
|
152
|
+
const result = analyzePackages(npm, registered);
|
|
153
|
+
|
|
154
|
+
assert.strictEqual(result.hasOrphans, false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should handle empty strings", () => {
|
|
158
|
+
const npm = [""];
|
|
159
|
+
const registered: string[] = [];
|
|
160
|
+
const result = analyzePackages(npm, registered);
|
|
161
|
+
|
|
162
|
+
assert.strictEqual(result.hasOrphans, true);
|
|
163
|
+
assert.deepStrictEqual(result.orphaned, [""]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should handle packages with special characters", () => {
|
|
167
|
+
const npm = ["pi-foo.bar", "pi-foo_bar", "pi-foo~bar"];
|
|
168
|
+
const registered: string[] = [];
|
|
169
|
+
const result = analyzePackages(npm, registered);
|
|
170
|
+
|
|
171
|
+
assert.strictEqual(result.orphaned.length, 3);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should be case-sensitive", () => {
|
|
175
|
+
const npm = ["pi-Foo", "PI-bar"];
|
|
176
|
+
const registered = ["pi-foo", "pi-bar"];
|
|
177
|
+
const result = analyzePackages(npm, registered);
|
|
178
|
+
|
|
179
|
+
// These are different packages due to case sensitivity
|
|
180
|
+
assert.strictEqual(result.hasOrphans, true);
|
|
181
|
+
assert.ok(result.orphaned.includes("pi-Foo"));
|
|
182
|
+
assert.ok(result.orphaned.includes("PI-bar"));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should handle very long package names", () => {
|
|
186
|
+
const longName = `pi-${"a".repeat(200)}`;
|
|
187
|
+
const npm = [longName];
|
|
188
|
+
const registered: string[] = [];
|
|
189
|
+
const result = analyzePackages(npm, registered);
|
|
190
|
+
|
|
191
|
+
assert.strictEqual(result.hasOrphans, true);
|
|
192
|
+
assert.deepStrictEqual(result.orphaned, [longName]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should handle unicode package names", () => {
|
|
196
|
+
const npm = ["pi-日本語"];
|
|
197
|
+
const registered: string[] = [];
|
|
198
|
+
const result = analyzePackages(npm, registered);
|
|
199
|
+
|
|
200
|
+
assert.strictEqual(result.hasOrphans, true);
|
|
201
|
+
assert.deepStrictEqual(result.orphaned, ["pi-日本語"]);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("syncOrphanedPackages - GOOD CASES", () => {
|
|
206
|
+
it("should add orphaned packages with npm: prefix", () => {
|
|
207
|
+
const orphaned = ["pi-foo", "pi-bar"];
|
|
208
|
+
const existing: string[] = [];
|
|
209
|
+
const result = syncOrphanedPackages(orphaned, existing);
|
|
210
|
+
|
|
211
|
+
assert.deepStrictEqual(result, ["npm:pi-foo", "npm:pi-bar"]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should preserve existing packages", () => {
|
|
215
|
+
const orphaned = ["pi-baz"];
|
|
216
|
+
const existing = ["npm:pi-foo", "npm:pi-bar"];
|
|
217
|
+
const result = syncOrphanedPackages(orphaned, existing);
|
|
218
|
+
|
|
219
|
+
assert.deepStrictEqual(result, ["npm:pi-foo", "npm:pi-bar", "npm:pi-baz"]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should handle empty orphaned list", () => {
|
|
223
|
+
const orphaned: string[] = [];
|
|
224
|
+
const existing = ["npm:pi-foo"];
|
|
225
|
+
const result = syncOrphanedPackages(orphaned, existing);
|
|
226
|
+
|
|
227
|
+
assert.deepStrictEqual(result, ["npm:pi-foo"]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should not duplicate if already registered without prefix", () => {
|
|
231
|
+
const orphaned = ["pi-foo"];
|
|
232
|
+
const existing = ["pi-foo"];
|
|
233
|
+
const result = syncOrphanedPackages(orphaned, existing);
|
|
234
|
+
|
|
235
|
+
// Should not add duplicate
|
|
236
|
+
assert.strictEqual(result.length, 1);
|
|
237
|
+
assert.deepStrictEqual(result, ["pi-foo"]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should not duplicate if already registered with prefix", () => {
|
|
241
|
+
const orphaned = ["pi-foo"];
|
|
242
|
+
const existing = ["npm:pi-foo"];
|
|
243
|
+
const result = syncOrphanedPackages(orphaned, existing);
|
|
244
|
+
|
|
245
|
+
// Should not add duplicate
|
|
246
|
+
assert.strictEqual(result.length, 1);
|
|
247
|
+
assert.deepStrictEqual(result, ["npm:pi-foo"]);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("syncOrphanedPackages - EDGE CASES", () => {
|
|
252
|
+
it("should handle duplicates in orphaned list", () => {
|
|
253
|
+
const orphaned = ["pi-foo", "pi-foo", "pi-bar"];
|
|
254
|
+
const existing: string[] = [];
|
|
255
|
+
const result = syncOrphanedPackages(orphaned, existing);
|
|
256
|
+
|
|
257
|
+
// Function deduplicates - checks includes() before adding
|
|
258
|
+
assert.strictEqual(result.length, 2);
|
|
259
|
+
assert.deepStrictEqual(result, ["npm:pi-foo", "npm:pi-bar"]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("should handle empty strings in orphaned", () => {
|
|
263
|
+
const orphaned = [""];
|
|
264
|
+
const existing: string[] = [];
|
|
265
|
+
const result = syncOrphanedPackages(orphaned, existing);
|
|
266
|
+
|
|
267
|
+
// Empty string becomes "npm:"
|
|
268
|
+
assert.deepStrictEqual(result, ["npm:"]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should handle mixed formats in existing", () => {
|
|
272
|
+
const orphaned = ["pi-baz"];
|
|
273
|
+
const existing = ["npm:pi-foo", "pi-bar"]; // Mixed prefixes
|
|
274
|
+
const result = syncOrphanedPackages(orphaned, existing);
|
|
275
|
+
|
|
276
|
+
assert.deepStrictEqual(result, ["npm:pi-foo", "pi-bar", "npm:pi-baz"]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("should handle very long package names", () => {
|
|
280
|
+
const longName = `pi-${"a".repeat(200)}`;
|
|
281
|
+
const orphaned = [longName];
|
|
282
|
+
const existing: string[] = [];
|
|
283
|
+
const result = syncOrphanedPackages(orphaned, existing);
|
|
284
|
+
|
|
285
|
+
assert.strictEqual(result.length, 1);
|
|
286
|
+
assert.ok(result[0].startsWith("npm:"));
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("normalizePackageName", () => {
|
|
291
|
+
it("should remove npm: prefix", () => {
|
|
292
|
+
assert.strictEqual(normalizePackageName("npm:pi-foo"), "pi-foo");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should leave unprefixed name unchanged", () => {
|
|
296
|
+
assert.strictEqual(normalizePackageName("pi-foo"), "pi-foo");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("should handle scoped packages with prefix", () => {
|
|
300
|
+
assert.strictEqual(
|
|
301
|
+
normalizePackageName("npm:@scope/pi-foo"),
|
|
302
|
+
"@scope/pi-foo",
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should handle empty string", () => {
|
|
307
|
+
assert.strictEqual(normalizePackageName(""), "");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("should handle string with just prefix", () => {
|
|
311
|
+
assert.strictEqual(normalizePackageName("npm:"), "");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("should handle multiple prefixes (only removes first)", () => {
|
|
315
|
+
// This is actually a bug - should we remove all npm: prefixes?
|
|
316
|
+
assert.strictEqual(normalizePackageName("npm:npm:pi-foo"), "npm:pi-foo");
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Test cases for type guards
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
|
|
6
|
+
// Type guard functions (copied from extensions/index.ts for testing)
|
|
7
|
+
function isPiSettings(
|
|
8
|
+
value: unknown,
|
|
9
|
+
): value is { packages?: string[]; extensions?: string[] } {
|
|
10
|
+
if (typeof value !== "object" || value === null) return false;
|
|
11
|
+
if (Array.isArray(value)) return false;
|
|
12
|
+
const candidate = value as Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
if (candidate.packages !== undefined) {
|
|
15
|
+
if (!Array.isArray(candidate.packages)) return false;
|
|
16
|
+
if (!candidate.packages.every((p) => typeof p === "string")) return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (candidate.extensions !== undefined) {
|
|
20
|
+
if (!Array.isArray(candidate.extensions)) return false;
|
|
21
|
+
if (!candidate.extensions.every((e) => typeof e === "string")) return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isBashToolInput(input: unknown): input is { command?: string } {
|
|
28
|
+
if (typeof input !== "object" || input === null) return false;
|
|
29
|
+
if (Array.isArray(input)) return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("isPiSettings", () => {
|
|
34
|
+
// GOOD CASES
|
|
35
|
+
it("should return true for valid PiSettings with packages", () => {
|
|
36
|
+
assert.strictEqual(isPiSettings({ packages: ["npm:pi-test"] }), true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should return true for valid PiSettings with extensions", () => {
|
|
40
|
+
assert.strictEqual(isPiSettings({ extensions: ["/path/to/ext"] }), true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should return true for valid PiSettings with both", () => {
|
|
44
|
+
assert.strictEqual(
|
|
45
|
+
isPiSettings({
|
|
46
|
+
packages: ["npm:pi-test"],
|
|
47
|
+
extensions: ["/path/to/ext"],
|
|
48
|
+
}),
|
|
49
|
+
true,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should return true for empty object", () => {
|
|
54
|
+
assert.strictEqual(isPiSettings({}), true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should return true for empty arrays", () => {
|
|
58
|
+
assert.strictEqual(isPiSettings({ packages: [], extensions: [] }), true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// BAD CASES
|
|
62
|
+
it("should return false for null", () => {
|
|
63
|
+
assert.strictEqual(isPiSettings(null), false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should return false for undefined", () => {
|
|
67
|
+
assert.strictEqual(isPiSettings(undefined), false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should return false for string", () => {
|
|
71
|
+
assert.strictEqual(isPiSettings("not an object"), false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should return false for number", () => {
|
|
75
|
+
assert.strictEqual(isPiSettings(123), false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should return false for array", () => {
|
|
79
|
+
assert.strictEqual(isPiSettings(["not", "valid"]), false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// EDGE CASES - packages
|
|
83
|
+
it("should return false for packages as string", () => {
|
|
84
|
+
assert.strictEqual(isPiSettings({ packages: "npm:pi-test" }), false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should return false for packages with non-string elements", () => {
|
|
88
|
+
assert.strictEqual(
|
|
89
|
+
isPiSettings({ packages: ["valid", 123, "valid"] }),
|
|
90
|
+
false,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should return false for packages as number", () => {
|
|
95
|
+
assert.strictEqual(isPiSettings({ packages: 42 }), false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should return false for packages as object", () => {
|
|
99
|
+
assert.strictEqual(isPiSettings({ packages: {} }), false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should return false for packages as null", () => {
|
|
103
|
+
assert.strictEqual(isPiSettings({ packages: null }), false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// EDGE CASES - extensions
|
|
107
|
+
it("should return false for extensions as string", () => {
|
|
108
|
+
assert.strictEqual(isPiSettings({ extensions: "/path/to/ext" }), false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should return false for extensions with non-string elements", () => {
|
|
112
|
+
assert.strictEqual(
|
|
113
|
+
isPiSettings({ extensions: ["valid", null, "valid"] }),
|
|
114
|
+
false,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// EDGE CASES - mixed
|
|
119
|
+
it("should return false for valid packages but invalid extensions", () => {
|
|
120
|
+
assert.strictEqual(
|
|
121
|
+
isPiSettings({ packages: ["npm:pi-test"], extensions: "bad" }),
|
|
122
|
+
false,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should return false for invalid packages but valid extensions", () => {
|
|
127
|
+
assert.strictEqual(
|
|
128
|
+
isPiSettings({ packages: 123, extensions: ["/path"] }),
|
|
129
|
+
false,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should return true for object with extra properties", () => {
|
|
134
|
+
assert.strictEqual(
|
|
135
|
+
isPiSettings({
|
|
136
|
+
packages: ["npm:pi-test"],
|
|
137
|
+
extensions: ["/path"],
|
|
138
|
+
extra: "ignored",
|
|
139
|
+
}),
|
|
140
|
+
true,
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("isBashToolInput", () => {
|
|
146
|
+
// GOOD CASES
|
|
147
|
+
it("should return true for object with command string", () => {
|
|
148
|
+
assert.strictEqual(isBashToolInput({ command: "npm install" }), true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should return true for empty object", () => {
|
|
152
|
+
assert.strictEqual(isBashToolInput({}), true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should return true for object with command undefined", () => {
|
|
156
|
+
assert.strictEqual(isBashToolInput({ command: undefined }), true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should return true for object with extra properties", () => {
|
|
160
|
+
assert.strictEqual(
|
|
161
|
+
isBashToolInput({ command: "npm install", extra: 123 }),
|
|
162
|
+
true,
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// BAD CASES
|
|
167
|
+
it("should return false for null", () => {
|
|
168
|
+
assert.strictEqual(isBashToolInput(null), false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should return false for undefined", () => {
|
|
172
|
+
assert.strictEqual(isBashToolInput(undefined), false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should return false for string", () => {
|
|
176
|
+
assert.strictEqual(isBashToolInput("command"), false);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should return false for number", () => {
|
|
180
|
+
assert.strictEqual(isBashToolInput(123), false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should return false for array", () => {
|
|
184
|
+
assert.strictEqual(isBashToolInput(["command"]), false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Test cases for npm install detection regex
|
|
2
|
+
|
|
3
|
+
import assert from "node:assert";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
|
|
6
|
+
// Regex patterns (from extensions/index.ts)
|
|
7
|
+
const NPM_GLOBAL_PATTERN = /npm\s+(?:install|i)(?:\s+\S+)*\s+(?:-g|--global)\b/;
|
|
8
|
+
const PI_PACKAGE_PATTERN = /\bpi-[a-z0-9-]+\b/;
|
|
9
|
+
|
|
10
|
+
function isGlobalPiInstall(command: string): {
|
|
11
|
+
isMatch: boolean;
|
|
12
|
+
packageName?: string;
|
|
13
|
+
} {
|
|
14
|
+
if (!NPM_GLOBAL_PATTERN.test(command)) {
|
|
15
|
+
return { isMatch: false };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const match = command.match(PI_PACKAGE_PATTERN);
|
|
19
|
+
if (!match) {
|
|
20
|
+
return { isMatch: false };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { isMatch: true, packageName: match[0] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("isGlobalPiInstall - GOOD CASES (should detect)", () => {
|
|
27
|
+
it("should detect 'npm install -g pi-foo'", () => {
|
|
28
|
+
const result = isGlobalPiInstall("npm install -g pi-foo");
|
|
29
|
+
assert.strictEqual(result.isMatch, true);
|
|
30
|
+
assert.strictEqual(result.packageName, "pi-foo");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should detect 'npm i -g pi-foo' (short install)", () => {
|
|
34
|
+
const result = isGlobalPiInstall("npm i -g pi-foo");
|
|
35
|
+
assert.strictEqual(result.isMatch, true);
|
|
36
|
+
assert.strictEqual(result.packageName, "pi-foo");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should detect 'npm install --global pi-foo'", () => {
|
|
40
|
+
const result = isGlobalPiInstall("npm install --global pi-foo");
|
|
41
|
+
assert.strictEqual(result.isMatch, true);
|
|
42
|
+
assert.strictEqual(result.packageName, "pi-foo");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should detect 'npm i --global pi-foo'", () => {
|
|
46
|
+
const result = isGlobalPiInstall("npm i --global pi-foo");
|
|
47
|
+
assert.strictEqual(result.isMatch, true);
|
|
48
|
+
assert.strictEqual(result.packageName, "pi-foo");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should detect with scoped package 'npm install -g @scope/pi-foo'", () => {
|
|
52
|
+
const result = isGlobalPiInstall("npm install -g @scope/pi-foo");
|
|
53
|
+
assert.strictEqual(result.isMatch, true);
|
|
54
|
+
assert.strictEqual(result.packageName, "pi-foo");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should detect 'npm install pi-foo -g' (-g at end)", () => {
|
|
58
|
+
const result = isGlobalPiInstall("npm install pi-foo -g");
|
|
59
|
+
assert.strictEqual(result.isMatch, true);
|
|
60
|
+
assert.strictEqual(result.packageName, "pi-foo");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should detect 'npm install pi-foo --global'", () => {
|
|
64
|
+
const result = isGlobalPiInstall("npm install pi-foo --global");
|
|
65
|
+
assert.strictEqual(result.isMatch, true);
|
|
66
|
+
assert.strictEqual(result.packageName, "pi-foo");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should detect with multiple packages 'npm install -g pi-foo pi-bar'", () => {
|
|
70
|
+
const result = isGlobalPiInstall("npm install -g pi-foo pi-bar");
|
|
71
|
+
assert.strictEqual(result.isMatch, true);
|
|
72
|
+
// Returns first match
|
|
73
|
+
assert.strictEqual(result.packageName, "pi-foo");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should detect with version 'npm install -g pi-foo@1.0.0'", () => {
|
|
77
|
+
const result = isGlobalPiInstall("npm install -g pi-foo@1.0.0");
|
|
78
|
+
assert.strictEqual(result.isMatch, true);
|
|
79
|
+
assert.strictEqual(result.packageName, "pi-foo");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("isGlobalPiInstall - BAD CASES (should NOT detect)", () => {
|
|
84
|
+
it("should NOT detect local install 'npm install pi-foo' (no -g)", () => {
|
|
85
|
+
const result = isGlobalPiInstall("npm install pi-foo");
|
|
86
|
+
assert.strictEqual(result.isMatch, false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should NOT detect 'npm install pi-foo --save'", () => {
|
|
90
|
+
const result = isGlobalPiInstall("npm install pi-foo --save");
|
|
91
|
+
assert.strictEqual(result.isMatch, false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should NOT detect 'npm install pi-foo --save-dev'", () => {
|
|
95
|
+
const result = isGlobalPiInstall("npm install pi-foo --save-dev");
|
|
96
|
+
assert.strictEqual(result.isMatch, false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should NOT detect non-pi package 'npm install -g lodash'", () => {
|
|
100
|
+
const result = isGlobalPiInstall("npm install -g lodash");
|
|
101
|
+
assert.strictEqual(result.isMatch, false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should NOT detect 'npm install -g typescript'", () => {
|
|
105
|
+
const result = isGlobalPiInstall("npm install -g typescript");
|
|
106
|
+
assert.strictEqual(result.isMatch, false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should NOT detect 'npm run install'", () => {
|
|
110
|
+
const result = isGlobalPiInstall("npm run install");
|
|
111
|
+
assert.strictEqual(result.isMatch, false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should NOT detect 'npm uninstall -g pi-foo'", () => {
|
|
115
|
+
const result = isGlobalPiInstall("npm uninstall -g pi-foo");
|
|
116
|
+
assert.strictEqual(result.isMatch, false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should NOT detect 'npm remove -g pi-foo'", () => {
|
|
120
|
+
const result = isGlobalPiInstall("npm remove -g pi-foo");
|
|
121
|
+
assert.strictEqual(result.isMatch, false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should NOT detect 'yarn global add pi-foo'", () => {
|
|
125
|
+
const result = isGlobalPiInstall("yarn global add pi-foo");
|
|
126
|
+
assert.strictEqual(result.isMatch, false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should NOT detect 'pnpm add -g pi-foo'", () => {
|
|
130
|
+
const result = isGlobalPiInstall("pnpm add -g pi-foo");
|
|
131
|
+
assert.strictEqual(result.isMatch, false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should NOT detect empty string", () => {
|
|
135
|
+
const result = isGlobalPiInstall("");
|
|
136
|
+
assert.strictEqual(result.isMatch, false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should NOT detect unrelated command 'ls -la'", () => {
|
|
140
|
+
const result = isGlobalPiInstall("ls -la");
|
|
141
|
+
assert.strictEqual(result.isMatch, false);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("isGlobalPiInstall - EDGE CASES", () => {
|
|
146
|
+
it("should NOT detect 'npm install api-foo -g' (pi- inside word)", () => {
|
|
147
|
+
// This is a tricky case - "api-foo" contains "pi-" but shouldn't match
|
|
148
|
+
// Our pattern uses \b which might not catch this perfectly
|
|
149
|
+
const result = isGlobalPiInstall("npm install api-foo -g");
|
|
150
|
+
// This will likely match "pi-foo" inside "api-foo" due to \b behavior
|
|
151
|
+
// Documenting this as a known limitation
|
|
152
|
+
console.log("Edge case 'api-foo':", result);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should NOT detect 'npm install @scope/api-foo -g'", () => {
|
|
156
|
+
const result = isGlobalPiInstall("npm install @scope/api-foo -g");
|
|
157
|
+
console.log("Edge case '@scope/api-foo':", result);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should detect 'npm install -g pi' (single 'pi' is not a package)", () => {
|
|
161
|
+
// "pi" by itself is not "pi-" followed by something
|
|
162
|
+
const result = isGlobalPiInstall("npm install -g pi");
|
|
163
|
+
assert.strictEqual(result.isMatch, false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should detect 'npm install -g pi-' (trailing dash, no name)", () => {
|
|
167
|
+
// "pi-" without anything after shouldn't match
|
|
168
|
+
const result = isGlobalPiInstall("npm install -g pi-");
|
|
169
|
+
assert.strictEqual(result.isMatch, false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should detect 'npm install -g my-pi-foo'", () => {
|
|
173
|
+
// This WILL detect because \b matches at start of word
|
|
174
|
+
// and "pi-foo" is a valid pattern within the word
|
|
175
|
+
const result = isGlobalPiInstall("npm install -g my-pi-foo");
|
|
176
|
+
assert.strictEqual(result.isMatch, true);
|
|
177
|
+
assert.strictEqual(result.packageName, "pi-foo");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should handle multiple -g flags 'npm install -g pi-foo -g'", () => {
|
|
181
|
+
const result = isGlobalPiInstall("npm install -g pi-foo -g");
|
|
182
|
+
assert.strictEqual(result.isMatch, true);
|
|
183
|
+
assert.strictEqual(result.packageName, "pi-foo");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should handle extra whitespace 'npm install -g pi-foo'", () => {
|
|
187
|
+
const result = isGlobalPiInstall("npm install -g pi-foo");
|
|
188
|
+
// Current pattern uses \s+ which matches one or more whitespace
|
|
189
|
+
assert.strictEqual(result.isMatch, true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should detect with hyphenated name 'npm install -g pi-my-extension'", () => {
|
|
193
|
+
const result = isGlobalPiInstall("npm install -g pi-my-extension");
|
|
194
|
+
assert.strictEqual(result.isMatch, true);
|
|
195
|
+
assert.strictEqual(result.packageName, "pi-my-extension");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should detect with numbers 'npm install -g pi-ext123'", () => {
|
|
199
|
+
const result = isGlobalPiInstall("npm install -g pi-ext123");
|
|
200
|
+
assert.strictEqual(result.isMatch, true);
|
|
201
|
+
assert.strictEqual(result.packageName, "pi-ext123");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should NOT detect with uppercase 'npm install -g PI-FOO'", () => {
|
|
205
|
+
// Pattern is case-sensitive (lowercase only)
|
|
206
|
+
const result = isGlobalPiInstall("npm install -g PI-FOO");
|
|
207
|
+
assert.strictEqual(result.isMatch, false);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("Regex pattern unit tests", () => {
|
|
212
|
+
describe("NPM_GLOBAL_PATTERN", () => {
|
|
213
|
+
it("should match 'npm install -g foo'", () => {
|
|
214
|
+
assert.strictEqual(NPM_GLOBAL_PATTERN.test("npm install -g foo"), true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should NOT match 'npm install foo'", () => {
|
|
218
|
+
assert.strictEqual(NPM_GLOBAL_PATTERN.test("npm install foo"), false);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should match 'npm i -g foo'", () => {
|
|
222
|
+
assert.strictEqual(NPM_GLOBAL_PATTERN.test("npm i -g foo"), true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should match 'npm install --global foo'", () => {
|
|
226
|
+
assert.strictEqual(
|
|
227
|
+
NPM_GLOBAL_PATTERN.test("npm install --global foo"),
|
|
228
|
+
true,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should NOT match 'npm uninstall -g foo'", () => {
|
|
233
|
+
assert.strictEqual(
|
|
234
|
+
NPM_GLOBAL_PATTERN.test("npm uninstall -g foo"),
|
|
235
|
+
false,
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("PI_PACKAGE_PATTERN", () => {
|
|
241
|
+
it("should match 'pi-foo'", () => {
|
|
242
|
+
assert.strictEqual(PI_PACKAGE_PATTERN.test("pi-foo"), true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should find 'pi-foo' in multiple words", () => {
|
|
246
|
+
const input = "pi-foo pi-bar pi-123";
|
|
247
|
+
const matches = input.match(PI_PACKAGE_PATTERN);
|
|
248
|
+
assert.ok(matches, "Should find matches");
|
|
249
|
+
assert.strictEqual(matches[0], "pi-foo");
|
|
250
|
+
assert.strictEqual(matches[1], undefined); // Without global flag, only first match
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should NOT match just 'pi'", () => {
|
|
254
|
+
assert.strictEqual(PI_PACKAGE_PATTERN.test("pi"), false);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should NOT match 'api-foo'", () => {
|
|
258
|
+
// \b at start means word boundary, so 'api-foo' won't match
|
|
259
|
+
assert.strictEqual(PI_PACKAGE_PATTERN.test("api-foo"), false);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("should match 'my-pi-foo' (within word)", () => {
|
|
263
|
+
// \b matches at start of 'pi' within the word
|
|
264
|
+
assert.strictEqual(PI_PACKAGE_PATTERN.test("my-pi-foo"), true);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"noImplicitAny": true,
|
|
13
|
+
"strictNullChecks": true,
|
|
14
|
+
"strictFunctionTypes": true,
|
|
15
|
+
"strictBindCallApply": true,
|
|
16
|
+
"strictPropertyInitialization": true,
|
|
17
|
+
"noImplicitThis": true,
|
|
18
|
+
"alwaysStrict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noImplicitReturns": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"types": ["node"]
|
|
24
|
+
},
|
|
25
|
+
"include": ["extensions/**/*", "test/**/*"],
|
|
26
|
+
"exclude": ["node_modules"]
|
|
27
|
+
}
|