@vue-skuilder/cli 0.1.11-9 → 0.1.12
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/CLAUDE.md +14 -5
- package/README.md +5 -4
- package/dist/commands/studio.d.ts.map +1 -1
- package/dist/commands/studio.js +199 -128
- package/dist/commands/studio.js.map +1 -1
- package/dist/mcp-server.d.ts +0 -1
- package/dist/mcp-server.js +8 -3
- package/dist/mcp-server.js.map +1 -1
- package/dist/standalone-ui-template/package.json +2 -2
- package/dist/studio-ui-src/api/index.ts +24 -2
- package/dist/studio-ui-src/components/StudioFlush.vue +12 -26
- package/dist/studio-ui-src/main.ts +28 -0
- package/dist/studio-ui-src/package.json +3 -2
- package/dist/studio-ui-src/utils/courseConfigRegistration.ts +86 -4
- package/dist/studio-ui-src/vite.config.ts +1 -1
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +11 -15
- package/dist/utils/template.js.map +1 -1
- package/package.json +12 -11
- package/src/commands/studio.ts +236 -159
- package/src/mcp-server.ts +8 -3
- package/src/utils/template.ts +11 -15
- package/dist/studio-ui-src/vite.config.base.js +0 -103
package/src/commands/studio.ts
CHANGED
|
@@ -57,7 +57,7 @@ export function createStudioCommand(): Command {
|
|
|
57
57
|
.description(
|
|
58
58
|
'Launch studio mode: a complete course editing environment with CouchDB, Express API, and web editor'
|
|
59
59
|
)
|
|
60
|
-
.argument('[coursePath]', 'Path to static course directory', '.')
|
|
60
|
+
.argument('[coursePath]', 'Path to static course directory or manifest file', '.')
|
|
61
61
|
.option('-p, --port <port>', 'CouchDB port for studio session', '5985')
|
|
62
62
|
.option('--no-browser', 'Skip automatic browser launch')
|
|
63
63
|
.action(launchStudio)
|
|
@@ -79,8 +79,11 @@ Studio Mode creates a full editing environment for static courses:
|
|
|
79
79
|
|
|
80
80
|
Requirements:
|
|
81
81
|
• Docker (for CouchDB instance)
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
- either
|
|
83
|
+
• Valid static course project (with package.json)
|
|
84
|
+
• Course data in public/static-courses/ directory
|
|
85
|
+
- OR
|
|
86
|
+
- a valid mainfest.json
|
|
84
87
|
|
|
85
88
|
Example:
|
|
86
89
|
skuilder studio # Launch in current directory
|
|
@@ -104,94 +107,63 @@ async function launchStudio(coursePath: string, options: StudioOptions) {
|
|
|
104
107
|
try {
|
|
105
108
|
console.log(chalk.cyan(`🎨 Launching Skuilder Studio...`));
|
|
106
109
|
|
|
107
|
-
//
|
|
110
|
+
// Input validation and course detection
|
|
108
111
|
const resolvedPath = path.resolve(coursePath);
|
|
109
|
-
console.log(chalk.gray(`📁
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// Ensure cache directory exists
|
|
136
|
-
await ensureCacheDirectory(resolvedPath);
|
|
137
|
-
|
|
138
|
-
const buildExists = studioBuildExists(resolvedPath, questionsHash);
|
|
139
|
-
const buildPath = getStudioBuildPath(resolvedPath, questionsHash);
|
|
140
|
-
|
|
141
|
-
console.log(chalk.gray(` Questions hash: ${questionsHash}`));
|
|
142
|
-
console.log(chalk.gray(` Cached build exists: ${buildExists ? 'Yes' : 'No'}`));
|
|
143
|
-
|
|
144
|
-
// Determine if we need to rebuild studio-ui
|
|
145
|
-
if (buildExists) {
|
|
146
|
-
console.log(chalk.gray(` Using cached build at: ${buildPath}`));
|
|
147
|
-
studioUIPath = buildPath;
|
|
148
|
-
} else {
|
|
149
|
-
console.log(chalk.cyan(`🔨 Building studio-ui with local question types...`));
|
|
150
|
-
studioUIPath = await buildStudioUIWithQuestions(resolvedPath, questionsHash);
|
|
151
|
-
console.log(chalk.green(`✅ Studio-UI build complete: ${studioUIPath}`));
|
|
152
|
-
}
|
|
153
|
-
} catch (error) {
|
|
154
|
-
// Handle catastrophic build errors by falling back to embedded source
|
|
155
|
-
console.log(
|
|
156
|
-
chalk.yellow(
|
|
157
|
-
`⚠️ Unable to process questions due to ${error},\n⚠️ Using embedded studio-ui`
|
|
158
|
-
)
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const embeddedPath = path.join(__dirname, '..', 'studio-ui-src');
|
|
162
|
-
|
|
163
|
-
if (fs.existsSync(embeddedPath)) {
|
|
164
|
-
studioUIPath = embeddedPath;
|
|
165
|
-
console.log(chalk.gray(` Using embedded studio-ui source directly`));
|
|
166
|
-
} else {
|
|
167
|
-
console.error(chalk.red(`❌ No viable studio-ui source available`));
|
|
168
|
-
throw new Error('Critical error: Cannot locate studio-ui source');
|
|
112
|
+
console.log(chalk.gray(`📁 Input path: ${resolvedPath}`));
|
|
113
|
+
|
|
114
|
+
let isManifestMode = false;
|
|
115
|
+
let actualCoursePath = resolvedPath;
|
|
116
|
+
|
|
117
|
+
// Check if input is a manifest file
|
|
118
|
+
const manifestValidation = await validateManifestCourse(resolvedPath);
|
|
119
|
+
if (manifestValidation.isValid) {
|
|
120
|
+
console.log(chalk.green(`✅ Valid course manifest detected`));
|
|
121
|
+
isManifestMode = true;
|
|
122
|
+
actualCoursePath = manifestValidation.coursePath!;
|
|
123
|
+
console.log(chalk.gray(`📁 Course data path: ${actualCoursePath}`));
|
|
124
|
+
} else {
|
|
125
|
+
// Check if it's a traditional scaffolded course directory
|
|
126
|
+
if (!(await validateSuiCourse(resolvedPath))) {
|
|
127
|
+
console.error(chalk.red(`❌ Not a valid course directory or manifest file`));
|
|
128
|
+
console.log(chalk.yellow(`💡 Studio mode accepts either:`));
|
|
129
|
+
console.log(
|
|
130
|
+
chalk.yellow(` - Scaffolded course directory with package.json and static-data/`)
|
|
131
|
+
);
|
|
132
|
+
console.log(
|
|
133
|
+
chalk.yellow(` - Course manifest.json file with chunks/ and indices/ directories`)
|
|
134
|
+
);
|
|
135
|
+
process.exit(1);
|
|
169
136
|
}
|
|
137
|
+
console.log(chalk.green(`✅ Valid standalone-ui course detected`));
|
|
170
138
|
}
|
|
171
139
|
|
|
172
|
-
//
|
|
173
|
-
const
|
|
140
|
+
// Studio UI build preparation
|
|
141
|
+
const studioUIPath = isManifestMode
|
|
142
|
+
? await handleManifestCourse()
|
|
143
|
+
: await handleSuiCourse(resolvedPath);
|
|
144
|
+
|
|
145
|
+
// Start CouchDB instance
|
|
146
|
+
const studioDatabaseName = generateStudioDatabaseName(actualCoursePath);
|
|
174
147
|
console.log(chalk.cyan(`🗄️ Starting studio CouchDB instance: ${studioDatabaseName}`));
|
|
175
148
|
|
|
176
149
|
couchDBManager = await startStudioCouchDB(studioDatabaseName, parseInt(options.port));
|
|
177
150
|
|
|
178
|
-
//
|
|
151
|
+
// Load course data into database
|
|
179
152
|
console.log(chalk.cyan(`📦 Unpacking course data to studio database...`));
|
|
180
153
|
const unpackResult = await unpackCourseToStudio(
|
|
181
|
-
|
|
182
|
-
couchDBManager.getConnectionDetails()
|
|
154
|
+
actualCoursePath,
|
|
155
|
+
couchDBManager.getConnectionDetails(),
|
|
156
|
+
isManifestMode
|
|
183
157
|
);
|
|
184
158
|
|
|
185
|
-
//
|
|
186
|
-
console.log(chalk.cyan(`⚡ Starting Express backend server...`));
|
|
159
|
+
// Start Express API server
|
|
187
160
|
const expressResult = await startExpressBackend(
|
|
188
161
|
couchDBManager.getConnectionDetails(),
|
|
189
|
-
resolvedPath,
|
|
190
162
|
unpackResult.databaseName
|
|
191
163
|
);
|
|
192
164
|
expressServer = expressResult.server;
|
|
193
165
|
|
|
194
|
-
//
|
|
166
|
+
// Launch studio web interface
|
|
195
167
|
console.log(chalk.cyan(`🌐 Starting studio-ui server...`));
|
|
196
168
|
console.log(
|
|
197
169
|
chalk.gray(
|
|
@@ -201,7 +173,8 @@ async function launchStudio(coursePath: string, options: StudioOptions) {
|
|
|
201
173
|
const studioUIPort = await startStudioUIServer(
|
|
202
174
|
couchDBManager.getConnectionDetails(),
|
|
203
175
|
unpackResult,
|
|
204
|
-
studioUIPath
|
|
176
|
+
studioUIPath,
|
|
177
|
+
expressResult.url
|
|
205
178
|
);
|
|
206
179
|
|
|
207
180
|
console.log(chalk.green(`✅ Studio session ready!`));
|
|
@@ -210,7 +183,7 @@ async function launchStudio(coursePath: string, options: StudioOptions) {
|
|
|
210
183
|
console.log(chalk.gray(` Express API: ${expressResult.url}`));
|
|
211
184
|
|
|
212
185
|
// Display MCP connection information
|
|
213
|
-
const mcpInfo = getMCPConnectionInfo(unpackResult, couchDBManager
|
|
186
|
+
const mcpInfo = getMCPConnectionInfo(unpackResult, couchDBManager);
|
|
214
187
|
console.log(chalk.blue(`🔗 MCP Server: ${mcpInfo.command}`));
|
|
215
188
|
console.log(chalk.gray(` Connect MCP clients using the command above`));
|
|
216
189
|
console.log(chalk.gray(` Environment variables for MCP:`));
|
|
@@ -219,7 +192,7 @@ async function launchStudio(coursePath: string, options: StudioOptions) {
|
|
|
219
192
|
});
|
|
220
193
|
|
|
221
194
|
// Display .mcp.json content for Claude Code integration
|
|
222
|
-
const mcpJsonContent = generateMCPJson(unpackResult, couchDBManager
|
|
195
|
+
const mcpJsonContent = generateMCPJson(unpackResult, couchDBManager);
|
|
223
196
|
console.log(chalk.blue(`📋 .mcp.json content:`));
|
|
224
197
|
console.log(chalk.gray(mcpJsonContent));
|
|
225
198
|
|
|
@@ -229,7 +202,7 @@ async function launchStudio(coursePath: string, options: StudioOptions) {
|
|
|
229
202
|
}
|
|
230
203
|
console.log(chalk.gray(` Press Ctrl+C to stop studio session`));
|
|
231
204
|
|
|
232
|
-
//
|
|
205
|
+
// Session lifecycle management
|
|
233
206
|
process.on('SIGINT', () => {
|
|
234
207
|
void (async () => {
|
|
235
208
|
console.log(chalk.cyan(`\n🔄 Stopping studio session...`));
|
|
@@ -256,7 +229,50 @@ async function launchStudio(coursePath: string, options: StudioOptions) {
|
|
|
256
229
|
}
|
|
257
230
|
|
|
258
231
|
/**
|
|
259
|
-
*
|
|
232
|
+
* Validate that the given path is a course manifest file
|
|
233
|
+
*/
|
|
234
|
+
async function validateManifestCourse(
|
|
235
|
+
manifestPath: string
|
|
236
|
+
): Promise<{ isValid: boolean; coursePath?: string }> {
|
|
237
|
+
try {
|
|
238
|
+
// Check if file exists
|
|
239
|
+
if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile()) {
|
|
240
|
+
return { isValid: false };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check if it's a JSON file
|
|
244
|
+
if (!manifestPath.endsWith('.json')) {
|
|
245
|
+
return { isValid: false };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Try to parse as JSON
|
|
249
|
+
const manifestContent = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
250
|
+
|
|
251
|
+
// Validate required manifest fields
|
|
252
|
+
if (!manifestContent.courseId || !manifestContent.courseName) {
|
|
253
|
+
return { isValid: false };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Find course data directory relative to manifest
|
|
257
|
+
const manifestDir = path.dirname(manifestPath);
|
|
258
|
+
const coursePath = manifestDir; // Course data should be alongside manifest
|
|
259
|
+
|
|
260
|
+
// Check for required course data structure
|
|
261
|
+
const chunksPath = path.join(coursePath, 'chunks');
|
|
262
|
+
const indicesPath = path.join(coursePath, 'indices');
|
|
263
|
+
|
|
264
|
+
if (!fs.existsSync(chunksPath) || !fs.existsSync(indicesPath)) {
|
|
265
|
+
return { isValid: false };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { isValid: true, coursePath };
|
|
269
|
+
} catch {
|
|
270
|
+
return { isValid: false };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Validate that the given path contains a standalone-ui course
|
|
260
276
|
*/
|
|
261
277
|
async function validateSuiCourse(coursePath: string): Promise<boolean> {
|
|
262
278
|
try {
|
|
@@ -407,7 +423,8 @@ interface UnpackResult {
|
|
|
407
423
|
async function startStudioUIServer(
|
|
408
424
|
connectionDetails: ConnectionDetails,
|
|
409
425
|
unpackResult: UnpackResult,
|
|
410
|
-
studioPath: string
|
|
426
|
+
studioPath: string,
|
|
427
|
+
expressApiUrl?: string
|
|
411
428
|
): Promise<number> {
|
|
412
429
|
// Serve from built dist directory if it exists, otherwise fallback to source
|
|
413
430
|
const distPath = path.join(studioPath, 'dist');
|
|
@@ -458,6 +475,9 @@ async function startStudioUIServer(
|
|
|
458
475
|
name: '${unpackResult.databaseName}',
|
|
459
476
|
courseId: '${unpackResult.courseId}',
|
|
460
477
|
originalCourseId: '${unpackResult.courseId}'
|
|
478
|
+
},
|
|
479
|
+
express: {
|
|
480
|
+
url: '${expressApiUrl || 'http://localhost:3000'}'
|
|
461
481
|
}
|
|
462
482
|
};
|
|
463
483
|
</script>
|
|
@@ -486,6 +506,9 @@ async function startStudioUIServer(
|
|
|
486
506
|
name: '${unpackResult.databaseName}',
|
|
487
507
|
courseId: '${unpackResult.courseId}',
|
|
488
508
|
originalCourseId: '${unpackResult.courseId}'
|
|
509
|
+
},
|
|
510
|
+
express: {
|
|
511
|
+
url: '${expressApiUrl || 'http://localhost:3000'}'
|
|
489
512
|
}
|
|
490
513
|
};
|
|
491
514
|
</script>
|
|
@@ -562,7 +585,6 @@ async function openBrowser(url: string): Promise<void> {
|
|
|
562
585
|
*/
|
|
563
586
|
async function startExpressBackend(
|
|
564
587
|
couchDbConnectionDetails: ConnectionDetails,
|
|
565
|
-
_projectPath: string,
|
|
566
588
|
databaseName: string
|
|
567
589
|
): Promise<{ server: http.Server; port: number; url: string }> {
|
|
568
590
|
console.log(chalk.blue('⚡ Starting Express backend server...'));
|
|
@@ -594,6 +616,9 @@ async function startExpressBackend(
|
|
|
594
616
|
};
|
|
595
617
|
|
|
596
618
|
try {
|
|
619
|
+
// Set NODE_ENV for studio mode authentication bypass
|
|
620
|
+
process.env.NODE_ENV = 'studio';
|
|
621
|
+
|
|
597
622
|
// Create Express app using factory
|
|
598
623
|
const app = createExpressApp(config);
|
|
599
624
|
|
|
@@ -624,28 +649,37 @@ async function startExpressBackend(
|
|
|
624
649
|
*/
|
|
625
650
|
async function unpackCourseToStudio(
|
|
626
651
|
coursePath: string,
|
|
627
|
-
connectionDetails: ConnectionDetails
|
|
652
|
+
connectionDetails: ConnectionDetails,
|
|
653
|
+
isManifestMode = false
|
|
628
654
|
): Promise<{ databaseName: string; courseId: string }> {
|
|
629
655
|
try {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (
|
|
633
|
-
//
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if (
|
|
643
|
-
|
|
656
|
+
let courseDataPath: string;
|
|
657
|
+
|
|
658
|
+
if (isManifestMode) {
|
|
659
|
+
// For manifest mode, the coursePath already points to the course data directory
|
|
660
|
+
courseDataPath = coursePath;
|
|
661
|
+
console.log(chalk.gray(` Manifest mode: using course data at ${courseDataPath}`));
|
|
662
|
+
} else {
|
|
663
|
+
// Find the course data directory (static-data OR public/static-courses)
|
|
664
|
+
courseDataPath = path.join(coursePath, 'static-data');
|
|
665
|
+
if (!fs.existsSync(courseDataPath)) {
|
|
666
|
+
// Try public/static-courses directory
|
|
667
|
+
const publicStaticPath = path.join(coursePath, 'public', 'static-courses');
|
|
668
|
+
if (fs.existsSync(publicStaticPath)) {
|
|
669
|
+
// Find the first course directory inside public/static-courses
|
|
670
|
+
const courses = fs
|
|
671
|
+
.readdirSync(publicStaticPath, { withFileTypes: true })
|
|
672
|
+
.filter((dirent) => dirent.isDirectory())
|
|
673
|
+
.map((dirent) => dirent.name);
|
|
674
|
+
|
|
675
|
+
if (courses.length > 0) {
|
|
676
|
+
courseDataPath = path.join(publicStaticPath, courses[0]);
|
|
677
|
+
} else {
|
|
678
|
+
throw new Error('No course directories found in public/static-courses/');
|
|
679
|
+
}
|
|
644
680
|
} else {
|
|
645
|
-
throw new Error('No course
|
|
681
|
+
throw new Error('No course data found in static-data/ or public/static-courses/');
|
|
646
682
|
}
|
|
647
|
-
} else {
|
|
648
|
-
throw new Error('No course data found in static-data/ or public/static-courses/');
|
|
649
683
|
}
|
|
650
684
|
}
|
|
651
685
|
|
|
@@ -836,9 +870,9 @@ async function buildDefaultStudioUI(buildPath: string): Promise<string> {
|
|
|
836
870
|
const studioPackageJsonPath = path.join(buildPath, 'package.json');
|
|
837
871
|
await transformPackageJsonForStudioBuild(studioPackageJsonPath);
|
|
838
872
|
|
|
839
|
-
// Fix Vite config to use npm packages
|
|
840
|
-
console.log(chalk.gray(` Updating Vite configuration for
|
|
841
|
-
await
|
|
873
|
+
// Fix Vite config to use npm packages and resolve custom questions imports
|
|
874
|
+
console.log(chalk.gray(` Updating Vite configuration for studio build...`));
|
|
875
|
+
await createStudioViteConfig(buildPath);
|
|
842
876
|
|
|
843
877
|
// Run Vite build process
|
|
844
878
|
console.log(chalk.gray(` Running Vite build process...`));
|
|
@@ -1036,9 +1070,9 @@ async function buildStudioUIWithCustomQuestions(
|
|
|
1036
1070
|
const studioPackageJsonPath = path.join(buildPath, 'package.json');
|
|
1037
1071
|
await transformPackageJsonForStudioBuild(studioPackageJsonPath);
|
|
1038
1072
|
|
|
1039
|
-
// Step 2.5: Fix Vite config to use npm packages
|
|
1040
|
-
console.log(chalk.gray(` Updating Vite configuration for
|
|
1041
|
-
await
|
|
1073
|
+
// Step 2.5: Fix Vite config to use npm packages and resolve custom questions imports
|
|
1074
|
+
console.log(chalk.gray(` Updating Vite configuration for studio build...`));
|
|
1075
|
+
await createStudioViteConfig(buildPath);
|
|
1042
1076
|
|
|
1043
1077
|
// Step 3: Install custom questions package
|
|
1044
1078
|
console.log(
|
|
@@ -1259,9 +1293,9 @@ async function runViteBuild(buildPath: string): Promise<void> {
|
|
|
1259
1293
|
}
|
|
1260
1294
|
|
|
1261
1295
|
/**
|
|
1262
|
-
*
|
|
1296
|
+
* Create Vite configuration for studio-ui build environment
|
|
1263
1297
|
*/
|
|
1264
|
-
async function
|
|
1298
|
+
async function createStudioViteConfig(buildPath: string): Promise<void> {
|
|
1265
1299
|
const viteConfigPath = path.join(buildPath, 'vite.config.ts');
|
|
1266
1300
|
|
|
1267
1301
|
if (!fs.existsSync(viteConfigPath)) {
|
|
@@ -1269,9 +1303,9 @@ async function fixViteConfigForStandaloneBuild(buildPath: string): Promise<void>
|
|
|
1269
1303
|
return;
|
|
1270
1304
|
}
|
|
1271
1305
|
|
|
1272
|
-
// Create a clean
|
|
1273
|
-
//
|
|
1274
|
-
const
|
|
1306
|
+
// Create a clean studio vite config for studio-ui environment
|
|
1307
|
+
// Includes aliases to resolve custom questions imports
|
|
1308
|
+
const studioViteConfig = `import { defineConfig } from 'vite';
|
|
1275
1309
|
import vue from '@vitejs/plugin-vue';
|
|
1276
1310
|
|
|
1277
1311
|
export default defineConfig({
|
|
@@ -1297,9 +1331,17 @@ export default defineConfig({
|
|
|
1297
1331
|
},
|
|
1298
1332
|
resolve: {
|
|
1299
1333
|
extensions: ['.js', '.ts', '.json', '.vue'],
|
|
1334
|
+
alias: {
|
|
1335
|
+
// Resolve @vue-skuilder packages to npm packages for custom questions import
|
|
1336
|
+
'@vue-skuilder/common': '@vue-skuilder/common',
|
|
1337
|
+
'@vue-skuilder/courseware': '@vue-skuilder/courseware',
|
|
1338
|
+
'@vue-skuilder/db': '@vue-skuilder/db',
|
|
1339
|
+
'@vue-skuilder/common-ui': '@vue-skuilder/common-ui',
|
|
1340
|
+
'@vue-skuilder/edit-ui': '@vue-skuilder/edit-ui'
|
|
1341
|
+
},
|
|
1300
1342
|
dedupe: [
|
|
1301
1343
|
'vue',
|
|
1302
|
-
'vuetify',
|
|
1344
|
+
'vuetify',
|
|
1303
1345
|
'vue-router',
|
|
1304
1346
|
'pinia',
|
|
1305
1347
|
'@vue-skuilder/db',
|
|
@@ -1311,50 +1353,23 @@ export default defineConfig({
|
|
|
1311
1353
|
}
|
|
1312
1354
|
});`;
|
|
1313
1355
|
|
|
1314
|
-
fs.writeFileSync(viteConfigPath,
|
|
1315
|
-
console.log(chalk.gray(` Vite config replaced with
|
|
1356
|
+
fs.writeFileSync(viteConfigPath, studioViteConfig);
|
|
1357
|
+
console.log(chalk.gray(` Vite config replaced with studio version`));
|
|
1316
1358
|
}
|
|
1317
1359
|
|
|
1318
1360
|
/**
|
|
1319
|
-
* Determine the correct MCP server executable path
|
|
1361
|
+
* Determine the correct MCP server executable path.
|
|
1362
|
+
* This is now greatly simplified because the bundled server is always
|
|
1363
|
+
* located relative to this `studio.ts` file.
|
|
1320
1364
|
*/
|
|
1321
|
-
function resolveMCPExecutable(
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
} {
|
|
1326
|
-
// Check if we're in the monorepo (packages/cli exists)
|
|
1327
|
-
const monorepoCliPath = path.join(projectPath, 'packages', 'cli', 'dist', 'mcp-server.js');
|
|
1328
|
-
if (fs.existsSync(monorepoCliPath)) {
|
|
1329
|
-
return {
|
|
1330
|
-
command: './packages/cli/dist/mcp-server.js',
|
|
1331
|
-
args: [],
|
|
1332
|
-
isNpx: false,
|
|
1333
|
-
};
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
// Check if @vue-skuilder/cli is installed as a dependency
|
|
1337
|
-
const scaffoldedCliPath = path.join(
|
|
1338
|
-
projectPath,
|
|
1339
|
-
'node_modules',
|
|
1340
|
-
'@vue-skuilder',
|
|
1341
|
-
'cli',
|
|
1342
|
-
'dist',
|
|
1343
|
-
'mcp-server.js'
|
|
1344
|
-
);
|
|
1345
|
-
if (fs.existsSync(scaffoldedCliPath)) {
|
|
1346
|
-
return {
|
|
1347
|
-
command: './node_modules/@vue-skuilder/cli/dist/mcp-server.js',
|
|
1348
|
-
args: [],
|
|
1349
|
-
isNpx: false,
|
|
1350
|
-
};
|
|
1351
|
-
}
|
|
1365
|
+
function resolveMCPExecutable(): { command: string; args: string[] } {
|
|
1366
|
+
// Resolve the path to the bundled server relative to the current file.
|
|
1367
|
+
// __dirname is the `dist/commands` directory.
|
|
1368
|
+
const serverPath = path.resolve(__dirname, '..', 'mcp-server.js');
|
|
1352
1369
|
|
|
1353
|
-
// Fallback to npx approach
|
|
1354
1370
|
return {
|
|
1355
|
-
command: '
|
|
1356
|
-
args: [
|
|
1357
|
-
isNpx: true,
|
|
1371
|
+
command: 'node',
|
|
1372
|
+
args: [serverPath],
|
|
1358
1373
|
};
|
|
1359
1374
|
}
|
|
1360
1375
|
|
|
@@ -1363,19 +1378,16 @@ function resolveMCPExecutable(projectPath: string): {
|
|
|
1363
1378
|
*/
|
|
1364
1379
|
function getMCPConnectionInfo(
|
|
1365
1380
|
unpackResult: UnpackResult,
|
|
1366
|
-
couchDBManager: CouchDBManager
|
|
1367
|
-
projectPath: string
|
|
1381
|
+
couchDBManager: CouchDBManager
|
|
1368
1382
|
): { command: string; env: Record<string, string> } {
|
|
1369
1383
|
const couchDetails = couchDBManager.getConnectionDetails();
|
|
1370
|
-
const executable = resolveMCPExecutable(
|
|
1384
|
+
const executable = resolveMCPExecutable();
|
|
1385
|
+
const port = couchDetails.port || 5985;
|
|
1371
1386
|
|
|
1372
1387
|
// Build command string for display
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
} else {
|
|
1377
|
-
commandStr = `node ${executable.command} ${unpackResult.databaseName} ${couchDetails.port}`;
|
|
1378
|
-
}
|
|
1388
|
+
const commandStr = `${executable.command} ${executable.args.join(' ')} ${
|
|
1389
|
+
unpackResult.databaseName
|
|
1390
|
+
} ${port}`;
|
|
1379
1391
|
|
|
1380
1392
|
return {
|
|
1381
1393
|
command: commandStr,
|
|
@@ -1394,12 +1406,11 @@ function getMCPConnectionInfo(
|
|
|
1394
1406
|
function generateMCPJson(
|
|
1395
1407
|
unpackResult: UnpackResult,
|
|
1396
1408
|
couchDBManager: CouchDBManager,
|
|
1397
|
-
projectPath: string,
|
|
1398
1409
|
serverName: string = 'vue-skuilder-studio'
|
|
1399
1410
|
): string {
|
|
1400
1411
|
const couchDetails = couchDBManager.getConnectionDetails();
|
|
1401
1412
|
const port = couchDetails.port || 5985;
|
|
1402
|
-
const executable = resolveMCPExecutable(
|
|
1413
|
+
const executable = resolveMCPExecutable();
|
|
1403
1414
|
|
|
1404
1415
|
const mcpConfig = {
|
|
1405
1416
|
mcpServers: {
|
|
@@ -1418,3 +1429,69 @@ function generateMCPJson(
|
|
|
1418
1429
|
|
|
1419
1430
|
return JSON.stringify(mcpConfig, null, 2);
|
|
1420
1431
|
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Handle SUI course build process - extract questions hashing and UI building logic
|
|
1435
|
+
*/
|
|
1436
|
+
async function handleSuiCourse(coursePath: string): Promise<string> {
|
|
1437
|
+
console.log(chalk.cyan(`🔍 Analyzing local question types...`));
|
|
1438
|
+
|
|
1439
|
+
try {
|
|
1440
|
+
const questionsHash = await withStudioBuildErrorHandling(
|
|
1441
|
+
() => hashQuestionsDirectory(coursePath),
|
|
1442
|
+
StudioBuildErrorType.QUESTIONS_HASH_ERROR,
|
|
1443
|
+
{ coursePath: coursePath }
|
|
1444
|
+
);
|
|
1445
|
+
|
|
1446
|
+
// Ensure cache directory exists
|
|
1447
|
+
await ensureCacheDirectory(coursePath);
|
|
1448
|
+
|
|
1449
|
+
const buildExists = studioBuildExists(coursePath, questionsHash);
|
|
1450
|
+
const buildPath = getStudioBuildPath(coursePath, questionsHash);
|
|
1451
|
+
|
|
1452
|
+
console.log(chalk.gray(` Questions hash: ${questionsHash}`));
|
|
1453
|
+
console.log(chalk.gray(` Cached build exists: ${buildExists ? 'Yes' : 'No'}`));
|
|
1454
|
+
|
|
1455
|
+
let studioUIPath: string;
|
|
1456
|
+
|
|
1457
|
+
// Determine if we need to rebuild studio-ui
|
|
1458
|
+
if (buildExists) {
|
|
1459
|
+
console.log(chalk.gray(` Using cached build at: ${buildPath}`));
|
|
1460
|
+
studioUIPath = buildPath;
|
|
1461
|
+
} else {
|
|
1462
|
+
console.log(chalk.cyan(`🔨 Building studio-ui with local question types...`));
|
|
1463
|
+
studioUIPath = await buildStudioUIWithQuestions(coursePath, questionsHash);
|
|
1464
|
+
console.log(chalk.green(`✅ Studio-UI build complete: ${studioUIPath}`));
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
return studioUIPath;
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
// Handle catastrophic build errors by falling back to embedded source
|
|
1470
|
+
console.log(
|
|
1471
|
+
chalk.yellow(`⚠️ Unable to process questions due to ${error},\n⚠️ Using embedded studio-ui`)
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1474
|
+
const embeddedPath = path.join(__dirname, '..', 'studio-ui-src');
|
|
1475
|
+
|
|
1476
|
+
if (fs.existsSync(embeddedPath)) {
|
|
1477
|
+
const studioUIPath = embeddedPath;
|
|
1478
|
+
console.log(chalk.gray(` Using embedded studio-ui source directly`));
|
|
1479
|
+
return studioUIPath;
|
|
1480
|
+
} else {
|
|
1481
|
+
console.error(chalk.red(`❌ No viable studio-ui source available`));
|
|
1482
|
+
throw new Error('Critical error: Cannot locate studio-ui source');
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Handle manifest course - simple default build without custom questions
|
|
1489
|
+
*/
|
|
1490
|
+
async function handleManifestCourse(): Promise<string> {
|
|
1491
|
+
console.log(chalk.cyan(`📋 Manifest mode: using default studio-ui build`));
|
|
1492
|
+
|
|
1493
|
+
const manifestBuildPath = path.join(__dirname, '..', 'studio-builds', 'manifest-default');
|
|
1494
|
+
const studioUIPath = await buildDefaultStudioUI(manifestBuildPath);
|
|
1495
|
+
|
|
1496
|
+
return studioUIPath;
|
|
1497
|
+
}
|
package/src/mcp-server.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
// MCP Server for Vue-Skuilder courses
|
|
2
|
+
// This file is bundled into a self-contained executable
|
|
2
3
|
|
|
3
4
|
import { initializeDataLayer, getDataLayer, initializeTuiLogging } from '@vue-skuilder/db';
|
|
4
5
|
import { MCPServer } from '@vue-skuilder/mcp';
|
|
6
|
+
import { consoleLogger } from '@vue-skuilder/common';
|
|
5
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
8
|
|
|
7
9
|
initializeTuiLogging();
|
|
@@ -42,10 +44,11 @@ async function main() {
|
|
|
42
44
|
await initializeDataLayer(couchdbConfig);
|
|
43
45
|
const courseDB = getDataLayer().getCourseDB(courseId);
|
|
44
46
|
|
|
45
|
-
// Create and start MCP server
|
|
47
|
+
// Create and start MCP server with console logger
|
|
46
48
|
const server = new MCPServer(courseDB, {
|
|
47
49
|
enableSourceLinking: true,
|
|
48
50
|
maxCardsPerQuery: 50,
|
|
51
|
+
logger: consoleLogger,
|
|
49
52
|
});
|
|
50
53
|
|
|
51
54
|
const transport = new StdioServerTransport();
|
|
@@ -53,7 +56,9 @@ async function main() {
|
|
|
53
56
|
|
|
54
57
|
console.error('MCP Server started successfully!');
|
|
55
58
|
console.error(`Course: ${courseId}`);
|
|
56
|
-
console.error('Available resources: course://config, cards://all,
|
|
59
|
+
console.error('Available resources: course://config, cards://all, cards://tag/{tagName}, cards://shape/{shapeName}, cards://elo/{eloRange}');
|
|
60
|
+
console.error(' shapes://all, shapes://{shapeName}, schema://{dataShapeName}');
|
|
61
|
+
console.error(' tags://all, tags://stats, tags://{tagName}, tags://union/{tags}, tags://intersect/{tags}, tags://exclusive/{tags}, tags://distribution');
|
|
57
62
|
console.error('Available tools: create_card, update_card, tag_card, delete_card');
|
|
58
63
|
console.error('Available prompts: fill-in-card-authoring, elo-scoring-guidance');
|
|
59
64
|
console.error('Ready for MCP client connections via stdio');
|
package/src/utils/template.ts
CHANGED
|
@@ -200,27 +200,22 @@ export default defineConfig({
|
|
|
200
200
|
},
|
|
201
201
|
rollupOptions: {
|
|
202
202
|
// External packages that shouldn't be bundled in library mode
|
|
203
|
+
// For studio integration, we bundle vue-skuilder packages to avoid npm resolution issues
|
|
203
204
|
external: [
|
|
204
|
-
|
|
205
|
-
'vue-router',
|
|
206
|
-
'vuetify',
|
|
207
|
-
'pinia',
|
|
208
|
-
'@vue-skuilder/common',
|
|
209
|
-
'@vue-skuilder/common-ui',
|
|
210
|
-
'@vue-skuilder/courseware',
|
|
211
|
-
'@vue-skuilder/db',
|
|
205
|
+
// Bundle everything for studio integration - no externals
|
|
212
206
|
],
|
|
213
207
|
output: {
|
|
214
208
|
// Global variables for UMD build
|
|
215
209
|
globals: {
|
|
216
|
-
|
|
210
|
+
vue: 'Vue',
|
|
217
211
|
'vue-router': 'VueRouter',
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
'@vue-skuilder/common
|
|
222
|
-
'@vue-skuilder/
|
|
223
|
-
'@vue-skuilder/
|
|
212
|
+
vuetify: 'Vuetify',
|
|
213
|
+
pinia: 'Pinia',
|
|
214
|
+
// Remove globals for bundled packages
|
|
215
|
+
// '@vue-skuilder/common': 'VueSkuilderCommon',
|
|
216
|
+
// '@vue-skuilder/common-ui': 'VueSkuilderCommonUI',
|
|
217
|
+
// '@vue-skuilder/courseware': 'VueSkuilderCourseWare',
|
|
218
|
+
// '@vue-skuilder/db': 'VueSkuilderDB',
|
|
224
219
|
},
|
|
225
220
|
exports: 'named',
|
|
226
221
|
// Preserve CSS in the output bundle
|
|
@@ -421,6 +416,7 @@ node_modules/
|
|
|
421
416
|
# Production builds
|
|
422
417
|
/dist
|
|
423
418
|
/build
|
|
419
|
+
/dist-lib
|
|
424
420
|
|
|
425
421
|
# Local env files
|
|
426
422
|
.env
|