@learnpack/learnpack 5.0.355 → 5.0.358

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.
Files changed (127) hide show
  1. package/lib/commands/publish.js +2 -2
  2. package/lib/commands/serve.d.ts +0 -17
  3. package/lib/commands/serve.js +252 -147
  4. package/lib/creatorDist/assets/{index-DnthLsvb.js → index-D6pmbMe9.js} +14030 -13903
  5. package/lib/creatorDist/assets/index-zrPponAn.css +1701 -0
  6. package/lib/creatorDist/index.html +2 -2
  7. package/lib/models/creator.d.ts +1 -1
  8. package/lib/services/ingest/adapters/gcsCourseStorage.d.ts +13 -0
  9. package/lib/services/ingest/adapters/gcsCourseStorage.js +58 -0
  10. package/lib/services/ingest/adapters/rigoUserTokenAuth.d.ts +15 -0
  11. package/lib/services/ingest/adapters/rigoUserTokenAuth.js +46 -0
  12. package/lib/services/ingest/adapters/rigobotPackageRegistry.d.ts +7 -0
  13. package/lib/services/ingest/adapters/rigobotPackageRegistry.js +119 -0
  14. package/lib/services/ingest/core/buildIngestConfig.d.ts +27 -0
  15. package/lib/services/ingest/core/buildIngestConfig.js +40 -0
  16. package/lib/services/ingest/core/buildInitialSyllabus.d.ts +24 -0
  17. package/lib/services/ingest/core/buildInitialSyllabus.js +104 -0
  18. package/lib/services/ingest/core/buildSidebar.d.ts +13 -0
  19. package/lib/services/ingest/core/buildSidebar.js +28 -0
  20. package/lib/services/ingest/core/coursePaths.d.ts +31 -0
  21. package/lib/services/ingest/core/coursePaths.js +50 -0
  22. package/lib/services/ingest/core/ingestCoursePackage.d.ts +52 -0
  23. package/lib/services/ingest/core/ingestCoursePackage.js +217 -0
  24. package/lib/services/ingest/core/normalizeLearnJson.d.ts +25 -0
  25. package/lib/services/ingest/core/normalizeLearnJson.js +29 -0
  26. package/lib/services/ingest/core/pairLessons.d.ts +29 -0
  27. package/lib/services/ingest/core/pairLessons.js +102 -0
  28. package/lib/services/ingest/core/planWrites.d.ts +44 -0
  29. package/lib/services/ingest/core/planWrites.js +59 -0
  30. package/lib/services/ingest/core/resolveSlug.d.ts +53 -0
  31. package/lib/services/ingest/core/resolveSlug.js +93 -0
  32. package/lib/services/ingest/core/types.d.ts +53 -0
  33. package/lib/services/ingest/core/types.js +2 -0
  34. package/lib/services/ingest/core/validatePackage.d.ts +38 -0
  35. package/lib/services/ingest/core/validatePackage.js +97 -0
  36. package/lib/services/ingest/createIngestService.d.ts +34 -0
  37. package/lib/services/ingest/createIngestService.js +38 -0
  38. package/lib/services/ingest/errors.d.ts +22 -0
  39. package/lib/services/ingest/errors.js +34 -0
  40. package/lib/services/ingest/http/errorMapping.d.ts +8 -0
  41. package/lib/services/ingest/http/errorMapping.js +40 -0
  42. package/lib/services/ingest/http/router.d.ts +32 -0
  43. package/lib/services/ingest/http/router.js +124 -0
  44. package/lib/services/ingest/http/zipPackageReader.d.ts +26 -0
  45. package/lib/services/ingest/http/zipPackageReader.js +62 -0
  46. package/lib/services/ingest/ports/courseStorage.d.ts +25 -0
  47. package/lib/services/ingest/ports/courseStorage.js +2 -0
  48. package/lib/services/ingest/ports/packageRegistry.d.ts +58 -0
  49. package/lib/services/ingest/ports/packageRegistry.js +2 -0
  50. package/lib/services/ingest/ports/requestAuthenticator.d.ts +25 -0
  51. package/lib/services/ingest/ports/requestAuthenticator.js +2 -0
  52. package/lib/ui/static/og-image.png +0 -0
  53. package/lib/utils/branding.d.ts +43 -0
  54. package/lib/utils/branding.js +75 -0
  55. package/lib/utils/coursePackage/bucketIo.d.ts +23 -0
  56. package/lib/utils/coursePackage/bucketIo.js +36 -0
  57. package/lib/utils/coursePackage/learnJson.d.ts +26 -0
  58. package/lib/utils/coursePackage/learnJson.js +36 -0
  59. package/lib/utils/coursePackage/sidebar.d.ts +15 -0
  60. package/lib/utils/coursePackage/sidebar.js +26 -0
  61. package/lib/utils/creatorSocket.js +15 -21
  62. package/lib/utils/gcpCredentials.d.ts +18 -0
  63. package/lib/utils/gcpCredentials.js +27 -0
  64. package/lib/utils/rigoActions.d.ts +16 -0
  65. package/lib/utils/rigoActions.js +102 -1
  66. package/lib/utils/socketRegistry.d.ts +46 -0
  67. package/lib/utils/socketRegistry.js +83 -0
  68. package/package.json +2 -2
  69. package/src/commands/publish.ts +2 -3
  70. package/src/commands/serve.ts +297 -136
  71. package/src/creator/README.md +63 -51
  72. package/src/creator/package-lock.json +1188 -8
  73. package/src/creator/package.json +5 -1
  74. package/src/creator/src/App.tsx +120 -54
  75. package/src/creator/src/components/FileUploader.tsx +1 -12
  76. package/src/creator/src/components/NotificationListener.tsx +1 -8
  77. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +101 -36
  78. package/src/creator/src/locales/en.json +8 -0
  79. package/src/creator/src/locales/es.json +8 -0
  80. package/src/creator/src/utils/completionResult.test.ts +144 -0
  81. package/src/creator/src/utils/completionResult.ts +154 -0
  82. package/src/creator/src/utils/constants.ts +17 -4
  83. package/src/creator/src/utils/socket.test.ts +113 -0
  84. package/src/creator/src/utils/socket.ts +60 -37
  85. package/src/creator/src/utils/store.ts +84 -67
  86. package/src/creator/src/utils/useCompletionWatchdog.test.ts +99 -0
  87. package/src/creator/src/utils/useCompletionWatchdog.ts +37 -0
  88. package/src/creator/vitest.config.ts +16 -0
  89. package/src/creatorDist/assets/{index-DnthLsvb.js → index-D6pmbMe9.js} +14030 -13903
  90. package/src/creatorDist/assets/index-zrPponAn.css +1701 -0
  91. package/src/creatorDist/index.html +2 -2
  92. package/src/models/creator.ts +1 -1
  93. package/src/services/ingest/adapters/gcsCourseStorage.ts +70 -0
  94. package/src/services/ingest/adapters/rigoUserTokenAuth.ts +65 -0
  95. package/src/services/ingest/adapters/rigobotPackageRegistry.ts +161 -0
  96. package/src/services/ingest/core/buildIngestConfig.ts +51 -0
  97. package/src/services/ingest/core/buildInitialSyllabus.ts +122 -0
  98. package/src/services/ingest/core/buildSidebar.ts +32 -0
  99. package/src/services/ingest/core/coursePaths.ts +59 -0
  100. package/src/services/ingest/core/ingestCoursePackage.ts +343 -0
  101. package/src/services/ingest/core/normalizeLearnJson.ts +47 -0
  102. package/src/services/ingest/core/pairLessons.ts +132 -0
  103. package/src/services/ingest/core/planWrites.ts +91 -0
  104. package/src/services/ingest/core/resolveSlug.ts +119 -0
  105. package/src/services/ingest/core/types.ts +57 -0
  106. package/src/services/ingest/core/validatePackage.ts +132 -0
  107. package/src/services/ingest/createIngestService.ts +66 -0
  108. package/src/services/ingest/errors.ts +45 -0
  109. package/src/services/ingest/http/errorMapping.ts +49 -0
  110. package/src/services/ingest/http/router.ts +156 -0
  111. package/src/services/ingest/http/zipPackageReader.ts +81 -0
  112. package/src/services/ingest/ports/courseStorage.ts +25 -0
  113. package/src/services/ingest/ports/packageRegistry.ts +60 -0
  114. package/src/services/ingest/ports/requestAuthenticator.ts +28 -0
  115. package/src/ui/_app/app.js +825 -825
  116. package/src/ui/app.tar.gz +0 -0
  117. package/src/ui/static/og-image.png +0 -0
  118. package/src/utils/branding.ts +78 -0
  119. package/src/utils/coursePackage/bucketIo.ts +46 -0
  120. package/src/utils/coursePackage/learnJson.ts +37 -0
  121. package/src/utils/coursePackage/sidebar.ts +28 -0
  122. package/src/utils/creatorSocket.ts +16 -22
  123. package/src/utils/gcpCredentials.ts +38 -0
  124. package/src/utils/rigoActions.ts +163 -0
  125. package/src/utils/socketRegistry.ts +85 -0
  126. package/lib/creatorDist/assets/index-CjddKHB_.css +0 -1
  127. package/src/creatorDist/assets/index-CjddKHB_.css +0 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateStepSlug = exports.generateCodeChallenge = exports.addInteractivity = exports.generateStepDescriptions = exports.initialContentGenerator = exports.getLanguageCodes = exports.isPackageAuthor = exports.fillSidebarJSON = exports.generateCourseShortName = exports.isValidRigoToken = exports.translateCourseMetadata = exports.createStructuredPreviewReadme = exports.readmeCreator = exports.createCodingReadme = exports.createCodeFile = exports.interactiveCreation = exports.generateCourseIntroduction = exports.translateExercise = exports.generateImage = exports.hasCreatorPermission = exports.createReadme = void 0;
