Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions agent/u.working-agreement.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,23 @@ eg: `a.aside.perf.many-round-trips-in-reviews-lookup.md`, `a.aside.security.addC

The agent should expect the user to review these asides async to the current main workflow.

## Durable vs Ephemeral Content

In conversational flow and working documents, it can be useful to 'refer back' to the context of discussion.

But when writing source code, documentation, or other 'durable' content, please refrain from leaving conversational notes that are particular to the current moment. The below diff illustrates an example of poor behaviour in this respect. The inserted comment informs the current reader of the diff - ok - but it is just noise for future readers of the file.

```diff - poor example
85 - // Create course database connection
86 - const courseDbUrl = `${dbUrl}/${dbName}`;
85 + // courseDbUrl is already defined above
```

```diff - preferred
85 - // Create course database connection
86 - const courseDbUrl = `${dbUrl}/${dbName}`;
```

# Coda

This document and flow are experimental. If, experientially, it feels like it is not working, we can change it. Open to suggestions!
Expand Down
15 changes: 13 additions & 2 deletions packages/cli/src/commands/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ async function launchStudio(coursePath: string, options: StudioOptions) {
const studioUIPort = await startStudioUIServer(
couchDBManager.getConnectionDetails(),
unpackResult,
studioUIPath
studioUIPath,
expressResult.url
);

console.log(chalk.green(`✅ Studio session ready!`));
Expand Down Expand Up @@ -406,7 +407,8 @@ interface UnpackResult {
async function startStudioUIServer(
connectionDetails: ConnectionDetails,
unpackResult: UnpackResult,
studioPath: string
studioPath: string,
expressApiUrl?: string
): Promise<number> {
// Serve from built dist directory if it exists, otherwise fallback to source
const distPath = path.join(studioPath, 'dist');
Expand Down Expand Up @@ -457,6 +459,9 @@ async function startStudioUIServer(
name: '${unpackResult.databaseName}',
courseId: '${unpackResult.courseId}',
originalCourseId: '${unpackResult.courseId}'
},
express: {
url: '${expressApiUrl || 'http://localhost:3000'}'
}
};
</script>
Expand Down Expand Up @@ -485,6 +490,9 @@ async function startStudioUIServer(
name: '${unpackResult.databaseName}',
courseId: '${unpackResult.courseId}',
originalCourseId: '${unpackResult.courseId}'
},
express: {
url: '${expressApiUrl || 'http://localhost:3000'}'
}
};
</script>
Expand Down Expand Up @@ -593,6 +601,9 @@ async function startExpressBackend(
};

try {
// Set NODE_ENV for studio mode authentication bypass
process.env.NODE_ENV = 'studio';

// Create Express app using factory
const app = createExpressApp(config);

Expand Down
1 change: 1 addition & 0 deletions packages/common/src/wire-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export interface PackCourse extends IServerRequest {
type: ServerRequestType.PACK_COURSE;
courseId: string;
outputPath?: string;
couchdbUrl?: string; // Optional full CouchDB connection URL for studio mode
response: {
status: Status;
ok: boolean;
Expand Down
6 changes: 3 additions & 3 deletions packages/db/src/impl/couch/pouchdb-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ PouchDB.plugin(PouchDBAuth);

// Configure PouchDB globally
PouchDB.defaults({
ajax: {
timeout: 60000,
},
// ajax: {
// timeout: 60000,
// },
});

export default PouchDB;
2 changes: 2 additions & 0 deletions packages/db/src/pouch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Export configured PouchDB instance
export { default } from '../impl/couch/pouchdb-setup.js';
1 change: 1 addition & 0 deletions packages/db/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default defineConfig({
entry: [
'src/index.ts',
'src/core/index.ts',
'src/pouch/index.ts',
'src/impl/couch/index.ts',
'src/impl/static/index.ts',
'src/util/packer/index.ts',
Expand Down
3 changes: 2 additions & 1 deletion packages/express/src/app-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ export function createExpressApp(config: AppConfig): express.Application {

body.response = await packCourse({
courseId: body.courseId,
outputPath: body.outputPath
outputPath: body.outputPath,
couchdbUrl: body.couchdbUrl
});
res.json(body.response);
}
Expand Down
36 changes: 27 additions & 9 deletions packages/express/src/client-requests/pack-requests.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Status } from '@vue-skuilder/common';
import logger from '../logger.js';
import ENV from '../utils/env.js';
import PouchDb from 'pouchdb';
import PouchDB from 'pouchdb';

interface PackCourseData {
courseId: string;
outputPath?: string;
couchdbUrl?: string;
}

interface PackCourseResponse {
Expand All @@ -29,9 +30,20 @@ export async function packCourse(data: PackCourseData): Promise<PackCourseRespon
// 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-${data.courseId}`;
// Create database connection URL - use provided couchdbUrl if available (studio mode)
logger.info(`Pack request data: ${JSON.stringify(data, null, 2)}`);
let courseDbUrl: string;
if (data.couchdbUrl) {
courseDbUrl = data.couchdbUrl;
logger.info(`Using provided CouchDB URL: "${courseDbUrl}"`);
} else {
// Fallback to ENV configuration for production mode
logger.info(`ENV values - Protocol: "${ENV.COUCHDB_PROTOCOL}", Admin: "${ENV.COUCHDB_ADMIN}", Password: "${ENV.COUCHDB_PASSWORD}", Server: "${ENV.COUCHDB_SERVER}"`);
const dbUrl = `${ENV.COUCHDB_PROTOCOL}://${ENV.COUCHDB_ADMIN}:${ENV.COUCHDB_PASSWORD}@${ENV.COUCHDB_SERVER}`;
const dbName = `coursedb-${data.courseId}`;
courseDbUrl = `${dbUrl}/${dbName}`;
logger.info(`Constructed dbUrl from ENV: "${courseDbUrl}"`);
}

