pilotignore 0.1.2 → 0.2.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 +79 -0
- package/LICENSE +7 -7
- package/README.md +155 -152
- package/bin/cli.js +269 -89
- package/package.json +19 -5
- package/src/pilotignore.cjs +516 -0
- package/src/templates/default.pilotignore +15 -0
- package/{templates → src/templates}/pilotignore.json +16 -15
- package/templates/default.pilotignore +0 -6
- package/templates/pilotignore.cjs +0 -178
package/bin/cli.js
CHANGED
|
@@ -1,89 +1,269 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from "fs/promises";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
console.log("
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
// ESM equivalents for __dirname
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const USAGE = `pilotignore — keep your secrets secret
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
npx pilotignore init Install the Copilot hook and create .pilotignore
|
|
16
|
+
npx pilotignore verify Check the installation with live hook runs
|
|
17
|
+
npx pilotignore help Show this message
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
async function init(targetDir) {
|
|
21
|
+
const githubHooksDir = path.join(targetDir, ".github", "hooks");
|
|
22
|
+
const srcDir = path.join(__dirname, "..", "src");
|
|
23
|
+
const templatesDir = path.join(srcDir, "templates");
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// 1. Create the directory structure
|
|
27
|
+
await fs.mkdir(githubHooksDir, { recursive: true });
|
|
28
|
+
console.log("✅ Ensured directory: .github/hooks");
|
|
29
|
+
|
|
30
|
+
// 2. Copy the configuration file
|
|
31
|
+
await fs.copyFile(
|
|
32
|
+
path.join(templatesDir, "pilotignore.json"),
|
|
33
|
+
path.join(githubHooksDir, "pilotignore.json"),
|
|
34
|
+
);
|
|
35
|
+
console.log("✅ Copied: .github/hooks/pilotignore.json");
|
|
36
|
+
|
|
37
|
+
// 3. Copy the actual firewall script
|
|
38
|
+
await fs.copyFile(
|
|
39
|
+
path.join(srcDir, "pilotignore.cjs"),
|
|
40
|
+
path.join(githubHooksDir, "pilotignore.cjs"),
|
|
41
|
+
);
|
|
42
|
+
console.log("✅ Copied: .github/hooks/pilotignore.cjs");
|
|
43
|
+
|
|
44
|
+
// 4. Create .pilotignore only if it doesn't exist
|
|
45
|
+
const targetPilotignore = path.join(targetDir, ".pilotignore");
|
|
46
|
+
try {
|
|
47
|
+
await fs.access(targetPilotignore);
|
|
48
|
+
console.log("ℹ️ .pilotignore already exists, skipping creation.");
|
|
49
|
+
} catch {
|
|
50
|
+
await fs.copyFile(
|
|
51
|
+
path.join(templatesDir, "default.pilotignore"),
|
|
52
|
+
targetPilotignore,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 5. Add .copilot/ to .gitignore if not already ignored
|
|
57
|
+
const targetGitignore = path.join(targetDir, ".gitignore");
|
|
58
|
+
try {
|
|
59
|
+
let gitignoreContent = "";
|
|
60
|
+
try {
|
|
61
|
+
gitignoreContent = await fs.readFile(targetGitignore, "utf8");
|
|
62
|
+
} catch {
|
|
63
|
+
// .gitignore doesn't exist yet, we will create it
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!gitignoreContent.includes(".copilot")) {
|
|
67
|
+
const separator = gitignoreContent && !gitignoreContent.endsWith("\n") ? "\n" : "";
|
|
68
|
+
await fs.writeFile(
|
|
69
|
+
targetGitignore,
|
|
70
|
+
gitignoreContent + separator + "\n# pilotignore logs\n.copilot/\n",
|
|
71
|
+
"utf8"
|
|
72
|
+
);
|
|
73
|
+
console.log("✅ Added .copilot/ to .gitignore");
|
|
74
|
+
} else {
|
|
75
|
+
console.log("ℹ️ .copilot/ already in .gitignore, skipping.");
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.log("ℹ️ Could not update .gitignore: " + err.message);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log("\n🚀 pilotignore initialization complete!");
|
|
82
|
+
console.log(
|
|
83
|
+
"Add your sensitive files to the .pilotignore file at the root of your project,",
|
|
84
|
+
);
|
|
85
|
+
console.log("then run `npx pilotignore verify` to confirm the firewall works.");
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error("❌ Installation error:", error.message);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runHook(scriptPath, cwd, payload) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
const child = spawn(process.execPath, [scriptPath], { cwd });
|
|
95
|
+
let stdout = "";
|
|
96
|
+
let stderr = "";
|
|
97
|
+
child.stdout.on("data", (d) => (stdout += d));
|
|
98
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
99
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
100
|
+
child.on("error", (err) =>
|
|
101
|
+
resolve({ code: -1, stdout, stderr: String(err) }),
|
|
102
|
+
);
|
|
103
|
+
child.stdin.write(JSON.stringify(payload));
|
|
104
|
+
child.stdin.end();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// An allow is silent (exit 0, no stdout); a deny carries a JSON decision.
|
|
109
|
+
function parseDecision({ code, stdout }) {
|
|
110
|
+
const out = stdout.trim();
|
|
111
|
+
if (!out) return code === 0 ? "allow" : null;
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(out);
|
|
114
|
+
return (
|
|
115
|
+
parsed.permissionDecision ||
|
|
116
|
+
parsed.hookSpecificOutput?.permissionDecision ||
|
|
117
|
+
null
|
|
118
|
+
);
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Turns a glob rule into one concrete path that should match it,
|
|
125
|
+
// so verify can exercise the real hook with real rules.
|
|
126
|
+
function concretePathFor(rule) {
|
|
127
|
+
let p = rule.trim();
|
|
128
|
+
if (p.startsWith("!") || p.startsWith("#")) return null;
|
|
129
|
+
p = p.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
130
|
+
p = p
|
|
131
|
+
.replace(/\*\*/g, "x/y")
|
|
132
|
+
.replace(/\*/g, "x")
|
|
133
|
+
.replace(/\?/g, "x")
|
|
134
|
+
.replace(/\[([^\]]*)\]/g, (m, cls) =>
|
|
135
|
+
cls.startsWith("!") || cls.startsWith("^")
|
|
136
|
+
? "x"
|
|
137
|
+
: cls.replace(/\\/g, "").charAt(0) || "x",
|
|
138
|
+
);
|
|
139
|
+
return p || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function verify(targetDir) {
|
|
143
|
+
let ok = true;
|
|
144
|
+
const results = [];
|
|
145
|
+
const check = (passed, label) => {
|
|
146
|
+
results.push(`${passed ? "✓" : "✗"} ${label}`);
|
|
147
|
+
if (!passed) ok = false;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const hooksDir = path.join(targetDir, ".github", "hooks");
|
|
151
|
+
const jsonPath = path.join(hooksDir, "pilotignore.json");
|
|
152
|
+
const scriptPath = path.join(hooksDir, "pilotignore.cjs");
|
|
153
|
+
const ignorePath = path.join(targetDir, ".pilotignore");
|
|
154
|
+
|
|
155
|
+
// 1. Hook configuration
|
|
156
|
+
try {
|
|
157
|
+
const cfg = JSON.parse(await fs.readFile(jsonPath, "utf8"));
|
|
158
|
+
const hooks = cfg?.hooks?.preToolUse;
|
|
159
|
+
check(
|
|
160
|
+
Array.isArray(hooks) && hooks.length > 0,
|
|
161
|
+
".github/hooks/pilotignore.json — valid preToolUse hook config",
|
|
162
|
+
);
|
|
163
|
+
} catch {
|
|
164
|
+
check(
|
|
165
|
+
false,
|
|
166
|
+
".github/hooks/pilotignore.json — missing or invalid (run: npx pilotignore init)",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 2. Hook script
|
|
171
|
+
const scriptExists = await fs
|
|
172
|
+
.access(scriptPath)
|
|
173
|
+
.then(() => true)
|
|
174
|
+
.catch(() => false);
|
|
175
|
+
check(
|
|
176
|
+
scriptExists,
|
|
177
|
+
".github/hooks/pilotignore.cjs — firewall script present",
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// 3. .pilotignore rules
|
|
181
|
+
let rules = [];
|
|
182
|
+
try {
|
|
183
|
+
rules = (await fs.readFile(ignorePath, "utf8"))
|
|
184
|
+
.split(/\r?\n/)
|
|
185
|
+
.map((l) => l.trim())
|
|
186
|
+
.filter((l) => l && !l.startsWith("#") && !l.startsWith("!"));
|
|
187
|
+
check(true, `.pilotignore — ${rules.length} blocking rule(s) loaded`);
|
|
188
|
+
if (rules.length === 0) {
|
|
189
|
+
check(false, ".pilotignore contains no blocking rules");
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
check(false, ".pilotignore — not found (run: npx pilotignore init)");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 4. Live checks against the real installed hook
|
|
196
|
+
if (scriptExists && rules.length > 0) {
|
|
197
|
+
let denyCandidate = null;
|
|
198
|
+
for (const rule of rules.slice(0, 10)) {
|
|
199
|
+
const candidate = concretePathFor(rule);
|
|
200
|
+
if (!candidate) continue;
|
|
201
|
+
const result = await runHook(scriptPath, targetDir, {
|
|
202
|
+
timestamp: Date.now(),
|
|
203
|
+
cwd: targetDir,
|
|
204
|
+
toolName: "view",
|
|
205
|
+
toolArgs: { path: candidate },
|
|
206
|
+
});
|
|
207
|
+
if (parseDecision(result) === "deny") {
|
|
208
|
+
denyCandidate = candidate;
|
|
209
|
+
check(true, `view "${candidate}" (rule "${rule}") → denied`);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!denyCandidate) {
|
|
214
|
+
check(
|
|
215
|
+
false,
|
|
216
|
+
"no .pilotignore rule produced a deny — check your patterns",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (denyCandidate) {
|
|
221
|
+
const result = await runHook(scriptPath, targetDir, {
|
|
222
|
+
timestamp: Date.now(),
|
|
223
|
+
cwd: targetDir,
|
|
224
|
+
toolName: "bash",
|
|
225
|
+
toolArgs: { command: `cat ${denyCandidate}` },
|
|
226
|
+
});
|
|
227
|
+
check(
|
|
228
|
+
parseDecision(result) === "deny",
|
|
229
|
+
`bash "cat ${denyCandidate}" → denied`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const probe = "pilotignore-verify-probe";
|
|
234
|
+
const result = await runHook(scriptPath, targetDir, {
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
cwd: targetDir,
|
|
237
|
+
toolName: "view",
|
|
238
|
+
toolArgs: { path: probe },
|
|
239
|
+
});
|
|
240
|
+
check(
|
|
241
|
+
parseDecision(result) === "allow",
|
|
242
|
+
`view "${probe}" → allowed (no output, Copilot's normal permission flow applies)`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log("\npilotignore verify\n");
|
|
247
|
+
results.forEach((r) => console.log(" " + r));
|
|
248
|
+
console.log(
|
|
249
|
+
`\n${ok ? "✅ All checks passed." : "❌ Some checks FAILED."}`,
|
|
250
|
+
);
|
|
251
|
+
console.log(
|
|
252
|
+
`Live hook decisions are logged to .copilot/logs/pilotignore.log\n`,
|
|
253
|
+
);
|
|
254
|
+
process.exit(ok ? 0 : 1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const command = process.argv[2];
|
|
258
|
+
const targetDir = process.cwd();
|
|
259
|
+
|
|
260
|
+
if (command === "init") {
|
|
261
|
+
await init(targetDir);
|
|
262
|
+
} else if (command === "verify") {
|
|
263
|
+
await verify(targetDir);
|
|
264
|
+
} else {
|
|
265
|
+
console.log(USAGE);
|
|
266
|
+
process.exit(
|
|
267
|
+
command === "help" || command === "--help" || command === "-h" ? 0 : 1,
|
|
268
|
+
);
|
|
269
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pilotignore",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Keep GitHub Copilot agents out of the files you list in .pilotignore. Works like .gitignore, for Copilot tool calls.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "bin/cli.js",
|
|
7
6
|
"bin": {
|
|
8
7
|
"pilotignore": "./bin/cli.js"
|
|
9
8
|
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/vkerttula/pilotignore.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/vkerttula/pilotignore/issues",
|
|
14
|
+
"homepage": "https://github.com/vkerttula/pilotignore#readme",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
10
18
|
"scripts": {
|
|
11
|
-
"test": "node --test
|
|
19
|
+
"test": "node --test",
|
|
20
|
+
"smoke": "node tests/smoke.mjs",
|
|
21
|
+
"sandbox": "node tests/sandbox/create.mjs"
|
|
12
22
|
},
|
|
13
23
|
"keywords": [
|
|
14
24
|
"copilot",
|
|
15
25
|
"github",
|
|
26
|
+
"github-copilot",
|
|
27
|
+
"copilot-cli",
|
|
28
|
+
"gitignore",
|
|
16
29
|
"hook",
|
|
17
30
|
"ignore",
|
|
18
31
|
"security",
|
|
@@ -24,7 +37,8 @@
|
|
|
24
37
|
],
|
|
25
38
|
"files": [
|
|
26
39
|
"bin",
|
|
27
|
-
"
|
|
40
|
+
"src",
|
|
41
|
+
"CHANGELOG.md"
|
|
28
42
|
],
|
|
29
43
|
"author": "Valtteri Kerttula",
|
|
30
44
|
"license": "MIT"
|