3
+ exports.generateStepSlug = exports.generateCodeChallenge = exports.addInteractivity = exports.generateStepDescriptions = exports.initialContentGenerator = exports.getLanguageCodes = exports.isPackageAuthor = exports.fillSidebarJSON = exports.generateCourseShortName = exports.isValidRigoToken = exports.translateCourseMetadata = exports.createStructuredPreviewReadme = exports.readmeCreator = exports.createCodingReadme = exports.createCodeFile = exports.interactiveCreation = exports.generateCourseIntroduction = exports.translateExerciseAndWait = exports.getCompletionJob = exports.translateExercise = exports.generateImage = exports.hasCreatorPermission = exports.createReadme = void 0;
4
4
  exports.downloadImage = downloadImage;
5
5
  exports.createPreviewReadme = createPreviewReadme;
6
6
  exports.makeReadmeReadable = makeReadmeReadable;
@@ -96,6 +96,107 @@ const translateExercise = async (token, inputs, webhookUrl) => {
96
96
  return response.data;
97
97
  };
98
98
  exports.translateExercise = translateExercise;
99
+ const POST_RETRY_DELAYS_MS = [1000, 3000, 6000];
100
+ const MAX_CONSECUTIVE_POLL_ERRORS = 5;
101
+ const sleep = (ms) => new Promise(resolve => {
102
+ setTimeout(resolve, ms);
103
+ });
104
+ const isFinalStatus = (status) => status === "SUCCESS" || status === "ERROR";
105
+ // Network failures and 5xx are worth retrying, a 4xx is not going to fix itself.
106
+ const isRetriablePostError = (error) => {
107
+ var _a;
108
+ const status = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status;
109
+ return status === undefined || status >= 500;
110
+ };
111
+ const assertJobSucceeded = (job) => {
112
+ if (job.status === "ERROR") {
113
+ throw new Error(job.status_text || `Rigobot translation job ${job.id} finished with ERROR`);
114
+ }
115
+ return job;
116
+ };
117
+ const getCompletionJob = async (token, completionId) => {
118
+ const response = await axios_1.default.get(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/${completionId}/`, {
119
+ headers: {
120
+ "Content-Type": "application/json",
121
+ Authorization: "Token " + token,
122
+ },
123
+ });
124
+ return response.data;
125
+ };
126
+ exports.getCompletionJob = getCompletionJob;
127
+ // Translates an exercise as an asynchronous Rigobot job and waits for the
128
+ // result by polling.
129
+ //
130
+ // Running the completion synchronously (execute_async: false) keeps the HTTP
131
+ // request open until the LLM is done, which for a full README takes 28-45s and
132
+ // gets killed by Heroku's 30s router limit. Firing the job asynchronously
133
+ // answers in about a second and moves the waiting to the polling loop, so it
134
+ // is no longer bound to the lifetime of a single request.
135
+ const translateExerciseAndWait = async (token, inputs, options = {}) => {
136
+ const { pollIntervalMs = 3000, timeoutMs = 6 * 60 * 1000, onPoll } = options;
137
+ let job;
138
+ for (let attempt = 0; attempt <= POST_RETRY_DELAYS_MS.length; attempt++) {
139
+ try {
140
+ // eslint-disable-next-line no-await-in-loop
141
+ const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/translate-asset-markdown/`, {
142
+ inputs: inputs,
143
+ include_purpose_objective: false,
144
+ execute_async: true,
145
+ }, {
146
+ headers: {
147
+ "Content-Type": "application/json",
148
+ Authorization: "Token " + token,
149
+ },
150
+ });
151
+ job = response.data;
152
+ break;
153
+ }
154
+ catch (error) {
155
+ if (!isRetriablePostError(error) ||
156
+ attempt === POST_RETRY_DELAYS_MS.length) {
157
+ throw error;
158
+ }
159
+ // eslint-disable-next-line no-await-in-loop
160
+ await sleep(POST_RETRY_DELAYS_MS[attempt]);
161
+ }
162
+ }
163
+ if (!job) {
164
+ throw new Error("Rigobot did not return a translation job");
165
+ }
166
+ // Rigobot answers straight away with the cached completion when the very
167
+ // same inputs were translated before, so there is nothing to wait for.
168
+ if (isFinalStatus(job.status)) {
169
+ return assertJobSucceeded(job);
170
+ }
171
+ const jobId = job.id;
172
+ const startedAt = Date.now();
173
+ let consecutivePollErrors = 0;
174
+ while (Date.now() - startedAt < timeoutMs) {
175
+ // eslint-disable-next-line no-await-in-loop
176
+ await sleep(pollIntervalMs);
177
+ try {
178
+ // eslint-disable-next-line no-await-in-loop
179
+ job = await (0, exports.getCompletionJob)(token, jobId);
180
+ consecutivePollErrors = 0;
181
+ }
182
+ catch (error) {
183
+ consecutivePollErrors++;
184
+ if (consecutivePollErrors >= MAX_CONSECUTIVE_POLL_ERRORS) {
185
+ throw new Error(`Could not read the status of the translation job ${jobId} after ${MAX_CONSECUTIVE_POLL_ERRORS} attempts: ${error.message}`);
186
+ }
187
+ continue;
188
+ }
189
+ if (onPoll) {
190
+ // eslint-disable-next-line no-await-in-loop
191
+ await onPoll(job);
192
+ }
193
+ if (isFinalStatus(job.status)) {
194
+ return assertJobSucceeded(job);
195
+ }
196
+ }
197
+ throw new Error(`The translation job ${jobId} did not finish after ${Math.round(timeoutMs / 1000)}s, its last known status was ${job.status}`);
198
+ };
199
+ exports.translateExerciseAndWait = translateExerciseAndWait;
99
200
  const generateCourseIntroduction = async (token, inputs) => {
100
201
  const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/192/`, {
101
202
  inputs: inputs,
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Which sockets are listening on which channel.
3
+ *
4
+ * Extracted from `creatorSocket.ts` so the bookkeeping can be tested without
5
+ * standing up a socket.io server. The registry deals in socket ids only; the
6
+ * socket objects themselves stay with the transport layer.
7
+ *
8
+ * The behaviour that matters is `removeSocket`: a disconnecting client has to
9
+ * disappear from **every** channel it joined. The notification map used to be
10
+ * skipped on disconnect, so it grew for the lifetime of the process and
11
+ * accumulated ids of sockets that were long gone.
12
+ */
13
+ export declare class SocketRegistry {
14
+ private readonly channels;
15
+ /**
16
+ * Adds a socket to a channel.
17
+ * @param channel - Channel key, such as a course slug or a notification id.
18
+ * @param socketId - The socket joining it.
19
+ */
20
+ register(channel: string, socketId: string): void;
21
+ /**
22
+ * Removes a socket from one channel, dropping the channel when it empties.
23
+ * @param channel - Channel to leave.
24
+ * @param socketId - The socket leaving it.
25
+ */
26
+ unregister(channel: string, socketId: string): void;
27
+ /**
28
+ * Removes a socket from every channel it joined.
29
+ *
30
+ * Called when the socket disconnects. Without it the registry keeps growing
31
+ * with ids that can never receive anything again.
32
+ * @param socketId - The socket that went away.
33
+ */
34
+ removeSocket(socketId: string): void;
35
+ /**
36
+ * The sockets currently listening on a channel.
37
+ * @param channel - Channel to look up.
38
+ * @returns Their socket ids, empty when nobody is listening.
39
+ */
40
+ listeners(channel: string): string[];
41
+ /**
42
+ * How many channels are being tracked. Exposed for tests and diagnostics.
43
+ * @returns The number of non-empty channels.
44
+ */
45
+ channelCount(): number;
46
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SocketRegistry = void 0;
4
+ /**
5
+ * Which sockets are listening on which channel.
6
+ *
7
+ * Extracted from `creatorSocket.ts` so the bookkeeping can be tested without
8
+ * standing up a socket.io server. The registry deals in socket ids only; the
9
+ * socket objects themselves stay with the transport layer.
10
+ *
11
+ * The behaviour that matters is `removeSocket`: a disconnecting client has to
12
+ * disappear from **every** channel it joined. The notification map used to be
13
+ * skipped on disconnect, so it grew for the lifetime of the process and
14
+ * accumulated ids of sockets that were long gone.
15
+ */
16
+ class SocketRegistry {
17
+ constructor() {
18
+ this.channels = new Map();
19
+ }
20
+ /**
21
+ * Adds a socket to a channel.
22
+ * @param channel - Channel key, such as a course slug or a notification id.
23
+ * @param socketId - The socket joining it.
24
+ */
25
+ register(channel, socketId) {
26
+ if (!channel || !socketId) {
27
+ return;
28
+ }
29
+ let listeners = this.channels.get(channel);
30
+ if (!listeners) {
31
+ listeners = new Set();
32
+ this.channels.set(channel, listeners);
33
+ }
34
+ listeners.add(socketId);
35
+ }
36
+ /**
37
+ * Removes a socket from one channel, dropping the channel when it empties.
38
+ * @param channel - Channel to leave.
39
+ * @param socketId - The socket leaving it.
40
+ */
41
+ unregister(channel, socketId) {
42
+ const listeners = this.channels.get(channel);
43
+ if (!listeners) {
44
+ return;
45
+ }
46
+ listeners.delete(socketId);
47
+ if (listeners.size === 0) {
48
+ this.channels.delete(channel);
49
+ }
50
+ }
51
+ /**
52
+ * Removes a socket from every channel it joined.
53
+ *
54
+ * Called when the socket disconnects. Without it the registry keeps growing
55
+ * with ids that can never receive anything again.
56
+ * @param socketId - The socket that went away.
57
+ */
58
+ removeSocket(socketId) {
59
+ for (const [channel, listeners] of this.channels) {
60
+ listeners.delete(socketId);
61
+ if (listeners.size === 0) {
62
+ this.channels.delete(channel);
63
+ }
64
+ }
65
+ }
66
+ /**
67
+ * The sockets currently listening on a channel.
68
+ * @param channel - Channel to look up.
69
+ * @returns Their socket ids, empty when nobody is listening.
70
+ */
71
+ listeners(channel) {
72
+ const listeners = this.channels.get(channel);
73
+ return listeners ? [...listeners] : [];
74
+ }
75
+ /**
76
+ * How many channels are being tracked. Exposed for tests and diagnostics.
77
+ * @returns The number of non-empty channels.
78
+ */
79
+ channelCount() {
80
+ return this.channels.size;
81
+ }
82
+ }
83
+ exports.SocketRegistry = SocketRegistry;
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.355",
4
+ "version": "5.0.358",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -166,7 +166,7 @@
166
166
  "audit:package-sources": "ts-node --project ./tsconfig.json ./scripts/audit-package-sources.ts",
167
167
  "backfill:package-manifest": "ts-node --project ./tsconfig.json ./scripts/backfill-package-manifest-s3.ts",
168
168
  "repair:legacy-packages": "ts-node --project ./tsconfig.json ./scripts/repair-legacy-packages.ts",
169
- "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",
169
+ "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 && npx cpy src/ui/static/**/* lib/ui/static --parents --verbose",
170
170
  "tsc": "tsc -b",
171
171
  "postpack": "rm -f oclif.manifest.json && eslint . --ext .ts --config .eslintrc",
172
172
  "prepack": "rm -rf lib && tsc -b && npm run copy-assets",
@@ -20,6 +20,7 @@ import * as prompts from "prompts"
20
20
  import { isValidRigoToken } from "../utils/rigoActions"
21
21
  import { minutesToISO8601Duration } from "../utils/misc"
22
22
  import { getReadmeExtension, slugify } from "../utils/creatorUtilities"
23
+ import { getFallbackPreviewUrl } from "../utils/branding"
23
24
 
24
25
  const uploadZipEndpont = RIGOBOT_HOST + "/v1/learnpack/upload"
25
26
 
@@ -588,9 +589,7 @@ class BuildCommand extends SessionCommand {
588
589
  getLocalizedValue(learnJson?.title, selectedLang) ||
589
590
  "LearnPack: Interactive Learning as a Service"
590
591
 
591
- const previewUrl =
592
- learnJson.preview ||
593
- "https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/public/learnpack.svg"
592
+ const previewUrl = learnJson.preview || getFallbackPreviewUrl()
594
593
  // Replace placeholders and the <title>Old title </title> tag for a new tag with the title
595
594
  indexHtmlContent = indexHtmlContent
596
595
  .replace(/{{description}}/g, description)