@wrongstack/plugins 0.277.2 → 0.280.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/README.md +838 -0
- package/dist/auto-doc.d.ts +8 -0
- package/dist/auto-doc.js +175 -13
- package/dist/auto-escalate.d.ts +45 -0
- package/dist/auto-escalate.js +190 -0
- package/dist/branch-guard.d.ts +33 -0
- package/dist/branch-guard.js +228 -0
- package/dist/changelog-writer.d.ts +73 -0
- package/dist/changelog-writer.js +369 -0
- package/dist/checkpoint.d.ts +55 -0
- package/dist/checkpoint.js +305 -0
- package/dist/commit-validator.d.ts +33 -0
- package/dist/commit-validator.js +315 -0
- package/dist/config-validator.d.ts +48 -0
- package/dist/config-validator.js +347 -0
- package/dist/context-pins.d.ts +45 -0
- package/dist/context-pins.js +240 -0
- package/dist/cost-tracker.d.ts +40 -1
- package/dist/cost-tracker.js +105 -4
- package/dist/dep-guard.d.ts +65 -0
- package/dist/dep-guard.js +316 -0
- package/dist/diff-summary.d.ts +36 -0
- package/dist/diff-summary.js +235 -0
- package/dist/error-lens.d.ts +67 -0
- package/dist/error-lens.js +280 -0
- package/dist/format-on-save.d.ts +35 -0
- package/dist/format-on-save.js +219 -0
- package/dist/git-autocommit.js +186 -26
- package/dist/import-organizer.d.ts +52 -0
- package/dist/import-organizer.js +274 -0
- package/dist/index.d.ts +32 -6
- package/dist/index.js +10151 -1628
- package/dist/injection-shield.d.ts +49 -0
- package/dist/injection-shield.js +205 -0
- package/dist/lint-gate.d.ts +33 -0
- package/dist/lint-gate.js +394 -0
- package/dist/llm-cache.d.ts +56 -0
- package/dist/llm-cache.js +251 -0
- package/dist/loop-breaker.d.ts +43 -0
- package/dist/loop-breaker.js +241 -0
- package/dist/model-router.d.ts +69 -0
- package/dist/model-router.js +198 -0
- package/dist/notify-hub.d.ts +45 -0
- package/dist/notify-hub.js +304 -0
- package/dist/path-guard.d.ts +54 -0
- package/dist/path-guard.js +235 -0
- package/dist/prompt-firewall.d.ts +57 -0
- package/dist/prompt-firewall.js +290 -0
- package/dist/secret-scanner.d.ts +34 -0
- package/dist/secret-scanner.js +409 -0
- package/dist/semver-bump.js +45 -0
- package/dist/session-recap.d.ts +50 -0
- package/dist/session-recap.js +421 -0
- package/dist/shell-check.js +52 -4
- package/dist/spec-linker.d.ts +51 -0
- package/dist/spec-linker.js +541 -0
- package/dist/template-engine.js +19 -1
- package/dist/test-runner-gate.d.ts +37 -0
- package/dist/test-runner-gate.js +356 -0
- package/dist/todo-listener.d.ts +37 -0
- package/dist/todo-listener.js +216 -0
- package/dist/todo-tracker.d.ts +5 -0
- package/dist/todo-tracker.js +441 -0
- package/dist/token-budget.d.ts +40 -0
- package/dist/token-budget.js +254 -0
- package/dist/token-throttle.d.ts +54 -0
- package/dist/token-throttle.js +203 -0
- package/package.json +116 -12
- package/dist/json-path.d.ts +0 -18
- package/dist/json-path.js +0 -15
- package/dist/web-search.d.ts +0 -19
- package/dist/web-search.js +0 -15
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync, statSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
// src/format-on-save/index.ts
|
|
5
|
+
var API_VERSION = "^0.1.10";
|
|
6
|
+
var state = {
|
|
7
|
+
invocationCount: 0,
|
|
8
|
+
/** Times formatting was applied (file changed). */
|
|
9
|
+
formattedCount: 0,
|
|
10
|
+
/** Times the file was already formatted (no change). */
|
|
11
|
+
cleanCount: 0,
|
|
12
|
+
/** Times biome failed (not installed, timeout, parse error). */
|
|
13
|
+
errorCount: 0,
|
|
14
|
+
/** Hook handle for teardown. */
|
|
15
|
+
hookUnregister: null,
|
|
16
|
+
/** Last format result — surfaced by health() + status tool. */
|
|
17
|
+
lastResult: null
|
|
18
|
+
};
|
|
19
|
+
var DEFAULTS = {
|
|
20
|
+
enabled: true,
|
|
21
|
+
timeoutMs: 5e3
|
|
22
|
+
};
|
|
23
|
+
function readConfig(raw) {
|
|
24
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
25
|
+
const r = raw;
|
|
26
|
+
return {
|
|
27
|
+
enabled: r["enabled"] !== false,
|
|
28
|
+
timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] > 0 ? r["timeoutMs"] : DEFAULTS.timeoutMs
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function formatFile(filePath, timeoutMs) {
|
|
32
|
+
if (!existsSync(filePath)) return null;
|
|
33
|
+
let bytesBefore;
|
|
34
|
+
try {
|
|
35
|
+
bytesBefore = statSync(filePath).size;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
execSync(`npx biome format --write "${filePath}"`, {
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
timeout: timeoutMs,
|
|
43
|
+
cwd: process.cwd(),
|
|
44
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
45
|
+
});
|
|
46
|
+
} catch (err) {
|
|
47
|
+
const e = err;
|
|
48
|
+
if (e.killed) return null;
|
|
49
|
+
}
|
|
50
|
+
let bytesAfter;
|
|
51
|
+
try {
|
|
52
|
+
bytesAfter = statSync(filePath).size;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (bytesAfter !== bytesBefore) {
|
|
57
|
+
return { changed: true, bytesBefore, bytesAfter };
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
execSync(`npx biome format "${filePath}"`, {
|
|
61
|
+
encoding: "utf-8",
|
|
62
|
+
timeout: timeoutMs,
|
|
63
|
+
cwd: process.cwd(),
|
|
64
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
65
|
+
});
|
|
66
|
+
return { changed: false, bytesBefore, bytesAfter };
|
|
67
|
+
} catch {
|
|
68
|
+
return { changed: true, bytesBefore, bytesAfter };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
var plugin = {
|
|
72
|
+
name: "format-on-save",
|
|
73
|
+
version: "0.1.0",
|
|
74
|
+
description: "PostToolUse hook that runs biome format --write on the file after every write or edit",
|
|
75
|
+
apiVersion: API_VERSION,
|
|
76
|
+
capabilities: { tools: true, hooks: true },
|
|
77
|
+
defaultConfig: { ...DEFAULTS },
|
|
78
|
+
configSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
enabled: {
|
|
82
|
+
type: "boolean",
|
|
83
|
+
default: true,
|
|
84
|
+
description: "Master switch. When false, the hook is a no-op."
|
|
85
|
+
},
|
|
86
|
+
timeoutMs: {
|
|
87
|
+
type: "number",
|
|
88
|
+
minimum: 1e3,
|
|
89
|
+
default: 5e3,
|
|
90
|
+
description: "Biome format process timeout in milliseconds."
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
setup(api) {
|
|
95
|
+
state.invocationCount = 0;
|
|
96
|
+
state.formattedCount = 0;
|
|
97
|
+
state.cleanCount = 0;
|
|
98
|
+
state.errorCount = 0;
|
|
99
|
+
state.hookUnregister = null;
|
|
100
|
+
state.lastResult = null;
|
|
101
|
+
const cfg = readConfig(api.config.extensions?.["format-on-save"]);
|
|
102
|
+
let biomeAvailable = false;
|
|
103
|
+
try {
|
|
104
|
+
execSync("npx biome --version", {
|
|
105
|
+
encoding: "utf-8",
|
|
106
|
+
timeout: 5e3,
|
|
107
|
+
cwd: process.cwd(),
|
|
108
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
109
|
+
});
|
|
110
|
+
biomeAvailable = true;
|
|
111
|
+
api.log.info("format-on-save: biome detected");
|
|
112
|
+
} catch {
|
|
113
|
+
biomeAvailable = false;
|
|
114
|
+
api.log.warn("format-on-save: biome not found \u2014 hook will be a no-op");
|
|
115
|
+
}
|
|
116
|
+
const hook = (input) => {
|
|
117
|
+
if (!cfg.enabled || !biomeAvailable) return;
|
|
118
|
+
if (input.toolResult?.isError) return;
|
|
119
|
+
const toolName = input.toolName ?? "";
|
|
120
|
+
const inp = input.toolInput ?? {};
|
|
121
|
+
const filePath = inp["path"];
|
|
122
|
+
if (!filePath || typeof filePath !== "string") return;
|
|
123
|
+
state.invocationCount += 1;
|
|
124
|
+
const result = formatFile(filePath, cfg.timeoutMs);
|
|
125
|
+
if (!result) {
|
|
126
|
+
state.errorCount += 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
state.lastResult = {
|
|
130
|
+
path: filePath,
|
|
131
|
+
tool: toolName,
|
|
132
|
+
changed: result.changed,
|
|
133
|
+
bytesBefore: result.bytesBefore,
|
|
134
|
+
bytesAfter: result.bytesAfter,
|
|
135
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
136
|
+
};
|
|
137
|
+
if (result.changed) {
|
|
138
|
+
state.formattedCount += 1;
|
|
139
|
+
const delta = result.bytesAfter - result.bytesBefore;
|
|
140
|
+
api.log.info(`format-on-save: formatted ${filePath}`, {
|
|
141
|
+
tool: toolName,
|
|
142
|
+
delta: `${delta >= 0 ? "+" : ""}${delta} bytes`
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
additionalContext: `
|
|
146
|
+
\u{1F527} format-on-save: applied biome formatting to '${filePath}' after ${toolName}. The file on disk has been reformatted (${delta >= 0 ? "+" : ""}${delta} bytes).`
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
state.cleanCount += 1;
|
|
150
|
+
return;
|
|
151
|
+
};
|
|
152
|
+
state.hookUnregister = api.registerHook("PostToolUse", "write|edit", hook);
|
|
153
|
+
api.tools.register({
|
|
154
|
+
name: "format_on_save_status",
|
|
155
|
+
description: "Reports format-on-save state: biome availability, and per-session formatted/clean/error counters.",
|
|
156
|
+
inputSchema: { type: "object", properties: {} },
|
|
157
|
+
permission: "auto",
|
|
158
|
+
category: "Code Quality",
|
|
159
|
+
mutating: false,
|
|
160
|
+
async execute() {
|
|
161
|
+
return {
|
|
162
|
+
ok: true,
|
|
163
|
+
enabled: cfg.enabled,
|
|
164
|
+
biomeAvailable,
|
|
165
|
+
timeoutMs: cfg.timeoutMs,
|
|
166
|
+
counters: {
|
|
167
|
+
invocations: state.invocationCount,
|
|
168
|
+
formatted: state.formattedCount,
|
|
169
|
+
clean: state.cleanCount,
|
|
170
|
+
errors: state.errorCount
|
|
171
|
+
},
|
|
172
|
+
lastResult: state.lastResult
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
api.log.info("format-on-save plugin loaded", {
|
|
177
|
+
version: "0.1.0",
|
|
178
|
+
enabled: cfg.enabled,
|
|
179
|
+
biomeAvailable
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
teardown(api) {
|
|
183
|
+
if (state.hookUnregister) {
|
|
184
|
+
try {
|
|
185
|
+
state.hookUnregister();
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
state.hookUnregister = null;
|
|
189
|
+
}
|
|
190
|
+
const final = {
|
|
191
|
+
invocations: state.invocationCount,
|
|
192
|
+
formatted: state.formattedCount,
|
|
193
|
+
clean: state.cleanCount,
|
|
194
|
+
errors: state.errorCount
|
|
195
|
+
};
|
|
196
|
+
state.invocationCount = 0;
|
|
197
|
+
state.formattedCount = 0;
|
|
198
|
+
state.cleanCount = 0;
|
|
199
|
+
state.errorCount = 0;
|
|
200
|
+
state.lastResult = null;
|
|
201
|
+
api.log.info("format-on-save: teardown complete", { final });
|
|
202
|
+
},
|
|
203
|
+
async health() {
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
message: state.lastResult === null ? `format-on-save: ${state.invocationCount} invocation(s), ${state.formattedCount} formatted` : state.lastResult.changed ? `format-on-save: last formatted ${state.lastResult.path} (${state.lastResult.tool}) at ${state.lastResult.when}` : `format-on-save: last check on ${state.lastResult.path} was already clean`,
|
|
207
|
+
counters: {
|
|
208
|
+
invocations: state.invocationCount,
|
|
209
|
+
formatted: state.formattedCount,
|
|
210
|
+
clean: state.cleanCount,
|
|
211
|
+
errors: state.errorCount
|
|
212
|
+
},
|
|
213
|
+
lastResult: state.lastResult
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
var format_on_save_default = plugin;
|
|
218
|
+
|
|
219
|
+
export { format_on_save_default as default };
|
package/dist/git-autocommit.js
CHANGED
|
@@ -3,6 +3,9 @@ import { existsSync } from 'fs';
|
|
|
3
3
|
|
|
4
4
|
// src/git-autocommit/index.ts
|
|
5
5
|
var API_VERSION = "^0.1.10";
|
|
6
|
+
var commitCount = { value: 0 };
|
|
7
|
+
var lastCommit = { hash: null, at: null };
|
|
8
|
+
var llmGenerated = { value: 0 };
|
|
6
9
|
function runGit(args, cwd) {
|
|
7
10
|
try {
|
|
8
11
|
return execFileSync("git", args, {
|
|
@@ -103,6 +106,54 @@ function generateCommitMessage(type, scope, summary, body) {
|
|
|
103
106
|
${body}` : "";
|
|
104
107
|
return `${type}${scopePart}: ${summary}${footer}`;
|
|
105
108
|
}
|
|
109
|
+
var VALID_TYPES = [
|
|
110
|
+
"feat",
|
|
111
|
+
"fix",
|
|
112
|
+
"docs",
|
|
113
|
+
"style",
|
|
114
|
+
"refactor",
|
|
115
|
+
"test",
|
|
116
|
+
"chore",
|
|
117
|
+
"perf",
|
|
118
|
+
"ci",
|
|
119
|
+
"build",
|
|
120
|
+
"revert"
|
|
121
|
+
];
|
|
122
|
+
async function generateCommitFromDiff(api, stat, diff) {
|
|
123
|
+
if (!api.llm) return null;
|
|
124
|
+
try {
|
|
125
|
+
const result = await api.llm.complete(
|
|
126
|
+
`Write a Conventional Commits message for this staged git diff. Respond with ONLY a JSON object of the form {"type": string, "scope": string, "summary": string, "body": string}. type is one of: ${VALID_TYPES.join(", ")}. scope is a short area (empty string if unclear). summary is an imperative, lower-case, <=72-char subject with no trailing period. body is an optional short explanation (empty string if not needed). No prose outside the JSON.
|
|
127
|
+
|
|
128
|
+
Stat:
|
|
129
|
+
${stat}
|
|
130
|
+
|
|
131
|
+
Diff:
|
|
132
|
+
${diff}`,
|
|
133
|
+
{
|
|
134
|
+
system: "You are a precise release engineer writing Conventional Commits. Output only JSON.",
|
|
135
|
+
maxTokens: 400,
|
|
136
|
+
responseFormat: "json"
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
const parsed = JSON.parse(extractJsonObject(result.text));
|
|
140
|
+
const type = VALID_TYPES.includes(parsed.type) ? parsed.type : null;
|
|
141
|
+
const summary = typeof parsed.summary === "string" && parsed.summary.trim() ? parsed.summary.trim() : null;
|
|
142
|
+
if (!type || !summary) return null;
|
|
143
|
+
const scope = typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : void 0;
|
|
144
|
+
const body = typeof parsed.body === "string" && parsed.body.trim() ? parsed.body.trim() : void 0;
|
|
145
|
+
return { type, summary, ...scope ? { scope } : {}, ...body ? { body } : {} };
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function extractJsonObject(text) {
|
|
151
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(text);
|
|
152
|
+
const body = fenced?.[1] ?? text;
|
|
153
|
+
const start = body.indexOf("{");
|
|
154
|
+
const end = body.lastIndexOf("}");
|
|
155
|
+
return start >= 0 && end > start ? body.slice(start, end + 1) : body.trim();
|
|
156
|
+
}
|
|
106
157
|
var plugin = {
|
|
107
158
|
name: "git-autocommit",
|
|
108
159
|
version: "0.2.0",
|
|
@@ -112,22 +163,40 @@ var plugin = {
|
|
|
112
163
|
defaultConfig: {
|
|
113
164
|
conventionalCommits: true,
|
|
114
165
|
autoStage: false,
|
|
115
|
-
defaultType: "feat"
|
|
166
|
+
defaultType: "feat",
|
|
167
|
+
useLlm: false
|
|
116
168
|
},
|
|
117
169
|
configSchema: {
|
|
118
170
|
type: "object",
|
|
119
171
|
properties: {
|
|
120
172
|
conventionalCommits: { type: "boolean", default: true },
|
|
121
173
|
autoStage: { type: "boolean", default: false },
|
|
122
|
-
defaultType: { type: "string", default: "feat" }
|
|
174
|
+
defaultType: { type: "string", default: "feat" },
|
|
175
|
+
useLlm: {
|
|
176
|
+
type: "boolean",
|
|
177
|
+
default: false,
|
|
178
|
+
description: 'Auto-generate the commit message with the LLM (api.llm) when the caller supplies neither type nor message. Provider/model follow extensions["git-autocommit"].llm, then the session default.'
|
|
179
|
+
},
|
|
180
|
+
llm: {
|
|
181
|
+
type: "object",
|
|
182
|
+
description: "Optional { provider, model } override for LLM commit messages."
|
|
183
|
+
}
|
|
123
184
|
}
|
|
124
185
|
},
|
|
125
186
|
setup(api) {
|
|
187
|
+
commitCount.value = 0;
|
|
188
|
+
llmGenerated.value = 0;
|
|
189
|
+
lastCommit.hash = null;
|
|
190
|
+
lastCommit.at = null;
|
|
126
191
|
const extConfig = api.config.extensions?.["git-autocommit"];
|
|
127
192
|
const opts = {
|
|
128
193
|
conventionalCommits: extConfig?.["conventionalCommits"] ?? true,
|
|
129
194
|
autoStage: extConfig?.["autoStage"] ?? false,
|
|
130
|
-
defaultType: extConfig?.["defaultType"] ?? "feat"
|
|
195
|
+
defaultType: extConfig?.["defaultType"] ?? "feat",
|
|
196
|
+
// Opt-in: when true, git_autocommit writes the commit message with
|
|
197
|
+
// the LLM from the staged diff whenever the caller supplies neither
|
|
198
|
+
// `type` nor `message` (an explicit `generate: true` always asks).
|
|
199
|
+
useLlm: extConfig?.["useLlm"] ?? false
|
|
131
200
|
};
|
|
132
201
|
api.tools.register({
|
|
133
202
|
name: "git_autocommit",
|
|
@@ -142,13 +211,33 @@ var plugin = {
|
|
|
142
211
|
},
|
|
143
212
|
type: {
|
|
144
213
|
type: "string",
|
|
145
|
-
enum: [
|
|
214
|
+
enum: [
|
|
215
|
+
"feat",
|
|
216
|
+
"fix",
|
|
217
|
+
"docs",
|
|
218
|
+
"style",
|
|
219
|
+
"refactor",
|
|
220
|
+
"test",
|
|
221
|
+
"chore",
|
|
222
|
+
"perf",
|
|
223
|
+
"ci",
|
|
224
|
+
"build",
|
|
225
|
+
"revert"
|
|
226
|
+
],
|
|
146
227
|
description: "Conventional commit type"
|
|
147
228
|
},
|
|
148
229
|
scope: { type: "string", description: "Commit scope (e.g. auth, api, ui)" },
|
|
149
230
|
message: { type: "string", description: "Commit summary message" },
|
|
150
231
|
body: { type: "string", description: "Optional commit body/description" },
|
|
151
|
-
|
|
232
|
+
generate: {
|
|
233
|
+
type: "boolean",
|
|
234
|
+
description: "Write the conventional commit message with the LLM (api.llm) from the staged diff. Ignored when no LLM is wired."
|
|
235
|
+
},
|
|
236
|
+
dry_run: {
|
|
237
|
+
type: "boolean",
|
|
238
|
+
default: false,
|
|
239
|
+
description: "Show what would be committed without committing"
|
|
240
|
+
}
|
|
152
241
|
}
|
|
153
242
|
},
|
|
154
243
|
permission: "confirm",
|
|
@@ -156,23 +245,14 @@ var plugin = {
|
|
|
156
245
|
mutating: true,
|
|
157
246
|
async execute(input, _ctx) {
|
|
158
247
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
248
|
+
let type = input["type"];
|
|
249
|
+
let scope = input["scope"];
|
|
250
|
+
let summary = input["message"] ?? "";
|
|
251
|
+
let body = input["body"];
|
|
163
252
|
const dryRun = input["dry_run"] ?? false;
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
ok: true,
|
|
169
|
-
dry_run: true,
|
|
170
|
-
message: `Would create: ${summary || "update code"}`
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
return { ok: false, error: "type is required and must be a valid conventional commit type" };
|
|
174
|
-
}
|
|
175
|
-
const msg = generateCommitMessage(type, scope, summary || "update code", body);
|
|
253
|
+
const explicitAsk = input["generate"] === true;
|
|
254
|
+
const autoAsk = opts.useLlm && !input["type"] && !input["message"];
|
|
255
|
+
const wantGenerate = (explicitAsk || autoAsk) && Boolean(api.llm);
|
|
176
256
|
let files;
|
|
177
257
|
const rawFiles = input["files"];
|
|
178
258
|
if (rawFiles !== void 0) {
|
|
@@ -185,7 +265,10 @@ var plugin = {
|
|
|
185
265
|
try {
|
|
186
266
|
stageFiles(files);
|
|
187
267
|
} catch (err) {
|
|
188
|
-
return {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
error: `Failed to stage files: ${err instanceof Error ? err.message : String(err)}`
|
|
271
|
+
};
|
|
189
272
|
}
|
|
190
273
|
}
|
|
191
274
|
let staged = [];
|
|
@@ -211,8 +294,51 @@ var plugin = {
|
|
|
211
294
|
} catch {
|
|
212
295
|
}
|
|
213
296
|
}
|
|
297
|
+
const { stat, diff: stagedDiff } = getStagedDiff();
|
|
298
|
+
let generatedByLlm = false;
|
|
299
|
+
if (wantGenerate && staged.length > 0) {
|
|
300
|
+
const g = await generateCommitFromDiff(api, stat, stagedDiff);
|
|
301
|
+
if (g) {
|
|
302
|
+
type = g.type;
|
|
303
|
+
if (g.scope) scope = g.scope;
|
|
304
|
+
summary = g.summary;
|
|
305
|
+
if (g.body && !body) body = g.body;
|
|
306
|
+
generatedByLlm = true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (!type) type = opts.defaultType;
|
|
310
|
+
const validTypes = [
|
|
311
|
+
"feat",
|
|
312
|
+
"fix",
|
|
313
|
+
"docs",
|
|
314
|
+
"style",
|
|
315
|
+
"refactor",
|
|
316
|
+
"test",
|
|
317
|
+
"chore",
|
|
318
|
+
"perf",
|
|
319
|
+
"ci",
|
|
320
|
+
"build",
|
|
321
|
+
"revert"
|
|
322
|
+
];
|
|
323
|
+
if (!type || !validTypes.includes(type)) {
|
|
324
|
+
if (dryRun) {
|
|
325
|
+
return {
|
|
326
|
+
ok: true,
|
|
327
|
+
dry_run: true,
|
|
328
|
+
message: `Would create: ${summary || "update code"}`
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
error: "type is required and must be a valid conventional commit type"
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const msg = generateCommitMessage(type, scope, summary || "update code", body);
|
|
214
337
|
if (staged.length === 0) {
|
|
215
|
-
return {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
error: "Nothing staged. Add files with git add or provide files input."
|
|
341
|
+
};
|
|
216
342
|
}
|
|
217
343
|
const worktreeWarn = simultaneousEditWarning();
|
|
218
344
|
const externalChanges = externalChangesSinceStage();
|
|
@@ -223,7 +349,6 @@ var plugin = {
|
|
|
223
349
|
externalWarning = `\u26A0 External changes detected since staging: ${preview}${suffix}. Another agent may be modifying files concurrently. These unstaged changes will NOT be included in this commit, but they indicate simultaneous edits. Review carefully.`;
|
|
224
350
|
}
|
|
225
351
|
const warning = [worktreeWarn, externalWarning].filter(Boolean).join("\n") || void 0;
|
|
226
|
-
const { stat, diff: stagedDiff } = getStagedDiff();
|
|
227
352
|
if (dryRun) {
|
|
228
353
|
return {
|
|
229
354
|
ok: true,
|
|
@@ -251,9 +376,16 @@ ${stagedDiff}
|
|
|
251
376
|
try {
|
|
252
377
|
hash = commitWithMessage(msg);
|
|
253
378
|
} catch (err) {
|
|
254
|
-
return {
|
|
379
|
+
return {
|
|
380
|
+
ok: false,
|
|
381
|
+
error: `Failed to commit: ${err instanceof Error ? err.message : String(err)}`
|
|
382
|
+
};
|
|
255
383
|
}
|
|
256
384
|
api.log.info("git-autocommit: created commit", { hash, type, scope });
|
|
385
|
+
commitCount.value += 1;
|
|
386
|
+
if (generatedByLlm) llmGenerated.value += 1;
|
|
387
|
+
lastCommit.hash = String(hash);
|
|
388
|
+
lastCommit.at = (/* @__PURE__ */ new Date()).toISOString();
|
|
257
389
|
try {
|
|
258
390
|
await api.session.append({
|
|
259
391
|
type: "git-autocommit:commit",
|
|
@@ -274,6 +406,7 @@ ${stagedDiff}
|
|
|
274
406
|
stagedFiles: staged,
|
|
275
407
|
type,
|
|
276
408
|
scope: scope ?? null,
|
|
409
|
+
generatedByLlm,
|
|
277
410
|
warning: warning ?? void 0,
|
|
278
411
|
diff: `
|
|
279
412
|
## Staged diff
|
|
@@ -285,7 +418,10 @@ ${preCommitDiff}
|
|
|
285
418
|
\`\`\``
|
|
286
419
|
};
|
|
287
420
|
} catch (err) {
|
|
288
|
-
return {
|
|
421
|
+
return {
|
|
422
|
+
ok: false,
|
|
423
|
+
error: `Uncaught error in git_autocommit: ${err instanceof Error ? err.message : String(err)}`
|
|
424
|
+
};
|
|
289
425
|
}
|
|
290
426
|
}
|
|
291
427
|
});
|
|
@@ -293,6 +429,30 @@ ${preCommitDiff}
|
|
|
293
429
|
version: "0.2.0",
|
|
294
430
|
conventionalCommits: opts.conventionalCommits
|
|
295
431
|
});
|
|
432
|
+
},
|
|
433
|
+
teardown(api) {
|
|
434
|
+
const finalCount = commitCount.value;
|
|
435
|
+
const finalHash = lastCommit.hash;
|
|
436
|
+
const finalLlm = llmGenerated.value;
|
|
437
|
+
commitCount.value = 0;
|
|
438
|
+
llmGenerated.value = 0;
|
|
439
|
+
lastCommit.hash = null;
|
|
440
|
+
lastCommit.at = null;
|
|
441
|
+
api.log.info("git-autocommit: teardown complete", {
|
|
442
|
+
commits: finalCount,
|
|
443
|
+
llmGenerated: finalLlm,
|
|
444
|
+
lastHash: finalHash
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
async health() {
|
|
448
|
+
return {
|
|
449
|
+
ok: true,
|
|
450
|
+
message: commitCount.value === 0 ? "git-autocommit: no commits yet this session" : `git-autocommit: ${commitCount.value} commit(s) (${llmGenerated.value} LLM-written), last ${String(lastCommit.hash).slice(0, 8)} at ${lastCommit.at}`,
|
|
451
|
+
commits: commitCount.value,
|
|
452
|
+
llmGenerated: llmGenerated.value,
|
|
453
|
+
lastCommitHash: lastCommit.hash,
|
|
454
|
+
lastCommitAt: lastCommit.at
|
|
455
|
+
};
|
|
296
456
|
}
|
|
297
457
|
};
|
|
298
458
|
var git_autocommit_default = plugin;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Plugin } from '@wrongstack/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* import-organizer plugin — PostToolUse hook that re-sorts and
|
|
5
|
+
* de-duplicates imports in a file after every `write` or `edit`.
|
|
6
|
+
*
|
|
7
|
+
* This is a heavier, post-write step than `format-on-save` (which only
|
|
8
|
+
* handles whitespace/formatting). It runs `biome check --write --unsafe`
|
|
9
|
+
* (or `eslint --fix` as a fallback) on the saved file. The `--unsafe`
|
|
10
|
+
* flag enables import-organization rules:
|
|
11
|
+
* - Sort imports alphabetically within import groups
|
|
12
|
+
* - Group by source (builtin, external, internal, relative)
|
|
13
|
+
* - Remove unused imports
|
|
14
|
+
* - Merge duplicate imports from the same module
|
|
15
|
+
*
|
|
16
|
+
* Tools registered:
|
|
17
|
+
* - import_organizer_status : Show config + per-session counters
|
|
18
|
+
* (invocations / organized / clean / errors + lastResult).
|
|
19
|
+
*
|
|
20
|
+
* Hooks registered:
|
|
21
|
+
* - PostToolUse with matcher `write|edit`. After the tool completes,
|
|
22
|
+
* runs the configured command on the file on disk. The hook reads
|
|
23
|
+
* the file fresh from disk (so `edit` tool's post-edit state is
|
|
24
|
+
* captured) and detects whether the file changed via byte-count
|
|
25
|
+
* comparison. If the file was modified, returns `additionalContext`
|
|
26
|
+
* so the LLM sees that imports were reorganized.
|
|
27
|
+
*
|
|
28
|
+
* Linter detection is lazy: on the first hook invocation, the plugin
|
|
29
|
+
* tries `biome` first (since `--unsafe` is required for import
|
|
30
|
+
* organization), then falls back to `eslint --fix`. If neither
|
|
31
|
+
* succeeds, the hook logs a one-time warning and becomes a no-op for
|
|
32
|
+
* the rest of the session. Linter presence is re-checked on every
|
|
33
|
+
* setup() call so plugin reload can recover if a linter is installed
|
|
34
|
+
* mid-session.
|
|
35
|
+
*
|
|
36
|
+
* Config (`config.extensions['import-organizer']`):
|
|
37
|
+
*
|
|
38
|
+
* ```jsonc
|
|
39
|
+
* {
|
|
40
|
+
* "enabled": true,
|
|
41
|
+
* "command": "npx @biomejs/biome check --write --unsafe",
|
|
42
|
+
* "fallbackCommand": "npx eslint --fix",
|
|
43
|
+
* "timeoutMs": 10000
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
declare const plugin: Plugin;
|
|
51
|
+
|
|
52
|
+
export { plugin as default };
|