@jay-framework/aiditor 0.17.4 → 0.18.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 +1 -5
- package/dist/actions/check-add-page-route.jay-action +12 -0
- package/dist/actions/check-add-page-route.jay-action.d.ts +10 -0
- package/dist/actions/check-page-add-page-brief.jay-action +9 -0
- package/dist/actions/check-page-add-page-brief.jay-action.d.ts +8 -0
- package/dist/actions/check-plugin-update.jay-action +14 -0
- package/dist/actions/check-plugin-update.jay-action.d.ts +13 -0
- package/dist/actions/ensure-project-plugin.jay-action +9 -0
- package/dist/actions/ensure-project-plugin.jay-action.d.ts +8 -0
- package/dist/actions/generate-add-page-brief-from-image.jay-action +20 -0
- package/dist/actions/generate-add-page-brief-from-image.jay-action.d.ts +20 -0
- package/dist/actions/get-plugin-setup-status.jay-action +9 -0
- package/dist/actions/get-plugin-setup-status.jay-action.d.ts +6 -0
- package/dist/actions/list-jay-plugins.jay-action +8 -0
- package/dist/actions/list-jay-plugins.jay-action.d.ts +5 -0
- package/dist/actions/list-plugin-contracts.jay-action +11 -0
- package/dist/actions/list-plugin-contracts.jay-action.d.ts +9 -0
- package/dist/actions/open-add-page-from-brief.jay-action +20 -0
- package/dist/actions/open-add-page-from-brief.jay-action.d.ts +18 -0
- package/dist/actions/reclassify-add-page-asset.jay-action +12 -0
- package/dist/actions/reclassify-add-page-asset.jay-action.d.ts +10 -0
- package/dist/actions/rerun-plugin-setup.jay-action +12 -0
- package/dist/actions/rerun-plugin-setup.jay-action.d.ts +10 -0
- package/dist/actions/save-add-page-draft.jay-action +10 -0
- package/dist/actions/save-add-page-draft.jay-action.d.ts +9 -0
- package/dist/actions/start-add-page-request.jay-action +12 -0
- package/dist/actions/start-add-page-request.jay-action.d.ts +10 -0
- package/dist/actions/submit-add-page.jay-action +21 -0
- package/dist/actions/submit-add-page.jay-action.d.ts +17 -0
- package/dist/actions/submit-task.jay-action +1 -0
- package/dist/actions/submit-task.jay-action.d.ts +1 -0
- package/dist/actions/sync-add-page-plugin-manifest.jay-action +11 -0
- package/dist/actions/sync-add-page-plugin-manifest.jay-action.d.ts +9 -0
- package/dist/actions/upload-add-page-asset.jay-action +13 -0
- package/dist/actions/upload-add-page-asset.jay-action.d.ts +13 -0
- package/dist/actions/write-plugin-source.jay-action +13 -0
- package/dist/actions/write-plugin-source.jay-action.d.ts +12 -0
- package/dist/add-page-agent/SKILL.md +79 -0
- package/dist/add-page-agent/system.md +47 -0
- package/dist/index.client.js +2695 -277
- package/dist/index.d.ts +797 -6
- package/dist/index.js +2682 -14
- package/dist/pages/aiditor/page.jay-html +603 -2
- package/dist/prompts/add-page-figma-brief-prompt.md +163 -0
- package/dist/prompts/add-page-figma-design-definitions-prompt.md +256 -0
- package/dist/prompts/add-page-figma-design-from-image-prompt.md +144 -0
- package/package.json +24 -7
- package/plugin.yaml +34 -0
package/dist/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { makeJayQuery, makeJayStream, makeJayStackComponent, phaseOutput, RenderPipeline } from "@jay-framework/fullstack-component";
|
|
2
2
|
import { DEV_SERVER_SERVICE } from "@jay-framework/dev-server";
|
|
3
|
-
import fs, { readFile } from "fs/promises";
|
|
3
|
+
import fs, { readFile, readdir } from "fs/promises";
|
|
4
4
|
import path, { extname } from "path";
|
|
5
5
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
6
6
|
import fs$1 from "fs";
|
|
7
7
|
import crypto from "crypto";
|
|
8
|
+
import { getLogger } from "@jay-framework/logger";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
import { scanRoutes } from "@jay-framework/stack-route-scanner";
|
|
8
12
|
import { setActionCallerOptions } from "@jay-framework/stack-client-runtime";
|
|
9
13
|
const getAiditorBootstrap = makeJayQuery(
|
|
10
14
|
"aiditor.getAiditorBootstrap"
|
|
@@ -225,6 +229,48 @@ async function replaceRouteSessionAfterStaleResume(projectDir, routeKey) {
|
|
|
225
229
|
return { sessionId };
|
|
226
230
|
});
|
|
227
231
|
}
|
|
232
|
+
const AGENT_KIT_PREAMBLE = `You are working in a Jay Stack project. The current working directory is the project root.
|
|
233
|
+
- Read agent-kit/plugins-index.yaml for installed plugins and materialized contract paths.
|
|
234
|
+
- Follow Jay Stack conventions for pages, routing, and headless component binding.
|
|
235
|
+
- Use Read/Glob to consult agent-kit guides when needed; do not guess contract field names.`;
|
|
236
|
+
function loadAgentKitSystemPrompt(projectDir) {
|
|
237
|
+
const parts = [];
|
|
238
|
+
const designerPath = path.join(
|
|
239
|
+
projectDir,
|
|
240
|
+
"agent-kit",
|
|
241
|
+
"designer",
|
|
242
|
+
"INSTRUCTIONS.md"
|
|
243
|
+
);
|
|
244
|
+
const developerPath = path.join(
|
|
245
|
+
projectDir,
|
|
246
|
+
"agent-kit",
|
|
247
|
+
"developer",
|
|
248
|
+
"INSTRUCTIONS.md"
|
|
249
|
+
);
|
|
250
|
+
if (fs$1.existsSync(designerPath)) {
|
|
251
|
+
parts.push(fs$1.readFileSync(designerPath, "utf-8"));
|
|
252
|
+
}
|
|
253
|
+
if (fs$1.existsSync(developerPath)) {
|
|
254
|
+
if (parts.length > 0) parts.push("\n\n---\n\n");
|
|
255
|
+
parts.push(fs$1.readFileSync(developerPath, "utf-8"));
|
|
256
|
+
}
|
|
257
|
+
if (parts.length > 0) parts.push("\n\n");
|
|
258
|
+
parts.push(AGENT_KIT_PREAMBLE);
|
|
259
|
+
return parts.join("");
|
|
260
|
+
}
|
|
261
|
+
function buildChatPrompt(config, pageRoute, renderedUrl, userMessage) {
|
|
262
|
+
const lines = [
|
|
263
|
+
`Project directory: ${config.projectDir}`,
|
|
264
|
+
`Current page route: ${pageRoute}`,
|
|
265
|
+
renderedUrl ? `Rendered preview URL: ${renderedUrl}` : null,
|
|
266
|
+
"",
|
|
267
|
+
"The user is continuing a conversation about this Jay project. Answer questions and perform requested edits using agent-kit. Read contract files from agent-kit/plugins-index.yaml when binding UI.",
|
|
268
|
+
"",
|
|
269
|
+
"User message:",
|
|
270
|
+
userMessage
|
|
271
|
+
];
|
|
272
|
+
return lines.filter((l) => l !== null).join("\n");
|
|
273
|
+
}
|
|
228
274
|
function buildNonVisualPrompt(config, notes, imagePath) {
|
|
229
275
|
const lines = [
|
|
230
276
|
`Project directory: ${config.projectDir}`,
|
|
@@ -239,6 +285,22 @@ const ANNOTATION_TARGETING_RULES = `Annotation targeting (follow strictly):
|
|
|
239
285
|
- The only authoritative edit target is the element indicated by the annotation marker (dot, arrow, or highlighted region). Do not choose another element because it is colorful, prominent, or a common control (e.g. buttons, links) unless the marker clearly overlaps that element.
|
|
240
286
|
- If several elements could match, prefer the text or content under/near the marker (e.g. a heading) over unrelated controls elsewhere on the page.
|
|
241
287
|
- If you cannot confidently map the marker to a single target in source, do not change a plausible but unmarked control. Briefly say what is ambiguous and list candidate elements or ask for clarification.`;
|
|
288
|
+
function structuredAnnotationBindings(ann) {
|
|
289
|
+
if (ann.pluginBindings?.length) return ann.pluginBindings;
|
|
290
|
+
if (ann.pluginBinding) return [ann.pluginBinding];
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
function appendBindingPromptLines(body, bindings, markerId, indent = "") {
|
|
294
|
+
if (bindings.length === 0) return;
|
|
295
|
+
body.push(`${indent}Headless bindings:`);
|
|
296
|
+
for (const b of bindings) {
|
|
297
|
+
const key = b.componentKey ? ` (key: ${b.componentKey})` : "";
|
|
298
|
+
body.push(`${indent}- Plugin: ${b.packageName} / ${b.contractName}${key}`);
|
|
299
|
+
body.push(
|
|
300
|
+
b.scope === "page" ? `${indent}- Scope: anywhere on page — add or connect this component where it best satisfies the instruction` : `${indent}- Scope: at marker — wire the element indicated by this marker to this contract's refs`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
242
304
|
function parseNotesField(notes) {
|
|
243
305
|
const empty = () => ({
|
|
244
306
|
structured: null,
|
|
@@ -279,7 +341,7 @@ function parseNotesField(notes) {
|
|
|
279
341
|
plain: ""
|
|
280
342
|
};
|
|
281
343
|
}
|
|
282
|
-
if (j.version === 1 && Array.isArray(j.annotations) && j.annotations.length > 0 && j.annotations.every(
|
|
344
|
+
if ((j.version === 1 || j.version === 1.1) && Array.isArray(j.annotations) && j.annotations.length > 0 && j.annotations.every(
|
|
283
345
|
(a) => a && typeof a === "object" && typeof a.id === "string" && typeof a.instruction === "string" && typeof a.mode === "string"
|
|
284
346
|
)) {
|
|
285
347
|
return {
|
|
@@ -320,6 +382,7 @@ function buildVisualPromptDual(config, pageRoute, renderedUrl, dual, parsed, att
|
|
|
320
382
|
const pin = ann.id;
|
|
321
383
|
const paths = attachmentPathsByAnnotationId.get(pin) ?? [];
|
|
322
384
|
body.push(`Annotation ${pin} (${ann.mode})`);
|
|
385
|
+
appendBindingPromptLines(body, structuredAnnotationBindings(ann), ann.id);
|
|
323
386
|
body.push(`Instruction: ${ann.instruction.trim()}`);
|
|
324
387
|
if (paths.length > 0) {
|
|
325
388
|
body.push(
|
|
@@ -392,6 +455,12 @@ function buildVisualPromptVideo(config, pageRoute, renderedUrl, videoPath, frame
|
|
|
392
455
|
const pinNum = pinById.get(ann.id) ?? "?";
|
|
393
456
|
const paths = attachmentPathsByPin.get(pinNum) ?? [];
|
|
394
457
|
body.push(`- Pin ${pinNum} (id ${ann.id}) — ${ann.mode}`);
|
|
458
|
+
appendBindingPromptLines(
|
|
459
|
+
body,
|
|
460
|
+
structuredAnnotationBindings(ann),
|
|
461
|
+
ann.id,
|
|
462
|
+
" "
|
|
463
|
+
);
|
|
395
464
|
body.push(` Instruction: ${ann.instruction.trim()}`);
|
|
396
465
|
if (ann.previewUrlAtTime) {
|
|
397
466
|
body.push(
|
|
@@ -420,7 +489,7 @@ function persistFile(file, destDir, fileName) {
|
|
|
420
489
|
fs$1.copyFileSync(file.path, dest);
|
|
421
490
|
return dest;
|
|
422
491
|
}
|
|
423
|
-
const submitTaskAction = makeJayStream("aiditor.submitTask").withFiles(
|
|
492
|
+
const submitTaskAction = makeJayStream("aiditor.submitTask").withFiles().withHandler(async function* (input) {
|
|
424
493
|
const projectDir = process.cwd();
|
|
425
494
|
const buildFolder = path.join(projectDir, "build");
|
|
426
495
|
const taskId = Math.random().toString(36).slice(2, 10);
|
|
@@ -503,20 +572,18 @@ const submitTaskAction = makeJayStream("aiditor.submitTask").withFiles({ maxFile
|
|
|
503
572
|
parsed,
|
|
504
573
|
attachmentPathsByAnnotationId
|
|
505
574
|
);
|
|
575
|
+
} else if (input.messageKind === "chat") {
|
|
576
|
+
promptContent = buildChatPrompt(
|
|
577
|
+
config,
|
|
578
|
+
input.pageRoute ?? "/",
|
|
579
|
+
input.renderedUrl ?? "",
|
|
580
|
+
input.notes
|
|
581
|
+
);
|
|
506
582
|
} else {
|
|
507
583
|
promptContent = buildNonVisualPrompt(config, input.notes);
|
|
508
584
|
}
|
|
509
585
|
yield { type: "status", message: "Running agent..." };
|
|
510
|
-
|
|
511
|
-
const agentKitPath = path.join(
|
|
512
|
-
projectDir,
|
|
513
|
-
"agent-kit",
|
|
514
|
-
"designer",
|
|
515
|
-
"INSTRUCTIONS.md"
|
|
516
|
-
);
|
|
517
|
-
if (fs$1.existsSync(agentKitPath)) {
|
|
518
|
-
systemPrompt = fs$1.readFileSync(agentKitPath, "utf-8");
|
|
519
|
-
}
|
|
586
|
+
const systemPrompt = loadAgentKitSystemPrompt(projectDir);
|
|
520
587
|
const routeKey = normalizePageRoute(input.pageRoute);
|
|
521
588
|
const sessionPlan = await getOrCreateSessionIdForRoute(
|
|
522
589
|
projectDir,
|
|
@@ -592,6 +659,2590 @@ const submitTaskAction = makeJayStream("aiditor.submitTask").withFiles({ maxFile
|
|
|
592
659
|
}
|
|
593
660
|
yield { type: "done" };
|
|
594
661
|
});
|
|
662
|
+
const CATALOG = [
|
|
663
|
+
{
|
|
664
|
+
pluginName: "wix-server-client",
|
|
665
|
+
packageName: "@jay-framework/wix-server-client",
|
|
666
|
+
description: "Wix API client and authentication",
|
|
667
|
+
kind: "service-only",
|
|
668
|
+
requires: [],
|
|
669
|
+
showInAddPagePicker: false,
|
|
670
|
+
configFiles: ["config/.wix.yaml"]
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
pluginName: "wix-cart",
|
|
674
|
+
packageName: "@jay-framework/wix-cart",
|
|
675
|
+
description: "Shopping cart headless components",
|
|
676
|
+
kind: "headless",
|
|
677
|
+
requires: ["wix-server-client"],
|
|
678
|
+
showInAddPagePicker: true
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
pluginName: "wix-stores",
|
|
682
|
+
packageName: "@jay-framework/wix-stores",
|
|
683
|
+
description: "Wix Stores product search, product page, categories",
|
|
684
|
+
kind: "headless",
|
|
685
|
+
requires: ["wix-server-client", "wix-cart"],
|
|
686
|
+
showInAddPagePicker: true,
|
|
687
|
+
configFiles: ["config/.wix-stores.yaml"]
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
pluginName: "wix-stores-v1",
|
|
691
|
+
packageName: "@jay-framework/wix-stores-v1",
|
|
692
|
+
description: "Wix Stores v1 catalog API",
|
|
693
|
+
kind: "headless",
|
|
694
|
+
requires: ["wix-server-client", "wix-cart"],
|
|
695
|
+
showInAddPagePicker: true
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
pluginName: "wix-data",
|
|
699
|
+
packageName: "@jay-framework/wix-data",
|
|
700
|
+
description: "Wix Data collections CMS",
|
|
701
|
+
kind: "headless",
|
|
702
|
+
requires: ["wix-server-client"],
|
|
703
|
+
showInAddPagePicker: true,
|
|
704
|
+
configFiles: ["config/.wix-data.yaml"]
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
pluginName: "wix-media",
|
|
708
|
+
packageName: "@jay-framework/wix-media",
|
|
709
|
+
description: "Wix media service (no page components)",
|
|
710
|
+
kind: "service-only",
|
|
711
|
+
requires: ["wix-server-client"],
|
|
712
|
+
showInAddPagePicker: true
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
pluginName: "ui-kit",
|
|
716
|
+
packageName: "@jay-framework/ui-kit",
|
|
717
|
+
description: "Jay UI kit headless components",
|
|
718
|
+
kind: "headless",
|
|
719
|
+
requires: [],
|
|
720
|
+
showInAddPagePicker: true
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
pluginName: "gemini-agent",
|
|
724
|
+
packageName: "@jay-framework/gemini-agent",
|
|
725
|
+
description: "Gemini AI agent plugin",
|
|
726
|
+
kind: "headless",
|
|
727
|
+
requires: [],
|
|
728
|
+
showInAddPagePicker: true,
|
|
729
|
+
configFiles: ["config/.gemini-agent.yaml"]
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
pluginName: "webmcp",
|
|
733
|
+
packageName: "@jay-framework/webmcp",
|
|
734
|
+
description: "Web MCP tooling",
|
|
735
|
+
kind: "tooling",
|
|
736
|
+
requires: [],
|
|
737
|
+
showInAddPagePicker: false
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
pluginName: "aiditor",
|
|
741
|
+
packageName: "@jay-framework/aiditor",
|
|
742
|
+
description: "AIditor self plugin",
|
|
743
|
+
kind: "tooling",
|
|
744
|
+
requires: [],
|
|
745
|
+
showInAddPagePicker: false
|
|
746
|
+
}
|
|
747
|
+
];
|
|
748
|
+
function getJayPluginCatalog() {
|
|
749
|
+
return CATALOG.map((e) => ({ ...e, requires: [...e.requires] }));
|
|
750
|
+
}
|
|
751
|
+
function getCatalogEntry(pluginName) {
|
|
752
|
+
return CATALOG.find((e) => e.pluginName === pluginName);
|
|
753
|
+
}
|
|
754
|
+
const DEFAULT_COMPONENT_KEYS = {
|
|
755
|
+
"product-page": "p",
|
|
756
|
+
"product-search": "search",
|
|
757
|
+
"cart-indicator": "cart",
|
|
758
|
+
"cart-page": "cartPage",
|
|
759
|
+
"category-list": "categories"
|
|
760
|
+
};
|
|
761
|
+
function getDefaultComponentKey(contractName) {
|
|
762
|
+
if (contractName in DEFAULT_COMPONENT_KEYS) {
|
|
763
|
+
return DEFAULT_COMPONENT_KEYS[contractName];
|
|
764
|
+
}
|
|
765
|
+
return contractName.charAt(0) || "c";
|
|
766
|
+
}
|
|
767
|
+
class AddPagePromptLoadError extends Error {
|
|
768
|
+
constructor(message) {
|
|
769
|
+
super(message);
|
|
770
|
+
this.name = "AddPagePromptLoadError";
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function resolveAddPageAgentsRoot(fromModuleUrl = import.meta.url) {
|
|
774
|
+
const thisDir = path.dirname(fileURLToPath(fromModuleUrl));
|
|
775
|
+
const candidates = [
|
|
776
|
+
path.join(thisDir, "add-page-agent"),
|
|
777
|
+
path.join(thisDir, "..", "add-page-agent"),
|
|
778
|
+
path.join(thisDir, "..", "..", "add-page-agent"),
|
|
779
|
+
path.join(thisDir, "..", "..", "..", "add-page-agent")
|
|
780
|
+
];
|
|
781
|
+
for (const root of candidates) {
|
|
782
|
+
const probe = path.join(root, "system.md");
|
|
783
|
+
if (fs$1.existsSync(probe)) {
|
|
784
|
+
return root;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
throw new AddPagePromptLoadError(
|
|
788
|
+
`add-page-agent root not found (searched from ${thisDir})`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
async function loadAddPageAgentPrompts(options) {
|
|
792
|
+
const agentsRoot = resolveAddPageAgentsRoot(import.meta.url);
|
|
793
|
+
const systemPath = path.join(agentsRoot, "system.md");
|
|
794
|
+
const skillPath = path.join(agentsRoot, "SKILL.md");
|
|
795
|
+
let system;
|
|
796
|
+
let skill;
|
|
797
|
+
try {
|
|
798
|
+
system = await readFile(systemPath, "utf-8");
|
|
799
|
+
} catch {
|
|
800
|
+
throw new AddPagePromptLoadError(`missing system prompt: ${systemPath}`);
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
skill = await readFile(skillPath, "utf-8");
|
|
804
|
+
} catch {
|
|
805
|
+
throw new AddPagePromptLoadError(`missing skill document: ${skillPath}`);
|
|
806
|
+
}
|
|
807
|
+
return { system, skill };
|
|
808
|
+
}
|
|
809
|
+
async function listDirFiles(dir, prefix) {
|
|
810
|
+
try {
|
|
811
|
+
const names = await readdir(dir);
|
|
812
|
+
return names.map((n) => `${prefix}/${n}`);
|
|
813
|
+
} catch {
|
|
814
|
+
return [];
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function buildPluginManifestPromptSection(selectedPlugins) {
|
|
818
|
+
const lines = [
|
|
819
|
+
"",
|
|
820
|
+
"## Page plugin manifest (authoritative)",
|
|
821
|
+
"",
|
|
822
|
+
"The user selected these headless components for this page. You MUST:",
|
|
823
|
+
'1. Add `<script type="application/jay-headless" plugin="..." contract="..." key="...">` for each entry',
|
|
824
|
+
"2. Bind UI using contract ViewState and refs per agent-kit designer guides",
|
|
825
|
+
"3. Run jay-stack validate",
|
|
826
|
+
"",
|
|
827
|
+
"| Plugin | Contract | Key | Notes |",
|
|
828
|
+
"| --- | --- | --- | --- |"
|
|
829
|
+
];
|
|
830
|
+
for (const plugin of selectedPlugins) {
|
|
831
|
+
for (const contract of plugin.contracts) {
|
|
832
|
+
const key = contract.componentKey ?? getDefaultComponentKey(contract.contractName);
|
|
833
|
+
lines.push(
|
|
834
|
+
`| ${plugin.packageName} | ${contract.contractName} | ${key} | |`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
lines.push(
|
|
839
|
+
"",
|
|
840
|
+
"If Wix config is incomplete, implement layout and headless imports; live store data may be unavailable until config/.wix.yaml is filled.",
|
|
841
|
+
""
|
|
842
|
+
);
|
|
843
|
+
return lines.join("\n");
|
|
844
|
+
}
|
|
845
|
+
async function buildAddPagePrompt(input) {
|
|
846
|
+
const { system, skill } = await loadAddPageAgentPrompts();
|
|
847
|
+
const contentPath = path.join(input.requestDir, "content.md");
|
|
848
|
+
const designPath = path.join(input.requestDir, "design.md");
|
|
849
|
+
const references = await listDirFiles(
|
|
850
|
+
path.join(input.requestDir, "references"),
|
|
851
|
+
"references"
|
|
852
|
+
);
|
|
853
|
+
const assets = await listDirFiles(
|
|
854
|
+
path.join(input.requestDir, "assets"),
|
|
855
|
+
"assets"
|
|
856
|
+
);
|
|
857
|
+
const designerGuide = path.join(
|
|
858
|
+
input.projectDir,
|
|
859
|
+
"agent-kit",
|
|
860
|
+
"designer",
|
|
861
|
+
"INSTRUCTIONS.md"
|
|
862
|
+
);
|
|
863
|
+
const developerGuide = path.join(
|
|
864
|
+
input.projectDir,
|
|
865
|
+
"agent-kit",
|
|
866
|
+
"developer",
|
|
867
|
+
"INSTRUCTIONS.md"
|
|
868
|
+
);
|
|
869
|
+
const retryNote = input.isRetry ? `
|
|
870
|
+
## Retry mode
|
|
871
|
+
The page at route \`${input.pageRoute}\` may already have partial files from a previous run. Read existing page files and **fix** validate errors rather than recreating from scratch.
|
|
872
|
+
` : "";
|
|
873
|
+
const manifestSection = input.selectedPlugins && input.selectedPlugins.length > 0 ? buildPluginManifestPromptSection(input.selectedPlugins) : "";
|
|
874
|
+
const userMessage = `# Add Page task
|
|
875
|
+
|
|
876
|
+
Create **one** Jay Stack page at route: \`${input.pageRoute}\` (${input.routeKind}).
|
|
877
|
+
${retryNote}
|
|
878
|
+
## Brief files (read these first)
|
|
879
|
+
|
|
880
|
+
- Content (behavior, plugins, contracts): \`${contentPath}\`
|
|
881
|
+
- Design (layout, typography, colors): \`${designPath}\`
|
|
882
|
+
|
|
883
|
+
## Reference attachments
|
|
884
|
+
|
|
885
|
+
- Inspiration only (do not copy verbatim unless brief says so): ${references.length ? references.join(", ") : "(none)"}
|
|
886
|
+
- Assets to embed (copy into project when cited in brief): ${assets.length ? assets.join(", ") : "(none)"}
|
|
887
|
+
|
|
888
|
+
Asset files live under the request folder. **Copy** cited assets from \`${input.requestDir}\` into appropriate project paths (\`public/\`, etc.) when implementing.
|
|
889
|
+
|
|
890
|
+
## Project guides
|
|
891
|
+
|
|
892
|
+
- Designer: \`${designerGuide}\`
|
|
893
|
+
- Developer: \`${developerGuide}\`
|
|
894
|
+
|
|
895
|
+
## Output
|
|
896
|
+
|
|
897
|
+
- Create page files under \`src/pages/\` matching the route per Jay directory routing conventions.
|
|
898
|
+
- Match user intent from content.md and design.md as closely as possible.
|
|
899
|
+
- Reuse existing site patterns (header, footer, styling) from other pages when design.md requests it.
|
|
900
|
+
- Summarize files created and how they cover the brief when done.
|
|
901
|
+
${manifestSection}
|
|
902
|
+
---
|
|
903
|
+
|
|
904
|
+
${skill}`;
|
|
905
|
+
return { system, userMessage };
|
|
906
|
+
}
|
|
907
|
+
async function* streamAddPageAgentQuery(input) {
|
|
908
|
+
const { system, userMessage } = await buildAddPagePrompt(input);
|
|
909
|
+
const routeKey = normalizePageRoute(input.pageRoute);
|
|
910
|
+
if (!tryBeginRouteQuery(routeKey)) {
|
|
911
|
+
yield {
|
|
912
|
+
type: "error",
|
|
913
|
+
message: "Another agent task is already running for this page. Wait for it to finish, then try again."
|
|
914
|
+
};
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const sessionPlan = await getOrCreateSessionIdForRoute(
|
|
918
|
+
input.projectDir,
|
|
919
|
+
routeKey
|
|
920
|
+
);
|
|
921
|
+
const sharedOptions = {
|
|
922
|
+
cwd: input.projectDir,
|
|
923
|
+
tools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
|
|
924
|
+
allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
|
|
925
|
+
maxTurns: 40,
|
|
926
|
+
persistSession: true,
|
|
927
|
+
systemPrompt: system
|
|
928
|
+
};
|
|
929
|
+
try {
|
|
930
|
+
async function* streamQuery(opts) {
|
|
931
|
+
for await (const message of query({
|
|
932
|
+
prompt: userMessage,
|
|
933
|
+
options: { ...sharedOptions, ...opts }
|
|
934
|
+
})) {
|
|
935
|
+
for (const chunk of transformSDKMessage(
|
|
936
|
+
message
|
|
937
|
+
)) {
|
|
938
|
+
yield chunk;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
let didStaleResumeRetry = false;
|
|
943
|
+
try {
|
|
944
|
+
if (sessionPlan.useResume) {
|
|
945
|
+
yield* streamQuery({ resume: sessionPlan.sessionId });
|
|
946
|
+
} else {
|
|
947
|
+
yield* streamQuery({
|
|
948
|
+
sessionId: sessionPlan.sessionId,
|
|
949
|
+
title: routeKey
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
} catch (err) {
|
|
953
|
+
if (sessionPlan.useResume && !didStaleResumeRetry) {
|
|
954
|
+
didStaleResumeRetry = true;
|
|
955
|
+
const fresh = await replaceRouteSessionAfterStaleResume(
|
|
956
|
+
input.projectDir,
|
|
957
|
+
routeKey
|
|
958
|
+
);
|
|
959
|
+
try {
|
|
960
|
+
yield* streamQuery({
|
|
961
|
+
sessionId: fresh.sessionId,
|
|
962
|
+
title: routeKey
|
|
963
|
+
});
|
|
964
|
+
} catch (err2) {
|
|
965
|
+
yield {
|
|
966
|
+
type: "error",
|
|
967
|
+
message: err2 instanceof Error ? err2.message : String(err2)
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
} else {
|
|
971
|
+
yield {
|
|
972
|
+
type: "error",
|
|
973
|
+
message: err instanceof Error ? err.message : String(err)
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
} finally {
|
|
978
|
+
endRouteQuery(routeKey);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const PAGE_REQUESTS_DIR_SEGMENT = ".aiditor/page-requests";
|
|
982
|
+
function getPageRequestsRoot(projectRoot) {
|
|
983
|
+
return path.join(projectRoot, PAGE_REQUESTS_DIR_SEGMENT);
|
|
984
|
+
}
|
|
985
|
+
function getPageRequestDir(projectRoot, requestId) {
|
|
986
|
+
return path.join(getPageRequestsRoot(projectRoot), requestId);
|
|
987
|
+
}
|
|
988
|
+
function generatePageRequestId() {
|
|
989
|
+
return crypto.randomUUID();
|
|
990
|
+
}
|
|
991
|
+
function sanitizeAttachmentFilename(name) {
|
|
992
|
+
const base = path.basename(name).toLowerCase();
|
|
993
|
+
const sanitized = base.replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-");
|
|
994
|
+
return sanitized || "file";
|
|
995
|
+
}
|
|
996
|
+
async function ensurePageRequestDir(projectRoot, requestId) {
|
|
997
|
+
const requestDir = getPageRequestDir(projectRoot, requestId);
|
|
998
|
+
await fs.mkdir(path.join(requestDir, "assets"), { recursive: true });
|
|
999
|
+
await fs.mkdir(path.join(requestDir, "references"), { recursive: true });
|
|
1000
|
+
return requestDir;
|
|
1001
|
+
}
|
|
1002
|
+
async function createPageRequest(projectRoot, input) {
|
|
1003
|
+
const requestId = input.requestId ?? generatePageRequestId();
|
|
1004
|
+
const requestDir = await ensurePageRequestDir(projectRoot, requestId);
|
|
1005
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1006
|
+
const request = {
|
|
1007
|
+
requestId,
|
|
1008
|
+
kind: "add_page",
|
|
1009
|
+
pageRoute: input.pageRoute,
|
|
1010
|
+
routeKind: input.routeKind,
|
|
1011
|
+
status: "draft",
|
|
1012
|
+
createdAt: now,
|
|
1013
|
+
updatedAt: now,
|
|
1014
|
+
contentMdPath: "content.md",
|
|
1015
|
+
designMdPath: "design.md",
|
|
1016
|
+
references: [],
|
|
1017
|
+
assets: []
|
|
1018
|
+
};
|
|
1019
|
+
await writePageRequest(projectRoot, request);
|
|
1020
|
+
await fs.writeFile(path.join(requestDir, "content.md"), "", "utf-8");
|
|
1021
|
+
await fs.writeFile(path.join(requestDir, "design.md"), "", "utf-8");
|
|
1022
|
+
return { requestDir, request };
|
|
1023
|
+
}
|
|
1024
|
+
async function readPageRequest(projectRoot, requestId) {
|
|
1025
|
+
const requestPath = path.join(
|
|
1026
|
+
getPageRequestDir(projectRoot, requestId),
|
|
1027
|
+
"request.json"
|
|
1028
|
+
);
|
|
1029
|
+
try {
|
|
1030
|
+
const raw = await fs.readFile(requestPath, "utf-8");
|
|
1031
|
+
return JSON.parse(raw);
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
|
|
1034
|
+
if (code === "ENOENT") return null;
|
|
1035
|
+
throw e;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
async function writePageRequestAtomic(requestDir, request) {
|
|
1039
|
+
const requestPath = path.join(requestDir, "request.json");
|
|
1040
|
+
const tmp = `${requestPath}.${process.pid}.${Date.now()}.tmp`;
|
|
1041
|
+
await fs.writeFile(tmp, `${JSON.stringify(request, null, 2)}
|
|
1042
|
+
`, "utf-8");
|
|
1043
|
+
await fs.rename(tmp, requestPath);
|
|
1044
|
+
}
|
|
1045
|
+
async function writePageRequest(projectRoot, request) {
|
|
1046
|
+
const requestDir = getPageRequestDir(projectRoot, request.requestId);
|
|
1047
|
+
await writePageRequestAtomic(requestDir, request);
|
|
1048
|
+
}
|
|
1049
|
+
async function updatePageRequestStatus(projectRoot, requestId, status) {
|
|
1050
|
+
const request = await readPageRequest(projectRoot, requestId);
|
|
1051
|
+
if (!request) {
|
|
1052
|
+
throw new Error(`Page request not found: ${requestId}`);
|
|
1053
|
+
}
|
|
1054
|
+
const updated = {
|
|
1055
|
+
...request,
|
|
1056
|
+
status,
|
|
1057
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1058
|
+
};
|
|
1059
|
+
await writePageRequest(projectRoot, updated);
|
|
1060
|
+
return updated;
|
|
1061
|
+
}
|
|
1062
|
+
async function writePageRequestMarkdown(projectRoot, requestId, input) {
|
|
1063
|
+
const requestDir = getPageRequestDir(projectRoot, requestId);
|
|
1064
|
+
await fs.writeFile(
|
|
1065
|
+
path.join(requestDir, "content.md"),
|
|
1066
|
+
input.contentMd,
|
|
1067
|
+
"utf-8"
|
|
1068
|
+
);
|
|
1069
|
+
await fs.writeFile(
|
|
1070
|
+
path.join(requestDir, "design.md"),
|
|
1071
|
+
input.designMd,
|
|
1072
|
+
"utf-8"
|
|
1073
|
+
);
|
|
1074
|
+
const request = await readPageRequest(projectRoot, requestId);
|
|
1075
|
+
if (request) {
|
|
1076
|
+
await writePageRequest(projectRoot, {
|
|
1077
|
+
...request,
|
|
1078
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
async function addPageRequestAttachment(projectRoot, requestId, entry, role) {
|
|
1083
|
+
const request = await readPageRequest(projectRoot, requestId);
|
|
1084
|
+
if (!request) {
|
|
1085
|
+
throw new Error(`Page request not found: ${requestId}`);
|
|
1086
|
+
}
|
|
1087
|
+
const updated = {
|
|
1088
|
+
...request,
|
|
1089
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1090
|
+
references: role === "reference" ? [
|
|
1091
|
+
...request.references.filter((r) => r.id !== entry.id),
|
|
1092
|
+
entry
|
|
1093
|
+
] : request.references,
|
|
1094
|
+
assets: role === "asset" ? [
|
|
1095
|
+
...request.assets.filter((a) => a.id !== entry.id),
|
|
1096
|
+
entry
|
|
1097
|
+
] : request.assets
|
|
1098
|
+
};
|
|
1099
|
+
await writePageRequest(projectRoot, updated);
|
|
1100
|
+
return updated;
|
|
1101
|
+
}
|
|
1102
|
+
async function reclassifyPageRequestAttachment(projectRoot, requestId, attachmentId, newRole) {
|
|
1103
|
+
const request = await readPageRequest(projectRoot, requestId);
|
|
1104
|
+
if (!request) {
|
|
1105
|
+
throw new Error(`Page request not found: ${requestId}`);
|
|
1106
|
+
}
|
|
1107
|
+
const requestDir = getPageRequestDir(projectRoot, requestId);
|
|
1108
|
+
const refIx = request.references.findIndex((r) => r.id === attachmentId);
|
|
1109
|
+
const assetIx = request.assets.findIndex((a) => a.id === attachmentId);
|
|
1110
|
+
let relPath;
|
|
1111
|
+
let label;
|
|
1112
|
+
if (refIx >= 0) {
|
|
1113
|
+
relPath = request.references[refIx].path;
|
|
1114
|
+
label = request.references[refIx].label;
|
|
1115
|
+
} else if (assetIx >= 0) {
|
|
1116
|
+
relPath = request.assets[assetIx].path;
|
|
1117
|
+
} else {
|
|
1118
|
+
throw new Error(`Attachment not found: ${attachmentId}`);
|
|
1119
|
+
}
|
|
1120
|
+
const filename = path.basename(relPath);
|
|
1121
|
+
const destSubdir = newRole === "asset" ? "assets" : "references";
|
|
1122
|
+
const srcAbs = path.join(requestDir, relPath);
|
|
1123
|
+
const destRel = `${destSubdir}/${filename}`;
|
|
1124
|
+
const destAbs = path.join(requestDir, destRel);
|
|
1125
|
+
await fs.rename(srcAbs, destAbs);
|
|
1126
|
+
const references = request.references.filter((r) => r.id !== attachmentId);
|
|
1127
|
+
const assets = request.assets.filter((a) => a.id !== attachmentId);
|
|
1128
|
+
if (newRole === "reference") {
|
|
1129
|
+
references.push({ id: attachmentId, path: destRel, label });
|
|
1130
|
+
} else {
|
|
1131
|
+
assets.push({
|
|
1132
|
+
id: attachmentId,
|
|
1133
|
+
path: destRel,
|
|
1134
|
+
markdownRef: destRel
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
const updated = {
|
|
1138
|
+
...request,
|
|
1139
|
+
references,
|
|
1140
|
+
assets,
|
|
1141
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1142
|
+
};
|
|
1143
|
+
await writePageRequest(projectRoot, updated);
|
|
1144
|
+
return updated;
|
|
1145
|
+
}
|
|
1146
|
+
function inferRouteKind(route) {
|
|
1147
|
+
return /\[[^\]/]+\]/.test(route) ? "dynamic" : "static";
|
|
1148
|
+
}
|
|
1149
|
+
function validatePageRouteSyntax(route) {
|
|
1150
|
+
const trimmed = route.trim();
|
|
1151
|
+
if (!trimmed) {
|
|
1152
|
+
return { valid: false, error: "Route is required." };
|
|
1153
|
+
}
|
|
1154
|
+
if (!trimmed.startsWith("/")) {
|
|
1155
|
+
return { valid: false, error: "Route must start with /." };
|
|
1156
|
+
}
|
|
1157
|
+
const normalized = normalizePageRoute(trimmed);
|
|
1158
|
+
if (normalized.includes("//")) {
|
|
1159
|
+
return { valid: false, error: "Route contains invalid segments." };
|
|
1160
|
+
}
|
|
1161
|
+
const dynamicSegments = normalized.match(/\[([^\]]*)\]/g) ?? [];
|
|
1162
|
+
for (const seg of dynamicSegments) {
|
|
1163
|
+
const inner = seg.slice(1, -1).trim();
|
|
1164
|
+
if (!inner) {
|
|
1165
|
+
return {
|
|
1166
|
+
valid: false,
|
|
1167
|
+
error: "Dynamic segments must have a param name, e.g. [slug]."
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(inner)) {
|
|
1171
|
+
return {
|
|
1172
|
+
valid: false,
|
|
1173
|
+
error: `Invalid dynamic segment ${seg}. Use [paramName] format.`
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return { valid: true };
|
|
1178
|
+
}
|
|
1179
|
+
function routeExistsInProject(normalizedRoute, projectRoutes) {
|
|
1180
|
+
return projectRoutes.some(
|
|
1181
|
+
(r) => normalizePageRoute(r.path) === normalizedRoute
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
async function runAddPageValidate(projectDir) {
|
|
1185
|
+
return new Promise((resolve) => {
|
|
1186
|
+
const child = spawn("npx", ["jay-stack-cli", "validate"], {
|
|
1187
|
+
cwd: projectDir,
|
|
1188
|
+
shell: true,
|
|
1189
|
+
env: process.env
|
|
1190
|
+
});
|
|
1191
|
+
let stdout = "";
|
|
1192
|
+
let stderr = "";
|
|
1193
|
+
child.stdout.on("data", (chunk) => {
|
|
1194
|
+
stdout += chunk.toString();
|
|
1195
|
+
});
|
|
1196
|
+
child.stderr.on("data", (chunk) => {
|
|
1197
|
+
stderr += chunk.toString();
|
|
1198
|
+
});
|
|
1199
|
+
child.on("close", (code) => {
|
|
1200
|
+
const combined = `${stdout}
|
|
1201
|
+
${stderr}`.trim();
|
|
1202
|
+
if (code === 0) {
|
|
1203
|
+
resolve({ ok: true, errors: [], stdout, stderr });
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const errors = combined.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1207
|
+
resolve({
|
|
1208
|
+
ok: false,
|
|
1209
|
+
errors: errors.length > 0 ? errors : [`validate exited with code ${code}`],
|
|
1210
|
+
stdout,
|
|
1211
|
+
stderr
|
|
1212
|
+
});
|
|
1213
|
+
});
|
|
1214
|
+
child.on("error", (err) => {
|
|
1215
|
+
resolve({
|
|
1216
|
+
ok: false,
|
|
1217
|
+
errors: [err.message],
|
|
1218
|
+
stdout,
|
|
1219
|
+
stderr
|
|
1220
|
+
});
|
|
1221
|
+
});
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
const PAGES_DIR_CANDIDATES = ["src/pages", "pages"];
|
|
1225
|
+
const PAGE_BRIEF_DIR_NAME = "add-page-brief";
|
|
1226
|
+
function resolveJayHtmlPath(projectDir, jayHtmlPath) {
|
|
1227
|
+
return path.isAbsolute(jayHtmlPath) ? jayHtmlPath : path.join(projectDir, jayHtmlPath);
|
|
1228
|
+
}
|
|
1229
|
+
function getPageBriefDirForJayHtml(jayHtmlPath) {
|
|
1230
|
+
return path.join(path.dirname(jayHtmlPath), PAGE_BRIEF_DIR_NAME);
|
|
1231
|
+
}
|
|
1232
|
+
function getPageBriefContentPath(briefDir) {
|
|
1233
|
+
return path.join(briefDir, "content.md");
|
|
1234
|
+
}
|
|
1235
|
+
function getPageBriefDesignPath(briefDir) {
|
|
1236
|
+
return path.join(briefDir, "design.md");
|
|
1237
|
+
}
|
|
1238
|
+
async function resolveJayHtmlPathForPageRoute(projectDir, normalizedRoute, cachedRoutes) {
|
|
1239
|
+
if (cachedRoutes) {
|
|
1240
|
+
const fromCache = cachedRoutes.find(
|
|
1241
|
+
(r) => normalizePageRoute(r.path) === normalizedRoute
|
|
1242
|
+
);
|
|
1243
|
+
if (fromCache?.jayHtmlPath) return fromCache.jayHtmlPath;
|
|
1244
|
+
}
|
|
1245
|
+
for (const rel of PAGES_DIR_CANDIDATES) {
|
|
1246
|
+
const pagesDir = path.join(projectDir, rel);
|
|
1247
|
+
try {
|
|
1248
|
+
await fs.access(pagesDir);
|
|
1249
|
+
} catch {
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
const routes = await scanRoutes(pagesDir, {
|
|
1253
|
+
jayHtmlFilename: "page.jay-html",
|
|
1254
|
+
compFilename: "page.ts"
|
|
1255
|
+
});
|
|
1256
|
+
const match = routes.find(
|
|
1257
|
+
(r) => normalizePageRoute(r.rawRoute) === normalizedRoute
|
|
1258
|
+
);
|
|
1259
|
+
if (match?.jayHtmlPath) return match.jayHtmlPath;
|
|
1260
|
+
}
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
async function tryBackfillPageBriefForRoute(projectDir, jayHtmlPath, pageRoute) {
|
|
1264
|
+
if (await pageBriefExists(projectDir, jayHtmlPath)) {
|
|
1265
|
+
return true;
|
|
1266
|
+
}
|
|
1267
|
+
const normalized = normalizePageRoute(pageRoute);
|
|
1268
|
+
const requestsRoot = getPageRequestsRoot(projectDir);
|
|
1269
|
+
let requestDirs = [];
|
|
1270
|
+
try {
|
|
1271
|
+
requestDirs = await fs.readdir(requestsRoot);
|
|
1272
|
+
} catch {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
let best = null;
|
|
1276
|
+
for (const dir of requestDirs) {
|
|
1277
|
+
const request = await readPageRequest(projectDir, dir);
|
|
1278
|
+
if (!request || request.status !== "completed" || normalizePageRoute(request.pageRoute) !== normalized) {
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
if (!best || new Date(request.updatedAt).getTime() > new Date(best.updatedAt).getTime()) {
|
|
1282
|
+
best = request;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (!best) return false;
|
|
1286
|
+
const requestDir = getPageRequestDir(projectDir, best.requestId);
|
|
1287
|
+
let contentMd = "";
|
|
1288
|
+
let designMd = "";
|
|
1289
|
+
try {
|
|
1290
|
+
contentMd = await fs.readFile(path.join(requestDir, "content.md"), "utf-8");
|
|
1291
|
+
designMd = await fs.readFile(path.join(requestDir, "design.md"), "utf-8");
|
|
1292
|
+
} catch {
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
await savePageBriefFromRequest(projectDir, jayHtmlPath, {
|
|
1296
|
+
requestId: best.requestId,
|
|
1297
|
+
pageRoute: normalized,
|
|
1298
|
+
contentMd,
|
|
1299
|
+
designMd
|
|
1300
|
+
});
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
async function pageBriefExists(projectDir, jayHtmlPath) {
|
|
1304
|
+
const absJayHtml = resolveJayHtmlPath(projectDir, jayHtmlPath);
|
|
1305
|
+
const briefDir = getPageBriefDirForJayHtml(absJayHtml);
|
|
1306
|
+
try {
|
|
1307
|
+
await fs.access(getPageBriefContentPath(briefDir));
|
|
1308
|
+
return true;
|
|
1309
|
+
} catch {
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
async function copyDirFiles(srcDir, destDir) {
|
|
1314
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
1315
|
+
let entries = [];
|
|
1316
|
+
try {
|
|
1317
|
+
entries = await fs.readdir(srcDir);
|
|
1318
|
+
} catch (e) {
|
|
1319
|
+
const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
|
|
1320
|
+
if (code === "ENOENT") return;
|
|
1321
|
+
throw e;
|
|
1322
|
+
}
|
|
1323
|
+
for (const name of entries) {
|
|
1324
|
+
const src = path.join(srcDir, name);
|
|
1325
|
+
const dest = path.join(destDir, name);
|
|
1326
|
+
const stat = await fs.stat(src);
|
|
1327
|
+
if (stat.isFile()) {
|
|
1328
|
+
await fs.copyFile(src, dest);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async function savePageBriefFromRequest(projectDir, jayHtmlPath, input) {
|
|
1333
|
+
const absJayHtml = resolveJayHtmlPath(projectDir, jayHtmlPath);
|
|
1334
|
+
const briefDir = getPageBriefDirForJayHtml(absJayHtml);
|
|
1335
|
+
const requestDir = getPageRequestDir(projectDir, input.requestId);
|
|
1336
|
+
await fs.mkdir(path.join(briefDir, "assets"), { recursive: true });
|
|
1337
|
+
await fs.mkdir(path.join(briefDir, "references"), { recursive: true });
|
|
1338
|
+
await fs.writeFile(
|
|
1339
|
+
getPageBriefContentPath(briefDir),
|
|
1340
|
+
input.contentMd,
|
|
1341
|
+
"utf-8"
|
|
1342
|
+
);
|
|
1343
|
+
await fs.writeFile(getPageBriefDesignPath(briefDir), input.designMd, "utf-8");
|
|
1344
|
+
await copyDirFiles(
|
|
1345
|
+
path.join(requestDir, "assets"),
|
|
1346
|
+
path.join(briefDir, "assets")
|
|
1347
|
+
);
|
|
1348
|
+
await copyDirFiles(
|
|
1349
|
+
path.join(requestDir, "references"),
|
|
1350
|
+
path.join(briefDir, "references")
|
|
1351
|
+
);
|
|
1352
|
+
const manifest = {
|
|
1353
|
+
sourceRoute: normalizePageRoute(input.pageRoute),
|
|
1354
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1355
|
+
requestId: input.requestId
|
|
1356
|
+
};
|
|
1357
|
+
await fs.writeFile(
|
|
1358
|
+
path.join(briefDir, "brief.json"),
|
|
1359
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
1360
|
+
`,
|
|
1361
|
+
"utf-8"
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
async function readOptionalJson(filePath) {
|
|
1365
|
+
try {
|
|
1366
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
1367
|
+
return JSON.parse(raw);
|
|
1368
|
+
} catch {
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
async function listBriefAttachments(briefDir) {
|
|
1373
|
+
const attachments = [];
|
|
1374
|
+
for (const role of ["asset", "reference"]) {
|
|
1375
|
+
const subdir = role === "asset" ? "assets" : "references";
|
|
1376
|
+
const dirPath = path.join(briefDir, subdir);
|
|
1377
|
+
let names = [];
|
|
1378
|
+
try {
|
|
1379
|
+
names = await fs.readdir(dirPath);
|
|
1380
|
+
} catch {
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
for (const name of names) {
|
|
1384
|
+
const abs = path.join(dirPath, name);
|
|
1385
|
+
const stat = await fs.stat(abs);
|
|
1386
|
+
if (!stat.isFile()) continue;
|
|
1387
|
+
attachments.push({
|
|
1388
|
+
id: crypto.randomUUID(),
|
|
1389
|
+
path: `${subdir}/${name}`,
|
|
1390
|
+
role,
|
|
1391
|
+
filename: name
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return attachments;
|
|
1396
|
+
}
|
|
1397
|
+
async function readPageBrief(projectDir, jayHtmlPath) {
|
|
1398
|
+
const absJayHtml = resolveJayHtmlPath(projectDir, jayHtmlPath);
|
|
1399
|
+
const briefDir = getPageBriefDirForJayHtml(absJayHtml);
|
|
1400
|
+
const contentPath = getPageBriefContentPath(briefDir);
|
|
1401
|
+
try {
|
|
1402
|
+
await fs.access(contentPath);
|
|
1403
|
+
} catch {
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
const contentMd = await fs.readFile(contentPath, "utf-8");
|
|
1407
|
+
let designMd = "";
|
|
1408
|
+
try {
|
|
1409
|
+
designMd = await fs.readFile(getPageBriefDesignPath(briefDir), "utf-8");
|
|
1410
|
+
} catch {
|
|
1411
|
+
}
|
|
1412
|
+
const manifest = await readOptionalJson(
|
|
1413
|
+
path.join(briefDir, "brief.json")
|
|
1414
|
+
) ?? {
|
|
1415
|
+
sourceRoute: "",
|
|
1416
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1417
|
+
requestId: ""
|
|
1418
|
+
};
|
|
1419
|
+
const attachments = await listBriefAttachments(briefDir);
|
|
1420
|
+
return { manifest, contentMd, designMd, attachments };
|
|
1421
|
+
}
|
|
1422
|
+
function isDynamicSegment(segment) {
|
|
1423
|
+
return segment.includes("[");
|
|
1424
|
+
}
|
|
1425
|
+
function suggestCopyRoute(sourceRoute, existingRoutes) {
|
|
1426
|
+
const normalized = normalizePageRoute(sourceRoute);
|
|
1427
|
+
const existing = new Set(
|
|
1428
|
+
existingRoutes.map((r) => normalizePageRoute(r.path))
|
|
1429
|
+
);
|
|
1430
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
1431
|
+
let lastStaticIdx = -1;
|
|
1432
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
1433
|
+
if (!isDynamicSegment(segments[i])) {
|
|
1434
|
+
lastStaticIdx = i;
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
const buildRoute = (suffix) => {
|
|
1439
|
+
if (lastStaticIdx < 0) {
|
|
1440
|
+
return normalizePageRoute(`${normalized}${suffix}`);
|
|
1441
|
+
}
|
|
1442
|
+
const next = [...segments];
|
|
1443
|
+
next[lastStaticIdx] = `${next[lastStaticIdx]}${suffix}`;
|
|
1444
|
+
return normalizePageRoute(`/${next.join("/")}`);
|
|
1445
|
+
};
|
|
1446
|
+
let candidate = buildRoute("-copy");
|
|
1447
|
+
if (!existing.has(candidate)) return candidate;
|
|
1448
|
+
for (let n = 2; n < 100; n++) {
|
|
1449
|
+
candidate = buildRoute(`-copy-${n}`);
|
|
1450
|
+
if (!existing.has(candidate)) return candidate;
|
|
1451
|
+
}
|
|
1452
|
+
return buildRoute(`-copy-${Date.now()}`);
|
|
1453
|
+
}
|
|
1454
|
+
async function clonePageBriefToRequest(projectDir, jayHtmlPath, input) {
|
|
1455
|
+
const brief = await readPageBrief(projectDir, jayHtmlPath);
|
|
1456
|
+
if (!brief) {
|
|
1457
|
+
throw new Error("No Add Page brief found for this page.");
|
|
1458
|
+
}
|
|
1459
|
+
const pageRoute = normalizePageRoute(input.pageRoute);
|
|
1460
|
+
const routeKind = input.routeKind ?? inferRouteKind(pageRoute);
|
|
1461
|
+
const { request } = await createPageRequest(projectDir, {
|
|
1462
|
+
pageRoute,
|
|
1463
|
+
routeKind
|
|
1464
|
+
});
|
|
1465
|
+
const absJayHtml = resolveJayHtmlPath(projectDir, jayHtmlPath);
|
|
1466
|
+
const briefDir = getPageBriefDirForJayHtml(absJayHtml);
|
|
1467
|
+
const requestDir = getPageRequestDir(projectDir, request.requestId);
|
|
1468
|
+
await fs.writeFile(
|
|
1469
|
+
path.join(requestDir, "content.md"),
|
|
1470
|
+
brief.contentMd,
|
|
1471
|
+
"utf-8"
|
|
1472
|
+
);
|
|
1473
|
+
await fs.writeFile(
|
|
1474
|
+
path.join(requestDir, "design.md"),
|
|
1475
|
+
brief.designMd,
|
|
1476
|
+
"utf-8"
|
|
1477
|
+
);
|
|
1478
|
+
const attachments = [];
|
|
1479
|
+
for (const attachment of brief.attachments) {
|
|
1480
|
+
const src = path.join(briefDir, attachment.path);
|
|
1481
|
+
const dest = path.join(requestDir, attachment.path);
|
|
1482
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
1483
|
+
await fs.copyFile(src, dest);
|
|
1484
|
+
const id = generatePageRequestId();
|
|
1485
|
+
if (attachment.role === "reference") {
|
|
1486
|
+
const entry = {
|
|
1487
|
+
id,
|
|
1488
|
+
path: attachment.path,
|
|
1489
|
+
label: attachment.filename
|
|
1490
|
+
};
|
|
1491
|
+
await addPageRequestAttachment(
|
|
1492
|
+
projectDir,
|
|
1493
|
+
request.requestId,
|
|
1494
|
+
entry,
|
|
1495
|
+
"reference"
|
|
1496
|
+
);
|
|
1497
|
+
attachments.push({ ...attachment, id });
|
|
1498
|
+
} else {
|
|
1499
|
+
const entry = {
|
|
1500
|
+
id,
|
|
1501
|
+
path: attachment.path,
|
|
1502
|
+
markdownRef: attachment.path
|
|
1503
|
+
};
|
|
1504
|
+
await addPageRequestAttachment(
|
|
1505
|
+
projectDir,
|
|
1506
|
+
request.requestId,
|
|
1507
|
+
entry,
|
|
1508
|
+
"asset"
|
|
1509
|
+
);
|
|
1510
|
+
attachments.push({ ...attachment, id });
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return {
|
|
1514
|
+
requestId: request.requestId,
|
|
1515
|
+
request,
|
|
1516
|
+
contentMd: brief.contentMd,
|
|
1517
|
+
designMd: brief.designMd,
|
|
1518
|
+
attachments
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
const BRIEF_FILL_MAX_IMAGES = 5;
|
|
1522
|
+
const BRIEF_FILL_MAX_TURNS = 3;
|
|
1523
|
+
const BRIEF_FILL_PROMPT_FILES = {
|
|
1524
|
+
brief: "add-page-figma-brief-prompt.md",
|
|
1525
|
+
designFromImage: "add-page-figma-design-from-image-prompt.md",
|
|
1526
|
+
designDefinitions: "add-page-figma-design-definitions-prompt.md"
|
|
1527
|
+
};
|
|
1528
|
+
function extractCopyPastePromptBody(markdown) {
|
|
1529
|
+
const parts = markdown.split(/\r?\n---\r?\n/);
|
|
1530
|
+
if (parts.length < 2) return markdown.trim();
|
|
1531
|
+
return parts[1].trim();
|
|
1532
|
+
}
|
|
1533
|
+
function moduleDirectory() {
|
|
1534
|
+
return path.dirname(fileURLToPath(import.meta.url));
|
|
1535
|
+
}
|
|
1536
|
+
function resolveBriefFillPromptsDir() {
|
|
1537
|
+
const candidates = [
|
|
1538
|
+
path.join(moduleDirectory(), "prompts"),
|
|
1539
|
+
path.join(moduleDirectory(), "../../../../../docs")
|
|
1540
|
+
];
|
|
1541
|
+
for (const dir of candidates) {
|
|
1542
|
+
const briefPath = path.join(dir, BRIEF_FILL_PROMPT_FILES.brief);
|
|
1543
|
+
if (fs$1.existsSync(briefPath)) return dir;
|
|
1544
|
+
}
|
|
1545
|
+
throw new Error(
|
|
1546
|
+
"Brief fill prompt files not found. Run `yarn build` in @jay-framework/aiditor or ensure docs/add-page-figma-*.md exist."
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
function readPromptFile(filename, promptsDir) {
|
|
1550
|
+
const dir = resolveBriefFillPromptsDir();
|
|
1551
|
+
const filePath = path.join(dir, filename);
|
|
1552
|
+
const raw = fs$1.readFileSync(filePath, "utf-8");
|
|
1553
|
+
return extractCopyPastePromptBody(raw);
|
|
1554
|
+
}
|
|
1555
|
+
const CONTENT_FILL_SUFFIX = `
|
|
1556
|
+
|
|
1557
|
+
---
|
|
1558
|
+
|
|
1559
|
+
## AIditor — Fill from image (Content field)
|
|
1560
|
+
|
|
1561
|
+
You are invoked from AIditor **Fill from image** on the **Content** composer field.
|
|
1562
|
+
|
|
1563
|
+
Follow the prompt above. Produce **only**:
|
|
1564
|
+
|
|
1565
|
+
- **Section 1** — Suggested page route (when inferable from the screenshot or context notes)
|
|
1566
|
+
- **Section 2** — Content brief (use the full structure specified above)
|
|
1567
|
+
|
|
1568
|
+
Do **not** output Section 3 (Design brief), Section 4 (Checklist), or any Design markdown. The user fills Design in a separate step.`;
|
|
1569
|
+
const DESIGN_FILL_SUFFIX = `
|
|
1570
|
+
|
|
1571
|
+
---
|
|
1572
|
+
|
|
1573
|
+
## AIditor — Fill from image (Design field)
|
|
1574
|
+
|
|
1575
|
+
You are invoked from AIditor **Fill from image** on the **Design** composer field.
|
|
1576
|
+
|
|
1577
|
+
Follow the prompt above. Produce **only** the Design markdown document (one block for AIditor → Design).
|
|
1578
|
+
|
|
1579
|
+
Do **not** output a Content brief, suggested route, or plugin behavior sections.`;
|
|
1580
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1581
|
+
function loadCached(key, loader) {
|
|
1582
|
+
const hit = cache.get(key);
|
|
1583
|
+
if (hit !== void 0) return hit;
|
|
1584
|
+
const value = loader();
|
|
1585
|
+
cache.set(key, value);
|
|
1586
|
+
return value;
|
|
1587
|
+
}
|
|
1588
|
+
function loadBriefFillSystemPrompt(target, promptsDir) {
|
|
1589
|
+
const cacheKey = `${target}:${"default"}`;
|
|
1590
|
+
return loadCached(cacheKey, () => {
|
|
1591
|
+
if (target === "content") {
|
|
1592
|
+
const briefBody = readPromptFile(
|
|
1593
|
+
BRIEF_FILL_PROMPT_FILES.brief
|
|
1594
|
+
);
|
|
1595
|
+
return `${briefBody}${CONTENT_FILL_SUFFIX}`;
|
|
1596
|
+
}
|
|
1597
|
+
const designFromImageBody = readPromptFile(
|
|
1598
|
+
BRIEF_FILL_PROMPT_FILES.designFromImage
|
|
1599
|
+
);
|
|
1600
|
+
const designDefinitionsBody = readPromptFile(
|
|
1601
|
+
BRIEF_FILL_PROMPT_FILES.designDefinitions
|
|
1602
|
+
);
|
|
1603
|
+
return `${designFromImageBody}${DESIGN_FILL_SUFFIX}
|
|
1604
|
+
|
|
1605
|
+
---
|
|
1606
|
+
|
|
1607
|
+
## Reference — design definitions board (Mode A)
|
|
1608
|
+
|
|
1609
|
+
When the image is a formal Figma **design definitions** board, apply these rules in full (in addition to Mode A above):
|
|
1610
|
+
|
|
1611
|
+
${designDefinitionsBody}`;
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
const BRIEF_FILL_MODEL = process.env.AIDITOR_BRIEF_FILL_MODEL ?? "claude-sonnet-4-20250514";
|
|
1615
|
+
const ALLOWED_IMAGE_MIMES = /* @__PURE__ */ new Set([
|
|
1616
|
+
"image/png",
|
|
1617
|
+
"image/jpeg",
|
|
1618
|
+
"image/jpg",
|
|
1619
|
+
"image/webp",
|
|
1620
|
+
"image/gif"
|
|
1621
|
+
]);
|
|
1622
|
+
function stripMarkdownFences(text) {
|
|
1623
|
+
const t = text.trim();
|
|
1624
|
+
const fenceMatch = /^```(?:markdown|md)?\s*\n([\s\S]*?)\n```\s*$/i.exec(t);
|
|
1625
|
+
if (fenceMatch) return fenceMatch[1].trim();
|
|
1626
|
+
return t;
|
|
1627
|
+
}
|
|
1628
|
+
const SUGGESTED_ROUTE_RE = /(?:Suggested route|Route)[^:\n]*:+\s*(?:\*\*\s*)?(?:`(\/[^`]+)`|(\/[^\s(]+(?:\[[^\]]+\])?))\s*(?:\((static|dynamic)\))?/i;
|
|
1629
|
+
function parseSuggestedRoute(raw) {
|
|
1630
|
+
const stripped = stripMarkdownFences(raw);
|
|
1631
|
+
const lines = stripped.split("\n");
|
|
1632
|
+
let suggestedRoute;
|
|
1633
|
+
let suggestedRouteKind;
|
|
1634
|
+
const bodyLines = [];
|
|
1635
|
+
for (const line of lines) {
|
|
1636
|
+
const m = SUGGESTED_ROUTE_RE.exec(line);
|
|
1637
|
+
if (m && !suggestedRoute) {
|
|
1638
|
+
suggestedRoute = (m[1] ?? m[2] ?? "").replace(/\/+$/, "") || "/";
|
|
1639
|
+
if (m[3] === "static" || m[3] === "dynamic") {
|
|
1640
|
+
suggestedRouteKind = m[3];
|
|
1641
|
+
}
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
bodyLines.push(line);
|
|
1645
|
+
}
|
|
1646
|
+
let markdown = bodyLines.join("\n").trim();
|
|
1647
|
+
markdown = markdown.replace(/^---\s*\n+/, "").trim();
|
|
1648
|
+
return { markdown, suggestedRoute, suggestedRouteKind };
|
|
1649
|
+
}
|
|
1650
|
+
function extractImageFilesFromExtraFiles(extraFiles) {
|
|
1651
|
+
if (!extraFiles) return [];
|
|
1652
|
+
const indexed = [];
|
|
1653
|
+
for (const [key, file] of Object.entries(extraFiles)) {
|
|
1654
|
+
const m = /^image_(\d+)$/.exec(key);
|
|
1655
|
+
if (!m || !file) continue;
|
|
1656
|
+
indexed.push({ index: Number.parseInt(m[1], 10), file });
|
|
1657
|
+
}
|
|
1658
|
+
indexed.sort((a, b) => a.index - b.index);
|
|
1659
|
+
return indexed.map((e) => e.file);
|
|
1660
|
+
}
|
|
1661
|
+
function normalizeImageMime(mime, filename) {
|
|
1662
|
+
const t = mime.toLowerCase().split(";")[0].trim();
|
|
1663
|
+
if (ALLOWED_IMAGE_MIMES.has(t)) {
|
|
1664
|
+
return t === "image/jpg" ? "image/jpeg" : t;
|
|
1665
|
+
}
|
|
1666
|
+
const ext = filename.toLowerCase().split(".").pop() ?? "";
|
|
1667
|
+
if (ext === "png") return "image/png";
|
|
1668
|
+
if (ext === "jpg" || ext === "jpeg") return "image/jpeg";
|
|
1669
|
+
if (ext === "webp") return "image/webp";
|
|
1670
|
+
if (ext === "gif") return "image/gif";
|
|
1671
|
+
return t;
|
|
1672
|
+
}
|
|
1673
|
+
function validateBriefFillImages(files) {
|
|
1674
|
+
if (files.length === 0) {
|
|
1675
|
+
return "Add at least one image before generating.";
|
|
1676
|
+
}
|
|
1677
|
+
if (files.length > BRIEF_FILL_MAX_IMAGES) {
|
|
1678
|
+
return `Maximum ${BRIEF_FILL_MAX_IMAGES} images per request.`;
|
|
1679
|
+
}
|
|
1680
|
+
for (const f of files) {
|
|
1681
|
+
const mime = normalizeImageMime(f.type ?? "", f.name);
|
|
1682
|
+
if (!ALLOWED_IMAGE_MIMES.has(mime) && !mime.startsWith("image/")) {
|
|
1683
|
+
return `Unsupported file type: ${f.name}. Use PNG, JPEG, WebP, or GIF.`;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
function readImageAsBase64(file) {
|
|
1689
|
+
const buf = fs$1.readFileSync(file.path);
|
|
1690
|
+
const mimeType = normalizeImageMime(file.type ?? "", file.name);
|
|
1691
|
+
return {
|
|
1692
|
+
name: file.name,
|
|
1693
|
+
mimeType,
|
|
1694
|
+
base64: buf.toString("base64")
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
function buildBriefFillUserMessage(target, images, contextNotes, pageRoute) {
|
|
1698
|
+
const parts = [];
|
|
1699
|
+
parts.push(
|
|
1700
|
+
target === "content" ? "Generate the Content brief markdown for AIditor Add Page from the attached screenshot(s)." : "Generate the Design brief markdown for AIditor Add Page from the attached screenshot(s)."
|
|
1701
|
+
);
|
|
1702
|
+
if (pageRoute?.trim()) {
|
|
1703
|
+
parts.push(`User already typed page route: ${pageRoute.trim()}`);
|
|
1704
|
+
}
|
|
1705
|
+
if (contextNotes?.trim()) {
|
|
1706
|
+
parts.push("", "User context notes:", contextNotes.trim());
|
|
1707
|
+
}
|
|
1708
|
+
parts.push("", `Attached images: ${images.length}`);
|
|
1709
|
+
for (let i = 0; i < images.length; i++) {
|
|
1710
|
+
parts.push(`- Image ${i + 1}: ${images[i].name}`);
|
|
1711
|
+
}
|
|
1712
|
+
return parts.join("\n");
|
|
1713
|
+
}
|
|
1714
|
+
function buildBriefFillMessageContent(images, userText) {
|
|
1715
|
+
return [
|
|
1716
|
+
...images.flatMap((img, i) => [
|
|
1717
|
+
{ type: "text", text: `[Image ${i + 1}: ${img.name}]` },
|
|
1718
|
+
{
|
|
1719
|
+
type: "image",
|
|
1720
|
+
source: {
|
|
1721
|
+
type: "base64",
|
|
1722
|
+
media_type: img.mimeType,
|
|
1723
|
+
data: img.base64
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
]),
|
|
1727
|
+
{ type: "text", text: userText }
|
|
1728
|
+
];
|
|
1729
|
+
}
|
|
1730
|
+
async function* briefFillPromptStream(userText, images) {
|
|
1731
|
+
yield {
|
|
1732
|
+
type: "user",
|
|
1733
|
+
message: {
|
|
1734
|
+
role: "user",
|
|
1735
|
+
content: buildBriefFillMessageContent(images, userText)
|
|
1736
|
+
},
|
|
1737
|
+
parent_tool_use_id: null
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
function extractBriefFillRawText(messages) {
|
|
1741
|
+
let rawText = "";
|
|
1742
|
+
for (const message of messages) {
|
|
1743
|
+
if (message.type === "assistant" && Array.isArray(message.message?.content)) {
|
|
1744
|
+
for (const block of message.message.content) {
|
|
1745
|
+
if (block.type === "text" && block.text?.trim()) {
|
|
1746
|
+
rawText = block.text.trim();
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
if (message.type === "result" && message.subtype === "success" && message.result?.trim() && !rawText) {
|
|
1751
|
+
rawText = message.result.trim();
|
|
1752
|
+
}
|
|
1753
|
+
if (message.type === "result" && message.is_error) {
|
|
1754
|
+
const detail = message.errors?.join("; ") || "Brief generation failed. Try again.";
|
|
1755
|
+
throw new Error(detail);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
return rawText;
|
|
1759
|
+
}
|
|
1760
|
+
async function runBriefFillAgentQuery(input) {
|
|
1761
|
+
const systemPrompt = loadBriefFillSystemPrompt(input.target);
|
|
1762
|
+
const userText = buildBriefFillUserMessage(
|
|
1763
|
+
input.target,
|
|
1764
|
+
input.images,
|
|
1765
|
+
input.contextNotes,
|
|
1766
|
+
input.pageRoute
|
|
1767
|
+
);
|
|
1768
|
+
const collected = [];
|
|
1769
|
+
for await (const message of query({
|
|
1770
|
+
prompt: briefFillPromptStream(userText, input.images),
|
|
1771
|
+
options: {
|
|
1772
|
+
cwd: input.projectDir,
|
|
1773
|
+
tools: [],
|
|
1774
|
+
maxTurns: BRIEF_FILL_MAX_TURNS,
|
|
1775
|
+
persistSession: false,
|
|
1776
|
+
systemPrompt,
|
|
1777
|
+
model: BRIEF_FILL_MODEL,
|
|
1778
|
+
thinking: { type: "disabled" }
|
|
1779
|
+
}
|
|
1780
|
+
})) {
|
|
1781
|
+
collected.push(message);
|
|
1782
|
+
}
|
|
1783
|
+
const rawText = extractBriefFillRawText(collected);
|
|
1784
|
+
if (!rawText) {
|
|
1785
|
+
throw new Error(
|
|
1786
|
+
"AI returned an empty brief. Try again with a clearer image."
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
return rawText;
|
|
1790
|
+
}
|
|
1791
|
+
async function generateBriefFromImages(input, options) {
|
|
1792
|
+
const rawText = await runBriefFillAgentQuery({
|
|
1793
|
+
projectDir: options.projectDir,
|
|
1794
|
+
...input
|
|
1795
|
+
});
|
|
1796
|
+
if (input.target === "content") {
|
|
1797
|
+
const parsed = parseSuggestedRoute(rawText);
|
|
1798
|
+
if (!parsed.markdown.trim()) {
|
|
1799
|
+
throw new Error("AI returned an empty Content brief.");
|
|
1800
|
+
}
|
|
1801
|
+
return parsed;
|
|
1802
|
+
}
|
|
1803
|
+
const markdown = stripMarkdownFences(rawText);
|
|
1804
|
+
if (!markdown.trim()) {
|
|
1805
|
+
throw new Error("AI returned an empty Design brief.");
|
|
1806
|
+
}
|
|
1807
|
+
return { markdown };
|
|
1808
|
+
}
|
|
1809
|
+
function persistUpload(file, destPath) {
|
|
1810
|
+
fs$1.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
1811
|
+
fs$1.copyFileSync(file.path, destPath);
|
|
1812
|
+
}
|
|
1813
|
+
const checkAddPageRouteAction = makeJayQuery("aiditor.checkAddPageRoute").withServices(DEV_SERVER_SERVICE).withHandler(async (input, devServer) => {
|
|
1814
|
+
const syntax = validatePageRouteSyntax(input.pageRoute);
|
|
1815
|
+
if (!syntax.valid) {
|
|
1816
|
+
return {
|
|
1817
|
+
normalizedRoute: "",
|
|
1818
|
+
routeKind: "static",
|
|
1819
|
+
routeExists: false,
|
|
1820
|
+
valid: false,
|
|
1821
|
+
error: syntax.error
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
const normalizedRoute = normalizePageRoute(input.pageRoute);
|
|
1825
|
+
const routeKind = inferRouteKind(normalizedRoute);
|
|
1826
|
+
const routeExists = routeExistsInProject(
|
|
1827
|
+
normalizedRoute,
|
|
1828
|
+
devServer.listRoutes()
|
|
1829
|
+
);
|
|
1830
|
+
return {
|
|
1831
|
+
normalizedRoute,
|
|
1832
|
+
routeKind,
|
|
1833
|
+
routeExists,
|
|
1834
|
+
valid: true
|
|
1835
|
+
};
|
|
1836
|
+
});
|
|
1837
|
+
const startAddPageRequestAction = makeJayQuery(
|
|
1838
|
+
"aiditor.startAddPageRequest"
|
|
1839
|
+
).withServices(DEV_SERVER_SERVICE).withHandler(
|
|
1840
|
+
async (input, devServer) => {
|
|
1841
|
+
const syntax = validatePageRouteSyntax(input.pageRoute);
|
|
1842
|
+
if (!syntax.valid) {
|
|
1843
|
+
return {
|
|
1844
|
+
requestId: "",
|
|
1845
|
+
requestDir: "",
|
|
1846
|
+
routeExists: false,
|
|
1847
|
+
routeExistsMessage: syntax.error
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
const normalizedRoute = normalizePageRoute(input.pageRoute);
|
|
1851
|
+
const routeKind = inferRouteKind(normalizedRoute);
|
|
1852
|
+
const routeExists = routeExistsInProject(
|
|
1853
|
+
normalizedRoute,
|
|
1854
|
+
devServer.listRoutes()
|
|
1855
|
+
);
|
|
1856
|
+
if (routeExists) {
|
|
1857
|
+
return {
|
|
1858
|
+
requestId: "",
|
|
1859
|
+
requestDir: "",
|
|
1860
|
+
routeExists: true,
|
|
1861
|
+
routeExistsMessage: "This route already exists."
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
const projectDir = process.cwd();
|
|
1865
|
+
const { requestDir, request } = await createPageRequest(projectDir, {
|
|
1866
|
+
pageRoute: normalizedRoute,
|
|
1867
|
+
routeKind
|
|
1868
|
+
});
|
|
1869
|
+
return {
|
|
1870
|
+
requestId: request.requestId,
|
|
1871
|
+
requestDir,
|
|
1872
|
+
routeExists: false
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
);
|
|
1876
|
+
const saveAddPageDraftAction = makeJayQuery(
|
|
1877
|
+
"aiditor.saveAddPageDraft"
|
|
1878
|
+
).withHandler(
|
|
1879
|
+
async (input) => {
|
|
1880
|
+
const projectDir = process.cwd();
|
|
1881
|
+
const request = await readPageRequest(projectDir, input.requestId);
|
|
1882
|
+
if (!request) {
|
|
1883
|
+
return { ok: false };
|
|
1884
|
+
}
|
|
1885
|
+
await writePageRequestMarkdown(projectDir, input.requestId, {
|
|
1886
|
+
contentMd: input.contentMd,
|
|
1887
|
+
designMd: input.designMd
|
|
1888
|
+
});
|
|
1889
|
+
return { ok: true };
|
|
1890
|
+
}
|
|
1891
|
+
);
|
|
1892
|
+
const uploadAddPageAssetAction = makeJayQuery(
|
|
1893
|
+
"aiditor.uploadAddPageAsset"
|
|
1894
|
+
).withMethod("POST").withFiles().withHandler(
|
|
1895
|
+
async (input) => {
|
|
1896
|
+
if (!input.file) {
|
|
1897
|
+
throw new Error("No file uploaded.");
|
|
1898
|
+
}
|
|
1899
|
+
const projectDir = process.cwd();
|
|
1900
|
+
const request = await readPageRequest(projectDir, input.requestId);
|
|
1901
|
+
if (!request) {
|
|
1902
|
+
throw new Error(`Page request not found: ${input.requestId}`);
|
|
1903
|
+
}
|
|
1904
|
+
const id = generatePageRequestId();
|
|
1905
|
+
const filename = sanitizeAttachmentFilename(input.file.name);
|
|
1906
|
+
const subdir = input.role === "asset" ? "assets" : "references";
|
|
1907
|
+
const relPath = `${subdir}/${filename}`;
|
|
1908
|
+
const destPath = path.join(
|
|
1909
|
+
getPageRequestDir(projectDir, input.requestId),
|
|
1910
|
+
relPath
|
|
1911
|
+
);
|
|
1912
|
+
persistUpload(input.file, destPath);
|
|
1913
|
+
if (input.role === "asset") {
|
|
1914
|
+
await addPageRequestAttachment(
|
|
1915
|
+
projectDir,
|
|
1916
|
+
input.requestId,
|
|
1917
|
+
{ id, path: relPath, markdownRef: relPath },
|
|
1918
|
+
"asset"
|
|
1919
|
+
);
|
|
1920
|
+
const base = path.basename(filename, path.extname(filename));
|
|
1921
|
+
return {
|
|
1922
|
+
id,
|
|
1923
|
+
path: relPath,
|
|
1924
|
+
markdownRef: relPath,
|
|
1925
|
+
markdownSnippet: ``
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
await addPageRequestAttachment(
|
|
1929
|
+
projectDir,
|
|
1930
|
+
input.requestId,
|
|
1931
|
+
{ id, path: relPath, label: filename },
|
|
1932
|
+
"reference"
|
|
1933
|
+
);
|
|
1934
|
+
return {
|
|
1935
|
+
id,
|
|
1936
|
+
path: relPath,
|
|
1937
|
+
markdownRef: relPath
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
);
|
|
1941
|
+
const reclassifyAddPageAssetAction = makeJayQuery(
|
|
1942
|
+
"aiditor.reclassifyAddPageAsset"
|
|
1943
|
+
).withHandler(
|
|
1944
|
+
async (input) => {
|
|
1945
|
+
const projectDir = process.cwd();
|
|
1946
|
+
const updated = await reclassifyPageRequestAttachment(
|
|
1947
|
+
projectDir,
|
|
1948
|
+
input.requestId,
|
|
1949
|
+
input.attachmentId,
|
|
1950
|
+
input.newRole
|
|
1951
|
+
);
|
|
1952
|
+
const entry = input.newRole === "asset" ? updated.assets.find((a) => a.id === input.attachmentId) : updated.references.find((r) => r.id === input.attachmentId);
|
|
1953
|
+
return {
|
|
1954
|
+
ok: true,
|
|
1955
|
+
path: entry?.path ?? "",
|
|
1956
|
+
markdownRef: entry && "markdownRef" in entry ? entry.markdownRef : entry?.path
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
);
|
|
1960
|
+
const checkPageAddPageBriefAction = makeJayQuery(
|
|
1961
|
+
"aiditor.checkPageAddPageBrief"
|
|
1962
|
+
).withHandler(async (input) => {
|
|
1963
|
+
const projectDir = process.cwd();
|
|
1964
|
+
if (await pageBriefExists(projectDir, input.jayHtmlPath)) {
|
|
1965
|
+
return { hasBrief: true };
|
|
1966
|
+
}
|
|
1967
|
+
if (input.pageRoute?.trim()) {
|
|
1968
|
+
const backfilled = await tryBackfillPageBriefForRoute(
|
|
1969
|
+
projectDir,
|
|
1970
|
+
input.jayHtmlPath,
|
|
1971
|
+
input.pageRoute
|
|
1972
|
+
);
|
|
1973
|
+
if (backfilled) {
|
|
1974
|
+
return { hasBrief: true };
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
return { hasBrief: false };
|
|
1978
|
+
});
|
|
1979
|
+
const openAddPageFromBriefAction = makeJayQuery(
|
|
1980
|
+
"aiditor.openAddPageFromBrief"
|
|
1981
|
+
).withServices(DEV_SERVER_SERVICE).withHandler(
|
|
1982
|
+
async (input, devServer) => {
|
|
1983
|
+
const projectDir = process.cwd();
|
|
1984
|
+
const syntax = validatePageRouteSyntax(input.sourcePageRoute);
|
|
1985
|
+
if (!syntax.valid) {
|
|
1986
|
+
return { ok: false, error: syntax.error ?? "Invalid source route." };
|
|
1987
|
+
}
|
|
1988
|
+
const sourceRoute = normalizePageRoute(input.sourcePageRoute);
|
|
1989
|
+
const copyRoute = suggestCopyRoute(sourceRoute, devServer.listRoutes());
|
|
1990
|
+
const copySyntax = validatePageRouteSyntax(copyRoute);
|
|
1991
|
+
if (!copySyntax.valid) {
|
|
1992
|
+
return {
|
|
1993
|
+
ok: false,
|
|
1994
|
+
error: copySyntax.error ?? "Could not derive a copy route."
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
if (routeExistsInProject(copyRoute, devServer.listRoutes())) {
|
|
1998
|
+
return {
|
|
1999
|
+
ok: false,
|
|
2000
|
+
error: "Could not find a free route for the page copy."
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
try {
|
|
2004
|
+
const cloned = await clonePageBriefToRequest(
|
|
2005
|
+
projectDir,
|
|
2006
|
+
input.jayHtmlPath,
|
|
2007
|
+
{ pageRoute: copyRoute }
|
|
2008
|
+
);
|
|
2009
|
+
return {
|
|
2010
|
+
ok: true,
|
|
2011
|
+
requestId: cloned.requestId,
|
|
2012
|
+
pageRoute: copyRoute,
|
|
2013
|
+
routeKind: inferRouteKind(copyRoute),
|
|
2014
|
+
contentMd: cloned.contentMd,
|
|
2015
|
+
designMd: cloned.designMd,
|
|
2016
|
+
attachments: cloned.attachments
|
|
2017
|
+
};
|
|
2018
|
+
} catch (err) {
|
|
2019
|
+
return {
|
|
2020
|
+
ok: false,
|
|
2021
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
);
|
|
2026
|
+
const submitAddPageAction = makeJayStream("aiditor.submitAddPage").withServices(DEV_SERVER_SERVICE).withHandler(async function* (input, devServer) {
|
|
2027
|
+
const projectDir = process.cwd();
|
|
2028
|
+
const syntax = validatePageRouteSyntax(input.pageRoute);
|
|
2029
|
+
if (!syntax.valid) {
|
|
2030
|
+
yield { type: "error", message: syntax.error ?? "Invalid route." };
|
|
2031
|
+
yield { type: "done" };
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
const normalizedRoute = normalizePageRoute(input.pageRoute);
|
|
2035
|
+
const routeKind = inferRouteKind(normalizedRoute);
|
|
2036
|
+
if (routeExistsInProject(normalizedRoute, devServer.listRoutes()) && !input.isRetry) {
|
|
2037
|
+
yield {
|
|
2038
|
+
type: "error",
|
|
2039
|
+
message: "This route already exists."
|
|
2040
|
+
};
|
|
2041
|
+
yield { type: "done" };
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
const request = await readPageRequest(projectDir, input.requestId);
|
|
2045
|
+
if (!request) {
|
|
2046
|
+
yield { type: "error", message: "Page request not found." };
|
|
2047
|
+
yield { type: "done" };
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
await writePageRequestMarkdown(projectDir, input.requestId, {
|
|
2051
|
+
contentMd: input.contentMd,
|
|
2052
|
+
designMd: input.designMd
|
|
2053
|
+
});
|
|
2054
|
+
await updatePageRequestStatus(projectDir, input.requestId, "running");
|
|
2055
|
+
const requestDir = getPageRequestDir(projectDir, input.requestId);
|
|
2056
|
+
yield { type: "status", message: "Running Add Page agent…" };
|
|
2057
|
+
let agentFailed = false;
|
|
2058
|
+
try {
|
|
2059
|
+
for await (const chunk of streamAddPageAgentQuery({
|
|
2060
|
+
projectDir,
|
|
2061
|
+
requestDir,
|
|
2062
|
+
pageRoute: normalizedRoute,
|
|
2063
|
+
routeKind,
|
|
2064
|
+
isRetry: input.isRetry,
|
|
2065
|
+
selectedPlugins: request.selectedPlugins
|
|
2066
|
+
})) {
|
|
2067
|
+
yield chunk;
|
|
2068
|
+
if (chunk.type === "error") {
|
|
2069
|
+
agentFailed = true;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
agentFailed = true;
|
|
2074
|
+
yield {
|
|
2075
|
+
type: "error",
|
|
2076
|
+
message: err instanceof Error ? err.message : String(err)
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
if (agentFailed) {
|
|
2080
|
+
await updatePageRequestStatus(projectDir, input.requestId, "failed");
|
|
2081
|
+
yield { type: "done" };
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
yield { type: "status", message: "Running jay-stack validate…" };
|
|
2085
|
+
const validateResult = await runAddPageValidate(projectDir);
|
|
2086
|
+
if (!validateResult.ok) {
|
|
2087
|
+
await updatePageRequestStatus(projectDir, input.requestId, "failed");
|
|
2088
|
+
yield {
|
|
2089
|
+
type: "error",
|
|
2090
|
+
message: "Validation failed.",
|
|
2091
|
+
text: validateResult.errors.join("\n"),
|
|
2092
|
+
validateErrors: validateResult.errors
|
|
2093
|
+
};
|
|
2094
|
+
yield { type: "done" };
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
await updatePageRequestStatus(projectDir, input.requestId, "completed");
|
|
2098
|
+
await devServer.refreshRoutes();
|
|
2099
|
+
const jayHtmlPath = await resolveJayHtmlPathForPageRoute(
|
|
2100
|
+
projectDir,
|
|
2101
|
+
normalizedRoute,
|
|
2102
|
+
devServer.listRoutes()
|
|
2103
|
+
);
|
|
2104
|
+
if (jayHtmlPath) {
|
|
2105
|
+
try {
|
|
2106
|
+
await savePageBriefFromRequest(projectDir, jayHtmlPath, {
|
|
2107
|
+
requestId: input.requestId,
|
|
2108
|
+
pageRoute: normalizedRoute,
|
|
2109
|
+
contentMd: input.contentMd,
|
|
2110
|
+
designMd: input.designMd
|
|
2111
|
+
});
|
|
2112
|
+
} catch (err) {
|
|
2113
|
+
getLogger().warn(
|
|
2114
|
+
`[Add Page] Failed to save page brief for ${normalizedRoute}: ${err instanceof Error ? err.message : String(err)}`
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
yield {
|
|
2119
|
+
type: "result",
|
|
2120
|
+
message: `Page created at ${normalizedRoute}.`,
|
|
2121
|
+
text: normalizedRoute
|
|
2122
|
+
};
|
|
2123
|
+
yield { type: "done" };
|
|
2124
|
+
});
|
|
2125
|
+
const generateAddPageBriefFromImageAction = makeJayQuery(
|
|
2126
|
+
"aiditor.generateAddPageBriefFromImage"
|
|
2127
|
+
).withMethod("POST").withFiles().withHandler(
|
|
2128
|
+
async (input) => {
|
|
2129
|
+
const imageFiles = extractImageFilesFromExtraFiles(input.extraFiles);
|
|
2130
|
+
const validationError = validateBriefFillImages(imageFiles);
|
|
2131
|
+
if (validationError) {
|
|
2132
|
+
return { ok: false, error: validationError };
|
|
2133
|
+
}
|
|
2134
|
+
const projectDir = process.cwd();
|
|
2135
|
+
if (input.requestId) {
|
|
2136
|
+
const request = await readPageRequest(projectDir, input.requestId);
|
|
2137
|
+
if (!request) {
|
|
2138
|
+
return {
|
|
2139
|
+
ok: false,
|
|
2140
|
+
error: `Page request not found: ${input.requestId}`
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
try {
|
|
2145
|
+
const images = imageFiles.map(readImageAsBase64);
|
|
2146
|
+
const result = await generateBriefFromImages(
|
|
2147
|
+
{
|
|
2148
|
+
target: input.target,
|
|
2149
|
+
images,
|
|
2150
|
+
contextNotes: input.contextNotes,
|
|
2151
|
+
pageRoute: input.pageRoute
|
|
2152
|
+
},
|
|
2153
|
+
{ projectDir }
|
|
2154
|
+
);
|
|
2155
|
+
const referenceAttachments = [];
|
|
2156
|
+
if (input.requestId) {
|
|
2157
|
+
for (const file of imageFiles) {
|
|
2158
|
+
const id = generatePageRequestId();
|
|
2159
|
+
const filename = sanitizeAttachmentFilename(file.name);
|
|
2160
|
+
const relPath = `references/${filename}`;
|
|
2161
|
+
const destPath = path.join(
|
|
2162
|
+
getPageRequestDir(projectDir, input.requestId),
|
|
2163
|
+
relPath
|
|
2164
|
+
);
|
|
2165
|
+
persistUpload(file, destPath);
|
|
2166
|
+
await addPageRequestAttachment(
|
|
2167
|
+
projectDir,
|
|
2168
|
+
input.requestId,
|
|
2169
|
+
{ id, path: relPath, label: filename },
|
|
2170
|
+
"reference"
|
|
2171
|
+
);
|
|
2172
|
+
referenceAttachments.push({ id, path: relPath, filename });
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return {
|
|
2176
|
+
ok: true,
|
|
2177
|
+
markdown: result.markdown,
|
|
2178
|
+
suggestedRoute: result.suggestedRoute,
|
|
2179
|
+
suggestedRouteKind: result.suggestedRouteKind,
|
|
2180
|
+
referenceAttachments
|
|
2181
|
+
};
|
|
2182
|
+
} catch (err) {
|
|
2183
|
+
return {
|
|
2184
|
+
ok: false,
|
|
2185
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
);
|
|
2190
|
+
function parsePluginsIndexYaml(yaml) {
|
|
2191
|
+
const entries = [];
|
|
2192
|
+
let currentPlugin = null;
|
|
2193
|
+
let inContracts = false;
|
|
2194
|
+
for (const line of yaml.split("\n")) {
|
|
2195
|
+
const pluginMatch = line.match(/^ - name:\s*(.+)$/);
|
|
2196
|
+
if (pluginMatch && !line.startsWith(" ")) {
|
|
2197
|
+
currentPlugin = pluginMatch[1].trim();
|
|
2198
|
+
inContracts = false;
|
|
2199
|
+
continue;
|
|
2200
|
+
}
|
|
2201
|
+
if (line.trim() === "contracts:") {
|
|
2202
|
+
inContracts = true;
|
|
2203
|
+
continue;
|
|
2204
|
+
}
|
|
2205
|
+
if (inContracts && currentPlugin) {
|
|
2206
|
+
const contractMatch = line.match(/^ - name:\s*(.+)$/);
|
|
2207
|
+
if (contractMatch) {
|
|
2208
|
+
entries.push({
|
|
2209
|
+
pluginName: currentPlugin,
|
|
2210
|
+
contractName: contractMatch[1].trim()
|
|
2211
|
+
});
|
|
2212
|
+
continue;
|
|
2213
|
+
}
|
|
2214
|
+
const descMatch = line.match(/^ description:\s*(.+)$/);
|
|
2215
|
+
if (descMatch && entries.length > 0) {
|
|
2216
|
+
const last = entries[entries.length - 1];
|
|
2217
|
+
if (last.pluginName === currentPlugin) {
|
|
2218
|
+
last.description = descMatch[1].trim();
|
|
2219
|
+
}
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
const typeMatch = line.match(/^ type:\s*(.+)$/);
|
|
2223
|
+
if (typeMatch && entries.length > 0) {
|
|
2224
|
+
const last = entries[entries.length - 1];
|
|
2225
|
+
if (last.pluginName === currentPlugin) {
|
|
2226
|
+
last.isDynamic = typeMatch[1].trim() === "dynamic";
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
if (line.match(/^ actions:/) || line.match(/^ routes:/)) {
|
|
2231
|
+
inContracts = false;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
return entries;
|
|
2235
|
+
}
|
|
2236
|
+
function packageNameToPluginName(packageName) {
|
|
2237
|
+
const prefix = "@jay-framework/";
|
|
2238
|
+
if (packageName.startsWith(prefix)) {
|
|
2239
|
+
return packageName.slice(prefix.length);
|
|
2240
|
+
}
|
|
2241
|
+
return packageName;
|
|
2242
|
+
}
|
|
2243
|
+
async function readPackageJsonDeps(projectRoot) {
|
|
2244
|
+
const raw = await fs.readFile(
|
|
2245
|
+
path.join(projectRoot, "package.json"),
|
|
2246
|
+
"utf-8"
|
|
2247
|
+
);
|
|
2248
|
+
const pkg = JSON.parse(raw);
|
|
2249
|
+
return { ...pkg.devDependencies, ...pkg.dependencies };
|
|
2250
|
+
}
|
|
2251
|
+
async function getInstalledPlugins(projectRoot) {
|
|
2252
|
+
const deps = await readPackageJsonDeps(projectRoot);
|
|
2253
|
+
const installed = [];
|
|
2254
|
+
for (const [packageName, spec] of Object.entries(deps)) {
|
|
2255
|
+
if (!packageName.startsWith("@jay-framework/")) continue;
|
|
2256
|
+
installed.push({
|
|
2257
|
+
pluginName: packageNameToPluginName(packageName),
|
|
2258
|
+
packageName,
|
|
2259
|
+
installSpec: spec
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
return installed;
|
|
2263
|
+
}
|
|
2264
|
+
async function isPluginInstalled(projectRoot, pluginName) {
|
|
2265
|
+
const installed = await getInstalledPlugins(projectRoot);
|
|
2266
|
+
return installed.some((p) => p.pluginName === pluginName);
|
|
2267
|
+
}
|
|
2268
|
+
async function parsePluginsIndexContracts(projectRoot) {
|
|
2269
|
+
const indexPath = path.join(projectRoot, "agent-kit", "plugins-index.yaml");
|
|
2270
|
+
try {
|
|
2271
|
+
const yaml = await fs.readFile(indexPath, "utf-8");
|
|
2272
|
+
return parsePluginsIndexYaml(yaml);
|
|
2273
|
+
} catch (e) {
|
|
2274
|
+
const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
|
|
2275
|
+
if (code === "ENOENT") return [];
|
|
2276
|
+
throw e;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
async function parsePluginYamlContracts(projectRoot, pluginName) {
|
|
2280
|
+
const pluginYamlPath = path.join(
|
|
2281
|
+
projectRoot,
|
|
2282
|
+
"node_modules",
|
|
2283
|
+
"@jay-framework",
|
|
2284
|
+
pluginName,
|
|
2285
|
+
"plugin.yaml"
|
|
2286
|
+
);
|
|
2287
|
+
try {
|
|
2288
|
+
const yaml = await fs.readFile(pluginYamlPath, "utf-8");
|
|
2289
|
+
const entries = [];
|
|
2290
|
+
let inContracts = false;
|
|
2291
|
+
for (const line of yaml.split("\n")) {
|
|
2292
|
+
if (line.trim() === "contracts:") {
|
|
2293
|
+
inContracts = true;
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
if (inContracts) {
|
|
2297
|
+
const nameMatch = line.match(/^ - name:\s*(.+)$/);
|
|
2298
|
+
if (nameMatch) {
|
|
2299
|
+
entries.push({
|
|
2300
|
+
pluginName,
|
|
2301
|
+
contractName: nameMatch[1].trim()
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
if (line.match(/^actions:/) || line.match(/^routes:/)) {
|
|
2305
|
+
break;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
return entries;
|
|
2310
|
+
} catch {
|
|
2311
|
+
return [];
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
const LOCAL_SPEC_PREFIXES = ["portal:", "file:", "workspace:"];
|
|
2315
|
+
function isRegistryInstallSpec(installSpec) {
|
|
2316
|
+
if (!installSpec) return true;
|
|
2317
|
+
return !LOCAL_SPEC_PREFIXES.some((p) => installSpec.startsWith(p));
|
|
2318
|
+
}
|
|
2319
|
+
const VALID_INSTALL_PREFIXES = ["portal:", "file:", "workspace:"];
|
|
2320
|
+
function stripYamlScalar(value) {
|
|
2321
|
+
return value.replace(/^["']|["']$/g, "");
|
|
2322
|
+
}
|
|
2323
|
+
function isValidInstallSpec(spec) {
|
|
2324
|
+
return VALID_INSTALL_PREFIXES.some((p) => spec.startsWith(p));
|
|
2325
|
+
}
|
|
2326
|
+
function getPluginSourcesPath(projectRoot) {
|
|
2327
|
+
return path.join(projectRoot, ".aiditor", "plugin-sources.yaml");
|
|
2328
|
+
}
|
|
2329
|
+
function parsePluginSourcesYaml(yaml) {
|
|
2330
|
+
const sources = [];
|
|
2331
|
+
let current = null;
|
|
2332
|
+
for (const line of yaml.split("\n")) {
|
|
2333
|
+
const itemMatch = line.match(/^ - pluginName:\s*(.+)$/);
|
|
2334
|
+
if (itemMatch) {
|
|
2335
|
+
if (current?.pluginName && current.packageName && current.installSpec) {
|
|
2336
|
+
sources.push(current);
|
|
2337
|
+
}
|
|
2338
|
+
current = { pluginName: itemMatch[1].trim() };
|
|
2339
|
+
continue;
|
|
2340
|
+
}
|
|
2341
|
+
if (!current) continue;
|
|
2342
|
+
const pkgMatch = line.match(/^ packageName:\s*(.+)$/);
|
|
2343
|
+
if (pkgMatch) {
|
|
2344
|
+
current.packageName = stripYamlScalar(pkgMatch[1].trim());
|
|
2345
|
+
continue;
|
|
2346
|
+
}
|
|
2347
|
+
const specMatch = line.match(/^ installSpec:\s*(.+)$/);
|
|
2348
|
+
if (specMatch) {
|
|
2349
|
+
current.installSpec = stripYamlScalar(specMatch[1].trim());
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
const labelMatch = line.match(/^ label:\s*(.+)$/);
|
|
2353
|
+
if (labelMatch) {
|
|
2354
|
+
current.label = stripYamlScalar(labelMatch[1].trim());
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
2357
|
+
const kindMatch = line.match(/^ kind:\s*(.+)$/);
|
|
2358
|
+
if (kindMatch) {
|
|
2359
|
+
current.kind = kindMatch[1].trim();
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
const descMatch = line.match(/^ description:\s*(.+)$/);
|
|
2363
|
+
if (descMatch) {
|
|
2364
|
+
current.description = descMatch[1].trim();
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
if (current?.pluginName && current.packageName && current.installSpec) {
|
|
2368
|
+
sources.push(current);
|
|
2369
|
+
}
|
|
2370
|
+
return { sources };
|
|
2371
|
+
}
|
|
2372
|
+
function serializePluginSources(file) {
|
|
2373
|
+
const lines = ["# Unpublished or monorepo-local plugins", "sources:"];
|
|
2374
|
+
for (const s of file.sources) {
|
|
2375
|
+
lines.push(` - pluginName: ${s.pluginName}`);
|
|
2376
|
+
lines.push(` packageName: ${s.packageName}`);
|
|
2377
|
+
lines.push(` installSpec: "${s.installSpec}"`);
|
|
2378
|
+
if (s.label) lines.push(` label: ${s.label}`);
|
|
2379
|
+
if (s.kind) lines.push(` kind: ${s.kind}`);
|
|
2380
|
+
if (s.description) lines.push(` description: ${s.description}`);
|
|
2381
|
+
}
|
|
2382
|
+
return `${lines.join("\n")}
|
|
2383
|
+
`;
|
|
2384
|
+
}
|
|
2385
|
+
async function readPluginSources(projectRoot) {
|
|
2386
|
+
const filePath = getPluginSourcesPath(projectRoot);
|
|
2387
|
+
try {
|
|
2388
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
2389
|
+
return parsePluginSourcesYaml(raw);
|
|
2390
|
+
} catch (e) {
|
|
2391
|
+
const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
|
|
2392
|
+
if (code === "ENOENT") return { sources: [] };
|
|
2393
|
+
throw e;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
async function writePluginSource(projectRoot, entry) {
|
|
2397
|
+
if (!isValidInstallSpec(entry.installSpec)) {
|
|
2398
|
+
throw new Error(
|
|
2399
|
+
`installSpec must start with portal:, file:, or workspace:`
|
|
2400
|
+
);
|
|
2401
|
+
}
|
|
2402
|
+
const file = await readPluginSources(projectRoot);
|
|
2403
|
+
const ix = file.sources.findIndex((s) => s.pluginName === entry.pluginName);
|
|
2404
|
+
if (ix >= 0) {
|
|
2405
|
+
file.sources[ix] = entry;
|
|
2406
|
+
} else {
|
|
2407
|
+
file.sources.push(entry);
|
|
2408
|
+
}
|
|
2409
|
+
const dir = path.join(projectRoot, ".aiditor");
|
|
2410
|
+
await fs.mkdir(dir, { recursive: true });
|
|
2411
|
+
await fs.writeFile(
|
|
2412
|
+
getPluginSourcesPath(projectRoot),
|
|
2413
|
+
serializePluginSources(file),
|
|
2414
|
+
"utf-8"
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
function mergeCatalogWithSources(catalog, sources) {
|
|
2418
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2419
|
+
for (const e of catalog) {
|
|
2420
|
+
byName.set(e.pluginName, { ...e, isLocalSource: false });
|
|
2421
|
+
}
|
|
2422
|
+
for (const s of sources) {
|
|
2423
|
+
const existing = byName.get(s.pluginName);
|
|
2424
|
+
byName.set(s.pluginName, {
|
|
2425
|
+
pluginName: s.pluginName,
|
|
2426
|
+
packageName: s.packageName,
|
|
2427
|
+
description: s.description ?? existing?.description ?? s.pluginName,
|
|
2428
|
+
kind: s.kind ?? existing?.kind ?? "headless",
|
|
2429
|
+
requires: existing?.requires ?? [],
|
|
2430
|
+
showInAddPagePicker: existing?.showInAddPagePicker ?? true,
|
|
2431
|
+
configFiles: existing?.configFiles,
|
|
2432
|
+
isLocalSource: true,
|
|
2433
|
+
installSpec: s.installSpec,
|
|
2434
|
+
localLabel: s.label
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
return [...byName.values()];
|
|
2438
|
+
}
|
|
2439
|
+
function readPackageJson(dir) {
|
|
2440
|
+
const pkgPath = path.join(dir, "package.json");
|
|
2441
|
+
if (!fs$1.existsSync(pkgPath)) return void 0;
|
|
2442
|
+
try {
|
|
2443
|
+
return JSON.parse(fs$1.readFileSync(pkgPath, "utf-8"));
|
|
2444
|
+
} catch {
|
|
2445
|
+
return void 0;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
function hasYarnWorkspaces(pkg) {
|
|
2449
|
+
if (!pkg?.workspaces) return false;
|
|
2450
|
+
return Array.isArray(pkg.workspaces) || typeof pkg.workspaces === "object" && pkg.workspaces !== null;
|
|
2451
|
+
}
|
|
2452
|
+
function findYarnWorkspaceRoot(startDir) {
|
|
2453
|
+
let dir = path.resolve(startDir);
|
|
2454
|
+
let innermostPkgDir;
|
|
2455
|
+
while (true) {
|
|
2456
|
+
const pkg = readPackageJson(dir);
|
|
2457
|
+
if (pkg) {
|
|
2458
|
+
innermostPkgDir = dir;
|
|
2459
|
+
if (hasYarnWorkspaces(pkg) || pkg.packageManager?.startsWith("yarn@")) {
|
|
2460
|
+
return dir;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
const parent = path.dirname(dir);
|
|
2464
|
+
if (parent === dir) break;
|
|
2465
|
+
dir = parent;
|
|
2466
|
+
}
|
|
2467
|
+
return innermostPkgDir;
|
|
2468
|
+
}
|
|
2469
|
+
function readYarnPathFromRc(workspaceRoot) {
|
|
2470
|
+
const rcPath = path.join(workspaceRoot, ".yarnrc.yml");
|
|
2471
|
+
if (!fs$1.existsSync(rcPath)) return void 0;
|
|
2472
|
+
const content = fs$1.readFileSync(rcPath, "utf-8");
|
|
2473
|
+
const match = content.match(/^yarnPath:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
2474
|
+
if (!match?.[1]) return void 0;
|
|
2475
|
+
return path.resolve(workspaceRoot, match[1].trim());
|
|
2476
|
+
}
|
|
2477
|
+
function resolveYarnRunner(workspaceRoot) {
|
|
2478
|
+
const pkg = readPackageJson(workspaceRoot);
|
|
2479
|
+
const yarnPath = readYarnPathFromRc(workspaceRoot);
|
|
2480
|
+
if (yarnPath && fs$1.existsSync(yarnPath)) {
|
|
2481
|
+
return { command: "node", prefixArgs: [yarnPath] };
|
|
2482
|
+
}
|
|
2483
|
+
if (pkg?.packageManager?.startsWith("yarn@")) {
|
|
2484
|
+
return { command: "corepack", prefixArgs: ["yarn"] };
|
|
2485
|
+
}
|
|
2486
|
+
return { command: "yarn", prefixArgs: [] };
|
|
2487
|
+
}
|
|
2488
|
+
function formatYarnInstallError(raw) {
|
|
2489
|
+
const text = raw.trim();
|
|
2490
|
+
if (!text) {
|
|
2491
|
+
return "Package install failed (Yarn produced no output).";
|
|
2492
|
+
}
|
|
2493
|
+
const lower = text.toLowerCase();
|
|
2494
|
+
if (lower.includes("reading 'manifest'") || lower.includes("missing version in workspace") || lower.includes("yarn add v1.")) {
|
|
2495
|
+
return [
|
|
2496
|
+
"Wrong Yarn version: use Yarn 4 from the monorepo root, not global Yarn 1.x.",
|
|
2497
|
+
"From jay-aiditor root: corepack yarn workspace aiditor-starter add <package>.",
|
|
2498
|
+
"Or click Fix with agent to let Claude install the plugin."
|
|
2499
|
+
].join(" ");
|
|
2500
|
+
}
|
|
2501
|
+
if (lower.includes("yn0027") || lower.includes("@unknown")) {
|
|
2502
|
+
return [
|
|
2503
|
+
"Yarn could not resolve a version (often because the registry request failed).",
|
|
2504
|
+
"Check network/VPN, retry Install, or use Fix with agent / Add local plugin (portal:)."
|
|
2505
|
+
].join(" ");
|
|
2506
|
+
}
|
|
2507
|
+
if (lower.includes("requesterror") || lower.includes("ebadf") || lower.includes("enotfound") || lower.includes("econnrefused") || lower.includes("etimedout") || lower.includes("getaddrinfo") || lower.includes("yn0035")) {
|
|
2508
|
+
return [
|
|
2509
|
+
"Could not reach the npm registry (network error during Yarn resolution).",
|
|
2510
|
+
"Check internet, VPN, or corporate proxy, then retry or use Fix with agent.",
|
|
2511
|
+
"For local Wix plugins: Add local plugin → portal:../../../wix/packages/<name>."
|
|
2512
|
+
].join(" ");
|
|
2513
|
+
}
|
|
2514
|
+
if (lower.includes("isn't listed by your request") || lower.includes("no candidates found") || text.includes("YN0035")) {
|
|
2515
|
+
return [
|
|
2516
|
+
"Package version is not on the npm registry (Wix plugins are published on the 0.16.x line).",
|
|
2517
|
+
"Retry Install, or use “Add local plugin” with portal:../../../wix/packages/<name>."
|
|
2518
|
+
].join(" ");
|
|
2519
|
+
}
|
|
2520
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(
|
|
2521
|
+
(l) => l.length > 0 && !l.includes("/.cache/node/corepack") && !l.includes("yarn.js:")
|
|
2522
|
+
);
|
|
2523
|
+
const summary = lines.slice(0, 8).join("\n");
|
|
2524
|
+
return summary.length > 600 ? `${summary.slice(0, 600)}…` : summary;
|
|
2525
|
+
}
|
|
2526
|
+
function resolveYarnAddInvocation(projectDir, spec) {
|
|
2527
|
+
const resolvedProject = path.resolve(projectDir);
|
|
2528
|
+
const workspaceRoot = findYarnWorkspaceRoot(resolvedProject);
|
|
2529
|
+
const root = workspaceRoot ?? resolvedProject;
|
|
2530
|
+
const runner = resolveYarnRunner(root);
|
|
2531
|
+
const projectPkg = readPackageJson(resolvedProject);
|
|
2532
|
+
const rootPkg = readPackageJson(root);
|
|
2533
|
+
const useWorkspaceCommand = workspaceRoot !== void 0 && path.resolve(root) !== resolvedProject && hasYarnWorkspaces(rootPkg) && typeof projectPkg?.name === "string" && projectPkg.name.length > 0;
|
|
2534
|
+
if (useWorkspaceCommand) {
|
|
2535
|
+
return {
|
|
2536
|
+
command: runner.command,
|
|
2537
|
+
args: [...runner.prefixArgs, "workspace", projectPkg.name, "add", spec],
|
|
2538
|
+
cwd: root
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
const args = runner.prefixArgs.length > 0 ? [...runner.prefixArgs, "add", spec] : ["add", spec];
|
|
2542
|
+
return {
|
|
2543
|
+
command: runner.command,
|
|
2544
|
+
args,
|
|
2545
|
+
cwd: resolvedProject
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
function parseSemver(version) {
|
|
2549
|
+
const m = version.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
2550
|
+
if (!m) return null;
|
|
2551
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
2552
|
+
}
|
|
2553
|
+
function compareSemver(a, b) {
|
|
2554
|
+
for (let i = 0; i < 3; i++) {
|
|
2555
|
+
const diff = (a[i] ?? 0) - (b[i] ?? 0);
|
|
2556
|
+
if (diff !== 0) return diff;
|
|
2557
|
+
}
|
|
2558
|
+
return 0;
|
|
2559
|
+
}
|
|
2560
|
+
function isNewerSemver(latest, installed) {
|
|
2561
|
+
const a = parseSemver(latest);
|
|
2562
|
+
const b = parseSemver(installed);
|
|
2563
|
+
if (!a || !b) return false;
|
|
2564
|
+
return compareSemver(a, b) > 0;
|
|
2565
|
+
}
|
|
2566
|
+
async function spawnCommand(command, args, cwd, options) {
|
|
2567
|
+
return new Promise((resolve) => {
|
|
2568
|
+
const child = spawn(command, args, {
|
|
2569
|
+
cwd,
|
|
2570
|
+
shell: options?.shell ?? false,
|
|
2571
|
+
env: process.env
|
|
2572
|
+
});
|
|
2573
|
+
let stdout = "";
|
|
2574
|
+
let stderr = "";
|
|
2575
|
+
child.stdout.on("data", (chunk) => {
|
|
2576
|
+
stdout += chunk.toString();
|
|
2577
|
+
});
|
|
2578
|
+
child.stderr.on("data", (chunk) => {
|
|
2579
|
+
stderr += chunk.toString();
|
|
2580
|
+
});
|
|
2581
|
+
child.on("close", (code) => {
|
|
2582
|
+
resolve({ code, stdout, stderr });
|
|
2583
|
+
});
|
|
2584
|
+
child.on("error", (err) => {
|
|
2585
|
+
resolve({ code: 1, stdout, stderr: err.message });
|
|
2586
|
+
});
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
async function yarnAdd(projectDir, spec) {
|
|
2590
|
+
const inv = resolveYarnAddInvocation(projectDir, spec);
|
|
2591
|
+
return spawnCommand(inv.command, inv.args, inv.cwd);
|
|
2592
|
+
}
|
|
2593
|
+
async function npmAdd(projectDir, packageName, version) {
|
|
2594
|
+
const spec = version ? `${packageName}@${version}` : packageName;
|
|
2595
|
+
return spawnCommand("npm", ["install", spec, "--save"], projectDir);
|
|
2596
|
+
}
|
|
2597
|
+
async function runJayStackSetup(projectDir, pluginName, force) {
|
|
2598
|
+
const args = ["jay-stack-cli", "setup", pluginName];
|
|
2599
|
+
if (force) args.push("--force");
|
|
2600
|
+
return spawnCommand("npx", args, projectDir, { shell: true });
|
|
2601
|
+
}
|
|
2602
|
+
async function runJayStackAgentKit(projectDir, pluginName) {
|
|
2603
|
+
return spawnCommand(
|
|
2604
|
+
"npx",
|
|
2605
|
+
["jay-stack-cli", "agent-kit", "--plugin", pluginName],
|
|
2606
|
+
projectDir,
|
|
2607
|
+
{ shell: true }
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
function parseSetupOutput(stdout, stderr, exitCode) {
|
|
2611
|
+
const combined = `${stdout}
|
|
2612
|
+
${stderr}`.trim();
|
|
2613
|
+
const jsonMatch = combined.match(/\{[\s\S]*"status"[\s\S]*\}/);
|
|
2614
|
+
if (jsonMatch) {
|
|
2615
|
+
try {
|
|
2616
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
2617
|
+
if (parsed.status === "configured") {
|
|
2618
|
+
return {
|
|
2619
|
+
setupStatus: "configured",
|
|
2620
|
+
configCreated: parsed.configCreated,
|
|
2621
|
+
message: parsed.message
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
if (parsed.status === "needs-config") {
|
|
2625
|
+
return {
|
|
2626
|
+
setupStatus: "needs-config",
|
|
2627
|
+
configCreated: parsed.configCreated,
|
|
2628
|
+
message: parsed.message
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
if (parsed.status === "error") {
|
|
2632
|
+
return {
|
|
2633
|
+
setupStatus: "error",
|
|
2634
|
+
message: parsed.message ?? combined
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
} catch {
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
if (exitCode === 0) {
|
|
2641
|
+
const lower = combined.toLowerCase();
|
|
2642
|
+
if (lower.includes("needs-config") || lower.includes("needs config") || lower.includes("placeholder")) {
|
|
2643
|
+
return {
|
|
2644
|
+
setupStatus: "needs-config",
|
|
2645
|
+
message: combined || "Plugin needs configuration."
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
return { setupStatus: "configured", message: combined || void 0 };
|
|
2649
|
+
}
|
|
2650
|
+
return {
|
|
2651
|
+
setupStatus: "error",
|
|
2652
|
+
message: combined || `setup exited with code ${exitCode}`
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
async function readInstalledPackageVersion(projectRoot, packageName) {
|
|
2656
|
+
const parts = packageName.split("/");
|
|
2657
|
+
const pkgPath = path.join(
|
|
2658
|
+
projectRoot,
|
|
2659
|
+
"node_modules",
|
|
2660
|
+
...parts,
|
|
2661
|
+
"package.json"
|
|
2662
|
+
);
|
|
2663
|
+
try {
|
|
2664
|
+
const raw = await fs.readFile(pkgPath, "utf-8");
|
|
2665
|
+
const pkg = JSON.parse(raw);
|
|
2666
|
+
return pkg.version ?? null;
|
|
2667
|
+
} catch {
|
|
2668
|
+
return null;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
async function fetchNpmLatestVersion(packageName) {
|
|
2672
|
+
const result = await spawnCommand(
|
|
2673
|
+
"npm",
|
|
2674
|
+
["view", packageName, "version", "--json"],
|
|
2675
|
+
process.cwd()
|
|
2676
|
+
);
|
|
2677
|
+
if (result.code !== 0) return null;
|
|
2678
|
+
const out = (result.stdout || result.stderr).trim();
|
|
2679
|
+
if (!out) return null;
|
|
2680
|
+
try {
|
|
2681
|
+
const parsed = JSON.parse(out);
|
|
2682
|
+
return typeof parsed === "string" ? parsed : null;
|
|
2683
|
+
} catch {
|
|
2684
|
+
return out.replace(/^"|"$/g, "").trim() || null;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
async function checkPluginUpdate(projectRoot, pluginName, packageName, installSpec) {
|
|
2688
|
+
const base = {
|
|
2689
|
+
pluginName,
|
|
2690
|
+
packageName,
|
|
2691
|
+
installedVersion: null,
|
|
2692
|
+
latestVersion: null,
|
|
2693
|
+
updateAvailable: false,
|
|
2694
|
+
isLocalSource: false
|
|
2695
|
+
};
|
|
2696
|
+
if (!isRegistryInstallSpec(installSpec)) {
|
|
2697
|
+
return {
|
|
2698
|
+
...base,
|
|
2699
|
+
isLocalSource: true,
|
|
2700
|
+
message: "Local source — registry updates do not apply."
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
const installedVersion = await readInstalledPackageVersion(
|
|
2704
|
+
projectRoot,
|
|
2705
|
+
packageName
|
|
2706
|
+
);
|
|
2707
|
+
const latestVersion = await fetchNpmLatestVersion(packageName);
|
|
2708
|
+
if (!latestVersion) {
|
|
2709
|
+
return {
|
|
2710
|
+
...base,
|
|
2711
|
+
installedVersion,
|
|
2712
|
+
message: "Could not fetch latest version from npm."
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
const updateAvailable = installedVersion != null && isNewerSemver(latestVersion, installedVersion);
|
|
2716
|
+
return {
|
|
2717
|
+
...base,
|
|
2718
|
+
installedVersion,
|
|
2719
|
+
latestVersion,
|
|
2720
|
+
updateAvailable,
|
|
2721
|
+
message: updateAvailable ? `Update available: ${installedVersion} → ${latestVersion}` : installedVersion ? `Up to date (${installedVersion})` : void 0
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
function getSetupCachePath(projectRoot) {
|
|
2725
|
+
return path.join(projectRoot, ".aiditor", "plugin-setup-status.json");
|
|
2726
|
+
}
|
|
2727
|
+
async function readSetupCache(projectRoot) {
|
|
2728
|
+
const filePath = getSetupCachePath(projectRoot);
|
|
2729
|
+
try {
|
|
2730
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
2731
|
+
return JSON.parse(raw);
|
|
2732
|
+
} catch (e) {
|
|
2733
|
+
const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
|
|
2734
|
+
if (code === "ENOENT") return {};
|
|
2735
|
+
throw e;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
async function writeSetupCacheEntry(projectRoot, pluginName, entry) {
|
|
2739
|
+
const cache2 = await readSetupCache(projectRoot);
|
|
2740
|
+
cache2[pluginName] = entry;
|
|
2741
|
+
const dir = path.join(projectRoot, ".aiditor");
|
|
2742
|
+
await fs.mkdir(dir, { recursive: true });
|
|
2743
|
+
await fs.writeFile(
|
|
2744
|
+
getSetupCachePath(projectRoot),
|
|
2745
|
+
`${JSON.stringify(cache2, null, 2)}
|
|
2746
|
+
`,
|
|
2747
|
+
"utf-8"
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
function getCachedSetupStatus(cache2, pluginName) {
|
|
2751
|
+
return cache2[pluginName];
|
|
2752
|
+
}
|
|
2753
|
+
function topologicalInstallOrder(target, catalog) {
|
|
2754
|
+
const byName = new Map(catalog.map((e) => [e.pluginName, e]));
|
|
2755
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2756
|
+
const order = [];
|
|
2757
|
+
function visit(name) {
|
|
2758
|
+
if (visited.has(name)) return;
|
|
2759
|
+
visited.add(name);
|
|
2760
|
+
const entry = byName.get(name);
|
|
2761
|
+
if (!entry) return;
|
|
2762
|
+
for (const req of entry.requires) visit(req);
|
|
2763
|
+
order.push(entry);
|
|
2764
|
+
}
|
|
2765
|
+
visit(target.pluginName);
|
|
2766
|
+
return order;
|
|
2767
|
+
}
|
|
2768
|
+
function buildRegistryInstallSpec(packageName) {
|
|
2769
|
+
return packageName;
|
|
2770
|
+
}
|
|
2771
|
+
async function resolveInstallSpec(projectRoot, entry, overrideSpec) {
|
|
2772
|
+
if (overrideSpec) return overrideSpec;
|
|
2773
|
+
const sources = await readPluginSources(projectRoot);
|
|
2774
|
+
const local = sources.sources.find((s) => s.pluginName === entry.pluginName);
|
|
2775
|
+
if (local?.installSpec) {
|
|
2776
|
+
return `${entry.packageName}@${local.installSpec}`;
|
|
2777
|
+
}
|
|
2778
|
+
const deps = await readPackageJsonDeps(projectRoot);
|
|
2779
|
+
const existing = deps[entry.packageName];
|
|
2780
|
+
if (existing && !isRegistryInstallSpec(existing)) {
|
|
2781
|
+
return `${entry.packageName}@${existing}`;
|
|
2782
|
+
}
|
|
2783
|
+
const latest = await fetchNpmLatestVersion(entry.packageName);
|
|
2784
|
+
if (latest) {
|
|
2785
|
+
return `${entry.packageName}@${latest}`;
|
|
2786
|
+
}
|
|
2787
|
+
return buildRegistryInstallSpec(entry.packageName);
|
|
2788
|
+
}
|
|
2789
|
+
function isRegistryResolutionFailure(output) {
|
|
2790
|
+
const lower = output.toLowerCase();
|
|
2791
|
+
return lower.includes("requesterror") || lower.includes("ebadf") || lower.includes("yn0027") || lower.includes("yn0035") || lower.includes("@unknown") || lower.includes("enotfound") || lower.includes("econnrefused");
|
|
2792
|
+
}
|
|
2793
|
+
async function installOnePlugin(projectRoot, entry, installSpec) {
|
|
2794
|
+
const spec = await resolveInstallSpec(projectRoot, entry, installSpec);
|
|
2795
|
+
const yarnResult = await yarnAdd(projectRoot, spec);
|
|
2796
|
+
if (yarnResult.code === 0) {
|
|
2797
|
+
return { ok: true };
|
|
2798
|
+
}
|
|
2799
|
+
const yarnOut = `${yarnResult.stderr}
|
|
2800
|
+
${yarnResult.stdout}`.trim();
|
|
2801
|
+
const isRegistryPackageSpec = spec === entry.packageName || spec.startsWith(`${entry.packageName}@`);
|
|
2802
|
+
const useNpmFallback = isRegistryPackageSpec && isRegistryResolutionFailure(yarnOut);
|
|
2803
|
+
if (useNpmFallback) {
|
|
2804
|
+
const latest = await fetchNpmLatestVersion(entry.packageName);
|
|
2805
|
+
const npmResult = await npmAdd(
|
|
2806
|
+
projectRoot,
|
|
2807
|
+
entry.packageName,
|
|
2808
|
+
latest ?? void 0
|
|
2809
|
+
);
|
|
2810
|
+
if (npmResult.code === 0) {
|
|
2811
|
+
return { ok: true };
|
|
2812
|
+
}
|
|
2813
|
+
const npmOut = `${npmResult.stderr}
|
|
2814
|
+
${npmResult.stdout}`.trim();
|
|
2815
|
+
return {
|
|
2816
|
+
ok: false,
|
|
2817
|
+
error: `Yarn failed:
|
|
2818
|
+
${yarnOut}
|
|
2819
|
+
|
|
2820
|
+
npm fallback failed:
|
|
2821
|
+
${npmOut}`
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
return {
|
|
2825
|
+
ok: false,
|
|
2826
|
+
error: yarnOut || "yarn add failed"
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
async function contractsForPlugin$1(projectRoot, pluginName) {
|
|
2830
|
+
const fromIndex = await parsePluginsIndexContracts(projectRoot);
|
|
2831
|
+
const filtered = fromIndex.filter((c) => c.pluginName === pluginName);
|
|
2832
|
+
if (filtered.length > 0) return filtered;
|
|
2833
|
+
return parsePluginYamlContracts(projectRoot, pluginName);
|
|
2834
|
+
}
|
|
2835
|
+
async function* ensureProjectPlugin(projectRoot, pluginName, options) {
|
|
2836
|
+
const catalog = getJayPluginCatalog();
|
|
2837
|
+
const entry = getCatalogEntry(pluginName);
|
|
2838
|
+
if (!entry) {
|
|
2839
|
+
yield { type: "error", message: `Unknown plugin: ${pluginName}` };
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
const order = topologicalInstallOrder(entry, catalog);
|
|
2843
|
+
for (const item of order) {
|
|
2844
|
+
const installed = await isPluginInstalled(projectRoot, item.pluginName);
|
|
2845
|
+
if (installed && item.pluginName !== pluginName) continue;
|
|
2846
|
+
const isTarget = item.pluginName === pluginName;
|
|
2847
|
+
const shouldInstall = !installed || isTarget && options?.upgrade === true;
|
|
2848
|
+
if (shouldInstall) {
|
|
2849
|
+
yield {
|
|
2850
|
+
type: "progress",
|
|
2851
|
+
step: isTarget ? "package" : "requires",
|
|
2852
|
+
message: isTarget ? options?.upgrade ? `Updating ${item.packageName} to latest…` : `Installing ${item.packageName}…` : `Installing dependency ${item.pluginName}…`
|
|
2853
|
+
};
|
|
2854
|
+
const spec = isTarget ? options?.installSpec : void 0;
|
|
2855
|
+
const installResult = await installOnePlugin(projectRoot, item, spec);
|
|
2856
|
+
if (!installResult.ok) {
|
|
2857
|
+
const raw = installResult.error ?? "";
|
|
2858
|
+
yield {
|
|
2859
|
+
type: "error",
|
|
2860
|
+
message: `Could not install ${item.pluginName}.`,
|
|
2861
|
+
installError: formatYarnInstallError(raw)
|
|
2862
|
+
};
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
yield {
|
|
2868
|
+
type: "progress",
|
|
2869
|
+
step: "setup",
|
|
2870
|
+
message: `Running setup for ${pluginName}…`
|
|
2871
|
+
};
|
|
2872
|
+
const setupResult = await runJayStackSetup(projectRoot, pluginName);
|
|
2873
|
+
const parsed = parseSetupOutput(
|
|
2874
|
+
setupResult.stdout,
|
|
2875
|
+
setupResult.stderr,
|
|
2876
|
+
setupResult.code
|
|
2877
|
+
);
|
|
2878
|
+
const configFiles = entry.configFiles ?? parsed.configCreated ?? [];
|
|
2879
|
+
await writeSetupCacheEntry(projectRoot, pluginName, {
|
|
2880
|
+
setupStatus: parsed.setupStatus,
|
|
2881
|
+
configFiles,
|
|
2882
|
+
message: parsed.message,
|
|
2883
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2884
|
+
});
|
|
2885
|
+
yield {
|
|
2886
|
+
type: "progress",
|
|
2887
|
+
step: "agent-kit",
|
|
2888
|
+
message: `Refreshing contracts for ${pluginName}…`
|
|
2889
|
+
};
|
|
2890
|
+
const agentKitResult = await runJayStackAgentKit(projectRoot, pluginName);
|
|
2891
|
+
if (agentKitResult.code !== 0) {
|
|
2892
|
+
const msg = (agentKitResult.stderr || agentKitResult.stdout).trim();
|
|
2893
|
+
yield {
|
|
2894
|
+
type: "error",
|
|
2895
|
+
message: `agent-kit failed for ${pluginName}: ${msg || "unknown error"}`
|
|
2896
|
+
};
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
const contracts = await contractsForPlugin$1(projectRoot, pluginName);
|
|
2900
|
+
yield {
|
|
2901
|
+
type: "complete",
|
|
2902
|
+
pluginName,
|
|
2903
|
+
setupStatus: parsed.setupStatus,
|
|
2904
|
+
configFiles,
|
|
2905
|
+
contracts,
|
|
2906
|
+
message: parsed.message
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
const PLUGIN_MANIFEST_START = "<!-- aiditor:plugins:start -->";
|
|
2910
|
+
const PLUGIN_MANIFEST_END = "<!-- aiditor:plugins:end -->";
|
|
2911
|
+
function formatManifestLine(packageName, contractName, componentKey) {
|
|
2912
|
+
const key = componentKey ?? getDefaultComponentKey(contractName);
|
|
2913
|
+
return `- ${packageName} / ${contractName} (key: ${key})`;
|
|
2914
|
+
}
|
|
2915
|
+
function buildPluginManifestSection(selectedPlugins) {
|
|
2916
|
+
const lines = [
|
|
2917
|
+
PLUGIN_MANIFEST_START,
|
|
2918
|
+
"",
|
|
2919
|
+
"## Plugins & contracts (selected)",
|
|
2920
|
+
""
|
|
2921
|
+
];
|
|
2922
|
+
for (const plugin of selectedPlugins) {
|
|
2923
|
+
for (const contract of plugin.contracts) {
|
|
2924
|
+
lines.push(
|
|
2925
|
+
formatManifestLine(
|
|
2926
|
+
plugin.packageName,
|
|
2927
|
+
contract.contractName,
|
|
2928
|
+
contract.componentKey
|
|
2929
|
+
)
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
lines.push(PLUGIN_MANIFEST_END);
|
|
2934
|
+
return lines.join("\n");
|
|
2935
|
+
}
|
|
2936
|
+
function syncPluginManifestInContentMd(contentMd, selectedPlugins) {
|
|
2937
|
+
const section = buildPluginManifestSection(selectedPlugins);
|
|
2938
|
+
const startIx = contentMd.indexOf(PLUGIN_MANIFEST_START);
|
|
2939
|
+
const endIx = contentMd.indexOf(PLUGIN_MANIFEST_END);
|
|
2940
|
+
if (startIx >= 0 && endIx > startIx) {
|
|
2941
|
+
const before = contentMd.slice(0, startIx).trimEnd();
|
|
2942
|
+
const after = contentMd.slice(endIx + PLUGIN_MANIFEST_END.length).trimStart();
|
|
2943
|
+
const parts = [before, section, after].filter((p) => p.length > 0);
|
|
2944
|
+
return parts.length > 0 ? `${parts.join("\n\n")}
|
|
2945
|
+
` : `${section}
|
|
2946
|
+
`;
|
|
2947
|
+
}
|
|
2948
|
+
const hasContracts = selectedPlugins.some((p) => p.contracts.length > 0);
|
|
2949
|
+
if (!hasContracts) return contentMd;
|
|
2950
|
+
const trimmed = contentMd.trimEnd();
|
|
2951
|
+
if (trimmed.length === 0) {
|
|
2952
|
+
return `${section}
|
|
2953
|
+
`;
|
|
2954
|
+
}
|
|
2955
|
+
return `${trimmed}
|
|
2956
|
+
|
|
2957
|
+
${section}
|
|
2958
|
+
`;
|
|
2959
|
+
}
|
|
2960
|
+
async function contractsForPlugin(projectRoot, pluginName) {
|
|
2961
|
+
const fromIndex = await parsePluginsIndexContracts(projectRoot);
|
|
2962
|
+
const filtered = fromIndex.filter((c) => c.pluginName === pluginName);
|
|
2963
|
+
if (filtered.length > 0) {
|
|
2964
|
+
return filtered.map((c) => ({
|
|
2965
|
+
contractName: c.contractName,
|
|
2966
|
+
description: c.description,
|
|
2967
|
+
isDynamic: c.isDynamic
|
|
2968
|
+
}));
|
|
2969
|
+
}
|
|
2970
|
+
const fromYaml = await parsePluginYamlContracts(projectRoot, pluginName);
|
|
2971
|
+
return fromYaml.map((c) => ({
|
|
2972
|
+
contractName: c.contractName,
|
|
2973
|
+
description: c.description
|
|
2974
|
+
}));
|
|
2975
|
+
}
|
|
2976
|
+
const listJayPluginsAction = makeJayQuery("aiditor.listJayPlugins").withServices(DEV_SERVER_SERVICE).withHandler(async (input, _devServer) => {
|
|
2977
|
+
const projectRoot = process.cwd();
|
|
2978
|
+
const catalog = mergeCatalogWithSources(
|
|
2979
|
+
getJayPluginCatalog(),
|
|
2980
|
+
(await readPluginSources(projectRoot)).sources
|
|
2981
|
+
);
|
|
2982
|
+
const installed = await getInstalledPlugins(projectRoot);
|
|
2983
|
+
const installedByName = new Map(installed.map((p) => [p.pluginName, p]));
|
|
2984
|
+
let cache2 = await readSetupCache(projectRoot);
|
|
2985
|
+
if (input.refreshSetupStatus) {
|
|
2986
|
+
for (const inst of installed) {
|
|
2987
|
+
const entry = getCatalogEntry(inst.pluginName);
|
|
2988
|
+
if (!entry?.configFiles?.length) continue;
|
|
2989
|
+
const setupResult = await runJayStackSetup(
|
|
2990
|
+
projectRoot,
|
|
2991
|
+
inst.pluginName
|
|
2992
|
+
);
|
|
2993
|
+
const parsed = parseSetupOutput(
|
|
2994
|
+
setupResult.stdout,
|
|
2995
|
+
setupResult.stderr,
|
|
2996
|
+
setupResult.code
|
|
2997
|
+
);
|
|
2998
|
+
await writeSetupCacheEntry(projectRoot, inst.pluginName, {
|
|
2999
|
+
setupStatus: parsed.setupStatus,
|
|
3000
|
+
configFiles: entry.configFiles ?? [],
|
|
3001
|
+
message: parsed.message,
|
|
3002
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
cache2 = await readSetupCache(projectRoot);
|
|
3006
|
+
}
|
|
3007
|
+
const plugins = await Promise.all(
|
|
3008
|
+
catalog.map(async (entry) => {
|
|
3009
|
+
const inst = installedByName.get(entry.pluginName);
|
|
3010
|
+
const cached = getCachedSetupStatus(cache2, entry.pluginName);
|
|
3011
|
+
const contracts = entry.kind === "headless" && inst ? await contractsForPlugin(projectRoot, entry.pluginName) : [];
|
|
3012
|
+
const installedVersion = inst && isRegistryInstallSpec(inst.installSpec) ? await readInstalledPackageVersion(projectRoot, entry.packageName) : null;
|
|
3013
|
+
return {
|
|
3014
|
+
pluginName: entry.pluginName,
|
|
3015
|
+
packageName: entry.packageName,
|
|
3016
|
+
description: entry.description,
|
|
3017
|
+
kind: entry.kind,
|
|
3018
|
+
requires: entry.requires,
|
|
3019
|
+
installed: !!inst,
|
|
3020
|
+
installSpec: inst?.installSpec,
|
|
3021
|
+
installedVersion: installedVersion ?? void 0,
|
|
3022
|
+
isLocalSource: entry.isLocalSource,
|
|
3023
|
+
showInAddPagePicker: entry.showInAddPagePicker,
|
|
3024
|
+
setupStatus: cached?.setupStatus ?? "unknown",
|
|
3025
|
+
configFiles: cached?.configFiles ?? entry.configFiles ?? [],
|
|
3026
|
+
setupMessage: cached?.message,
|
|
3027
|
+
contracts
|
|
3028
|
+
};
|
|
3029
|
+
})
|
|
3030
|
+
);
|
|
3031
|
+
return { plugins };
|
|
3032
|
+
});
|
|
3033
|
+
const listPluginContractsAction = makeJayQuery(
|
|
3034
|
+
"aiditor.listPluginContracts"
|
|
3035
|
+
).withServices(DEV_SERVER_SERVICE).withHandler(async (input) => {
|
|
3036
|
+
const projectRoot = process.cwd();
|
|
3037
|
+
const installed = await getInstalledPlugins(projectRoot);
|
|
3038
|
+
const isInstalled = installed.some(
|
|
3039
|
+
(p) => p.pluginName === input.pluginName
|
|
3040
|
+
);
|
|
3041
|
+
const contracts = isInstalled ? await contractsForPlugin(projectRoot, input.pluginName) : [];
|
|
3042
|
+
const entry = getCatalogEntry(input.pluginName);
|
|
3043
|
+
let setupRequiredMessage;
|
|
3044
|
+
if (isInstalled && contracts.length === 0 && entry?.kind === "headless") {
|
|
3045
|
+
setupRequiredMessage = "Run setup to load contracts (dynamic contracts may require Wix configuration).";
|
|
3046
|
+
}
|
|
3047
|
+
return {
|
|
3048
|
+
pluginName: input.pluginName,
|
|
3049
|
+
installed: isInstalled,
|
|
3050
|
+
contracts,
|
|
3051
|
+
setupRequiredMessage
|
|
3052
|
+
};
|
|
3053
|
+
});
|
|
3054
|
+
const checkPluginUpdateAction = makeJayQuery("aiditor.checkPluginUpdate").withServices(DEV_SERVER_SERVICE).withHandler(async (input) => {
|
|
3055
|
+
const projectRoot = process.cwd();
|
|
3056
|
+
const entry = getCatalogEntry(input.pluginName);
|
|
3057
|
+
if (!entry) {
|
|
3058
|
+
return {
|
|
3059
|
+
pluginName: input.pluginName,
|
|
3060
|
+
packageName: "",
|
|
3061
|
+
installedVersion: null,
|
|
3062
|
+
latestVersion: null,
|
|
3063
|
+
updateAvailable: false,
|
|
3064
|
+
isLocalSource: false,
|
|
3065
|
+
message: `Unknown plugin: ${input.pluginName}`
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
const installed = await getInstalledPlugins(projectRoot);
|
|
3069
|
+
const inst = installed.find((p) => p.pluginName === input.pluginName);
|
|
3070
|
+
if (!inst) {
|
|
3071
|
+
return {
|
|
3072
|
+
pluginName: input.pluginName,
|
|
3073
|
+
packageName: entry.packageName,
|
|
3074
|
+
installedVersion: null,
|
|
3075
|
+
latestVersion: null,
|
|
3076
|
+
updateAvailable: false,
|
|
3077
|
+
isLocalSource: false,
|
|
3078
|
+
message: "Plugin is not installed."
|
|
3079
|
+
};
|
|
3080
|
+
}
|
|
3081
|
+
return checkPluginUpdate(
|
|
3082
|
+
projectRoot,
|
|
3083
|
+
input.pluginName,
|
|
3084
|
+
entry.packageName,
|
|
3085
|
+
inst.installSpec
|
|
3086
|
+
);
|
|
3087
|
+
});
|
|
3088
|
+
const ensureProjectPluginAction = makeJayStream(
|
|
3089
|
+
"aiditor.ensureProjectPlugin"
|
|
3090
|
+
).withServices(DEV_SERVER_SERVICE).withHandler(async function* (input, devServer) {
|
|
3091
|
+
const projectRoot = process.cwd();
|
|
3092
|
+
try {
|
|
3093
|
+
for await (const chunk of ensureProjectPlugin(
|
|
3094
|
+
projectRoot,
|
|
3095
|
+
input.pluginName,
|
|
3096
|
+
{ installSpec: input.installSpec, upgrade: input.upgrade }
|
|
3097
|
+
)) {
|
|
3098
|
+
if (chunk.type === "progress") {
|
|
3099
|
+
yield chunk;
|
|
3100
|
+
} else if (chunk.type === "complete") {
|
|
3101
|
+
await devServer.refreshRoutes();
|
|
3102
|
+
yield {
|
|
3103
|
+
type: "complete",
|
|
3104
|
+
pluginName: chunk.pluginName,
|
|
3105
|
+
setupStatus: chunk.setupStatus,
|
|
3106
|
+
configFiles: chunk.configFiles,
|
|
3107
|
+
contracts: chunk.contracts.map((c) => ({
|
|
3108
|
+
contractName: c.contractName,
|
|
3109
|
+
description: c.description,
|
|
3110
|
+
isDynamic: c.isDynamic
|
|
3111
|
+
}))
|
|
3112
|
+
};
|
|
3113
|
+
} else if (chunk.type === "error") {
|
|
3114
|
+
yield {
|
|
3115
|
+
type: "error",
|
|
3116
|
+
message: chunk.message,
|
|
3117
|
+
installError: chunk.installError
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
} catch (err) {
|
|
3122
|
+
yield {
|
|
3123
|
+
type: "error",
|
|
3124
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
yield { type: "done" };
|
|
3128
|
+
});
|
|
3129
|
+
const getPluginSetupStatusAction = makeJayQuery(
|
|
3130
|
+
"aiditor.getPluginSetupStatus"
|
|
3131
|
+
).withServices(DEV_SERVER_SERVICE).withHandler(async (input) => {
|
|
3132
|
+
const projectRoot = process.cwd();
|
|
3133
|
+
const installed = await getInstalledPlugins(projectRoot);
|
|
3134
|
+
const targets = input.pluginName ? installed.filter((p) => p.pluginName === input.pluginName) : installed;
|
|
3135
|
+
const plugins = [];
|
|
3136
|
+
for (const inst of targets) {
|
|
3137
|
+
const entry = getCatalogEntry(inst.pluginName);
|
|
3138
|
+
if (input.refresh) {
|
|
3139
|
+
const setupResult = await runJayStackSetup(
|
|
3140
|
+
projectRoot,
|
|
3141
|
+
inst.pluginName
|
|
3142
|
+
);
|
|
3143
|
+
const parsed = parseSetupOutput(
|
|
3144
|
+
setupResult.stdout,
|
|
3145
|
+
setupResult.stderr,
|
|
3146
|
+
setupResult.code
|
|
3147
|
+
);
|
|
3148
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3149
|
+
await writeSetupCacheEntry(projectRoot, inst.pluginName, {
|
|
3150
|
+
setupStatus: parsed.setupStatus,
|
|
3151
|
+
configFiles: entry?.configFiles ?? [],
|
|
3152
|
+
message: parsed.message,
|
|
3153
|
+
checkedAt
|
|
3154
|
+
});
|
|
3155
|
+
plugins.push({
|
|
3156
|
+
pluginName: inst.pluginName,
|
|
3157
|
+
setupStatus: parsed.setupStatus,
|
|
3158
|
+
configFiles: entry?.configFiles ?? [],
|
|
3159
|
+
message: parsed.message,
|
|
3160
|
+
checkedAt
|
|
3161
|
+
});
|
|
3162
|
+
} else {
|
|
3163
|
+
const cache2 = await readSetupCache(projectRoot);
|
|
3164
|
+
const cached = getCachedSetupStatus(cache2, inst.pluginName);
|
|
3165
|
+
plugins.push({
|
|
3166
|
+
pluginName: inst.pluginName,
|
|
3167
|
+
setupStatus: cached?.setupStatus ?? "unknown",
|
|
3168
|
+
configFiles: cached?.configFiles ?? entry?.configFiles ?? [],
|
|
3169
|
+
message: cached?.message,
|
|
3170
|
+
checkedAt: cached?.checkedAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
return { plugins };
|
|
3175
|
+
});
|
|
3176
|
+
const rerunPluginSetupAction = makeJayQuery("aiditor.rerunPluginSetup").withServices(DEV_SERVER_SERVICE).withHandler(async (input) => {
|
|
3177
|
+
const projectRoot = process.cwd();
|
|
3178
|
+
const entry = getCatalogEntry(input.pluginName);
|
|
3179
|
+
const setupResult = await runJayStackSetup(
|
|
3180
|
+
projectRoot,
|
|
3181
|
+
input.pluginName,
|
|
3182
|
+
input.force
|
|
3183
|
+
);
|
|
3184
|
+
const parsed = parseSetupOutput(
|
|
3185
|
+
setupResult.stdout,
|
|
3186
|
+
setupResult.stderr,
|
|
3187
|
+
setupResult.code
|
|
3188
|
+
);
|
|
3189
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3190
|
+
await writeSetupCacheEntry(projectRoot, input.pluginName, {
|
|
3191
|
+
setupStatus: parsed.setupStatus,
|
|
3192
|
+
configFiles: entry?.configFiles ?? parsed.configCreated ?? [],
|
|
3193
|
+
message: parsed.message,
|
|
3194
|
+
checkedAt
|
|
3195
|
+
});
|
|
3196
|
+
return {
|
|
3197
|
+
pluginName: input.pluginName,
|
|
3198
|
+
setupStatus: parsed.setupStatus,
|
|
3199
|
+
configCreated: parsed.configCreated,
|
|
3200
|
+
message: parsed.message
|
|
3201
|
+
};
|
|
3202
|
+
});
|
|
3203
|
+
const syncAddPagePluginManifestAction = makeJayQuery(
|
|
3204
|
+
"aiditor.syncAddPagePluginManifest"
|
|
3205
|
+
).withServices(DEV_SERVER_SERVICE).withHandler(
|
|
3206
|
+
async (input) => {
|
|
3207
|
+
const projectRoot = process.cwd();
|
|
3208
|
+
const request = await readPageRequest(projectRoot, input.requestId);
|
|
3209
|
+
if (!request) {
|
|
3210
|
+
return { ok: false, contentMd: "", error: "Page request not found." };
|
|
3211
|
+
}
|
|
3212
|
+
const requestDir = path.join(
|
|
3213
|
+
projectRoot,
|
|
3214
|
+
".aiditor",
|
|
3215
|
+
"page-requests",
|
|
3216
|
+
input.requestId
|
|
3217
|
+
);
|
|
3218
|
+
const contentPath = path.join(requestDir, "content.md");
|
|
3219
|
+
let contentMd = "";
|
|
3220
|
+
try {
|
|
3221
|
+
contentMd = await fs.readFile(contentPath, "utf-8");
|
|
3222
|
+
} catch {
|
|
3223
|
+
contentMd = "";
|
|
3224
|
+
}
|
|
3225
|
+
const nextMd = syncPluginManifestInContentMd(
|
|
3226
|
+
contentMd,
|
|
3227
|
+
input.selectedPlugins
|
|
3228
|
+
);
|
|
3229
|
+
await writePageRequestMarkdown(projectRoot, input.requestId, {
|
|
3230
|
+
contentMd: nextMd,
|
|
3231
|
+
designMd: await fs.readFile(path.join(requestDir, "design.md"), "utf-8").catch(() => "")
|
|
3232
|
+
});
|
|
3233
|
+
await writePageRequest(projectRoot, {
|
|
3234
|
+
...request,
|
|
3235
|
+
selectedPlugins: input.selectedPlugins,
|
|
3236
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3237
|
+
});
|
|
3238
|
+
return { ok: true, contentMd: nextMd };
|
|
3239
|
+
}
|
|
3240
|
+
);
|
|
3241
|
+
const writePluginSourceAction = makeJayQuery("aiditor.writePluginSource").withServices(DEV_SERVER_SERVICE).withHandler(async (input) => {
|
|
3242
|
+
const projectRoot = process.cwd();
|
|
3243
|
+
await writePluginSource(projectRoot, input);
|
|
3244
|
+
return { ok: true };
|
|
3245
|
+
});
|
|
595
3246
|
setActionCallerOptions({ timeout: 12e4 });
|
|
596
3247
|
const page = makeJayStackComponent().withProps();
|
|
597
3248
|
const aiditorShell = makeJayStackComponent().withProps().withSlowlyRender(async () => {
|
|
@@ -608,10 +3259,27 @@ const aiditorShell = makeJayStackComponent().withProps().withSlowlyRender(async
|
|
|
608
3259
|
export {
|
|
609
3260
|
page as aiditorPage,
|
|
610
3261
|
aiditorShell,
|
|
3262
|
+
checkAddPageRouteAction,
|
|
3263
|
+
checkPageAddPageBriefAction,
|
|
3264
|
+
checkPluginUpdateAction,
|
|
3265
|
+
ensureProjectPluginAction,
|
|
3266
|
+
generateAddPageBriefFromImageAction,
|
|
611
3267
|
getAiditorBootstrap,
|
|
612
3268
|
getPageParamsAction,
|
|
3269
|
+
getPluginSetupStatusAction,
|
|
613
3270
|
getProjectInfoAction,
|
|
614
3271
|
listFreezesAction,
|
|
3272
|
+
listJayPluginsAction,
|
|
3273
|
+
listPluginContractsAction,
|
|
3274
|
+
openAddPageFromBriefAction,
|
|
615
3275
|
readFileAction,
|
|
616
|
-
|
|
3276
|
+
reclassifyAddPageAssetAction,
|
|
3277
|
+
rerunPluginSetupAction,
|
|
3278
|
+
saveAddPageDraftAction,
|
|
3279
|
+
startAddPageRequestAction,
|
|
3280
|
+
submitAddPageAction,
|
|
3281
|
+
submitTaskAction,
|
|
3282
|
+
syncAddPagePluginManifestAction,
|
|
3283
|
+
uploadAddPageAssetAction,
|
|
3284
|
+
writePluginSourceAction
|
|
617
3285
|
};
|