@wrongstack/plugins 0.305.1 → 0.306.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/index.d.ts +6 -0
- package/dist/audit.js +7 -0
- package/dist/factories/index.d.ts +1 -1
- package/dist/factories.js +1 -0
- package/dist/generated-plugin-exports.d.ts +1 -0
- package/dist/gitignore-guard/index.d.ts +82 -0
- package/dist/gitignore-guard.d.ts +2 -0
- package/dist/gitignore-guard.js +442 -0
- package/dist/index.js +2721 -2284
- package/dist/manifest.js +1 -0
- package/dist/plugin-audit-catalog.js +7 -0
- package/dist/spec-linker.js +1 -0
- package/package.json +7 -3
package/dist/audit/index.d.ts
CHANGED
|
@@ -142,6 +142,12 @@ export declare const OFFICIAL_PLUGIN_AUDIT_ENTRIES: readonly [{
|
|
|
142
142
|
readonly summary: 'Detects runaway tool-call loops (identical repeats and A-B-A-B oscillation) — warns the model, then blocks';
|
|
143
143
|
readonly defaultState: 'active';
|
|
144
144
|
readonly canDisable: true;
|
|
145
|
+
}, {
|
|
146
|
+
readonly name: 'gitignore-guard';
|
|
147
|
+
readonly risk: 'low';
|
|
148
|
+
readonly summary: 'PostToolUse hook that suggests or appends .gitignore entries for build-artifact-looking files after every write or edit';
|
|
149
|
+
readonly defaultState: 'inactive';
|
|
150
|
+
readonly canDisable: true;
|
|
145
151
|
}, {
|
|
146
152
|
readonly name: 'path-guard';
|
|
147
153
|
readonly risk: 'medium';
|
package/dist/audit.js
CHANGED
|
@@ -168,6 +168,13 @@ var OFFICIAL_PLUGIN_AUDIT_ENTRIES = [
|
|
|
168
168
|
defaultState: "active",
|
|
169
169
|
canDisable: true
|
|
170
170
|
},
|
|
171
|
+
{
|
|
172
|
+
name: "gitignore-guard",
|
|
173
|
+
risk: "low",
|
|
174
|
+
summary: "PostToolUse hook that suggests or appends .gitignore entries for build-artifact-looking files after every write or edit",
|
|
175
|
+
defaultState: "inactive",
|
|
176
|
+
canDisable: true
|
|
177
|
+
},
|
|
171
178
|
{
|
|
172
179
|
name: "path-guard",
|
|
173
180
|
risk: "medium",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Plugin } from '@wrongstack/core/types';
|
|
2
|
-
export declare const OFFICIAL_PLUGIN_SPECIFIERS: readonly ['@wrongstack/plugins/agent-handoff', '@wrongstack/plugins/cost-tracker', '@wrongstack/plugins/file-watcher', '@wrongstack/plugins/git-autocommit', '@wrongstack/plugins/auto-doc', '@wrongstack/plugins/shell-check', '@wrongstack/plugins/cron', '@wrongstack/plugins/template-engine', '@wrongstack/plugins/semver-bump', '@wrongstack/plugins/secret-scanner', '@wrongstack/plugins/todo-tracker', '@wrongstack/plugins/token-budget', '@wrongstack/plugins/lint-gate', '@wrongstack/plugins/branch-guard', '@wrongstack/plugins/diff-summary', '@wrongstack/plugins/commit-validator', '@wrongstack/plugins/format-on-save', '@wrongstack/plugins/test-runner-gate', '@wrongstack/plugins/import-organizer', '@wrongstack/plugins/knowledge-graph', '@wrongstack/plugins/todo-listener', '@wrongstack/plugins/session-recap', '@wrongstack/plugins/spec-linker', '@wrongstack/plugins/loop-breaker', '@wrongstack/plugins/path-guard', '@wrongstack/plugins/process-guard', '@wrongstack/plugins/context-pins', '@wrongstack/plugins/checkpoint', '@wrongstack/plugins/error-lens', '@wrongstack/plugins/dep-guard', '@wrongstack/plugins/config-validator', '@wrongstack/plugins/notify-hub', '@wrongstack/plugins/changelog-writer', '@wrongstack/plugins/injection-shield', '@wrongstack/plugins/llm-cache', '@wrongstack/plugins/model-router', '@wrongstack/plugins/pr-drafter', '@wrongstack/plugins/prompt-firewall', '@wrongstack/plugins/auto-escalate', '@wrongstack/plugins/test-coverage-gate', '@wrongstack/plugins/type-gate', '@wrongstack/plugins/token-throttle', '@wrongstack/plugins/plugin-stack-observer', '@wrongstack/plugins/dependency-vulnerability-gate', '@wrongstack/plugins/migration-planner', '@wrongstack/plugins/semantic-search-indexer', '@wrongstack/plugins/auto-i18n-extractor', '@wrongstack/plugins/doc-sync-guard', '@wrongstack/plugins/api-compatibility-gate', '@wrongstack/plugins/performance-regression-gate', '@wrongstack/plugins/test-flake-detector', '@wrongstack/plugins/schema-evolution-guard', '@wrongstack/plugins/license-audit-gate', '@wrongstack/plugins/accessibility-auditor', '@wrongstack/plugins/security-hotspot-scanner', '@wrongstack/plugins/dead-code-detector', '@wrongstack/plugins/duplicate-code-detector', '@wrongstack/plugins/code-metrics', '@wrongstack/plugins/refactor-suggester', '@wrongstack/plugins/test-generator', '@wrongstack/plugins/release-notes-generator', '@wrongstack/plugins/smart-rename', '@wrongstack/plugins/feature-flag-tracker', '@wrongstack/plugins/interface-contract-guard'];
|
|
2
|
+
export declare const OFFICIAL_PLUGIN_SPECIFIERS: readonly ['@wrongstack/plugins/agent-handoff', '@wrongstack/plugins/cost-tracker', '@wrongstack/plugins/file-watcher', '@wrongstack/plugins/git-autocommit', '@wrongstack/plugins/auto-doc', '@wrongstack/plugins/shell-check', '@wrongstack/plugins/cron', '@wrongstack/plugins/template-engine', '@wrongstack/plugins/semver-bump', '@wrongstack/plugins/secret-scanner', '@wrongstack/plugins/todo-tracker', '@wrongstack/plugins/token-budget', '@wrongstack/plugins/lint-gate', '@wrongstack/plugins/branch-guard', '@wrongstack/plugins/diff-summary', '@wrongstack/plugins/commit-validator', '@wrongstack/plugins/format-on-save', '@wrongstack/plugins/test-runner-gate', '@wrongstack/plugins/import-organizer', '@wrongstack/plugins/knowledge-graph', '@wrongstack/plugins/todo-listener', '@wrongstack/plugins/session-recap', '@wrongstack/plugins/spec-linker', '@wrongstack/plugins/loop-breaker', '@wrongstack/plugins/gitignore-guard', '@wrongstack/plugins/path-guard', '@wrongstack/plugins/process-guard', '@wrongstack/plugins/context-pins', '@wrongstack/plugins/checkpoint', '@wrongstack/plugins/error-lens', '@wrongstack/plugins/dep-guard', '@wrongstack/plugins/config-validator', '@wrongstack/plugins/notify-hub', '@wrongstack/plugins/changelog-writer', '@wrongstack/plugins/injection-shield', '@wrongstack/plugins/llm-cache', '@wrongstack/plugins/model-router', '@wrongstack/plugins/pr-drafter', '@wrongstack/plugins/prompt-firewall', '@wrongstack/plugins/auto-escalate', '@wrongstack/plugins/test-coverage-gate', '@wrongstack/plugins/type-gate', '@wrongstack/plugins/token-throttle', '@wrongstack/plugins/plugin-stack-observer', '@wrongstack/plugins/dependency-vulnerability-gate', '@wrongstack/plugins/migration-planner', '@wrongstack/plugins/semantic-search-indexer', '@wrongstack/plugins/auto-i18n-extractor', '@wrongstack/plugins/doc-sync-guard', '@wrongstack/plugins/api-compatibility-gate', '@wrongstack/plugins/performance-regression-gate', '@wrongstack/plugins/test-flake-detector', '@wrongstack/plugins/schema-evolution-guard', '@wrongstack/plugins/license-audit-gate', '@wrongstack/plugins/accessibility-auditor', '@wrongstack/plugins/security-hotspot-scanner', '@wrongstack/plugins/dead-code-detector', '@wrongstack/plugins/duplicate-code-detector', '@wrongstack/plugins/code-metrics', '@wrongstack/plugins/refactor-suggester', '@wrongstack/plugins/test-generator', '@wrongstack/plugins/release-notes-generator', '@wrongstack/plugins/smart-rename', '@wrongstack/plugins/feature-flag-tracker', '@wrongstack/plugins/interface-contract-guard'];
|
|
3
3
|
export declare const OFFICIAL_PLUGIN_FACTORIES: readonly (() => Promise<Plugin>)[];
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/factories.js
CHANGED
|
@@ -24,6 +24,7 @@ var OFFICIAL_PLUGIN_SPECIFIERS = [
|
|
|
24
24
|
"@wrongstack/plugins/session-recap",
|
|
25
25
|
"@wrongstack/plugins/spec-linker",
|
|
26
26
|
"@wrongstack/plugins/loop-breaker",
|
|
27
|
+
"@wrongstack/plugins/gitignore-guard",
|
|
27
28
|
"@wrongstack/plugins/path-guard",
|
|
28
29
|
"@wrongstack/plugins/process-guard",
|
|
29
30
|
"@wrongstack/plugins/context-pins",
|
|
@@ -24,6 +24,7 @@ export { default as featureFlagTrackerPlugin } from './feature-flag-tracker/inde
|
|
|
24
24
|
export { default as fileWatcherPlugin } from './file-watcher/index.js';
|
|
25
25
|
export { default as formatOnSavePlugin } from './format-on-save/index.js';
|
|
26
26
|
export { default as gitAutocommitPlugin } from './git-autocommit/index.js';
|
|
27
|
+
export { default as gitignoreGuardPlugin } from './gitignore-guard/index.js';
|
|
27
28
|
export { default as importOrganizerPlugin } from './import-organizer/index.js';
|
|
28
29
|
export { default as injectionShieldPlugin } from './injection-shield/index.js';
|
|
29
30
|
export { default as interfaceContractGuardPlugin } from './interface-contract-guard/index.js';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gitignore-guard plugin — repo hygiene for generated files.
|
|
3
|
+
*
|
|
4
|
+
* PostToolUse hook that detects writes of build-artifact-looking files
|
|
5
|
+
* (`dist/`, `coverage/`, `node_modules/`, `*.env`, …) and either
|
|
6
|
+
* *suggests* a `.gitignore` entry (default) or *appends* it for you.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately orthogonal to path-guard: path-guard BLOCKS writes/edits
|
|
9
|
+
* to protected paths (lockfiles, .env, .git, migrations); gitignore-guard
|
|
10
|
+
* never blocks a tool — it keeps generated files out of the repository by
|
|
11
|
+
* growing the project's `.gitignore`. Both hooks can be active at once.
|
|
12
|
+
*
|
|
13
|
+
* What counts as a build artifact is configurable (`artifactPatterns`),
|
|
14
|
+
* with a curated high-precision default list. Patterns use gitignore-style
|
|
15
|
+
* semantics implemented in `matchGitignorePattern`:
|
|
16
|
+
*
|
|
17
|
+
* - trailing `/` → directory pattern. Bare name (e.g. `dist/`) matches a
|
|
18
|
+
* directory with that name at any depth; an internal slash
|
|
19
|
+
* (e.g. `docs/generated/`) anchors it to the root.
|
|
20
|
+
* - contains `/` → anchored glob matched against the path relative to
|
|
21
|
+
* the project root, e.g. `docs/generated`
|
|
22
|
+
* - otherwise → glob matched against the basename at any depth,
|
|
23
|
+
* e.g. `*.env` matches `.env` and `config/.env`
|
|
24
|
+
*
|
|
25
|
+
* The hook only ever touches `.gitignore` files inside the project root
|
|
26
|
+
* (containment is checked against the hook's `cwd`). In `append` mode it
|
|
27
|
+
* appends missing pattern lines to the first existing `.gitignore` walking
|
|
28
|
+
* from the root down toward the artifact, creating a root `.gitignore` when
|
|
29
|
+
* none exists. It never rewrites or removes existing lines.
|
|
30
|
+
*
|
|
31
|
+
* Tools registered:
|
|
32
|
+
* - gitignore_guard_status : config + per-session counters.
|
|
33
|
+
* - gitignore_guard_append : append an entry for a path (or an explicit
|
|
34
|
+
* pattern) on demand — the completion path for a `suggest`-mode notice.
|
|
35
|
+
*
|
|
36
|
+
* Hooks registered:
|
|
37
|
+
* - PostToolUse, matcher `write|edit`. Skips tool errors, paths outside
|
|
38
|
+
* the project, `.gitignore` writes themselves, paths that already match
|
|
39
|
+
* an existing `.gitignore` line, and paths matched by `ignorePatterns`.
|
|
40
|
+
*
|
|
41
|
+
* Config (`config.extensions['gitignore-guard']`):
|
|
42
|
+
*
|
|
43
|
+
* ```jsonc
|
|
44
|
+
* {
|
|
45
|
+
* "enabled": true, // master switch
|
|
46
|
+
* "mode": "suggest", // "suggest" | "append"
|
|
47
|
+
* "artifactPatterns": [...], // patterns that flag a file as an artifact
|
|
48
|
+
* "ignorePatterns": [], // patterns that never flag (opt-outs)
|
|
49
|
+
* "maxAppendPerCall": 5 // append cap per hook invocation
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
import type { Plugin } from '@wrongstack/core/types';
|
|
56
|
+
/**
|
|
57
|
+
* Curated default list of build-artifact-looking names. High precision on
|
|
58
|
+
* purpose: this hook fires on every write/edit, so the defaults avoid
|
|
59
|
+
* ambiguous names (e.g. `build/` and `out/` are committed by some repos).
|
|
60
|
+
* Users extend or replace the list via `config.extensions['gitignore-guard']`.
|
|
61
|
+
*/
|
|
62
|
+
export declare const DEFAULT_ARTIFACT_PATTERNS: readonly string[];
|
|
63
|
+
/**
|
|
64
|
+
* Gitignore-style match of `pattern` against `relPath` (project-relative,
|
|
65
|
+
* forward slashes). See the module doc for the three pattern shapes.
|
|
66
|
+
*/
|
|
67
|
+
export declare function matchGitignorePattern(relPath: string, pattern: string): boolean;
|
|
68
|
+
/** First artifact pattern matching `relPath`, or null when none match. */
|
|
69
|
+
export declare function classifyArtifact(relPath: string, patterns: readonly string[]): string | null;
|
|
70
|
+
export interface GitignoreGuardConfig {
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
mode: 'suggest' | 'append';
|
|
73
|
+
artifactPatterns: readonly string[];
|
|
74
|
+
ignorePatterns: readonly string[];
|
|
75
|
+
maxAppendPerCall: number;
|
|
76
|
+
}
|
|
77
|
+
export declare const DEFAULTS: GitignoreGuardConfig;
|
|
78
|
+
/** Read and validate config; every bad field falls back to its default. */
|
|
79
|
+
export declare function readConfig(raw: unknown): GitignoreGuardConfig;
|
|
80
|
+
declare const plugin: Plugin;
|
|
81
|
+
export default plugin;
|
|
82
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
// src/gitignore-guard/index.ts
|
|
2
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
var API_VERSION = "^0.1.10";
|
|
5
|
+
var DEFAULT_ARTIFACT_PATTERNS = Object.freeze([
|
|
6
|
+
"dist/",
|
|
7
|
+
"coverage/",
|
|
8
|
+
"node_modules/",
|
|
9
|
+
".next/",
|
|
10
|
+
".nuxt/",
|
|
11
|
+
".output/",
|
|
12
|
+
".cache/",
|
|
13
|
+
".parcel-cache/",
|
|
14
|
+
".turbo/",
|
|
15
|
+
".vite/",
|
|
16
|
+
".swc/",
|
|
17
|
+
".pytest_cache/",
|
|
18
|
+
".mypy_cache/",
|
|
19
|
+
"__pycache__/",
|
|
20
|
+
".venv/",
|
|
21
|
+
"venv/",
|
|
22
|
+
"target/",
|
|
23
|
+
".eslintcache",
|
|
24
|
+
"*.tsbuildinfo",
|
|
25
|
+
"*.pyc",
|
|
26
|
+
"*.env",
|
|
27
|
+
"*.env.*"
|
|
28
|
+
]);
|
|
29
|
+
function toForwardSlashes(p) {
|
|
30
|
+
return sep === "/" ? p : p.split(sep).join("/");
|
|
31
|
+
}
|
|
32
|
+
function globToSource(glob) {
|
|
33
|
+
return glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
34
|
+
}
|
|
35
|
+
function globToRegExp(glob) {
|
|
36
|
+
return new RegExp(`^${globToSource(glob)}$`);
|
|
37
|
+
}
|
|
38
|
+
function matchGitignorePattern(relPath, pattern) {
|
|
39
|
+
const rel = toForwardSlashes(relPath);
|
|
40
|
+
const pat = pattern.trim();
|
|
41
|
+
if (pat.length === 0 || pat.startsWith("#")) return false;
|
|
42
|
+
const segments = rel.split("/");
|
|
43
|
+
if (pat.endsWith("/")) {
|
|
44
|
+
const dir = pat.slice(0, -1);
|
|
45
|
+
if (dir.length === 0) return false;
|
|
46
|
+
if (dir.includes("/")) {
|
|
47
|
+
return new RegExp(`^${globToSource(dir)}(?:/|$)`).test(rel);
|
|
48
|
+
}
|
|
49
|
+
return segments.some((s) => s === dir);
|
|
50
|
+
}
|
|
51
|
+
if (pat.includes("/")) {
|
|
52
|
+
return globToRegExp(toForwardSlashes(pat)).test(rel);
|
|
53
|
+
}
|
|
54
|
+
const base = segments[segments.length - 1] ?? "";
|
|
55
|
+
return globToRegExp(pat).test(base);
|
|
56
|
+
}
|
|
57
|
+
function classifyArtifact(relPath, patterns) {
|
|
58
|
+
for (const p of patterns) {
|
|
59
|
+
if (matchGitignorePattern(relPath, p)) return p;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
async function pathExists(p) {
|
|
64
|
+
try {
|
|
65
|
+
await access(p);
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function readGitignoreLines(file) {
|
|
72
|
+
if (!await pathExists(file)) return [];
|
|
73
|
+
try {
|
|
74
|
+
const content = await readFile(file, "utf8");
|
|
75
|
+
return content.split(/\r?\n/);
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isCoveredByLines(lines, relPath) {
|
|
81
|
+
for (const raw of lines) {
|
|
82
|
+
const line = raw.trim();
|
|
83
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
84
|
+
if (matchGitignorePattern(relPath, line)) return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
function gitignoreCandidates(relDir, root) {
|
|
89
|
+
const parts = relDir === "" ? [] : toForwardSlashes(relDir).split("/");
|
|
90
|
+
const out = [join(root, ".gitignore")];
|
|
91
|
+
let cur = root;
|
|
92
|
+
for (const part of parts) {
|
|
93
|
+
cur = join(cur, part);
|
|
94
|
+
out.push(join(cur, ".gitignore"));
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function relDirOf(rel) {
|
|
99
|
+
const idx = rel.lastIndexOf("/");
|
|
100
|
+
return idx < 0 ? "" : rel.slice(0, idx);
|
|
101
|
+
}
|
|
102
|
+
async function firstExistingGitignore(candidates) {
|
|
103
|
+
for (const c of candidates) {
|
|
104
|
+
if (await pathExists(c)) return c;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
async function appendPatterns(target, patterns, limit) {
|
|
109
|
+
const existing = await readGitignoreLines(target);
|
|
110
|
+
const seen = new Set(
|
|
111
|
+
existing.map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"))
|
|
112
|
+
);
|
|
113
|
+
const missing = patterns.filter((p) => !seen.has(p)).slice(0, limit);
|
|
114
|
+
if (missing.length === 0) return { appended: [] };
|
|
115
|
+
const current = await pathExists(target) ? await readFile(target, "utf8") : "";
|
|
116
|
+
const base = current.length === 0 ? "" : current.endsWith("\n") ? current : `${current}
|
|
117
|
+
`;
|
|
118
|
+
const next = `${base}${missing.map((p) => `${p}
|
|
119
|
+
`).join("")}`;
|
|
120
|
+
await writeFile(target, next, "utf8");
|
|
121
|
+
return { appended: missing };
|
|
122
|
+
}
|
|
123
|
+
function projectRelativePath(rawPath, cwd) {
|
|
124
|
+
const root = cwd ?? process.cwd();
|
|
125
|
+
const abs = isAbsolute(rawPath) ? rawPath : resolve(root, rawPath);
|
|
126
|
+
const rel = toForwardSlashes(relative(root, abs));
|
|
127
|
+
if (rel.startsWith("..") || isAbsolute(rel) || rel === "") return null;
|
|
128
|
+
return { abs, rel, root };
|
|
129
|
+
}
|
|
130
|
+
var DEFAULTS = {
|
|
131
|
+
enabled: true,
|
|
132
|
+
mode: "suggest",
|
|
133
|
+
artifactPatterns: DEFAULT_ARTIFACT_PATTERNS,
|
|
134
|
+
ignorePatterns: [],
|
|
135
|
+
maxAppendPerCall: 5
|
|
136
|
+
};
|
|
137
|
+
function normalizePatterns(value) {
|
|
138
|
+
if (!Array.isArray(value)) return [];
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const v of value) {
|
|
141
|
+
if (typeof v !== "string") continue;
|
|
142
|
+
const t = v.trim();
|
|
143
|
+
if (t.length === 0 || t.startsWith("#")) continue;
|
|
144
|
+
out.push(t);
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function readConfig(raw) {
|
|
149
|
+
if (!raw || typeof raw !== "object") {
|
|
150
|
+
return { ...DEFAULTS, artifactPatterns: [...DEFAULTS.artifactPatterns] };
|
|
151
|
+
}
|
|
152
|
+
const r = raw;
|
|
153
|
+
const artifact = normalizePatterns(r["artifactPatterns"]);
|
|
154
|
+
return {
|
|
155
|
+
enabled: r["enabled"] !== false,
|
|
156
|
+
mode: r["mode"] === "append" ? "append" : "suggest",
|
|
157
|
+
artifactPatterns: artifact.length > 0 ? artifact : [...DEFAULTS.artifactPatterns],
|
|
158
|
+
ignorePatterns: normalizePatterns(r["ignorePatterns"]),
|
|
159
|
+
maxAppendPerCall: typeof r["maxAppendPerCall"] === "number" && Number.isInteger(r["maxAppendPerCall"]) && r["maxAppendPerCall"] > 0 ? Math.min(r["maxAppendPerCall"], 50) : DEFAULTS.maxAppendPerCall
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
var state = {
|
|
163
|
+
invocationCount: 0,
|
|
164
|
+
/** Artifacts detected after ignore/coverage checks passed. */
|
|
165
|
+
matchedCount: 0,
|
|
166
|
+
/** Suggest-mode notices injected into the tool result. */
|
|
167
|
+
suggestCount: 0,
|
|
168
|
+
/** Lines appended to a .gitignore (hook or append tool). */
|
|
169
|
+
appendCount: 0,
|
|
170
|
+
/** Paths skipped because an existing .gitignore line already covers them. */
|
|
171
|
+
coveredSkipCount: 0,
|
|
172
|
+
errorCount: 0,
|
|
173
|
+
/** Hook handle for teardown. */
|
|
174
|
+
hookUnregister: null,
|
|
175
|
+
/** Last detection — surfaced by health() + status tool. */
|
|
176
|
+
lastResult: null
|
|
177
|
+
};
|
|
178
|
+
function clearRegistrations() {
|
|
179
|
+
if (state.hookUnregister) {
|
|
180
|
+
try {
|
|
181
|
+
state.hookUnregister();
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
state.hookUnregister = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
var plugin = {
|
|
188
|
+
name: "gitignore-guard",
|
|
189
|
+
version: "0.1.0",
|
|
190
|
+
description: "PostToolUse hook that suggests or appends .gitignore entries for build-artifact-looking files after every write or edit",
|
|
191
|
+
apiVersion: API_VERSION,
|
|
192
|
+
capabilities: { hooks: true, tools: true },
|
|
193
|
+
configFields: {
|
|
194
|
+
enabled: { lifecycle: "hot" },
|
|
195
|
+
mode: { lifecycle: "hot" },
|
|
196
|
+
artifactPatterns: { lifecycle: "hot" },
|
|
197
|
+
ignorePatterns: { lifecycle: "hot" },
|
|
198
|
+
maxAppendPerCall: { lifecycle: "hot" }
|
|
199
|
+
},
|
|
200
|
+
defaultConfig: {
|
|
201
|
+
enabled: DEFAULTS.enabled,
|
|
202
|
+
mode: DEFAULTS.mode,
|
|
203
|
+
artifactPatterns: [...DEFAULTS.artifactPatterns],
|
|
204
|
+
ignorePatterns: [],
|
|
205
|
+
maxAppendPerCall: DEFAULTS.maxAppendPerCall
|
|
206
|
+
},
|
|
207
|
+
configSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
enabled: {
|
|
211
|
+
type: "boolean",
|
|
212
|
+
default: true,
|
|
213
|
+
description: "Master switch. When false, the hook is a no-op."
|
|
214
|
+
},
|
|
215
|
+
mode: {
|
|
216
|
+
type: "string",
|
|
217
|
+
enum: ["suggest", "append"],
|
|
218
|
+
default: "suggest",
|
|
219
|
+
description: '"suggest" injects a notice into the tool result; "append" writes the missing pattern to a .gitignore automatically.'
|
|
220
|
+
},
|
|
221
|
+
artifactPatterns: {
|
|
222
|
+
type: "array",
|
|
223
|
+
items: { type: "string" },
|
|
224
|
+
default: [...DEFAULTS.artifactPatterns],
|
|
225
|
+
description: "Gitignore-style patterns that flag a written file as a build artifact. Empty array falls back to the curated defaults."
|
|
226
|
+
},
|
|
227
|
+
ignorePatterns: {
|
|
228
|
+
type: "array",
|
|
229
|
+
items: { type: "string" },
|
|
230
|
+
default: [],
|
|
231
|
+
description: "Gitignore-style patterns that never flag a file, even when artifactPatterns matches. Use for intentionally-committed generated files."
|
|
232
|
+
},
|
|
233
|
+
maxAppendPerCall: {
|
|
234
|
+
type: "integer",
|
|
235
|
+
minimum: 1,
|
|
236
|
+
maximum: 50,
|
|
237
|
+
default: 5,
|
|
238
|
+
description: "Maximum number of lines appended per hook invocation."
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
async setup(api) {
|
|
243
|
+
clearRegistrations();
|
|
244
|
+
state.invocationCount = 0;
|
|
245
|
+
state.matchedCount = 0;
|
|
246
|
+
state.suggestCount = 0;
|
|
247
|
+
state.appendCount = 0;
|
|
248
|
+
state.coveredSkipCount = 0;
|
|
249
|
+
state.errorCount = 0;
|
|
250
|
+
state.lastResult = null;
|
|
251
|
+
const hook = async (input) => {
|
|
252
|
+
const cfg = readConfig(api.config.extensions?.["gitignore-guard"]);
|
|
253
|
+
if (!cfg.enabled) return;
|
|
254
|
+
if (input.toolResult?.isError) return;
|
|
255
|
+
const toolName = input.toolName ?? "";
|
|
256
|
+
const inp = input.toolInput ?? {};
|
|
257
|
+
const rawPath = inp["path"];
|
|
258
|
+
if (typeof rawPath !== "string" || rawPath.trim().length === 0) return;
|
|
259
|
+
if (basename(rawPath) === ".gitignore") return;
|
|
260
|
+
const resolved = projectRelativePath(rawPath, input.cwd);
|
|
261
|
+
if (!resolved) return;
|
|
262
|
+
state.invocationCount += 1;
|
|
263
|
+
const matched = classifyArtifact(resolved.rel, cfg.artifactPatterns);
|
|
264
|
+
if (!matched) return;
|
|
265
|
+
if (classifyArtifact(resolved.rel, cfg.ignorePatterns)) return;
|
|
266
|
+
state.matchedCount += 1;
|
|
267
|
+
const candidates = gitignoreCandidates(relDirOf(resolved.rel), resolved.root);
|
|
268
|
+
for (const candidate of candidates) {
|
|
269
|
+
const lines = await readGitignoreLines(candidate);
|
|
270
|
+
if (isCoveredByLines(lines, resolved.rel)) {
|
|
271
|
+
state.coveredSkipCount += 1;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
state.lastResult = {
|
|
276
|
+
path: resolved.rel,
|
|
277
|
+
tool: toolName,
|
|
278
|
+
pattern: matched,
|
|
279
|
+
mode: cfg.mode,
|
|
280
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
281
|
+
};
|
|
282
|
+
if (cfg.mode === "append") {
|
|
283
|
+
const nearest = await firstExistingGitignore(candidates);
|
|
284
|
+
const target = nearest ?? join(resolved.root, ".gitignore");
|
|
285
|
+
const { appended } = await appendPatterns(target, [matched], cfg.maxAppendPerCall);
|
|
286
|
+
if (appended.length === 0) {
|
|
287
|
+
state.coveredSkipCount += 1;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
state.appendCount += 1;
|
|
291
|
+
api.metrics.counter("append");
|
|
292
|
+
api.log.info(`gitignore-guard: appended '${matched}' to ${target}`, {
|
|
293
|
+
path: resolved.rel,
|
|
294
|
+
tool: toolName
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
additionalContext: `
|
|
298
|
+
\u{1F4E6} gitignore-guard: '${resolved.rel}' looks like a build artifact (pattern '${matched}'). Appended '${matched}' to ${target}.`
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
state.suggestCount += 1;
|
|
302
|
+
api.metrics.counter("suggest");
|
|
303
|
+
api.log.info(`gitignore-guard: suggested '${matched}' for ${resolved.rel}`, {
|
|
304
|
+
path: resolved.rel,
|
|
305
|
+
tool: toolName
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
additionalContext: `
|
|
309
|
+
\u{1F4E6} gitignore-guard: '${resolved.rel}' looks like a build artifact (pattern '${matched}'). Add '${matched}' to .gitignore \u2014 call the gitignore_guard_append tool with path: "${resolved.rel}" to append it now, or set config mode to "append" to do this automatically.`
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook, {
|
|
313
|
+
name: "gitignore-guard"
|
|
314
|
+
});
|
|
315
|
+
api.tools.register({
|
|
316
|
+
name: "gitignore_guard_status",
|
|
317
|
+
description: "Reports gitignore-guard state: mode, active pattern lists, and per-session suggest/append/skip counters.",
|
|
318
|
+
inputSchema: { type: "object", properties: {} },
|
|
319
|
+
permission: "auto",
|
|
320
|
+
category: "Code Quality",
|
|
321
|
+
mutating: false,
|
|
322
|
+
async execute() {
|
|
323
|
+
const cfg = readConfig(api.config.extensions?.["gitignore-guard"]);
|
|
324
|
+
return {
|
|
325
|
+
ok: true,
|
|
326
|
+
enabled: cfg.enabled,
|
|
327
|
+
mode: cfg.mode,
|
|
328
|
+
artifactPatterns: cfg.artifactPatterns,
|
|
329
|
+
ignorePatterns: cfg.ignorePatterns,
|
|
330
|
+
maxAppendPerCall: cfg.maxAppendPerCall,
|
|
331
|
+
counters: {
|
|
332
|
+
invocations: state.invocationCount,
|
|
333
|
+
matched: state.matchedCount,
|
|
334
|
+
suggests: state.suggestCount,
|
|
335
|
+
appends: state.appendCount,
|
|
336
|
+
coveredSkips: state.coveredSkipCount,
|
|
337
|
+
errors: state.errorCount
|
|
338
|
+
},
|
|
339
|
+
lastResult: state.lastResult
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
api.tools.register({
|
|
344
|
+
name: "gitignore_guard_append",
|
|
345
|
+
description: "Appends a .gitignore entry for a build-artifact-looking path (or an explicit pattern). Use after gitignore-guard suggests an entry, or to ignore a generated file proactively.",
|
|
346
|
+
inputSchema: {
|
|
347
|
+
type: "object",
|
|
348
|
+
properties: {
|
|
349
|
+
path: {
|
|
350
|
+
type: "string",
|
|
351
|
+
description: "Path of the artifact file or directory, relative to the project root or absolute."
|
|
352
|
+
},
|
|
353
|
+
pattern: {
|
|
354
|
+
type: "string",
|
|
355
|
+
description: "Optional explicit gitignore pattern to append. When omitted, the pattern is derived from the path via artifactPatterns."
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
required: ["path"]
|
|
359
|
+
},
|
|
360
|
+
permission: "auto",
|
|
361
|
+
category: "Code Quality",
|
|
362
|
+
mutating: true,
|
|
363
|
+
async execute(input) {
|
|
364
|
+
const rawPath = typeof input?.path === "string" ? input.path : "";
|
|
365
|
+
if (rawPath.length === 0) return { ok: false, reason: "path is required" };
|
|
366
|
+
const resolved = projectRelativePath(rawPath, process.cwd());
|
|
367
|
+
if (!resolved) {
|
|
368
|
+
return { ok: false, reason: `path outside project root: ${rawPath}` };
|
|
369
|
+
}
|
|
370
|
+
const cfg = readConfig(api.config.extensions?.["gitignore-guard"]);
|
|
371
|
+
const explicit = typeof input?.pattern === "string" && input.pattern.trim().length > 0 ? input.pattern.trim() : null;
|
|
372
|
+
const pattern = explicit ?? classifyArtifact(resolved.rel, cfg.artifactPatterns);
|
|
373
|
+
if (!pattern) {
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
reason: `'${resolved.rel}' does not match any configured artifact pattern; pass an explicit pattern`
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const candidates = gitignoreCandidates(relDirOf(resolved.rel), resolved.root);
|
|
380
|
+
const nearest = await firstExistingGitignore(candidates);
|
|
381
|
+
const target = nearest ?? join(resolved.root, ".gitignore");
|
|
382
|
+
const { appended } = await appendPatterns(target, [pattern], cfg.maxAppendPerCall);
|
|
383
|
+
if (appended.length === 0) {
|
|
384
|
+
state.coveredSkipCount += 1;
|
|
385
|
+
return { ok: true, alreadyCovered: true, gitignore: target, appended: [] };
|
|
386
|
+
}
|
|
387
|
+
state.appendCount += 1;
|
|
388
|
+
api.log.info(`gitignore-guard: manual append '${pattern}' to ${target}`, {
|
|
389
|
+
path: resolved.rel
|
|
390
|
+
});
|
|
391
|
+
return { ok: true, alreadyCovered: false, gitignore: target, appended };
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
api.log.info("gitignore-guard plugin loaded", {
|
|
395
|
+
version: "0.1.0",
|
|
396
|
+
mode: readConfig(api.config.extensions?.["gitignore-guard"]).mode
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
teardown(api) {
|
|
400
|
+
clearRegistrations();
|
|
401
|
+
const final = {
|
|
402
|
+
invocations: state.invocationCount,
|
|
403
|
+
matched: state.matchedCount,
|
|
404
|
+
suggests: state.suggestCount,
|
|
405
|
+
appends: state.appendCount,
|
|
406
|
+
coveredSkips: state.coveredSkipCount,
|
|
407
|
+
errors: state.errorCount
|
|
408
|
+
};
|
|
409
|
+
state.invocationCount = 0;
|
|
410
|
+
state.matchedCount = 0;
|
|
411
|
+
state.suggestCount = 0;
|
|
412
|
+
state.appendCount = 0;
|
|
413
|
+
state.coveredSkipCount = 0;
|
|
414
|
+
state.errorCount = 0;
|
|
415
|
+
state.lastResult = null;
|
|
416
|
+
api.log.info("gitignore-guard: teardown complete", { final });
|
|
417
|
+
},
|
|
418
|
+
async health() {
|
|
419
|
+
return {
|
|
420
|
+
ok: true,
|
|
421
|
+
message: state.lastResult === null ? `gitignore-guard: ${state.invocationCount} invocation(s), ${state.suggestCount} suggested, ${state.appendCount} appended, ${state.coveredSkipCount} already-covered` : `gitignore-guard: last '${state.lastResult.pattern}' for ${state.lastResult.path} (${state.lastResult.tool}, ${state.lastResult.mode} mode) at ${state.lastResult.when}`,
|
|
422
|
+
counters: {
|
|
423
|
+
invocations: state.invocationCount,
|
|
424
|
+
matched: state.matchedCount,
|
|
425
|
+
suggests: state.suggestCount,
|
|
426
|
+
appends: state.appendCount,
|
|
427
|
+
coveredSkips: state.coveredSkipCount,
|
|
428
|
+
errors: state.errorCount
|
|
429
|
+
},
|
|
430
|
+
lastResult: state.lastResult
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
var gitignore_guard_default = plugin;
|
|
435
|
+
export {
|
|
436
|
+
DEFAULTS,
|
|
437
|
+
DEFAULT_ARTIFACT_PATTERNS,
|
|
438
|
+
classifyArtifact,
|
|
439
|
+
gitignore_guard_default as default,
|
|
440
|
+
matchGitignorePattern,
|
|
441
|
+
readConfig
|
|
442
|
+
};
|