@learnpack/learnpack 5.0.328 → 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/publish.js +2 -0
- 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/lib/utils/api.d.ts +1 -0
- package/lib/utils/api.js +1 -1
- package/package.json +2 -1
- package/src/commands/publish.ts +2 -0
- 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
- package/src/utils/api.ts +2 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HistoryManager = void 0;
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
class HistoryManager {
|
|
7
|
+
constructor(redisClient) {
|
|
8
|
+
this.TTL = "432000"; // 5 days in seconds
|
|
9
|
+
this.LIMIT = 5; // Keep last 5 states
|
|
10
|
+
this.redis = redisClient;
|
|
11
|
+
// Load Lua scripts
|
|
12
|
+
const luaDir = path.join(__dirname, "../lua");
|
|
13
|
+
this.saveStateLua = fs.readFileSync(path.join(luaDir, "saveState.lua"), "utf8");
|
|
14
|
+
this.undoLua = fs.readFileSync(path.join(luaDir, "undo.lua"), "utf8");
|
|
15
|
+
this.redoLua = fs.readFileSync(path.join(luaDir, "redo.lua"), "utf8");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Builds Redis keys for an exercise in a specific language
|
|
19
|
+
* @param courseSlug - The course slug identifier
|
|
20
|
+
* @param exerciseSlug - The exercise slug identifier
|
|
21
|
+
* @param lang - The language code
|
|
22
|
+
* @returns Object containing undo, redo, and version keys
|
|
23
|
+
*/
|
|
24
|
+
getKeys(courseSlug, exerciseSlug, lang) {
|
|
25
|
+
const baseKey = `${courseSlug}:${exerciseSlug}:${lang}`;
|
|
26
|
+
return {
|
|
27
|
+
undo: `undo:${baseKey}`,
|
|
28
|
+
redo: `redo:${baseKey}`,
|
|
29
|
+
version: `undo:${baseKey}:version`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Saves a new state in history
|
|
34
|
+
* @param courseSlug - The course slug identifier
|
|
35
|
+
* @param exerciseSlug - The exercise slug identifier
|
|
36
|
+
* @param lang - The language code
|
|
37
|
+
* @param content - The content of the state
|
|
38
|
+
* @param versionId - The version ID
|
|
39
|
+
* @returns Object containing success, newVersion, and error
|
|
40
|
+
*/
|
|
41
|
+
async saveState(courseSlug, exerciseSlug, lang, content, versionId) {
|
|
42
|
+
const keys = this.getKeys(courseSlug, exerciseSlug, lang);
|
|
43
|
+
const state = {
|
|
44
|
+
content,
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
};
|
|
47
|
+
const stateJson = JSON.stringify(state);
|
|
48
|
+
try {
|
|
49
|
+
// Official Redis client syntax: eval(script, { keys: [...], arguments: [...] })
|
|
50
|
+
const result = await this.redis.eval(this.saveStateLua, {
|
|
51
|
+
keys: [keys.undo, keys.redo, keys.version],
|
|
52
|
+
arguments: [
|
|
53
|
+
stateJson,
|
|
54
|
+
versionId,
|
|
55
|
+
this.TTL.toString(),
|
|
56
|
+
this.LIMIT.toString(),
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
// Check if it's an error response (table with err field)
|
|
60
|
+
if (result && typeof result === "object" && result.err) {
|
|
61
|
+
return { success: false, error: result.err };
|
|
62
|
+
}
|
|
63
|
+
// Success response is a string (the new version)
|
|
64
|
+
const newVersion = typeof result === "string" ? result : String(result);
|
|
65
|
+
return { success: true, newVersion };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error("⏮️ HISTORY: Error saving state to Redis:", error);
|
|
69
|
+
return { success: false, error: error.message };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Performs an undo operation
|
|
74
|
+
* @param courseSlug - The course slug identifier
|
|
75
|
+
* @param exerciseSlug - The exercise slug identifier
|
|
76
|
+
* @param lang - The language code
|
|
77
|
+
* @param currentContent - The current content of the state
|
|
78
|
+
* @param versionId - The version ID
|
|
79
|
+
* @returns Object containing success, newVersion, and error
|
|
80
|
+
*/
|
|
81
|
+
async undo(courseSlug, exerciseSlug, lang, currentContent, versionId) {
|
|
82
|
+
const keys = this.getKeys(courseSlug, exerciseSlug, lang);
|
|
83
|
+
const currentState = {
|
|
84
|
+
content: currentContent,
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
const result = await this.redis.eval(this.undoLua, {
|
|
89
|
+
keys: [keys.undo, keys.redo, keys.version],
|
|
90
|
+
arguments: [
|
|
91
|
+
JSON.stringify(currentState),
|
|
92
|
+
versionId,
|
|
93
|
+
this.TTL.toString(),
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
// Check if it's an error response (table with err field)
|
|
97
|
+
if (result && typeof result === "object" && result.err) {
|
|
98
|
+
return { success: false, error: result.err };
|
|
99
|
+
}
|
|
100
|
+
// Success response is an array: [newVersion, previousState]
|
|
101
|
+
if (!Array.isArray(result) || result.length < 2) {
|
|
102
|
+
console.error("⏮️ HISTORY: Invalid response from undo script:", result);
|
|
103
|
+
return { success: false, error: "INVALID_RESPONSE" };
|
|
104
|
+
}
|
|
105
|
+
const [newVersion, previousStateJson] = result;
|
|
106
|
+
if (!previousStateJson) {
|
|
107
|
+
console.error("⏮️ HISTORY: No state returned from undo script");
|
|
108
|
+
return { success: false, error: "NO_STATE_RETURNED" };
|
|
109
|
+
}
|
|
110
|
+
const previousState = JSON.parse(previousStateJson);
|
|
111
|
+
return {
|
|
112
|
+
success: true,
|
|
113
|
+
newVersion: String(newVersion),
|
|
114
|
+
content: previousState.content,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error("⏮️ HISTORY: Error performing undo:", error);
|
|
119
|
+
return { success: false, error: error.message };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Performs a redo operation
|
|
124
|
+
* @param courseSlug - The course slug identifier
|
|
125
|
+
* @param exerciseSlug - The exercise slug identifier
|
|
126
|
+
* @param lang - The language code
|
|
127
|
+
* @param currentContent - The current content of the state
|
|
128
|
+
* @param versionId - The version ID
|
|
129
|
+
* @returns Object containing success, newVersion, and error
|
|
130
|
+
*/
|
|
131
|
+
async redo(courseSlug, exerciseSlug, lang, currentContent, versionId) {
|
|
132
|
+
const keys = this.getKeys(courseSlug, exerciseSlug, lang);
|
|
133
|
+
const currentState = {
|
|
134
|
+
content: currentContent,
|
|
135
|
+
timestamp: Date.now(),
|
|
136
|
+
};
|
|
137
|
+
try {
|
|
138
|
+
const result = await this.redis.eval(this.redoLua, {
|
|
139
|
+
keys: [keys.undo, keys.redo, keys.version],
|
|
140
|
+
arguments: [
|
|
141
|
+
JSON.stringify(currentState),
|
|
142
|
+
versionId,
|
|
143
|
+
this.TTL.toString(),
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
// Check if it's an error response (table with err field)
|
|
147
|
+
if (result && typeof result === "object" && result.err) {
|
|
148
|
+
return { success: false, error: result.err };
|
|
149
|
+
}
|
|
150
|
+
// Success response is an array: [newVersion, futureState]
|
|
151
|
+
if (!Array.isArray(result) || result.length < 2) {
|
|
152
|
+
console.error("⏮️ HISTORY: Invalid response from redo script:", result);
|
|
153
|
+
return { success: false, error: "INVALID_RESPONSE" };
|
|
154
|
+
}
|
|
155
|
+
const [newVersion, futureStateJson] = result;
|
|
156
|
+
if (!futureStateJson) {
|
|
157
|
+
console.error("⏮️ HISTORY: No state returned from redo script");
|
|
158
|
+
return { success: false, error: "NO_STATE_RETURNED" };
|
|
159
|
+
}
|
|
160
|
+
const futureState = JSON.parse(futureStateJson);
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
newVersion: String(newVersion),
|
|
164
|
+
content: futureState.content,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error("⏮️ HISTORY: Error performing redo:", error);
|
|
169
|
+
return { success: false, error: error.message };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Gets the history status (can undo/redo, current version)
|
|
174
|
+
* @param courseSlug - The course slug identifier
|
|
175
|
+
* @param exerciseSlug - The exercise slug identifier
|
|
176
|
+
* @param lang - The language code
|
|
177
|
+
* @returns Object containing canUndo, canRedo, and version
|
|
178
|
+
*/
|
|
179
|
+
async getHistoryStatus(courseSlug, exerciseSlug, lang) {
|
|
180
|
+
const keys = this.getKeys(courseSlug, exerciseSlug, lang);
|
|
181
|
+
try {
|
|
182
|
+
const [undoLen, redoLen, version] = await Promise.all([
|
|
183
|
+
this.redis.lLen(keys.undo),
|
|
184
|
+
this.redis.lLen(keys.redo),
|
|
185
|
+
this.redis.get(keys.version),
|
|
186
|
+
]);
|
|
187
|
+
return {
|
|
188
|
+
canUndo: undoLen >= 1, // Need at least 1 (we save state BEFORE changes)
|
|
189
|
+
canRedo: redoLen > 0,
|
|
190
|
+
version: version || "0",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.error("⏮️ HISTORY: Error getting history status:", error);
|
|
195
|
+
return {
|
|
196
|
+
canUndo: false,
|
|
197
|
+
canRedo: false,
|
|
198
|
+
version: "0",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
exports.HistoryManager = HistoryManager;
|
|
@@ -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/lib/utils/api.d.ts
CHANGED
package/lib/utils/api.js
CHANGED
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",
|
package/src/commands/publish.ts
CHANGED
|
@@ -47,6 +47,7 @@ export const handleAssetCreation = async (
|
|
|
47
47
|
slug: slug,
|
|
48
48
|
title: learnJson.title[selectedLang],
|
|
49
49
|
lang: selectedLang,
|
|
50
|
+
graded: true,
|
|
50
51
|
description: learnJson.description[selectedLang],
|
|
51
52
|
learnpack_deploy_url: learnpackDeployUrl,
|
|
52
53
|
technologies: learnJson.technologies.map((tech: string) =>
|
|
@@ -78,6 +79,7 @@ export const handleAssetCreation = async (
|
|
|
78
79
|
|
|
79
80
|
Console.info("Asset exists, updating it")
|
|
80
81
|
const asset = await api.updateAsset(sessionPayload.token, slug, {
|
|
82
|
+
graded: true,
|
|
81
83
|
learnpack_deploy_url: learnpackDeployUrl,
|
|
82
84
|
title: learnJson.title[selectedLang],
|
|
83
85
|
category: category,
|