@jay-framework/jay-stack-cli 0.20.0 → 0.22.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/agent-kit-template/designer/jay-html-components.md +42 -0
- package/agent-kit-template/designer/routing.md +19 -7
- package/agent-kit-template/developer/routing.md +19 -7
- package/agent-kit-template/plugin/INSTRUCTIONS.md +20 -3
- package/agent-kit-template/plugin/plugin-structure.md +20 -13
- package/agent-kit-template/plugin/setup-guide.md +180 -0
- package/agent-kit-template/plugin/validation.md +18 -4
- package/dist/index.js +1047 -82
- package/package.json +11 -11
package/dist/index.js
CHANGED
|
@@ -18,7 +18,6 @@ import path$1 from "node:path";
|
|
|
18
18
|
import fs$1 from "node:fs/promises";
|
|
19
19
|
import { createRequire } from "module";
|
|
20
20
|
import { glob } from "glob";
|
|
21
|
-
import { parse } from "node-html-parser";
|
|
22
21
|
import fsSync from "node:fs";
|
|
23
22
|
import { fileURLToPath } from "node:url";
|
|
24
23
|
const DEFAULT_CONFIG = {
|
|
@@ -2748,15 +2747,6 @@ async function runRebuild(projectPath, options) {
|
|
|
2748
2747
|
process.exit(1);
|
|
2749
2748
|
}
|
|
2750
2749
|
}
|
|
2751
|
-
const runProduction = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
2752
|
-
__proto__: null,
|
|
2753
|
-
initLogger,
|
|
2754
|
-
resolveProductionContext,
|
|
2755
|
-
resolveVersionFromPackageJson,
|
|
2756
|
-
runBuild,
|
|
2757
|
-
runRebuild,
|
|
2758
|
-
runServe
|
|
2759
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
2760
2750
|
const s = createRequire(import.meta.url), e = s("typescript"), u = e;
|
|
2761
2751
|
new Proxy(e, {
|
|
2762
2752
|
get(t, r) {
|
|
@@ -3031,6 +3021,769 @@ function checkParamsConsistency(paramsTypeNames, localInterfaces, contractImport
|
|
|
3031
3021
|
}
|
|
3032
3022
|
}
|
|
3033
3023
|
}
|
|
3024
|
+
const REJECTED_ITEM_FIELDS = ["kind", "parameters", "component", "allowedScopes"];
|
|
3025
|
+
const HTML_SOFT_LIMIT_BYTES = 8 * 1024;
|
|
3026
|
+
const HTML_HARD_LIMIT_BYTES = 32 * 1024;
|
|
3027
|
+
const FOLDER_PATH_MAX_SEGMENTS = 32;
|
|
3028
|
+
const BLOCKED_TAGS = /<\s*(script|iframe|object|embed)\b[^>]*>[\s\S]*?<\/\s*\1\s*>|<\s*(script|iframe|object|embed)\b[^>]*\/?>/gi;
|
|
3029
|
+
const EVENT_HANDLER_ATTR = /\s+on[a-z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi;
|
|
3030
|
+
const JAVASCRIPT_URL = /\b(href|src|xlink:href)\s*=\s*("|')\s*javascript:/gi;
|
|
3031
|
+
function isRecord(value) {
|
|
3032
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3033
|
+
}
|
|
3034
|
+
function byteLengthUtf8(value) {
|
|
3035
|
+
return new TextEncoder().encode(value).length;
|
|
3036
|
+
}
|
|
3037
|
+
function detectUnsafeAddMenuHtmlFragment(html) {
|
|
3038
|
+
if (BLOCKED_TAGS.test(html)) {
|
|
3039
|
+
BLOCKED_TAGS.lastIndex = 0;
|
|
3040
|
+
return {
|
|
3041
|
+
code: "html-fragment-unsafe-markup",
|
|
3042
|
+
message: "html-fragment must not include script, iframe, object, or embed"
|
|
3043
|
+
};
|
|
3044
|
+
}
|
|
3045
|
+
BLOCKED_TAGS.lastIndex = 0;
|
|
3046
|
+
if (EVENT_HANDLER_ATTR.test(html)) {
|
|
3047
|
+
EVENT_HANDLER_ATTR.lastIndex = 0;
|
|
3048
|
+
return {
|
|
3049
|
+
code: "html-fragment-unsafe-markup",
|
|
3050
|
+
message: "html-fragment must not include inline event handler attributes"
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3053
|
+
EVENT_HANDLER_ATTR.lastIndex = 0;
|
|
3054
|
+
if (JAVASCRIPT_URL.test(html)) {
|
|
3055
|
+
JAVASCRIPT_URL.lastIndex = 0;
|
|
3056
|
+
return {
|
|
3057
|
+
code: "html-fragment-unsafe-markup",
|
|
3058
|
+
message: "html-fragment must not include javascript: URLs"
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
JAVASCRIPT_URL.lastIndex = 0;
|
|
3062
|
+
return null;
|
|
3063
|
+
}
|
|
3064
|
+
function sanitizeAddMenuHtmlFragment(html) {
|
|
3065
|
+
let result = html;
|
|
3066
|
+
result = result.replace(BLOCKED_TAGS, "");
|
|
3067
|
+
BLOCKED_TAGS.lastIndex = 0;
|
|
3068
|
+
result = result.replace(EVENT_HANDLER_ATTR, "");
|
|
3069
|
+
EVENT_HANDLER_ATTR.lastIndex = 0;
|
|
3070
|
+
result = result.replace(JAVASCRIPT_URL, "");
|
|
3071
|
+
JAVASCRIPT_URL.lastIndex = 0;
|
|
3072
|
+
return result.trim();
|
|
3073
|
+
}
|
|
3074
|
+
function catalogWarning(code, message, itemId, sourcePath) {
|
|
3075
|
+
return { code, message, ...itemId ? { itemId } : {}, ...sourcePath ? { sourcePath } : {} };
|
|
3076
|
+
}
|
|
3077
|
+
function requiredString(obj, field, itemPath, errors, code) {
|
|
3078
|
+
const value = obj[field];
|
|
3079
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
3080
|
+
errors.push({
|
|
3081
|
+
path: `${itemPath}.${field}`,
|
|
3082
|
+
message: "required non-empty string",
|
|
3083
|
+
...code ? { code } : {}
|
|
3084
|
+
});
|
|
3085
|
+
return null;
|
|
3086
|
+
}
|
|
3087
|
+
return value.trim();
|
|
3088
|
+
}
|
|
3089
|
+
function optionalString(obj, field) {
|
|
3090
|
+
const value = obj[field];
|
|
3091
|
+
if (value === void 0)
|
|
3092
|
+
return void 0;
|
|
3093
|
+
if (typeof value !== "string")
|
|
3094
|
+
return void 0;
|
|
3095
|
+
const trimmed = value.trim();
|
|
3096
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
3097
|
+
}
|
|
3098
|
+
function validateInteraction(raw, itemPath, errors) {
|
|
3099
|
+
if (raw === void 0)
|
|
3100
|
+
return void 0;
|
|
3101
|
+
if (!isRecord(raw)) {
|
|
3102
|
+
errors.push({
|
|
3103
|
+
path: itemPath,
|
|
3104
|
+
message: "interaction must be an object",
|
|
3105
|
+
code: "interaction-not-object"
|
|
3106
|
+
});
|
|
3107
|
+
return void 0;
|
|
3108
|
+
}
|
|
3109
|
+
const mode = raw.mode;
|
|
3110
|
+
if (mode !== "reference" && mode !== "stage-place") {
|
|
3111
|
+
errors.push({
|
|
3112
|
+
path: `${itemPath}.mode`,
|
|
3113
|
+
message: 'interaction.mode must be "reference" or "stage-place"',
|
|
3114
|
+
code: "interaction-invalid-mode"
|
|
3115
|
+
});
|
|
3116
|
+
return void 0;
|
|
3117
|
+
}
|
|
3118
|
+
return {
|
|
3119
|
+
mode,
|
|
3120
|
+
stagePromptTemplate: optionalString(raw, "stagePromptTemplate")
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
function validatePresentation(raw, itemPath, errors) {
|
|
3124
|
+
if (raw === void 0)
|
|
3125
|
+
return void 0;
|
|
3126
|
+
if (!isRecord(raw)) {
|
|
3127
|
+
errors.push({
|
|
3128
|
+
path: itemPath,
|
|
3129
|
+
message: "presentation must be an object",
|
|
3130
|
+
code: "presentation-not-object"
|
|
3131
|
+
});
|
|
3132
|
+
return void 0;
|
|
3133
|
+
}
|
|
3134
|
+
const type = raw.type;
|
|
3135
|
+
if (type !== "image" && type !== "gif" && type !== "html-fragment") {
|
|
3136
|
+
errors.push({
|
|
3137
|
+
path: `${itemPath}.type`,
|
|
3138
|
+
message: 'presentation.type must be "image", "gif", or "html-fragment"',
|
|
3139
|
+
code: "presentation-unknown-type"
|
|
3140
|
+
});
|
|
3141
|
+
return void 0;
|
|
3142
|
+
}
|
|
3143
|
+
if (type === "image") {
|
|
3144
|
+
const src = requiredString(raw, "src", itemPath, errors, "presentation-missing-src");
|
|
3145
|
+
if (!src)
|
|
3146
|
+
return void 0;
|
|
3147
|
+
return { type: "image", src };
|
|
3148
|
+
}
|
|
3149
|
+
if (type === "gif") {
|
|
3150
|
+
const src = requiredString(raw, "src", itemPath, errors, "presentation-missing-src");
|
|
3151
|
+
if (!src)
|
|
3152
|
+
return void 0;
|
|
3153
|
+
return {
|
|
3154
|
+
type: "gif",
|
|
3155
|
+
src,
|
|
3156
|
+
poster: optionalString(raw, "poster")
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
if ("src" in raw) {
|
|
3160
|
+
errors.push({
|
|
3161
|
+
path: `${itemPath}.src`,
|
|
3162
|
+
message: "html-fragment must use presentation.html, not presentation.src",
|
|
3163
|
+
code: "html-fragment-src-not-allowed"
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
const htmlValue = raw.html;
|
|
3167
|
+
if (typeof htmlValue !== "string" || htmlValue.trim().length === 0) {
|
|
3168
|
+
errors.push({
|
|
3169
|
+
path: `${itemPath}.html`,
|
|
3170
|
+
message: "html-fragment requires non-empty presentation.html",
|
|
3171
|
+
code: "html-fragment-missing-html"
|
|
3172
|
+
});
|
|
3173
|
+
return void 0;
|
|
3174
|
+
}
|
|
3175
|
+
const unsafe = detectUnsafeAddMenuHtmlFragment(htmlValue);
|
|
3176
|
+
if (unsafe) {
|
|
3177
|
+
errors.push({
|
|
3178
|
+
path: `${itemPath}.html`,
|
|
3179
|
+
message: unsafe.message,
|
|
3180
|
+
code: unsafe.code
|
|
3181
|
+
});
|
|
3182
|
+
return void 0;
|
|
3183
|
+
}
|
|
3184
|
+
const sanitized = sanitizeAddMenuHtmlFragment(htmlValue);
|
|
3185
|
+
if (byteLengthUtf8(sanitized) > HTML_HARD_LIMIT_BYTES) {
|
|
3186
|
+
errors.push({
|
|
3187
|
+
path: `${itemPath}.html`,
|
|
3188
|
+
message: `html-fragment exceeds ${HTML_HARD_LIMIT_BYTES} bytes after sanitize`,
|
|
3189
|
+
code: "html-fragment-oversize-hard"
|
|
3190
|
+
});
|
|
3191
|
+
return void 0;
|
|
3192
|
+
}
|
|
3193
|
+
return { type: "html-fragment", html: htmlValue };
|
|
3194
|
+
}
|
|
3195
|
+
const BROWSE_SIZES = /* @__PURE__ */ new Set(["large", "medium", "small"]);
|
|
3196
|
+
function validateBrowse(raw, itemPath, errors) {
|
|
3197
|
+
if (raw === void 0)
|
|
3198
|
+
return void 0;
|
|
3199
|
+
if (!isRecord(raw)) {
|
|
3200
|
+
errors.push({
|
|
3201
|
+
path: itemPath,
|
|
3202
|
+
message: "browse must be an object",
|
|
3203
|
+
code: "browse-not-object"
|
|
3204
|
+
});
|
|
3205
|
+
return void 0;
|
|
3206
|
+
}
|
|
3207
|
+
const sizeRaw = raw.size;
|
|
3208
|
+
if (sizeRaw === void 0) {
|
|
3209
|
+
return {};
|
|
3210
|
+
}
|
|
3211
|
+
if (typeof sizeRaw !== "string" || !BROWSE_SIZES.has(sizeRaw)) {
|
|
3212
|
+
errors.push({
|
|
3213
|
+
path: `${itemPath}.size`,
|
|
3214
|
+
message: 'browse.size must be "large", "medium", or "small"',
|
|
3215
|
+
code: "browse-unknown-size"
|
|
3216
|
+
});
|
|
3217
|
+
return void 0;
|
|
3218
|
+
}
|
|
3219
|
+
return { size: sizeRaw };
|
|
3220
|
+
}
|
|
3221
|
+
function validateFolderPath(raw, itemPath, errors) {
|
|
3222
|
+
if (raw === void 0)
|
|
3223
|
+
return void 0;
|
|
3224
|
+
if (!Array.isArray(raw)) {
|
|
3225
|
+
errors.push({
|
|
3226
|
+
path: `${itemPath}.folderPath`,
|
|
3227
|
+
message: "folderPath must be an array of strings",
|
|
3228
|
+
code: "folder-path-not-array"
|
|
3229
|
+
});
|
|
3230
|
+
return void 0;
|
|
3231
|
+
}
|
|
3232
|
+
const segments = [];
|
|
3233
|
+
for (let index = 0; index < raw.length; index++) {
|
|
3234
|
+
const value = raw[index];
|
|
3235
|
+
if (typeof value !== "string") {
|
|
3236
|
+
errors.push({
|
|
3237
|
+
path: `${itemPath}.folderPath[${index}]`,
|
|
3238
|
+
message: "folderPath segments must be strings",
|
|
3239
|
+
code: "folder-path-segment-type"
|
|
3240
|
+
});
|
|
3241
|
+
return void 0;
|
|
3242
|
+
}
|
|
3243
|
+
const trimmed = value.trim();
|
|
3244
|
+
if (trimmed.length === 0) {
|
|
3245
|
+
errors.push({
|
|
3246
|
+
path: `${itemPath}.folderPath[${index}]`,
|
|
3247
|
+
message: "folderPath segments must be non-empty",
|
|
3248
|
+
code: "folder-path-empty-segment"
|
|
3249
|
+
});
|
|
3250
|
+
return void 0;
|
|
3251
|
+
}
|
|
3252
|
+
if (trimmed === ".." || trimmed.includes("/") || trimmed.includes("\\")) {
|
|
3253
|
+
errors.push({
|
|
3254
|
+
path: `${itemPath}.folderPath[${index}]`,
|
|
3255
|
+
message: 'folderPath segments must not contain ".." or path separators',
|
|
3256
|
+
code: "folder-path-invalid-segment"
|
|
3257
|
+
});
|
|
3258
|
+
return void 0;
|
|
3259
|
+
}
|
|
3260
|
+
if (segments.at(-1) === trimmed) {
|
|
3261
|
+
errors.push({
|
|
3262
|
+
path: `${itemPath}.folderPath[${index}]`,
|
|
3263
|
+
message: "folderPath must not contain duplicate adjacent segments",
|
|
3264
|
+
code: "folder-path-adjacent-duplicate"
|
|
3265
|
+
});
|
|
3266
|
+
return void 0;
|
|
3267
|
+
}
|
|
3268
|
+
segments.push(trimmed);
|
|
3269
|
+
}
|
|
3270
|
+
if (segments.length > FOLDER_PATH_MAX_SEGMENTS) {
|
|
3271
|
+
errors.push({
|
|
3272
|
+
path: `${itemPath}.folderPath`,
|
|
3273
|
+
message: `folderPath exceeds ${FOLDER_PATH_MAX_SEGMENTS} segments`,
|
|
3274
|
+
code: "folder-path-too-deep"
|
|
3275
|
+
});
|
|
3276
|
+
return void 0;
|
|
3277
|
+
}
|
|
3278
|
+
return segments.length > 0 ? segments : void 0;
|
|
3279
|
+
}
|
|
3280
|
+
function validateAddMenuItem(raw, itemPath) {
|
|
3281
|
+
const errors = [];
|
|
3282
|
+
if (!isRecord(raw)) {
|
|
3283
|
+
return {
|
|
3284
|
+
item: null,
|
|
3285
|
+
errors: [
|
|
3286
|
+
{ path: itemPath, message: "item must be an object", code: "item-not-object" }
|
|
3287
|
+
]
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
for (const field of REJECTED_ITEM_FIELDS) {
|
|
3291
|
+
if (field in raw) {
|
|
3292
|
+
errors.push({
|
|
3293
|
+
path: `${itemPath}.${field}`,
|
|
3294
|
+
message: `field "${field}" is not allowed in Add Menu catalog items`,
|
|
3295
|
+
code: `rejected-field-${field}`
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
const id = requiredString(raw, "id", itemPath, errors, "missing-id");
|
|
3300
|
+
const title = requiredString(raw, "title", itemPath, errors, "missing-title");
|
|
3301
|
+
const category = requiredString(raw, "category", itemPath, errors, "missing-category");
|
|
3302
|
+
const prompt = requiredString(raw, "prompt", itemPath, errors, "missing-prompt");
|
|
3303
|
+
if (errors.length > 0 || !id || !title || !category || !prompt) {
|
|
3304
|
+
return { item: null, errors };
|
|
3305
|
+
}
|
|
3306
|
+
const interaction = validateInteraction(raw.interaction, `${itemPath}.interaction`, errors);
|
|
3307
|
+
const presentation = validatePresentation(raw.presentation, `${itemPath}.presentation`, errors);
|
|
3308
|
+
const browse = validateBrowse(raw.browse, `${itemPath}.browse`, errors);
|
|
3309
|
+
const folderPath = validateFolderPath(raw.folderPath, itemPath, errors);
|
|
3310
|
+
if (errors.length > 0) {
|
|
3311
|
+
return { item: null, errors };
|
|
3312
|
+
}
|
|
3313
|
+
return {
|
|
3314
|
+
item: {
|
|
3315
|
+
id,
|
|
3316
|
+
title,
|
|
3317
|
+
category,
|
|
3318
|
+
prompt,
|
|
3319
|
+
pluginName: optionalString(raw, "pluginName"),
|
|
3320
|
+
packageName: optionalString(raw, "packageName"),
|
|
3321
|
+
subCategory: optionalString(raw, "subCategory"),
|
|
3322
|
+
...folderPath ? { folderPath } : {},
|
|
3323
|
+
thumbnail: optionalString(raw, "thumbnail"),
|
|
3324
|
+
...presentation ? { presentation } : {},
|
|
3325
|
+
...browse ? { browse } : {},
|
|
3326
|
+
...interaction ? { interaction } : {}
|
|
3327
|
+
},
|
|
3328
|
+
errors
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
3331
|
+
function validateAddMenuCatalogFile(raw, sourcePath) {
|
|
3332
|
+
const errors = [];
|
|
3333
|
+
if (!isRecord(raw)) {
|
|
3334
|
+
return {
|
|
3335
|
+
file: null,
|
|
3336
|
+
errors: [
|
|
3337
|
+
{
|
|
3338
|
+
path: sourcePath,
|
|
3339
|
+
message: "catalog file must be an object",
|
|
3340
|
+
code: "catalog-not-object"
|
|
3341
|
+
}
|
|
3342
|
+
]
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
if (!Array.isArray(raw.items)) {
|
|
3346
|
+
return {
|
|
3347
|
+
file: null,
|
|
3348
|
+
errors: [
|
|
3349
|
+
{
|
|
3350
|
+
path: `${sourcePath}.items`,
|
|
3351
|
+
message: "items must be an array",
|
|
3352
|
+
code: "catalog-items-not-array"
|
|
3353
|
+
}
|
|
3354
|
+
]
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
const items = [];
|
|
3358
|
+
raw.items.forEach((entry, index) => {
|
|
3359
|
+
const result = validateAddMenuItem(entry, `${sourcePath}.items[${index}]`);
|
|
3360
|
+
errors.push(...result.errors);
|
|
3361
|
+
if (result.item)
|
|
3362
|
+
items.push(result.item);
|
|
3363
|
+
});
|
|
3364
|
+
if (items.length === 0 && errors.length > 0) {
|
|
3365
|
+
return { file: null, errors };
|
|
3366
|
+
}
|
|
3367
|
+
return { file: { items }, errors };
|
|
3368
|
+
}
|
|
3369
|
+
function normalizeAddMenuPresentation(item) {
|
|
3370
|
+
if (item.presentation)
|
|
3371
|
+
return item.presentation;
|
|
3372
|
+
const thumbnail = item.thumbnail?.trim();
|
|
3373
|
+
if (!thumbnail)
|
|
3374
|
+
return void 0;
|
|
3375
|
+
if (/\.gif$/i.test(thumbnail)) {
|
|
3376
|
+
return { type: "gif", src: thumbnail };
|
|
3377
|
+
}
|
|
3378
|
+
return { type: "image", src: thumbnail };
|
|
3379
|
+
}
|
|
3380
|
+
function normalizeAddMenuBrowseSize(item) {
|
|
3381
|
+
return item.browse?.size ?? "medium";
|
|
3382
|
+
}
|
|
3383
|
+
function hasSingleRootDiv(html) {
|
|
3384
|
+
const trimmed = html.trim();
|
|
3385
|
+
if (!trimmed.startsWith("<div"))
|
|
3386
|
+
return false;
|
|
3387
|
+
const openMatch = trimmed.match(/^<div\b[^>]*>/i);
|
|
3388
|
+
if (!openMatch)
|
|
3389
|
+
return false;
|
|
3390
|
+
const afterOpen = trimmed.slice(openMatch[0].length);
|
|
3391
|
+
const closeIdx = afterOpen.lastIndexOf("</div>");
|
|
3392
|
+
if (closeIdx < 0)
|
|
3393
|
+
return false;
|
|
3394
|
+
const tail = afterOpen.slice(closeIdx + "</div>".length).trim();
|
|
3395
|
+
return tail.length === 0;
|
|
3396
|
+
}
|
|
3397
|
+
function hasAtScopeStyle(html) {
|
|
3398
|
+
return /<style\b[^>]*>[\s\S]*@scope\b/i.test(html);
|
|
3399
|
+
}
|
|
3400
|
+
function lintHtmlFragment(item, sourcePath) {
|
|
3401
|
+
const errors = [];
|
|
3402
|
+
const warnings = [];
|
|
3403
|
+
const presentation = item.presentation;
|
|
3404
|
+
if (!presentation || presentation.type !== "html-fragment") {
|
|
3405
|
+
return { errors, warnings };
|
|
3406
|
+
}
|
|
3407
|
+
const rawHtml = presentation.html?.trim() ?? "";
|
|
3408
|
+
if (!rawHtml) {
|
|
3409
|
+
errors.push(
|
|
3410
|
+
catalogWarning(
|
|
3411
|
+
"html-fragment-missing-html",
|
|
3412
|
+
"html-fragment requires non-empty presentation.html",
|
|
3413
|
+
item.id,
|
|
3414
|
+
sourcePath
|
|
3415
|
+
)
|
|
3416
|
+
);
|
|
3417
|
+
return { errors, warnings };
|
|
3418
|
+
}
|
|
3419
|
+
const unsafe = detectUnsafeAddMenuHtmlFragment(rawHtml);
|
|
3420
|
+
if (unsafe) {
|
|
3421
|
+
errors.push(catalogWarning(unsafe.code, unsafe.message, item.id, sourcePath));
|
|
3422
|
+
return { errors, warnings };
|
|
3423
|
+
}
|
|
3424
|
+
const sanitized = sanitizeAddMenuHtmlFragment(rawHtml);
|
|
3425
|
+
const bytes = byteLengthUtf8(sanitized);
|
|
3426
|
+
if (bytes > HTML_HARD_LIMIT_BYTES) {
|
|
3427
|
+
errors.push(
|
|
3428
|
+
catalogWarning(
|
|
3429
|
+
"html-fragment-oversize-hard",
|
|
3430
|
+
`html-fragment exceeds ${HTML_HARD_LIMIT_BYTES} bytes after sanitize (${bytes})`,
|
|
3431
|
+
item.id,
|
|
3432
|
+
sourcePath
|
|
3433
|
+
)
|
|
3434
|
+
);
|
|
3435
|
+
} else if (bytes > HTML_SOFT_LIMIT_BYTES) {
|
|
3436
|
+
warnings.push(
|
|
3437
|
+
catalogWarning(
|
|
3438
|
+
"html-fragment-oversize-soft",
|
|
3439
|
+
`html-fragment exceeds ${HTML_SOFT_LIMIT_BYTES} bytes after sanitize (${bytes}) — consider trimming`,
|
|
3440
|
+
item.id,
|
|
3441
|
+
sourcePath
|
|
3442
|
+
)
|
|
3443
|
+
);
|
|
3444
|
+
}
|
|
3445
|
+
if (!hasSingleRootDiv(sanitized)) {
|
|
3446
|
+
warnings.push(
|
|
3447
|
+
catalogWarning(
|
|
3448
|
+
"html-fragment-missing-root-div",
|
|
3449
|
+
"html-fragment should use a single root <div>",
|
|
3450
|
+
item.id,
|
|
3451
|
+
sourcePath
|
|
3452
|
+
)
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3455
|
+
if (!hasAtScopeStyle(sanitized)) {
|
|
3456
|
+
warnings.push(
|
|
3457
|
+
catalogWarning(
|
|
3458
|
+
"html-fragment-missing-at-scope",
|
|
3459
|
+
"html-fragment should include <style> with @scope for preview isolation",
|
|
3460
|
+
item.id,
|
|
3461
|
+
sourcePath
|
|
3462
|
+
)
|
|
3463
|
+
);
|
|
3464
|
+
}
|
|
3465
|
+
return { errors, warnings };
|
|
3466
|
+
}
|
|
3467
|
+
function lintGifPoster(item, sourcePath) {
|
|
3468
|
+
const presentation = normalizeAddMenuPresentation(item);
|
|
3469
|
+
if (presentation?.type !== "gif" || presentation.poster?.trim())
|
|
3470
|
+
return [];
|
|
3471
|
+
return [
|
|
3472
|
+
catalogWarning(
|
|
3473
|
+
"gif-missing-poster",
|
|
3474
|
+
`gif item "${item.id}" has no poster — add poster for prefers-reduced-motion accessibility`,
|
|
3475
|
+
item.id,
|
|
3476
|
+
sourcePath
|
|
3477
|
+
)
|
|
3478
|
+
];
|
|
3479
|
+
}
|
|
3480
|
+
function lintBrowseLargeWithoutPresentation(item, sourcePath) {
|
|
3481
|
+
if (normalizeAddMenuBrowseSize(item) !== "large")
|
|
3482
|
+
return [];
|
|
3483
|
+
if (normalizeAddMenuPresentation(item))
|
|
3484
|
+
return [];
|
|
3485
|
+
return [
|
|
3486
|
+
catalogWarning(
|
|
3487
|
+
"browse-large-without-presentation",
|
|
3488
|
+
`large browse item "${item.id}" has no presentation or thumbnail — add a preview`,
|
|
3489
|
+
item.id,
|
|
3490
|
+
sourcePath
|
|
3491
|
+
)
|
|
3492
|
+
];
|
|
3493
|
+
}
|
|
3494
|
+
function lintAddMenuCatalog(items, sourcePath) {
|
|
3495
|
+
const errors = [];
|
|
3496
|
+
const warnings = [];
|
|
3497
|
+
for (const item of items) {
|
|
3498
|
+
warnings.push(...lintGifPoster(item, sourcePath));
|
|
3499
|
+
warnings.push(...lintBrowseLargeWithoutPresentation(item, sourcePath));
|
|
3500
|
+
const htmlLint = lintHtmlFragment(item, sourcePath);
|
|
3501
|
+
errors.push(...htmlLint.errors);
|
|
3502
|
+
warnings.push(...htmlLint.warnings);
|
|
3503
|
+
}
|
|
3504
|
+
return { errors, warnings };
|
|
3505
|
+
}
|
|
3506
|
+
const ADD_MENU_VALIDATION_SUGGESTIONS = {
|
|
3507
|
+
"gif-missing-poster": "Add presentation.poster with a static image for prefers-reduced-motion accessibility",
|
|
3508
|
+
"html-fragment-missing-root-div": 'Wrap preview markup in a single root <div class="am-preview"> — see agent-kit/plugin/aiditor-add-menu.md',
|
|
3509
|
+
"html-fragment-missing-at-scope": "Add <style> with @scope (.am-preview) inside the root div — see agent-kit/plugin/aiditor-add-menu.md",
|
|
3510
|
+
"html-fragment-missing-html": "Set presentation.html with inline markup — external preview files are not supported",
|
|
3511
|
+
"html-fragment-src-not-allowed": "Remove presentation.src; use presentation.html for html-fragment previews",
|
|
3512
|
+
"html-fragment-unsafe-markup": "Remove scripts, event handlers, and javascript: URLs from presentation.html",
|
|
3513
|
+
"html-fragment-oversize-soft": "Trim inline html or use a gif preview for rich motion",
|
|
3514
|
+
"html-fragment-oversize-hard": "Reduce presentation.html below 32 KB after sanitize",
|
|
3515
|
+
"browse-large-without-presentation": "Add presentation or thumbnail — large browse tiles need a visual preview",
|
|
3516
|
+
"browse-unknown-size": "Set browse.size to large, medium, or small",
|
|
3517
|
+
"presentation-unknown-type": "Set presentation.type to image, gif, or html-fragment",
|
|
3518
|
+
"interaction-invalid-mode": "Set interaction.mode to reference (default) or stage-place",
|
|
3519
|
+
"folder-path-not-array": "folderPath must be a yaml array of folder name strings",
|
|
3520
|
+
"folder-path-segment-type": "Each folderPath entry must be a non-empty string",
|
|
3521
|
+
"folder-path-empty-segment": "Remove empty folderPath segments",
|
|
3522
|
+
"folder-path-invalid-segment": 'Use separate array elements per folder level — no "/" or "\\" in segment names',
|
|
3523
|
+
"folder-path-adjacent-duplicate": "Remove duplicate consecutive folderPath segments",
|
|
3524
|
+
"folder-path-too-deep": `Shorten folderPath to at most ${FOLDER_PATH_MAX_SEGMENTS} segments`,
|
|
3525
|
+
"rejected-field-kind": "Remove kind — express item intent in prompt text",
|
|
3526
|
+
"rejected-field-parameters": "Remove parameters — materialize values into prompt",
|
|
3527
|
+
"rejected-field-component": "Remove component — use prompt plus contract paths in agent-kit",
|
|
3528
|
+
"rejected-field-allowedScopes": "Remove allowedScopes — attachment scope is chosen in the AIditor UI",
|
|
3529
|
+
"add-menu-missing-agentkit-handler": "Declare agentkit in plugin.yaml pointing at an agent-kit handler export; run jay-stack agent-kit (project yarn agent-kit) to materialize catalogs — not jay-stack setup",
|
|
3530
|
+
"add-menu-legacy-setup-handler": "Move add-menu catalog writes from the setup handler to agentkit. Setup is for config/credentials only; run jay-stack agent-kit to generate agent-kit/aiditor/add-menu/*.yaml",
|
|
3531
|
+
"deprecated-nested-setup-keys": "Replace nested setup.handler / setup.references with flat keys: setup: <handler> and agentkit: <handler>"
|
|
3532
|
+
};
|
|
3533
|
+
const ADD_MENU_CATALOG_REL_PATHS = [
|
|
3534
|
+
"agent-kit/aiditor/add-menu.template.yaml",
|
|
3535
|
+
"agent-kit/aiditor/add-menu.yaml"
|
|
3536
|
+
];
|
|
3537
|
+
const CONTRIBUTOR_GUIDE = "agent-kit/plugin/aiditor-add-menu.md";
|
|
3538
|
+
const ADD_MENU_WRITE_MARKERS = [
|
|
3539
|
+
"agent-kit/aiditor/add-menu",
|
|
3540
|
+
"aiditor-add-menu-thumbnails",
|
|
3541
|
+
"writeAddMenuCatalog",
|
|
3542
|
+
"copyAiditorAddMenuThumbnails"
|
|
3543
|
+
];
|
|
3544
|
+
function isRelativeHandlerRef(value) {
|
|
3545
|
+
return value.startsWith("./") || value.startsWith("../");
|
|
3546
|
+
}
|
|
3547
|
+
function resolveModulePath$1(basePath) {
|
|
3548
|
+
for (const ext of ["", ".ts", ".js", "/index.ts", "/index.js"]) {
|
|
3549
|
+
const candidate = basePath + ext;
|
|
3550
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
3551
|
+
return candidate;
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
return void 0;
|
|
3555
|
+
}
|
|
3556
|
+
function collectTypeScriptFiles(dir, depth = 0) {
|
|
3557
|
+
if (depth > 4)
|
|
3558
|
+
return [];
|
|
3559
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
3560
|
+
const files = [];
|
|
3561
|
+
for (const entry of entries) {
|
|
3562
|
+
const fullPath = path.join(dir, entry.name);
|
|
3563
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "test") {
|
|
3564
|
+
files.push(...collectTypeScriptFiles(fullPath, depth + 1));
|
|
3565
|
+
} else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) {
|
|
3566
|
+
files.push(fullPath);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
return files;
|
|
3570
|
+
}
|
|
3571
|
+
function resolveHandlerSourceFile(pluginPath, handlerRef, isNpmPackage) {
|
|
3572
|
+
if (isRelativeHandlerRef(handlerRef)) {
|
|
3573
|
+
return resolveModulePath$1(path.join(pluginPath, handlerRef)) ?? null;
|
|
3574
|
+
}
|
|
3575
|
+
const searchRoots = isNpmPackage ? [path.join(pluginPath, "lib"), path.join(pluginPath, "dist")] : [pluginPath];
|
|
3576
|
+
for (const root of searchRoots) {
|
|
3577
|
+
if (!fs.existsSync(root))
|
|
3578
|
+
continue;
|
|
3579
|
+
for (const file of collectTypeScriptFiles(root)) {
|
|
3580
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
3581
|
+
const definesHandler = new RegExp(
|
|
3582
|
+
`export\\s+(?:async\\s+)?function\\s+${handlerRef}\\b`
|
|
3583
|
+
).test(content);
|
|
3584
|
+
const definesDefaultNamed = new RegExp(
|
|
3585
|
+
`export\\s+default\\s+async\\s+function\\s+${handlerRef}\\b`
|
|
3586
|
+
).test(content);
|
|
3587
|
+
if (definesHandler || definesDefaultNamed) {
|
|
3588
|
+
return file;
|
|
3589
|
+
}
|
|
3590
|
+
const reExportMatch = content.match(
|
|
3591
|
+
new RegExp(
|
|
3592
|
+
`export\\s*\\{[^}]*\\b${handlerRef}\\b[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`
|
|
3593
|
+
)
|
|
3594
|
+
);
|
|
3595
|
+
if (reExportMatch) {
|
|
3596
|
+
const importSpec = reExportMatch[1].replace(/\.js$/, "");
|
|
3597
|
+
const resolved = resolveModulePath$1(path.resolve(path.dirname(file), importSpec));
|
|
3598
|
+
if (resolved)
|
|
3599
|
+
return resolved;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
return null;
|
|
3604
|
+
}
|
|
3605
|
+
function extractBalancedBlock(source, openBraceIndex) {
|
|
3606
|
+
let depth = 0;
|
|
3607
|
+
for (let index = openBraceIndex; index < source.length; index++) {
|
|
3608
|
+
const char = source[index];
|
|
3609
|
+
if (char === "{")
|
|
3610
|
+
depth++;
|
|
3611
|
+
else if (char === "}") {
|
|
3612
|
+
depth--;
|
|
3613
|
+
if (depth === 0) {
|
|
3614
|
+
return source.slice(openBraceIndex, index + 1);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
return null;
|
|
3619
|
+
}
|
|
3620
|
+
function findFunctionBodyOpenBrace(source, searchFrom) {
|
|
3621
|
+
const arrowMatch = /=>\s*\{/.exec(source.slice(searchFrom));
|
|
3622
|
+
const parenMatch = /\)\s*(?::[^{]+)?\{/.exec(source.slice(searchFrom));
|
|
3623
|
+
const candidates = [];
|
|
3624
|
+
if (arrowMatch?.index !== void 0) {
|
|
3625
|
+
candidates.push(searchFrom + arrowMatch.index + arrowMatch[0].length - 1);
|
|
3626
|
+
}
|
|
3627
|
+
if (parenMatch?.index !== void 0) {
|
|
3628
|
+
candidates.push(searchFrom + parenMatch.index + parenMatch[0].length - 1);
|
|
3629
|
+
}
|
|
3630
|
+
if (candidates.length === 0)
|
|
3631
|
+
return -1;
|
|
3632
|
+
return Math.min(...candidates);
|
|
3633
|
+
}
|
|
3634
|
+
function extractFunctionBody(source, functionName) {
|
|
3635
|
+
const patterns = [
|
|
3636
|
+
new RegExp(`export\\s+async\\s+function\\s+${functionName}\\b`),
|
|
3637
|
+
new RegExp(`export\\s+function\\s+${functionName}\\b`),
|
|
3638
|
+
new RegExp(`export\\s+default\\s+async\\s+function\\s+${functionName}\\b`),
|
|
3639
|
+
new RegExp(
|
|
3640
|
+
`export\\s+const\\s+${functionName}\\s*=\\s*async\\s*\\([^)]*\\)\\s*(?::[^{]+)?=>\\s*\\{`
|
|
3641
|
+
),
|
|
3642
|
+
new RegExp(
|
|
3643
|
+
`export\\s+const\\s+${functionName}\\s*=\\s*\\([^)]*\\)\\s*(?::[^{]+)?=>\\s*\\{`
|
|
3644
|
+
)
|
|
3645
|
+
];
|
|
3646
|
+
for (const pattern of patterns) {
|
|
3647
|
+
const match = pattern.exec(source);
|
|
3648
|
+
if (!match)
|
|
3649
|
+
continue;
|
|
3650
|
+
const braceIndex = findFunctionBodyOpenBrace(source, match.index);
|
|
3651
|
+
if (braceIndex === -1)
|
|
3652
|
+
continue;
|
|
3653
|
+
return extractBalancedBlock(source, braceIndex);
|
|
3654
|
+
}
|
|
3655
|
+
return null;
|
|
3656
|
+
}
|
|
3657
|
+
function extractDefaultExportFunctionBody(source) {
|
|
3658
|
+
const patterns = [
|
|
3659
|
+
/export\s+default\s+async\s+function\s+\w+\b/,
|
|
3660
|
+
/export\s+default\s+async\s+function\s*\(/,
|
|
3661
|
+
/export\s+default\s+async\s*\([^)]*\)\s*(?::[^{]+)?=>\s*\{/
|
|
3662
|
+
];
|
|
3663
|
+
for (const pattern of patterns) {
|
|
3664
|
+
const match = pattern.exec(source);
|
|
3665
|
+
if (!match)
|
|
3666
|
+
continue;
|
|
3667
|
+
const braceIndex = findFunctionBodyOpenBrace(source, match.index);
|
|
3668
|
+
if (braceIndex === -1)
|
|
3669
|
+
continue;
|
|
3670
|
+
return extractBalancedBlock(source, braceIndex);
|
|
3671
|
+
}
|
|
3672
|
+
return null;
|
|
3673
|
+
}
|
|
3674
|
+
function handlerBodyWritesAddMenuCatalog(body) {
|
|
3675
|
+
return ADD_MENU_WRITE_MARKERS.some((marker) => body.includes(marker));
|
|
3676
|
+
}
|
|
3677
|
+
function resolveSetupHandlerFunctionBody(pluginPath, handlerRef, isNpmPackage) {
|
|
3678
|
+
const sourceFile = resolveHandlerSourceFile(pluginPath, handlerRef, isNpmPackage);
|
|
3679
|
+
if (!sourceFile)
|
|
3680
|
+
return null;
|
|
3681
|
+
const source = fs.readFileSync(sourceFile, "utf-8");
|
|
3682
|
+
if (isRelativeHandlerRef(handlerRef)) {
|
|
3683
|
+
return extractDefaultExportFunctionBody(source) ?? extractFunctionBody(source, "setup") ?? extractFunctionBody(source, handlerRef);
|
|
3684
|
+
}
|
|
3685
|
+
return extractFunctionBody(source, handlerRef);
|
|
3686
|
+
}
|
|
3687
|
+
function suggestionForCode(code) {
|
|
3688
|
+
if (!code)
|
|
3689
|
+
return `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
|
|
3690
|
+
return ADD_MENU_VALIDATION_SUGGESTIONS[code] ?? `See ${CONTRIBUTOR_GUIDE} for schema and validation rules`;
|
|
3691
|
+
}
|
|
3692
|
+
function mapSchemaError(error, catalogPath) {
|
|
3693
|
+
const code = error.code ?? "catalog-validation-error";
|
|
3694
|
+
return {
|
|
3695
|
+
type: "add-menu-catalog",
|
|
3696
|
+
code,
|
|
3697
|
+
message: `[${code}] ${error.message}`,
|
|
3698
|
+
location: error.path || catalogPath,
|
|
3699
|
+
suggestion: suggestionForCode(code)
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
function mapLintFinding(finding, catalogPath, severity) {
|
|
3703
|
+
return {
|
|
3704
|
+
type: "add-menu-catalog",
|
|
3705
|
+
code: finding.code,
|
|
3706
|
+
message: `[${finding.code}] ${finding.message}`,
|
|
3707
|
+
location: finding.sourcePath ?? catalogPath,
|
|
3708
|
+
itemId: finding.itemId,
|
|
3709
|
+
suggestion: suggestionForCode(finding.code),
|
|
3710
|
+
...severity === "warning" ? {} : {}
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
function pluginShipsAddMenuCatalog(context) {
|
|
3714
|
+
return ADD_MENU_CATALOG_REL_PATHS.some(
|
|
3715
|
+
(relPath) => fs.existsSync(path.join(context.pluginPath, relPath))
|
|
3716
|
+
);
|
|
3717
|
+
}
|
|
3718
|
+
function validateAddMenuAgentKitHandler(context, result) {
|
|
3719
|
+
if (!pluginShipsAddMenuCatalog(context))
|
|
3720
|
+
return;
|
|
3721
|
+
const agentKitHandler = context.manifest.agentkit;
|
|
3722
|
+
if (!agentKitHandler) {
|
|
3723
|
+
result.warnings.push({
|
|
3724
|
+
type: "add-menu-catalog",
|
|
3725
|
+
code: "add-menu-missing-agentkit-handler",
|
|
3726
|
+
message: "[add-menu-missing-agentkit-handler] Plugin ships add-menu catalog yaml but plugin.yaml has no agentkit handler — catalogs must be generated during jay-stack agent-kit (yarn agent-kit), not jay-stack setup",
|
|
3727
|
+
location: "plugin.yaml",
|
|
3728
|
+
suggestion: suggestionForCode("add-menu-missing-agentkit-handler")
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
const setupHandler = typeof context.manifest.setup === "string" ? context.manifest.setup : void 0;
|
|
3732
|
+
if (!setupHandler)
|
|
3733
|
+
return;
|
|
3734
|
+
const setupBody = resolveSetupHandlerFunctionBody(
|
|
3735
|
+
context.pluginPath,
|
|
3736
|
+
setupHandler,
|
|
3737
|
+
context.isNpmPackage
|
|
3738
|
+
);
|
|
3739
|
+
if (!setupBody || !handlerBodyWritesAddMenuCatalog(setupBody))
|
|
3740
|
+
return;
|
|
3741
|
+
result.warnings.push({
|
|
3742
|
+
type: "add-menu-catalog",
|
|
3743
|
+
code: "add-menu-legacy-setup-handler",
|
|
3744
|
+
message: "[add-menu-legacy-setup-handler] setup handler writes add-menu catalogs — move catalog materialization to agentkit and run jay-stack agent-kit (yarn agent-kit); keep setup for config and credentials only",
|
|
3745
|
+
location: `plugin.yaml setup (${setupHandler})`,
|
|
3746
|
+
suggestion: suggestionForCode("add-menu-legacy-setup-handler")
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
async function validateAddMenuCatalogFileAtPath(catalogPath, relPath, result) {
|
|
3750
|
+
let parsed;
|
|
3751
|
+
try {
|
|
3752
|
+
const content = await fs.promises.readFile(catalogPath, "utf-8");
|
|
3753
|
+
parsed = YAML.parse(content);
|
|
3754
|
+
} catch (error) {
|
|
3755
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3756
|
+
result.errors.push({
|
|
3757
|
+
type: "add-menu-catalog",
|
|
3758
|
+
code: "catalog-yaml-parse-error",
|
|
3759
|
+
message: `Invalid add-menu catalog YAML: ${message}`,
|
|
3760
|
+
location: relPath,
|
|
3761
|
+
suggestion: `Check YAML syntax in the add-menu catalog file. See ${CONTRIBUTOR_GUIDE}`
|
|
3762
|
+
});
|
|
3763
|
+
return;
|
|
3764
|
+
}
|
|
3765
|
+
const validated = validateAddMenuCatalogFile(parsed, relPath);
|
|
3766
|
+
result.errors.push(...validated.errors.map((error) => mapSchemaError(error, relPath)));
|
|
3767
|
+
if (!validated.file?.items.length) {
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
const linted = lintAddMenuCatalog(validated.file.items, relPath);
|
|
3771
|
+
result.errors.push(
|
|
3772
|
+
...linted.errors.map((finding) => mapLintFinding(finding, relPath, "error"))
|
|
3773
|
+
);
|
|
3774
|
+
result.warnings.push(
|
|
3775
|
+
...linted.warnings.map((finding) => mapLintFinding(finding, relPath, "warning"))
|
|
3776
|
+
);
|
|
3777
|
+
}
|
|
3778
|
+
async function validateAddMenuCatalog(context, result) {
|
|
3779
|
+
validateAddMenuAgentKitHandler(context, result);
|
|
3780
|
+
for (const relPath of ADD_MENU_CATALOG_REL_PATHS) {
|
|
3781
|
+
const catalogPath = path.join(context.pluginPath, relPath);
|
|
3782
|
+
if (!fs.existsSync(catalogPath))
|
|
3783
|
+
continue;
|
|
3784
|
+
await validateAddMenuCatalogFileAtPath(catalogPath, relPath, result);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3034
3787
|
async function validatePlugin(options = {}) {
|
|
3035
3788
|
const pluginPath = options.pluginPath || process.cwd();
|
|
3036
3789
|
if (options.local) {
|
|
@@ -3097,6 +3850,7 @@ async function validatePluginPackage(pluginPath, options) {
|
|
|
3097
3850
|
if (pluginManifest.dynamic_contracts) {
|
|
3098
3851
|
await validateDynamicContracts(context, result);
|
|
3099
3852
|
}
|
|
3853
|
+
await validateAddMenuCatalog(context, result);
|
|
3100
3854
|
result.valid = result.errors.length === 0;
|
|
3101
3855
|
return result;
|
|
3102
3856
|
}
|
|
@@ -3210,6 +3964,14 @@ async function validateSchema(context, result) {
|
|
|
3210
3964
|
location: "plugin.yaml",
|
|
3211
3965
|
suggestion: 'Specify the exported member name from the module (e.g., "moodTracker")'
|
|
3212
3966
|
});
|
|
3967
|
+
} else {
|
|
3968
|
+
validateHandlerRef(
|
|
3969
|
+
contract.component,
|
|
3970
|
+
`Contract "${contract.name}" component`,
|
|
3971
|
+
`plugin.yaml contracts[${index}]`,
|
|
3972
|
+
context,
|
|
3973
|
+
result
|
|
3974
|
+
);
|
|
3213
3975
|
}
|
|
3214
3976
|
});
|
|
3215
3977
|
}
|
|
@@ -3242,6 +4004,24 @@ async function validateSchema(context, result) {
|
|
|
3242
4004
|
suggestion: 'Specify prefix for dynamic contract names (e.g., "cms")'
|
|
3243
4005
|
});
|
|
3244
4006
|
}
|
|
4007
|
+
if (config.component) {
|
|
4008
|
+
validateHandlerRef(
|
|
4009
|
+
config.component,
|
|
4010
|
+
`dynamic_contracts[${prefix}] component`,
|
|
4011
|
+
`plugin.yaml dynamic_contracts`,
|
|
4012
|
+
context,
|
|
4013
|
+
result
|
|
4014
|
+
);
|
|
4015
|
+
}
|
|
4016
|
+
if (config.generator) {
|
|
4017
|
+
validateHandlerRef(
|
|
4018
|
+
config.generator,
|
|
4019
|
+
`dynamic_contracts[${prefix}] generator`,
|
|
4020
|
+
`plugin.yaml dynamic_contracts`,
|
|
4021
|
+
context,
|
|
4022
|
+
result
|
|
4023
|
+
);
|
|
4024
|
+
}
|
|
3245
4025
|
}
|
|
3246
4026
|
}
|
|
3247
4027
|
if (!manifest.contracts && !manifest.dynamic_contracts) {
|
|
@@ -3312,6 +4092,23 @@ async function validateSchema(context, result) {
|
|
|
3312
4092
|
});
|
|
3313
4093
|
}
|
|
3314
4094
|
}
|
|
4095
|
+
if (manifest.actions) {
|
|
4096
|
+
for (const entry of manifest.actions) {
|
|
4097
|
+
const exportName = typeof entry === "string" ? entry : entry.name;
|
|
4098
|
+
if (exportName) {
|
|
4099
|
+
validateHandlerRef(
|
|
4100
|
+
exportName,
|
|
4101
|
+
`Action "${exportName}"`,
|
|
4102
|
+
"plugin.yaml actions",
|
|
4103
|
+
context,
|
|
4104
|
+
result
|
|
4105
|
+
);
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
if (manifest.init) {
|
|
4110
|
+
validateHandlerRef(manifest.init, "Init handler", "plugin.yaml init", context, result);
|
|
4111
|
+
}
|
|
3315
4112
|
if (manifest.routes) {
|
|
3316
4113
|
if (!Array.isArray(manifest.routes)) {
|
|
3317
4114
|
result.errors.push({
|
|
@@ -3343,6 +4140,14 @@ async function validateSchema(context, result) {
|
|
|
3343
4140
|
location: "plugin.yaml",
|
|
3344
4141
|
suggestion: "Specify the exported member name for the page component"
|
|
3345
4142
|
});
|
|
4143
|
+
} else {
|
|
4144
|
+
validateHandlerRef(
|
|
4145
|
+
route.component,
|
|
4146
|
+
`Route "${route.path}" component`,
|
|
4147
|
+
`plugin.yaml routes`,
|
|
4148
|
+
context,
|
|
4149
|
+
result
|
|
4150
|
+
);
|
|
3346
4151
|
}
|
|
3347
4152
|
if (route.jayHtml) {
|
|
3348
4153
|
validateDocFile(
|
|
@@ -3382,22 +4187,112 @@ async function validateSchema(context, result) {
|
|
|
3382
4187
|
suggestion: "Specify the relative path to the validator handler module"
|
|
3383
4188
|
});
|
|
3384
4189
|
}
|
|
3385
|
-
if (validator.handler
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
location: "plugin.yaml validators",
|
|
3394
|
-
suggestion: `Create the validator handler at ${handlerPath}.ts`
|
|
3395
|
-
});
|
|
3396
|
-
}
|
|
4190
|
+
if (validator.handler) {
|
|
4191
|
+
validateHandlerRef(
|
|
4192
|
+
validator.handler,
|
|
4193
|
+
`Validator "${validator.name}" handler`,
|
|
4194
|
+
"plugin.yaml validators",
|
|
4195
|
+
context,
|
|
4196
|
+
result
|
|
4197
|
+
);
|
|
3397
4198
|
}
|
|
3398
4199
|
});
|
|
3399
4200
|
}
|
|
3400
4201
|
}
|
|
4202
|
+
if (manifest.setup) {
|
|
4203
|
+
if (typeof manifest.setup !== "string") {
|
|
4204
|
+
result.errors.push({
|
|
4205
|
+
type: "schema",
|
|
4206
|
+
message: "Deprecated nested setup keys (setup.handler / setup.references) — use flat setup: and agentkit: in plugin.yaml",
|
|
4207
|
+
location: "plugin.yaml setup",
|
|
4208
|
+
suggestion: "Replace setup.handler with top-level setup: and setup.references with top-level agentkit:"
|
|
4209
|
+
});
|
|
4210
|
+
} else {
|
|
4211
|
+
validateHandlerRef(
|
|
4212
|
+
manifest.setup,
|
|
4213
|
+
"Setup handler",
|
|
4214
|
+
"plugin.yaml setup",
|
|
4215
|
+
context,
|
|
4216
|
+
result
|
|
4217
|
+
);
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
if (manifest.agentkit) {
|
|
4221
|
+
validateHandlerRef(
|
|
4222
|
+
manifest.agentkit,
|
|
4223
|
+
"Agent-kit handler",
|
|
4224
|
+
"plugin.yaml agentkit",
|
|
4225
|
+
context,
|
|
4226
|
+
result
|
|
4227
|
+
);
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
function checkExportExists(exportName, context) {
|
|
4231
|
+
const packageJsonPath = path.join(context.pluginPath, "package.json");
|
|
4232
|
+
if (!fs.existsSync(packageJsonPath))
|
|
4233
|
+
return true;
|
|
4234
|
+
let mainPath;
|
|
4235
|
+
try {
|
|
4236
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
4237
|
+
if (packageJson.exports?.["."]) {
|
|
4238
|
+
const entry = packageJson.exports["."];
|
|
4239
|
+
const entryPath = typeof entry === "string" ? entry : entry.default || entry.import;
|
|
4240
|
+
if (entryPath)
|
|
4241
|
+
mainPath = path.join(context.pluginPath, entryPath);
|
|
4242
|
+
}
|
|
4243
|
+
if (!mainPath && packageJson.main) {
|
|
4244
|
+
mainPath = path.join(context.pluginPath, packageJson.main);
|
|
4245
|
+
}
|
|
4246
|
+
} catch {
|
|
4247
|
+
return true;
|
|
4248
|
+
}
|
|
4249
|
+
if (!mainPath || !fs.existsSync(mainPath))
|
|
4250
|
+
return true;
|
|
4251
|
+
try {
|
|
4252
|
+
const content = fs.readFileSync(mainPath, "utf-8");
|
|
4253
|
+
const patterns = [
|
|
4254
|
+
new RegExp(`export\\s*\\{[^}]*\\b${exportName}\\b[^}]*\\}`, "m"),
|
|
4255
|
+
new RegExp(`export\\s+(?:async\\s+)?function\\s+${exportName}\\b`),
|
|
4256
|
+
new RegExp(`export\\s+(?:const|let|var)\\s+${exportName}\\b`)
|
|
4257
|
+
];
|
|
4258
|
+
return patterns.some((p) => p.test(content));
|
|
4259
|
+
} catch {
|
|
4260
|
+
return true;
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
function isRelativePath(value) {
|
|
4264
|
+
return value.startsWith("./") || value.startsWith("../");
|
|
4265
|
+
}
|
|
4266
|
+
function validateHandlerRef(value, label, location, context, result) {
|
|
4267
|
+
if (context.isNpmPackage) {
|
|
4268
|
+
if (isRelativePath(value)) {
|
|
4269
|
+
result.errors.push({
|
|
4270
|
+
type: "export-mismatch",
|
|
4271
|
+
message: `${label} "${value}" is a relative path, but NPM plugins must use an export name`,
|
|
4272
|
+
location,
|
|
4273
|
+
suggestion: `Export the function from the package entry point and use the export name instead of a path`
|
|
4274
|
+
});
|
|
4275
|
+
} else if (!checkExportExists(value, context)) {
|
|
4276
|
+
result.errors.push({
|
|
4277
|
+
type: "export-mismatch",
|
|
4278
|
+
message: `${label} "${value}" is not exported from the package`,
|
|
4279
|
+
location,
|
|
4280
|
+
suggestion: `Add "export { ${value} } from '...'" to the package entry point`
|
|
4281
|
+
});
|
|
4282
|
+
}
|
|
4283
|
+
} else if (isRelativePath(value)) {
|
|
4284
|
+
const handlerPath = path.join(context.pluginPath, value);
|
|
4285
|
+
const extensions = ["", ".ts", ".js", "/index.ts", "/index.js"];
|
|
4286
|
+
const found = extensions.some((ext) => fs.existsSync(handlerPath + ext));
|
|
4287
|
+
if (!found) {
|
|
4288
|
+
result.errors.push({
|
|
4289
|
+
type: "file-missing",
|
|
4290
|
+
message: `${label} not found: ${value}`,
|
|
4291
|
+
location,
|
|
4292
|
+
suggestion: `Create the handler at ${handlerPath}.ts`
|
|
4293
|
+
});
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
3401
4296
|
}
|
|
3402
4297
|
function resolveContractFile(contractSpec, context) {
|
|
3403
4298
|
if (context.isNpmPackage) {
|
|
@@ -3717,6 +4612,18 @@ async function validatePackageJson(context, result) {
|
|
|
3717
4612
|
suggestion: 'Add "./plugin.yaml": "./plugin.yaml" to exports field'
|
|
3718
4613
|
});
|
|
3719
4614
|
}
|
|
4615
|
+
const agentKitDir = path.join(context.pluginPath, "agent-kit");
|
|
4616
|
+
if (fs.existsSync(agentKitDir) && fs.statSync(agentKitDir).isDirectory()) {
|
|
4617
|
+
const filesArray = packageJson.files;
|
|
4618
|
+
if (!filesArray || !filesArray.includes("agent-kit")) {
|
|
4619
|
+
result.warnings.push({
|
|
4620
|
+
type: "export-mismatch",
|
|
4621
|
+
message: 'agent-kit directory exists but is not listed in package.json "files"',
|
|
4622
|
+
location: packageJsonPath,
|
|
4623
|
+
suggestion: 'Add "agent-kit" to the "files" array so agent-kit files are shipped with the package'
|
|
4624
|
+
});
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
3720
4627
|
} catch (error) {
|
|
3721
4628
|
result.errors.push({
|
|
3722
4629
|
type: "schema",
|
|
@@ -4108,38 +5015,18 @@ function extractRouteParams(filePath, pagesBase) {
|
|
|
4108
5015
|
}
|
|
4109
5016
|
return params;
|
|
4110
5017
|
}
|
|
4111
|
-
function
|
|
4112
|
-
const
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
}
|
|
4118
|
-
function extractJayParams(content) {
|
|
4119
|
-
const root = parse(content, {
|
|
4120
|
-
comment: true,
|
|
4121
|
-
blockTextElements: { script: true, style: true }
|
|
4122
|
-
});
|
|
4123
|
-
const head = root.querySelector("head");
|
|
4124
|
-
if (!head)
|
|
4125
|
-
return /* @__PURE__ */ new Set();
|
|
4126
|
-
const paramScripts = head.querySelectorAll('script[type="application/jay-params"]');
|
|
4127
|
-
if (paramScripts.length !== 1)
|
|
4128
|
-
return /* @__PURE__ */ new Set();
|
|
4129
|
-
const body = dedentYaml(paramScripts[0].textContent ?? "");
|
|
4130
|
-
if (!body)
|
|
4131
|
-
return /* @__PURE__ */ new Set();
|
|
4132
|
-
try {
|
|
4133
|
-
const parsed = YAML.parse(body);
|
|
4134
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
4135
|
-
return new Set(Object.keys(parsed));
|
|
5018
|
+
function extractHeadlessPropsParamNames(parsedFile) {
|
|
5019
|
+
const names = /* @__PURE__ */ new Set();
|
|
5020
|
+
for (const imp of parsedFile.headlessImports) {
|
|
5021
|
+
if (imp.headlessProps) {
|
|
5022
|
+
for (const key of Object.keys(imp.headlessProps)) {
|
|
5023
|
+
names.add(key);
|
|
5024
|
+
}
|
|
4136
5025
|
}
|
|
4137
|
-
return /* @__PURE__ */ new Set();
|
|
4138
|
-
} catch {
|
|
4139
|
-
return /* @__PURE__ */ new Set();
|
|
4140
5026
|
}
|
|
5027
|
+
return names;
|
|
4141
5028
|
}
|
|
4142
|
-
function checkRouteParams(parsedFile, filePath, pagesBase
|
|
5029
|
+
function checkRouteParams(parsedFile, filePath, pagesBase) {
|
|
4143
5030
|
const requiredParams = /* @__PURE__ */ new Set();
|
|
4144
5031
|
function collectParams(params) {
|
|
4145
5032
|
for (const p of params) {
|
|
@@ -4159,13 +5046,13 @@ function checkRouteParams(parsedFile, filePath, pagesBase, jayHtmlContent) {
|
|
|
4159
5046
|
if (requiredParams.size === 0)
|
|
4160
5047
|
return [];
|
|
4161
5048
|
const routeParams = extractRouteParams(filePath, pagesBase);
|
|
4162
|
-
const
|
|
4163
|
-
const availableParams = /* @__PURE__ */ new Set([...routeParams, ...
|
|
5049
|
+
const headlessProps = extractHeadlessPropsParamNames(parsedFile);
|
|
5050
|
+
const availableParams = /* @__PURE__ */ new Set([...routeParams, ...headlessProps]);
|
|
4164
5051
|
const warnings = [];
|
|
4165
5052
|
for (const param of requiredParams) {
|
|
4166
5053
|
if (!availableParams.has(param)) {
|
|
4167
5054
|
warnings.push(
|
|
4168
|
-
`Contract requires param "${param}" but the route does not provide it. Add a dynamic segment [${param}] to the route path or
|
|
5055
|
+
`Contract requires param "${param}" but the route does not provide it. Add a dynamic segment [${param}] to the route path or provide it in the headless component's YAML body.`
|
|
4169
5056
|
);
|
|
4170
5057
|
}
|
|
4171
5058
|
}
|
|
@@ -4218,6 +5105,32 @@ const HEADLESS_SKIP_ATTRS = /* @__PURE__ */ new Set([
|
|
|
4218
5105
|
"jay-coordinate-base",
|
|
4219
5106
|
"jay-scope"
|
|
4220
5107
|
]);
|
|
5108
|
+
const PHASE_ORDER = {
|
|
5109
|
+
slow: 0,
|
|
5110
|
+
fast: 1,
|
|
5111
|
+
"fast+interactive": 2
|
|
5112
|
+
};
|
|
5113
|
+
function resolveBindingPhase(bindingPath, jayHtml) {
|
|
5114
|
+
const segments = bindingPath.split(".");
|
|
5115
|
+
const root = segments[0];
|
|
5116
|
+
const keyedImport = jayHtml.headlessImports.find((i) => i.key === root && i.contract);
|
|
5117
|
+
if (keyedImport?.contract) {
|
|
5118
|
+
const tagPath = segments.slice(1).join(".");
|
|
5119
|
+
if (!tagPath)
|
|
5120
|
+
return void 0;
|
|
5121
|
+
const tag = resolveContractTag(keyedImport.contract, tagPath);
|
|
5122
|
+
if (!tag)
|
|
5123
|
+
return void 0;
|
|
5124
|
+
return tag.phase || "slow";
|
|
5125
|
+
}
|
|
5126
|
+
if (jayHtml.contract) {
|
|
5127
|
+
const tag = resolveContractTag(jayHtml.contract, bindingPath);
|
|
5128
|
+
if (!tag)
|
|
5129
|
+
return void 0;
|
|
5130
|
+
return tag.phase || "slow";
|
|
5131
|
+
}
|
|
5132
|
+
return void 0;
|
|
5133
|
+
}
|
|
4221
5134
|
function checkHeadlessInstanceProps(jayHtml, file) {
|
|
4222
5135
|
const imports = jayHtml.headlessImports;
|
|
4223
5136
|
const warnings = [];
|
|
@@ -4236,25 +5149,53 @@ function checkHeadlessInstanceProps(jayHtml, file) {
|
|
|
4236
5149
|
}
|
|
4237
5150
|
}
|
|
4238
5151
|
if (passedProps.size > 0) {
|
|
4239
|
-
const
|
|
5152
|
+
const contractPropNamesLower = new Set(
|
|
5153
|
+
(contract.props || []).map((p) => p.name.toLowerCase())
|
|
5154
|
+
);
|
|
4240
5155
|
for (const prop of passedProps) {
|
|
4241
|
-
if (!
|
|
4242
|
-
imp.key ? `${imp.key} (${contractName})` : contractName;
|
|
5156
|
+
if (!contractPropNamesLower.has(prop.toLowerCase())) {
|
|
4243
5157
|
warnings.push(
|
|
4244
5158
|
`<jay:${contractName}> passes attribute "${prop}" but the "${contract.name}" contract does not declare it as a prop. Add to ${contractName}.jay-contract: props: [{ name: ${prop}, type: string }]`
|
|
4245
5159
|
);
|
|
4246
5160
|
}
|
|
4247
5161
|
}
|
|
4248
5162
|
}
|
|
5163
|
+
const passedPropsLower = new Set([...passedProps].map((p) => p.toLowerCase()));
|
|
4249
5164
|
if (contract.props) {
|
|
4250
5165
|
for (const contractProp of contract.props) {
|
|
4251
|
-
if (contractProp.required && !
|
|
5166
|
+
if (contractProp.required && !passedPropsLower.has(contractProp.name.toLowerCase())) {
|
|
4252
5167
|
warnings.push(
|
|
4253
5168
|
`<jay:${contractName}> is missing required prop "${contractProp.name}" declared in the "${contract.name}" contract.`
|
|
4254
5169
|
);
|
|
4255
5170
|
}
|
|
4256
5171
|
}
|
|
4257
5172
|
}
|
|
5173
|
+
if (contract.props) {
|
|
5174
|
+
const lowerAttrs = {};
|
|
5175
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
5176
|
+
lowerAttrs[k.toLowerCase()] = v;
|
|
5177
|
+
}
|
|
5178
|
+
for (const contractProp of contract.props) {
|
|
5179
|
+
const attrValue = lowerAttrs[contractProp.name.toLowerCase()];
|
|
5180
|
+
if (!attrValue)
|
|
5181
|
+
continue;
|
|
5182
|
+
const bindingMatch = attrValue.match(/^\{(.+)\}$/);
|
|
5183
|
+
if (!bindingMatch)
|
|
5184
|
+
continue;
|
|
5185
|
+
const bindingPath = bindingMatch[1];
|
|
5186
|
+
const sourcePhase = resolveBindingPhase(bindingPath, jayHtml);
|
|
5187
|
+
if (!sourcePhase)
|
|
5188
|
+
continue;
|
|
5189
|
+
const propPhase = contractProp.phase ?? "slow";
|
|
5190
|
+
const sourceOrder = PHASE_ORDER[sourcePhase] ?? 0;
|
|
5191
|
+
const propOrder = PHASE_ORDER[propPhase] ?? 0;
|
|
5192
|
+
if (sourceOrder > propOrder) {
|
|
5193
|
+
warnings.push(
|
|
5194
|
+
`<jay:${contractName}> prop "${contractProp.name}" (phase: ${propPhase}) is bound to {${bindingPath}} which is phase: ${sourcePhase}. The binding source phase must be ≤ the prop phase. Use a ${propPhase}-phase binding, a route param, or a literal value.`
|
|
5195
|
+
);
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
4258
5199
|
}
|
|
4259
5200
|
}
|
|
4260
5201
|
for (const child of element.childNodes ?? []) {
|
|
@@ -4294,6 +5235,7 @@ async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
|
|
|
4294
5235
|
if (!plugin.manifest.validators)
|
|
4295
5236
|
continue;
|
|
4296
5237
|
for (const validatorDef of plugin.manifest.validators) {
|
|
5238
|
+
const source = `${plugin.name}/${validatorDef.name}`;
|
|
4297
5239
|
let validatorFn;
|
|
4298
5240
|
try {
|
|
4299
5241
|
let handlerModule;
|
|
@@ -4308,19 +5250,22 @@ async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
|
|
|
4308
5250
|
errors.push({
|
|
4309
5251
|
file: `plugin:${plugin.name}`,
|
|
4310
5252
|
message: `Validator "${validatorDef.name}" handler does not export a "validate" function`,
|
|
4311
|
-
stage: "plugin"
|
|
5253
|
+
stage: "plugin",
|
|
5254
|
+
source
|
|
4312
5255
|
});
|
|
5256
|
+
loadedValidators.push(source);
|
|
4313
5257
|
continue;
|
|
4314
5258
|
}
|
|
4315
5259
|
} catch (loadErr) {
|
|
4316
5260
|
errors.push({
|
|
4317
5261
|
file: `plugin:${plugin.name}`,
|
|
4318
5262
|
message: `Failed to load validator "${validatorDef.name}": ${loadErr.message}`,
|
|
4319
|
-
stage: "plugin"
|
|
5263
|
+
stage: "plugin",
|
|
5264
|
+
source
|
|
4320
5265
|
});
|
|
5266
|
+
loadedValidators.push(source);
|
|
4321
5267
|
continue;
|
|
4322
5268
|
}
|
|
4323
|
-
const source = `${plugin.name}/${validatorDef.name}`;
|
|
4324
5269
|
loadedValidators.push(source);
|
|
4325
5270
|
for (const { relativePath, parsed } of parsedFiles) {
|
|
4326
5271
|
const pageContractPath = parsed.contractRef ? path.resolve(
|
|
@@ -4331,6 +5276,7 @@ async function runPluginValidators(projectRoot, parsedFiles, errors, warnings) {
|
|
|
4331
5276
|
const ctx = {
|
|
4332
5277
|
filePath: relativePath,
|
|
4333
5278
|
body: parsed.body,
|
|
5279
|
+
css: parsed.css,
|
|
4334
5280
|
head: parsed.headMeta,
|
|
4335
5281
|
contract: resolvedPageContract ? {
|
|
4336
5282
|
name: resolvedPageContract.name,
|
|
@@ -4477,7 +5423,13 @@ async function validateJayFiles(options = {}) {
|
|
|
4477
5423
|
continue;
|
|
4478
5424
|
}
|
|
4479
5425
|
parsedFiles.push({ relativePath, parsed: parsedFile.val });
|
|
4480
|
-
|
|
5426
|
+
if (content.includes("application/jay-params")) {
|
|
5427
|
+
warnings.push({
|
|
5428
|
+
file: relativePath,
|
|
5429
|
+
message: '<script type="application/jay-params"> is deprecated. Move the values into the YAML body of the headless component that uses them. See agent-kit/developer/routing.md for details.'
|
|
5430
|
+
});
|
|
5431
|
+
}
|
|
5432
|
+
const routeParamWarnings = checkRouteParams(parsedFile.val, jayFile, scanDir);
|
|
4481
5433
|
for (const msg of routeParamWarnings) {
|
|
4482
5434
|
warnings.push({ file: relativePath, message: msg });
|
|
4483
5435
|
}
|
|
@@ -4601,11 +5553,25 @@ function printJayValidationResult(result, options) {
|
|
|
4601
5553
|
logger.important(chalk.blue(` Suggestion: ${error.suggestion}`));
|
|
4602
5554
|
}
|
|
4603
5555
|
}
|
|
5556
|
+
const fileGroups = /* @__PURE__ */ new Map();
|
|
4604
5557
|
for (const warning of warns) {
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
5558
|
+
const group = fileGroups.get(warning.file) || [];
|
|
5559
|
+
group.push(warning);
|
|
5560
|
+
fileGroups.set(warning.file, group);
|
|
5561
|
+
}
|
|
5562
|
+
for (const [file, groupWarns] of fileGroups) {
|
|
5563
|
+
logger.important(chalk.yellow(` ⚠ ${file}`));
|
|
5564
|
+
for (const warning of groupWarns) {
|
|
5565
|
+
if (warning.message) {
|
|
5566
|
+
logger.important(chalk.gray(` ${warning.message}`));
|
|
5567
|
+
}
|
|
5568
|
+
}
|
|
5569
|
+
const suggestions = [...new Set(groupWarns.map((w) => w.suggestion).filter(Boolean))];
|
|
5570
|
+
if (suggestions.length > 0) {
|
|
5571
|
+
logger.important(chalk.blue(` Suggestions:`));
|
|
5572
|
+
for (const s2 of suggestions) {
|
|
5573
|
+
logger.important(chalk.blue(` ${s2}`));
|
|
5574
|
+
}
|
|
4609
5575
|
}
|
|
4610
5576
|
}
|
|
4611
5577
|
}
|
|
@@ -4660,12 +5626,12 @@ async function runValidatePlugin(pluginPath, options) {
|
|
|
4660
5626
|
strict: options.strict,
|
|
4661
5627
|
generateTypes: options.generateTypes
|
|
4662
5628
|
});
|
|
4663
|
-
|
|
5629
|
+
printPluginValidationResult(result, options.verbose ?? false);
|
|
4664
5630
|
if (!result.valid || options.strict && result.warnings.length > 0) {
|
|
4665
5631
|
process.exit(1);
|
|
4666
5632
|
}
|
|
4667
5633
|
}
|
|
4668
|
-
function
|
|
5634
|
+
function printPluginValidationResult(result, verbose) {
|
|
4669
5635
|
const logger = getLogger();
|
|
4670
5636
|
if (result.valid && result.warnings.length === 0) {
|
|
4671
5637
|
logger.important(chalk.green("Plugin validation successful!\n"));
|
|
@@ -4726,7 +5692,7 @@ async function runAgentKit(options) {
|
|
|
4726
5692
|
await ensureAgentKitDocs(projectRoot, options.force, options.mode);
|
|
4727
5693
|
await mergePluginAgentKitGuides(projectRoot, options.mode);
|
|
4728
5694
|
if (options.references !== false) {
|
|
4729
|
-
await
|
|
5695
|
+
await generatePluginAgentKit(projectRoot, options, initErrors, viteServer);
|
|
4730
5696
|
}
|
|
4731
5697
|
}
|
|
4732
5698
|
} finally {
|
|
@@ -4923,9 +5889,9 @@ async function mergePluginAgentKitGuides(projectRoot, mode) {
|
|
|
4923
5889
|
await fs$1.appendFile(instructionsPath, lines.join("\n"));
|
|
4924
5890
|
}
|
|
4925
5891
|
}
|
|
4926
|
-
async function
|
|
4927
|
-
const {
|
|
4928
|
-
const plugins = await
|
|
5892
|
+
async function generatePluginAgentKit(projectRoot, options, initErrors, viteServer) {
|
|
5893
|
+
const { discoverPluginsWithAgentKit, executePluginAgentKit } = await import("@jay-framework/stack-server-runtime");
|
|
5894
|
+
const plugins = await discoverPluginsWithAgentKit({
|
|
4929
5895
|
projectRoot,
|
|
4930
5896
|
verbose: options.verbose,
|
|
4931
5897
|
pluginFilter: options.plugin
|
|
@@ -4934,35 +5900,35 @@ async function generatePluginReferences(projectRoot, options, initErrors, viteSe
|
|
|
4934
5900
|
return;
|
|
4935
5901
|
const logger = getLogger();
|
|
4936
5902
|
logger.important("");
|
|
4937
|
-
logger.important(chalk.bold("Generating plugin
|
|
5903
|
+
logger.important(chalk.bold("Generating plugin agent-kit data..."));
|
|
4938
5904
|
for (const plugin of plugins) {
|
|
4939
5905
|
const pluginInitError = initErrors.get(plugin.name);
|
|
4940
5906
|
if (pluginInitError) {
|
|
4941
5907
|
logger.warn(
|
|
4942
5908
|
chalk.yellow(
|
|
4943
|
-
` ${plugin.name}:
|
|
5909
|
+
` ${plugin.name}: agent-kit skipped — init failed: ${pluginInitError.message}`
|
|
4944
5910
|
)
|
|
4945
5911
|
);
|
|
4946
5912
|
continue;
|
|
4947
5913
|
}
|
|
4948
5914
|
try {
|
|
4949
|
-
const result = await
|
|
5915
|
+
const result = await executePluginAgentKit(plugin, {
|
|
4950
5916
|
projectRoot,
|
|
4951
5917
|
force: options.force ?? false,
|
|
4952
5918
|
viteServer,
|
|
4953
5919
|
verbose: options.verbose
|
|
4954
5920
|
});
|
|
4955
|
-
if (result.
|
|
5921
|
+
if (result.agentKitCreated.length > 0) {
|
|
4956
5922
|
logger.important(chalk.green(` ${plugin.name}:`));
|
|
4957
|
-
for (const
|
|
4958
|
-
logger.important(chalk.gray(` ${
|
|
5923
|
+
for (const created of result.agentKitCreated) {
|
|
5924
|
+
logger.important(chalk.gray(` ${created}`));
|
|
4959
5925
|
}
|
|
4960
5926
|
if (result.message) {
|
|
4961
5927
|
logger.important(chalk.gray(` ${result.message}`));
|
|
4962
5928
|
}
|
|
4963
5929
|
}
|
|
4964
5930
|
} catch (error) {
|
|
4965
|
-
logger.warn(chalk.yellow(` ${plugin.name}:
|
|
5931
|
+
logger.warn(chalk.yellow(` ${plugin.name}: agent-kit skipped — ${error.message}`));
|
|
4966
5932
|
}
|
|
4967
5933
|
}
|
|
4968
5934
|
}
|
|
@@ -5462,9 +6428,8 @@ program.command("rebuild").description("Rebuild instances by contract, route, or
|
|
|
5462
6428
|
});
|
|
5463
6429
|
program.command("cleanup").description("Delete orphaned files from previous rebuilds").option("--version <n>", "Build version (default: from package.json)").option("-p, --path <path>", "Project root (default: cwd)").action(async (options) => {
|
|
5464
6430
|
try {
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
const ctx = await resolveProductionContext2(options.path, options.version);
|
|
6431
|
+
initLogger();
|
|
6432
|
+
const ctx = await resolveProductionContext(options.path, options.version);
|
|
5468
6433
|
const { cleanupOrphanedFiles } = await import("@jay-framework/production-server");
|
|
5469
6434
|
await cleanupOrphanedFiles(ctx.buildRoot, ctx.version);
|
|
5470
6435
|
} catch (error) {
|