@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.
@@ -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.329",
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",