// Determine output path based on environment and provided path
let outputPath: string;
Expand All @@ -56,7 +68,7 @@ export async function packCourse(data: PackCourseData): Promise<PackCourseRespon
process.cwd();
}

logger.info(`Packing course ${data.courseId} from ${dbName} to ${outputPath}`);
logger.info(`Packing course ${data.courseId} from ${courseDbUrl} to ${outputPath}`);

// Clean up existing output directory for replace-in-place functionality
const fsExtra = await import('fs-extra');
Expand All @@ -72,9 +84,6 @@ export async function packCourse(data: PackCourseData): Promise<PackCourseRespon
// Continue anyway - the write operation might still succeed
}

// Create course database connection
const courseDbUrl = `${dbUrl}/${dbName}`;

// Initialize packer and perform pack operation with file writing
const packer = new CouchDBToStaticPacker();

Expand Down Expand Up @@ -132,7 +141,16 @@ export async function packCourse(data: PackCourseData): Promise<PackCourseRespon
};

const fsAdapter = await createFsAdapter();
const packResult = await packer.packCourseToFiles(new PouchDb(courseDbUrl), data.courseId, outputPath, fsAdapter);

// Use regular PouchDB for simple data reading
logger.info(`Creating PouchDB instance with URL: ${courseDbUrl}`);
logger.info(`PouchDB constructor available: ${typeof PouchDB}`);
logger.info(`PouchDB adapters: ${JSON.stringify(Object.keys((PouchDB as any).adapters || {}))}`);

const courseDb = new PouchDB(courseDbUrl);
logger.info(`PouchDB instance created, adapter: ${(courseDb as any).adapter}`);

const packResult = await packer.packCourseToFiles(courseDb, data.courseId, outputPath, fsAdapter);

const duration = Date.now() - startTime;

Expand Down
26 changes: 24 additions & 2 deletions packages/studio-ui/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { ServerRequest, ServerRequestType, PackCourse } from '@vue-skuilder/common';

