From 6f687b57e86b0773b2638dedfddd35faa17e8f53 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Thu, 3 Jul 2025 14:11:52 -0300 Subject: [PATCH 01/29] package `express` app in cli --- packages/cli/package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index a17044dfd..4854a703f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -18,9 +18,11 @@ } }, "scripts": { - "build": "rm -rf dist && npm run build:studio-ui && tsc && npm run embed:studio-ui", + "build": "rm -rf dist && npm run build:studio-ui && npm run build:express && tsc && npm run embed:studio-ui && npm run embed:express", "build:studio-ui": "cd ../studio-ui && npm run build", + "build:express": "cd ../express && npm run build", "embed:studio-ui": "mkdir -p dist/studio-ui-assets && cp -r ../studio-ui/dist/* dist/studio-ui-assets/", + "embed:express": "mkdir -p dist/express-assets && cp -r ../express/dist/* dist/express-assets/", "dev": "tsc --watch", "lint": "npx eslint .", "lint:fix": "npx eslint . --fix", @@ -50,6 +52,7 @@ "@types/inquirer": "^9.0.0", "@types/node": "^20.0.0", "@types/serve-static": "^1.15.0", + "@vue-skuilder/express": "workspace:*", "@vue-skuilder/studio-ui": "workspace:*", "typescript": "~5.7.2" }, From 2dcfa9142b939fa7067ff068a773855795ea389a Mon Sep 17 00:00:00 2001 From: NiloCK Date: Thu, 3 Jul 2025 14:12:17 -0300 Subject: [PATCH 02/29] add expressManager helper --- packages/cli/src/utils/ExpressManager.ts | 192 +++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 packages/cli/src/utils/ExpressManager.ts diff --git a/packages/cli/src/utils/ExpressManager.ts b/packages/cli/src/utils/ExpressManager.ts new file mode 100644 index 000000000..9bfc4f1a9 --- /dev/null +++ b/packages/cli/src/utils/ExpressManager.ts @@ -0,0 +1,192 @@ +import { spawn, ChildProcess } from 'child_process'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import fs from 'fs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export interface ExpressManagerOptions { + port: number; + couchdbUrl: string; + couchdbUsername: string; + couchdbPassword: string; +} + +export interface ExpressManagerCallbacks { + onLog?: (message: string) => void; + onError?: (error: string) => void; +} + +export class ExpressManager { + private process: ChildProcess | null = null; + private readonly options: ExpressManagerOptions; + private readonly callbacks: ExpressManagerCallbacks; + + constructor(options: ExpressManagerOptions, callbacks: ExpressManagerCallbacks = {}) { + this.options = options; + this.callbacks = callbacks; + } + + async start(): Promise { + if (this.process) { + throw new Error('Express server is already running'); + } + + const expressAssetsPath = join(__dirname, '..', 'express-assets'); + + if (!fs.existsSync(expressAssetsPath)) { + throw new Error('Express assets not found. Please rebuild the CLI package.'); + } + + const expressMainPath = join(expressAssetsPath, 'app.js'); + + if (!fs.existsSync(expressMainPath)) { + throw new Error('Express main file not found at: ' + expressMainPath); + } + + // Find available port starting from requested port + const availablePort = await this.findAvailablePort(this.options.port); + + // Set environment variables for express + const env = { + ...process.env, + PORT: availablePort.toString(), + COUCHDB_SERVER: this.extractServerFromUrl(this.options.couchdbUrl), + COUCHDB_PROTOCOL: this.extractProtocolFromUrl(this.options.couchdbUrl), + COUCHDB_ADMIN: this.options.couchdbUsername, + COUCHDB_PASSWORD: this.options.couchdbPassword, + NODE_ENV: 'studio' + }; + + return new Promise((resolve, reject) => { + this.process = spawn('node', [expressMainPath], { + env, + stdio: ['pipe', 'pipe', 'pipe'], + cwd: expressAssetsPath + }); + + if (!this.process) { + reject(new Error('Failed to start Express process')); + return; + } + + let started = false; + + // Handle stdout + this.process.stdout?.on('data', (data: Buffer) => { + const message = data.toString().trim(); + if (message.includes('Server running on port') && !started) { + started = true; + this.options.port = availablePort; // Update with actual port + resolve(); + } + this.callbacks.onLog?.(message); + }); + + // Handle stderr + this.process.stderr?.on('data', (data: Buffer) => { + const error = data.toString().trim(); + this.callbacks.onError?.(error); + }); + + // Handle process exit + this.process.on('exit', (code) => { + this.process = null; + if (code !== 0 && code !== null) { + this.callbacks.onError?.(Express process exited with code ${code}); + } + }); + + // Handle process errors + this.process.on('error', (error) => { + this.process = null; + if (!started) { + reject(error); + } else { + this.callbacks.onError?.(Express process error: ${error.message}); + } + }); + + // Timeout if server doesn't start within 10 seconds + setTimeout(() => { + if (!started) { + this.stop(); + reject(new Error('Express server failed to start within timeout')); + } + }, 10000); + }); + } + + async stop(): Promise { + if (!this.process) { + return; + } + + return new Promise((resolve) => { + if (!this.process) { + resolve(); + return; + } + + this.process.on('exit', () => { + this.process = null; + resolve(); + }); + + // Try graceful shutdown first + this.process.kill('SIGTERM'); + + // Force kill after 5 seconds if still running + setTimeout(() => { + if (this.process) { + this.process.kill('SIGKILL'); + this.process = null; + resolve(); + } + }, 5000); + }); + } + + getConnectionDetails() { + return { + url: http://localhost:${this.options.port}, + port: this.options.port + }; + } + + private async findAvailablePort(startPort: number): Promise { + const net = await import('net'); + + for (let port = startPort; port < startPort + 100; port++) { + if (await this.isPortAvailable(port, net)) { + return port; + } + } + + throw new Error(No available port found starting from ${startPort}); + } + + private isPortAvailable(port: number, net: any): Promise { + return new Promise((resolve) => { + const server = net.createServer(); + + server.listen(port, '127.0.0.1', () => { + server.close(() => resolve(true)); + }); + + server.on('error', () => resolve(false)); + }); + } + + private extractServerFromUrl(url: string): string { + // Extract hostname:port from URL like "http://localhost:5984" + const match = url.match(/https?:\/\/([^\/]+)/); + return match ? match[1] : 'localhost:5984'; + } + + private extractProtocolFromUrl(url: string): string { + // Extract protocol from URL like "http://localhost:5984" + return url.startsWith('https') ? 'https' : 'http'; + } +} \ No newline at end of file From f8f7667bfe2ff4fc33fe47f4000d2a637a8a0a1c Mon Sep 17 00:00:00 2001 From: NiloCK Date: Thu, 3 Jul 2025 14:12:37 -0300 Subject: [PATCH 03/29] add working docs re: express integration w/ studio --- agent/791/a.todo.md | 9 +- agent/a.1.assessment.md | 211 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 agent/a.1.assessment.md diff --git a/agent/791/a.todo.md b/agent/791/a.todo.md index b6ccfb5c4..46cf355e7 100644 --- a/agent/791/a.todo.md +++ b/agent/791/a.todo.md @@ -71,11 +71,14 @@ - [ ] Create basic studio mode documentation - [x] Add studio-ui package to monorepo build system -## Phase 9.5: Express Backend Integration **CURRENT PRIORITY** -- [ ] **CURRENT**: Add mandatory express backend service to `skuilder studio` command +## Phase 9.5: Express Backend Integration **CURRENT PRIORITY** (Option A: Bundle Express Subprocess) +- [x] Add express build integration to CLI package.json (similar to studio-ui pattern) +- [x] Create express bundling/embedding process in CLI build scripts +- [x] Create `ExpressManager` class for subprocess management (similar to `CouchDBManager`) +- [ ] **CURRENT**: Add express startup to `launchStudio()` function with dynamic port assignment - [ ] Configure express environment variables to connect to studio CouchDB instance +- [ ] Add express to cleanup process in CLI signal handlers (SIGINT/SIGTERM) - [ ] Enable audio normalization processing for studio content (FFMPEG pipeline) -- [ ] Add express service shutdown handling for graceful studio exit - [ ] Extend wire-format.ts with `FLUSH_COURSE` ServerRequestType - [ ] Create `FlushCourse` interface in wire-format.ts - [ ] Add express route for flush operations using `CouchDBToStaticPacker` directly diff --git a/agent/a.1.assessment.md b/agent/a.1.assessment.md new file mode 100644 index 000000000..869cbf5c5 --- /dev/null +++ b/agent/a.1.assessment.md @@ -0,0 +1,211 @@ +# Assessment: CLI Studio Express Integration + +## Current State Analysis + +### How CLI Currently Handles Studio-UI + +The CLI's `studio` command currently: + +1. **Bundled Static Assets**: Studio-UI is built as a static Vue.js app and bundled into the CLI package + - Built via `npm run build:studio-ui` in CLI build process + - Assets copied to `dist/studio-ui-assets/` via `embed:studio-ui` script + - Served via Node.js `serve-static` middleware with dynamic config injection + +2. **Process Management**: CLI manages processes directly in Node.js + - **CouchDB**: Uses `CouchDBManager` class to spawn Docker containers + - **Studio-UI Server**: Creates HTTP server using Node.js `http` module + - **Process Lifecycle**: Handles graceful shutdown via SIGINT/SIGTERM handlers + +3. **Configuration Injection**: Dynamic config injection for studio-ui + - Injects CouchDB connection details into `window.STUDIO_CONFIG` + - Modifies `index.html` at runtime with database connection info + - Uses SPA fallback routing for client-side routing + +### Express Backend Architecture + +The Express backend (`@vue-skuilder/express`) is: + +1. **Standalone Service**: Designed as independent Node.js/Express application + - Main entry: `src/app.ts` + - Hardcoded port: 3000 + - Manages own CouchDB connections via `nano` client + - Handles authentication, course management, classroom operations + +2. **External Dependencies**: Requires external CouchDB instance + - Connects to CouchDB via environment configuration + - Manages multiple databases (courses, classrooms, users) + - Includes its own initialization and setup logic + +3. **Heavyweight Service**: Full-featured API server + - Authentication middleware + - File upload processing + - Complex business logic for course/classroom management + - Logging and error handling + +## Integration Options Analysis + +### Option A: Bundle Express and Run as Subprocess + +**Approach**: Bundle express into CLI and spawn it as a child process + +**Pros**: +- Clean separation of concerns +- Express runs in its own process space +- Can leverage existing Express configuration +- Easy to manage process lifecycle (start/stop) +- Familiar process management pattern (similar to CouchDB) + +**Cons**: +- Requires bundling entire Express app with CLI +- Multiple Node.js processes running +- More complex communication between CLI and Express +- Harder to pass configuration dynamically +- Potential port conflicts + +**Technical Implementation**: +```typescript +// Similar to how CLI spawns CouchDB +const expressProcess = spawn('node', [expressDistPath], { + env: { ...process.env, COUCHDB_URL: couchUrl } +}); +``` + +### Option B: Import Express Directly (Same Process) + +**Approach**: Import Express app and run it in the same Node.js process as CLI + +**Pros**: +- Single process - more efficient resource usage +- Direct communication between CLI and Express +- Easy to pass configuration objects +- Simpler deployment (single Node.js process) +- Can share CouchDB connection instances + +**Cons**: +- Tight coupling between CLI and Express +- Harder to isolate Express errors from CLI +- Express initialization could block CLI startup +- More complex to handle Express-specific configuration +- Potential conflicts with CLI's HTTP server + +**Technical Implementation**: +```typescript +// Import Express app and configure it +import { createExpressApp } from '@vue-skuilder/express'; +const expressApp = createExpressApp(couchConfig); +expressApp.listen(3000); +``` + +### Option C: Expect Express Running Separately + +**Approach**: CLI expects Express to be running as separate service + +**Pros**: +- Complete separation of concerns +- Express can be managed independently +- No changes needed to CLI architecture +- Easy to scale Express separately +- Clear service boundaries + +**Cons**: +- Additional setup complexity for users +- Need to coordinate between multiple services +- User must manage Express lifecycle manually +- Harder to provide "one-command" studio experience +- Complex error handling when Express is down + +**Technical Implementation**: +```typescript +// CLI just checks if Express is available +const expressHealthCheck = await fetch('http://localhost:3000/health'); +if (!expressHealthCheck.ok) { + throw new Error('Express server not running'); +} +``` + +### Option D: Hybrid Approach - Express Module + +**Approach**: Refactor Express into a configurable module that CLI can import and control + +**Pros**: +- Best of both worlds - modularity with integration +- CLI maintains control over process lifecycle +- Express can be configured per CLI session +- Clean API boundaries +- Reusable Express module + +**Cons**: +- Requires significant refactoring of Express package +- Breaking changes to Express architecture +- More complex implementation +- Need to maintain backward compatibility + +**Technical Implementation**: +```typescript +// Express as configurable module +import { ExpressService } from '@vue-skuilder/express'; +const expressService = new ExpressService({ + port: 3001, + couchdb: couchConfig, + logger: cliLogger +}); +await expressService.start(); +``` + +## Key Considerations + +### 1. **Process Management Consistency** +- CLI already manages CouchDB via subprocess (Docker) +- Studio-UI runs as HTTP server within CLI process +- Express subprocess would follow CouchDB pattern + +### 2. **Configuration Management** +- CLI injects config into Studio-UI at runtime +- Express needs CouchDB connection details +- Studio-UI needs to know Express endpoint + +### 3. **Port Management** +- CLI finds available ports dynamically (Studio-UI: 7174+) +- Express hardcoded to port 3000 +- Need to avoid port conflicts + +### 4. **Error Handling & Lifecycle** +- CLI handles graceful shutdown for all services +- Express needs to integrate with CLI's process management +- Studio-UI depends on both CouchDB and Express + +### 5. **User Experience** +- Current: Single `skuilder studio` command starts everything +- Goal: Maintain single-command simplicity +- Express adds complexity but provides powerful features + +## Recommendation + +**Option A: Bundle Express and Run as Subprocess** is the best approach because: + +1. **Architectural Consistency**: Matches existing CouchDB subprocess pattern +2. **Clean Separation**: Express runs independently but managed by CLI +3. **Minimal Changes**: Can reuse existing Express code with minimal refactoring +4. **Process Management**: Leverages CLI's existing process lifecycle handling +5. **Configuration**: Can pass config via environment variables (established pattern) + +### Implementation Plan + +1. **Express Modifications**: + - Make port configurable via environment variable + - Add health check endpoint + - Ensure clean shutdown on SIGTERM/SIGINT + +2. **CLI Integration**: + - Add Express process management (similar to CouchDB) + - Bundle Express dist in CLI build process + - Dynamic port allocation for Express + - Update Studio-UI config injection to include Express endpoint + +3. **Process Orchestration**: + - Start CouchDB first (as currently done) + - Start Express with CouchDB connection details + - Start Studio-UI with both CouchDB and Express endpoints + - Coordinate shutdown of all services + +This approach maintains the current architecture's clarity while adding the powerful Express backend capabilities that users need for full studio functionality. \ No newline at end of file From 3bdeba97fb33461daabdceb885e48d8fd4be16d4 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Thu, 3 Jul 2025 14:16:55 -0300 Subject: [PATCH 04/29] run & shutdown of studio express server --- agent/791/a.todo.md | 8 ++--- packages/cli/src/commands/studio.ts | 54 ++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/agent/791/a.todo.md b/agent/791/a.todo.md index 46cf355e7..cce242bf3 100644 --- a/agent/791/a.todo.md +++ b/agent/791/a.todo.md @@ -75,10 +75,10 @@ - [x] Add express build integration to CLI package.json (similar to studio-ui pattern) - [x] Create express bundling/embedding process in CLI build scripts - [x] Create `ExpressManager` class for subprocess management (similar to `CouchDBManager`) -- [ ] **CURRENT**: Add express startup to `launchStudio()` function with dynamic port assignment -- [ ] Configure express environment variables to connect to studio CouchDB instance -- [ ] Add express to cleanup process in CLI signal handlers (SIGINT/SIGTERM) -- [ ] Enable audio normalization processing for studio content (FFMPEG pipeline) +- [x] Add express startup to `launchStudio()` function with dynamic port assignment +- [x] Configure express environment variables to connect to studio CouchDB instance +- [x] Add express to cleanup process in CLI signal handlers (SIGINT/SIGTERM) +- [ ] **CURRENT**: Enable audio normalization processing for studio content (FFMPEG pipeline) - [ ] Extend wire-format.ts with `FLUSH_COURSE` ServerRequestType - [ ] Create `FlushCourse` interface in wire-format.ts - [ ] Add express route for flush operations using `CouchDBToStaticPacker` directly diff --git a/packages/cli/src/commands/studio.ts b/packages/cli/src/commands/studio.ts index e8ec86420..5781aace8 100644 --- a/packages/cli/src/commands/studio.ts +++ b/packages/cli/src/commands/studio.ts @@ -7,6 +7,7 @@ import { dirname } from 'path'; import http from 'http'; import { CouchDBManager } from '@vue-skuilder/common/docker'; import serveStatic from 'serve-static'; +import { ExpressManager } from '../utils/ExpressManager.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -27,6 +28,7 @@ interface StudioOptions { // Global references for cleanup let couchDBManager: CouchDBManager | null = null; +let expressManager: ExpressManager | null = null; let studioUIServer: http.Server | null = null; async function launchStudio(coursePath: string, options: StudioOptions) { @@ -62,6 +64,10 @@ async function launchStudio(coursePath: string, options: StudioOptions) { couchDBManager.getConnectionDetails() ); + // Phase 9.5: Launch Express backend + console.log(chalk.cyan(`⚡ Starting Express backend server...`)); + expressManager = await startExpressBackend(couchDBManager.getConnectionDetails()); + // Phase 7: Launch studio-ui server console.log(chalk.cyan(`🌐 Starting studio-ui server...`)); console.log( @@ -77,6 +83,7 @@ async function launchStudio(coursePath: string, options: StudioOptions) { console.log(chalk.green(`✅ Studio session ready!`)); console.log(chalk.white(`🎨 Studio URL: http://localhost:${studioUIPort}`)); console.log(chalk.gray(` Database: ${studioDatabaseName} on port ${options.port}`)); + console.log(chalk.gray(` Express API: ${expressManager.getConnectionDetails().url}`)); if (options.browser) { console.log(chalk.cyan(`🌐 Opening browser...`)); await openBrowser(`http://localhost:${studioUIPort}`); @@ -200,7 +207,7 @@ async function startStudioCouchDB(_databaseName: string, port: number): Promise< } /** - * Stop entire studio session (CouchDB + UI server) + * Stop entire studio session (CouchDB + Express + UI server) */ async function stopStudioSession(): Promise { // Stop studio-ui server @@ -215,6 +222,18 @@ async function stopStudioSession(): Promise { studioUIServer = null; } + // Stop Express backend + if (expressManager) { + try { + await expressManager.stop(); + console.log(chalk.green(`✅ Express backend stopped`)); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(chalk.red(`Error stopping Express backend: ${errorMessage}`)); + } + expressManager = null; + } + // Stop CouchDB if (couchDBManager) { try { @@ -384,6 +403,39 @@ async function openBrowser(url: string): Promise { } } +/** + * Phase 9.5: Start Express backend server + */ +async function startExpressBackend(couchDbConnectionDetails: ConnectionDetails): Promise { + const expressManager = new ExpressManager( + { + port: 3001, // Start from 3001 to avoid conflicts + couchdbUrl: couchDbConnectionDetails.url, + couchdbUsername: couchDbConnectionDetails.username, + couchdbPassword: couchDbConnectionDetails.password + }, + { + onLog: (message) => console.log(chalk.gray(` Express: ${message}`)), + onError: (error) => console.error(chalk.red(` Express Error: ${error}`)) + } + ); + + try { + await expressManager.start(); + + const connectionDetails = expressManager.getConnectionDetails(); + console.log(chalk.green(`✅ Express backend ready`)); + console.log(chalk.gray(` URL: ${connectionDetails.url}`)); + console.log(chalk.gray(` Port: ${connectionDetails.port}`)); + + return expressManager; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(chalk.red(`Failed to start Express backend: ${errorMessage}`)); + throw error; + } +} + /** * Phase 4: Unpack course data to studio CouchDB */ From b9c907b7c702f5cb8068a33d6bebf9b13dfee6b3 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Thu, 3 Jul 2025 14:30:23 -0300 Subject: [PATCH 05/29] add pack_course endpoint to express server --- packages/common/src/wire-format.ts | 19 ++++++++++++- packages/express/src/app.ts | 45 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/common/src/wire-format.ts b/packages/common/src/wire-format.ts index bd8201387..877ee20b7 100644 --- a/packages/common/src/wire-format.ts +++ b/packages/common/src/wire-format.ts @@ -139,6 +139,21 @@ export interface AddCourseData extends IServerRequest { }; } +export interface PackCourse extends IServerRequest { + type: ServerRequestType.PACK_COURSE; + courseId: string; + outputPath?: string; + response: { + status: Status; + ok: boolean; + errorText?: string; + packedFiles?: string[]; + outputPath?: string; + totalFiles?: number; + duration?: number; + } | null; +} + export type ServerRequest = | CreateClassroom | DeleteClassroom @@ -146,7 +161,8 @@ export type ServerRequest = | LeaveClassroom | CreateCourse | DeleteCourse - | AddCourseData; + | AddCourseData + | PackCourse; export enum ServerRequestType { CREATE_CLASSROOM = 'CREATE_CLASSROOM', @@ -156,4 +172,5 @@ export enum ServerRequestType { CREATE_COURSE = 'CREATE_COURSE', DELETE_COURSE = 'DELETE_COURSE', ADD_COURSE_DATA = 'ADD_COURSE_DATA', + PACK_COURSE = 'PACK_COURSE', } diff --git a/packages/express/src/app.ts b/packages/express/src/app.ts index 6ff483257..880309979 100644 --- a/packages/express/src/app.ts +++ b/packages/express/src/app.ts @@ -169,6 +169,51 @@ async function postHandler( logger.info(`\t\t\tCouchDB insert error: ${JSON.stringify(e)}`); res.json(e); }); + } else if (body.type === RequestEnum.PACK_COURSE) { + logger.info(`Starting PACK_COURSE for ${body.courseId}...`); + + try { + const startTime = Date.now(); + + // Use CouchDBToStaticPacker directly from db package + const { CouchDBToStaticPacker } = await import('@vue-skuilder/db'); + + // Create database connection URL + const dbUrl = `${ENV.COUCHDB_PROTOCOL}://${ENV.COUCHDB_ADMIN}:${ENV.COUCHDB_PASSWORD}@${ENV.COUCHDB_SERVER}`; + const dbName = `coursedb-${body.courseId}`; + + // Determine output path (use provided path or current working directory) + const outputPath = body.outputPath || process.cwd(); + + logger.info(`Packing course ${body.courseId} from ${dbName} to ${outputPath}`); + + // Initialize packer and perform pack operation + const packer = new CouchDBToStaticPacker(dbUrl, dbName); + const result = await packer.packToDirectory(outputPath); + + const duration = Date.now() - startTime; + + const response = { + status: 'ok' as const, + ok: true, + packedFiles: result.files || [], + outputPath: outputPath, + totalFiles: result.files?.length || 0, + duration: duration + }; + + logger.info(`Pack completed in ${duration}ms. Files: ${response.totalFiles}`); + res.json(response); + + } catch (error) { + logger.error('Pack operation failed:', error); + const response = { + status: 'error' as const, + ok: false, + errorText: error instanceof Error ? error.message : 'Pack operation failed' + }; + res.status(500).json(response); + } } } else { logger.info(`\tREQUEST UNAUTHORIZED!`); From 80ae6951f6a116464f9c9ad1e130cd6518a3d755 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Thu, 3 Jul 2025 15:25:05 -0300 Subject: [PATCH 06/29] add check for studio-mode coursedb --- .../src/attachment-preprocessing/index.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/packages/express/src/attachment-preprocessing/index.ts b/packages/express/src/attachment-preprocessing/index.ts index 70393e73f..50e2271d8 100644 --- a/packages/express/src/attachment-preprocessing/index.ts +++ b/packages/express/src/attachment-preprocessing/index.ts @@ -4,6 +4,7 @@ import { normalize } from './normalize.js'; import AsyncProcessQueue, { Result } from '../utils/processQueue.js'; import logger from '../logger.js'; import { CourseLookup } from '@vue-skuilder/db'; +import ENV from '../utils/env.js'; // @ts-expect-error [todo] const Q = new AsyncProcessQueue( @@ -65,16 +66,51 @@ export default async function postProcess(): Promise { try { logger.info(`Following all course databases for changes...`); + // Existing behavior for platform-ui courses const courses = await CourseLookup.allCourses(); + const processedCourseIds = new Set(); for (const course of courses) { try { postProcessCourse(course._id); + processedCourseIds.add(`coursedb-${course._id}`); } catch (e) { logger.error(`Error processing course ${course._id}: ${e}`); throw e; } } + + // Studio mode: discover additional databases not in coursedb-lookup + if (ENV.NODE_ENV === 'studio') { + logger.info('Studio mode detected: scanning for additional course databases...'); + + try { + const allDbs = await CouchDB.db.list(); + const studioDbs = allDbs.filter(db => + db.startsWith('coursedb-') && + !processedCourseIds.has(db) + ); + + logger.info(`Found ${studioDbs.length} potential studio databases`); + + for (const studioDb of studioDbs) { + const courseId = studioDb.replace('coursedb-', ''); + + try { + if (await hasCourseConfig(studioDb)) { + logger.info(`Starting postprocessing for studio database: ${studioDb}`); + postProcessCourse(courseId); + } else { + logger.debug(`Skipping ${studioDb}: no course config found`); + } + } catch (e) { + logger.error(`Error processing studio database ${studioDb}: ${e}`); + } + } + } catch (e) { + logger.error(`Error discovering studio databases: ${e}`); + } + } } catch (e) { logger.error(`Error in postProcess: ${e}`); } @@ -202,3 +238,31 @@ interface ProcessingField { mimetype: string; returnData?: string; } + +/** + * Check if a database contains course configuration (indicating it's a valid course database) + */ +async function hasCourseConfig(databaseName: string): Promise { + try { + const db = CouchDB.use(databaseName); + + // Try to find a course configuration document + // Course databases should have documents with course metadata + const result = await db.find({ + selector: { + $or: [ + { 'type': 'course' }, + { 'shape': { $exists: true } }, + { 'course_id': { $exists: true } }, + { 'courseID': { $exists: true } } + ] + }, + limit: 1 + }); + + return result.docs && result.docs.length > 0; + } catch (e) { + logger.debug(`Error checking course config for ${databaseName}: ${e}`); + return false; + } +} From 4c66c6a84aeea69d14558e342b2320ed99b7d2a1 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Thu, 3 Jul 2025 15:25:24 -0300 Subject: [PATCH 07/29] add hook to express pack-course --- packages/studio-ui/src/api/index.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/studio-ui/src/api/index.ts diff --git a/packages/studio-ui/src/api/index.ts b/packages/studio-ui/src/api/index.ts new file mode 100644 index 000000000..77317b34a --- /dev/null +++ b/packages/studio-ui/src/api/index.ts @@ -0,0 +1,25 @@ +import { ServerRequest, ServerRequestType } from '@vue-skuilder/common'; + +async function postWithResult(request: Omit): Promise { + const response = await fetch('http://localhost:3000/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); +} + +export async function flushCourse(courseId: string, outputPath?: string) { + return await postWithResult({ + type: ServerRequestType.PACK_COURSE, + courseId, + outputPath, + }); +} From d507930b11e2707a144c0adeb6ec0b0e9806fc03 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Thu, 3 Jul 2025 15:26:13 -0300 Subject: [PATCH 08/29] add linting, lint --- packages/studio-ui/eslint.config.mjs | 32 +++++++++++++++++++ packages/studio-ui/package.json | 5 ++- packages/studio-ui/src/App.vue | 10 +++--- packages/studio-ui/src/config/development.ts | 2 +- packages/studio-ui/src/main.ts | 2 +- .../studio-ui/src/views/BulkImportView.vue | 4 +-- .../studio-ui/src/views/CreateCardView.vue | 4 +-- 7 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 packages/studio-ui/eslint.config.mjs diff --git a/packages/studio-ui/eslint.config.mjs b/packages/studio-ui/eslint.config.mjs new file mode 100644 index 000000000..e01472af2 --- /dev/null +++ b/packages/studio-ui/eslint.config.mjs @@ -0,0 +1,32 @@ +import eslint from '@eslint/js'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import eslintPluginVue from 'eslint-plugin-vue'; +import eslintPlugiVuetify from 'eslint-plugin-vuetify'; +import globals from 'globals'; +import typescriptEslint from 'typescript-eslint'; + +export default typescriptEslint.config( + { ignores: ['*.d.ts', '**/coverage', '**/dist', '**/src/courses/chess/chessground'] }, + { + extends: [ + eslint.configs.recommended, + ...typescriptEslint.configs.recommended, + ...eslintPluginVue.configs['flat/recommended'], + ...eslintPlugiVuetify.configs['flat/recommended'], + ], + files: ['**/*.{ts,vue}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: globals.browser, + parserOptions: { + parser: typescriptEslint.parser, + }, + }, + rules: { + // your rules + '@typescript-eslint/no-explicit-any': 'warn', + }, + }, + eslintConfigPrettier +); diff --git a/packages/studio-ui/package.json b/packages/studio-ui/package.json index 504fb4d7d..a9b1c6531 100644 --- a/packages/studio-ui/package.json +++ b/packages/studio-ui/package.json @@ -8,7 +8,9 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "lint": "eslint . --fix", + "lint:check": "eslint ." }, "dependencies": { "@mdi/font": "^7.3.67", @@ -24,6 +26,7 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.1", + "eslint": "^9.30.1", "typescript": "^5.7.2", "vite": "^6.0.9", "vue-tsc": "^1.8.0" diff --git a/packages/studio-ui/src/App.vue b/packages/studio-ui/src/App.vue index 740d936f2..dbe784f00 100644 --- a/packages/studio-ui/src/App.vue +++ b/packages/studio-ui/src/App.vue @@ -1,8 +1,8 @@