@wrongstack/plugins 0.280.1 → 0.281.1
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 +325 -34
- package/dist/auto-doc.js +22 -7
- package/dist/branch-guard.js +18 -4
- package/dist/changelog-writer.js +12 -1
- package/dist/checkpoint.js +25 -3
- package/dist/context-pins.js +11 -2
- package/dist/git-autocommit.js +5 -3
- package/dist/index.js +232 -67
- package/dist/notify-hub.js +26 -1
- package/dist/semver-bump.js +43 -9
- package/dist/spec-linker.js +24 -3
- package/dist/template-engine.js +44 -29
- package/package.json +3 -3
package/dist/notify-hub.js
CHANGED
|
@@ -18,6 +18,31 @@ var DEFAULTS = {
|
|
|
18
18
|
timeoutMs: 5e3,
|
|
19
19
|
maxConsecutiveFailures: 5
|
|
20
20
|
};
|
|
21
|
+
function isPrivateIPv4(hostname) {
|
|
22
|
+
const parts = hostname.split(".").map((p) => Number(p));
|
|
23
|
+
if (parts.length !== 4 || parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const [a, b] = parts;
|
|
27
|
+
return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
28
|
+
}
|
|
29
|
+
function isBlockedHostname(hostname) {
|
|
30
|
+
const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
31
|
+
return h === "localhost" || h.endsWith(".localhost") || h === "::1" || h === "0:0:0:0:0:0:0:1" || h.startsWith("fc") || h.startsWith("fd") || h.startsWith("fe80:") || isPrivateIPv4(h);
|
|
32
|
+
}
|
|
33
|
+
function normalizeWebhookUrl(raw) {
|
|
34
|
+
if (typeof raw !== "string" || raw.trim().length === 0) return "";
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(raw.trim());
|
|
37
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return "";
|
|
38
|
+
if (url.username || url.password) return "";
|
|
39
|
+
if (!url.hostname || isBlockedHostname(url.hostname)) return "";
|
|
40
|
+
url.hash = "";
|
|
41
|
+
return url.toString();
|
|
42
|
+
} catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
21
46
|
function readConfig(raw) {
|
|
22
47
|
if (!raw || typeof raw !== "object") return { ...DEFAULTS, events: [...DEFAULTS.events] };
|
|
23
48
|
const r = raw;
|
|
@@ -29,7 +54,7 @@ function readConfig(raw) {
|
|
|
29
54
|
}
|
|
30
55
|
return {
|
|
31
56
|
enabled: r["enabled"] !== false,
|
|
32
|
-
webhookUrl:
|
|
57
|
+
webhookUrl: normalizeWebhookUrl(r["webhookUrl"]),
|
|
33
58
|
events: Array.isArray(r["events"]) ? r["events"].filter((e) => KNOWN_EVENTS.includes(e)) : [...DEFAULTS.events],
|
|
34
59
|
headers,
|
|
35
60
|
timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] >= 500 && r["timeoutMs"] <= 6e4 ? r["timeoutMs"] : DEFAULTS.timeoutMs,
|
package/dist/semver-bump.js
CHANGED
|
@@ -2,10 +2,18 @@ import { expectDefined } from '@wrongstack/core';
|
|
|
2
2
|
import { toErrorMessage } from '@wrongstack/core/utils';
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
5
|
+
import { join, resolve, isAbsolute, relative } from 'path';
|
|
6
6
|
|
|
7
7
|
// src/semver-bump/index.ts
|
|
8
8
|
var API_VERSION = "^0.1.10";
|
|
9
|
+
function resolveProjectRoot(rawCwd, root = process.cwd()) {
|
|
10
|
+
if (typeof rawCwd !== "string" || rawCwd.length === 0) return root;
|
|
11
|
+
const base = resolve(root);
|
|
12
|
+
const resolved = isAbsolute(rawCwd) ? resolve(rawCwd) : resolve(base, rawCwd);
|
|
13
|
+
const rel = relative(base, resolved);
|
|
14
|
+
if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return resolved;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
9
17
|
var state = {
|
|
10
18
|
/** Total invocations across all three tools this session. */
|
|
11
19
|
invocationCount: 0,
|
|
@@ -56,7 +64,11 @@ function collectManifests(root) {
|
|
|
56
64
|
function parseVersion(v) {
|
|
57
65
|
const m = v.match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
58
66
|
if (!m) return [0, 0, 0];
|
|
59
|
-
return [
|
|
67
|
+
return [
|
|
68
|
+
Number.parseInt(expectDefined(m[1]), 10),
|
|
69
|
+
Number.parseInt(expectDefined(m[2]), 10),
|
|
70
|
+
Number.parseInt(expectDefined(m[3]), 10)
|
|
71
|
+
];
|
|
60
72
|
}
|
|
61
73
|
function bumpVersion(version, part) {
|
|
62
74
|
let [major, minor, patch] = parseVersion(version);
|
|
@@ -192,6 +204,11 @@ var plugin = {
|
|
|
192
204
|
defaultPart = readDefaultPart(next);
|
|
193
205
|
});
|
|
194
206
|
async function performBump(part, dryRun, cwd) {
|
|
207
|
+
const safeCwd = resolveProjectRoot(cwd);
|
|
208
|
+
if (!safeCwd) {
|
|
209
|
+
return { ok: false, error: "cwd must stay within the current project directory" };
|
|
210
|
+
}
|
|
211
|
+
cwd = safeCwd;
|
|
195
212
|
const pkg = getPackageJson(cwd);
|
|
196
213
|
if (!pkg) {
|
|
197
214
|
return { ok: false, error: "No package.json found" };
|
|
@@ -299,7 +316,12 @@ var plugin = {
|
|
|
299
316
|
properties: {
|
|
300
317
|
cwd: { type: "string", description: "Working directory (defaults to project root)" },
|
|
301
318
|
dry_run: { type: "boolean", default: false },
|
|
302
|
-
part: {
|
|
319
|
+
part: {
|
|
320
|
+
type: "string",
|
|
321
|
+
enum: ["major", "minor", "patch", "auto"],
|
|
322
|
+
default: defaultPart,
|
|
323
|
+
description: "Version part to bump. Omitted \u2192 the configured default (/settings semver-part, factory default: patch). Use auto to infer from commits."
|
|
324
|
+
}
|
|
303
325
|
}
|
|
304
326
|
},
|
|
305
327
|
permission: "confirm",
|
|
@@ -363,7 +385,11 @@ var plugin = {
|
|
|
363
385
|
if (mode !== "patch" && mode !== "minor" && mode !== "major" && mode !== "auto") {
|
|
364
386
|
return { message: `Unknown mode "${mode}". Use status, patch, minor, major or auto.` };
|
|
365
387
|
}
|
|
366
|
-
const
|
|
388
|
+
const safeCwd = resolveProjectRoot(cwd);
|
|
389
|
+
if (!safeCwd) {
|
|
390
|
+
return { message: "cwd must stay within the current project directory" };
|
|
391
|
+
}
|
|
392
|
+
const result = await performBump(mode, dry, safeCwd);
|
|
367
393
|
return { message: String(result["message"] ?? result["error"] ?? JSON.stringify(result)) };
|
|
368
394
|
}
|
|
369
395
|
});
|
|
@@ -381,16 +407,20 @@ var plugin = {
|
|
|
381
407
|
async execute(input) {
|
|
382
408
|
state.invocationCount += 1;
|
|
383
409
|
state.perTool["semver_current"] = (state.perTool["semver_current"] ?? 0) + 1;
|
|
384
|
-
const
|
|
385
|
-
const
|
|
410
|
+
const cwdInput = input["cwd"];
|
|
411
|
+
const safeCwd = resolveProjectRoot(cwdInput);
|
|
412
|
+
if (!safeCwd) {
|
|
413
|
+
return { ok: false, error: "cwd must stay within the current project directory" };
|
|
414
|
+
}
|
|
415
|
+
const pkg = getPackageJson(safeCwd);
|
|
386
416
|
const currentVersion = pkg?.version ?? "unknown";
|
|
387
417
|
let latestTag = null;
|
|
388
418
|
let commitsSinceTag = 0;
|
|
389
419
|
try {
|
|
390
|
-
const tagsOutput = runGit(["describe", "--tags", "--abbrev=0"],
|
|
420
|
+
const tagsOutput = runGit(["describe", "--tags", "--abbrev=0"], safeCwd);
|
|
391
421
|
latestTag = tagsOutput || null;
|
|
392
422
|
if (latestTag) {
|
|
393
|
-
const countOutput = runGit(["rev-list", "--count", `${latestTag}..HEAD`],
|
|
423
|
+
const countOutput = runGit(["rev-list", "--count", `${latestTag}..HEAD`], safeCwd);
|
|
394
424
|
commitsSinceTag = Number.parseInt(countOutput, 10) || 0;
|
|
395
425
|
}
|
|
396
426
|
} catch {
|
|
@@ -425,11 +455,15 @@ var plugin = {
|
|
|
425
455
|
const from = input["from"];
|
|
426
456
|
const to = input["to"] ?? "HEAD";
|
|
427
457
|
const cwd = input["cwd"];
|
|
458
|
+
const safeCwd = resolveProjectRoot(cwd);
|
|
459
|
+
if (!safeCwd) {
|
|
460
|
+
return { ok: false, error: "cwd must stay within the current project directory" };
|
|
461
|
+
}
|
|
428
462
|
const format = input["format"] ?? "markdown";
|
|
429
463
|
const range = from ? `${from}..${to}` : to;
|
|
430
464
|
let commits;
|
|
431
465
|
try {
|
|
432
|
-
const output = runGit(["log", range === to ? "-30" : range, "--format=%H %s"],
|
|
466
|
+
const output = runGit(["log", range === to ? "-30" : range, "--format=%H %s"], safeCwd);
|
|
433
467
|
commits = output.split("\n").filter(Boolean).map((line) => {
|
|
434
468
|
const spaceIdx = line.indexOf(" ");
|
|
435
469
|
const hash = line.slice(0, spaceIdx);
|
package/dist/spec-linker.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'fs';
|
|
2
2
|
import * as fsp from 'fs/promises';
|
|
3
|
-
import 'child_process';
|
|
4
3
|
import 'path';
|
|
4
|
+
import 'child_process';
|
|
5
5
|
import '@wrongstack/core';
|
|
6
6
|
import 'os';
|
|
7
7
|
import 'crypto';
|
|
@@ -292,9 +292,26 @@ function isWrappedAsLinkOrCode(line, name) {
|
|
|
292
292
|
function escapeRegExp(s) {
|
|
293
293
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
294
294
|
}
|
|
295
|
+
function mapMarkdownFences(lines) {
|
|
296
|
+
const fenced = new Array(lines.length).fill(false);
|
|
297
|
+
let inFence = false;
|
|
298
|
+
for (let i = 0; i < lines.length; i++) {
|
|
299
|
+
const line = lines[i];
|
|
300
|
+
if (/^\s*```/.test(line)) {
|
|
301
|
+
fenced[i] = true;
|
|
302
|
+
inFence = !inFence;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
fenced[i] = inFence;
|
|
306
|
+
}
|
|
307
|
+
return fenced;
|
|
308
|
+
}
|
|
295
309
|
function findUnlinkedReferences(lines, names) {
|
|
296
310
|
const found = /* @__PURE__ */ new Map();
|
|
297
|
-
|
|
311
|
+
const fencedLines = mapMarkdownFences(lines);
|
|
312
|
+
for (let i = 0; i < lines.length; i++) {
|
|
313
|
+
if (fencedLines[i]) continue;
|
|
314
|
+
const line = lines[i];
|
|
298
315
|
if (line.length === 0) continue;
|
|
299
316
|
for (const name of names) {
|
|
300
317
|
const re = new RegExp(`(^|[^\\w-])${escapeRegExp(name)}(?![\\w-])`, "i");
|
|
@@ -307,8 +324,10 @@ function findUnlinkedReferences(lines, names) {
|
|
|
307
324
|
}
|
|
308
325
|
function wrapUnlinkedReferences(content) {
|
|
309
326
|
const lines = content.split("\n");
|
|
327
|
+
const fencedLines = mapMarkdownFences(lines);
|
|
310
328
|
let changed = false;
|
|
311
329
|
for (let i = 0; i < lines.length; i++) {
|
|
330
|
+
if (fencedLines[i]) continue;
|
|
312
331
|
const line = lines[i];
|
|
313
332
|
if (line.length === 0) continue;
|
|
314
333
|
const newLine = wrapLineReferences(line);
|
|
@@ -424,7 +443,9 @@ var plugin36 = {
|
|
|
424
443
|
state32.unlinkedCount += 1;
|
|
425
444
|
const limited = unlinked.slice(0, cfg.maxReferences);
|
|
426
445
|
const overflow = unlinked.length - limited.length;
|
|
427
|
-
const lines = limited.map(
|
|
446
|
+
const lines = limited.map(
|
|
447
|
+
(name) => `- \`${name}\` \u2192 \`[${name}](${PLUGIN_CATALOG.get(name) ?? `./src/${name}`})\``
|
|
448
|
+
).join("\n");
|
|
428
449
|
const overflowNote = overflow > 0 ? `
|
|
429
450
|
- \u2026and ${overflow} more` : "";
|
|
430
451
|
return {
|
package/dist/template-engine.js
CHANGED
|
@@ -13,27 +13,21 @@ function expandTemplate(template, variables) {
|
|
|
13
13
|
return result;
|
|
14
14
|
}
|
|
15
15
|
function expandConditionals(template, variables) {
|
|
16
|
-
return template.replace(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return val !== void 0 && val !== "" && val !== "false" && val !== "0" ? content : "";
|
|
21
|
-
}
|
|
22
|
-
);
|
|
16
|
+
return template.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, key, content) => {
|
|
17
|
+
const val = variables[key];
|
|
18
|
+
return val !== void 0 && val !== "" && val !== "false" && val !== "0" ? content : "";
|
|
19
|
+
});
|
|
23
20
|
}
|
|
24
21
|
function expandLoops(template, variables) {
|
|
25
|
-
return template.replace(
|
|
26
|
-
|
|
27
|
-
(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const items = val.split(",").map((s) => s.trim());
|
|
32
|
-
return items.map((item) => expandTemplate(content, { ...variables, [key]: item })).join("\n");
|
|
33
|
-
}
|
|
34
|
-
return expandTemplate(content, variables);
|
|
22
|
+
return template.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_, key, content) => {
|
|
23
|
+
const val = variables[key];
|
|
24
|
+
if (!val) return "";
|
|
25
|
+
if (typeof val === "string" && val.includes(",")) {
|
|
26
|
+
const items = val.split(",").map((s) => s.trim());
|
|
27
|
+
return items.map((item) => expandTemplate(content, { ...variables, [key]: item })).join("\n");
|
|
35
28
|
}
|
|
36
|
-
|
|
29
|
+
return expandTemplate(content, variables);
|
|
30
|
+
});
|
|
37
31
|
}
|
|
38
32
|
function renderTemplate(template, variables, escapeHtml = true) {
|
|
39
33
|
let result = template;
|
|
@@ -52,6 +46,12 @@ function renderTemplateRaw(template, variables) {
|
|
|
52
46
|
result = expandTemplate(result, variables);
|
|
53
47
|
return result;
|
|
54
48
|
}
|
|
49
|
+
function validateRelativeTemplatePath(field, value) {
|
|
50
|
+
if (isAbsolute(value) || value.split(/[\\/]+/).includes("..")) {
|
|
51
|
+
return `${field} must be a relative path without ".." components`;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
55
|
var plugin = {
|
|
56
56
|
name: "template-engine",
|
|
57
57
|
version: "0.1.0",
|
|
@@ -80,13 +80,19 @@ var plugin = {
|
|
|
80
80
|
inputSchema: {
|
|
81
81
|
type: "object",
|
|
82
82
|
properties: {
|
|
83
|
-
template: {
|
|
83
|
+
template: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Template string with {{variable}} placeholders"
|
|
86
|
+
},
|
|
84
87
|
variables: {
|
|
85
88
|
type: "object",
|
|
86
89
|
description: "Variables to substitute into the template",
|
|
87
90
|
additionalProperties: { type: "string" }
|
|
88
91
|
},
|
|
89
|
-
output_path: {
|
|
92
|
+
output_path: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Optional path to write the expanded result"
|
|
95
|
+
},
|
|
90
96
|
raw: { type: "boolean", default: false, description: "Disable HTML auto-escaping" }
|
|
91
97
|
},
|
|
92
98
|
required: ["template", "variables"]
|
|
@@ -112,9 +118,8 @@ var plugin = {
|
|
|
112
118
|
return { ok: false, error: String(err) };
|
|
113
119
|
}
|
|
114
120
|
if (output_path) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
121
|
+
const pathError = validateRelativeTemplatePath("output_path", output_path);
|
|
122
|
+
if (pathError) return { ok: false, error: pathError };
|
|
118
123
|
const { writeFileSync } = await import('fs');
|
|
119
124
|
writeFileSync(output_path, result, "utf-8");
|
|
120
125
|
return {
|
|
@@ -144,7 +149,10 @@ var plugin = {
|
|
|
144
149
|
description: "Variables to substitute",
|
|
145
150
|
additionalProperties: { type: "string" }
|
|
146
151
|
},
|
|
147
|
-
output_path: {
|
|
152
|
+
output_path: {
|
|
153
|
+
type: "string",
|
|
154
|
+
description: "Optional path to write the rendered result"
|
|
155
|
+
},
|
|
148
156
|
raw: { type: "boolean", default: false }
|
|
149
157
|
},
|
|
150
158
|
required: ["template_path", "variables"]
|
|
@@ -159,6 +167,8 @@ var plugin = {
|
|
|
159
167
|
if (!template_path || typeof template_path !== "string") {
|
|
160
168
|
return { ok: false, error: "template_path is required and must be a string" };
|
|
161
169
|
}
|
|
170
|
+
const templatePathError = validateRelativeTemplatePath("template_path", template_path);
|
|
171
|
+
if (templatePathError) return { ok: false, error: templatePathError };
|
|
162
172
|
if (!variables || typeof variables !== "object") {
|
|
163
173
|
return { ok: false, error: "variables is required and must be an object" };
|
|
164
174
|
}
|
|
@@ -176,9 +186,8 @@ var plugin = {
|
|
|
176
186
|
return { ok: false, error: `Template rendering failed: ${err}` };
|
|
177
187
|
}
|
|
178
188
|
if (output_path) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
189
|
+
const pathError = validateRelativeTemplatePath("output_path", output_path);
|
|
190
|
+
if (pathError) return { ok: false, error: pathError };
|
|
182
191
|
const { writeFileSync } = await import('fs');
|
|
183
192
|
writeFileSync(output_path, result, "utf-8");
|
|
184
193
|
return {
|
|
@@ -203,8 +212,14 @@ var plugin = {
|
|
|
203
212
|
type: "object",
|
|
204
213
|
properties: {
|
|
205
214
|
name: { type: "string", description: "Unique name for this template" },
|
|
206
|
-
content: {
|
|
207
|
-
|
|
215
|
+
content: {
|
|
216
|
+
type: "string",
|
|
217
|
+
description: "Template content with {{variable}} placeholders"
|
|
218
|
+
},
|
|
219
|
+
description: {
|
|
220
|
+
type: "string",
|
|
221
|
+
description: "Optional description of what this template is for"
|
|
222
|
+
}
|
|
208
223
|
},
|
|
209
224
|
required: ["name", "content"]
|
|
210
225
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/plugins",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.281.1",
|
|
4
4
|
"description": "Official WrongStack plugin collection — auto-doc, git-autocommit, shell-check, cost-tracker, file-watcher, cron, template-engine, semver-bump, secret-scanner, todo-tracker, token-budget, lint-gate, branch-guard, diff-summary, commit-validator, format-on-save, test-runner-gate, import-organizer, todo-listener, session-recap, spec-linker, loop-breaker, path-guard, context-pins, checkpoint, error-lens, dep-guard, config-validator, notify-hub, changelog-writer, injection-shield, llm-cache, model-router, prompt-firewall, auto-escalate, token-throttle",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ECOSTACK TECHNOLOGY OÜ",
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
"vitest": "^4.1.9"
|
|
168
168
|
},
|
|
169
169
|
"dependencies": {
|
|
170
|
-
"@wrongstack/core": "0.
|
|
171
|
-
"@wrongstack/tools": "0.
|
|
170
|
+
"@wrongstack/core": "0.281.1",
|
|
171
|
+
"@wrongstack/tools": "0.281.1"
|
|
172
172
|
},
|
|
173
173
|
"scripts": {
|
|
174
174
|
"build": "tsup",
|