vibespot 0.5.2 → 0.7.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/dist/index.js +779 -180
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/ui/chat.js +152 -18
- package/ui/dashboard.js +189 -0
- package/ui/index.html +30 -9
- package/ui/settings.js +10 -7
- package/ui/setup.js +116 -11
- package/ui/styles.css +208 -75
package/dist/index.js
CHANGED
|
@@ -1096,60 +1096,49 @@ import { join as join6, basename as basename2 } from "path";
|
|
|
1096
1096
|
import { readdirSync as readdirSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1097
1097
|
|
|
1098
1098
|
// src/ai/prompts.ts
|
|
1099
|
-
|
|
1099
|
+
var guideCache = /* @__PURE__ */ new Map();
|
|
1100
|
+
function cachedAsset(name) {
|
|
1101
|
+
let val = guideCache.get(name);
|
|
1102
|
+
if (val !== void 0) return val;
|
|
1100
1103
|
try {
|
|
1101
|
-
|
|
1104
|
+
val = readFile(resolveAsset(name));
|
|
1102
1105
|
} catch {
|
|
1103
|
-
|
|
1106
|
+
val = "";
|
|
1104
1107
|
}
|
|
1108
|
+
guideCache.set(name, val);
|
|
1109
|
+
return val;
|
|
1110
|
+
}
|
|
1111
|
+
function getConversionGuide() {
|
|
1112
|
+
return cachedAsset("conversion-guide.md") || "Conversion guide not found. Using built-in rules.";
|
|
1105
1113
|
}
|
|
1106
1114
|
function getDesignGuide() {
|
|
1107
|
-
|
|
1108
|
-
return readFile(resolveAsset("design-guide.md"));
|
|
1109
|
-
} catch {
|
|
1110
|
-
return "";
|
|
1111
|
-
}
|
|
1115
|
+
return cachedAsset("design-guide.md");
|
|
1112
1116
|
}
|
|
1113
1117
|
function getContentGuide() {
|
|
1114
|
-
|
|
1115
|
-
return readFile(resolveAsset("content-guide.md"));
|
|
1116
|
-
} catch {
|
|
1117
|
-
return "";
|
|
1118
|
-
}
|
|
1118
|
+
return cachedAsset("content-guide.md");
|
|
1119
1119
|
}
|
|
1120
1120
|
function getHubspotRules() {
|
|
1121
|
-
|
|
1122
|
-
return readFile(resolveAsset("hubspot-rules.md"));
|
|
1123
|
-
} catch {
|
|
1124
|
-
return "";
|
|
1125
|
-
}
|
|
1121
|
+
return cachedAsset("hubspot-rules.md");
|
|
1126
1122
|
}
|
|
1127
1123
|
function getHumanifyGuide() {
|
|
1128
|
-
|
|
1129
|
-
return readFile(resolveAsset("humanify-guide.md"));
|
|
1130
|
-
} catch {
|
|
1131
|
-
return "";
|
|
1132
|
-
}
|
|
1124
|
+
return cachedAsset("humanify-guide.md");
|
|
1133
1125
|
}
|
|
1134
1126
|
function getPageTypeGuide(pageType) {
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
} catch {
|
|
1151
|
-
return "";
|
|
1152
|
-
}
|
|
1127
|
+
const fullGuide = cachedAsset("page-types.md");
|
|
1128
|
+
if (!fullGuide) return "";
|
|
1129
|
+
const sectionHeaders = {
|
|
1130
|
+
landing_page: "## Landing Page",
|
|
1131
|
+
blog_post: "## Blog Post",
|
|
1132
|
+
website_page: "## Website Page",
|
|
1133
|
+
module_only: "## Module Only"
|
|
1134
|
+
};
|
|
1135
|
+
const header = sectionHeaders[pageType];
|
|
1136
|
+
if (!header) return "";
|
|
1137
|
+
const startIdx = fullGuide.indexOf(header);
|
|
1138
|
+
if (startIdx < 0) return "";
|
|
1139
|
+
const afterHeader = fullGuide.indexOf("\n## ", startIdx + header.length);
|
|
1140
|
+
const section = afterHeader >= 0 ? fullGuide.slice(startIdx, afterHeader).trim() : fullGuide.slice(startIdx).trim();
|
|
1141
|
+
return section;
|
|
1153
1142
|
}
|
|
1154
1143
|
function buildSystemPrompt(conversionGuide) {
|
|
1155
1144
|
return `You are a HubSpot CMS expert converting React/Tailwind pages to native HubSpot modules.
|
|
@@ -1654,7 +1643,7 @@ import { join as join7, basename as basename3 } from "path";
|
|
|
1654
1643
|
import { readdirSync as readdirSync5 } from "fs";
|
|
1655
1644
|
var ClaudeAPIEngine = class {
|
|
1656
1645
|
client;
|
|
1657
|
-
model = "claude-sonnet-4-
|
|
1646
|
+
model = "claude-sonnet-4-6";
|
|
1658
1647
|
constructor(apiKey) {
|
|
1659
1648
|
this.client = new Anthropic({
|
|
1660
1649
|
apiKey: apiKey || process.env.ANTHROPIC_API_KEY
|
|
@@ -3096,15 +3085,15 @@ import chalk2 from "chalk";
|
|
|
3096
3085
|
|
|
3097
3086
|
// src/server/server.ts
|
|
3098
3087
|
import { createServer } from "http";
|
|
3099
|
-
import { readFileSync as readFileSync5, existsSync as existsSync5, readdirSync as readdirSync11, appendFileSync, rmSync as rmSync5, renameSync as
|
|
3088
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5, readdirSync as readdirSync11, appendFileSync, rmSync as rmSync5, renameSync as renameSync3 } from "fs";
|
|
3100
3089
|
import { join as join15, extname as extname2, basename as basename7 } from "path";
|
|
3101
3090
|
import { homedir as homedir4 } from "os";
|
|
3102
3091
|
import { execSync as execSync4 } from "child_process";
|
|
3103
3092
|
import { WebSocketServer } from "ws";
|
|
3104
3093
|
|
|
3105
3094
|
// src/server/session.ts
|
|
3106
|
-
import { readFileSync as readFileSync4, readdirSync as readdirSync10, existsSync as existsSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, rmSync as rmSync4 } from "fs";
|
|
3107
|
-
import { join as join14 } from "path";
|
|
3095
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync10, existsSync as existsSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, rmSync as rmSync4, renameSync as renameSync2 } from "fs";
|
|
3096
|
+
import { join as join14, dirname as dirname2 } from "path";
|
|
3108
3097
|
import { homedir as homedir3 } from "os";
|
|
3109
3098
|
|
|
3110
3099
|
// src/server/project-git.ts
|
|
@@ -3158,6 +3147,29 @@ function commitThemeState(themePath, message) {
|
|
|
3158
3147
|
const hashResult = run("git rev-parse --short HEAD", { cwd: themePath });
|
|
3159
3148
|
return hashResult.success ? hashResult.stdout : null;
|
|
3160
3149
|
}
|
|
3150
|
+
function commitTemplateState(themePath, templateId, message, filePaths) {
|
|
3151
|
+
if (!isGitAvailable()) return null;
|
|
3152
|
+
if (!existsSync3(join13(themePath, ".git"))) return null;
|
|
3153
|
+
for (const fp of filePaths) {
|
|
3154
|
+
const fullPath = join13(themePath, fp);
|
|
3155
|
+
if (existsSync3(fullPath)) {
|
|
3156
|
+
run(`git add "${fp}"`, { cwd: themePath });
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
const diff = run("git diff --cached --quiet", { cwd: themePath });
|
|
3160
|
+
if (diff.success) return null;
|
|
3161
|
+
const prefix = `[${templateId}] `;
|
|
3162
|
+
const maxMsg = 72 - prefix.length;
|
|
3163
|
+
const truncated = message.length > maxMsg ? message.slice(0, maxMsg - 3) + "..." : message;
|
|
3164
|
+
const fullMessage = prefix + truncated;
|
|
3165
|
+
const commitResult = run(`git commit -m "${fullMessage.replace(/"/g, '\\"')}"`, { cwd: themePath });
|
|
3166
|
+
if (!commitResult.success) {
|
|
3167
|
+
console.warn(`[project-git] template commit failed: ${commitResult.stderr}`);
|
|
3168
|
+
return null;
|
|
3169
|
+
}
|
|
3170
|
+
const hashResult = run("git rev-parse --short HEAD", { cwd: themePath });
|
|
3171
|
+
return hashResult.success ? hashResult.stdout : null;
|
|
3172
|
+
}
|
|
3161
3173
|
function getHistory(themePath, limit = 50) {
|
|
3162
3174
|
if (!isGitAvailable()) return [];
|
|
3163
3175
|
if (!existsSync3(join13(themePath, ".git"))) return [];
|
|
@@ -3181,6 +3193,30 @@ function getHistory(themePath, limit = 50) {
|
|
|
3181
3193
|
}
|
|
3182
3194
|
return commits;
|
|
3183
3195
|
}
|
|
3196
|
+
function getTemplateHistory(themePath, templateId, limit = 50) {
|
|
3197
|
+
if (!isGitAvailable()) return [];
|
|
3198
|
+
if (!existsSync3(join13(themePath, ".git"))) return [];
|
|
3199
|
+
const escapedId = templateId.replace(/[[\]\\]/g, "\\$&");
|
|
3200
|
+
const result = run(
|
|
3201
|
+
`git log --grep="\\[${escapedId}\\]" --pretty=format:"%h|%H|%s|%at" -n ${limit}`,
|
|
3202
|
+
{ cwd: themePath }
|
|
3203
|
+
);
|
|
3204
|
+
if (!result.success || !result.stdout.trim()) return [];
|
|
3205
|
+
const commits = [];
|
|
3206
|
+
for (const line of result.stdout.split("\n")) {
|
|
3207
|
+
const parts = line.split("|");
|
|
3208
|
+
if (parts.length < 4) continue;
|
|
3209
|
+
const timestamp = parseInt(parts[3], 10) * 1e3;
|
|
3210
|
+
commits.push({
|
|
3211
|
+
hash: parts[0],
|
|
3212
|
+
fullHash: parts[1],
|
|
3213
|
+
message: parts[2],
|
|
3214
|
+
timestamp,
|
|
3215
|
+
date: new Date(timestamp).toISOString()
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
return commits;
|
|
3219
|
+
}
|
|
3184
3220
|
function rollbackToCommit(themePath, commitHash) {
|
|
3185
3221
|
if (!isGitAvailable()) return { success: false, error: "Git not available" };
|
|
3186
3222
|
if (!existsSync3(join13(themePath, ".git"))) return { success: false, error: "Not a git repo" };
|
|
@@ -3198,9 +3234,91 @@ function rollbackToCommit(themePath, commitHash) {
|
|
|
3198
3234
|
run(`git commit -m "${rollbackMsg.replace(/"/g, '\\"')}"`, { cwd: themePath });
|
|
3199
3235
|
return { success: true };
|
|
3200
3236
|
}
|
|
3237
|
+
function rollbackTemplateToCommit(themePath, templateId, commitHash, filePaths) {
|
|
3238
|
+
if (!isGitAvailable()) return { success: false, error: "Git not available" };
|
|
3239
|
+
if (!existsSync3(join13(themePath, ".git"))) return { success: false, error: "Not a git repo" };
|
|
3240
|
+
const verify = run(`git cat-file -t ${commitHash}`, { cwd: themePath });
|
|
3241
|
+
if (!verify.success || verify.stdout.trim() !== "commit") {
|
|
3242
|
+
return { success: false, error: `Commit ${commitHash} not found` };
|
|
3243
|
+
}
|
|
3244
|
+
const msgResult = run(`git log --format="%s" -1 ${commitHash}`, { cwd: themePath });
|
|
3245
|
+
const origMessage = msgResult.success ? msgResult.stdout : commitHash;
|
|
3246
|
+
let restored = 0;
|
|
3247
|
+
for (const fp of filePaths) {
|
|
3248
|
+
const checkout = run(`git checkout ${commitHash} -- "${fp}"`, { cwd: themePath });
|
|
3249
|
+
if (checkout.success) restored++;
|
|
3250
|
+
}
|
|
3251
|
+
if (restored === 0) {
|
|
3252
|
+
return { success: false, error: "No files could be restored from that commit" };
|
|
3253
|
+
}
|
|
3254
|
+
run("git add -A", { cwd: themePath });
|
|
3255
|
+
const prefix = `[${templateId}] `;
|
|
3256
|
+
const rollbackMsg = `${prefix}Rollback to: ${origMessage}`.slice(0, 72);
|
|
3257
|
+
run(`git commit -m "${rollbackMsg.replace(/"/g, '\\"')}"`, { cwd: themePath });
|
|
3258
|
+
return { success: true };
|
|
3259
|
+
}
|
|
3201
3260
|
|
|
3202
3261
|
// src/server/session.ts
|
|
3203
3262
|
var SESSIONS_DIR = join14(homedir3(), ".vibespot", "sessions");
|
|
3263
|
+
var INDEX_PATH = join14(SESSIONS_DIR, "_index.json");
|
|
3264
|
+
function readIndex() {
|
|
3265
|
+
try {
|
|
3266
|
+
if (!existsSync4(INDEX_PATH)) return rebuildIndex();
|
|
3267
|
+
return JSON.parse(readFileSync4(INDEX_PATH, "utf-8"));
|
|
3268
|
+
} catch {
|
|
3269
|
+
return rebuildIndex();
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
function writeIndex(entries) {
|
|
3273
|
+
try {
|
|
3274
|
+
mkdirSync3(SESSIONS_DIR, { recursive: true });
|
|
3275
|
+
writeFileSync4(INDEX_PATH, JSON.stringify(entries), "utf-8");
|
|
3276
|
+
} catch {
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
function rebuildIndex() {
|
|
3280
|
+
if (!existsSync4(SESSIONS_DIR)) return [];
|
|
3281
|
+
const entries = [];
|
|
3282
|
+
for (const f of readdirSync10(SESSIONS_DIR).filter((f2) => f2.endsWith(".json") && f2 !== "_index.json")) {
|
|
3283
|
+
try {
|
|
3284
|
+
const data = JSON.parse(readFileSync4(join14(SESSIONS_DIR, f), "utf-8"));
|
|
3285
|
+
const templates = data.templates || [];
|
|
3286
|
+
entries.push({
|
|
3287
|
+
id: data.id,
|
|
3288
|
+
themeName: data.themeName,
|
|
3289
|
+
updatedAt: data.updatedAt,
|
|
3290
|
+
moduleCount: templates.reduce((n, t) => n + (t.modules?.length || 0), 0),
|
|
3291
|
+
templateCount: templates.length
|
|
3292
|
+
});
|
|
3293
|
+
} catch {
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
writeIndex(entries);
|
|
3297
|
+
return entries;
|
|
3298
|
+
}
|
|
3299
|
+
function upsertIndex(session) {
|
|
3300
|
+
const entries = readIndex();
|
|
3301
|
+
const templates = session.templates || [];
|
|
3302
|
+
const entry = {
|
|
3303
|
+
id: session.id,
|
|
3304
|
+
themeName: session.themeName,
|
|
3305
|
+
updatedAt: session.updatedAt,
|
|
3306
|
+
moduleCount: templates.reduce((n, t) => n + (t.modules?.length || 0), 0),
|
|
3307
|
+
templateCount: templates.length
|
|
3308
|
+
};
|
|
3309
|
+
const idx = entries.findIndex((e) => e.id === session.id);
|
|
3310
|
+
if (idx >= 0) entries[idx] = entry;
|
|
3311
|
+
else entries.push(entry);
|
|
3312
|
+
writeIndex(entries);
|
|
3313
|
+
}
|
|
3314
|
+
function removeFromIndex(sessionId) {
|
|
3315
|
+
const entries = readIndex().filter((e) => e.id !== sessionId);
|
|
3316
|
+
writeIndex(entries);
|
|
3317
|
+
}
|
|
3318
|
+
function removeFromIndexByTheme(themeName) {
|
|
3319
|
+
const entries = readIndex().filter((e) => e.themeName !== themeName);
|
|
3320
|
+
writeIndex(entries);
|
|
3321
|
+
}
|
|
3204
3322
|
var activeSession = null;
|
|
3205
3323
|
function createSession(themePath, themeName) {
|
|
3206
3324
|
const session = {
|
|
@@ -3282,6 +3400,14 @@ function addTemplate(pageType, label) {
|
|
|
3282
3400
|
activeSession.updatedAt = Date.now();
|
|
3283
3401
|
return entry;
|
|
3284
3402
|
}
|
|
3403
|
+
function renameTemplate(templateId, newLabel) {
|
|
3404
|
+
if (!activeSession) return false;
|
|
3405
|
+
const tpl = activeSession.templates.find((t) => t.id === templateId);
|
|
3406
|
+
if (!tpl) return false;
|
|
3407
|
+
tpl.label = newLabel;
|
|
3408
|
+
activeSession.updatedAt = Date.now();
|
|
3409
|
+
return true;
|
|
3410
|
+
}
|
|
3285
3411
|
function removeTemplate(templateId) {
|
|
3286
3412
|
if (!activeSession) return false;
|
|
3287
3413
|
const idx = activeSession.templates.findIndex((t) => t.id === templateId);
|
|
@@ -3482,6 +3608,7 @@ function saveSession() {
|
|
|
3482
3608
|
mkdirSync3(SESSIONS_DIR, { recursive: true });
|
|
3483
3609
|
const filePath = join14(SESSIONS_DIR, `${activeSession.id}.json`);
|
|
3484
3610
|
writeFileSync4(filePath, JSON.stringify(activeSession, null, 2), "utf-8");
|
|
3611
|
+
upsertIndex(activeSession);
|
|
3485
3612
|
}
|
|
3486
3613
|
function loadSession(sessionId) {
|
|
3487
3614
|
const filePath = join14(SESSIONS_DIR, sessionId + ".json");
|
|
@@ -3499,21 +3626,7 @@ function loadSession(sessionId) {
|
|
|
3499
3626
|
}
|
|
3500
3627
|
function listSessions() {
|
|
3501
3628
|
if (!existsSync4(SESSIONS_DIR)) return [];
|
|
3502
|
-
return
|
|
3503
|
-
try {
|
|
3504
|
-
const data = JSON.parse(readFileSync4(join14(SESSIONS_DIR, f), "utf-8"));
|
|
3505
|
-
const templates = data.templates || [];
|
|
3506
|
-
return {
|
|
3507
|
-
id: data.id,
|
|
3508
|
-
themeName: data.themeName,
|
|
3509
|
-
updatedAt: data.updatedAt,
|
|
3510
|
-
moduleCount: templates.reduce((n, t) => n + (t.modules?.length || 0), 0),
|
|
3511
|
-
templateCount: templates.length
|
|
3512
|
-
};
|
|
3513
|
-
} catch {
|
|
3514
|
-
return null;
|
|
3515
|
-
}
|
|
3516
|
-
}).filter(Boolean);
|
|
3629
|
+
return readIndex();
|
|
3517
3630
|
}
|
|
3518
3631
|
function deleteSession(sessionId, deleteFiles = false) {
|
|
3519
3632
|
const filePath = join14(SESSIONS_DIR, sessionId + ".json");
|
|
@@ -3539,7 +3652,7 @@ function deleteSession(sessionId, deleteFiles = false) {
|
|
|
3539
3652
|
} catch {
|
|
3540
3653
|
}
|
|
3541
3654
|
if (themeName && existsSync4(SESSIONS_DIR)) {
|
|
3542
|
-
for (const f of readdirSync10(SESSIONS_DIR).filter((f2) => f2.endsWith(".json"))) {
|
|
3655
|
+
for (const f of readdirSync10(SESSIONS_DIR).filter((f2) => f2.endsWith(".json") && f2 !== "_index.json")) {
|
|
3543
3656
|
try {
|
|
3544
3657
|
const data = JSON.parse(readFileSync4(join14(SESSIONS_DIR, f), "utf-8"));
|
|
3545
3658
|
if (data.themeName === themeName) {
|
|
@@ -3548,11 +3661,79 @@ function deleteSession(sessionId, deleteFiles = false) {
|
|
|
3548
3661
|
} catch {
|
|
3549
3662
|
}
|
|
3550
3663
|
}
|
|
3664
|
+
removeFromIndexByTheme(themeName);
|
|
3665
|
+
} else {
|
|
3666
|
+
removeFromIndex(sessionId);
|
|
3551
3667
|
}
|
|
3552
3668
|
if (activeSession?.id === sessionId) {
|
|
3553
3669
|
activeSession = null;
|
|
3554
3670
|
}
|
|
3555
3671
|
}
|
|
3672
|
+
function renameSession(sessionId, newName) {
|
|
3673
|
+
const filePath = join14(SESSIONS_DIR, sessionId + ".json");
|
|
3674
|
+
if (!existsSync4(filePath)) return { ok: false, error: "Session not found" };
|
|
3675
|
+
let session;
|
|
3676
|
+
try {
|
|
3677
|
+
session = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
3678
|
+
} catch {
|
|
3679
|
+
return { ok: false, error: "Failed to read session" };
|
|
3680
|
+
}
|
|
3681
|
+
const oldName = session.themeName;
|
|
3682
|
+
if (oldName === newName) return { ok: true };
|
|
3683
|
+
const oldPath = session.themePath;
|
|
3684
|
+
const newPath = join14(dirname2(oldPath), newName);
|
|
3685
|
+
if (existsSync4(oldPath)) {
|
|
3686
|
+
if (existsSync4(newPath)) return { ok: false, error: "A project with that name already exists" };
|
|
3687
|
+
try {
|
|
3688
|
+
renameSync2(oldPath, newPath);
|
|
3689
|
+
} catch (err) {
|
|
3690
|
+
return { ok: false, error: `Failed to rename folder: ${err instanceof Error ? err.message : String(err)}` };
|
|
3691
|
+
}
|
|
3692
|
+
const cssOld = join14(newPath, "css", `${oldName}-theme.css`);
|
|
3693
|
+
const cssNew = join14(newPath, "css", `${newName}-theme.css`);
|
|
3694
|
+
if (existsSync4(cssOld)) try {
|
|
3695
|
+
renameSync2(cssOld, cssNew);
|
|
3696
|
+
} catch {
|
|
3697
|
+
}
|
|
3698
|
+
const jsOld = join14(newPath, "js", `${oldName}-animations.js`);
|
|
3699
|
+
const jsNew = join14(newPath, "js", `${newName}-animations.js`);
|
|
3700
|
+
if (existsSync4(jsOld)) try {
|
|
3701
|
+
renameSync2(jsOld, jsNew);
|
|
3702
|
+
} catch {
|
|
3703
|
+
}
|
|
3704
|
+
const themeJsonPath = join14(newPath, "theme.json");
|
|
3705
|
+
if (existsSync4(themeJsonPath)) {
|
|
3706
|
+
try {
|
|
3707
|
+
const themeData = JSON.parse(readFileSync4(themeJsonPath, "utf-8"));
|
|
3708
|
+
themeData.label = newName;
|
|
3709
|
+
themeData.name = newName;
|
|
3710
|
+
writeFileSync4(themeJsonPath, JSON.stringify(themeData, null, 2), "utf-8");
|
|
3711
|
+
} catch {
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
if (existsSync4(SESSIONS_DIR)) {
|
|
3716
|
+
for (const f of readdirSync10(SESSIONS_DIR).filter((f2) => f2.endsWith(".json") && f2 !== "_index.json")) {
|
|
3717
|
+
try {
|
|
3718
|
+
const data = JSON.parse(readFileSync4(join14(SESSIONS_DIR, f), "utf-8"));
|
|
3719
|
+
if (data.themeName === oldName) {
|
|
3720
|
+
data.themeName = newName;
|
|
3721
|
+
data.themePath = newPath;
|
|
3722
|
+
data.updatedAt = Date.now();
|
|
3723
|
+
writeFileSync4(join14(SESSIONS_DIR, f), JSON.stringify(data, null, 2), "utf-8");
|
|
3724
|
+
}
|
|
3725
|
+
} catch {
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
if (activeSession && activeSession.themeName === oldName) {
|
|
3730
|
+
activeSession.themeName = newName;
|
|
3731
|
+
activeSession.themePath = newPath;
|
|
3732
|
+
activeSession.updatedAt = Date.now();
|
|
3733
|
+
}
|
|
3734
|
+
rebuildIndex();
|
|
3735
|
+
return { ok: true };
|
|
3736
|
+
}
|
|
3556
3737
|
function writeModulesToDisk() {
|
|
3557
3738
|
if (!activeSession) return;
|
|
3558
3739
|
const themePath = activeSession.themePath;
|
|
@@ -3663,6 +3844,37 @@ function reloadModulesFromDisk() {
|
|
|
3663
3844
|
activeSession.updatedAt = Date.now();
|
|
3664
3845
|
syncFlatFieldsToTemplate();
|
|
3665
3846
|
}
|
|
3847
|
+
function reloadActiveTemplateFromDisk() {
|
|
3848
|
+
if (!activeSession) return;
|
|
3849
|
+
const tpl = getActiveTemplate();
|
|
3850
|
+
if (!tpl) return;
|
|
3851
|
+
const themePath = activeSession.themePath;
|
|
3852
|
+
const modulesDir = join14(themePath, "modules");
|
|
3853
|
+
tpl.modules = [];
|
|
3854
|
+
for (const name of tpl.moduleOrder) {
|
|
3855
|
+
const modDir = join14(modulesDir, `${name}.module`);
|
|
3856
|
+
if (!existsSync4(modDir)) continue;
|
|
3857
|
+
const mod = {
|
|
3858
|
+
moduleName: name,
|
|
3859
|
+
fieldsJson: safeRead(join14(modDir, "fields.json")),
|
|
3860
|
+
metaJson: safeRead(join14(modDir, "meta.json")),
|
|
3861
|
+
moduleHtml: safeRead(join14(modDir, "module.html")),
|
|
3862
|
+
moduleCss: safeRead(join14(modDir, "module.css")),
|
|
3863
|
+
moduleJs: safeRead(join14(modDir, "module.js")) || void 0
|
|
3864
|
+
};
|
|
3865
|
+
if (mod.fieldsJson && mod.moduleHtml) {
|
|
3866
|
+
tpl.modules.push(mod);
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
if (tpl.templateFile) {
|
|
3870
|
+
const tplPath = join14(themePath, tpl.templateFile);
|
|
3871
|
+
if (existsSync4(tplPath)) {
|
|
3872
|
+
tpl.template = safeRead(tplPath);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
syncFlatFieldsFromTemplate(tpl);
|
|
3876
|
+
activeSession.updatedAt = Date.now();
|
|
3877
|
+
}
|
|
3666
3878
|
function patchBaseTemplate() {
|
|
3667
3879
|
if (!activeSession) return;
|
|
3668
3880
|
const basePath = join14(activeSession.themePath, "templates", "layouts", "base.html");
|
|
@@ -4457,51 +4669,60 @@ ${conversionGuide}`;
|
|
|
4457
4669
|
async function handleGenerateStream(userMessage, onChunk, onStatus) {
|
|
4458
4670
|
const session = getSession();
|
|
4459
4671
|
if (!session) throw new Error("No active session");
|
|
4460
|
-
const
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
apiKey
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
apiKey,
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4672
|
+
const capturedSessionId = session.id;
|
|
4673
|
+
generatingSessionId = capturedSessionId;
|
|
4674
|
+
try {
|
|
4675
|
+
const config = loadConfig();
|
|
4676
|
+
const engine = config.aiEngine || detectDefaultEngine();
|
|
4677
|
+
switch (engine) {
|
|
4678
|
+
case "anthropic-api":
|
|
4679
|
+
case "api": {
|
|
4680
|
+
const apiKey = getApiKeyForEngine("anthropic-api", config);
|
|
4681
|
+
if (!apiKey) throw new Error("Anthropic API key not configured. Open Settings to add one.");
|
|
4682
|
+
await streamWithAnthropicAPI(
|
|
4683
|
+
userMessage,
|
|
4684
|
+
apiKey,
|
|
4685
|
+
session.themeName,
|
|
4686
|
+
config.anthropicApiModel || "claude-sonnet-4-6",
|
|
4687
|
+
onChunk,
|
|
4688
|
+
onStatus
|
|
4689
|
+
);
|
|
4690
|
+
break;
|
|
4691
|
+
}
|
|
4692
|
+
case "openai-api": {
|
|
4693
|
+
const apiKey = getApiKeyForEngine("openai-api", config);
|
|
4694
|
+
if (!apiKey) throw new Error("OpenAI API key not configured. Open Settings to add one.");
|
|
4695
|
+
await streamWithOpenAIAPI(
|
|
4696
|
+
userMessage,
|
|
4697
|
+
apiKey,
|
|
4698
|
+
session.themeName,
|
|
4699
|
+
config.openaiApiModel || "gpt-4o",
|
|
4700
|
+
onChunk,
|
|
4701
|
+
onStatus
|
|
4702
|
+
);
|
|
4703
|
+
break;
|
|
4704
|
+
}
|
|
4705
|
+
case "gemini-api": {
|
|
4706
|
+
const apiKey = getApiKeyForEngine("gemini-api", config);
|
|
4707
|
+
if (!apiKey) throw new Error("Gemini API key not configured. Open Settings to add one.");
|
|
4708
|
+
await streamWithGeminiAPI(userMessage, apiKey, session.themeName, onChunk, onStatus);
|
|
4709
|
+
break;
|
|
4710
|
+
}
|
|
4711
|
+
case "claude-code":
|
|
4712
|
+
await generateWithClaudeCode(userMessage, session.themeName, onChunk, onStatus);
|
|
4713
|
+
break;
|
|
4714
|
+
case "gemini-cli":
|
|
4715
|
+
await generateWithCLI("gemini", userMessage, session.themeName, onChunk, onStatus);
|
|
4716
|
+
break;
|
|
4717
|
+
case "codex-cli":
|
|
4718
|
+
await generateWithCLI("codex", userMessage, session.themeName, onChunk, onStatus);
|
|
4719
|
+
break;
|
|
4720
|
+
default:
|
|
4721
|
+
throw new Error(`Unknown AI engine: ${engine}. Open Settings to configure one.`);
|
|
4493
4722
|
}
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
case "gemini-cli":
|
|
4498
|
-
await generateWithCLI("gemini", userMessage, session.themeName, onChunk, onStatus);
|
|
4499
|
-
break;
|
|
4500
|
-
case "codex-cli":
|
|
4501
|
-
await generateWithCLI("codex", userMessage, session.themeName, onChunk, onStatus);
|
|
4502
|
-
break;
|
|
4503
|
-
default:
|
|
4504
|
-
throw new Error(`Unknown AI engine: ${engine}. Open Settings to configure one.`);
|
|
4723
|
+
} finally {
|
|
4724
|
+
generatingSessionId = null;
|
|
4725
|
+
parseWarningCallback = null;
|
|
4505
4726
|
}
|
|
4506
4727
|
}
|
|
4507
4728
|
function detectDefaultEngine() {
|
|
@@ -4607,33 +4828,73 @@ function setParseWarningCallback(cb) {
|
|
|
4607
4828
|
parseWarningCallback = cb;
|
|
4608
4829
|
}
|
|
4609
4830
|
function finishResponse(fullResponse) {
|
|
4831
|
+
if (generatingSessionId) {
|
|
4832
|
+
const current = getSession();
|
|
4833
|
+
if (!current || current.id !== generatingSessionId) {
|
|
4834
|
+
console.warn("[ai-handler] Session changed during generation \u2014 discarding AI output");
|
|
4835
|
+
return;
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4610
4838
|
addMessage("assistant", fullResponse);
|
|
4611
4839
|
parseAndApplyModules(fullResponse);
|
|
4612
4840
|
saveSession();
|
|
4613
4841
|
}
|
|
4614
|
-
|
|
4842
|
+
var generatingSessionId = null;
|
|
4843
|
+
function isGenerating() {
|
|
4844
|
+
return generatingSessionId !== null;
|
|
4845
|
+
}
|
|
4846
|
+
var RATE_LIMIT_DELAYS = [10, 20, 40, 60, 120];
|
|
4847
|
+
async function streamWithAnthropicAPI(userMessage, apiKey, themeName, model, onChunk, onStatus) {
|
|
4615
4848
|
const client = new Anthropic2({ apiKey });
|
|
4616
4849
|
const conversionGuide = getConversionGuide();
|
|
4617
4850
|
const session = getSession();
|
|
4618
4851
|
const editMode = session.modules.length > 0;
|
|
4619
4852
|
const messages = buildMessagesWithContext(userMessage);
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4853
|
+
const systemPrompt = buildVibeSystemPrompt(conversionGuide, themeName, editMode, getPromptContext().pageType, getPromptContext().brandAssets);
|
|
4854
|
+
for (let attempt = 0; ; attempt++) {
|
|
4855
|
+
try {
|
|
4856
|
+
let fullResponse = "";
|
|
4857
|
+
let statusIndex = 0;
|
|
4858
|
+
const sendStatus = onStatus || (() => {
|
|
4859
|
+
});
|
|
4860
|
+
sendStatus(CLI_STATUS_MESSAGES[0]);
|
|
4861
|
+
const heartbeat = setInterval(() => {
|
|
4862
|
+
statusIndex++;
|
|
4863
|
+
sendStatus(CLI_STATUS_MESSAGES[Math.min(statusIndex, CLI_STATUS_MESSAGES.length - 1)]);
|
|
4864
|
+
}, 6e3);
|
|
4865
|
+
try {
|
|
4866
|
+
const stream = client.messages.stream({
|
|
4867
|
+
model,
|
|
4868
|
+
max_tokens: 48e3,
|
|
4869
|
+
system: systemPrompt,
|
|
4870
|
+
messages
|
|
4871
|
+
});
|
|
4872
|
+
for await (const event of stream) {
|
|
4873
|
+
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
4874
|
+
const text3 = event.delta.text;
|
|
4875
|
+
fullResponse += text3;
|
|
4876
|
+
onChunk(text3);
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
} finally {
|
|
4880
|
+
clearInterval(heartbeat);
|
|
4881
|
+
}
|
|
4882
|
+
finishResponse(fullResponse);
|
|
4883
|
+
return;
|
|
4884
|
+
} catch (err) {
|
|
4885
|
+
const status = err.status;
|
|
4886
|
+
const errType = err.error?.type;
|
|
4887
|
+
const is429 = status === 429 || errType === "rate_limit_error" || err instanceof Error && err.message.includes("429");
|
|
4888
|
+
if (!is429 || attempt >= RATE_LIMIT_DELAYS.length) throw err;
|
|
4889
|
+
const wait = RATE_LIMIT_DELAYS[attempt];
|
|
4890
|
+
console.warn(`[ai-handler] Rate limited (429), attempt ${attempt + 1}/${RATE_LIMIT_DELAYS.length} \u2014 waiting ${wait}s`);
|
|
4891
|
+
if (onStatus) onStatus(`Rate limited by Anthropic API \u2014 retrying in ${wait}s...`);
|
|
4892
|
+
await new Promise((r) => setTimeout(r, wait * 1e3));
|
|
4893
|
+
if (onStatus) onStatus("Retrying...");
|
|
4632
4894
|
}
|
|
4633
4895
|
}
|
|
4634
|
-
finishResponse(fullResponse);
|
|
4635
4896
|
}
|
|
4636
|
-
async function streamWithOpenAIAPI(userMessage, apiKey, themeName, model, onChunk) {
|
|
4897
|
+
async function streamWithOpenAIAPI(userMessage, apiKey, themeName, model, onChunk, onStatus) {
|
|
4637
4898
|
const conversionGuide = getConversionGuide();
|
|
4638
4899
|
const editMode = getSession().modules.length > 0;
|
|
4639
4900
|
const messages = buildMessagesWithContext(userMessage);
|
|
@@ -4645,7 +4906,7 @@ async function streamWithOpenAIAPI(userMessage, apiKey, themeName, model, onChun
|
|
|
4645
4906
|
},
|
|
4646
4907
|
body: JSON.stringify({
|
|
4647
4908
|
model,
|
|
4648
|
-
max_tokens:
|
|
4909
|
+
max_tokens: 48e3,
|
|
4649
4910
|
stream: true,
|
|
4650
4911
|
messages: [
|
|
4651
4912
|
{ role: "system", content: buildVibeSystemPrompt(conversionGuide, themeName, editMode, getPromptContext().pageType, getPromptContext().brandAssets) },
|
|
@@ -4657,34 +4918,46 @@ async function streamWithOpenAIAPI(userMessage, apiKey, themeName, model, onChun
|
|
|
4657
4918
|
const err = await response.text();
|
|
4658
4919
|
throw new Error(`OpenAI API error (${response.status}): ${err}`);
|
|
4659
4920
|
}
|
|
4921
|
+
let statusIndex = 0;
|
|
4922
|
+
const sendStatus = onStatus || (() => {
|
|
4923
|
+
});
|
|
4924
|
+
sendStatus(CLI_STATUS_MESSAGES[0]);
|
|
4925
|
+
const heartbeat = setInterval(() => {
|
|
4926
|
+
statusIndex++;
|
|
4927
|
+
sendStatus(CLI_STATUS_MESSAGES[Math.min(statusIndex, CLI_STATUS_MESSAGES.length - 1)]);
|
|
4928
|
+
}, 6e3);
|
|
4660
4929
|
let fullResponse = "";
|
|
4661
4930
|
const reader = response.body.getReader();
|
|
4662
4931
|
const decoder = new TextDecoder();
|
|
4663
4932
|
let buffer = "";
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4933
|
+
try {
|
|
4934
|
+
while (true) {
|
|
4935
|
+
const { done, value } = await reader.read();
|
|
4936
|
+
if (done) break;
|
|
4937
|
+
buffer += decoder.decode(value, { stream: true });
|
|
4938
|
+
const lines = buffer.split("\n");
|
|
4939
|
+
buffer = lines.pop() || "";
|
|
4940
|
+
for (const line of lines) {
|
|
4941
|
+
if (!line.startsWith("data: ")) continue;
|
|
4942
|
+
const data = line.slice(6).trim();
|
|
4943
|
+
if (data === "[DONE]") break;
|
|
4944
|
+
try {
|
|
4945
|
+
const parsed = JSON.parse(data);
|
|
4946
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
4947
|
+
if (delta) {
|
|
4948
|
+
fullResponse += delta;
|
|
4949
|
+
onChunk(delta);
|
|
4950
|
+
}
|
|
4951
|
+
} catch {
|
|
4680
4952
|
}
|
|
4681
|
-
} catch {
|
|
4682
4953
|
}
|
|
4683
4954
|
}
|
|
4955
|
+
} finally {
|
|
4956
|
+
clearInterval(heartbeat);
|
|
4684
4957
|
}
|
|
4685
4958
|
finishResponse(fullResponse);
|
|
4686
4959
|
}
|
|
4687
|
-
async function streamWithGeminiAPI(userMessage, apiKey, themeName, onChunk) {
|
|
4960
|
+
async function streamWithGeminiAPI(userMessage, apiKey, themeName, onChunk, onStatus) {
|
|
4688
4961
|
const conversionGuide = getConversionGuide();
|
|
4689
4962
|
const session = getSession();
|
|
4690
4963
|
const editMode = session.modules.length > 0;
|
|
@@ -4701,7 +4974,7 @@ async function streamWithGeminiAPI(userMessage, apiKey, themeName, onChunk) {
|
|
|
4701
4974
|
---
|
|
4702
4975
|
${stateContext}` : userMessage;
|
|
4703
4976
|
contents.push({ role: "user", parts: [{ text: userContent }] });
|
|
4704
|
-
const model = "gemini-2.
|
|
4977
|
+
const model = "gemini-2.5-flash";
|
|
4705
4978
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${apiKey}`;
|
|
4706
4979
|
const response = await fetch(url, {
|
|
4707
4980
|
method: "POST",
|
|
@@ -4709,36 +4982,48 @@ ${stateContext}` : userMessage;
|
|
|
4709
4982
|
body: JSON.stringify({
|
|
4710
4983
|
systemInstruction: { parts: [{ text: buildVibeSystemPrompt(conversionGuide, themeName, editMode, getPromptContext().pageType, getPromptContext().brandAssets) }] },
|
|
4711
4984
|
contents,
|
|
4712
|
-
generationConfig: { maxOutputTokens:
|
|
4985
|
+
generationConfig: { maxOutputTokens: 48e3 }
|
|
4713
4986
|
})
|
|
4714
4987
|
});
|
|
4715
4988
|
if (!response.ok) {
|
|
4716
4989
|
const err = await response.text();
|
|
4717
4990
|
throw new Error(`Gemini API error (${response.status}): ${err}`);
|
|
4718
4991
|
}
|
|
4992
|
+
let statusIndex = 0;
|
|
4993
|
+
const sendStatus = onStatus || (() => {
|
|
4994
|
+
});
|
|
4995
|
+
sendStatus(CLI_STATUS_MESSAGES[0]);
|
|
4996
|
+
const heartbeat = setInterval(() => {
|
|
4997
|
+
statusIndex++;
|
|
4998
|
+
sendStatus(CLI_STATUS_MESSAGES[Math.min(statusIndex, CLI_STATUS_MESSAGES.length - 1)]);
|
|
4999
|
+
}, 6e3);
|
|
4719
5000
|
let fullResponse = "";
|
|
4720
5001
|
const reader = response.body.getReader();
|
|
4721
5002
|
const decoder = new TextDecoder();
|
|
4722
5003
|
let buffer = "";
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
5004
|
+
try {
|
|
5005
|
+
while (true) {
|
|
5006
|
+
const { done, value } = await reader.read();
|
|
5007
|
+
if (done) break;
|
|
5008
|
+
buffer += decoder.decode(value, { stream: true });
|
|
5009
|
+
const lines = buffer.split("\n");
|
|
5010
|
+
buffer = lines.pop() || "";
|
|
5011
|
+
for (const line of lines) {
|
|
5012
|
+
if (!line.startsWith("data: ")) continue;
|
|
5013
|
+
const data = line.slice(6).trim();
|
|
5014
|
+
try {
|
|
5015
|
+
const parsed = JSON.parse(data);
|
|
5016
|
+
const text3 = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
5017
|
+
if (text3) {
|
|
5018
|
+
fullResponse += text3;
|
|
5019
|
+
onChunk(text3);
|
|
5020
|
+
}
|
|
5021
|
+
} catch {
|
|
4738
5022
|
}
|
|
4739
|
-
} catch {
|
|
4740
5023
|
}
|
|
4741
5024
|
}
|
|
5025
|
+
} finally {
|
|
5026
|
+
clearInterval(heartbeat);
|
|
4742
5027
|
}
|
|
4743
5028
|
finishResponse(fullResponse);
|
|
4744
5029
|
}
|
|
@@ -4883,14 +5168,58 @@ function tryParseJSON(raw) {
|
|
|
4883
5168
|
}
|
|
4884
5169
|
return null;
|
|
4885
5170
|
}
|
|
5171
|
+
function tryRepairTruncatedJSON(raw) {
|
|
5172
|
+
const modulesIdx = raw.indexOf('"modules"');
|
|
5173
|
+
if (modulesIdx === -1) return null;
|
|
5174
|
+
const arrayStart = raw.indexOf("[", modulesIdx);
|
|
5175
|
+
if (arrayStart === -1) return null;
|
|
5176
|
+
let lastCompleteModule = -1;
|
|
5177
|
+
let braceDepth = 0;
|
|
5178
|
+
let inString = false;
|
|
5179
|
+
let escaped = false;
|
|
5180
|
+
for (let i = arrayStart + 1; i < raw.length; i++) {
|
|
5181
|
+
const ch = raw[i];
|
|
5182
|
+
if (escaped) {
|
|
5183
|
+
escaped = false;
|
|
5184
|
+
continue;
|
|
5185
|
+
}
|
|
5186
|
+
if (ch === "\\") {
|
|
5187
|
+
escaped = true;
|
|
5188
|
+
continue;
|
|
5189
|
+
}
|
|
5190
|
+
if (ch === '"') {
|
|
5191
|
+
inString = !inString;
|
|
5192
|
+
continue;
|
|
5193
|
+
}
|
|
5194
|
+
if (inString) continue;
|
|
5195
|
+
if (ch === "{") braceDepth++;
|
|
5196
|
+
if (ch === "}") {
|
|
5197
|
+
braceDepth--;
|
|
5198
|
+
if (braceDepth === 0) {
|
|
5199
|
+
lastCompleteModule = i;
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
}
|
|
5203
|
+
if (lastCompleteModule === -1) return null;
|
|
5204
|
+
const upToLastModule = raw.slice(0, lastCompleteModule + 1);
|
|
5205
|
+
const repaired = upToLastModule + "]}";
|
|
5206
|
+
const jsonStr = repaired.trimStart().startsWith("{") ? repaired : "{" + repaired;
|
|
5207
|
+
return tryParseJSON(jsonStr);
|
|
5208
|
+
}
|
|
4886
5209
|
function parseAndApplyModules(response) {
|
|
4887
5210
|
let modulesApplied = false;
|
|
4888
|
-
const blockPattern = /```vibespot-modules\s*\n([\s\S]*?)```/g;
|
|
5211
|
+
const blockPattern = /```vibespot-modules\s*\n?([\s\S]*?)```/g;
|
|
4889
5212
|
let match;
|
|
4890
5213
|
while ((match = blockPattern.exec(response)) !== null) {
|
|
4891
5214
|
try {
|
|
5215
|
+
console.log("[parse] Found vibespot-modules block, length:", match[1].length);
|
|
5216
|
+
console.log("[parse] Block start:", JSON.stringify(match[1].slice(0, 100)));
|
|
5217
|
+
console.log("[parse] Block end:", JSON.stringify(match[1].slice(-100)));
|
|
4892
5218
|
const data = tryParseJSON(match[1]);
|
|
4893
|
-
if (!data || typeof data !== "object")
|
|
5219
|
+
if (!data || typeof data !== "object") {
|
|
5220
|
+
console.warn("[parse] tryParseJSON returned:", data);
|
|
5221
|
+
throw new Error("Invalid JSON after repair");
|
|
5222
|
+
}
|
|
4894
5223
|
const obj = data;
|
|
4895
5224
|
if (obj.modules && Array.isArray(obj.modules)) {
|
|
4896
5225
|
const modules = obj.modules.map((m) => ({
|
|
@@ -4914,8 +5243,9 @@ function parseAndApplyModules(response) {
|
|
|
4914
5243
|
}
|
|
4915
5244
|
}
|
|
4916
5245
|
if (!modulesApplied) {
|
|
4917
|
-
const jsonPattern = /```(?:json)?\s*\n(
|
|
5246
|
+
const jsonPattern = /```(?:json)?\s*\n([\s\S]*?)```/g;
|
|
4918
5247
|
while ((match = jsonPattern.exec(response)) !== null) {
|
|
5248
|
+
if (!match[1].includes('"modules"')) continue;
|
|
4919
5249
|
try {
|
|
4920
5250
|
const data = tryParseJSON(match[1]);
|
|
4921
5251
|
if (!data || typeof data !== "object") throw new Error("Invalid JSON after repair");
|
|
@@ -4942,6 +5272,44 @@ function parseAndApplyModules(response) {
|
|
|
4942
5272
|
}
|
|
4943
5273
|
}
|
|
4944
5274
|
if (!modulesApplied) {
|
|
5275
|
+
const fenceCount = (response.match(/```/g) || []).length;
|
|
5276
|
+
if (fenceCount % 2 !== 0 && response.includes('"modules"')) {
|
|
5277
|
+
console.log("[parse] Detected truncated response (odd fence count), attempting salvage...");
|
|
5278
|
+
const lastFenceIdx = response.lastIndexOf("```");
|
|
5279
|
+
let truncated = response.slice(lastFenceIdx + 3);
|
|
5280
|
+
truncated = truncated.replace(/^[\w-]*\s*\n?/, "");
|
|
5281
|
+
const salvaged = tryRepairTruncatedJSON(truncated);
|
|
5282
|
+
if (salvaged) {
|
|
5283
|
+
const obj = salvaged;
|
|
5284
|
+
if (obj.modules && Array.isArray(obj.modules) && obj.modules.length > 0) {
|
|
5285
|
+
console.log("[parse] Salvaged", obj.modules.length, "modules from truncated response");
|
|
5286
|
+
const modules = obj.modules.map((m) => ({
|
|
5287
|
+
moduleName: String(m.moduleName || ""),
|
|
5288
|
+
fieldsJson: typeof m.fieldsJson === "string" ? m.fieldsJson : JSON.stringify(m.fieldsJson, null, 2),
|
|
5289
|
+
metaJson: typeof m.metaJson === "string" ? m.metaJson : JSON.stringify(m.metaJson, null, 2),
|
|
5290
|
+
moduleHtml: String(m.moduleHtml || ""),
|
|
5291
|
+
moduleCss: String(m.moduleCss || ""),
|
|
5292
|
+
moduleJs: m.moduleJs ? String(m.moduleJs) : void 0
|
|
5293
|
+
}));
|
|
5294
|
+
updateModules({
|
|
5295
|
+
modules,
|
|
5296
|
+
sharedCss: obj.sharedCss !== void 0 ? String(obj.sharedCss) : void 0,
|
|
5297
|
+
sharedJs: obj.sharedJs !== void 0 ? String(obj.sharedJs) : void 0
|
|
5298
|
+
});
|
|
5299
|
+
modulesApplied = true;
|
|
5300
|
+
if (parseWarningCallback) {
|
|
5301
|
+
parseWarningCallback("Response was truncated \u2014 some modules may be incomplete. Try sending your request again for the full set.");
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
if (!modulesApplied) {
|
|
5308
|
+
console.log("[parse] No modules applied. Response length:", response.length);
|
|
5309
|
+
console.log("[parse] Contains 'vibespot-modules':", response.includes("vibespot-modules"));
|
|
5310
|
+
console.log(`[parse] Contains '"modules"':`, response.includes('"modules"'));
|
|
5311
|
+
console.log("[parse] Fence count:", (response.match(/```/g) || []).length);
|
|
5312
|
+
console.log("[parse] Response preview:", response.slice(0, 500));
|
|
4945
5313
|
const hasModuleRef = response.includes("vibespot-modules") || response.includes('"modules"');
|
|
4946
5314
|
const describesProse = /\bmodule|modul/i.test(response) && (/\bcreated?\b|\berstellt\b|\bgenerat/i.test(response) || /\|.*\|.*\|/m.test(response));
|
|
4947
5315
|
if (hasModuleRef || describesProse) {
|
|
@@ -5255,8 +5623,12 @@ function handleApiRoute(method, path, req, res) {
|
|
|
5255
5623
|
if (method === "POST") handleDeleteLocalThemeRoute(req, res);
|
|
5256
5624
|
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5257
5625
|
break;
|
|
5626
|
+
case "/api/themes/rename":
|
|
5627
|
+
if (method === "POST") handleRenameThemeRoute(req, res);
|
|
5628
|
+
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5629
|
+
break;
|
|
5258
5630
|
case "/api/history":
|
|
5259
|
-
if (method === "GET") handleHistoryRoute(res);
|
|
5631
|
+
if (method === "GET") handleHistoryRoute(req, res);
|
|
5260
5632
|
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5261
5633
|
break;
|
|
5262
5634
|
case "/api/rollback":
|
|
@@ -5275,6 +5647,10 @@ function handleApiRoute(method, path, req, res) {
|
|
|
5275
5647
|
if (method === "POST") handleTemplateActivateRoute(req, res);
|
|
5276
5648
|
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5277
5649
|
break;
|
|
5650
|
+
case "/api/templates/rename":
|
|
5651
|
+
if (method === "POST") handleTemplateRenameRoute(req, res);
|
|
5652
|
+
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5653
|
+
break;
|
|
5278
5654
|
case "/api/module-library":
|
|
5279
5655
|
if (method === "GET") handleModuleLibraryRoute(res);
|
|
5280
5656
|
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
@@ -5282,6 +5658,10 @@ function handleApiRoute(method, path, req, res) {
|
|
|
5282
5658
|
case "/api/brand-assets":
|
|
5283
5659
|
handleBrandAssetsRoute(method, req, res);
|
|
5284
5660
|
break;
|
|
5661
|
+
case "/api/download-zip":
|
|
5662
|
+
if (method === "GET") handleDownloadZipRoute(res);
|
|
5663
|
+
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5664
|
+
break;
|
|
5285
5665
|
default:
|
|
5286
5666
|
if (path.startsWith("/api/settings/job/") && method === "GET") {
|
|
5287
5667
|
handleSettingsJobRoute(path, res);
|
|
@@ -5482,6 +5862,10 @@ function handleSetupInfoRoute(res) {
|
|
|
5482
5862
|
function handleSetupCreateRoute(req, res) {
|
|
5483
5863
|
readBody(req, (body) => {
|
|
5484
5864
|
try {
|
|
5865
|
+
if (isGenerating()) {
|
|
5866
|
+
jsonResponse(res, 409, { error: "Cannot switch projects while AI is generating.", generating: true });
|
|
5867
|
+
return;
|
|
5868
|
+
}
|
|
5485
5869
|
const { name } = JSON.parse(body);
|
|
5486
5870
|
if (!name || typeof name !== "string") {
|
|
5487
5871
|
jsonResponse(res, 400, { error: "Theme name is required" });
|
|
@@ -5505,7 +5889,7 @@ function handleSetupCreateRoute(req, res) {
|
|
|
5505
5889
|
if (newDir) createdAt = join15(process.cwd(), newDir);
|
|
5506
5890
|
}
|
|
5507
5891
|
if (createdAt !== themePath && existsSync5(createdAt)) {
|
|
5508
|
-
|
|
5892
|
+
renameSync3(createdAt, themePath);
|
|
5509
5893
|
}
|
|
5510
5894
|
const tplDir = join15(themePath, "templates");
|
|
5511
5895
|
if (existsSync5(tplDir)) {
|
|
@@ -5528,6 +5912,10 @@ function handleSetupCreateRoute(req, res) {
|
|
|
5528
5912
|
function handleSetupFetchRoute(req, res) {
|
|
5529
5913
|
readBody(req, (body) => {
|
|
5530
5914
|
try {
|
|
5915
|
+
if (isGenerating()) {
|
|
5916
|
+
jsonResponse(res, 409, { error: "Cannot switch projects while AI is generating.", generating: true });
|
|
5917
|
+
return;
|
|
5918
|
+
}
|
|
5531
5919
|
const { name } = JSON.parse(body);
|
|
5532
5920
|
if (!name || typeof name !== "string") {
|
|
5533
5921
|
jsonResponse(res, 400, { error: "Theme name is required" });
|
|
@@ -5558,6 +5946,10 @@ function handleSetupFetchRoute(req, res) {
|
|
|
5558
5946
|
function handleSetupOpenRoute(req, res) {
|
|
5559
5947
|
readBody(req, (body) => {
|
|
5560
5948
|
try {
|
|
5949
|
+
if (isGenerating()) {
|
|
5950
|
+
jsonResponse(res, 409, { error: "Cannot switch projects while AI is generating.", generating: true });
|
|
5951
|
+
return;
|
|
5952
|
+
}
|
|
5561
5953
|
const { path: themePath } = JSON.parse(body);
|
|
5562
5954
|
if (!themePath || typeof themePath !== "string") {
|
|
5563
5955
|
jsonResponse(res, 400, { error: "Theme path is required" });
|
|
@@ -5589,6 +5981,10 @@ function handleSetupOpenRoute(req, res) {
|
|
|
5589
5981
|
function handleSetupResumeRoute(req, res) {
|
|
5590
5982
|
readBody(req, (body) => {
|
|
5591
5983
|
try {
|
|
5984
|
+
if (isGenerating()) {
|
|
5985
|
+
jsonResponse(res, 409, { error: "Cannot switch projects while AI is generating.", generating: true });
|
|
5986
|
+
return;
|
|
5987
|
+
}
|
|
5592
5988
|
const { sessionId } = JSON.parse(body);
|
|
5593
5989
|
if (!sessionId || typeof sessionId !== "string") {
|
|
5594
5990
|
jsonResponse(res, 400, { error: "Session ID is required" });
|
|
@@ -5626,17 +6022,112 @@ function handleSetupApiKeyRoute(req, res) {
|
|
|
5626
6022
|
}
|
|
5627
6023
|
});
|
|
5628
6024
|
}
|
|
6025
|
+
var modelCache = { data: {}, ts: 0 };
|
|
6026
|
+
var MODEL_CACHE_TTL = 10 * 60 * 1e3;
|
|
6027
|
+
var STATIC_MODELS = {
|
|
6028
|
+
"claude-code": [
|
|
6029
|
+
{ id: "sonnet", label: "Claude Sonnet (default)" },
|
|
6030
|
+
{ id: "opus", label: "Claude Opus" },
|
|
6031
|
+
{ id: "haiku", label: "Claude Haiku" }
|
|
6032
|
+
],
|
|
6033
|
+
"codex-cli": [
|
|
6034
|
+
{ id: "o4-mini", label: "o4 Mini (default)" },
|
|
6035
|
+
{ id: "o3", label: "o3" },
|
|
6036
|
+
{ id: "gpt-4o", label: "GPT-4o" }
|
|
6037
|
+
]
|
|
6038
|
+
};
|
|
6039
|
+
async function fetchAnthropicModels(apiKey) {
|
|
6040
|
+
const resp = await fetch("https://api.anthropic.com/v1/models", {
|
|
6041
|
+
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
|
|
6042
|
+
});
|
|
6043
|
+
if (!resp.ok) return [];
|
|
6044
|
+
const data = await resp.json();
|
|
6045
|
+
return data.data.filter((m) => !m.id.startsWith("claude-3-") && !m.id.startsWith("claude-2")).map((m) => ({ id: m.id, label: m.display_name }));
|
|
6046
|
+
}
|
|
6047
|
+
async function fetchOpenAIModels(apiKey) {
|
|
6048
|
+
const resp = await fetch("https://api.openai.com/v1/models", {
|
|
6049
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
6050
|
+
});
|
|
6051
|
+
if (!resp.ok) return [];
|
|
6052
|
+
const data = await resp.json();
|
|
6053
|
+
const keep = /^(gpt-4o|gpt-4o-mini|o[1-4](-mini)?|o[1-4]-pro)$/;
|
|
6054
|
+
return data.data.filter((m) => keep.test(m.id)).sort((a, b) => a.id.localeCompare(b.id)).map((m) => ({ id: m.id, label: m.id }));
|
|
6055
|
+
}
|
|
6056
|
+
async function fetchGeminiModels(apiKey) {
|
|
6057
|
+
const resp = await fetch(
|
|
6058
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
|
|
6059
|
+
);
|
|
6060
|
+
if (!resp.ok) return [];
|
|
6061
|
+
const data = await resp.json();
|
|
6062
|
+
return data.models.filter((m) => m.name.includes("gemini-2")).map((m) => ({ id: m.name.replace("models/", ""), label: m.displayName }));
|
|
6063
|
+
}
|
|
6064
|
+
async function getModelCatalog() {
|
|
6065
|
+
if (Date.now() - modelCache.ts < MODEL_CACHE_TTL && Object.keys(modelCache.data).length > 0) {
|
|
6066
|
+
return modelCache.data;
|
|
6067
|
+
}
|
|
6068
|
+
const config = loadConfig();
|
|
6069
|
+
const catalog = { ...STATIC_MODELS };
|
|
6070
|
+
const jobs2 = [];
|
|
6071
|
+
const anthropicKey = getApiKeyForEngine("anthropic-api", config);
|
|
6072
|
+
if (anthropicKey) {
|
|
6073
|
+
jobs2.push(
|
|
6074
|
+
fetchAnthropicModels(anthropicKey).then((models) => {
|
|
6075
|
+
if (models.length) catalog["anthropic-api"] = models;
|
|
6076
|
+
}).catch(() => {
|
|
6077
|
+
})
|
|
6078
|
+
);
|
|
6079
|
+
}
|
|
6080
|
+
const openaiKey = getApiKeyForEngine("openai-api", config);
|
|
6081
|
+
if (openaiKey) {
|
|
6082
|
+
jobs2.push(
|
|
6083
|
+
fetchOpenAIModels(openaiKey).then((models) => {
|
|
6084
|
+
if (models.length) catalog["openai-api"] = models;
|
|
6085
|
+
}).catch(() => {
|
|
6086
|
+
})
|
|
6087
|
+
);
|
|
6088
|
+
}
|
|
6089
|
+
const geminiKey = getApiKeyForEngine("gemini-api", config);
|
|
6090
|
+
if (geminiKey) {
|
|
6091
|
+
jobs2.push(
|
|
6092
|
+
fetchGeminiModels(geminiKey).then((models) => {
|
|
6093
|
+
if (models.length) {
|
|
6094
|
+
catalog["gemini-api"] = models;
|
|
6095
|
+
catalog["gemini-cli"] = models;
|
|
6096
|
+
}
|
|
6097
|
+
}).catch(() => {
|
|
6098
|
+
})
|
|
6099
|
+
);
|
|
6100
|
+
}
|
|
6101
|
+
await Promise.all(jobs2);
|
|
6102
|
+
modelCache.data = catalog;
|
|
6103
|
+
modelCache.ts = Date.now();
|
|
6104
|
+
return catalog;
|
|
6105
|
+
}
|
|
5629
6106
|
function handleSettingsStatusRoute(res) {
|
|
5630
6107
|
const env = detectEnvironment();
|
|
5631
6108
|
const config = loadConfig();
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
6109
|
+
getModelCatalog().then((models) => {
|
|
6110
|
+
jsonResponse(res, 200, {
|
|
6111
|
+
environment: env,
|
|
6112
|
+
config: {
|
|
6113
|
+
aiEngine: config.aiEngine || null,
|
|
6114
|
+
claudeCodeModel: config.claudeCodeModel || null,
|
|
6115
|
+
anthropicApiModel: config.anthropicApiModel || null,
|
|
6116
|
+
openaiApiModel: config.openaiApiModel || null
|
|
6117
|
+
},
|
|
6118
|
+
models
|
|
6119
|
+
});
|
|
6120
|
+
}).catch(() => {
|
|
6121
|
+
jsonResponse(res, 200, {
|
|
6122
|
+
environment: env,
|
|
6123
|
+
config: {
|
|
6124
|
+
aiEngine: config.aiEngine || null,
|
|
6125
|
+
claudeCodeModel: config.claudeCodeModel || null,
|
|
6126
|
+
anthropicApiModel: config.anthropicApiModel || null,
|
|
6127
|
+
openaiApiModel: config.openaiApiModel || null
|
|
6128
|
+
},
|
|
6129
|
+
models: STATIC_MODELS
|
|
6130
|
+
});
|
|
5640
6131
|
});
|
|
5641
6132
|
}
|
|
5642
6133
|
function handleSettingsEngineRoute(req, res) {
|
|
@@ -6031,7 +6522,31 @@ function handleDeleteLocalThemeRoute(req, res) {
|
|
|
6031
6522
|
}
|
|
6032
6523
|
});
|
|
6033
6524
|
}
|
|
6034
|
-
function
|
|
6525
|
+
function handleRenameThemeRoute(req, res) {
|
|
6526
|
+
readBody(req, (body) => {
|
|
6527
|
+
try {
|
|
6528
|
+
const { sessionId, newName } = JSON.parse(body);
|
|
6529
|
+
if (!sessionId || !newName || typeof newName !== "string") {
|
|
6530
|
+
jsonResponse(res, 400, { error: "sessionId and newName are required" });
|
|
6531
|
+
return;
|
|
6532
|
+
}
|
|
6533
|
+
const sanitized = newName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-|-$/g, "").replace(/-{2,}/g, "-");
|
|
6534
|
+
if (!sanitized) {
|
|
6535
|
+
jsonResponse(res, 400, { error: "Invalid name" });
|
|
6536
|
+
return;
|
|
6537
|
+
}
|
|
6538
|
+
const result = renameSession(sessionId, sanitized);
|
|
6539
|
+
if (result.ok) {
|
|
6540
|
+
jsonResponse(res, 200, { ok: true, newName: sanitized });
|
|
6541
|
+
} else {
|
|
6542
|
+
jsonResponse(res, 400, { error: result.error });
|
|
6543
|
+
}
|
|
6544
|
+
} catch (err) {
|
|
6545
|
+
jsonResponse(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
6546
|
+
}
|
|
6547
|
+
});
|
|
6548
|
+
}
|
|
6549
|
+
function handleHistoryRoute(req, res) {
|
|
6035
6550
|
const session = getSession();
|
|
6036
6551
|
if (!session) {
|
|
6037
6552
|
jsonResponse(res, 404, { error: "No active session" });
|
|
@@ -6041,8 +6556,10 @@ function handleHistoryRoute(res) {
|
|
|
6041
6556
|
jsonResponse(res, 200, { available: false, commits: [] });
|
|
6042
6557
|
return;
|
|
6043
6558
|
}
|
|
6044
|
-
const
|
|
6045
|
-
|
|
6559
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
6560
|
+
const templateId = url.searchParams.get("templateId");
|
|
6561
|
+
const commits = templateId ? getTemplateHistory(session.themePath, templateId, 50) : getHistory(session.themePath, 50);
|
|
6562
|
+
jsonResponse(res, 200, { available: true, commits, filtered: !!templateId });
|
|
6046
6563
|
}
|
|
6047
6564
|
function handleRollbackRoute(req, res) {
|
|
6048
6565
|
readBody(req, (body) => {
|
|
@@ -6052,18 +6569,34 @@ function handleRollbackRoute(req, res) {
|
|
|
6052
6569
|
jsonResponse(res, 404, { error: "No active session" });
|
|
6053
6570
|
return;
|
|
6054
6571
|
}
|
|
6055
|
-
const { hash } = JSON.parse(body);
|
|
6572
|
+
const { hash, templateId } = JSON.parse(body);
|
|
6056
6573
|
if (!hash || typeof hash !== "string") {
|
|
6057
6574
|
jsonResponse(res, 400, { error: "Commit hash is required" });
|
|
6058
6575
|
return;
|
|
6059
6576
|
}
|
|
6060
6577
|
addMessage("assistant", `Rolled back to version ${hash.slice(0, 7)}.`);
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6578
|
+
if (templateId) {
|
|
6579
|
+
const tpl = session.templates.find((t) => t.id === templateId);
|
|
6580
|
+
if (!tpl) {
|
|
6581
|
+
jsonResponse(res, 404, { error: "Template not found" });
|
|
6582
|
+
return;
|
|
6583
|
+
}
|
|
6584
|
+
const filePaths = tpl.moduleOrder.map((n) => `modules/${n}.module`);
|
|
6585
|
+
if (tpl.templateFile) filePaths.push(tpl.templateFile);
|
|
6586
|
+
const result = rollbackTemplateToCommit(session.themePath, templateId, hash, filePaths);
|
|
6587
|
+
if (!result.success) {
|
|
6588
|
+
jsonResponse(res, 500, { error: result.error || "Rollback failed" });
|
|
6589
|
+
return;
|
|
6590
|
+
}
|
|
6591
|
+
reloadActiveTemplateFromDisk();
|
|
6592
|
+
} else {
|
|
6593
|
+
const result = rollbackToCommit(session.themePath, hash);
|
|
6594
|
+
if (!result.success) {
|
|
6595
|
+
jsonResponse(res, 500, { error: result.error || "Rollback failed" });
|
|
6596
|
+
return;
|
|
6597
|
+
}
|
|
6598
|
+
reloadModulesFromDisk();
|
|
6065
6599
|
}
|
|
6066
|
-
reloadModulesFromDisk();
|
|
6067
6600
|
saveSession();
|
|
6068
6601
|
jsonResponse(res, 200, {
|
|
6069
6602
|
ok: true,
|
|
@@ -6103,6 +6636,41 @@ function handleDashboardRoute(res) {
|
|
|
6103
6636
|
}
|
|
6104
6637
|
});
|
|
6105
6638
|
}
|
|
6639
|
+
function handleDownloadZipRoute(res) {
|
|
6640
|
+
const session = getSession();
|
|
6641
|
+
if (!session) {
|
|
6642
|
+
jsonResponse(res, 404, { error: "No active session" });
|
|
6643
|
+
return;
|
|
6644
|
+
}
|
|
6645
|
+
const themePath = session.themePath;
|
|
6646
|
+
if (!existsSync5(themePath)) {
|
|
6647
|
+
jsonResponse(res, 404, { error: "Theme directory not found" });
|
|
6648
|
+
return;
|
|
6649
|
+
}
|
|
6650
|
+
const themeName = session.themeName || "theme";
|
|
6651
|
+
const parentDir = join15(themePath, "..");
|
|
6652
|
+
const folderName = basename7(themePath);
|
|
6653
|
+
try {
|
|
6654
|
+
const zipFileName = `${themeName}.zip`;
|
|
6655
|
+
const tmpZip = join15(parentDir, zipFileName);
|
|
6656
|
+
if (existsSync5(tmpZip)) rmSync5(tmpZip);
|
|
6657
|
+
execSync4(
|
|
6658
|
+
`zip -r "${zipFileName}" "${folderName}" -x "${folderName}/.git/*" "${folderName}/.vibespot/*" "${folderName}/node_modules/*"`,
|
|
6659
|
+
{ cwd: parentDir, timeout: 3e4 }
|
|
6660
|
+
);
|
|
6661
|
+
const zipData = readFileSync5(tmpZip);
|
|
6662
|
+
rmSync5(tmpZip);
|
|
6663
|
+
res.writeHead(200, {
|
|
6664
|
+
"Content-Type": "application/zip",
|
|
6665
|
+
"Content-Disposition": `attachment; filename="${zipFileName}"`,
|
|
6666
|
+
"Content-Length": zipData.length
|
|
6667
|
+
});
|
|
6668
|
+
res.end(zipData);
|
|
6669
|
+
} catch (err) {
|
|
6670
|
+
console.warn("[download-zip] Failed:", err.message);
|
|
6671
|
+
jsonResponse(res, 500, { error: "Failed to create zip archive" });
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6106
6674
|
function handleTemplatesRoute(method, req, res) {
|
|
6107
6675
|
const session = getSession();
|
|
6108
6676
|
if (!session) {
|
|
@@ -6198,6 +6766,26 @@ function handleTemplateActivateRoute(req, res) {
|
|
|
6198
6766
|
}
|
|
6199
6767
|
});
|
|
6200
6768
|
}
|
|
6769
|
+
function handleTemplateRenameRoute(req, res) {
|
|
6770
|
+
readBody(req, (body) => {
|
|
6771
|
+
try {
|
|
6772
|
+
const { templateId, newLabel } = JSON.parse(body);
|
|
6773
|
+
if (!templateId || !newLabel || typeof newLabel !== "string") {
|
|
6774
|
+
jsonResponse(res, 400, { error: "templateId and newLabel are required" });
|
|
6775
|
+
return;
|
|
6776
|
+
}
|
|
6777
|
+
const success = renameTemplate(templateId, newLabel.trim());
|
|
6778
|
+
if (!success) {
|
|
6779
|
+
jsonResponse(res, 404, { error: "Template not found" });
|
|
6780
|
+
return;
|
|
6781
|
+
}
|
|
6782
|
+
saveSession();
|
|
6783
|
+
jsonResponse(res, 200, { ok: true, newLabel: newLabel.trim() });
|
|
6784
|
+
} catch (err) {
|
|
6785
|
+
jsonResponse(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
6786
|
+
}
|
|
6787
|
+
});
|
|
6788
|
+
}
|
|
6201
6789
|
function handleModuleLibraryRoute(res) {
|
|
6202
6790
|
const library = getModuleLibrary();
|
|
6203
6791
|
jsonResponse(res, 200, {
|
|
@@ -6346,7 +6934,17 @@ function handleWsConnection(ws) {
|
|
|
6346
6934
|
const currentSession = getSession();
|
|
6347
6935
|
if (currentSession) {
|
|
6348
6936
|
writeModulesToDisk();
|
|
6349
|
-
const
|
|
6937
|
+
const activeTpl = getActiveTemplate();
|
|
6938
|
+
let commitHash = null;
|
|
6939
|
+
if (activeTpl) {
|
|
6940
|
+
const filePaths = activeTpl.moduleOrder.map((n) => `modules/${n}.module`);
|
|
6941
|
+
if (activeTpl.templateFile) filePaths.push(activeTpl.templateFile);
|
|
6942
|
+
if (activeTpl.sharedCss) filePaths.push(`css/${currentSession.themeName}-theme.css`);
|
|
6943
|
+
if (activeTpl.sharedJs) filePaths.push(`js/${currentSession.themeName}-animations.js`);
|
|
6944
|
+
commitHash = commitTemplateState(currentSession.themePath, activeTpl.id, userMessage, filePaths);
|
|
6945
|
+
} else {
|
|
6946
|
+
commitHash = commitThemeState(currentSession.themePath, userMessage);
|
|
6947
|
+
}
|
|
6350
6948
|
if (commitHash) {
|
|
6351
6949
|
ws.send(JSON.stringify({ type: "version_created", hash: commitHash }));
|
|
6352
6950
|
}
|
|
@@ -6491,6 +7089,7 @@ ${errorContext}`;
|
|
|
6491
7089
|
const activeTpl = getActiveTemplate();
|
|
6492
7090
|
ws.send(JSON.stringify({
|
|
6493
7091
|
type: "init",
|
|
7092
|
+
sessionId: session.id,
|
|
6494
7093
|
themeName: session.themeName,
|
|
6495
7094
|
modules: getOrderedModules().map((m) => m.moduleName),
|
|
6496
7095
|
messageCount: session.messages.length,
|