@pyreon/mcp 0.13.1 → 0.14.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 +62 -1
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +1306 -303
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +7 -1
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/anti-patterns.ts +210 -0
- package/src/api-reference.ts +495 -72
- package/src/changelog.ts +433 -0
- package/src/index.ts +279 -33
- package/src/manifest.ts +187 -0
- package/src/patterns.ts +243 -0
- package/src/tests/anti-patterns.test.ts +180 -0
- package/src/tests/changelog-server.test.ts +176 -0
- package/src/tests/changelog.test.ts +312 -0
- package/src/tests/manifest-snapshot.test.ts +36 -0
- package/src/tests/patterns-code.test.ts +216 -0
- package/src/tests/patterns-content.test.ts +147 -0
- package/src/tests/patterns-server.test.ts +160 -0
- package/src/tests/patterns.test.ts +236 -0
- package/src/tests/server-integration.test.ts +155 -0
- package/src/tests/test-audit-server.test.ts +128 -0
- package/src/tests/validate.test.ts +69 -0
package/lib/index.js
CHANGED
|
@@ -3,7 +3,9 @@ import Ajv from "ajv";
|
|
|
3
3
|
import _addFormats from "ajv-formats";
|
|
4
4
|
import { ZodOptional, z } from "zod";
|
|
5
5
|
import process$1 from "node:process";
|
|
6
|
-
import { detectReactPatterns, diagnoseError, generateContext, migrateReactCode } from "@pyreon/compiler";
|
|
6
|
+
import { auditTestEnvironment, detectPyreonPatterns, detectReactPatterns, diagnoseError, formatTestAudit, generateContext, migrateReactCode } from "@pyreon/compiler";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
9
|
|
|
8
10
|
//#region ../../../node_modules/.bun/zod@4.3.6/node_modules/zod/v3/helpers/util.js
|
|
9
11
|
var util;
|
|
@@ -426,7 +428,8 @@ const handleResult = (ctx, result) => {
|
|
|
426
428
|
success: false,
|
|
427
429
|
get error() {
|
|
428
430
|
if (this._error) return this._error;
|
|
429
|
-
|
|
431
|
+
const error = new ZodError$1(ctx.common.issues);
|
|
432
|
+
this._error = error;
|
|
430
433
|
return this._error;
|
|
431
434
|
}
|
|
432
435
|
};
|
|
@@ -2114,9 +2117,10 @@ var ZodObject$1 = class ZodObject$1 extends ZodType$1 {
|
|
|
2114
2117
|
_getCached() {
|
|
2115
2118
|
if (this._cached !== null) return this._cached;
|
|
2116
2119
|
const shape = this._def.shape();
|
|
2120
|
+
const keys = util.objectKeys(shape);
|
|
2117
2121
|
this._cached = {
|
|
2118
2122
|
shape,
|
|
2119
|
-
keys
|
|
2123
|
+
keys
|
|
2120
2124
|
};
|
|
2121
2125
|
return this._cached;
|
|
2122
2126
|
}
|
|
@@ -4050,38 +4054,30 @@ const _encode = (_Err) => (schema, value, _ctx) => {
|
|
|
4050
4054
|
const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
|
|
4051
4055
|
return _parse(_Err)(schema, value, ctx);
|
|
4052
4056
|
};
|
|
4053
|
-
const encode$1 = /* @__PURE__ */ _encode($ZodRealError);
|
|
4054
4057
|
const _decode = (_Err) => (schema, value, _ctx) => {
|
|
4055
4058
|
return _parse(_Err)(schema, value, _ctx);
|
|
4056
4059
|
};
|
|
4057
|
-
const decode$1 = /* @__PURE__ */ _decode($ZodRealError);
|
|
4058
4060
|
const _encodeAsync = (_Err) => async (schema, value, _ctx) => {
|
|
4059
4061
|
const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
|
|
4060
4062
|
return _parseAsync(_Err)(schema, value, ctx);
|
|
4061
4063
|
};
|
|
4062
|
-
const encodeAsync$1 = /* @__PURE__ */ _encodeAsync($ZodRealError);
|
|
4063
4064
|
const _decodeAsync = (_Err) => async (schema, value, _ctx) => {
|
|
4064
4065
|
return _parseAsync(_Err)(schema, value, _ctx);
|
|
4065
4066
|
};
|
|
4066
|
-
const decodeAsync$1 = /* @__PURE__ */ _decodeAsync($ZodRealError);
|
|
4067
4067
|
const _safeEncode = (_Err) => (schema, value, _ctx) => {
|
|
4068
4068
|
const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
|
|
4069
4069
|
return _safeParse(_Err)(schema, value, ctx);
|
|
4070
4070
|
};
|
|
4071
|
-
const safeEncode$1 = /* @__PURE__ */ _safeEncode($ZodRealError);
|
|
4072
4071
|
const _safeDecode = (_Err) => (schema, value, _ctx) => {
|
|
4073
4072
|
return _safeParse(_Err)(schema, value, _ctx);
|
|
4074
4073
|
};
|
|
4075
|
-
const safeDecode$1 = /* @__PURE__ */ _safeDecode($ZodRealError);
|
|
4076
4074
|
const _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => {
|
|
4077
4075
|
const ctx = _ctx ? Object.assign(_ctx, { direction: "backward" }) : { direction: "backward" };
|
|
4078
4076
|
return _safeParseAsync(_Err)(schema, value, ctx);
|
|
4079
4077
|
};
|
|
4080
|
-
const safeEncodeAsync$1 = /* @__PURE__ */ _safeEncodeAsync($ZodRealError);
|
|
4081
4078
|
const _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {
|
|
4082
4079
|
return _safeParseAsync(_Err)(schema, value, _ctx);
|
|
4083
4080
|
};
|
|
4084
|
-
const safeDecodeAsync$1 = /* @__PURE__ */ _safeDecodeAsync($ZodRealError);
|
|
4085
4081
|
|
|
4086
4082
|
//#endregion
|
|
4087
4083
|
//#region ../../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/regexes.js
|
|
@@ -6326,32 +6322,6 @@ function _check(fn, params) {
|
|
|
6326
6322
|
ch._zod.check = fn;
|
|
6327
6323
|
return ch;
|
|
6328
6324
|
}
|
|
6329
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
6330
|
-
function describe$2(description) {
|
|
6331
|
-
const ch = new $ZodCheck({ check: "describe" });
|
|
6332
|
-
ch._zod.onattach = [(inst) => {
|
|
6333
|
-
const existing = globalRegistry.get(inst) ?? {};
|
|
6334
|
-
globalRegistry.add(inst, {
|
|
6335
|
-
...existing,
|
|
6336
|
-
description
|
|
6337
|
-
});
|
|
6338
|
-
}];
|
|
6339
|
-
ch._zod.check = () => {};
|
|
6340
|
-
return ch;
|
|
6341
|
-
}
|
|
6342
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
6343
|
-
function meta$2(metadata) {
|
|
6344
|
-
const ch = new $ZodCheck({ check: "meta" });
|
|
6345
|
-
ch._zod.onattach = [(inst) => {
|
|
6346
|
-
const existing = globalRegistry.get(inst) ?? {};
|
|
6347
|
-
globalRegistry.add(inst, {
|
|
6348
|
-
...existing,
|
|
6349
|
-
...metadata
|
|
6350
|
-
});
|
|
6351
|
-
}];
|
|
6352
|
-
ch._zod.check = () => {};
|
|
6353
|
-
return ch;
|
|
6354
|
-
}
|
|
6355
6325
|
|
|
6356
6326
|
//#endregion
|
|
6357
6327
|
//#region ../../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/to-json-schema.js
|
|
@@ -7139,8 +7109,6 @@ function object$1(shape, params) {
|
|
|
7139
7109
|
...normalizeParams(params)
|
|
7140
7110
|
});
|
|
7141
7111
|
}
|
|
7142
|
-
const describe$1 = describe$2;
|
|
7143
|
-
const meta$1 = meta$2;
|
|
7144
7112
|
|
|
7145
7113
|
//#endregion
|
|
7146
7114
|
//#region ../../../node_modules/.bun/@modelcontextprotocol+sdk@1.29.0/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
|
|
@@ -7954,8 +7922,6 @@ function refine(fn, _params = {}) {
|
|
|
7954
7922
|
function superRefine(fn) {
|
|
7955
7923
|
return _superRefine(fn);
|
|
7956
7924
|
}
|
|
7957
|
-
const describe = describe$2;
|
|
7958
|
-
const meta = meta$2;
|
|
7959
7925
|
function preprocess(fn, schema) {
|
|
7960
7926
|
return pipe(transform(fn), schema);
|
|
7961
7927
|
}
|
|
@@ -9239,7 +9205,7 @@ function isTerminal(status) {
|
|
|
9239
9205
|
}
|
|
9240
9206
|
|
|
9241
9207
|
//#endregion
|
|
9242
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9208
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/Options.js
|
|
9243
9209
|
const ignoreOverride = Symbol("Let zodToJsonSchema decide on which parser to use");
|
|
9244
9210
|
const defaultOptions = {
|
|
9245
9211
|
name: void 0,
|
|
@@ -9274,7 +9240,7 @@ const getDefaultOptions = (options) => typeof options === "string" ? {
|
|
|
9274
9240
|
};
|
|
9275
9241
|
|
|
9276
9242
|
//#endregion
|
|
9277
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9243
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/Refs.js
|
|
9278
9244
|
const getRefs = (options) => {
|
|
9279
9245
|
const _options = getDefaultOptions(options);
|
|
9280
9246
|
const currentPath = _options.name !== void 0 ? [
|
|
@@ -9300,7 +9266,7 @@ const getRefs = (options) => {
|
|
|
9300
9266
|
};
|
|
9301
9267
|
|
|
9302
9268
|
//#endregion
|
|
9303
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9269
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/errorMessages.js
|
|
9304
9270
|
function addErrorMessage(res, key, errorMessage, refs) {
|
|
9305
9271
|
if (!refs?.errorMessages) return;
|
|
9306
9272
|
if (errorMessage) res.errorMessage = {
|
|
@@ -9314,7 +9280,7 @@ function setResponseValueAndErrors(res, key, value, errorMessage, refs) {
|
|
|
9314
9280
|
}
|
|
9315
9281
|
|
|
9316
9282
|
//#endregion
|
|
9317
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9283
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/getRelativePath.js
|
|
9318
9284
|
const getRelativePath = (pathA, pathB) => {
|
|
9319
9285
|
let i = 0;
|
|
9320
9286
|
for (; i < pathA.length && i < pathB.length; i++) if (pathA[i] !== pathB[i]) break;
|
|
@@ -9322,7 +9288,7 @@ const getRelativePath = (pathA, pathB) => {
|
|
|
9322
9288
|
};
|
|
9323
9289
|
|
|
9324
9290
|
//#endregion
|
|
9325
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9291
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/any.js
|
|
9326
9292
|
function parseAnyDef(refs) {
|
|
9327
9293
|
if (refs.target !== "openAi") return {};
|
|
9328
9294
|
const anyDefinitionPath = [
|
|
@@ -9335,7 +9301,7 @@ function parseAnyDef(refs) {
|
|
|
9335
9301
|
}
|
|
9336
9302
|
|
|
9337
9303
|
//#endregion
|
|
9338
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9304
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/array.js
|
|
9339
9305
|
function parseArrayDef(def, refs) {
|
|
9340
9306
|
const res = { type: "array" };
|
|
9341
9307
|
if (def.type?._def && def.type?._def?.typeName !== ZodFirstPartyTypeKind.ZodAny) res.items = parseDef(def.type._def, {
|
|
@@ -9352,7 +9318,7 @@ function parseArrayDef(def, refs) {
|
|
|
9352
9318
|
}
|
|
9353
9319
|
|
|
9354
9320
|
//#endregion
|
|
9355
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9321
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/bigint.js
|
|
9356
9322
|
function parseBigintDef(def, refs) {
|
|
9357
9323
|
const res = {
|
|
9358
9324
|
type: "integer",
|
|
@@ -9384,25 +9350,25 @@ function parseBigintDef(def, refs) {
|
|
|
9384
9350
|
}
|
|
9385
9351
|
|
|
9386
9352
|
//#endregion
|
|
9387
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9353
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/boolean.js
|
|
9388
9354
|
function parseBooleanDef() {
|
|
9389
9355
|
return { type: "boolean" };
|
|
9390
9356
|
}
|
|
9391
9357
|
|
|
9392
9358
|
//#endregion
|
|
9393
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9359
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/branded.js
|
|
9394
9360
|
function parseBrandedDef(_def, refs) {
|
|
9395
9361
|
return parseDef(_def.type._def, refs);
|
|
9396
9362
|
}
|
|
9397
9363
|
|
|
9398
9364
|
//#endregion
|
|
9399
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9365
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/catch.js
|
|
9400
9366
|
const parseCatchDef = (def, refs) => {
|
|
9401
9367
|
return parseDef(def.innerType._def, refs);
|
|
9402
9368
|
};
|
|
9403
9369
|
|
|
9404
9370
|
//#endregion
|
|
9405
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9371
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/date.js
|
|
9406
9372
|
function parseDateDef(def, refs, overrideDateStrategy) {
|
|
9407
9373
|
const strategy = overrideDateStrategy ?? refs.dateStrategy;
|
|
9408
9374
|
if (Array.isArray(strategy)) return { anyOf: strategy.map((item, i) => parseDateDef(def, refs, item)) };
|
|
@@ -9437,7 +9403,7 @@ const integerDateParser = (def, refs) => {
|
|
|
9437
9403
|
};
|
|
9438
9404
|
|
|
9439
9405
|
//#endregion
|
|
9440
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9406
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/default.js
|
|
9441
9407
|
function parseDefaultDef(_def, refs) {
|
|
9442
9408
|
return {
|
|
9443
9409
|
...parseDef(_def.innerType._def, refs),
|
|
@@ -9446,13 +9412,13 @@ function parseDefaultDef(_def, refs) {
|
|
|
9446
9412
|
}
|
|
9447
9413
|
|
|
9448
9414
|
//#endregion
|
|
9449
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9415
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/effects.js
|
|
9450
9416
|
function parseEffectsDef(_def, refs) {
|
|
9451
9417
|
return refs.effectStrategy === "input" ? parseDef(_def.schema._def, refs) : parseAnyDef(refs);
|
|
9452
9418
|
}
|
|
9453
9419
|
|
|
9454
9420
|
//#endregion
|
|
9455
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9421
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/enum.js
|
|
9456
9422
|
function parseEnumDef(def) {
|
|
9457
9423
|
return {
|
|
9458
9424
|
type: "string",
|
|
@@ -9461,7 +9427,7 @@ function parseEnumDef(def) {
|
|
|
9461
9427
|
}
|
|
9462
9428
|
|
|
9463
9429
|
//#endregion
|
|
9464
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9430
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/intersection.js
|
|
9465
9431
|
const isJsonSchema7AllOfType = (type) => {
|
|
9466
9432
|
if ("type" in type && type.type === "string") return false;
|
|
9467
9433
|
return "allOf" in type;
|
|
@@ -9504,7 +9470,7 @@ function parseIntersectionDef(def, refs) {
|
|
|
9504
9470
|
}
|
|
9505
9471
|
|
|
9506
9472
|
//#endregion
|
|
9507
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9473
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/literal.js
|
|
9508
9474
|
function parseLiteralDef(def, refs) {
|
|
9509
9475
|
const parsedType = typeof def.value;
|
|
9510
9476
|
if (parsedType !== "bigint" && parsedType !== "number" && parsedType !== "boolean" && parsedType !== "string") return { type: Array.isArray(def.value) ? "array" : "object" };
|
|
@@ -9519,7 +9485,7 @@ function parseLiteralDef(def, refs) {
|
|
|
9519
9485
|
}
|
|
9520
9486
|
|
|
9521
9487
|
//#endregion
|
|
9522
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9488
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/string.js
|
|
9523
9489
|
let emojiRegex = void 0;
|
|
9524
9490
|
/**
|
|
9525
9491
|
* Generated from the regular expressions found here as of 2024-05-22:
|
|
@@ -9765,7 +9731,7 @@ function stringifyRegExpWithFlags(regex, refs) {
|
|
|
9765
9731
|
}
|
|
9766
9732
|
|
|
9767
9733
|
//#endregion
|
|
9768
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9734
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/record.js
|
|
9769
9735
|
function parseRecordDef(def, refs) {
|
|
9770
9736
|
if (refs.target === "openAi") console.warn("Warning: OpenAI may not support records in schemas! Try an array of key-value pairs instead.");
|
|
9771
9737
|
if (refs.target === "openApi3" && def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodEnum) return {
|
|
@@ -9813,7 +9779,7 @@ function parseRecordDef(def, refs) {
|
|
|
9813
9779
|
}
|
|
9814
9780
|
|
|
9815
9781
|
//#endregion
|
|
9816
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9782
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/map.js
|
|
9817
9783
|
function parseMapDef(def, refs) {
|
|
9818
9784
|
if (refs.mapStrategy === "record") return parseRecordDef(def, refs);
|
|
9819
9785
|
return {
|
|
@@ -9845,7 +9811,7 @@ function parseMapDef(def, refs) {
|
|
|
9845
9811
|
}
|
|
9846
9812
|
|
|
9847
9813
|
//#endregion
|
|
9848
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9814
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/nativeEnum.js
|
|
9849
9815
|
function parseNativeEnumDef(def) {
|
|
9850
9816
|
const object = def.values;
|
|
9851
9817
|
const actualValues = Object.keys(def.values).filter((key) => {
|
|
@@ -9859,7 +9825,7 @@ function parseNativeEnumDef(def) {
|
|
|
9859
9825
|
}
|
|
9860
9826
|
|
|
9861
9827
|
//#endregion
|
|
9862
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9828
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/never.js
|
|
9863
9829
|
function parseNeverDef(refs) {
|
|
9864
9830
|
return refs.target === "openAi" ? void 0 : { not: parseAnyDef({
|
|
9865
9831
|
...refs,
|
|
@@ -9868,7 +9834,7 @@ function parseNeverDef(refs) {
|
|
|
9868
9834
|
}
|
|
9869
9835
|
|
|
9870
9836
|
//#endregion
|
|
9871
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9837
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/null.js
|
|
9872
9838
|
function parseNullDef(refs) {
|
|
9873
9839
|
return refs.target === "openApi3" ? {
|
|
9874
9840
|
enum: ["null"],
|
|
@@ -9877,7 +9843,7 @@ function parseNullDef(refs) {
|
|
|
9877
9843
|
}
|
|
9878
9844
|
|
|
9879
9845
|
//#endregion
|
|
9880
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9846
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/union.js
|
|
9881
9847
|
const primitiveMappings = {
|
|
9882
9848
|
ZodString: "string",
|
|
9883
9849
|
ZodNumber: "number",
|
|
@@ -9934,7 +9900,7 @@ const asAnyOf = (def, refs) => {
|
|
|
9934
9900
|
};
|
|
9935
9901
|
|
|
9936
9902
|
//#endregion
|
|
9937
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9903
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/nullable.js
|
|
9938
9904
|
function parseNullableDef(def, refs) {
|
|
9939
9905
|
if ([
|
|
9940
9906
|
"ZodString",
|
|
@@ -9975,7 +9941,7 @@ function parseNullableDef(def, refs) {
|
|
|
9975
9941
|
}
|
|
9976
9942
|
|
|
9977
9943
|
//#endregion
|
|
9978
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9944
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/number.js
|
|
9979
9945
|
function parseNumberDef(def, refs) {
|
|
9980
9946
|
const res = { type: "number" };
|
|
9981
9947
|
if (!def.checks) return res;
|
|
@@ -10008,7 +9974,7 @@ function parseNumberDef(def, refs) {
|
|
|
10008
9974
|
}
|
|
10009
9975
|
|
|
10010
9976
|
//#endregion
|
|
10011
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
9977
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/object.js
|
|
10012
9978
|
function parseObjectDef(def, refs) {
|
|
10013
9979
|
const forceOptionalIntoNullable = refs.target === "openAi";
|
|
10014
9980
|
const result = {
|
|
@@ -10068,7 +10034,7 @@ function safeIsOptional(schema) {
|
|
|
10068
10034
|
}
|
|
10069
10035
|
|
|
10070
10036
|
//#endregion
|
|
10071
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
10037
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/optional.js
|
|
10072
10038
|
const parseOptionalDef = (def, refs) => {
|
|
10073
10039
|
if (refs.currentPath.toString() === refs.propertyPath?.toString()) return parseDef(def.innerType._def, refs);
|
|
10074
10040
|
const innerSchema = parseDef(def.innerType._def, {
|
|
@@ -10083,7 +10049,7 @@ const parseOptionalDef = (def, refs) => {
|
|
|
10083
10049
|
};
|
|
10084
10050
|
|
|
10085
10051
|
//#endregion
|
|
10086
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
10052
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/pipeline.js
|
|
10087
10053
|
const parsePipelineDef = (def, refs) => {
|
|
10088
10054
|
if (refs.pipeStrategy === "input") return parseDef(def.in._def, refs);
|
|
10089
10055
|
else if (refs.pipeStrategy === "output") return parseDef(def.out._def, refs);
|
|
@@ -10106,13 +10072,13 @@ const parsePipelineDef = (def, refs) => {
|
|
|
10106
10072
|
};
|
|
10107
10073
|
|
|
10108
10074
|
//#endregion
|
|
10109
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
10075
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/promise.js
|
|
10110
10076
|
function parsePromiseDef(def, refs) {
|
|
10111
10077
|
return parseDef(def.type._def, refs);
|
|
10112
10078
|
}
|
|
10113
10079
|
|
|
10114
10080
|
//#endregion
|
|
10115
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
10081
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/set.js
|
|
10116
10082
|
function parseSetDef(def, refs) {
|
|
10117
10083
|
const schema = {
|
|
10118
10084
|
type: "array",
|
|
@@ -10128,7 +10094,7 @@ function parseSetDef(def, refs) {
|
|
|
10128
10094
|
}
|
|
10129
10095
|
|
|
10130
10096
|
//#endregion
|
|
10131
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
10097
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/tuple.js
|
|
10132
10098
|
function parseTupleDef(def, refs) {
|
|
10133
10099
|
if (def.rest) return {
|
|
10134
10100
|
type: "array",
|
|
@@ -10162,25 +10128,25 @@ function parseTupleDef(def, refs) {
|
|
|
10162
10128
|
}
|
|
10163
10129
|
|
|
10164
10130
|
//#endregion
|
|
10165
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
10131
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/undefined.js
|
|
10166
10132
|
function parseUndefinedDef(refs) {
|
|
10167
10133
|
return { not: parseAnyDef(refs) };
|
|
10168
10134
|
}
|
|
10169
10135
|
|
|
10170
10136
|
//#endregion
|
|
10171
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
10137
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/unknown.js
|
|
10172
10138
|
function parseUnknownDef(refs) {
|
|
10173
10139
|
return parseAnyDef(refs);
|
|
10174
10140
|
}
|
|
10175
10141
|
|
|
10176
10142
|
//#endregion
|
|
10177
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
10143
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parsers/readonly.js
|
|
10178
10144
|
const parseReadonlyDef = (def, refs) => {
|
|
10179
10145
|
return parseDef(def.innerType._def, refs);
|
|
10180
10146
|
};
|
|
10181
10147
|
|
|
10182
10148
|
//#endregion
|
|
10183
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
10149
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/selectParser.js
|
|
10184
10150
|
const selectParser = (def, typeName, refs) => {
|
|
10185
10151
|
switch (typeName) {
|
|
10186
10152
|
case ZodFirstPartyTypeKind.ZodString: return parseStringDef(def, refs);
|
|
@@ -10224,7 +10190,7 @@ const selectParser = (def, typeName, refs) => {
|
|
|
10224
10190
|
};
|
|
10225
10191
|
|
|
10226
10192
|
//#endregion
|
|
10227
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
10193
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/parseDef.js
|
|
10228
10194
|
function parseDef(def, refs, forceResolution = false) {
|
|
10229
10195
|
const seenItem = refs.seen.get(def);
|
|
10230
10196
|
if (refs.override) {
|
|
@@ -10274,7 +10240,7 @@ const addMeta = (def, refs, jsonSchema) => {
|
|
|
10274
10240
|
};
|
|
10275
10241
|
|
|
10276
10242
|
//#endregion
|
|
10277
|
-
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.
|
|
10243
|
+
//#region ../../../node_modules/.bun/zod-to-json-schema@3.25.2+3c5d820c62823f0b/node_modules/zod-to-json-schema/dist/esm/zodToJsonSchema.js
|
|
10278
10244
|
const zodToJsonSchema = (schema, options) => {
|
|
10279
10245
|
const refs = getRefs(options);
|
|
10280
10246
|
let definitions = typeof options === "object" && options.definitions ? Object.entries(options.definitions).reduce((acc, [name, schema]) => ({
|
|
@@ -12750,7 +12716,129 @@ var StdioServerTransport = class {
|
|
|
12750
12716
|
|
|
12751
12717
|
//#endregion
|
|
12752
12718
|
//#region package.json
|
|
12753
|
-
var version = "0.
|
|
12719
|
+
var version = "0.14.0";
|
|
12720
|
+
|
|
12721
|
+
//#endregion
|
|
12722
|
+
//#region src/anti-patterns.ts
|
|
12723
|
+
const CATEGORY_MAP = {
|
|
12724
|
+
"Reactivity Mistakes": "reactivity",
|
|
12725
|
+
"JSX Mistakes": "jsx",
|
|
12726
|
+
"Context & Provider Mistakes": "context",
|
|
12727
|
+
"Architecture Mistakes": "architecture",
|
|
12728
|
+
"Testing Mistakes": "testing",
|
|
12729
|
+
"Lifecycle & Cleanup Mistakes": "lifecycle",
|
|
12730
|
+
"Documentation Mistakes": "documentation"
|
|
12731
|
+
};
|
|
12732
|
+
const ANTI_PATTERN_CATEGORIES = [
|
|
12733
|
+
"reactivity",
|
|
12734
|
+
"jsx",
|
|
12735
|
+
"context",
|
|
12736
|
+
"architecture",
|
|
12737
|
+
"testing",
|
|
12738
|
+
"lifecycle",
|
|
12739
|
+
"documentation"
|
|
12740
|
+
];
|
|
12741
|
+
function normaliseCategory(heading) {
|
|
12742
|
+
return CATEGORY_MAP[heading.trim()] ?? null;
|
|
12743
|
+
}
|
|
12744
|
+
function splitSections(doc) {
|
|
12745
|
+
const lines = doc.split("\n");
|
|
12746
|
+
const sections = [];
|
|
12747
|
+
let currentHeading = null;
|
|
12748
|
+
let currentBody = [];
|
|
12749
|
+
for (const line of lines) {
|
|
12750
|
+
const headingMatch = /^## (.+)$/.exec(line);
|
|
12751
|
+
if (headingMatch) {
|
|
12752
|
+
if (currentHeading !== null) sections.push({
|
|
12753
|
+
heading: currentHeading,
|
|
12754
|
+
body: currentBody.join("\n")
|
|
12755
|
+
});
|
|
12756
|
+
currentHeading = headingMatch[1];
|
|
12757
|
+
currentBody = [];
|
|
12758
|
+
} else if (currentHeading !== null) currentBody.push(line);
|
|
12759
|
+
}
|
|
12760
|
+
if (currentHeading !== null) sections.push({
|
|
12761
|
+
heading: currentHeading,
|
|
12762
|
+
body: currentBody.join("\n")
|
|
12763
|
+
});
|
|
12764
|
+
return sections;
|
|
12765
|
+
}
|
|
12766
|
+
function splitBullets(sectionBody) {
|
|
12767
|
+
const lines = sectionBody.split("\n");
|
|
12768
|
+
const bullets = [];
|
|
12769
|
+
let current = [];
|
|
12770
|
+
for (const line of lines) if (/^- \*\*/.test(line)) {
|
|
12771
|
+
if (current.length > 0) bullets.push(current.join("\n").trim());
|
|
12772
|
+
current = [line];
|
|
12773
|
+
} else if (current.length > 0) current.push(line);
|
|
12774
|
+
if (current.length > 0) bullets.push(current.join("\n").trim());
|
|
12775
|
+
return bullets.filter((b) => b.length > 0);
|
|
12776
|
+
}
|
|
12777
|
+
function parseBullet(bullet) {
|
|
12778
|
+
const nameMatch = /^- \*\*([^*]+)\*\*/.exec(bullet);
|
|
12779
|
+
if (!nameMatch) return null;
|
|
12780
|
+
const name = nameMatch[1].trim();
|
|
12781
|
+
const afterName = bullet.slice(nameMatch[0].length);
|
|
12782
|
+
const detectorMatch = /`?\[detector:\s*([a-z0-9\-\/ ]+)\]`?/i.exec(afterName);
|
|
12783
|
+
const detectorCodes = [];
|
|
12784
|
+
if (detectorMatch) for (const code of detectorMatch[1].split("/")) {
|
|
12785
|
+
const c = code.trim();
|
|
12786
|
+
if (c) detectorCodes.push(c);
|
|
12787
|
+
}
|
|
12788
|
+
let description = afterName;
|
|
12789
|
+
if (detectorMatch) description = description.replace(detectorMatch[0], "");
|
|
12790
|
+
description = description.replace(/^[\s:]+/, "").trim();
|
|
12791
|
+
return {
|
|
12792
|
+
name,
|
|
12793
|
+
description,
|
|
12794
|
+
detectorCodes
|
|
12795
|
+
};
|
|
12796
|
+
}
|
|
12797
|
+
function parseAntiPatterns(doc) {
|
|
12798
|
+
const sections = splitSections(doc);
|
|
12799
|
+
const entries = [];
|
|
12800
|
+
for (const { heading, body } of sections) {
|
|
12801
|
+
const category = normaliseCategory(heading);
|
|
12802
|
+
if (!category) continue;
|
|
12803
|
+
for (const bullet of splitBullets(body)) {
|
|
12804
|
+
const parsed = parseBullet(bullet);
|
|
12805
|
+
if (!parsed) continue;
|
|
12806
|
+
entries.push({
|
|
12807
|
+
name: parsed.name,
|
|
12808
|
+
category,
|
|
12809
|
+
categoryHeading: heading,
|
|
12810
|
+
description: parsed.description,
|
|
12811
|
+
detectorCodes: parsed.detectorCodes
|
|
12812
|
+
});
|
|
12813
|
+
}
|
|
12814
|
+
}
|
|
12815
|
+
return entries;
|
|
12816
|
+
}
|
|
12817
|
+
/** Format a list of entries into a single Markdown block suitable for MCP. */
|
|
12818
|
+
function formatAntiPatterns(entries, filterCategory) {
|
|
12819
|
+
if (entries.length === 0) return filterCategory === "all" ? "No anti-patterns found. Check that `.claude/rules/anti-patterns.md` is reachable." : `No anti-patterns found in category '${filterCategory}'. Valid categories: ${ANTI_PATTERN_CATEGORIES.join(", ")}, all.`;
|
|
12820
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
12821
|
+
for (const entry of entries) {
|
|
12822
|
+
if (!byCategory.has(entry.category)) byCategory.set(entry.category, []);
|
|
12823
|
+
byCategory.get(entry.category).push(entry);
|
|
12824
|
+
}
|
|
12825
|
+
const parts = [];
|
|
12826
|
+
const header = filterCategory === "all" ? `# Pyreon Anti-Patterns (${entries.length} total, ${byCategory.size} categor${byCategory.size === 1 ? "y" : "ies"})` : `# Pyreon Anti-Patterns — ${filterCategory} (${entries.length})`;
|
|
12827
|
+
parts.push(header);
|
|
12828
|
+
parts.push("");
|
|
12829
|
+
parts.push("Each entry is a known mistake documented at `.claude/rules/anti-patterns.md`. Entries tagged `[detector: <code>]` are caught statically by the MCP `validate` tool — the rest require a human / AI review. Read them BEFORE writing new code, not during code review.");
|
|
12830
|
+
parts.push("");
|
|
12831
|
+
for (const [, catEntries] of byCategory) {
|
|
12832
|
+
parts.push(`## ${catEntries[0].categoryHeading} (${catEntries.length})`);
|
|
12833
|
+
parts.push("");
|
|
12834
|
+
for (const entry of catEntries) {
|
|
12835
|
+
const tag = entry.detectorCodes.length > 0 ? ` \`[detector: ${entry.detectorCodes.join(" / ")}]\`` : "";
|
|
12836
|
+
parts.push(`- **${entry.name}**${tag}: ${entry.description}`);
|
|
12837
|
+
}
|
|
12838
|
+
parts.push("");
|
|
12839
|
+
}
|
|
12840
|
+
return parts.join("\n").trimEnd();
|
|
12841
|
+
}
|
|
12754
12842
|
|
|
12755
12843
|
//#endregion
|
|
12756
12844
|
//#region src/api-reference.ts
|
|
@@ -13330,17 +13418,47 @@ useHead({ title: "My Page", meta: [{ name: "description", content: "..." }] })
|
|
|
13330
13418
|
useHead(() => ({
|
|
13331
13419
|
title: \`\${username()} — Profile\`,
|
|
13332
13420
|
meta: [{ property: "og:title", content: username() }]
|
|
13333
|
-
}))
|
|
13421
|
+
}))`,
|
|
13422
|
+
notes: "Register head tags from any component in the tree. Pass a static `UseHeadInput` object for one-shot registration, or a `() => UseHeadInput` thunk for reactive re-registration when signal reads inside the thunk change. Calling `useHead()` outside a `HeadProvider` ancestor (CSR) or `renderWithHead()` invocation (SSR) is a silent no-op — it does not throw. See also: HeadProvider, renderWithHead.",
|
|
13423
|
+
mistakes: `- Using \`\${...}\` in a \`titleTemplate\` string — the placeholder is \`%s\` (or pass a function form \`(title) => …\`)
|
|
13424
|
+
- Calling \`useHead()\` outside any \`HeadProvider\` / \`renderWithHead()\` boundary — silent no-op, the entries simply go nowhere
|
|
13425
|
+
- Wrapping the input in \`computed()\` instead of a thunk — pass a plain \`() => ({...})\` arrow; \`useHead\` registers its own effect
|
|
13426
|
+
- Expecting \`<\/script>\` inside an inline script body to render verbatim — the SSR escaper rewrites it as \`<\/script>\` to prevent breaking out of the inline tag`
|
|
13334
13427
|
},
|
|
13335
13428
|
"head/HeadProvider": {
|
|
13336
|
-
signature: "
|
|
13337
|
-
example:
|
|
13429
|
+
signature: "(props: HeadProviderProps) => VNodeChild",
|
|
13430
|
+
example: `<HeadProvider>{children}</HeadProvider>
|
|
13431
|
+
|
|
13432
|
+
// Client-side setup:
|
|
13338
13433
|
mount(
|
|
13339
13434
|
<HeadProvider>
|
|
13340
13435
|
<App />
|
|
13341
13436
|
</HeadProvider>,
|
|
13342
13437
|
document.getElementById("app")!
|
|
13343
|
-
)
|
|
13438
|
+
)`,
|
|
13439
|
+
notes: "Client-side context provider that collects every `useHead()` call from descendants and syncs the resolved tags into the live `document.head` element. Mount once near the application root. Auto-creates a `HeadContextValue` when no `context` prop is passed; nested providers each own an independent context. See also: useHead, renderWithHead, createHeadContext.",
|
|
13440
|
+
mistakes: `- Mounting two \`HeadProvider\` instances at sibling roots — each owns an independent context, so a \`useHead()\` deeper in tree A is invisible to tree B
|
|
13441
|
+
- Forgetting to mount \`HeadProvider\` and expecting \`useHead()\` to still update \`document.head\` — silent no-op outside a provider`
|
|
13442
|
+
},
|
|
13443
|
+
"head/renderWithHead": {
|
|
13444
|
+
signature: "renderWithHead(app: VNode): Promise<{ html: string; head: string; htmlAttrs: string; bodyAttrs: string }>",
|
|
13445
|
+
example: `import { renderWithHead } from '@pyreon/head'
|
|
13446
|
+
|
|
13447
|
+
const { html, head, htmlAttrs, bodyAttrs } = await renderWithHead(<App />)
|
|
13448
|
+
const doc = \`<!doctype html><html\${htmlAttrs}><head>\${head}</head><body\${bodyAttrs}>\${html}</body></html>\``,
|
|
13449
|
+
notes: "SSR companion to `HeadProvider`. Renders the app to HTML via `renderToString` while collecting every `useHead()` call from the tree, then serializes the resolved tags into a single `head` string plus separate `htmlAttrs` / `bodyAttrs` strings. Async components that call `useHead()` in their body work — the renderer awaits suspended subtrees before serialization. See also: useHead, HeadProvider.",
|
|
13450
|
+
mistakes: `- Awaiting \`renderWithHead\` and then NOT splicing \`head\` into the \`<head>\` element — every \`useHead()\` call quietly disappears
|
|
13451
|
+
- Forgetting to interpolate \`htmlAttrs\` / \`bodyAttrs\` (the leading space is included in each string) — \`htmlAttrs.lang\` and \`bodyAttrs.class\` set via \`useHead\` won\'t reach the DOM`
|
|
13452
|
+
},
|
|
13453
|
+
"head/createHeadContext": {
|
|
13454
|
+
signature: "() => HeadContextValue",
|
|
13455
|
+
example: `import { createHeadContext, HeadContext } from '@pyreon/head'
|
|
13456
|
+
|
|
13457
|
+
const ctx = createHeadContext()
|
|
13458
|
+
provide(HeadContext, ctx)
|
|
13459
|
+
// ... render tree that calls useHead() ...
|
|
13460
|
+
const { tags, htmlAttrs, bodyAttrs } = ctx.resolve()`,
|
|
13461
|
+
notes: "Manual factory for a `HeadContextValue` — only needed when wiring up a custom SSR pipeline that bypasses `renderWithHead`, or when running multiple isolated head contexts in the same process. The value exposes `add` / `remove` / `resolve` / `resolveTitleTemplate` / `resolveHtmlAttrs` / `resolveBodyAttrs` for full programmatic control. See also: HeadProvider, renderWithHead."
|
|
13344
13462
|
},
|
|
13345
13463
|
"server/createHandler": {
|
|
13346
13464
|
signature: "createHandler(options: HandlerOptions): (req: Request) => Promise<Response>",
|
|
@@ -13351,7 +13469,12 @@ export default createHandler({
|
|
|
13351
13469
|
routes,
|
|
13352
13470
|
clientEntry: "/src/entry-client.ts",
|
|
13353
13471
|
mode: "stream", // or "string"
|
|
13354
|
-
})
|
|
13472
|
+
})`,
|
|
13473
|
+
notes: "Build a production SSR handler from your `App`, `routes`, and optional template / client entry / middleware. The template is precompiled once at handler-creation (split into 4 parts to skip three string scans per request); a missing `<!--pyreon-app-->` placeholder throws at creation time, not per request. Middleware runs before render with `ctx.locals` for cross-middleware data passing — return a `Response` to short-circuit the chain. `mode: \"stream\"` uses `renderToStream` so Suspense boundaries flush out-of-order; `mode: \"string\"` uses `renderToString` (default). See also: prerender, island, useRequestLocals.",
|
|
13474
|
+
mistakes: `- Omitting \`<!--pyreon-app-->\` from the custom template — throws at handler-creation, not per request
|
|
13475
|
+
- Returning a \`Response\` from middleware and expecting downstream middleware to still run — the chain short-circuits on the first \`Response\`
|
|
13476
|
+
- Reading \`ctx.locals\` from inside the component without \`useRequestLocals()\` — the component tree only sees locals when bridged through that hook
|
|
13477
|
+
- Forgetting to escape user data inserted into a custom template — \`createHandler\` only escapes its own loader-data injection (\`<\/script>\` → \`<\/script>\`); your template content is your responsibility`
|
|
13355
13478
|
},
|
|
13356
13479
|
"server/island": {
|
|
13357
13480
|
signature: "island(loader: () => Promise<ComponentFn>, options: { name: string; hydrate?: HydrationStrategy }): ComponentFn",
|
|
@@ -13360,7 +13483,12 @@ export default createHandler({
|
|
|
13360
13483
|
{ name: "SearchBar", hydrate: "visible" }
|
|
13361
13484
|
)
|
|
13362
13485
|
|
|
13363
|
-
// Hydration strategies: "load" | "idle" | "visible" | "media" | "never"
|
|
13486
|
+
// Hydration strategies: "load" | "idle" | "visible" | "media" | "never"`,
|
|
13487
|
+
notes: "Wrap a lazily-loaded component in a `<pyreon-island>` boundary with a hydration strategy. The rest of the page stays HTML-only; only the island fetches its JS bundle and hydrates. Strategies: `\"load\"` (immediate), `\"idle\"` (`requestIdleCallback`), `\"visible\"` (IntersectionObserver), `\"media(query)\"` (matchMedia), `\"never\"` (HTML-only, no JS). Props passed to islands are JSON-serialized — non-JSON values (functions, symbols, undefined, children) are stripped. See also: createHandler, hydrateIslands.",
|
|
13488
|
+
mistakes: `- Passing function props (event handlers, callbacks) — silently stripped during JSON serialization, the island sees \`undefined\`
|
|
13489
|
+
- Passing children to an island — stripped; islands cannot render arbitrary descendant trees from props
|
|
13490
|
+
- Forgetting to call \`hydrateIslands({ Name: () => import("./Path") })\` on the client — islands render as HTML and never hydrate
|
|
13491
|
+
- Using a duplicate \`name\` across two islands — the client-side registry collapses them, only one loader will fire`
|
|
13364
13492
|
},
|
|
13365
13493
|
"server/prerender": {
|
|
13366
13494
|
signature: "prerender(options: PrerenderOptions): Promise<PrerenderResult>",
|
|
@@ -13368,7 +13496,11 @@ export default createHandler({
|
|
|
13368
13496
|
handler,
|
|
13369
13497
|
paths: ["/", "/about", "/blog/1", "/blog/2"],
|
|
13370
13498
|
outDir: "./dist",
|
|
13371
|
-
})
|
|
13499
|
+
})`,
|
|
13500
|
+
notes: "Static-site generator built on `createHandler`. Walks the `paths` array (or async generator), invokes the handler for each path, and writes the rendered HTML to `outDir/<path>.html`. The `onPage(path, html)` callback fires per page so callers can post-process or stream output. Validates `outDir` against path traversal (`../` segments are rejected). Errors per-page are collected in the result, not thrown. See also: createHandler.",
|
|
13501
|
+
mistakes: `- Passing a relative \`outDir\` and being surprised when it resolves against \`process.cwd()\` — pass an absolute path for predictability
|
|
13502
|
+
- Expecting per-page errors to throw — they\'re collected in \`result.errors\`; check the array after \`await\`
|
|
13503
|
+
- Generating thousands of paths without batching — the function processes the array sequentially; if you need parallelism, batch the \`paths\` array yourself`
|
|
13372
13504
|
},
|
|
13373
13505
|
"runtime-dom/mount": {
|
|
13374
13506
|
signature: "mount(root: VNodeChild, container: Element): () => void",
|
|
@@ -14554,7 +14686,7 @@ lint({
|
|
|
14554
14686
|
"pyreon/no-window-in-ssr": { exemptPaths: ["src/foundation/"] },
|
|
14555
14687
|
},
|
|
14556
14688
|
})`,
|
|
14557
|
-
notes: "
|
|
14689
|
+
notes: "59 rules across 12 categories. Auto-loads `.pyreonlintrc.json`. Presets: `recommended`, `strict`, `app`, `lib`. Per-rule options via tuple form in config (`[\"error\", { exemptPaths: [...] }]`) or `ruleOptionsOverrides`. Wrong-typed options surface on `result.configDiagnostics`. Uses `oxc-parser` with AST caching. See also: lintFile, getPreset, AstCache."
|
|
14558
14690
|
},
|
|
14559
14691
|
"lint/lintFile": {
|
|
14560
14692
|
signature: "lintFile(filePath: string, sourceText: string, rules: Rule[], config: LintConfig, cache?: AstCache, configDiagnosticsSink?: ConfigDiagnostic[]): LintFileResult",
|
|
@@ -14564,17 +14696,17 @@ const cache = new AstCache()
|
|
|
14564
14696
|
const config = getPreset("recommended")
|
|
14565
14697
|
const configSink: ConfigDiagnostic[] = []
|
|
14566
14698
|
const result = lintFile("app.tsx", source, allRules, config, cache, configSink)`,
|
|
14567
|
-
notes: "Low-level single-file API. Optional AstCache for repeat runs (FNV-1a hash keyed). Optional `configDiagnosticsSink` collects malformed-option diagnostics; without it they print to stderr."
|
|
14699
|
+
notes: "Low-level single-file API. Optional `AstCache` for repeat runs (FNV-1a hash keyed). Optional `configDiagnosticsSink` collects malformed-option diagnostics; without it they print to stderr. See also: lint, AstCache."
|
|
14568
14700
|
},
|
|
14569
14701
|
"lint/cli": {
|
|
14570
|
-
signature:
|
|
14702
|
+
signature: `pyreon-lint [--preset name] [--fix] [--format text|json|compact] [--quiet] [--watch] [--list] [--config path] [--ignore path] [--rule id=severity] [--rule-options id='{json}'] [path...]`,
|
|
14571
14703
|
example: `pyreon-lint --preset strict --quiet # CI mode
|
|
14572
14704
|
pyreon-lint --fix # auto-fix
|
|
14573
14705
|
pyreon-lint --watch src/ # watch mode
|
|
14574
14706
|
pyreon-lint --list # list all 59 rules
|
|
14575
14707
|
pyreon-lint --format json # machine-readable
|
|
14576
14708
|
pyreon-lint --rule-options 'pyreon/no-window-in-ssr={"exemptPaths":["src/foundation/"]}' src/`,
|
|
14577
|
-
notes:
|
|
14709
|
+
notes: `CLI entry. Config: \`.pyreonlintrc.json\` (reference \`schema/pyreonlintrc.schema.json\` for IDE autocomplete) or \`package.json\`'s \`'pyreonlint'\` field. Ignore: \`.pyreonlintignore\` + \`.gitignore\`. Watch: \`fs.watch\` recursive with 100ms debounce. \`--rule-options id='{json}'\` passes per-rule options on a single run. See also: lint.`
|
|
14578
14710
|
},
|
|
14579
14711
|
"lint/no-process-dev-gate": {
|
|
14580
14712
|
signature: "rule: pyreon/no-process-dev-gate (architecture, error, auto-fixable)",
|
|
@@ -14586,7 +14718,7 @@ if (__DEV__) console.warn('hello')
|
|
|
14586
14718
|
// @ts-ignore — provided by Vite/Rolldown at build time
|
|
14587
14719
|
const __DEV__ = import.meta.env?.DEV === true
|
|
14588
14720
|
if (__DEV__) console.warn('hello')`,
|
|
14589
|
-
notes:
|
|
14721
|
+
notes: `The \`typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\` pattern works in vitest (Node, \`process\` is defined) but is silently dead code in real Vite browser bundles because Vite does NOT polyfill \`process\` for the client. Every \`console.warn\` gated on the broken constant never fires for real users in dev mode — unit tests pass while users get nothing. Use \`import.meta.env.DEV\` instead — Vite/Rolldown literal-replace it at build time, prod tree-shakes the warning to zero bytes, and vitest sets it to \`true\` automatically. Server-only packages (\`zero\`, \`core/server\`, \`core/runtime-server\`, \`vite-plugin\`, \`cli\`, \`lint\`, \`mcp\`, \`storybook\`, \`typescript\`) and test files are exempt. Reference implementation: \`packages/fundamentals/flow/src/layout.ts:warnIgnoredOptions\`. The rule has an auto-fix that replaces the broken expression with \`import.meta.env?.DEV === true\`. See also: require-browser-smoke-test.`,
|
|
14590
14722
|
mistakes: `- Copying the \`typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\` pattern from existing codebases — it works in Node but is dead in browser bundles
|
|
14591
14723
|
- Trying to test with \`delete globalThis.process\` — vitest's own \`import.meta.env\` depends on \`process\`, so deleting it breaks the FIXED gate too (not because the gate is wrong, but because vitest can't resolve it)
|
|
14592
14724
|
- Adding \`process: { env: { ... } }\` polyfills to vite.config.ts as a workaround — fix the source instead
|
|
@@ -14606,7 +14738,7 @@ if (__DEV__) console.warn('hello')`,
|
|
|
14606
14738
|
]
|
|
14607
14739
|
}
|
|
14608
14740
|
}`,
|
|
14609
|
-
notes:
|
|
14741
|
+
notes: `Locks in the durability of the T1.1 browser smoke harness (PRs #224, #227, #229, #231). Every browser-categorized package MUST ship at least one \`*.browser.test.{ts,tsx}\` file under \`src/\`. Without this rule, new browser packages can quietly ship without smoke coverage and we drift back to the world before T1.1 — happy-dom silently masks environment-divergence bugs (PR #197 mock-vnode metadata drop, PR #200 \`typeof process\` dead code, multi-word event delegation bug). Default browser-package list mirrors \`.claude/rules/test-environment-parity.md\`. The rule fires once per package on its \`src/index.ts\`, walks the package directory looking for \`*.browser.test.*\`, and reports if none are found. Off in \`app\` preset because apps don't ship as packages with smoke obligations. See also: no-process-dev-gate.`,
|
|
14610
14742
|
mistakes: `- Adding a new browser-running package without a browser test — the rule will fail your PR
|
|
14611
14743
|
- Hardcoding the browser-package list in the rule — the list lives in \`.claude/rules/browser-packages.json\` (single source of truth), not in the rule source
|
|
14612
14744
|
- Disabling the rule globally — use \`exemptPaths\` to exempt specific packages still under construction
|
|
@@ -14618,11 +14750,84 @@ if (__DEV__) console.warn('hello')`,
|
|
|
14618
14750
|
// "which Pyreon packages are missing browser smoke coverage?"
|
|
14619
14751
|
// Tool walks packages/, matches against .claude/rules/browser-packages.json,
|
|
14620
14752
|
// returns a coverage report.`,
|
|
14621
|
-
notes:
|
|
14753
|
+
notes: `Companion to the \`pyreon/require-browser-smoke-test\` lint rule. Reports which browser-categorized Pyreon packages have at least one \`*.browser.test.{ts,tsx}\` file under \`src/\`. Uses the same \`.claude/rules/browser-packages.json\` single source of truth as the rule + the CI script. Lets an AI agent check coverage before writing a new browser package (so it adds a smoke test in the same PR) instead of discovering the failure when CI runs. Falls back with a clear message if the JSON isn't present (e.g. consumer apps that don't ship the Pyreon monorepo layout). See also: audit_test_environment.`,
|
|
14622
14754
|
mistakes: `- Using the tool's output as a substitute for running the CI script — this tool only checks file existence, not the self-expiring-exemption check that \`bun run lint:browser-smoke\` performs`
|
|
14623
14755
|
},
|
|
14756
|
+
"mcp/get_api": {
|
|
14757
|
+
signature: "tool: get_api({ package: string; symbol: string }) → APIEntry",
|
|
14758
|
+
example: `// Agent-side
|
|
14759
|
+
get_api({ package: 'flow', symbol: 'createFlow' })
|
|
14760
|
+
get_api({ package: '@pyreon/router', symbol: 'useTypedSearchParams' })`,
|
|
14761
|
+
notes: `Look up any Pyreon API by \`package\` (e.g. \`"flow"\` or \`"@pyreon/flow"\`) and \`symbol\` (e.g. \`"createFlow"\`). Returns the canonical signature, example, foot-gun catalogue, and cross-references — drawn from \`api-reference.ts\`, which is regenerated from each package\'s \`manifest.ts\`. The single agent-facing entry point for "what does this API do and how do I avoid the common mistakes." See also: validate, get_pattern.`
|
|
14762
|
+
},
|
|
14763
|
+
"mcp/validate": {
|
|
14764
|
+
signature: "tool: validate({ code: string; filename?: string }) → Diagnostics[]",
|
|
14765
|
+
example: `validate({ code: \`
|
|
14766
|
+
function MyComp(props) {
|
|
14767
|
+
const { value } = props // → props-destructured
|
|
14768
|
+
return <For each={items}>{...}</For> // → for-missing-by
|
|
14769
|
+
}
|
|
14770
|
+
\` })`,
|
|
14771
|
+
notes: "Two AST-based detectors run in parallel: `detectReactPatterns` flags \"coming from React\" mistakes (`useState`, `useEffect`, `className`, `onChange` on inputs, React-package imports), and `detectPyreonPatterns` flags \"using Pyreon wrong\" mistakes (`<For>` missing `by`, props destructured at component signature, `typeof process` dev gates, raw `addEventListener`, `Date.now() + Math.random()` IDs). Diagnostics are merged + sorted by line / column for top-down reading. See also: get_anti_patterns, migrate_react."
|
|
14772
|
+
},
|
|
14773
|
+
"mcp/migrate_react": {
|
|
14774
|
+
signature: "tool: migrate_react({ code: string; filename?: string }) → MigrationResult",
|
|
14775
|
+
example: `migrate_react({ code: \`
|
|
14776
|
+
import { useState, useEffect } from 'react'
|
|
14777
|
+
function Counter() {
|
|
14778
|
+
const [count, setCount] = useState(0)
|
|
14779
|
+
useEffect(() => { console.log(count) }, [count])
|
|
14780
|
+
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
|
14781
|
+
}
|
|
14782
|
+
\` })`,
|
|
14783
|
+
notes: "Convert React code to idiomatic Pyreon. Handles `useState` → `signal()`, `useEffect` → `effect()`, `className` → `class`, `onChange` → `onInput`, `useMemo` → `computed()`, React imports → Pyreon imports. Reports per-edit fixable diagnostics so callers can apply or review. See also: validate."
|
|
14784
|
+
},
|
|
14785
|
+
"mcp/diagnose": {
|
|
14786
|
+
signature: "tool: diagnose({ error: string }) → DiagnoseResult",
|
|
14787
|
+
example: `diagnose({ error: 'Cannot redefine property X on object [object Object]' })
|
|
14788
|
+
// → cause: configurable: false on a getter; fix: set configurable: true`,
|
|
14789
|
+
notes: "Parse a Pyreon runtime / build error message into structured fix information: probable cause, recommended fix, related docs, and the `.claude/rules/anti-patterns.md` entry (if any) the error matches. Useful when an agent sees a stack trace and wants to skip the \"search the codebase for similar errors\" step. See also: validate, get_anti_patterns."
|
|
14790
|
+
},
|
|
14791
|
+
"mcp/get_routes": {
|
|
14792
|
+
signature: "tool: get_routes() → Route[]",
|
|
14793
|
+
example: `get_routes()
|
|
14794
|
+
// → [{ path: '/', name: 'home', hasLoader: true, params: [] }, ...]`,
|
|
14795
|
+
notes: "List every route in the current project — path, loader presence, guards, params, and named-route name. Walks the project source from `process.cwd()` down. Cached per server instance with auto-invalidation on `cwd` change. See also: get_components."
|
|
14796
|
+
},
|
|
14797
|
+
"mcp/get_components": {
|
|
14798
|
+
signature: "tool: get_components() → ComponentInfo[]",
|
|
14799
|
+
example: `get_components()
|
|
14800
|
+
// → [{ name: 'Button', file: 'src/Button.tsx', props: ['onClick', 'children'], signals: ['count'] }, ...]`,
|
|
14801
|
+
notes: "List every component in the current project with its props and signal usage. Same scanner as `get_routes`. Useful for an agent before generating new code that needs to reference existing components. See also: get_routes."
|
|
14802
|
+
},
|
|
14803
|
+
"mcp/get_pattern": {
|
|
14804
|
+
signature: "tool: get_pattern({ name?: string }) → PatternBody | string[]",
|
|
14805
|
+
example: `get_pattern({ name: 'controllable-state' })
|
|
14806
|
+
// → full canonical pattern body
|
|
14807
|
+
get_pattern({})
|
|
14808
|
+
// → [{ name: 'controllable-state', summary: '...' }, ...]`,
|
|
14809
|
+
notes: "Fetch a canonical \"how do I do X\" pattern body from `docs/patterns/`. Eight foundational patterns ship: `dev-warnings`, `controllable-state`, `ssr-safe-hooks`, `signal-writes`, `keyed-lists`, `reactive-context`, `event-listeners`, `form-fields`. Omit `name` to list available patterns. Drop a new `docs/patterns/<slug>.md` file to add one — picked up on next call. See also: get_anti_patterns."
|
|
14810
|
+
},
|
|
14811
|
+
"mcp/get_anti_patterns": {
|
|
14812
|
+
signature: `tool: get_anti_patterns({ category?: 'reactivity' | 'jsx' | 'context' | 'architecture' | 'testing' | 'lifecycle' | 'documentation' | 'all' }) → AntiPattern[]`,
|
|
14813
|
+
example: `get_anti_patterns({ category: 'reactivity' })
|
|
14814
|
+
// → ['Bare signal in JSX text', 'Stale closures', 'Destructuring props', ...]`,
|
|
14815
|
+
notes: "Browse the anti-patterns catalog parsed from `.claude/rules/anti-patterns.md`. Each entry surfaces its `[detector: <code>]` tag inline so an agent can pair the catalog entry with the live static detector exposed by `validate`. Optional `category` filter; default returns all categories. See also: validate, get_pattern."
|
|
14816
|
+
},
|
|
14817
|
+
"mcp/get_changelog": {
|
|
14818
|
+
signature: "tool: get_changelog({ package?: string; limit?: number; includeDependencyUpdates?: boolean; since?: string }) → ChangelogEntry[]",
|
|
14819
|
+
example: `get_changelog({ package: 'flow', limit: 5 })
|
|
14820
|
+
get_changelog({ package: '@pyreon/router', since: '0.12.0' })`,
|
|
14821
|
+
notes: "Recent release notes for any `@pyreon/*` package without scraping `git log`. Parses `packages/**/CHANGELOG.md` into version entries (`{ version, changes[], dependencyUpdates[], empty }`) and returns the N most recent substantive versions (default 5). Filters out ceremonial version bumps (pure dependency-update releases with no user-facing body) by default — opt back in with `includeDependencyUpdates: true`. `since: \"0.12.0\"` returns the delta from a known floor — useful when an agent knows the version it was trained against. See also: get_api."
|
|
14822
|
+
},
|
|
14823
|
+
"mcp/audit_test_environment": {
|
|
14824
|
+
signature: `tool: audit_test_environment({ minRisk?: 'high' | 'medium' | 'low'; limit?: number }) → AuditReport`,
|
|
14825
|
+
example: `audit_test_environment({ minRisk: 'medium', limit: 10 })
|
|
14826
|
+
// → grouped report with HIGH / MEDIUM / LOW sections`,
|
|
14827
|
+
notes: `Scan every \`*.test.{ts,tsx}\` under \`packages/\` for the mock-vnode anti-pattern that caused PR #197\'s silent metadata drop. Files are classified HIGH / MEDIUM / LOW based on the balance of mock-vnode literals + helpers + helper-call sites vs real \`h()\` calls + \`@pyreon/core\` import. Three context-aware skips (helper-def vs binding discrimination, type-guard call-arg skip, template-string fixture mask) keep the false-positive rate low. Run before merging a new test file or after a framework change. See also: get_browser_smoke_status.`
|
|
14828
|
+
},
|
|
14624
14829
|
"ui-core/PyreonUI": {
|
|
14625
|
-
signature:
|
|
14830
|
+
signature: `(props: { theme?: Theme; mode?: 'light' | 'dark' | 'system'; inversed?: boolean; children: VNodeChild }) => VNodeChild`,
|
|
14626
14831
|
example: `import { PyreonUI } from "@pyreon/ui-core"
|
|
14627
14832
|
import { enrichTheme } from "@pyreon/unistyle"
|
|
14628
14833
|
|
|
@@ -14634,18 +14839,22 @@ const theme = enrichTheme({ colors: { primary: "#3b82f6" } })
|
|
|
14634
14839
|
|
|
14635
14840
|
// mode="system" auto-detects OS dark mode via prefers-color-scheme
|
|
14636
14841
|
// inversed flips the resolved mode (light↔dark)`,
|
|
14637
|
-
notes:
|
|
14638
|
-
mistakes: `- Using ThemeProvider + ModeProvider + ConfigProvider separately
|
|
14639
|
-
- Forgetting enrichTheme()
|
|
14842
|
+
notes: `Unified provider replacing the previous theme / mode / config split (3 nested providers became 1). Accepts an enriched \`theme\` object (merge with defaults via \`enrichTheme()\`), a \`mode\` of \`'light' | 'dark' | 'system'\`, and an optional \`inversed\` flip. When \`mode='system'\`, the provider subscribes to \`matchMedia('(prefers-color-scheme: dark)')\` and re-resolves the mode reactively. Calls \`init()\` internally so consumers don\'t need to wire it up themselves. Whole-theme swaps (user-preference themes) propagate through the styler resolver and re-resolve CSS without remounting the VNode. See also: useMode, enrichTheme, init.`,
|
|
14843
|
+
mistakes: `- Using \`ThemeProvider\` + \`ModeProvider\` + \`ConfigProvider\` separately — \`PyreonUI\` is the single replacement covering all three
|
|
14844
|
+
- Forgetting \`enrichTheme()\` — raw theme objects miss default breakpoints / spacing / unit utilities
|
|
14845
|
+
- Destructuring \`props\` inside the provider — components run once; destructuring captures values at setup. Read \`props.mode\` lazily inside reactive scopes
|
|
14846
|
+
- Re-augmenting the \`ThemeDefault\` / \`StylesDefault\` interfaces in your app — \`@pyreon/ui-theme\` already augments them; double-augmentation throws TS2320`
|
|
14640
14847
|
},
|
|
14641
14848
|
"ui-core/useMode": {
|
|
14642
|
-
signature:
|
|
14849
|
+
signature: `useMode(): Signal<'light' | 'dark'>`,
|
|
14643
14850
|
example: `import { useMode } from "@pyreon/ui-core"
|
|
14644
14851
|
|
|
14645
14852
|
const mode = useMode()
|
|
14646
14853
|
// mode() returns "light" or "dark" (resolved, reactive)
|
|
14647
14854
|
// Reflects OS preference when PyreonUI mode="system"`,
|
|
14648
|
-
notes:
|
|
14855
|
+
notes: `Returns the currently resolved mode as a reactive signal — \`'light'\` or \`'dark'\`. When the nearest \`PyreonUI\` ancestor uses \`mode='system'\`, the signal reflects the OS preference and updates when the user changes their system setting. When \`inversed\` is true on any ancestor, the mode is flipped before resolution. Component-scoped subscription — readers re-run only when the resolved mode actually changes. See also: PyreonUI.`,
|
|
14856
|
+
mistakes: `- Reading \`useMode()\` without calling it — the value is a \`Signal\`; use \`mode()\` to read
|
|
14857
|
+
- Using \`useMode()\` outside any \`PyreonUI\` ancestor — falls back to a default but loses the reactive system / inversed handling`
|
|
14649
14858
|
},
|
|
14650
14859
|
"unistyle/enrichTheme": {
|
|
14651
14860
|
signature: "enrichTheme(theme: PartialTheme): Theme",
|
|
@@ -14657,15 +14866,97 @@ const theme = enrichTheme({
|
|
|
14657
14866
|
})
|
|
14658
14867
|
|
|
14659
14868
|
// Merges user overrides with default breakpoints, spacing, and units`,
|
|
14660
|
-
notes: "
|
|
14869
|
+
notes: "Merge a partial theme with the full default theme (breakpoints, spacing, unit utilities, fallback colors). Always call this before passing a user theme to `PyreonUI` — raw theme objects miss the default breakpoints and spacing scale that the rest of the UI system reads from. Idempotent: enriching an already-enriched theme is a no-op. See also: breakpoints, createMediaQueries.",
|
|
14870
|
+
mistakes: `- Passing the raw partial theme to \`<PyreonUI theme={...}>\` without enriching — \`theme.breakpoints\` is undefined and every responsive prop falls back to the desktop value
|
|
14871
|
+
- Mutating the theme after passing it to \`PyreonUI\` — the styler resolver caches off the theme identity; clone + re-enrich for whole-theme swaps`
|
|
14872
|
+
},
|
|
14873
|
+
"unistyle/breakpoints": {
|
|
14874
|
+
signature: "breakpoints(): Breakpoints",
|
|
14875
|
+
example: `import { breakpoints } from '@pyreon/unistyle'
|
|
14876
|
+
|
|
14877
|
+
const bp = breakpoints()
|
|
14878
|
+
// { xs: 0, sm: 640, md: 768, lg: 1024, xl: 1280, xxl: 1536 }`,
|
|
14879
|
+
notes: "Return the default breakpoint set keyed by name (`xs`, `sm`, `md`, `lg`, `xl`, `xxl`) with min-width values in pixels. The same map is folded into `enrichTheme()` output, so most consumers read `theme.breakpoints` rather than calling this directly. Use it when you need the defaults outside a theme context (e.g. building a custom theme programmatically). See also: enrichTheme, createMediaQueries."
|
|
14880
|
+
},
|
|
14881
|
+
"unistyle/createMediaQueries": {
|
|
14882
|
+
signature: "createMediaQueries(breakpoints: Breakpoints): Record<string, string>",
|
|
14883
|
+
example: `import { createMediaQueries, breakpoints } from '@pyreon/unistyle'
|
|
14884
|
+
|
|
14885
|
+
const queries = createMediaQueries(breakpoints())
|
|
14886
|
+
// { xs: '@media (min-width: 0)', sm: '@media (min-width: 640px)', md: '@media (min-width: 768px)', ... }`,
|
|
14887
|
+
notes: "Build a record of media-query strings keyed by breakpoint name. Each value is a `min-width` query — `xs` is `(min-width: 0)`, `sm` becomes `(min-width: 640px)`, and so on. Used internally by `makeItResponsive()`; expose to consumers when they need to compose custom CSS-in-JS rules outside the responsive-prop pipeline. See also: breakpoints, makeItResponsive."
|
|
14888
|
+
},
|
|
14889
|
+
"unistyle/makeItResponsive": {
|
|
14890
|
+
signature: "makeItResponsive<T>(options: { value: T | T[] | Record<string, T>; property: string; theme: Theme }): string",
|
|
14891
|
+
example: `import { makeItResponsive } from '@pyreon/unistyle'
|
|
14892
|
+
|
|
14893
|
+
makeItResponsive({ value: 16, property: 'padding', theme })
|
|
14894
|
+
// → 'padding: 16px;'
|
|
14895
|
+
|
|
14896
|
+
makeItResponsive({ value: [8, 12, 16], property: 'padding', theme })
|
|
14897
|
+
// → 'padding: 8px; @media (min-width: 640px) { padding: 12px } @media (min-width: 768px) { padding: 16px }'
|
|
14898
|
+
|
|
14899
|
+
makeItResponsive({ value: { xs: 8, md: 16, xl: 24 }, property: 'padding', theme })
|
|
14900
|
+
// → '@media (min-width: 0) { padding: 8px } @media (min-width: 768px) { padding: 16px } @media (min-width: 1280px) { padding: 24px }'`,
|
|
14901
|
+
notes: "Resolve a responsive prop value to CSS for the current screen. Accepts three input shapes: single value (applies at all breakpoints), mobile-first array `[xs, sm, md, lg]` (each entry maps to the next breakpoint), or breakpoint object `{ xs: ..., md: ..., xl: ... }` (named keys map directly). The output is a CSS string with media queries already embedded; insert into a styled component template literal. See also: createMediaQueries, styles.",
|
|
14902
|
+
mistakes: `- Passing CSS-spec property names (\`borderTopWidth\`) — unistyle uses property-first naming (\`borderWidthTop\`); the responsive transformer expects the unistyle convention
|
|
14903
|
+
- Forgetting to pass an enriched theme — without \`theme.breakpoints\`, the array form falls back to the first value at every breakpoint`
|
|
14904
|
+
},
|
|
14905
|
+
"unistyle/styles": {
|
|
14906
|
+
signature: "styles(theme: Theme): string",
|
|
14907
|
+
example: `import { styles, enrichTheme } from '@pyreon/unistyle'
|
|
14908
|
+
|
|
14909
|
+
const theme = enrichTheme({ colors: { primary: '#3b82f6' } })
|
|
14910
|
+
const css = styles(theme)
|
|
14911
|
+
// → ':root { --color-primary: #3b82f6; --spacing-xs: 4px; ... }'`,
|
|
14912
|
+
notes: `Generate the CSS string for a complete theme — colors, spacing, fonts, breakpoints, the works. Used to produce the cascade of CSS variables / global declarations that backs every styled component. Most consumers don\'t call this directly; the \`PyreonUI\` provider invokes it internally on theme mount. See also: enrichTheme, extendCss.`
|
|
14913
|
+
},
|
|
14914
|
+
"unistyle/alignContent": {
|
|
14915
|
+
signature: `alignContent(options: { alignX?: AlignXKey; alignY?: AlignYKey; direction?: 'row' | 'column' | 'inline' | 'rows' }): string`,
|
|
14916
|
+
example: `import { alignContent } from '@pyreon/unistyle'
|
|
14917
|
+
|
|
14918
|
+
alignContent({ alignX: 'center', alignY: 'start', direction: 'row' })
|
|
14919
|
+
// → 'justify-content: center; align-items: flex-start;'
|
|
14920
|
+
|
|
14921
|
+
alignContent({ alignX: 'spaceBetween', direction: 'inline' })
|
|
14922
|
+
// → 'justify-content: space-between;'`,
|
|
14923
|
+
notes: `Resolve \`alignX\` / \`alignY\` / \`direction\` shorthand to the matching flex / grid CSS (\`justify-content\`, \`align-items\`). The Element / Row / Column primitives use this internally — it\'s exposed for custom layout components that want the same alignment semantics. \`direction: "inline"\` maps to \`row\`; \`direction: "rows"\` maps to \`column\`. See also: makeItResponsive.`
|
|
14924
|
+
},
|
|
14925
|
+
"unistyle/extendCss": {
|
|
14926
|
+
signature: "extendCss(base: ExtendCss, override?: ExtendCss): ExtendCss",
|
|
14927
|
+
example: `import { extendCss } from '@pyreon/unistyle'
|
|
14928
|
+
|
|
14929
|
+
const base = { color: 'red', hover: { color: 'darkred' } }
|
|
14930
|
+
const extended = extendCss(base, { hover: { background: 'pink' } })
|
|
14931
|
+
// → { color: 'red', hover: { color: 'darkred', background: 'pink' } }`,
|
|
14932
|
+
notes: "Extend a CSS definition (theme block, style descriptor) with overrides — deep-merges nested objects without losing the base. Used by rocketstyle dimension chains to layer dimension-specific CSS over a baseline. The base is not mutated; the result is a new object. See also: styles."
|
|
14933
|
+
},
|
|
14934
|
+
"unistyle/stripUnit": {
|
|
14935
|
+
signature: "stripUnit(value: string | number): number",
|
|
14936
|
+
example: `import { stripUnit } from '@pyreon/unistyle'
|
|
14937
|
+
|
|
14938
|
+
stripUnit('16px') // → 16
|
|
14939
|
+
stripUnit('1.5rem') // → 1.5
|
|
14940
|
+
stripUnit(16) // → 16`,
|
|
14941
|
+
notes: "Strip the unit suffix from a CSS value and return the numeric part (`\"16px\"` → `16`, `\"1.5rem\"` → `1.5`). Returns the input unchanged when already a number. Useful for arithmetic on theme values declared as strings (`\"16px\"`) without manually parsing. See also: value, values."
|
|
14942
|
+
},
|
|
14943
|
+
"unistyle/value": {
|
|
14944
|
+
signature: "value(input: PropertyValue, fallback?: PropertyValue): UnitValue",
|
|
14945
|
+
example: `import { value } from '@pyreon/unistyle'
|
|
14946
|
+
|
|
14947
|
+
value(16) // → { value: 16, unit: 'px' }
|
|
14948
|
+
value('1.5rem') // → { value: 1.5, unit: 'rem' }
|
|
14949
|
+
value('50%') // → { value: 50, unit: '%' }
|
|
14950
|
+
value('garbage', 0) // → { value: 0, unit: 'px' }`,
|
|
14951
|
+
notes: "Parse and validate a single property value into a `UnitValue` shape (`{ value, unit }`). Accepts numbers (treated as pixels), strings with units (`\"16px\"`, `\"1rem\"`, `\"50%\"`), or objects already in `UnitValue` form. Optional `fallback` is returned when the input is invalid. The companion `values()` does the same over an array. See also: stripUnit, values."
|
|
14661
14952
|
},
|
|
14662
14953
|
"rx/rx": {
|
|
14663
|
-
signature: "Readonly<{ filter, map, sortBy, groupBy, keyBy, uniqBy, take, skip, last, chunk, flatten, find, mapValues, count, sum, min, max, average, distinct, scan, combine, debounce, throttle, search, pipe }>",
|
|
14954
|
+
signature: "Readonly<{ filter, map, sortBy, groupBy, keyBy, uniqBy, take, skip, last, chunk, flatten, find, mapValues, first, compact, reverse, partition, takeWhile, dropWhile, unique, sample, count, sum, min, max, average, reduce, every, some, distinct, scan, combine, zip, merge, debounce, throttle, search, pipe }>",
|
|
14664
14955
|
example: `const active = rx.filter(users, u => u.active) // Computed<User[]>
|
|
14665
14956
|
const sorted = rx.sortBy(active, 'name') // Computed<User[]>
|
|
14666
14957
|
const total = rx.sum(users, u => u.age) // Computed<number>
|
|
14667
14958
|
const grouped = rx.groupBy(users, u => u.department) // Computed<Map<string, User[]>>`,
|
|
14668
|
-
notes: "Namespaced object exposing all
|
|
14959
|
+
notes: "Namespaced object exposing all 37 reactive transform functions plus `pipe`. Use `rx.filter(...)` for dot-notation style, or destructure individual functions for tree-shaking. Every function is overloaded: `Signal<T[]>` input produces `Computed<T[]>` that auto-tracks, plain `T[]` input produces a static result. See also: pipe, filter.",
|
|
14669
14960
|
mistakes: `- Expecting \`rx.filter(signal, pred)\` to return a plain array — signal inputs always produce \`Computed\` outputs. Call the result to read: \`active()\`
|
|
14670
14961
|
- Passing a signal accessor (\`() => items()\`) instead of the signal itself — pass \`items\` not \`() => items()\`; the function checks for \`.subscribe\` to detect signals`
|
|
14671
14962
|
},
|
|
@@ -14759,39 +15050,22 @@ setUrlRouter(router)
|
|
|
14759
15050
|
} from '@pyreon/document-primitives'
|
|
14760
15051
|
import { download } from '@pyreon/document'
|
|
14761
15052
|
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
)
|
|
14779
|
-
}
|
|
14780
|
-
|
|
14781
|
-
// One-step extraction. The two-step createDocumentExport(...).getDocNode()
|
|
14782
|
-
// form is still exported for callers that want to pass the helper
|
|
14783
|
-
// object around, but extractDocNode is the recommended form.
|
|
14784
|
-
const tree = extractDocNode(() => <ResumeTemplate resume={store.resume} />)
|
|
14785
|
-
await download(tree, 'resume.pdf')
|
|
14786
|
-
await download(tree, 'resume.docx')
|
|
14787
|
-
await download(tree, 'resume.html')
|
|
14788
|
-
await download(tree, 'resume.md')`,
|
|
14789
|
-
notes: "18 primitives: DocDocument, DocPage, DocSection, DocRow, DocColumn, DocHeading, DocText, DocLink, DocImage, DocTable, DocList, DocListItem, DocCode, DocDivider, DocSpacer, DocButton, DocQuote, DocPageBreak. Same component tree renders in browser AND exports — primitives carry _documentType statics that extractDocumentTree (from @pyreon/connector-document) walks to produce a DocNode for @pyreon/document's render() to consume. DocDocument's title/author/subject accept either a string OR a `() => string` accessor; function values are stored in _documentProps and resolved at extraction time so reactive metadata works without `const initial = get()` workarounds. PR #197 also fixed a latent bug in extractDocumentTree: it now CALLS rocketstyle component functions to read post-attrs _documentProps, where before it only looked at the JSX vnode's props directly — every primitive's metadata was silently dropped during export until that fix landed.",
|
|
14790
|
-
mistakes: `- Calling props.title() at the top of a template body to "fix" reactivity — components run ONCE at mount, so this captures the initial value forever. Pass the accessor through to DocDocument as-is: <DocDocument title={() => get().name}>
|
|
14791
|
-
- DocRow direction: layout props (direction, gap) go in .attrs() not .theme(). Element accepts 'inline' | 'rows' | 'reverseInline' | 'reverseRows' — 'row' is NOT valid
|
|
14792
|
-
- For text children reactivity, pass a signal accessor and read inside body: <DocText>{() => store.field()}</DocText>
|
|
14793
|
-
- Don't declare runtime-filled fields (tag, _documentProps) in the rocketstyle .attrs<P>() generic — they leak as required JSX props
|
|
14794
|
-
- Using createDocumentExport(...).getDocNode() in new code — prefer extractDocNode(fn) which is one call instead of two. createDocumentExport is kept for backward compat`
|
|
15053
|
+
const tree = extractDocNode(() => (
|
|
15054
|
+
<DocDocument title="Quarterly Report" author="Aisha">
|
|
15055
|
+
<DocPage>
|
|
15056
|
+
<DocHeading level="h1">Q4 Results</DocHeading>
|
|
15057
|
+
<DocText>Revenue grew 23% YoY.</DocText>
|
|
15058
|
+
</DocPage>
|
|
15059
|
+
</DocDocument>
|
|
15060
|
+
))
|
|
15061
|
+
await download(tree, 'report.pdf')
|
|
15062
|
+
await download(tree, 'report.docx')`,
|
|
15063
|
+
notes: `18 primitives: \`DocDocument\`, \`DocPage\`, \`DocSection\`, \`DocRow\`, \`DocColumn\`, \`DocHeading\`, \`DocText\`, \`DocLink\`, \`DocImage\`, \`DocTable\`, \`DocList\`, \`DocListItem\`, \`DocCode\`, \`DocDivider\`, \`DocSpacer\`, \`DocButton\`, \`DocQuote\`, \`DocPageBreak\`. Same component tree renders in browser AND exports — primitives carry \`_documentType\` statics that \`extractDocumentTree\` (from \`@pyreon/connector-document\`) walks to produce a \`DocNode\` for \`@pyreon/document\`\'s \`render()\` to consume. \`DocDocument\`\'s \`title\` / \`author\` / \`subject\` accept either a string OR a \`() => string\` accessor; function values are stored in \`_documentProps\` and resolved at extraction time so reactive metadata works without \`const initial = get()\` workarounds. PR #197 also fixed a latent bug in \`extractDocumentTree\`: it now CALLS rocketstyle component functions to read post-attrs \`_documentProps\`, where before it only looked at the JSX vnode\'s props directly — every primitive\'s metadata was silently dropped during export until that fix landed. See also: createDocumentExport.`,
|
|
15064
|
+
mistakes: `- Calling \`props.title()\` at the top of a template body to "fix" reactivity — components run ONCE at mount, so this captures the initial value forever. Pass the accessor through to DocDocument as-is: \`<DocDocument title={() => get().name}>\`
|
|
15065
|
+
- DocRow direction: layout props (direction, gap) go in \`.attrs()\` not \`.theme()\`. Element accepts \`'inline'\` | \`'rows'\` | \`'reverseInline'\` | \`'reverseRows'\` — \`'row'\` is NOT valid
|
|
15066
|
+
- For text children reactivity, pass a signal accessor and read inside body: \`<DocText>{() => store.field()}</DocText>\`
|
|
15067
|
+
- Don't declare runtime-filled fields (\`tag\`, \`_documentProps\`) in the rocketstyle \`.attrs<P>()\` generic — they leak as required JSX props
|
|
15068
|
+
- Using \`createDocumentExport(...).getDocNode()\` in new code — prefer \`extractDocNode(fn)\` which is one call instead of two. \`createDocumentExport\` is kept for backward compat`
|
|
14795
15069
|
},
|
|
14796
15070
|
"document-primitives/createDocumentExport": {
|
|
14797
15071
|
signature: "createDocumentExport(templateFn: () => VNode): { getDocNode(): DocNode }",
|
|
@@ -14801,10 +15075,651 @@ import { createDocumentExport } from '@pyreon/document-primitives'
|
|
|
14801
15075
|
|
|
14802
15076
|
const helper = createDocumentExport(() => <Resume name="Aisha" />)
|
|
14803
15077
|
const tree = helper.getDocNode()`,
|
|
14804
|
-
notes: "Wrapper around extractDocNode
|
|
15078
|
+
notes: "Wrapper around `extractDocNode`. The wrapper-object form is kept for callers that want to pass the helper around (e.g. to wrapper components that take a `DocumentExport` instance). New code should use `extractDocNode(templateFn)` which is one call instead of two. See also: extractDocNode."
|
|
15079
|
+
},
|
|
15080
|
+
"document-primitives/DocDocument": {
|
|
15081
|
+
signature: "(props: { title?: string | (() => string); author?: string | (() => string); subject?: string | (() => string); children: VNodeChild }) => VNodeChild",
|
|
15082
|
+
example: `<DocDocument title="Quarterly Report" author="Aisha" subject="Q4 2025">
|
|
15083
|
+
<DocPage>...</DocPage>
|
|
15084
|
+
</DocDocument>
|
|
15085
|
+
|
|
15086
|
+
// Reactive metadata via accessor
|
|
15087
|
+
<DocDocument title={() => \`\${user().name} — Resume\`}>
|
|
15088
|
+
<DocPage>...</DocPage>
|
|
15089
|
+
</DocDocument>`,
|
|
15090
|
+
notes: "Root container for a document tree — produces a `_documentType: \"document\"` node. Accepts optional metadata: `title`, `author`, `subject`. Each accepts either a plain string OR a `() => string` accessor; function values are stored in `_documentProps` and resolved at extraction time so each export call reads the LIVE value from any underlying signal. See also: DocPage, extractDocNode."
|
|
15091
|
+
},
|
|
15092
|
+
"document-primitives/DocPage": {
|
|
15093
|
+
signature: `(props: { size?: string; orientation?: 'portrait' | 'landscape'; children: VNodeChild }) => VNodeChild`,
|
|
15094
|
+
example: `<DocDocument>
|
|
15095
|
+
<DocPage size="A4" orientation="portrait">
|
|
15096
|
+
<DocHeading level="h1">Page 1</DocHeading>
|
|
15097
|
+
</DocPage>
|
|
15098
|
+
<DocPage size="A4" orientation="landscape">
|
|
15099
|
+
<DocHeading level="h1">Page 2 — landscape</DocHeading>
|
|
15100
|
+
</DocPage>
|
|
15101
|
+
</DocDocument>`,
|
|
15102
|
+
notes: "A page boundary inside a `DocDocument`. Paginated outputs (PDF, DOCX) treat each `DocPage` as a separate page; flow outputs (HTML, Markdown) render the contents inline with no page boundary. `size` and `orientation` configure paginated formats — common values: `\"A4\"`, `\"Letter\"`, `\"Legal\"`. See also: DocDocument, DocPageBreak."
|
|
15103
|
+
},
|
|
15104
|
+
"document-primitives/DocSection": {
|
|
15105
|
+
signature: `(props: { direction?: 'column' | 'row'; children: VNodeChild }) => VNodeChild`,
|
|
15106
|
+
example: `<DocPage>
|
|
15107
|
+
<DocSection direction="column">
|
|
15108
|
+
<DocHeading level="h2">Introduction</DocHeading>
|
|
15109
|
+
<DocText>Background paragraph.</DocText>
|
|
15110
|
+
</DocSection>
|
|
15111
|
+
</DocPage>`,
|
|
15112
|
+
notes: "Semantic grouping inside a page. Default `direction` is `\"column\"` (children stack vertically); `\"row\"` arranges them horizontally. Use to group related content for visual rhythm and for export targets that emit semantic section markers (HTML `<section>`, DOCX section breaks). See also: DocRow, DocColumn."
|
|
15113
|
+
},
|
|
15114
|
+
"document-primitives/DocRow": {
|
|
15115
|
+
signature: "(props: { children: VNodeChild }) => VNodeChild",
|
|
15116
|
+
example: `<DocRow>
|
|
15117
|
+
<DocText>Name:</DocText>
|
|
15118
|
+
<DocText>Aisha Patel</DocText>
|
|
15119
|
+
</DocRow>`,
|
|
15120
|
+
notes: "Horizontal layout container — children flow inline with a fixed 8px gap. Use for side-by-side content (label + value pairs, columns of metadata, button rows). Layout-only — no user-configurable props on this primitive; for columns with custom widths use `DocColumn` inside. See also: DocColumn, DocSection."
|
|
15121
|
+
},
|
|
15122
|
+
"document-primitives/DocColumn": {
|
|
15123
|
+
signature: "(props: { width?: number | string; children: VNodeChild }) => VNodeChild",
|
|
15124
|
+
example: `<DocRow>
|
|
15125
|
+
<DocColumn width="30%">
|
|
15126
|
+
<DocText>Label</DocText>
|
|
15127
|
+
</DocColumn>
|
|
15128
|
+
<DocColumn width="70%">
|
|
15129
|
+
<DocText>Value</DocText>
|
|
15130
|
+
</DocColumn>
|
|
15131
|
+
</DocRow>`,
|
|
15132
|
+
notes: `A column inside a row layout. Optional \`width\` controls the column\'s share of the row — accepts a number (interpreted as pixels) or a string (\`"50%"\`, \`"1fr"\`). When omitted, columns share available width equally. Most common shape is \`<DocRow><DocColumn width="30%" /> <DocColumn width="70%" /></DocRow>\`. See also: DocRow, DocSection.`
|
|
15133
|
+
},
|
|
15134
|
+
"document-primitives/DocHeading": {
|
|
15135
|
+
signature: `(props: { level?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; children: VNodeChild }) => VNodeChild`,
|
|
15136
|
+
example: `<DocHeading level="h1">Quarterly Report</DocHeading>
|
|
15137
|
+
<DocHeading level="h2">Q4 Results</DocHeading>
|
|
15138
|
+
<DocHeading level="h3">Revenue Breakdown</DocHeading>`,
|
|
15139
|
+
notes: "Heading text — `level` (`\"h1\"` through `\"h6\"`) controls both visual size and the semantic level emitted to outputs (HTML `<h1>...<h6>`, DOCX heading styles, Markdown `#`...`######`). Default `level` is `\"h1\"`. Used for document structure that downstream tooling can build a TOC from. See also: DocText, DocSection."
|
|
15140
|
+
},
|
|
15141
|
+
"document-primitives/DocText": {
|
|
15142
|
+
signature: "(props: { children: VNodeChild }) => VNodeChild",
|
|
15143
|
+
example: `<DocText>Static paragraph content.</DocText>
|
|
15144
|
+
|
|
15145
|
+
// Reactive children
|
|
15146
|
+
<DocText>{() => \`Hello, \${user().name}\`}</DocText>`,
|
|
15147
|
+
notes: "Paragraph / inline text. The most common primitive — wraps any text content for the document. Children may be string literals OR signal accessors (`{() => store.field()}`) for reactive content. Visual styling (font weight, variant) is controlled via rocketstyle dimension props on the wrapping component definition. See also: DocHeading, DocLink."
|
|
15148
|
+
},
|
|
15149
|
+
"document-primitives/DocLink": {
|
|
15150
|
+
signature: "(props: { href?: string; children: VNodeChild }) => VNodeChild",
|
|
15151
|
+
example: `<DocText>
|
|
15152
|
+
Read more on
|
|
15153
|
+
<DocLink href="https://pyreon.dev">our blog</DocLink>
|
|
15154
|
+
for the latest releases.
|
|
15155
|
+
</DocText>`,
|
|
15156
|
+
notes: "Hyperlink within text. `href` is the URL — defaults to `\"#\"`. Outputs that support hyperlinks (HTML, PDF, DOCX, email) render this as a clickable link; flat outputs (plain text, certain Slack variants) render the link target inline as `text (href)`. See also: DocText."
|
|
15157
|
+
},
|
|
15158
|
+
"document-primitives/DocImage": {
|
|
15159
|
+
signature: "(props: { src: string; alt?: string; width?: number; height?: number; caption?: string }) => VNodeChild",
|
|
15160
|
+
example: `<DocImage
|
|
15161
|
+
src="/charts/q4-revenue.png"
|
|
15162
|
+
alt="Revenue grew 23% in Q4"
|
|
15163
|
+
width={600}
|
|
15164
|
+
height={400}
|
|
15165
|
+
caption="Figure 1: Quarterly revenue, 2024-2025"
|
|
15166
|
+
/>`,
|
|
15167
|
+
notes: "An image embedded in the document. `src` is the image URL or data URI. `alt` is the accessible description (also used as fallback text in non-visual outputs). `width` / `height` constrain dimensions in pixels. Optional `caption` renders a caption beneath the image. See also: DocCode."
|
|
15168
|
+
},
|
|
15169
|
+
"document-primitives/DocTable": {
|
|
15170
|
+
signature: "(props: { columns: TableColumn[]; rows: TableRow[]; headerStyle?: object; striped?: boolean; bordered?: boolean; caption?: string }) => VNodeChild",
|
|
15171
|
+
example: `<DocTable
|
|
15172
|
+
caption="Q4 results by region"
|
|
15173
|
+
bordered
|
|
15174
|
+
striped
|
|
15175
|
+
columns={[
|
|
15176
|
+
{ key: 'region', label: 'Region', align: 'left' },
|
|
15177
|
+
{ key: 'revenue', label: 'Revenue', align: 'right' },
|
|
15178
|
+
{ key: 'growth', label: 'YoY Growth', align: 'right' },
|
|
15179
|
+
]}
|
|
15180
|
+
rows={[
|
|
15181
|
+
{ region: 'NA', revenue: '$12.4M', growth: '+23%' },
|
|
15182
|
+
{ region: 'EU', revenue: '$8.7M', growth: '+18%' },
|
|
15183
|
+
{ region: 'APAC', revenue: '$5.1M', growth: '+41%' },
|
|
15184
|
+
]}
|
|
15185
|
+
/>`,
|
|
15186
|
+
notes: "Tabular data. `columns` defines the header cells (label, key, optional alignment). `rows` is an array of data rows keyed by column key. `striped` adds alternating row backgrounds; `bordered` adds cell borders; `caption` renders an accessible table caption. Both `rows` and `columns` are filtered before reaching the DOM via `.attrs(..., { filter: [...] })` because `HTMLTableElement.rows` / `.cells` are read-only DOM properties — assignment would crash. See also: DocList, DocSection."
|
|
15187
|
+
},
|
|
15188
|
+
"document-primitives/DocList": {
|
|
15189
|
+
signature: "(props: { ordered?: boolean; children: VNodeChild }) => VNodeChild",
|
|
15190
|
+
example: `<DocList>
|
|
15191
|
+
<DocListItem>First bullet</DocListItem>
|
|
15192
|
+
<DocListItem>Second bullet</DocListItem>
|
|
15193
|
+
</DocList>
|
|
15194
|
+
|
|
15195
|
+
<DocList ordered>
|
|
15196
|
+
<DocListItem>First step</DocListItem>
|
|
15197
|
+
<DocListItem>Second step</DocListItem>
|
|
15198
|
+
</DocList>`,
|
|
15199
|
+
notes: "Bulleted (default) or numbered (`ordered`) list. Children are typically `DocListItem` instances. Outputs map this to the right native list type — HTML `<ul>` / `<ol>`, Markdown `-` / `1.`, DOCX list styles. See also: DocListItem."
|
|
15200
|
+
},
|
|
15201
|
+
"document-primitives/DocListItem": {
|
|
15202
|
+
signature: "(props: { children: VNodeChild }) => VNodeChild",
|
|
15203
|
+
example: `<DocList>
|
|
15204
|
+
<DocListItem>Top-level item</DocListItem>
|
|
15205
|
+
<DocListItem>
|
|
15206
|
+
Item with nested list
|
|
15207
|
+
<DocList>
|
|
15208
|
+
<DocListItem>Nested A</DocListItem>
|
|
15209
|
+
<DocListItem>Nested B</DocListItem>
|
|
15210
|
+
</DocList>
|
|
15211
|
+
</DocListItem>
|
|
15212
|
+
</DocList>`,
|
|
15213
|
+
notes: `Single item inside a \`DocList\`. Children may be plain text, \`DocText\`, nested \`DocList\` for sublists, or any other inline primitive. Visual marker (bullet vs number) is decided by the parent list\'s \`ordered\` prop, not by the item. See also: DocList.`
|
|
15214
|
+
},
|
|
15215
|
+
"document-primitives/DocCode": {
|
|
15216
|
+
signature: "(props: { language?: string; children: VNodeChild }) => VNodeChild",
|
|
15217
|
+
example: `<DocCode language="typescript">{
|
|
15218
|
+
\`const flow = createFlow({
|
|
15219
|
+
nodes: [{ id: '1', position: { x: 0, y: 0 } }],
|
|
15220
|
+
edges: [],
|
|
15221
|
+
})\`
|
|
15222
|
+
}</DocCode>`,
|
|
15223
|
+
notes: "Monospace code block. Optional `language` hint enables syntax highlighting in outputs that support it (HTML via Prism / Shiki, Markdown fenced code blocks with language tag). Whitespace is preserved verbatim — pass code as a single string child to keep newlines. See also: DocText."
|
|
15224
|
+
},
|
|
15225
|
+
"document-primitives/DocDivider": {
|
|
15226
|
+
signature: "(props: { color?: string; thickness?: number }) => VNodeChild",
|
|
15227
|
+
example: `<DocText>Above the divider.</DocText>
|
|
15228
|
+
<DocDivider color="#e5e7eb" thickness={1} />
|
|
15229
|
+
<DocText>Below the divider.</DocText>`,
|
|
15230
|
+
notes: "Horizontal rule — visual section separator. `color` controls the line color (any CSS color string); `thickness` controls the line thickness in pixels. Outputs map this to native dividers — HTML `<hr>`, Markdown `---`, DOCX horizontal rule. See also: DocSpacer."
|
|
15231
|
+
},
|
|
15232
|
+
"document-primitives/DocSpacer": {
|
|
15233
|
+
signature: "(props: { height?: number }) => VNodeChild",
|
|
15234
|
+
example: `<DocSection>
|
|
15235
|
+
<DocHeading level="h2">Section A</DocHeading>
|
|
15236
|
+
<DocText>Content...</DocText>
|
|
15237
|
+
<DocSpacer height={32} />
|
|
15238
|
+
<DocHeading level="h2">Section B</DocHeading>
|
|
15239
|
+
<DocText>More content...</DocText>
|
|
15240
|
+
</DocSection>`,
|
|
15241
|
+
notes: "Vertical whitespace — adds a blank vertical gap. `height` is in pixels (default 16). Use to space out content beyond what `DocSection` / `DocPage` margins provide. In flow outputs this becomes a styled blank block; in plain-text outputs, a sequence of newlines. See also: DocDivider."
|
|
15242
|
+
},
|
|
15243
|
+
"document-primitives/DocButton": {
|
|
15244
|
+
signature: "(props: { href?: string; children: VNodeChild }) => VNodeChild",
|
|
15245
|
+
example: `<DocButton href="https://pyreon.dev/signup">
|
|
15246
|
+
Get started
|
|
15247
|
+
</DocButton>`,
|
|
15248
|
+
notes: "Call-to-action button. Renders as a styled clickable element in HTML / email outputs (mail-safe button table layout for email), and as a labeled link in PDF / DOCX. `href` is the action URL — defaults to `\"#\"`. Visual style (variant) is controlled via rocketstyle dimensions on the component definition. See also: DocLink."
|
|
15249
|
+
},
|
|
15250
|
+
"document-primitives/DocQuote": {
|
|
15251
|
+
signature: "(props: { borderColor?: string; children: VNodeChild }) => VNodeChild",
|
|
15252
|
+
example: `<DocQuote borderColor="#3b82f6">
|
|
15253
|
+
<DocText>"The best way to predict the future is to build it."</DocText>
|
|
15254
|
+
<DocText>— Aisha Patel, Q4 keynote</DocText>
|
|
15255
|
+
</DocQuote>`,
|
|
15256
|
+
notes: "Block quote — sets off a quoted passage with an indented left border. `borderColor` controls the indicator stripe (any CSS color). Outputs map this to native quote styling — HTML `<blockquote>`, Markdown `> ...`, DOCX quote style. See also: DocText."
|
|
15257
|
+
},
|
|
15258
|
+
"document-primitives/DocPageBreak": {
|
|
15259
|
+
signature: "() => VNodeChild",
|
|
15260
|
+
example: `<DocPage>
|
|
15261
|
+
<DocHeading level="h1">Section 1</DocHeading>
|
|
15262
|
+
<DocText>...long content...</DocText>
|
|
15263
|
+
<DocPageBreak />
|
|
15264
|
+
<DocHeading level="h1">Section 2 — new page</DocHeading>
|
|
15265
|
+
</DocPage>`,
|
|
15266
|
+
notes: "Explicit page boundary inside a `DocPage`. Forces the renderer to start a new page at this point in paginated outputs (PDF, DOCX). In flow outputs (HTML, Markdown), it renders as visible whitespace or is omitted entirely. Use for explicit pagination control beyond what `DocPage` boundaries already provide. See also: DocPage."
|
|
14805
15267
|
}
|
|
14806
15268
|
};
|
|
14807
15269
|
|
|
15270
|
+
//#endregion
|
|
15271
|
+
//#region src/changelog.ts
|
|
15272
|
+
/**
|
|
15273
|
+
* Changelog parser + registry for the `get_changelog` MCP tool (T2.5.8).
|
|
15274
|
+
*
|
|
15275
|
+
* AI agents often ask "what changed in @pyreon/X recently?" before
|
|
15276
|
+
* writing code against the package — a stale mental model is a
|
|
15277
|
+
* frequent bug source. `get_changelog` surfaces recent release notes
|
|
15278
|
+
* without the agent having to scrape `git log` or read raw markdown.
|
|
15279
|
+
*
|
|
15280
|
+
* The parser reads `CHANGELOG.md` files populated by changesets. Each
|
|
15281
|
+
* changeset-managed package has a predictable shape:
|
|
15282
|
+
*
|
|
15283
|
+
* # @pyreon/<name>
|
|
15284
|
+
*
|
|
15285
|
+
* ## <version>
|
|
15286
|
+
*
|
|
15287
|
+
* ### Minor Changes | Patch Changes | Major Changes
|
|
15288
|
+
*
|
|
15289
|
+
* - [#PR] [`sha`] Thanks [@author]! - <body>
|
|
15290
|
+
*
|
|
15291
|
+
* <continuation>
|
|
15292
|
+
*
|
|
15293
|
+
* - Updated dependencies [[`sha`]]:
|
|
15294
|
+
* - @pyreon/core@0.13.0
|
|
15295
|
+
*
|
|
15296
|
+
* The parser extracts each `## <version>` section, collecting the
|
|
15297
|
+
* user-facing body plus the dependency bumps separately. Consumers
|
|
15298
|
+
* typically want the N most recent non-empty versions (the tool's
|
|
15299
|
+
* default is 5).
|
|
15300
|
+
*/
|
|
15301
|
+
function findMonorepoRoot(startDir) {
|
|
15302
|
+
let dir = resolve(startDir);
|
|
15303
|
+
for (let i = 0; i < 30; i++) {
|
|
15304
|
+
if (existsSync(join(dir, "packages")) && statSync(join(dir, "packages")).isDirectory()) return dir;
|
|
15305
|
+
const parent = dirname(dir);
|
|
15306
|
+
if (parent === dir) return null;
|
|
15307
|
+
dir = parent;
|
|
15308
|
+
}
|
|
15309
|
+
return null;
|
|
15310
|
+
}
|
|
15311
|
+
function walkPackages(dir, out, depth = 0) {
|
|
15312
|
+
if (depth > 4) return;
|
|
15313
|
+
let entries;
|
|
15314
|
+
try {
|
|
15315
|
+
entries = readdirSync(dir);
|
|
15316
|
+
} catch {
|
|
15317
|
+
return;
|
|
15318
|
+
}
|
|
15319
|
+
for (const name of entries) {
|
|
15320
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
15321
|
+
const full = join(dir, name);
|
|
15322
|
+
let isDir = false;
|
|
15323
|
+
try {
|
|
15324
|
+
isDir = statSync(full).isDirectory();
|
|
15325
|
+
} catch {
|
|
15326
|
+
continue;
|
|
15327
|
+
}
|
|
15328
|
+
if (!isDir) continue;
|
|
15329
|
+
const pkgJsonPath = join(full, "package.json");
|
|
15330
|
+
const changelogPath = join(full, "CHANGELOG.md");
|
|
15331
|
+
if (existsSync(pkgJsonPath) && existsSync(changelogPath)) {
|
|
15332
|
+
try {
|
|
15333
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
15334
|
+
if (typeof pkg.name === "string") out.push({
|
|
15335
|
+
name: pkg.name,
|
|
15336
|
+
dir: full,
|
|
15337
|
+
changelogPath
|
|
15338
|
+
});
|
|
15339
|
+
} catch {}
|
|
15340
|
+
continue;
|
|
15341
|
+
}
|
|
15342
|
+
if (existsSync(pkgJsonPath)) continue;
|
|
15343
|
+
walkPackages(full, out, depth + 1);
|
|
15344
|
+
}
|
|
15345
|
+
}
|
|
15346
|
+
/**
|
|
15347
|
+
* Parse a CHANGELOG.md body into version entries. The parser assumes
|
|
15348
|
+
* the changesets format but tolerates:
|
|
15349
|
+
* - empty `## <version>` sections with no body (ceremonial bumps)
|
|
15350
|
+
* - mixed `### Patch Changes` / `### Minor Changes` / `### Major Changes` under one version
|
|
15351
|
+
* - bullets with multi-line continuations (indented with 2+ spaces)
|
|
15352
|
+
* - `- Updated dependencies …` bullets, split off separately
|
|
15353
|
+
*/
|
|
15354
|
+
function parseChangelog(body) {
|
|
15355
|
+
const lines = body.split("\n");
|
|
15356
|
+
const entries = [];
|
|
15357
|
+
let currentVersion = null;
|
|
15358
|
+
let currentBullets = [];
|
|
15359
|
+
let currentDepUpdates = [];
|
|
15360
|
+
let currentBuf = [];
|
|
15361
|
+
let bufKind = null;
|
|
15362
|
+
const flushBullet = () => {
|
|
15363
|
+
if (currentBuf.length > 0 && bufKind !== null) {
|
|
15364
|
+
const text = currentBuf.join("\n").trim();
|
|
15365
|
+
if (text.length > 0) if (bufKind === "dep") currentDepUpdates.push(text);
|
|
15366
|
+
else currentBullets.push(text);
|
|
15367
|
+
}
|
|
15368
|
+
currentBuf = [];
|
|
15369
|
+
bufKind = null;
|
|
15370
|
+
};
|
|
15371
|
+
const flushVersion = () => {
|
|
15372
|
+
flushBullet();
|
|
15373
|
+
if (currentVersion !== null) entries.push({
|
|
15374
|
+
version: currentVersion,
|
|
15375
|
+
changes: currentBullets,
|
|
15376
|
+
dependencyUpdates: currentDepUpdates,
|
|
15377
|
+
empty: currentBullets.length === 0 && currentDepUpdates.length === 0
|
|
15378
|
+
});
|
|
15379
|
+
currentVersion = null;
|
|
15380
|
+
currentBullets = [];
|
|
15381
|
+
currentDepUpdates = [];
|
|
15382
|
+
};
|
|
15383
|
+
for (const line of lines) {
|
|
15384
|
+
const versionMatch = /^## (.+)$/.exec(line);
|
|
15385
|
+
if (versionMatch) {
|
|
15386
|
+
flushVersion();
|
|
15387
|
+
currentVersion = versionMatch[1].trim();
|
|
15388
|
+
continue;
|
|
15389
|
+
}
|
|
15390
|
+
if (currentVersion === null) continue;
|
|
15391
|
+
if (/^### /.test(line)) {
|
|
15392
|
+
flushBullet();
|
|
15393
|
+
continue;
|
|
15394
|
+
}
|
|
15395
|
+
if (/^- /.test(line)) {
|
|
15396
|
+
flushBullet();
|
|
15397
|
+
currentBuf = [line.replace(/^- /, "")];
|
|
15398
|
+
bufKind = /^- Updated dependencies/.test(line) ? "dep" : "change";
|
|
15399
|
+
continue;
|
|
15400
|
+
}
|
|
15401
|
+
if (bufKind !== null && line.length > 0 && /^\s/.test(line)) {
|
|
15402
|
+
currentBuf.push(line.replace(/^ {2,4}/, ""));
|
|
15403
|
+
continue;
|
|
15404
|
+
}
|
|
15405
|
+
if (bufKind !== null && line === "") {
|
|
15406
|
+
currentBuf.push("");
|
|
15407
|
+
continue;
|
|
15408
|
+
}
|
|
15409
|
+
flushBullet();
|
|
15410
|
+
}
|
|
15411
|
+
flushVersion();
|
|
15412
|
+
return entries;
|
|
15413
|
+
}
|
|
15414
|
+
function loadChangelogRegistry(startDir = process.cwd()) {
|
|
15415
|
+
const root = findMonorepoRoot(startDir);
|
|
15416
|
+
if (!root) return {
|
|
15417
|
+
root: null,
|
|
15418
|
+
byName: /* @__PURE__ */ new Map()
|
|
15419
|
+
};
|
|
15420
|
+
const found = [];
|
|
15421
|
+
walkPackages(join(root, "packages"), found);
|
|
15422
|
+
const byName = /* @__PURE__ */ new Map();
|
|
15423
|
+
for (const { name, dir, changelogPath } of found) {
|
|
15424
|
+
let source;
|
|
15425
|
+
try {
|
|
15426
|
+
source = readFileSync(changelogPath, "utf8");
|
|
15427
|
+
} catch {
|
|
15428
|
+
continue;
|
|
15429
|
+
}
|
|
15430
|
+
byName.set(name, {
|
|
15431
|
+
packageName: name,
|
|
15432
|
+
path: changelogPath,
|
|
15433
|
+
dir,
|
|
15434
|
+
entries: parseChangelog(source)
|
|
15435
|
+
});
|
|
15436
|
+
}
|
|
15437
|
+
return {
|
|
15438
|
+
root,
|
|
15439
|
+
byName
|
|
15440
|
+
};
|
|
15441
|
+
}
|
|
15442
|
+
function findChangelog(registry, packageName) {
|
|
15443
|
+
if (registry.byName.has(packageName)) return registry.byName.get(packageName);
|
|
15444
|
+
if (!packageName.startsWith("@")) {
|
|
15445
|
+
const qualified = `@pyreon/${packageName}`;
|
|
15446
|
+
if (registry.byName.has(qualified)) return registry.byName.get(qualified);
|
|
15447
|
+
}
|
|
15448
|
+
return null;
|
|
15449
|
+
}
|
|
15450
|
+
function suggestChangelogs(registry, needle) {
|
|
15451
|
+
const lower = needle.toLowerCase();
|
|
15452
|
+
const matches = [];
|
|
15453
|
+
for (const name of registry.byName.keys()) if (name.toLowerCase().includes(lower)) matches.push(name);
|
|
15454
|
+
return matches.slice(0, 5);
|
|
15455
|
+
}
|
|
15456
|
+
/**
|
|
15457
|
+
* Compare two semver-ish version strings. Returns negative if `a < b`,
|
|
15458
|
+
* positive if `a > b`, zero if equal. Pre-release suffixes (`-alpha.3`)
|
|
15459
|
+
* are compared lexicographically AFTER the numeric portion.
|
|
15460
|
+
*
|
|
15461
|
+
* Scoped to what changesets actually produce (`0.13.0`, `1.0.0-alpha.3`,
|
|
15462
|
+
* `2.5.1`). Not a general-purpose semver implementation — we intentionally
|
|
15463
|
+
* do not depend on an external semver package for a 30-line need.
|
|
15464
|
+
*/
|
|
15465
|
+
function compareVersions(a, b) {
|
|
15466
|
+
const parse = (v) => {
|
|
15467
|
+
const [core, ...preParts] = v.split("-");
|
|
15468
|
+
const pre = preParts.join("-");
|
|
15469
|
+
return {
|
|
15470
|
+
parts: core.split(".").map((n) => {
|
|
15471
|
+
const num = Number.parseInt(n, 10);
|
|
15472
|
+
return Number.isNaN(num) ? 0 : num;
|
|
15473
|
+
}),
|
|
15474
|
+
pre
|
|
15475
|
+
};
|
|
15476
|
+
};
|
|
15477
|
+
const pa = parse(a);
|
|
15478
|
+
const pb = parse(b);
|
|
15479
|
+
const maxLen = Math.max(pa.parts.length, pb.parts.length);
|
|
15480
|
+
for (let i = 0; i < maxLen; i++) {
|
|
15481
|
+
const ai = pa.parts[i] ?? 0;
|
|
15482
|
+
const bi = pb.parts[i] ?? 0;
|
|
15483
|
+
if (ai !== bi) return ai - bi;
|
|
15484
|
+
}
|
|
15485
|
+
if (pa.pre === "" && pb.pre !== "") return 1;
|
|
15486
|
+
if (pa.pre !== "" && pb.pre === "") return -1;
|
|
15487
|
+
if (pa.pre < pb.pre) return -1;
|
|
15488
|
+
if (pa.pre > pb.pre) return 1;
|
|
15489
|
+
return 0;
|
|
15490
|
+
}
|
|
15491
|
+
/**
|
|
15492
|
+
* Filter entries to those strictly NEWER than `sinceVersion`. Ceremonial
|
|
15493
|
+
* bumps are preserved — the caller decides whether to also filter them
|
|
15494
|
+
* via the `empty` flag. Returns all entries in file order (newest first)
|
|
15495
|
+
* that satisfy `compareVersions(entry.version, sinceVersion) > 0`.
|
|
15496
|
+
*/
|
|
15497
|
+
function filterSince(entries, sinceVersion) {
|
|
15498
|
+
return entries.filter((e) => compareVersions(e.version, sinceVersion) > 0);
|
|
15499
|
+
}
|
|
15500
|
+
/**
|
|
15501
|
+
* Format a package's changelog for the MCP response. Filters empty
|
|
15502
|
+
* versions and slices to the `limit` most recent.
|
|
15503
|
+
*/
|
|
15504
|
+
function formatChangelog(changelog, { limit = 5, includeDependencyUpdates = false, since } = {}) {
|
|
15505
|
+
const nonEmpty = changelog.entries.filter((e) => !e.empty);
|
|
15506
|
+
const afterSince = since ? filterSince(nonEmpty, since) : nonEmpty;
|
|
15507
|
+
const sliced = afterSince.slice(0, limit);
|
|
15508
|
+
if (sliced.length === 0) {
|
|
15509
|
+
if (since) return `# ${changelog.packageName} — no changes since v${since}\n\nThe package has ${nonEmpty.length} substantive version entr${nonEmpty.length === 1 ? "y" : "ies"} in total, but none are newer than v${since}. The known latest substantive version is v${nonEmpty[0]?.version ?? "(none)"}. Drop the \`since\` filter, or pass a lower floor, to see earlier entries.`;
|
|
15510
|
+
const ceremonial = changelog.entries.length;
|
|
15511
|
+
const versions = changelog.entries.slice(0, 3).map((e) => e.version).join(", ");
|
|
15512
|
+
return `# ${changelog.packageName} — no substantive changes\n\nCHANGELOG.md has ${ceremonial} version entr${ceremonial === 1 ? "y" : "ies"} (${versions}${ceremonial > 3 ? ", …" : ""}) but every one is a ceremonial version bump with no user-facing body. This usually means the package only received dependency updates during the captured history. Check \`get_changelog({ includeDependencyUpdates: true })\` or the git log if you need the bumps themselves.`;
|
|
15513
|
+
}
|
|
15514
|
+
const parts = [];
|
|
15515
|
+
const sinceSuffix = since ? ` since v${since}` : "";
|
|
15516
|
+
parts.push(`# ${changelog.packageName} — changelog${sinceSuffix} (${sliced.length}/${afterSince.length} shown)`);
|
|
15517
|
+
parts.push("");
|
|
15518
|
+
if (afterSince.length > limit) {
|
|
15519
|
+
parts.push(`Showing the ${limit} most recent substantive versions${sinceSuffix}. ${afterSince.length - limit} older versions omitted. Pass \`limit\` to expand.`);
|
|
15520
|
+
parts.push("");
|
|
15521
|
+
}
|
|
15522
|
+
for (const entry of sliced) {
|
|
15523
|
+
parts.push(`## ${entry.version}`);
|
|
15524
|
+
parts.push("");
|
|
15525
|
+
for (const change of entry.changes) {
|
|
15526
|
+
parts.push(`- ${change}`);
|
|
15527
|
+
parts.push("");
|
|
15528
|
+
}
|
|
15529
|
+
if (includeDependencyUpdates && entry.dependencyUpdates.length > 0) {
|
|
15530
|
+
parts.push("### Updated dependencies");
|
|
15531
|
+
for (const dep of entry.dependencyUpdates) parts.push(`- ${dep}`);
|
|
15532
|
+
parts.push("");
|
|
15533
|
+
}
|
|
15534
|
+
}
|
|
15535
|
+
return parts.join("\n").trimEnd();
|
|
15536
|
+
}
|
|
15537
|
+
/**
|
|
15538
|
+
* Format the registry as an index when `get_changelog` is called
|
|
15539
|
+
* with no package name. Lists every package with its latest
|
|
15540
|
+
* substantive version (for orientation).
|
|
15541
|
+
*/
|
|
15542
|
+
function formatChangelogIndex(registry) {
|
|
15543
|
+
if (!registry.root || registry.byName.size === 0) return "No changelogs found. This tool reads CHANGELOG.md files from the Pyreon monorepo. If you are running the MCP in a consumer project without the Pyreon packages directory, run the MCP from the Pyreon repo root to browse releases.";
|
|
15544
|
+
const names = [...registry.byName.keys()].sort();
|
|
15545
|
+
const parts = [`# Pyreon Changelogs (${names.length} packages)`, ""];
|
|
15546
|
+
parts.push(`Call \`get_changelog({ package: "<name>" })\` for per-package release notes. Pass \`limit\` to control how many versions (default 5). The short form \`foo\` maps to \`@pyreon/foo\`.`);
|
|
15547
|
+
parts.push("");
|
|
15548
|
+
for (const name of names) {
|
|
15549
|
+
const latest = registry.byName.get(name).entries.find((e) => !e.empty);
|
|
15550
|
+
const summary = latest ? `latest substantive: v${latest.version}` : "ceremonial bumps only";
|
|
15551
|
+
parts.push(`- **${name}** — ${summary}`);
|
|
15552
|
+
}
|
|
15553
|
+
return parts.join("\n");
|
|
15554
|
+
}
|
|
15555
|
+
|
|
15556
|
+
//#endregion
|
|
15557
|
+
//#region src/patterns.ts
|
|
15558
|
+
/**
|
|
15559
|
+
* Pattern registry for the `get_pattern` MCP tool (T2.5.3).
|
|
15560
|
+
*
|
|
15561
|
+
* Each pattern answers a "how do I do X the right way" question with a
|
|
15562
|
+
* code example and rationale. The content is the body of the
|
|
15563
|
+
* corresponding `docs/patterns/<name>.md` file, discovered at runtime
|
|
15564
|
+
* by walking up from `process.cwd()` to the nearest repo that contains
|
|
15565
|
+
* `docs/patterns/`.
|
|
15566
|
+
*
|
|
15567
|
+
* Why a filesystem lookup instead of bundled content: the patterns
|
|
15568
|
+
* belong in the VitePress site (they're first-class docs), and having
|
|
15569
|
+
* the MCP fetch them live means the AI sees the same text the human
|
|
15570
|
+
* would. Bundling copies would drift.
|
|
15571
|
+
*
|
|
15572
|
+
* Fallback: if no `docs/patterns/` exists in the walk (e.g. the MCP is
|
|
15573
|
+
* running in a consumer repo), the tool reports the miss and lists
|
|
15574
|
+
* what patterns WOULD be available if running against the Pyreon
|
|
15575
|
+
* monorepo. The list itself is seeded from the directory walk, so
|
|
15576
|
+
* adding a new pattern file makes it discoverable without code changes.
|
|
15577
|
+
*/
|
|
15578
|
+
const PATTERN_PATH_CANDIDATES = [[
|
|
15579
|
+
"docs",
|
|
15580
|
+
"docs",
|
|
15581
|
+
"patterns"
|
|
15582
|
+
], ["docs", "patterns"]];
|
|
15583
|
+
function findPatternsDir(startDir) {
|
|
15584
|
+
let dir = resolve(startDir);
|
|
15585
|
+
for (let i = 0; i < 30; i++) {
|
|
15586
|
+
for (const segments of PATTERN_PATH_CANDIDATES) {
|
|
15587
|
+
const candidate = join(dir, ...segments);
|
|
15588
|
+
if (existsSync(candidate) && statSync(candidate).isDirectory()) return candidate;
|
|
15589
|
+
}
|
|
15590
|
+
const parent = dirname(dir);
|
|
15591
|
+
if (parent === dir) return null;
|
|
15592
|
+
dir = parent;
|
|
15593
|
+
}
|
|
15594
|
+
return null;
|
|
15595
|
+
}
|
|
15596
|
+
/**
|
|
15597
|
+
* Minimal frontmatter parser. Supports:
|
|
15598
|
+
* title: string (unquoted or quoted)
|
|
15599
|
+
* summary: string
|
|
15600
|
+
* seeAlso: [one, two, three] OR seeAlso:\n - one\n - two
|
|
15601
|
+
*
|
|
15602
|
+
* Anything else is ignored. Full YAML would be overkill here.
|
|
15603
|
+
*/
|
|
15604
|
+
function parseFrontmatter(source) {
|
|
15605
|
+
const match = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(source);
|
|
15606
|
+
if (!match) return {
|
|
15607
|
+
meta: {},
|
|
15608
|
+
body: source
|
|
15609
|
+
};
|
|
15610
|
+
const rawMeta = match[1];
|
|
15611
|
+
const body = match[2].trim();
|
|
15612
|
+
const meta = {};
|
|
15613
|
+
const lines = rawMeta.split("\n");
|
|
15614
|
+
let seeAlsoActive = false;
|
|
15615
|
+
const seeAlsoItems = [];
|
|
15616
|
+
for (const line of lines) {
|
|
15617
|
+
if (seeAlsoActive) {
|
|
15618
|
+
const bullet = /^\s*-\s*(.+?)\s*$/.exec(line);
|
|
15619
|
+
if (bullet) {
|
|
15620
|
+
seeAlsoItems.push(bullet[1]);
|
|
15621
|
+
continue;
|
|
15622
|
+
}
|
|
15623
|
+
seeAlsoActive = false;
|
|
15624
|
+
}
|
|
15625
|
+
const kv = /^([a-zA-Z]+):\s*(.*)$/.exec(line);
|
|
15626
|
+
if (!kv) continue;
|
|
15627
|
+
const key = kv[1];
|
|
15628
|
+
const value = kv[2].trim();
|
|
15629
|
+
if (key === "title") meta.title = value.replace(/^["']|["']$/g, "");
|
|
15630
|
+
else if (key === "summary") meta.summary = value.replace(/^["']|["']$/g, "");
|
|
15631
|
+
else if (key === "seeAlso") {
|
|
15632
|
+
if (value.startsWith("[") && value.endsWith("]")) meta.seeAlso = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
15633
|
+
else if (value === "") seeAlsoActive = true;
|
|
15634
|
+
}
|
|
15635
|
+
}
|
|
15636
|
+
if (seeAlsoActive && seeAlsoItems.length > 0) meta.seeAlso = seeAlsoItems;
|
|
15637
|
+
return {
|
|
15638
|
+
meta,
|
|
15639
|
+
body
|
|
15640
|
+
};
|
|
15641
|
+
}
|
|
15642
|
+
function extractFirstHeading(body) {
|
|
15643
|
+
for (const line of body.split("\n")) {
|
|
15644
|
+
const h = /^#\s+(.+)$/.exec(line);
|
|
15645
|
+
if (h) return h[1];
|
|
15646
|
+
}
|
|
15647
|
+
return null;
|
|
15648
|
+
}
|
|
15649
|
+
function loadPatternRegistry(startDir = process.cwd()) {
|
|
15650
|
+
const root = findPatternsDir(startDir);
|
|
15651
|
+
if (!root) return {
|
|
15652
|
+
root: null,
|
|
15653
|
+
patterns: []
|
|
15654
|
+
};
|
|
15655
|
+
const patterns = [];
|
|
15656
|
+
const entries = readdirSync(root).sort();
|
|
15657
|
+
for (const entry of entries) {
|
|
15658
|
+
if (!entry.endsWith(".md")) continue;
|
|
15659
|
+
if (entry.startsWith(".") || entry === "README.md" || entry === "index.md") continue;
|
|
15660
|
+
const filePath = join(root, entry);
|
|
15661
|
+
let source;
|
|
15662
|
+
try {
|
|
15663
|
+
source = readFileSync(filePath, "utf8");
|
|
15664
|
+
} catch {
|
|
15665
|
+
continue;
|
|
15666
|
+
}
|
|
15667
|
+
const { meta, body } = parseFrontmatter(source);
|
|
15668
|
+
const name = entry.replace(/\.md$/, "");
|
|
15669
|
+
const title = meta.title ?? extractFirstHeading(body) ?? name;
|
|
15670
|
+
patterns.push({
|
|
15671
|
+
name,
|
|
15672
|
+
path: filePath,
|
|
15673
|
+
body: source,
|
|
15674
|
+
title,
|
|
15675
|
+
summary: meta.summary ?? null,
|
|
15676
|
+
seeAlso: meta.seeAlso ?? []
|
|
15677
|
+
});
|
|
15678
|
+
}
|
|
15679
|
+
return {
|
|
15680
|
+
root,
|
|
15681
|
+
patterns
|
|
15682
|
+
};
|
|
15683
|
+
}
|
|
15684
|
+
/**
|
|
15685
|
+
* Format the registry as a short index listing for the "no arg" case.
|
|
15686
|
+
* Each entry: ` - slug — title (summary)`.
|
|
15687
|
+
*/
|
|
15688
|
+
function formatPatternIndex(registry) {
|
|
15689
|
+
if (!registry.root || registry.patterns.length === 0) return "No patterns found. Patterns live at `docs/docs/patterns/<name>.md` (the VitePress content directory) in the Pyreon monorepo. If you are running the MCP in a consumer project, patterns are not available locally — run the MCP in the Pyreon repo to browse them.";
|
|
15690
|
+
const parts = [`# Pyreon Patterns (${registry.patterns.length})`, ""];
|
|
15691
|
+
parts.push("Call `get_pattern({ name: \"<slug>\" })` for the full body. Each pattern shows the canonical \"do it this way\" with code + rationale, plus the anti-pattern to avoid.");
|
|
15692
|
+
parts.push("");
|
|
15693
|
+
for (const p of registry.patterns) {
|
|
15694
|
+
const summary = p.summary ? ` — ${p.summary}` : "";
|
|
15695
|
+
parts.push(`- **${p.name}** — ${p.title}${summary}`);
|
|
15696
|
+
}
|
|
15697
|
+
return parts.join("\n");
|
|
15698
|
+
}
|
|
15699
|
+
/**
|
|
15700
|
+
* Format a single pattern's full body for the MCP response. Prepends
|
|
15701
|
+
* a breadcrumb and appends a cross-reference footer if `seeAlso` was
|
|
15702
|
+
* populated.
|
|
15703
|
+
*/
|
|
15704
|
+
function formatPatternBody(pattern) {
|
|
15705
|
+
const parts = [pattern.body.trimEnd()];
|
|
15706
|
+
if (pattern.seeAlso.length > 0) {
|
|
15707
|
+
parts.push("");
|
|
15708
|
+
parts.push(`---\n\n**See also:** ${pattern.seeAlso.map((s) => `\`get_pattern({ name: "${s}" })\``).join(", ")}`);
|
|
15709
|
+
}
|
|
15710
|
+
return parts.join("\n");
|
|
15711
|
+
}
|
|
15712
|
+
function findPattern(registry, name) {
|
|
15713
|
+
for (const p of registry.patterns) if (p.name === name) return p;
|
|
15714
|
+
return null;
|
|
15715
|
+
}
|
|
15716
|
+
function suggestPatterns(registry, name) {
|
|
15717
|
+
const needle = name.toLowerCase();
|
|
15718
|
+
const matches = [];
|
|
15719
|
+
for (const p of registry.patterns) if (p.name.toLowerCase().includes(needle) || p.title.toLowerCase().includes(needle)) matches.push(p.name);
|
|
15720
|
+
return matches.slice(0, 5);
|
|
15721
|
+
}
|
|
15722
|
+
|
|
14808
15723
|
//#endregion
|
|
14809
15724
|
//#region src/index.ts
|
|
14810
15725
|
/**
|
|
@@ -14821,204 +15736,292 @@ const tree = helper.getDocNode()`,
|
|
|
14821
15736
|
* get_routes — List all routes in the current project
|
|
14822
15737
|
* get_components — List all components with their props and signals
|
|
14823
15738
|
* get_browser_smoke_status — Report which browser-categorized packages have smoke coverage
|
|
15739
|
+
* get_pattern — Fetch a "how do I do X" pattern body from docs/patterns/
|
|
15740
|
+
* get_anti_patterns — Browse the anti-patterns catalog, optionally filtered by category
|
|
15741
|
+
* get_changelog — Recent release notes for a @pyreon/* package, parsed from CHANGELOG.md
|
|
15742
|
+
* audit_test_environment — Scan test files for mock-vnode patterns (PR #197 bug class)
|
|
14824
15743
|
*
|
|
14825
15744
|
* Usage:
|
|
14826
15745
|
* bunx @pyreon/mcp # stdio transport (for IDE integration)
|
|
14827
15746
|
*/
|
|
14828
|
-
const server = new McpServer({
|
|
14829
|
-
name: "pyreon",
|
|
14830
|
-
version
|
|
14831
|
-
});
|
|
14832
|
-
let cachedContext = null;
|
|
14833
|
-
let contextCwd = process.cwd();
|
|
14834
|
-
function getContext() {
|
|
14835
|
-
if (!cachedContext || contextCwd !== process.cwd()) {
|
|
14836
|
-
contextCwd = process.cwd();
|
|
14837
|
-
cachedContext = generateContext(contextCwd);
|
|
14838
|
-
}
|
|
14839
|
-
return cachedContext;
|
|
14840
|
-
}
|
|
14841
15747
|
function textResult(text) {
|
|
14842
15748
|
return { content: [{
|
|
14843
15749
|
type: "text",
|
|
14844
15750
|
text
|
|
14845
15751
|
}] };
|
|
14846
15752
|
}
|
|
14847
|
-
|
|
14848
|
-
|
|
14849
|
-
|
|
14850
|
-
|
|
14851
|
-
|
|
14852
|
-
|
|
14853
|
-
|
|
14854
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
}
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
});
|
|
14885
|
-
server.tool("
|
|
14886
|
-
|
|
14887
|
-
|
|
14888
|
-
|
|
14889
|
-
const
|
|
14890
|
-
|
|
14891
|
-
|
|
14892
|
-
|
|
14893
|
-
|
|
14894
|
-
|
|
14895
|
-
|
|
14896
|
-
|
|
14897
|
-
|
|
14898
|
-
}
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
14904
|
-
|
|
14905
|
-
|
|
14906
|
-
|
|
14907
|
-
|
|
14908
|
-
|
|
14909
|
-
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
|
|
14919
|
-
|
|
14920
|
-
|
|
14921
|
-
|
|
14922
|
-
|
|
15753
|
+
function createServer() {
|
|
15754
|
+
const server = new McpServer({
|
|
15755
|
+
name: "pyreon",
|
|
15756
|
+
version
|
|
15757
|
+
});
|
|
15758
|
+
let cachedContext = null;
|
|
15759
|
+
let contextCwd = process.cwd();
|
|
15760
|
+
function getContext() {
|
|
15761
|
+
if (!cachedContext || contextCwd !== process.cwd()) {
|
|
15762
|
+
contextCwd = process.cwd();
|
|
15763
|
+
cachedContext = generateContext(contextCwd);
|
|
15764
|
+
}
|
|
15765
|
+
return cachedContext;
|
|
15766
|
+
}
|
|
15767
|
+
server.tool("get_api", {
|
|
15768
|
+
package: z.string(),
|
|
15769
|
+
symbol: z.string()
|
|
15770
|
+
}, async ({ package: pkg, symbol }) => {
|
|
15771
|
+
const entry = API_REFERENCE[`${pkg}/${symbol}`];
|
|
15772
|
+
if (!entry) {
|
|
15773
|
+
const suggestions = Object.keys(API_REFERENCE).filter((k) => k.toLowerCase().includes(symbol.toLowerCase())).slice(0, 5);
|
|
15774
|
+
return textResult(`Symbol '${symbol}' not found in @pyreon/${pkg}.\n\n${suggestions.length > 0 ? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join("\n")}` : "No similar symbols found."}`);
|
|
15775
|
+
}
|
|
15776
|
+
return textResult(`## @pyreon/${pkg} — ${symbol}\n\n**Signature:**\n\`\`\`typescript\n${entry.signature}\n\`\`\`\n\n**Usage:**\n\`\`\`typescript\n${entry.example}\n\`\`\`\n\n${entry.notes ? `**Notes:** ${entry.notes}\n\n` : ""}${entry.mistakes ? `**Common mistakes:**\n${entry.mistakes}\n` : ""}`);
|
|
15777
|
+
});
|
|
15778
|
+
server.tool("validate", {
|
|
15779
|
+
code: z.string(),
|
|
15780
|
+
filename: z.string().optional()
|
|
15781
|
+
}, async ({ code, filename }) => {
|
|
15782
|
+
const fname = filename ?? "snippet.tsx";
|
|
15783
|
+
const reactDiags = detectReactPatterns(code, fname);
|
|
15784
|
+
const pyreonDiags = detectPyreonPatterns(code, fname);
|
|
15785
|
+
if (reactDiags.length === 0 && pyreonDiags.length === 0) return textResult("✓ No issues found. The code follows Pyreon patterns correctly.");
|
|
15786
|
+
const merged = [...reactDiags, ...pyreonDiags];
|
|
15787
|
+
merged.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
15788
|
+
const issueText = merged.map((d, i) => `${i + 1}. **${d.code}** (line ${d.line})\n ${d.message}\n Current: \`${d.current}\`\n Fix: \`${d.suggested}\`\n Auto-fixable: ${d.fixable ? "yes" : "no"}`).join("\n\n");
|
|
15789
|
+
return textResult(`Found ${merged.length} issue${merged.length === 1 ? "" : "s"}:\n\n${issueText}`);
|
|
15790
|
+
});
|
|
15791
|
+
server.tool("migrate_react", {
|
|
15792
|
+
code: z.string(),
|
|
15793
|
+
filename: z.string().optional()
|
|
15794
|
+
}, async ({ code, filename }) => {
|
|
15795
|
+
const result = migrateReactCode(code, filename ?? "component.tsx");
|
|
15796
|
+
const changeList = result.changes.map((c) => `- Line ${c.line}: ${c.description}`).join("\n");
|
|
15797
|
+
const remainingIssues = result.diagnostics.filter((d) => !d.fixable);
|
|
15798
|
+
const manualText = remainingIssues.length > 0 ? `\n\n**Remaining issues (manual fix needed):**\n${remainingIssues.map((d) => `- Line ${d.line}: ${d.message}\n Suggested: \`${d.suggested}\``).join("\n")}` : "";
|
|
15799
|
+
return textResult(`## Migrated Code\n\n\`\`\`tsx\n${result.code}\n\`\`\`\n\n**Changes applied (${result.changes.length}):**\n${changeList || "No changes needed."}${manualText}`);
|
|
15800
|
+
});
|
|
15801
|
+
server.tool("diagnose", { error: z.string() }, async ({ error }) => {
|
|
15802
|
+
const diagnosis = diagnoseError(error);
|
|
15803
|
+
if (!diagnosis) return textResult(`Could not identify a Pyreon-specific pattern in this error.\n\nError: ${error}\n\nSuggestions:\n- Check for typos in variable/function names\n- Verify all imports are correct\n- Run \`bun run typecheck\` for full TypeScript diagnostics\n- Run \`pyreon doctor\` for project-wide health check`);
|
|
15804
|
+
let text = `**Cause:** ${diagnosis.cause}\n\n**Fix:** ${diagnosis.fix}`;
|
|
15805
|
+
if (diagnosis.fixCode) text += `\n\n**Code:**\n\`\`\`typescript\n${diagnosis.fixCode}\n\`\`\``;
|
|
15806
|
+
if (diagnosis.related) text += `\n\n**Related:** ${diagnosis.related}`;
|
|
15807
|
+
return textResult(text);
|
|
15808
|
+
});
|
|
15809
|
+
server.tool("get_routes", {}, async () => {
|
|
15810
|
+
const ctx = getContext();
|
|
15811
|
+
if (ctx.routes.length === 0) return textResult("No routes detected. Routes are defined via createRouter() or a routes array.");
|
|
15812
|
+
const routeTable = ctx.routes.map((r) => {
|
|
15813
|
+
const flags = [
|
|
15814
|
+
r.hasLoader ? "loader" : "",
|
|
15815
|
+
r.hasGuard ? "guard" : "",
|
|
15816
|
+
r.params.length > 0 ? `params: ${r.params.join(", ")}` : "",
|
|
15817
|
+
r.name ? `name: "${r.name}"` : ""
|
|
15818
|
+
].filter(Boolean).join(", ");
|
|
15819
|
+
return ` ${r.path}${flags ? ` (${flags})` : ""}`;
|
|
15820
|
+
}).join("\n");
|
|
15821
|
+
return textResult(`**Routes (${ctx.routes.length}):**\n\n${routeTable}`);
|
|
15822
|
+
});
|
|
15823
|
+
server.tool("get_components", {}, async () => {
|
|
15824
|
+
const ctx = getContext();
|
|
15825
|
+
if (ctx.components.length === 0) return textResult("No components detected.");
|
|
15826
|
+
const compList = ctx.components.map((c) => {
|
|
15827
|
+
const details = [c.props.length > 0 ? `props: { ${c.props.join(", ")} }` : "", c.hasSignals ? `signals: [${c.signalNames.join(", ")}]` : ""].filter(Boolean).join(", ");
|
|
15828
|
+
return ` ${c.name} — ${c.file}${details ? `\n ${details}` : ""}`;
|
|
15829
|
+
}).join("\n");
|
|
15830
|
+
return textResult(`**Components (${ctx.components.length}):**\n\n${compList}`);
|
|
15831
|
+
});
|
|
15832
|
+
server.tool("get_browser_smoke_status", {}, async () => {
|
|
15833
|
+
const fs = await import("node:fs");
|
|
15834
|
+
const path = await import("node:path");
|
|
15835
|
+
const cwd = process.cwd();
|
|
15836
|
+
let browserPackages = [];
|
|
15837
|
+
{
|
|
15838
|
+
let dir = cwd;
|
|
15839
|
+
for (let i = 0; i < 30; i++) {
|
|
15840
|
+
const candidate = path.join(dir, ".claude", "rules", "browser-packages.json");
|
|
15841
|
+
if (fs.existsSync(candidate)) {
|
|
15842
|
+
try {
|
|
15843
|
+
const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
15844
|
+
if (Array.isArray(parsed.packages)) browserPackages = parsed.packages.filter((p) => typeof p === "string");
|
|
15845
|
+
} catch {}
|
|
15846
|
+
break;
|
|
15847
|
+
}
|
|
15848
|
+
const parent = path.dirname(dir);
|
|
15849
|
+
if (parent === dir) break;
|
|
15850
|
+
dir = parent;
|
|
14923
15851
|
}
|
|
14924
|
-
const parent = path.dirname(dir);
|
|
14925
|
-
if (parent === dir) break;
|
|
14926
|
-
dir = parent;
|
|
14927
|
-
}
|
|
14928
|
-
}
|
|
14929
|
-
if (browserPackages.length === 0) return textResult("No `.claude/rules/browser-packages.json` found in the current project. This tool reports browser-smoke coverage for Pyreon monorepos that ship the single-source-of-truth list. Consumer apps can still opt in via the lint rule's `additionalPackages` option.");
|
|
14930
|
-
function hasBrowserTest(dir) {
|
|
14931
|
-
let entries;
|
|
14932
|
-
try {
|
|
14933
|
-
entries = fs.readdirSync(dir);
|
|
14934
|
-
} catch {
|
|
14935
|
-
return false;
|
|
14936
15852
|
}
|
|
14937
|
-
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
let isDir = false;
|
|
15853
|
+
if (browserPackages.length === 0) return textResult("No `.claude/rules/browser-packages.json` found in the current project. This tool reports browser-smoke coverage for Pyreon monorepos that ship the single-source-of-truth list. Consumer apps can still opt in via the lint rule's `additionalPackages` option.");
|
|
15854
|
+
function hasBrowserTest(dir) {
|
|
15855
|
+
let entries;
|
|
14941
15856
|
try {
|
|
14942
|
-
|
|
15857
|
+
entries = fs.readdirSync(dir);
|
|
14943
15858
|
} catch {
|
|
14944
|
-
|
|
15859
|
+
return false;
|
|
14945
15860
|
}
|
|
14946
|
-
|
|
14947
|
-
if (
|
|
14948
|
-
|
|
15861
|
+
for (const name of entries) {
|
|
15862
|
+
if (name.startsWith(".") || name === "node_modules" || name === "lib" || name === "dist") continue;
|
|
15863
|
+
const full = path.join(dir, name);
|
|
15864
|
+
let isDir = false;
|
|
15865
|
+
try {
|
|
15866
|
+
isDir = fs.statSync(full).isDirectory();
|
|
15867
|
+
} catch {
|
|
15868
|
+
continue;
|
|
15869
|
+
}
|
|
15870
|
+
if (isDir) {
|
|
15871
|
+
if (hasBrowserTest(full)) return true;
|
|
15872
|
+
continue;
|
|
15873
|
+
}
|
|
15874
|
+
if (/\.browser\.test\.(?:ts|tsx)$/.test(name)) return true;
|
|
14949
15875
|
}
|
|
14950
|
-
|
|
14951
|
-
}
|
|
14952
|
-
return false;
|
|
14953
|
-
}
|
|
14954
|
-
const pkgDirs = /* @__PURE__ */ new Map();
|
|
14955
|
-
function walkPkgs(dir, depth = 0) {
|
|
14956
|
-
if (depth > 4) return;
|
|
14957
|
-
let entries;
|
|
14958
|
-
try {
|
|
14959
|
-
entries = fs.readdirSync(dir);
|
|
14960
|
-
} catch {
|
|
14961
|
-
return;
|
|
15876
|
+
return false;
|
|
14962
15877
|
}
|
|
14963
|
-
|
|
14964
|
-
|
|
14965
|
-
|
|
14966
|
-
let
|
|
15878
|
+
const pkgDirs = /* @__PURE__ */ new Map();
|
|
15879
|
+
function walkPkgs(dir, depth = 0) {
|
|
15880
|
+
if (depth > 4) return;
|
|
15881
|
+
let entries;
|
|
14967
15882
|
try {
|
|
14968
|
-
|
|
15883
|
+
entries = fs.readdirSync(dir);
|
|
14969
15884
|
} catch {
|
|
14970
|
-
|
|
15885
|
+
return;
|
|
15886
|
+
}
|
|
15887
|
+
for (const name of entries) {
|
|
15888
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
15889
|
+
const full = path.join(dir, name);
|
|
15890
|
+
let isDir = false;
|
|
15891
|
+
try {
|
|
15892
|
+
isDir = fs.statSync(full).isDirectory();
|
|
15893
|
+
} catch {
|
|
15894
|
+
continue;
|
|
15895
|
+
}
|
|
15896
|
+
if (!isDir) continue;
|
|
15897
|
+
const pkgJsonPath = path.join(full, "package.json");
|
|
15898
|
+
if (fs.existsSync(pkgJsonPath)) try {
|
|
15899
|
+
const parsed = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
15900
|
+
if (typeof parsed.name === "string") pkgDirs.set(parsed.name, full);
|
|
15901
|
+
} catch {}
|
|
15902
|
+
else walkPkgs(full, depth + 1);
|
|
14971
15903
|
}
|
|
14972
|
-
if (!isDir) continue;
|
|
14973
|
-
const pkgJsonPath = path.join(full, "package.json");
|
|
14974
|
-
if (fs.existsSync(pkgJsonPath)) try {
|
|
14975
|
-
const parsed = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
14976
|
-
if (typeof parsed.name === "string") pkgDirs.set(parsed.name, full);
|
|
14977
|
-
} catch {}
|
|
14978
|
-
else walkPkgs(full, depth + 1);
|
|
14979
15904
|
}
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
14986
|
-
|
|
14987
|
-
|
|
14988
|
-
|
|
14989
|
-
|
|
15905
|
+
walkPkgs(path.join(cwd, "packages"));
|
|
15906
|
+
const covered = [];
|
|
15907
|
+
const missing = [];
|
|
15908
|
+
const unknown = [];
|
|
15909
|
+
for (const name of browserPackages) {
|
|
15910
|
+
const dir = pkgDirs.get(name);
|
|
15911
|
+
if (!dir) {
|
|
15912
|
+
unknown.push(name);
|
|
15913
|
+
continue;
|
|
15914
|
+
}
|
|
15915
|
+
if (hasBrowserTest(dir)) covered.push(name);
|
|
15916
|
+
else missing.push(name);
|
|
14990
15917
|
}
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
}
|
|
14994
|
-
const parts = [];
|
|
14995
|
-
parts.push(`**Browser smoke coverage** (${covered.length} / ${browserPackages.length}):`);
|
|
14996
|
-
parts.push("");
|
|
14997
|
-
if (covered.length > 0) {
|
|
14998
|
-
parts.push(`✓ Covered (${covered.length}):`);
|
|
14999
|
-
for (const n of covered) parts.push(` - ${n}`);
|
|
15918
|
+
const parts = [];
|
|
15919
|
+
parts.push(`**Browser smoke coverage** (${covered.length} / ${browserPackages.length}):`);
|
|
15000
15920
|
parts.push("");
|
|
15921
|
+
if (covered.length > 0) {
|
|
15922
|
+
parts.push(`✓ Covered (${covered.length}):`);
|
|
15923
|
+
for (const n of covered) parts.push(` - ${n}`);
|
|
15924
|
+
parts.push("");
|
|
15925
|
+
}
|
|
15926
|
+
if (missing.length > 0) {
|
|
15927
|
+
parts.push(`✗ Missing \`*.browser.test.*\` (${missing.length}):`);
|
|
15928
|
+
for (const n of missing) parts.push(` - ${n}`);
|
|
15929
|
+
parts.push("");
|
|
15930
|
+
parts.push("Add a `*.browser.test.{ts,tsx}` file under `src/` in each missing package. See `.claude/rules/test-environment-parity.md` for the setup recipe.");
|
|
15931
|
+
}
|
|
15932
|
+
if (unknown.length > 0) {
|
|
15933
|
+
parts.push(`? Listed in browser-packages.json but not found in this repo (${unknown.length}):`);
|
|
15934
|
+
for (const n of unknown) parts.push(` - ${n}`);
|
|
15935
|
+
}
|
|
15936
|
+
return textResult(parts.join("\n"));
|
|
15937
|
+
});
|
|
15938
|
+
server.tool("get_pattern", { name: z.string().optional().describe("Pattern slug (e.g. \"dev-warnings\", \"controllable-state\"). Omit to list available patterns.") }, async ({ name }) => {
|
|
15939
|
+
const registry = loadPatternRegistry();
|
|
15940
|
+
if (!name) return textResult(formatPatternIndex(registry));
|
|
15941
|
+
const pattern = findPattern(registry, name);
|
|
15942
|
+
if (pattern) return textResult(formatPatternBody(pattern));
|
|
15943
|
+
const suggestions = suggestPatterns(registry, name);
|
|
15944
|
+
return textResult(`Pattern "${name}" not found.\n\n${suggestions.length > 0 ? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join("\n")}\n\nOr call get_pattern() with no arg to see the full list.` : "Call get_pattern() with no arg to see available patterns."}`);
|
|
15945
|
+
});
|
|
15946
|
+
server.tool("get_anti_patterns", { category: z.enum([
|
|
15947
|
+
"reactivity",
|
|
15948
|
+
"jsx",
|
|
15949
|
+
"context",
|
|
15950
|
+
"architecture",
|
|
15951
|
+
"testing",
|
|
15952
|
+
"lifecycle",
|
|
15953
|
+
"documentation",
|
|
15954
|
+
"all"
|
|
15955
|
+
]).optional().describe(`Category filter: ${ANTI_PATTERN_CATEGORIES.join(", ")}, or "all" (default).`) }, async ({ category = "all" }) => {
|
|
15956
|
+
const cat = category;
|
|
15957
|
+
const doc = loadAntiPatternsDoc();
|
|
15958
|
+
if (!doc) return textResult("Could not locate `.claude/rules/anti-patterns.md`. This tool reads the file from the Pyreon monorepo — running in a consumer project without the rules directory surfaces this miss. File issues against pyreon/pyreon if the file exists but is not being found.");
|
|
15959
|
+
const all = parseAntiPatterns(doc);
|
|
15960
|
+
return textResult(formatAntiPatterns(cat === "all" ? all : all.filter((e) => e.category === cat), cat));
|
|
15961
|
+
});
|
|
15962
|
+
server.tool("get_changelog", {
|
|
15963
|
+
package: z.string().optional().describe("Package name, e.g. \"query\" or \"@pyreon/query\". Omit to list every package with a CHANGELOG."),
|
|
15964
|
+
limit: z.number().int().positive().optional().describe("Maximum number of substantive versions to return. Default 5."),
|
|
15965
|
+
includeDependencyUpdates: z.boolean().optional().describe("Include `Updated dependencies` bullets. Default false (usually noise)."),
|
|
15966
|
+
since: z.string().optional().describe("Only include versions strictly newer than this one (e.g. \"0.12.0\"). Useful when an agent knows the version it was trained against and wants just the delta.")
|
|
15967
|
+
}, async ({ package: pkg, limit, includeDependencyUpdates, since }) => {
|
|
15968
|
+
const registry = loadChangelogRegistry();
|
|
15969
|
+
if (!pkg) return textResult(formatChangelogIndex(registry));
|
|
15970
|
+
const changelog = findChangelog(registry, pkg);
|
|
15971
|
+
if (changelog) return textResult(formatChangelog(changelog, {
|
|
15972
|
+
limit,
|
|
15973
|
+
includeDependencyUpdates,
|
|
15974
|
+
since
|
|
15975
|
+
}));
|
|
15976
|
+
const suggestions = suggestChangelogs(registry, pkg);
|
|
15977
|
+
return textResult(`Changelog for "${pkg}" not found.\n\n${suggestions.length > 0 ? `Did you mean one of these?\n${suggestions.map((s) => ` - ${s}`).join("\n")}\n\nOr call get_changelog() with no arg for the full list.` : "Call get_changelog() with no arg for the list of packages that ship a CHANGELOG."}`);
|
|
15978
|
+
});
|
|
15979
|
+
server.tool("audit_test_environment", {
|
|
15980
|
+
minRisk: z.enum([
|
|
15981
|
+
"high",
|
|
15982
|
+
"medium",
|
|
15983
|
+
"low"
|
|
15984
|
+
]).optional().describe("Minimum risk level to surface. Default \"medium\" — HIGH + MEDIUM files. Use \"high\" for only the riskiest, \"low\" to see everything."),
|
|
15985
|
+
limit: z.number().int().positive().optional().describe("Maximum entries to show per risk group. Default 20.")
|
|
15986
|
+
}, async ({ minRisk, limit }) => {
|
|
15987
|
+
return textResult(formatTestAudit(auditTestEnvironment(process.cwd()), {
|
|
15988
|
+
minRisk,
|
|
15989
|
+
limit
|
|
15990
|
+
}));
|
|
15991
|
+
});
|
|
15992
|
+
return server;
|
|
15993
|
+
}
|
|
15994
|
+
/**
|
|
15995
|
+
* Locate `.claude/rules/anti-patterns.md` by walking up from cwd.
|
|
15996
|
+
* Returns the file contents or null if not found within 30 levels.
|
|
15997
|
+
* Separate from the patterns loader because the doc path is fixed
|
|
15998
|
+
* (`.claude/rules/`) — no glob needed.
|
|
15999
|
+
*/
|
|
16000
|
+
function loadAntiPatternsDoc(startDir = process.cwd()) {
|
|
16001
|
+
let dir = resolve(startDir);
|
|
16002
|
+
for (let i = 0; i < 30; i++) {
|
|
16003
|
+
const candidate = join(dir, ".claude", "rules", "anti-patterns.md");
|
|
16004
|
+
if (existsSync(candidate)) try {
|
|
16005
|
+
return readFileSync(candidate, "utf8");
|
|
16006
|
+
} catch {
|
|
16007
|
+
return null;
|
|
16008
|
+
}
|
|
16009
|
+
const parent = dirname(dir);
|
|
16010
|
+
if (parent === dir) return null;
|
|
16011
|
+
dir = parent;
|
|
15001
16012
|
}
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
for (const n of missing) parts.push(` - ${n}`);
|
|
15005
|
-
parts.push("");
|
|
15006
|
-
parts.push("Add a `*.browser.test.{ts,tsx}` file under `src/` in each missing package. See `.claude/rules/test-environment-parity.md` for the setup recipe.");
|
|
15007
|
-
}
|
|
15008
|
-
if (unknown.length > 0) {
|
|
15009
|
-
parts.push(`? Listed in browser-packages.json but not found in this repo (${unknown.length}):`);
|
|
15010
|
-
for (const n of unknown) parts.push(` - ${n}`);
|
|
15011
|
-
}
|
|
15012
|
-
return textResult(parts.join("\n"));
|
|
15013
|
-
});
|
|
16013
|
+
return null;
|
|
16014
|
+
}
|
|
15014
16015
|
async function main() {
|
|
16016
|
+
const server = createServer();
|
|
15015
16017
|
const transport = new StdioServerTransport();
|
|
15016
16018
|
await server.connect(transport);
|
|
15017
16019
|
}
|
|
15018
|
-
main().catch((err) => {
|
|
16020
|
+
if (import.meta.main) main().catch((err) => {
|
|
15019
16021
|
console.error("MCP server error:", err);
|
|
15020
16022
|
process.exit(1);
|
|
15021
16023
|
});
|
|
15022
16024
|
|
|
15023
16025
|
//#endregion
|
|
16026
|
+
export { createServer };
|
|
15024
16027
|
//# sourceMappingURL=index.js.map
|