async function postWithResult<T extends ServerRequest>(request: Omit<T, 'response' | 'user'>): Promise<T['response']> {
const response = await fetch('http://localhost:3000/', {
// Get Express API URL from studio configuration
const studioConfig = (window as any).STUDIO_CONFIG;
const expressUrl = studioConfig?.express?.url || 'http://localhost:3000/';

console.log('🚀 Sending request to:', expressUrl);
console.log('📦 Request payload:', JSON.stringify(request, null, 2));

const response = await fetch(expressUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -13,18 +20,33 @@ async function postWithResult<T extends ServerRequest>(request: Omit<T, 'respons
throw new Error(`HTTP error! status: ${response.status}`);
}

return await response.json();
const result = await response.json();
console.log('📥 Response received:', JSON.stringify(result, null, 2));
return result;
}

export async function flushCourse(courseId: string, outputPath?: string) {
// Get the original course ID for the output path
// courseId here is the decorated database name, we need the original ID for the path
const studioConfig = (window as any).STUDIO_CONFIG;
console.log('🎯 Studio config:', JSON.stringify(studioConfig, null, 2));

const originalCourseId = studioConfig?.database?.originalCourseId || courseId;
console.log('📋 Original course ID:', originalCourseId);

// Build CouchDB URL from studio configuration with credentials
const couchdbConfig = studioConfig?.couchdb;
const couchdbUrl = couchdbConfig
? `http://${couchdbConfig.username}:${couchdbConfig.password}@${couchdbConfig.url.replace(/^https?:\/\//, '').replace(/\/$/, '')}/coursedb-${courseId}`
: undefined;

console.log('🗄️ CouchDB config:', couchdbConfig);
console.log('🔗 Constructed CouchDB URL:', couchdbUrl);

return await postWithResult<PackCourse>({
type: ServerRequestType.PACK_COURSE,
courseId,
outputPath: outputPath ? outputPath : `./public/static-courses/${originalCourseId}`,
couchdbUrl: couchdbUrl,
});
}
38 changes: 12 additions & 26 deletions packages/studio-ui/src/components/StudioFlush.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
<template>
<v-btn
color="success"
:loading="flushing"
:disabled="flushing"
@click="handleFlush"
>
<v-btn color="success" :loading="flushing" :disabled="flushing" @click="handleFlush">
<v-icon start>mdi-content-save</v-icon>
Flush to Static
</v-btn>
Expand All @@ -16,37 +11,29 @@
<v-icon :color="dialogIcon.color" class="me-2">{{ dialogIcon.icon }}</v-icon>
{{ dialogTitle }}
</v-card-title>

<v-card-text>
<div v-if="flushing">
<v-progress-linear indeterminate class="mb-4" />
<p>{{ flushStatus }}</p>
</div>

<div v-else-if="flushError">
<v-alert type="error" class="mb-4">
{{ flushError }}
</v-alert>
<p>The flush operation failed. Please check the console for more details.</p>
</div>

<div v-else>
<v-alert type="success" class="mb-4">
Course successfully saved to static files!
</v-alert>
<v-alert type="success" class="mb-4"> Course successfully saved to static files! </v-alert>
<p>Your changes have been packed and saved to the course directory.</p>
</div>
</v-card-text>

<v-card-actions>
<v-spacer />
<v-btn
v-if="!flushing"
color="primary"
@click="showDialog = false"
>
Close
</v-btn>
<v-btn v-if="!flushing" color="primary" @click="showDialog = false"> Close </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
Expand Down Expand Up @@ -86,23 +73,22 @@ async function handleFlush() {
flushing.value = true;
flushError.value = null;
showDialog.value = true;

try {
flushStatus.value = 'Connecting to CLI...';

const result = await flushCourse(props.courseId);

if (!result.ok) {
if (result === null) {
throw new Error('null result from flushCourse');
} else if (!result.ok) {
throw new Error(result.errorText ?? 'Unknown error');
}

} catch (error) {
console.error('Flush failed:', error);
flushError.value = error instanceof Error ? error.message : 'Unknown error occurred';
} finally {
flushing.value = false;
}
}


</script>
</script>
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ This project is licensed under:

The following e2e / cypress tests are run nightly on the main branch - a little slow for CI, but good for catching regressions while they are fresh.

These are not meant to be permantently green, but rather as safety indicators for publishing new packages to NPM.

![`platform-ui` E2E](https://github.com/patched-network/vue-skuilder/actions/workflows/nightly-e2e-platform-ui.yml/badge.svg)
![`standalone-ui` E2E](https://github.com/patched-network/vue-skuilder/actions/workflows/nightly-e2e-standalone-ui.yml/badge.svg)
![CLI Regression](https://github.com/patched-network/vue-skuilder/actions/workflows/nightly-cli-regression-test.yml/badge.svg)
Loading