@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.
@@ -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
- Valid static course project (with package.json)
83
- Course data in public/static-courses/ directory
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
- // Phase 2: Course Detection & Validation
110
+ // Input validation and course detection
108
111
  const resolvedPath = path.resolve(coursePath);
109
- console.log(chalk.gray(`📁 Course path: ${resolvedPath}`));
110
-
111
- if (!(await validateSuiCourse(resolvedPath))) {
112
- console.error(chalk.red(`❌ Not a valid standalone-ui course directory`));
113
- console.log(chalk.yellow(`💡 Studio mode requires a vue-skuilder course with:`));
114
- console.log(chalk.yellow(` - package.json with @vue-skuilder/* dependencies`));
115
- console.log(
116
- chalk.yellow(` - static-data/ OR public/static-courses/ directory with course content`)
117
- );
118
- process.exit(1);
119
- }
120
-
121
- console.log(chalk.green(`✅ Valid standalone-ui course detected`));
122
-
123
- // Phase 0.5: Hash questions directory to determine studio-ui build needs
124
- console.log(chalk.cyan(`🔍 Analyzing local question types...`));
125
- let questionsHash: string;
126
- let studioUIPath: string;
127
-
128
- try {
129
- questionsHash = await withStudioBuildErrorHandling(
130
- () => hashQuestionsDirectory(resolvedPath),
131
- StudioBuildErrorType.QUESTIONS_HASH_ERROR,
132
- { coursePath: resolvedPath }
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
- // Phase 1: CouchDB Management
173
- const studioDatabaseName = generateStudioDatabaseName(resolvedPath);
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
- // Phase 4: Populate CouchDB with course data
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
- resolvedPath,
182
- couchDBManager.getConnectionDetails()
154
+ actualCoursePath,
155
+ couchDBManager.getConnectionDetails(),
156
+ isManifestMode
183
157
  );
184
158
 
185
- // Phase 9.5: Launch Express backend
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
- // Phase 7: Launch studio-ui server
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, resolvedPath);
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, resolvedPath);
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
- // Keep process alive and handle cleanup
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
- * Phase 2: Validate that the given path contains a standalone-ui course
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
- // Find the course data directory (static-data OR public/static-courses)
631
- let courseDataPath = path.join(coursePath, 'static-data');
632
- if (!fs.existsSync(courseDataPath)) {
633
- // Try public/static-courses directory
634
- const publicStaticPath = path.join(coursePath, 'public', 'static-courses');
635
- if (fs.existsSync(publicStaticPath)) {
636
- // Find the first course directory inside public/static-courses
637
- const courses = fs
638
- .readdirSync(publicStaticPath, { withFileTypes: true })
639
- .filter((dirent) => dirent.isDirectory())
640
- .map((dirent) => dirent.name);
641
-
642
- if (courses.length > 0) {
643
- courseDataPath = path.join(publicStaticPath, courses[0]);
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 directories found in public/static-courses/');
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 instead of monorepo paths
840
- console.log(chalk.gray(` Updating Vite configuration for standalone build...`));
841
- await fixViteConfigForStandaloneBuild(buildPath);
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 instead of monorepo paths
1040
- console.log(chalk.gray(` Updating Vite configuration for standalone build...`));
1041
- await fixViteConfigForStandaloneBuild(buildPath);
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
- * Fix Vite configuration to work in standalone build environment
1296
+ * Create Vite configuration for studio-ui build environment
1263
1297
  */
1264
- async function fixViteConfigForStandaloneBuild(buildPath: string): Promise<void> {
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 standalone vite config for external projects
1273
- // Relies on standard npm package resolution instead of monorepo paths
1274
- const standaloneViteConfig = `import { defineConfig } from 'vite';
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, standaloneViteConfig);
1315
- console.log(chalk.gray(` Vite config replaced with standalone version`));
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/command based on project context
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(projectPath: string): {
1322
- command: string;
1323
- args: string[];
1324
- isNpx: boolean;
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: 'npx',
1356
- args: ['@vue-skuilder/cli', 'mcp-server'],
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(projectPath);
1384
+ const executable = resolveMCPExecutable();
1385
+ const port = couchDetails.port || 5985;
1371
1386
 
1372
1387
  // Build command string for display
1373
- let commandStr: string;
1374
- if (executable.isNpx) {
1375
- commandStr = `${executable.command} ${executable.args.join(' ')} ${unpackResult.databaseName} ${couchDetails.port}`;
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(projectPath);
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
- #!/usr/bin/env node
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, tags://all, shapes://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');
@@ -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
- 'vue',
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
- 'vue': 'Vue',
210
+ vue: 'Vue',
217
211
  'vue-router': 'VueRouter',
218
- 'vuetify': 'Vuetify',
219
- 'pinia': 'Pinia',
220
- '@vue-skuilder/common': 'VueSkuilderCommon',
221
- '@vue-skuilder/common-ui': 'VueSkuilderCommonUI',
222
- '@vue-skuilder/courseware': 'VueSkuilderCourseWare',
223
- '@vue-skuilder/db': 'VueSkuilderDB',
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