@learnpack/learnpack 5.0.329 → 5.0.331
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/lib/commands/serve.d.ts +3 -1
- package/lib/commands/serve.js +306 -15
- package/lib/managers/historyManager.d.ts +67 -0
- package/lib/managers/historyManager.js +203 -0
- package/lib/managers/readmeHistoryService.d.ts +76 -0
- package/lib/managers/readmeHistoryService.js +191 -0
- package/package.json +2 -1
- package/src/commands/serve.ts +452 -20
- package/src/lua/redo.lua +48 -0
- package/src/lua/saveState.lua +40 -0
- package/src/lua/undo.lua +50 -0
- package/src/managers/historyManager.ts +281 -0
- package/src/managers/readmeHistoryService.ts +284 -0
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +2090 -2090
- package/src/ui/app.tar.gz +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Bucket } from "@google-cloud/storage";
|
|
2
|
+
import type { HistoryManager } from "./historyManager";
|
|
3
|
+
export interface SaveReadmeWithHistoryOptions {
|
|
4
|
+
courseSlug: string;
|
|
5
|
+
exerciseSlug: string;
|
|
6
|
+
lang: string;
|
|
7
|
+
fileName: string;
|
|
8
|
+
newContent: string;
|
|
9
|
+
previousContent?: string;
|
|
10
|
+
currentVersion?: string;
|
|
11
|
+
throwOnConflict?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SaveReadmeResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
newVersion: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Service to handle README file updates with automatic history tracking
|
|
20
|
+
* Encapsulates the logic of saving state to history before updating files
|
|
21
|
+
*/
|
|
22
|
+
export declare class ReadmeHistoryService {
|
|
23
|
+
private historyManager;
|
|
24
|
+
private bucket;
|
|
25
|
+
/**
|
|
26
|
+
* Constructor for the ReadmeHistoryService
|
|
27
|
+
* @param historyManager - The history manager to use
|
|
28
|
+
* @param bucket - The bucket to use
|
|
29
|
+
*/
|
|
30
|
+
constructor(historyManager: HistoryManager | null, bucket: Bucket);
|
|
31
|
+
/**
|
|
32
|
+
* Saves a README file to GCS with automatic history tracking
|
|
33
|
+
*
|
|
34
|
+
* Workflow:
|
|
35
|
+
* 1. If previousContent is not provided, downloads it from GCS
|
|
36
|
+
* 2. If currentVersion is not provided, gets it from history
|
|
37
|
+
* 3. Saves the PREVIOUS state to history (for undo)
|
|
38
|
+
* 4. Saves the NEW content to GCS
|
|
39
|
+
*
|
|
40
|
+
* @param options - Options for the saveReadmeWithHistory method
|
|
41
|
+
* @returns Result with success status and new version
|
|
42
|
+
*/
|
|
43
|
+
saveReadmeWithHistory(options: SaveReadmeWithHistoryOptions): Promise<SaveReadmeResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Gets the previous content of a README from GCS
|
|
46
|
+
* Returns undefined if file doesn't exist (first version)
|
|
47
|
+
* @param courseSlug - The slug of the course
|
|
48
|
+
* @param exerciseSlug - The slug of the exercise
|
|
49
|
+
* @param fileName - The name of the file
|
|
50
|
+
* @returns The previous content of the file or undefined if it doesn't exist
|
|
51
|
+
*/
|
|
52
|
+
private getPreviousContent;
|
|
53
|
+
/**
|
|
54
|
+
* Gets the current version from history
|
|
55
|
+
* @param courseSlug - The slug of the course
|
|
56
|
+
* @param exerciseSlug - The slug of the exercise
|
|
57
|
+
* @param lang - The language of the file
|
|
58
|
+
* @returns The current version of the file
|
|
59
|
+
*/
|
|
60
|
+
private getCurrentVersion;
|
|
61
|
+
/**
|
|
62
|
+
* Saves content to Google Cloud Storage
|
|
63
|
+
* @param courseSlug - The slug of the course
|
|
64
|
+
* @param exerciseSlug - The slug of the exercise
|
|
65
|
+
* @param fileName - The name of the file
|
|
66
|
+
* @param content - The content to save
|
|
67
|
+
* @returns void
|
|
68
|
+
*/
|
|
69
|
+
private saveToGCS;
|
|
70
|
+
/**
|
|
71
|
+
* Helper to get README extension from language code
|
|
72
|
+
* @param lang - The language of the file
|
|
73
|
+
* @returns The name of the README file
|
|
74
|
+
*/
|
|
75
|
+
static getReadmeFileName(lang: string): string;
|
|
76
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReadmeHistoryService = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Service to handle README file updates with automatic history tracking
|
|
6
|
+
* Encapsulates the logic of saving state to history before updating files
|
|
7
|
+
*/
|
|
8
|
+
class ReadmeHistoryService {
|
|
9
|
+
/**
|
|
10
|
+
* Constructor for the ReadmeHistoryService
|
|
11
|
+
* @param historyManager - The history manager to use
|
|
12
|
+
* @param bucket - The bucket to use
|
|
13
|
+
*/
|
|
14
|
+
constructor(historyManager, bucket) {
|
|
15
|
+
this.historyManager = historyManager;
|
|
16
|
+
this.bucket = bucket;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Saves a README file to GCS with automatic history tracking
|
|
20
|
+
*
|
|
21
|
+
* Workflow:
|
|
22
|
+
* 1. If previousContent is not provided, downloads it from GCS
|
|
23
|
+
* 2. If currentVersion is not provided, gets it from history
|
|
24
|
+
* 3. Saves the PREVIOUS state to history (for undo)
|
|
25
|
+
* 4. Saves the NEW content to GCS
|
|
26
|
+
*
|
|
27
|
+
* @param options - Options for the saveReadmeWithHistory method
|
|
28
|
+
* @returns Result with success status and new version
|
|
29
|
+
*/
|
|
30
|
+
async saveReadmeWithHistory(options) {
|
|
31
|
+
const { courseSlug, exerciseSlug, lang, fileName, newContent, previousContent: providedPreviousContent, currentVersion: providedVersion, throwOnConflict = false, } = options;
|
|
32
|
+
// Guard: Only process README files
|
|
33
|
+
if (!fileName.startsWith("README")) {
|
|
34
|
+
console.warn(`⚠️ ReadmeHistoryService called for non-README file: ${fileName}`);
|
|
35
|
+
return { success: false, newVersion: "0", error: "NOT_README_FILE" };
|
|
36
|
+
}
|
|
37
|
+
// Guard: History manager not available
|
|
38
|
+
if (!this.historyManager) {
|
|
39
|
+
console.warn("⚠️ History manager not available, skipping history");
|
|
40
|
+
// Still save the file without history
|
|
41
|
+
await this.saveToGCS(courseSlug, exerciseSlug, fileName, newContent);
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
newVersion: "0",
|
|
45
|
+
error: "HISTORY_NOT_AVAILABLE",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
// Step 1: Get previous content if not provided
|
|
50
|
+
const previousContent = providedPreviousContent !== null && providedPreviousContent !== void 0 ? providedPreviousContent : (await this.getPreviousContent(courseSlug, exerciseSlug, fileName));
|
|
51
|
+
// If there's no previous content, we can't save to history
|
|
52
|
+
// (this is the first version of the file)
|
|
53
|
+
if (!previousContent) {
|
|
54
|
+
console.log(`📝 HISTORY: First version of ${fileName} for ${lang}, skipping history`);
|
|
55
|
+
await this.saveToGCS(courseSlug, exerciseSlug, fileName, newContent);
|
|
56
|
+
return {
|
|
57
|
+
success: false,
|
|
58
|
+
newVersion: "0",
|
|
59
|
+
error: "NO_PREVIOUS_CONTENT",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Step 2: Get current version if not provided
|
|
63
|
+
const currentVersion = providedVersion !== null && providedVersion !== void 0 ? providedVersion : (await this.getCurrentVersion(courseSlug, exerciseSlug, lang));
|
|
64
|
+
// Step 3: Save to history BEFORE updating the file
|
|
65
|
+
const historyResult = await this.historyManager.saveState(courseSlug, exerciseSlug, lang, previousContent, // Save the content BEFORE the change
|
|
66
|
+
currentVersion);
|
|
67
|
+
// Step 4: Handle history save result
|
|
68
|
+
if (!historyResult.success) {
|
|
69
|
+
// VERSION_CONFLICT: Another user/process modified the file
|
|
70
|
+
if (historyResult.error === "VERSION_CONFLICT") {
|
|
71
|
+
const message = "Version conflict detected. Content may have been modified by another process.";
|
|
72
|
+
if (throwOnConflict) {
|
|
73
|
+
throw new Error(message);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.error(`⏮️ HISTORY: ${message}`);
|
|
77
|
+
// Still save the file despite conflict
|
|
78
|
+
await this.saveToGCS(courseSlug, exerciseSlug, fileName, newContent);
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
newVersion: currentVersion,
|
|
82
|
+
error: "VERSION_CONFLICT",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Other errors: log but don't fail
|
|
87
|
+
console.error(`⏮️ HISTORY: Failed to save history (non-critical): ${historyResult.error}`);
|
|
88
|
+
await this.saveToGCS(courseSlug, exerciseSlug, fileName, newContent);
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
newVersion: currentVersion,
|
|
92
|
+
error: historyResult.error,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Step 5: History saved successfully, now save the new content
|
|
96
|
+
await this.saveToGCS(courseSlug, exerciseSlug, fileName, newContent);
|
|
97
|
+
console.log(`✅ HISTORY: Saved ${fileName} (${lang}) - version ${historyResult.newVersion}`);
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
newVersion: historyResult.newVersion,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const errorMessage = error.message || "Unknown error";
|
|
105
|
+
console.error(`❌ HISTORY: Error in saveReadmeWithHistory: ${errorMessage}`);
|
|
106
|
+
// Try to save the file anyway (best effort)
|
|
107
|
+
try {
|
|
108
|
+
await this.saveToGCS(courseSlug, exerciseSlug, fileName, newContent);
|
|
109
|
+
}
|
|
110
|
+
catch (saveError) {
|
|
111
|
+
console.error("❌ Failed to save file after history error:", saveError);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
newVersion: "0",
|
|
116
|
+
error: errorMessage,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Gets the previous content of a README from GCS
|
|
122
|
+
* Returns undefined if file doesn't exist (first version)
|
|
123
|
+
* @param courseSlug - The slug of the course
|
|
124
|
+
* @param exerciseSlug - The slug of the exercise
|
|
125
|
+
* @param fileName - The name of the file
|
|
126
|
+
* @returns The previous content of the file or undefined if it doesn't exist
|
|
127
|
+
*/
|
|
128
|
+
async getPreviousContent(courseSlug, exerciseSlug, fileName) {
|
|
129
|
+
try {
|
|
130
|
+
const filePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${fileName}`;
|
|
131
|
+
const [content] = await this.bucket.file(filePath).download();
|
|
132
|
+
return content.toString();
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
// File doesn't exist (404) - this is normal for first save
|
|
136
|
+
if (error.code === 404) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
console.error(`⚠️ Error downloading previous content: ${error.message}`);
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Gets the current version from history
|
|
145
|
+
* @param courseSlug - The slug of the course
|
|
146
|
+
* @param exerciseSlug - The slug of the exercise
|
|
147
|
+
* @param lang - The language of the file
|
|
148
|
+
* @returns The current version of the file
|
|
149
|
+
*/
|
|
150
|
+
async getCurrentVersion(courseSlug, exerciseSlug, lang) {
|
|
151
|
+
if (!this.historyManager) {
|
|
152
|
+
return "0";
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const status = await this.historyManager.getHistoryStatus(courseSlug, exerciseSlug, lang);
|
|
156
|
+
return status.version;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
console.error("⚠️ Error getting current version, defaulting to '0':", error);
|
|
160
|
+
return "0";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Saves content to Google Cloud Storage
|
|
165
|
+
* @param courseSlug - The slug of the course
|
|
166
|
+
* @param exerciseSlug - The slug of the exercise
|
|
167
|
+
* @param fileName - The name of the file
|
|
168
|
+
* @param content - The content to save
|
|
169
|
+
* @returns void
|
|
170
|
+
*/
|
|
171
|
+
async saveToGCS(courseSlug, exerciseSlug, fileName, content) {
|
|
172
|
+
const filePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${fileName}`;
|
|
173
|
+
const file = this.bucket.file(filePath);
|
|
174
|
+
await file.save(content, {
|
|
175
|
+
resumable: false,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Helper to get README extension from language code
|
|
180
|
+
* @param lang - The language of the file
|
|
181
|
+
* @returns The name of the README file
|
|
182
|
+
*/
|
|
183
|
+
static getReadmeFileName(lang) {
|
|
184
|
+
if (lang === "en") {
|
|
185
|
+
return "README.md";
|
|
186
|
+
}
|
|
187
|
+
// es -> README.es.md, fr -> README.fr.md
|
|
188
|
+
return `README.${lang}.md`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.ReadmeHistoryService = ReadmeHistoryService;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learnpack/learnpack",
|
|
3
3
|
"description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
|
|
4
|
-
"version": "5.0.
|
|
4
|
+
"version": "5.0.331",
|
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
|
6
6
|
"contributors": [
|
|
7
7
|
{
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"node-persist": "^3.1.0",
|
|
63
63
|
"ora": "^8.2.0",
|
|
64
64
|
"prompts": "^2.3.2",
|
|
65
|
+
"redis": "^5.10.0",
|
|
65
66
|
"rimraf": "^6.0.1",
|
|
66
67
|
"shelljs": "^0.8.4",
|
|
67
68
|
"socket.io": "^4.4.1",
|