@learnpack/learnpack 5.0.340 → 5.0.342
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.js +17 -4
- package/lib/creatorDist/assets/index-BhqDgBS9.js +76255 -6072
- package/lib/lua/redo.lua +48 -0
- package/lib/lua/saveState.lua +40 -0
- package/lib/lua/undo.lua +50 -0
- package/lib/utils/api.js +10 -1
- package/package.json +2 -2
- package/src/commands/serve.ts +23 -4
- package/src/creatorDist/assets/index-BhqDgBS9.js +76255 -6072
- package/src/utils/api.ts +10 -1
package/lib/lua/redo.lua
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- Lua script to redo the last undone change
|
|
2
|
+
-- Implements Memento pattern with optimistic locking
|
|
3
|
+
--
|
|
4
|
+
-- KEYS[1]: undo:{course}:{exercise}:{lang}
|
|
5
|
+
-- KEYS[2]: redo:{course}:{exercise}:{lang}
|
|
6
|
+
-- KEYS[3]: undo:{course}:{exercise}:{lang}:version
|
|
7
|
+
-- ARGV[1]: current_state_json (state before redo, to save in undo)
|
|
8
|
+
-- ARGV[2]: version_id (current client version for optimistic locking)
|
|
9
|
+
-- ARGV[3]: ttl (432000 seconds = 5 days)
|
|
10
|
+
|
|
11
|
+
-- Get current version
|
|
12
|
+
local current_version = redis.call('GET', KEYS[3])
|
|
13
|
+
|
|
14
|
+
-- Verify version (optimistic locking)
|
|
15
|
+
if current_version ~= false and current_version ~= ARGV[2] then
|
|
16
|
+
return {err = 'VERSION_CONFLICT'}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
-- Verify there are states to redo
|
|
20
|
+
local redo_length = redis.call('LLEN', KEYS[2])
|
|
21
|
+
if redo_length == 0 then
|
|
22
|
+
return {err = 'NO_REDO_AVAILABLE'}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
-- Get future state (the most recent in redo stack)
|
|
26
|
+
local future_state = redis.call('LPOP', KEYS[2])
|
|
27
|
+
|
|
28
|
+
if not future_state then
|
|
29
|
+
return {err = 'NO_FUTURE_STATE'}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
-- Save current state in undo stack
|
|
33
|
+
redis.call('LPUSH', KEYS[1], ARGV[1])
|
|
34
|
+
|
|
35
|
+
-- Maintain state limit in undo (last 5)
|
|
36
|
+
redis.call('LTRIM', KEYS[1], 0, 4)
|
|
37
|
+
|
|
38
|
+
-- Renew TTL of both stacks
|
|
39
|
+
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[3]))
|
|
40
|
+
redis.call('EXPIRE', KEYS[2], tonumber(ARGV[3]))
|
|
41
|
+
|
|
42
|
+
-- Increment version and renew its TTL
|
|
43
|
+
local new_version = redis.call('INCR', KEYS[3])
|
|
44
|
+
redis.call('EXPIRE', KEYS[3], tonumber(ARGV[3]))
|
|
45
|
+
|
|
46
|
+
-- Return array [newVersion, futureState] for compatibility with Redis client
|
|
47
|
+
return {tostring(new_version), future_state}
|
|
48
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- Lua script to save a new state in history
|
|
2
|
+
-- Implements Memento pattern with optimistic locking
|
|
3
|
+
-- NOTE: We save the state BEFORE changes, so users can undo to it
|
|
4
|
+
--
|
|
5
|
+
-- KEYS[1]: undo:{course}:{exercise}:{lang}
|
|
6
|
+
-- KEYS[2]: redo:{course}:{exercise}:{lang}
|
|
7
|
+
-- KEYS[3]: undo:{course}:{exercise}:{lang}:version
|
|
8
|
+
-- ARGV[1]: state_json (complete README BEFORE changes - serialized)
|
|
9
|
+
-- ARGV[2]: version_id (current client version for optimistic locking)
|
|
10
|
+
-- ARGV[3]: ttl (432000 seconds = 5 days)
|
|
11
|
+
-- ARGV[4]: limit (5 states maximum)
|
|
12
|
+
|
|
13
|
+
-- Get current version
|
|
14
|
+
local current_version = redis.call('GET', KEYS[3])
|
|
15
|
+
|
|
16
|
+
-- Verify version (optimistic locking)
|
|
17
|
+
-- If the sent version doesn't match the current one, reject
|
|
18
|
+
if current_version ~= false and current_version ~= ARGV[2] then
|
|
19
|
+
return {err = 'VERSION_CONFLICT'}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
-- Save new state in undo stack (at the beginning)
|
|
23
|
+
redis.call('LPUSH', KEYS[1], ARGV[1])
|
|
24
|
+
|
|
25
|
+
-- Invalidate redo stack (new change invalidates the "alternative future")
|
|
26
|
+
redis.call('DEL', KEYS[2])
|
|
27
|
+
|
|
28
|
+
-- Keep only the last N states (0 to limit-1)
|
|
29
|
+
redis.call('LTRIM', KEYS[1], 0, tonumber(ARGV[4]) - 1)
|
|
30
|
+
|
|
31
|
+
-- Renew TTL of undo stack
|
|
32
|
+
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[3]))
|
|
33
|
+
|
|
34
|
+
-- Increment version and renew its TTL
|
|
35
|
+
local new_version = redis.call('INCR', KEYS[3])
|
|
36
|
+
redis.call('EXPIRE', KEYS[3], tonumber(ARGV[3]))
|
|
37
|
+
|
|
38
|
+
-- Return new version (as string for compatibility with Redis client)
|
|
39
|
+
return tostring(new_version)
|
|
40
|
+
|
package/lib/lua/undo.lua
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- Lua script to undo the last change
|
|
2
|
+
-- Implements Memento pattern with optimistic locking
|
|
3
|
+
-- NOTE: Stack contains states BEFORE changes, so index 0 is what we want to restore
|
|
4
|
+
--
|
|
5
|
+
-- KEYS[1]: undo:{course}:{exercise}:{lang}
|
|
6
|
+
-- KEYS[2]: redo:{course}:{exercise}:{lang}
|
|
7
|
+
-- KEYS[3]: undo:{course}:{exercise}:{lang}:version
|
|
8
|
+
-- ARGV[1]: current_state_json (current state to save in redo for potential redo)
|
|
9
|
+
-- ARGV[2]: version_id (current client version for optimistic locking)
|
|
10
|
+
-- ARGV[3]: ttl (432000 seconds = 5 days)
|
|
11
|
+
|
|
12
|
+
-- Get current version
|
|
13
|
+
local current_version = redis.call('GET', KEYS[3])
|
|
14
|
+
|
|
15
|
+
-- Verify version (optimistic locking)
|
|
16
|
+
if current_version ~= false and current_version ~= ARGV[2] then
|
|
17
|
+
return {err = 'VERSION_CONFLICT'}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
-- Verify there are states to undo
|
|
21
|
+
-- We need at least 1 element (the state before the current change)
|
|
22
|
+
local undo_length = redis.call('LLEN', KEYS[1])
|
|
23
|
+
if undo_length < 1 then
|
|
24
|
+
return {err = 'NO_UNDO_AVAILABLE'}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
-- Get the saved state (index 0 - the state before current changes)
|
|
28
|
+
local previous_state = redis.call('LINDEX', KEYS[1], 0)
|
|
29
|
+
|
|
30
|
+
if not previous_state then
|
|
31
|
+
return {err = 'NO_PREVIOUS_STATE'}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
-- Move current state to redo stack
|
|
35
|
+
redis.call('LPUSH', KEYS[2], ARGV[1])
|
|
36
|
+
|
|
37
|
+
-- Remove current state from undo stack (index 0)
|
|
38
|
+
redis.call('LPOP', KEYS[1])
|
|
39
|
+
|
|
40
|
+
-- Renew TTL of both stacks
|
|
41
|
+
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[3]))
|
|
42
|
+
redis.call('EXPIRE', KEYS[2], tonumber(ARGV[3]))
|
|
43
|
+
|
|
44
|
+
-- Increment version and renew its TTL
|
|
45
|
+
local new_version = redis.call('INCR', KEYS[3])
|
|
46
|
+
redis.call('EXPIRE', KEYS[3], tonumber(ARGV[3]))
|
|
47
|
+
|
|
48
|
+
-- Return array [newVersion, previousState] for compatibility with Redis client
|
|
49
|
+
return {tostring(new_version), previous_state}
|
|
50
|
+
|
package/lib/utils/api.js
CHANGED
|
@@ -460,8 +460,17 @@ const createRigoPackage = async (token, slug, config) => {
|
|
|
460
460
|
}
|
|
461
461
|
};
|
|
462
462
|
let technologiesCache = [];
|
|
463
|
+
/** Strip .env comments (e.g. "token # comment") so the token is sent without spaces. */
|
|
464
|
+
function sanitizeToken(token) {
|
|
465
|
+
if (!token)
|
|
466
|
+
return "";
|
|
467
|
+
const trimmed = token.trim();
|
|
468
|
+
const beforeComment = trimmed.split(/\s+#/)[0].trim();
|
|
469
|
+
return beforeComment;
|
|
470
|
+
}
|
|
463
471
|
const fetchTechnologies = async () => {
|
|
464
|
-
const
|
|
472
|
+
const rawToken = process.env.BREATHECODE_PERMANENT_TOKEN;
|
|
473
|
+
const BREATHECODE_PERMANENT_TOKEN = sanitizeToken(rawToken);
|
|
465
474
|
const LANGS = ["en", "es"];
|
|
466
475
|
if (!BREATHECODE_PERMANENT_TOKEN) {
|
|
467
476
|
throw new Error("BREATHECODE_PERMANENT_TOKEN is not defined in environment variables");
|
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.342",
|
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
|
6
6
|
"contributors": [
|
|
7
7
|
{
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
]
|
|
162
162
|
},
|
|
163
163
|
"scripts": {
|
|
164
|
-
"copy-assets": "npx cpy src/creatorDist/**/* lib/creatorDist --parents --verbose && npx cpy src/utils/templates/**/* lib/utils/templates --parents --verbose",
|
|
164
|
+
"copy-assets": "npx cpy src/creatorDist/**/* lib/creatorDist --parents --verbose && npx cpy src/utils/templates/**/* lib/utils/templates --parents --verbose && npx cpy src/lua/**/* lib/lua --parents --verbose",
|
|
165
165
|
"tsc": "tsc -b",
|
|
166
166
|
"postpack": "rm -f oclif.manifest.json && eslint . --ext .ts --config .eslintrc",
|
|
167
167
|
"prepack": "rm -rf lib && tsc -b && npm run copy-assets",
|
package/src/commands/serve.ts
CHANGED
|
@@ -1184,7 +1184,8 @@ class ServeCommand extends SessionCommand {
|
|
|
1184
1184
|
process.env.GCP_BUCKET_NAME || "learnpack-packages"
|
|
1185
1185
|
)
|
|
1186
1186
|
|
|
1187
|
-
const
|
|
1187
|
+
const rawHost = process.env.HOST || ""
|
|
1188
|
+
const host = rawHost.trim().split(/\s+#/)[0].trim()
|
|
1188
1189
|
|
|
1189
1190
|
if (!host) {
|
|
1190
1191
|
console.log(
|
|
@@ -1196,15 +1197,31 @@ class ServeCommand extends SessionCommand {
|
|
|
1196
1197
|
}
|
|
1197
1198
|
|
|
1198
1199
|
// Initialize Redis (official client)
|
|
1199
|
-
|
|
1200
|
+
let redisUrl = (process.env.REDIS_URL || "redis://localhost:6379").trim()
|
|
1201
|
+
// Strip surrounding quotes if present (e.g. from .env)
|
|
1202
|
+
if (
|
|
1203
|
+
(redisUrl.startsWith('"') && redisUrl.endsWith('"')) ||
|
|
1204
|
+
(redisUrl.startsWith("'") && redisUrl.endsWith("'"))
|
|
1205
|
+
) {
|
|
1206
|
+
redisUrl = redisUrl.slice(1, -1)
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const isPlaceholderRedisUrl =
|
|
1210
|
+
redisUrl.includes("your_production_password") ||
|
|
1211
|
+
redisUrl.includes("your-redis-instance.cloud")
|
|
1200
1212
|
|
|
1201
1213
|
if (
|
|
1202
1214
|
process.env.NODE_ENV === "production" &&
|
|
1203
|
-
redisUrl === "redis://localhost:6379"
|
|
1215
|
+
(redisUrl === "redis://localhost:6379" || isPlaceholderRedisUrl)
|
|
1204
1216
|
) {
|
|
1205
1217
|
console.error("❌ REDIS_URL not configured for production environment!")
|
|
1206
1218
|
console.warn("⚠️ History features will be unavailable")
|
|
1207
1219
|
this.redis = null
|
|
1220
|
+
} else if (isPlaceholderRedisUrl) {
|
|
1221
|
+
console.warn(
|
|
1222
|
+
"⚠️ REDIS_URL looks like a placeholder; skipping Redis. History features (undo/redo) will be unavailable"
|
|
1223
|
+
)
|
|
1224
|
+
this.redis = null
|
|
1208
1225
|
} else {
|
|
1209
1226
|
try {
|
|
1210
1227
|
const useTLS = redisUrl.startsWith("rediss://")
|
|
@@ -1262,7 +1279,9 @@ class ServeCommand extends SessionCommand {
|
|
|
1262
1279
|
const server = http.createServer(app)
|
|
1263
1280
|
initSocketIO(server)
|
|
1264
1281
|
|
|
1265
|
-
const
|
|
1282
|
+
const rawPort = process.env.PORT || "3000"
|
|
1283
|
+
const PORT =
|
|
1284
|
+
parseInt(String(rawPort).trim().split(/\s+#/)[0].trim(), 10) || 3000
|
|
1266
1285
|
|
|
1267
1286
|
const distPath = path.resolve(__dirname, "../creatorDist")
|
|
1268
1287
|
|