coursecode 0.1.0

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 (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +322 -0
  3. package/THIRD_PARTY_NOTICES.md +22 -0
  4. package/bin/cli.js +331 -0
  5. package/framework/assets/logo-coursecode-black.svg +14 -0
  6. package/framework/assets/logo-coursecode-white.svg +14 -0
  7. package/framework/assets/logo-coursecode.svg +14 -0
  8. package/framework/css/01-base.css +160 -0
  9. package/framework/css/02-layout.css +499 -0
  10. package/framework/css/accessibility.css +834 -0
  11. package/framework/css/components/accordions.css +710 -0
  12. package/framework/css/components/assessments.css +520 -0
  13. package/framework/css/components/audio-player.css +570 -0
  14. package/framework/css/components/badges.css +80 -0
  15. package/framework/css/components/breadcrumbs.css +87 -0
  16. package/framework/css/components/buttons.css +707 -0
  17. package/framework/css/components/callouts.css +1280 -0
  18. package/framework/css/components/cards.css +475 -0
  19. package/framework/css/components/carousel.css +193 -0
  20. package/framework/css/components/checkbox-group.css +123 -0
  21. package/framework/css/components/checklist.css +203 -0
  22. package/framework/css/components/collapse.css +96 -0
  23. package/framework/css/components/comparison.css +33 -0
  24. package/framework/css/components/content-image.css +36 -0
  25. package/framework/css/components/document-gallery.css +425 -0
  26. package/framework/css/components/dropdown.css +115 -0
  27. package/framework/css/components/embed-frame.css +142 -0
  28. package/framework/css/components/engagement.css +412 -0
  29. package/framework/css/components/features.css +35 -0
  30. package/framework/css/components/flip-cards.css +253 -0
  31. package/framework/css/components/footer.css +353 -0
  32. package/framework/css/components/forms.css +294 -0
  33. package/framework/css/components/hero.css +216 -0
  34. package/framework/css/components/images.css +528 -0
  35. package/framework/css/components/interactive-timeline.css +274 -0
  36. package/framework/css/components/intro-cards.css +30 -0
  37. package/framework/css/components/lightbox.css +666 -0
  38. package/framework/css/components/loading.css +65 -0
  39. package/framework/css/components/modals.css +235 -0
  40. package/framework/css/components/notifications.css +107 -0
  41. package/framework/css/components/quote.css +150 -0
  42. package/framework/css/components/sidebar.css +684 -0
  43. package/framework/css/components/slide-header.css +52 -0
  44. package/framework/css/components/spinner.css +62 -0
  45. package/framework/css/components/stats.css +44 -0
  46. package/framework/css/components/steps.css +232 -0
  47. package/framework/css/components/tables.css +90 -0
  48. package/framework/css/components/tabs.css +347 -0
  49. package/framework/css/components/timeline.css +154 -0
  50. package/framework/css/components/toggle.css +95 -0
  51. package/framework/css/components/tooltip.css +226 -0
  52. package/framework/css/components/video-player.css +438 -0
  53. package/framework/css/design-tokens.css +707 -0
  54. package/framework/css/framework.css +86 -0
  55. package/framework/css/interactions/accessibility.css +75 -0
  56. package/framework/css/interactions/base.css +92 -0
  57. package/framework/css/interactions/drag-drop.css +295 -0
  58. package/framework/css/interactions/fill-in-the-blank.css +236 -0
  59. package/framework/css/interactions/hotspots.css +69 -0
  60. package/framework/css/interactions/index.css +45 -0
  61. package/framework/css/interactions/interactive-image.css +359 -0
  62. package/framework/css/interactions/likert.css +126 -0
  63. package/framework/css/interactions/matching.css +354 -0
  64. package/framework/css/interactions/numeric-input.css +78 -0
  65. package/framework/css/interactions/sequencing.css +378 -0
  66. package/framework/css/interactions/true-false.css +177 -0
  67. package/framework/css/layouts/article.css +258 -0
  68. package/framework/css/layouts/base.css +30 -0
  69. package/framework/css/layouts/canvas.css +38 -0
  70. package/framework/css/layouts/focused.css +236 -0
  71. package/framework/css/layouts/index.css +29 -0
  72. package/framework/css/layouts/presentation.css +191 -0
  73. package/framework/css/layouts/traditional.css +52 -0
  74. package/framework/css/responsive.css +439 -0
  75. package/framework/css/utilities/accessibility-utils.css +59 -0
  76. package/framework/css/utilities/animations.css +419 -0
  77. package/framework/css/utilities/borders.css +72 -0
  78. package/framework/css/utilities/colors.css +76 -0
  79. package/framework/css/utilities/container.css +46 -0
  80. package/framework/css/utilities/decorative.css +442 -0
  81. package/framework/css/utilities/display.css +257 -0
  82. package/framework/css/utilities/flexbox.css +80 -0
  83. package/framework/css/utilities/grid.css +69 -0
  84. package/framework/css/utilities/icons.css +534 -0
  85. package/framework/css/utilities/lists.css +190 -0
  86. package/framework/css/utilities/spacing.css +167 -0
  87. package/framework/css/utilities/tables.css +81 -0
  88. package/framework/css/utilities/typography.css +159 -0
  89. package/framework/css/utilities/visibility.css +117 -0
  90. package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
  91. package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
  92. package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
  93. package/framework/docs/DATA_MODEL.md +409 -0
  94. package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
  95. package/framework/docs/USER_GUIDE.md +583 -0
  96. package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
  97. package/framework/docs/examples/cloudflare-data-worker.js +102 -0
  98. package/framework/docs/examples/cloudflare-error-worker.js +228 -0
  99. package/framework/index.html +175 -0
  100. package/framework/js/app/AppActions.js +410 -0
  101. package/framework/js/app/AppState.js +225 -0
  102. package/framework/js/app/AppUI.js +616 -0
  103. package/framework/js/assessment/AssessmentActions.js +615 -0
  104. package/framework/js/assessment/AssessmentFactory.js +471 -0
  105. package/framework/js/assessment/AssessmentState.js +322 -0
  106. package/framework/js/assessment/AssessmentUI.js +451 -0
  107. package/framework/js/automation/api-engagement.js +196 -0
  108. package/framework/js/automation/api-interactions.js +167 -0
  109. package/framework/js/automation/api.js +242 -0
  110. package/framework/js/automation/index.js +41 -0
  111. package/framework/js/components/interactions/drag-drop.js +884 -0
  112. package/framework/js/components/interactions/fill-in.js +535 -0
  113. package/framework/js/components/interactions/hotspot.js +702 -0
  114. package/framework/js/components/interactions/interaction-base.js +511 -0
  115. package/framework/js/components/interactions/likert.js +301 -0
  116. package/framework/js/components/interactions/matching.js +699 -0
  117. package/framework/js/components/interactions/multiple-choice.js +377 -0
  118. package/framework/js/components/interactions/numeric.js +271 -0
  119. package/framework/js/components/interactions/sequencing.js +423 -0
  120. package/framework/js/components/interactions/true-false.js +241 -0
  121. package/framework/js/components/ui-components/accordion.js +442 -0
  122. package/framework/js/components/ui-components/alert.js +88 -0
  123. package/framework/js/components/ui-components/audio-player.js +1193 -0
  124. package/framework/js/components/ui-components/callout.js +121 -0
  125. package/framework/js/components/ui-components/carousel.js +145 -0
  126. package/framework/js/components/ui-components/checkbox-group.js +87 -0
  127. package/framework/js/components/ui-components/checklist.js +40 -0
  128. package/framework/js/components/ui-components/collapse.js +114 -0
  129. package/framework/js/components/ui-components/comparison.js +30 -0
  130. package/framework/js/components/ui-components/conditional-display.js +150 -0
  131. package/framework/js/components/ui-components/content-image.js +41 -0
  132. package/framework/js/components/ui-components/dropdown.js +262 -0
  133. package/framework/js/components/ui-components/embed-frame.js +274 -0
  134. package/framework/js/components/ui-components/features.js +33 -0
  135. package/framework/js/components/ui-components/flip-card.js +230 -0
  136. package/framework/js/components/ui-components/form-validator.js +76 -0
  137. package/framework/js/components/ui-components/hero.js +49 -0
  138. package/framework/js/components/ui-components/index.js +12 -0
  139. package/framework/js/components/ui-components/interactive-image.js +235 -0
  140. package/framework/js/components/ui-components/interactive-timeline.js +285 -0
  141. package/framework/js/components/ui-components/intro-cards.js +35 -0
  142. package/framework/js/components/ui-components/lightbox.js +652 -0
  143. package/framework/js/components/ui-components/modal.js +386 -0
  144. package/framework/js/components/ui-components/notifications.js +145 -0
  145. package/framework/js/components/ui-components/progress.js +88 -0
  146. package/framework/js/components/ui-components/quote.js +41 -0
  147. package/framework/js/components/ui-components/stats.js +33 -0
  148. package/framework/js/components/ui-components/steps.js +41 -0
  149. package/framework/js/components/ui-components/tabs.js +255 -0
  150. package/framework/js/components/ui-components/timeline.js +42 -0
  151. package/framework/js/components/ui-components/toggle-group.js +73 -0
  152. package/framework/js/components/ui-components/tooltip.js +458 -0
  153. package/framework/js/components/ui-components/value-display.js +133 -0
  154. package/framework/js/components/ui-components/video-player.js +686 -0
  155. package/framework/js/core/component-catalog.js +121 -0
  156. package/framework/js/core/event-bus.js +178 -0
  157. package/framework/js/core/interaction-catalog.js +149 -0
  158. package/framework/js/dev/runtime-linter.js +1725 -0
  159. package/framework/js/drivers/cmi5-driver.js +768 -0
  160. package/framework/js/drivers/driver-factory.js +77 -0
  161. package/framework/js/drivers/driver-interface.js +110 -0
  162. package/framework/js/drivers/http-driver-base.js +241 -0
  163. package/framework/js/drivers/lti-driver.js +508 -0
  164. package/framework/js/drivers/proxy-driver.js +444 -0
  165. package/framework/js/drivers/scorm-12-driver.js +560 -0
  166. package/framework/js/drivers/scorm-2004-driver.js +775 -0
  167. package/framework/js/drivers/scorm-driver-base.js +112 -0
  168. package/framework/js/engagement/engagement-manager.js +404 -0
  169. package/framework/js/engagement/engagement-progress.js +191 -0
  170. package/framework/js/engagement/engagement-trackers.js +215 -0
  171. package/framework/js/engagement/requirement-strategies.js +268 -0
  172. package/framework/js/main.js +727 -0
  173. package/framework/js/managers/accessibility-manager.js +499 -0
  174. package/framework/js/managers/assessment-manager.js +230 -0
  175. package/framework/js/managers/audio-manager.js +944 -0
  176. package/framework/js/managers/comment-manager.js +88 -0
  177. package/framework/js/managers/flag-manager.js +86 -0
  178. package/framework/js/managers/interaction-manager.js +254 -0
  179. package/framework/js/managers/interaction-registry.js +96 -0
  180. package/framework/js/managers/objective-manager.js +423 -0
  181. package/framework/js/managers/score-manager.js +441 -0
  182. package/framework/js/managers/video-manager.js +536 -0
  183. package/framework/js/navigation/Breadcrumbs.js +234 -0
  184. package/framework/js/navigation/NavigationActions.js +1132 -0
  185. package/framework/js/navigation/NavigationState.js +276 -0
  186. package/framework/js/navigation/NavigationUI.js +574 -0
  187. package/framework/js/navigation/document-gallery.js +357 -0
  188. package/framework/js/navigation/navigation-helpers.js +175 -0
  189. package/framework/js/navigation/navigation-validators.js +174 -0
  190. package/framework/js/state/index.js +8 -0
  191. package/framework/js/state/lms-connection.js +482 -0
  192. package/framework/js/state/lms-error-utils.js +58 -0
  193. package/framework/js/state/state-commits.js +200 -0
  194. package/framework/js/state/state-domains.js +86 -0
  195. package/framework/js/state/state-manager.js +502 -0
  196. package/framework/js/state/state-validation.js +311 -0
  197. package/framework/js/state/transaction-log.js +41 -0
  198. package/framework/js/state/xapi-statement-service.js +325 -0
  199. package/framework/js/utilities/access-control.js +99 -0
  200. package/framework/js/utilities/breakpoint-manager.js +315 -0
  201. package/framework/js/utilities/canvas-slide.js +35 -0
  202. package/framework/js/utilities/conditional-display.js +388 -0
  203. package/framework/js/utilities/course-channel.js +214 -0
  204. package/framework/js/utilities/course-helpers.js +420 -0
  205. package/framework/js/utilities/data-reporter.js +273 -0
  206. package/framework/js/utilities/error-reporter.js +313 -0
  207. package/framework/js/utilities/hotspot-helper.js +341 -0
  208. package/framework/js/utilities/icons.js +348 -0
  209. package/framework/js/utilities/logger.js +92 -0
  210. package/framework/js/utilities/markdown-renderer.js +45 -0
  211. package/framework/js/utilities/scroll-tracker.js +68 -0
  212. package/framework/js/utilities/ui-initializer.js +146 -0
  213. package/framework/js/utilities/utilities.js +293 -0
  214. package/framework/js/utilities/view-manager.js +227 -0
  215. package/framework/js/validation/html-validators.js +422 -0
  216. package/framework/js/validation/scorm-validators.js +438 -0
  217. package/framework/js/vendor/pipwerks.js +931 -0
  218. package/framework/scripts/generate-narration.js +629 -0
  219. package/framework/scripts/tts-providers/azure-provider.js +178 -0
  220. package/framework/scripts/tts-providers/base-provider.js +81 -0
  221. package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
  222. package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
  223. package/framework/scripts/tts-providers/google-provider.js +272 -0
  224. package/framework/scripts/tts-providers/index.js +158 -0
  225. package/framework/scripts/tts-providers/openai-provider.js +143 -0
  226. package/framework/version.json +63 -0
  227. package/lib/authoring-api.js +919 -0
  228. package/lib/build-linter.js +450 -0
  229. package/lib/build-packaging.js +186 -0
  230. package/lib/build.js +88 -0
  231. package/lib/cloud.js +691 -0
  232. package/lib/convert.js +341 -0
  233. package/lib/course-parser.js +936 -0
  234. package/lib/course-writer.js +258 -0
  235. package/lib/create.js +248 -0
  236. package/lib/css-index.js +237 -0
  237. package/lib/dev.js +51 -0
  238. package/lib/export-content.js +1246 -0
  239. package/lib/headless-browser.js +413 -0
  240. package/lib/import.js +377 -0
  241. package/lib/index.js +80 -0
  242. package/lib/info.js +79 -0
  243. package/lib/interaction-formatters.js +568 -0
  244. package/lib/manifest/cmi5-manifest.js +63 -0
  245. package/lib/manifest/lti-tool-config.js +53 -0
  246. package/lib/manifest/manifest-factory.js +99 -0
  247. package/lib/manifest/scorm-12-manifest.js +61 -0
  248. package/lib/manifest/scorm-2004-manifest.js +94 -0
  249. package/lib/manifest/scorm-proxy-manifest.js +104 -0
  250. package/lib/manifest-parser.js +96 -0
  251. package/lib/mcp-prompts.js +753 -0
  252. package/lib/mcp-server.js +316 -0
  253. package/lib/narration.js +53 -0
  254. package/lib/pdf-structure.js +142 -0
  255. package/lib/preview-export.js +231 -0
  256. package/lib/preview-routes-api.js +662 -0
  257. package/lib/preview-routes-editing.js +159 -0
  258. package/lib/preview-routes-lms.js +230 -0
  259. package/lib/preview-server.js +564 -0
  260. package/lib/project-utils.js +269 -0
  261. package/lib/proxy-templates/proxy.html +68 -0
  262. package/lib/proxy-templates/scorm-bridge.js +112 -0
  263. package/lib/scaffold.js +193 -0
  264. package/lib/schema-extractor.js +361 -0
  265. package/lib/slide-source-editor.js +586 -0
  266. package/lib/stub-player/app-viewer.js +195 -0
  267. package/lib/stub-player/app.js +370 -0
  268. package/lib/stub-player/catalog-panel.js +312 -0
  269. package/lib/stub-player/config-panel.js +1303 -0
  270. package/lib/stub-player/content-generator.js +586 -0
  271. package/lib/stub-player/content-viewer.js +173 -0
  272. package/lib/stub-player/debug-panel.js +420 -0
  273. package/lib/stub-player/edit-mode.js +922 -0
  274. package/lib/stub-player/edit-utils.js +400 -0
  275. package/lib/stub-player/header-bar.js +354 -0
  276. package/lib/stub-player/interaction-editor.js +210 -0
  277. package/lib/stub-player/interactions-panel.js +565 -0
  278. package/lib/stub-player/lms-api.js +1094 -0
  279. package/lib/stub-player/login-screen.js +74 -0
  280. package/lib/stub-player/outline-mode.js +689 -0
  281. package/lib/stub-player/styles/_assessments-panel.css +245 -0
  282. package/lib/stub-player/styles/_base.css +89 -0
  283. package/lib/stub-player/styles/_catalog-icons.css +96 -0
  284. package/lib/stub-player/styles/_catalog-panel.css +291 -0
  285. package/lib/stub-player/styles/_config-panel.css +636 -0
  286. package/lib/stub-player/styles/_content-viewer.css +834 -0
  287. package/lib/stub-player/styles/_debug-panel.css +576 -0
  288. package/lib/stub-player/styles/_edit-mode.css +128 -0
  289. package/lib/stub-player/styles/_header-bar.css +343 -0
  290. package/lib/stub-player/styles/_interaction-editor.css +140 -0
  291. package/lib/stub-player/styles/_interactions-panel.css +1038 -0
  292. package/lib/stub-player/styles/_login-screen.css +102 -0
  293. package/lib/stub-player/styles/_outline-mode.css +752 -0
  294. package/lib/stub-player/styles.css +15 -0
  295. package/lib/stub-player.js +160 -0
  296. package/lib/test-data-reporting.js +176 -0
  297. package/lib/test-error-reporting.js +146 -0
  298. package/lib/token.js +86 -0
  299. package/lib/upgrade.js +257 -0
  300. package/lib/validation-rules.js +517 -0
  301. package/lib/vite-plugin-content-discovery.js +296 -0
  302. package/package.json +108 -0
  303. package/schemas/XMLSchema.dtd +402 -0
  304. package/schemas/adlcp_v1p3.xsd +111 -0
  305. package/schemas/adlnav_v1p3.xsd +61 -0
  306. package/schemas/adlseq_v1p3.xsd +93 -0
  307. package/schemas/common/anyElement.xsd +27 -0
  308. package/schemas/common/dataTypes.xsd +138 -0
  309. package/schemas/common/elementNames.xsd +767 -0
  310. package/schemas/common/elementTypes.xsd +786 -0
  311. package/schemas/common/rootElement.xsd +31 -0
  312. package/schemas/common/vocabTypes.xsd +345 -0
  313. package/schemas/common/vocabValues.xsd +257 -0
  314. package/schemas/datatypes.dtd +203 -0
  315. package/schemas/ims_xml.xsd +35 -0
  316. package/schemas/imscp_v1p1.xsd +368 -0
  317. package/schemas/imsss_v1p0.xsd +67 -0
  318. package/schemas/imsss_v1p0auxresource.xsd +19 -0
  319. package/schemas/imsss_v1p0control.xsd +20 -0
  320. package/schemas/imsss_v1p0delivery.xsd +17 -0
  321. package/schemas/imsss_v1p0limit.xsd +47 -0
  322. package/schemas/imsss_v1p0objective.xsd +67 -0
  323. package/schemas/imsss_v1p0random.xsd +16 -0
  324. package/schemas/imsss_v1p0rollup.xsd +46 -0
  325. package/schemas/imsss_v1p0seqrule.xsd +108 -0
  326. package/schemas/imsss_v1p0util.xsd +94 -0
  327. package/schemas/license.txt +17 -0
  328. package/schemas/lom.xsd +102 -0
  329. package/schemas/lomCustom.xsd +62 -0
  330. package/schemas/lomLoose.xsd +62 -0
  331. package/schemas/lomStrict.xsd +62 -0
  332. package/schemas/xml.xsd +81 -0
  333. package/template/.env.example +92 -0
  334. package/template/course/assets/audio/example-intro.mp3 +0 -0
  335. package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
  336. package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
  337. package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
  338. package/template/course/assets/docs/example_md_1.md +39 -0
  339. package/template/course/assets/docs/example_md_2.md +41 -0
  340. package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
  341. package/template/course/assets/docs/example_pdf_2.pdf +0 -0
  342. package/template/course/assets/images/course-architecture.svg +36 -0
  343. package/template/course/assets/images/logo.svg +14 -0
  344. package/template/course/assets/widgets/counter-demo.html +190 -0
  345. package/template/course/assets/widgets/gravity-painter.html +384 -0
  346. package/template/course/course-config.js +539 -0
  347. package/template/course/icons.js +19 -0
  348. package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
  349. package/template/course/slides/example-course-structure.js +138 -0
  350. package/template/course/slides/example-final-exam.js +144 -0
  351. package/template/course/slides/example-finishing.js +127 -0
  352. package/template/course/slides/example-interactions-showcase.js +615 -0
  353. package/template/course/slides/example-preview-tour.js +129 -0
  354. package/template/course/slides/example-remedial.js +143 -0
  355. package/template/course/slides/example-summary.js +103 -0
  356. package/template/course/slides/example-ui-showcase.js +1805 -0
  357. package/template/course/slides/example-welcome.js +123 -0
  358. package/template/course/slides/example-workflow.js +140 -0
  359. package/template/course/theme.css +165 -0
  360. package/template/eslint.config.js +47 -0
  361. package/template/package.json +28 -0
  362. package/template/vite.config.js +339 -0
package/lib/cloud.js ADDED
@@ -0,0 +1,691 @@
1
+ /**
2
+ * CourseCode Cloud CLI — auth, credentials, HTTP helpers, and cloud commands.
3
+ *
4
+ * Implements the CLI → Cloud integration spec:
5
+ * login, logout, whoami, courses, deploy, status
6
+ *
7
+ * Zero external dependencies — uses Node 18+ built-in fetch, crypto, readline.
8
+ */
9
+
10
+ import crypto from 'crypto';
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import os from 'os';
14
+ import { exec } from 'child_process';
15
+ import readline from 'readline';
16
+ import { fileURLToPath } from 'url';
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
20
+
21
+ // =============================================================================
22
+ // CONSTANTS
23
+ // =============================================================================
24
+
25
+ const DEFAULT_CLOUD_URL = 'https://www.coursecodecloud.com';
26
+ const LOCAL_CLOUD_URL = 'http://localhost:3000';
27
+ let useLocal = false;
28
+ const CREDENTIALS_DIR = path.join(os.homedir(), '.coursecode');
29
+ const CREDENTIALS_PATH = path.join(CREDENTIALS_DIR, 'credentials.json');
30
+ const PROJECT_CONFIG_DIR = '.coursecode';
31
+ const PROJECT_CONFIG_PATH = path.join(PROJECT_CONFIG_DIR, 'project.json');
32
+
33
+ const POLL_INTERVAL_MS = 2000;
34
+ const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
35
+ const USER_AGENT = `coursecode-cli/${packageJson.version}`;
36
+
37
+ // =============================================================================
38
+ // SLUG UTILITIES
39
+ // =============================================================================
40
+
41
+ /**
42
+ * Slugify a string for use as a course slug.
43
+ * Rules: lowercase, spaces/underscores → hyphens, strip non-alphanumeric,
44
+ * collapse consecutive hyphens, trim leading/trailing hyphens.
45
+ */
46
+ export function slugify(name) {
47
+ return name
48
+ .toLowerCase()
49
+ .replace(/[\s_]+/g, '-')
50
+ .replace(/[^a-z0-9-]/g, '')
51
+ .replace(/-{2,}/g, '-')
52
+ .replace(/^-+|-+$/g, '');
53
+ }
54
+
55
+ /**
56
+ * Resolve the course slug.
57
+ * Priority: .coursecode/project.json → directory name (slugified)
58
+ */
59
+ function resolveSlug() {
60
+ const projectConfig = readProjectConfig();
61
+ if (projectConfig?.slug) return projectConfig.slug;
62
+ return slugify(path.basename(process.cwd()));
63
+ }
64
+
65
+ // =============================================================================
66
+ // CREDENTIALS (global: ~/.coursecode/credentials.json)
67
+ // Local mode uses credentials.local.json to avoid clobbering production.
68
+ // =============================================================================
69
+
70
+ function getCredentialsPath() {
71
+ if (useLocal) return path.join(CREDENTIALS_DIR, 'credentials.local.json');
72
+ return CREDENTIALS_PATH;
73
+ }
74
+
75
+ function readCredentials() {
76
+ try {
77
+ const credPath = getCredentialsPath();
78
+ if (!fs.existsSync(credPath)) return null;
79
+ return JSON.parse(fs.readFileSync(credPath, 'utf-8'));
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ function writeCredentials(token, cloudUrl = DEFAULT_CLOUD_URL) {
86
+ fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
87
+ const data = JSON.stringify({ token, cloud_url: cloudUrl }, null, 2);
88
+ fs.writeFileSync(getCredentialsPath(), data, { mode: 0o600 });
89
+ }
90
+
91
+ function deleteCredentials() {
92
+ try { fs.unlinkSync(getCredentialsPath()); } catch { /* already gone */ }
93
+ }
94
+
95
+ function getCloudUrl() {
96
+ if (useLocal) return LOCAL_CLOUD_URL;
97
+ return readCredentials()?.cloud_url || DEFAULT_CLOUD_URL;
98
+ }
99
+
100
+ /**
101
+ * Enable local mode — route all API calls to LOCAL_CLOUD_URL.
102
+ * Called by CLI when --local flag is passed.
103
+ */
104
+ export function setLocalMode() {
105
+ useLocal = true;
106
+ }
107
+
108
+ // =============================================================================
109
+ // PROJECT BINDING (local: .coursecode/project.json)
110
+ // =============================================================================
111
+
112
+ function readProjectConfig() {
113
+ try {
114
+ const fullPath = path.join(process.cwd(), PROJECT_CONFIG_PATH);
115
+ if (!fs.existsSync(fullPath)) return null;
116
+ return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
117
+ } catch {
118
+ return null;
119
+ }
120
+ }
121
+
122
+ function writeProjectConfig(data) {
123
+ const dir = path.join(process.cwd(), PROJECT_CONFIG_DIR);
124
+ fs.mkdirSync(dir, { recursive: true });
125
+ fs.writeFileSync(
126
+ path.join(process.cwd(), PROJECT_CONFIG_PATH),
127
+ JSON.stringify(data, null, 2) + '\n'
128
+ );
129
+ }
130
+
131
+ // =============================================================================
132
+ // COURSE IDENTITY (committed: .coursecoderc.json → cloudId)
133
+ // =============================================================================
134
+
135
+ /**
136
+ * Read .coursecoderc.json from the project root.
137
+ */
138
+ function readRcConfig() {
139
+ try {
140
+ const rcPath = path.join(process.cwd(), '.coursecoderc.json');
141
+ if (!fs.existsSync(rcPath)) return null;
142
+ return JSON.parse(fs.readFileSync(rcPath, 'utf-8'));
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Stamp cloudId into .coursecoderc.json without clobbering other fields.
150
+ */
151
+ function writeRcCloudId(cloudId) {
152
+ const rcPath = path.join(process.cwd(), '.coursecoderc.json');
153
+ const existing = readRcConfig() || {};
154
+ existing.cloudId = cloudId;
155
+ fs.writeFileSync(rcPath, JSON.stringify(existing, null, 2) + '\n');
156
+ }
157
+
158
+ // =============================================================================
159
+ // HTTP HELPERS
160
+ // =============================================================================
161
+
162
+ /**
163
+ * Make an authenticated request to the Cloud API.
164
+ * Handles User-Agent, Bearer token, and error formatting per §7.
165
+ *
166
+ * @param {string} urlPath - API path (e.g. '/api/cli/whoami')
167
+ * @param {object} options - fetch options (method, body, headers, etc.)
168
+ * @param {string} [token] - Override token (for unauthenticated requests)
169
+ * @returns {Promise<Response>}
170
+ */
171
+ async function cloudFetch(urlPath, options = {}, token = null) {
172
+ const cloudUrl = getCloudUrl();
173
+ const url = `${cloudUrl}${urlPath}`;
174
+
175
+ const headers = {
176
+ 'User-Agent': USER_AGENT,
177
+ ...options.headers,
178
+ };
179
+
180
+ if (token) {
181
+ headers['Authorization'] = `Bearer ${token}`;
182
+ }
183
+
184
+ try {
185
+ return await fetch(url, { ...options, headers });
186
+ } catch (_error) {
187
+ console.error('\nāŒ Could not connect to CourseCode Cloud. Check your internet connection.\n');
188
+ process.exit(1);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Handle HTTP error responses per §7.
194
+ * Returns the parsed JSON body, or exits on error.
195
+ */
196
+ async function handleResponse(res, { retryFn, _isRetry = false } = {}) {
197
+ if (res.ok) return res.json();
198
+
199
+ const status = res.status;
200
+
201
+ // 401 — invalid token, trigger re-auth and retry once
202
+ if (status === 401 && retryFn && !_isRetry) {
203
+ console.log('\n ⚠ Session expired. Re-authenticating...\n');
204
+ deleteCredentials();
205
+ await runLoginFlow();
206
+ return retryFn(true);
207
+ }
208
+
209
+ // Parse error body
210
+ let body;
211
+ try { body = await res.json(); } catch { body = {}; }
212
+ const message = body.error || `HTTP ${status}`;
213
+
214
+ if (status === 403 || status === 409) {
215
+ console.error(`\nāŒ ${message}\n`);
216
+ } else if (status === 404) {
217
+ console.error('\nāŒ Course not found on Cloud.\n');
218
+ } else if (status >= 500) {
219
+ console.error('\nāŒ Cloud server error. Try again later.\n');
220
+ } else {
221
+ console.error(`\nāŒ ${message}\n`);
222
+ }
223
+
224
+ process.exit(1);
225
+ }
226
+
227
+ // =============================================================================
228
+ // AUTHENTICATION
229
+ // =============================================================================
230
+
231
+ /**
232
+ * Open a URL in the system browser.
233
+ */
234
+ function openBrowser(url) {
235
+ const platform = process.platform;
236
+ const cmd = platform === 'darwin' ? 'open'
237
+ : platform === 'win32' ? 'start'
238
+ : 'xdg-open';
239
+ exec(`${cmd} "${url}"`);
240
+ }
241
+
242
+ /**
243
+ * Prompt the user for input via readline.
244
+ */
245
+ function prompt(question) {
246
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
247
+ return new Promise(resolve => {
248
+ rl.question(question, answer => {
249
+ rl.close();
250
+ resolve(answer.trim());
251
+ });
252
+ });
253
+ }
254
+
255
+ /**
256
+ * Sleep for a given number of milliseconds.
257
+ */
258
+ function sleep(ms) {
259
+ return new Promise(resolve => setTimeout(resolve, ms));
260
+ }
261
+
262
+ /**
263
+ * Run the nonce exchange login flow.
264
+ * 1. Generate nonce
265
+ * 2. POST /api/auth/connect to create session
266
+ * 3. Open browser
267
+ * 4. Poll until token received or timeout
268
+ * 5. Store credentials
269
+ */
270
+ async function runLoginFlow() {
271
+ const nonce = crypto.randomBytes(32).toString('hex');
272
+ const cloudUrl = getCloudUrl();
273
+
274
+ // Step 1: Create CLI session
275
+ console.log(' → Registering session...');
276
+ const createRes = await cloudFetch('/api/auth/connect', {
277
+ method: 'POST',
278
+ headers: { 'Content-Type': 'application/json' },
279
+ body: JSON.stringify({ nonce }),
280
+ });
281
+
282
+ if (!createRes.ok) {
283
+ const body = await createRes.json().catch(() => ({}));
284
+ console.error(`\nāŒ Failed to start login: ${body.error || `HTTP ${createRes.status}`}\n`);
285
+ process.exit(1);
286
+ }
287
+
288
+ // Step 2: Open browser
289
+ const loginUrl = `${cloudUrl}/auth/connect?session=${nonce}`;
290
+ console.log(' → Opening browser for authentication...');
291
+ openBrowser(loginUrl);
292
+
293
+ // Step 3: Poll for token
294
+ const startTime = Date.now();
295
+ while (Date.now() - startTime < POLL_TIMEOUT_MS) {
296
+ await sleep(POLL_INTERVAL_MS);
297
+
298
+ const pollRes = await cloudFetch(`/api/auth/connect?session=${nonce}`);
299
+
300
+ if (pollRes.status === 410) {
301
+ console.error('\nāŒ Login session expired. Try again.\n');
302
+ process.exit(1);
303
+ }
304
+
305
+ if (!pollRes.ok) continue;
306
+
307
+ const data = await pollRes.json();
308
+ if (data.pending) continue;
309
+
310
+ if (data.token) {
311
+ writeCredentials(data.token, cloudUrl);
312
+ console.log(' āœ“ Logged in successfully');
313
+ return data.token;
314
+ }
315
+ }
316
+
317
+ console.error('\nāŒ Login timed out. Try again.\n');
318
+ process.exit(1);
319
+ }
320
+
321
+ /**
322
+ * Ensure the user is authenticated. Auto-triggers login if no credentials.
323
+ * @returns {Promise<string>} The API token
324
+ */
325
+ export async function ensureAuthenticated() {
326
+ const creds = readCredentials();
327
+ if (creds?.token) return creds.token;
328
+
329
+ console.log('\n No Cloud credentials found. Launching login...');
330
+ return runLoginFlow();
331
+ }
332
+
333
+ // =============================================================================
334
+ // ORG RESOLUTION (§3)
335
+ // =============================================================================
336
+
337
+ /**
338
+ * Resolve the org and course for a given slug.
339
+ * Returns { orgId, courseId, orgName } or prompts the user.
340
+ */
341
+ async function resolveOrgAndCourse(slug, token) {
342
+ // 1. Check .coursecoderc.json for cloudId (committed, shared across team)
343
+ const rcConfig = readRcConfig();
344
+ if (rcConfig?.cloudId) {
345
+ // Still need orgId from local project.json if available
346
+ const projectConfig = readProjectConfig();
347
+ if (projectConfig?.orgId) {
348
+ return { orgId: projectConfig.orgId, courseId: rcConfig.cloudId };
349
+ }
350
+ // Have cloudId but no orgId — fall through to API resolution
351
+ // which will match on courseId
352
+ }
353
+
354
+ // 2. Check cached project config (gitignored, per-developer)
355
+ const projectConfig = readProjectConfig();
356
+ if (projectConfig?.orgId && projectConfig?.courseId) {
357
+ return { orgId: projectConfig.orgId, courseId: projectConfig.courseId };
358
+ }
359
+
360
+ // Call resolve endpoint
361
+ const res = await cloudFetch(`/api/cli/courses/${encodeURIComponent(slug)}/resolve`, {}, token);
362
+ const data = await handleResponse(res);
363
+
364
+ // Found in exactly one org
365
+ if (data.found) {
366
+ const binding = { orgId: data.orgId, courseId: data.courseId, slug };
367
+ writeProjectConfig(binding);
368
+ return { orgId: data.orgId, courseId: data.courseId, orgName: data.orgName };
369
+ }
370
+
371
+ // Ambiguous — exists in multiple orgs
372
+ if (data.ambiguous) {
373
+ console.log(`\n Course "${slug}" exists in multiple organizations:\n`);
374
+ data.matches.forEach((m, i) => {
375
+ console.log(` ${i + 1}. ${m.orgName}`);
376
+ });
377
+ const answer = await prompt('\n Which org? ');
378
+ const idx = parseInt(answer, 10) - 1;
379
+ if (idx < 0 || idx >= data.matches.length) {
380
+ console.error('\nāŒ Invalid selection.\n');
381
+ process.exit(1);
382
+ }
383
+ const match = data.matches[idx];
384
+ const binding = { orgId: match.orgId, courseId: match.courseId, slug };
385
+ writeProjectConfig(binding);
386
+ return { orgId: match.orgId, courseId: match.courseId, orgName: match.orgName };
387
+ }
388
+
389
+ // Not found — auto-create
390
+ const orgs = data.orgs || [];
391
+ if (orgs.length === 0) {
392
+ console.error('\nāŒ You don\'t belong to any organizations. Create one at coursecodecloud.com.\n');
393
+ process.exit(1);
394
+ }
395
+
396
+ let targetOrg;
397
+ if (orgs.length === 1) {
398
+ targetOrg = orgs[0];
399
+ } else {
400
+ console.log(`\n Course "${slug}" not found on Cloud. Creating...\n`);
401
+ console.log(' You belong to multiple organizations:\n');
402
+ orgs.forEach((org, i) => {
403
+ console.log(` ${i + 1}. ${org.name} (${org.role})`);
404
+ });
405
+ const answer = await prompt('\n Which org? ');
406
+ const idx = parseInt(answer, 10) - 1;
407
+ if (idx < 0 || idx >= orgs.length) {
408
+ console.error('\nāŒ Invalid selection.\n');
409
+ process.exit(1);
410
+ }
411
+ targetOrg = orgs[idx];
412
+ }
413
+
414
+ return { orgId: targetOrg.id, courseId: null, orgName: targetOrg.name };
415
+ }
416
+
417
+ // =============================================================================
418
+ // FORMAT HELPERS
419
+ // =============================================================================
420
+
421
+ function formatBytes(bytes) {
422
+ if (bytes < 1024) return `${bytes} B`;
423
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
424
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
425
+ }
426
+
427
+ function formatDate(isoString) {
428
+ if (!isoString) return '—';
429
+ return new Date(isoString).toLocaleDateString('en-US', {
430
+ year: 'numeric', month: 'short', day: 'numeric',
431
+ hour: 'numeric', minute: '2-digit',
432
+ });
433
+ }
434
+
435
+ // =============================================================================
436
+ // CLI COMMANDS
437
+ // =============================================================================
438
+
439
+ /**
440
+ * coursecode login — explicit (re-)authentication
441
+ */
442
+ export async function login() {
443
+ console.log('\nšŸ”‘ Logging in to CourseCode Cloud...\n');
444
+ await runLoginFlow();
445
+
446
+ // Show who they are
447
+ const token = readCredentials()?.token;
448
+ if (token) {
449
+ const res = await cloudFetch('/api/cli/whoami', {}, token);
450
+ if (res.ok) {
451
+ const data = await res.json();
452
+ console.log(` āœ“ Logged in as ${data.full_name} (${data.email})\n`);
453
+ return;
454
+ }
455
+ }
456
+ console.log('');
457
+ }
458
+
459
+ /**
460
+ * coursecode logout — delete credentials and local project.json
461
+ */
462
+ export async function logout() {
463
+ deleteCredentials();
464
+
465
+ // Also delete local project.json if it exists
466
+ const localConfig = path.join(process.cwd(), PROJECT_CONFIG_PATH);
467
+ try { fs.unlinkSync(localConfig); } catch { /* not there */ }
468
+
469
+ console.log('\nāœ“ Logged out of CourseCode Cloud.\n');
470
+ }
471
+
472
+ /**
473
+ * coursecode whoami — show user info and orgs
474
+ */
475
+ export async function whoami(options = {}) {
476
+ await ensureAuthenticated();
477
+
478
+ const makeRequest = async (_isRetry = false) => {
479
+ const token = readCredentials()?.token;
480
+ const res = await cloudFetch('/api/cli/whoami', {}, token);
481
+ return handleResponse(res, { retryFn: makeRequest, _isRetry });
482
+ };
483
+
484
+ const data = await makeRequest();
485
+
486
+ if (options.json) {
487
+ console.log(JSON.stringify(data));
488
+ return;
489
+ }
490
+
491
+ console.log(`\nāœ“ Logged in as ${data.full_name} (${data.email})`);
492
+ if (data.orgs?.length) {
493
+ console.log(' Organizations:');
494
+ for (const org of data.orgs) {
495
+ console.log(` ${org.name} (${org.role})`);
496
+ }
497
+ }
498
+ console.log('');
499
+ }
500
+
501
+ /**
502
+ * coursecode courses — list courses across all orgs
503
+ */
504
+ export async function listCourses() {
505
+ await ensureAuthenticated();
506
+
507
+ const makeRequest = async (_isRetry = false) => {
508
+ const token = readCredentials()?.token;
509
+ const res = await cloudFetch('/api/cli/courses', {}, token);
510
+ return handleResponse(res, { retryFn: makeRequest, _isRetry });
511
+ };
512
+
513
+ const courses = await makeRequest();
514
+
515
+ if (!courses.length) {
516
+ console.log('\n No courses found. Deploy one with: coursecode deploy\n');
517
+ return;
518
+ }
519
+
520
+ // Group by org
521
+ const byOrg = {};
522
+ for (const course of courses) {
523
+ const org = course.orgName || 'Unknown';
524
+ if (!byOrg[org]) byOrg[org] = [];
525
+ byOrg[org].push(course);
526
+ }
527
+
528
+ console.log('');
529
+ for (const [orgName, orgCourses] of Object.entries(byOrg)) {
530
+ console.log(`${orgName}:`);
531
+ for (const c of orgCourses) {
532
+ const repo = c.github_repo ? `GitHub: ${c.github_repo}` : '—';
533
+ console.log(` ${c.slug.padEnd(22)} ${(c.source_type || '').padEnd(13)} ${repo}`);
534
+ }
535
+ console.log('');
536
+ }
537
+ }
538
+
539
+ /**
540
+ * coursecode deploy — build, zip, resolve org, upload
541
+ */
542
+ export async function deploy(options = {}) {
543
+ const { validateProject } = await import('./project-utils.js');
544
+ validateProject();
545
+
546
+ await ensureAuthenticated();
547
+ const slug = resolveSlug();
548
+
549
+ console.log('\nšŸ“¦ Building...\n');
550
+
551
+ // Step 1: Build
552
+ const { build } = await import('./build.js');
553
+ await build({ ...options, _skipValidation: true });
554
+
555
+ // Step 2: Verify dist/ exists
556
+ const distPath = path.join(process.cwd(), 'dist');
557
+ if (!fs.existsSync(distPath)) {
558
+ console.error('\nāŒ Build did not produce a dist/ directory.\n');
559
+ process.exit(1);
560
+ }
561
+
562
+ // Step 3: Resolve org
563
+ const { orgId, courseId, orgName } = await resolveOrgAndCourse(slug, readCredentials()?.token);
564
+ const displayOrg = orgName ? ` to ${orgName}` : '';
565
+
566
+ // Step 4: Zip dist/ contents
567
+ const zipPath = path.join(os.tmpdir(), `coursecode-deploy-${Date.now()}.zip`);
568
+ await zipDirectory(distPath, zipPath);
569
+
570
+ // Step 5: Upload
571
+ const mode = options.preview ? 'preview' : 'production';
572
+ console.log(`\nDeploying ${slug}${displayOrg} as ${mode}...\n`);
573
+
574
+ const formData = new FormData();
575
+ const zipBuffer = fs.readFileSync(zipPath);
576
+ formData.append('file', new Blob([zipBuffer], { type: 'application/zip' }), 'deploy.zip');
577
+ formData.append('orgId', orgId);
578
+
579
+ if (options.preview && options.password) {
580
+ const pw = await prompt(' Preview password: ');
581
+ formData.append('password', pw);
582
+ }
583
+
584
+ const queryString = options.preview ? '?mode=preview' : '';
585
+
586
+ const makeRequest = async (_isRetry = false) => {
587
+ const token = readCredentials()?.token;
588
+ const res = await cloudFetch(
589
+ `/api/cli/courses/${encodeURIComponent(slug)}/deploy${queryString}`,
590
+ { method: 'POST', body: formData },
591
+ token
592
+ );
593
+ return handleResponse(res, { retryFn: makeRequest, _isRetry });
594
+ };
595
+
596
+ const result = await makeRequest();
597
+
598
+ // Step 6: Write project.json + stamp cloudId
599
+ const finalCourseId = result.courseId || courseId;
600
+ writeProjectConfig({
601
+ orgId: result.orgId || orgId,
602
+ courseId: finalCourseId,
603
+ slug,
604
+ });
605
+
606
+ // Stamp cloudId into .coursecoderc.json (committed, shared with team)
607
+ const rc = readRcConfig();
608
+ if (finalCourseId && (!rc || rc.cloudId !== finalCourseId)) {
609
+ writeRcCloudId(finalCourseId);
610
+ }
611
+
612
+ // Step 7: Display result
613
+ if (result.mode === 'preview') {
614
+ console.log(`āœ“ Preview deployed (${result.fileCount} files)`);
615
+ console.log(` URL: ${result.url}`);
616
+ if (result.expiresAt) console.log(` Expires: ${formatDate(result.expiresAt)}`);
617
+ } else {
618
+ console.log(`āœ“ Deployed to production (${result.fileCount} files, ${formatBytes(result.size)})`);
619
+ const cloudUrl = getCloudUrl();
620
+ console.log(` ${cloudUrl}/dashboard/courses/${result.courseId}`);
621
+ }
622
+ console.log('');
623
+
624
+ // Cleanup temp zip
625
+ try { fs.unlinkSync(zipPath); } catch { /* fine */ }
626
+ }
627
+
628
+ /**
629
+ * coursecode status — show deployment status for current course
630
+ */
631
+ export async function status() {
632
+ await ensureAuthenticated();
633
+ const slug = resolveSlug();
634
+
635
+ const projectConfig = readProjectConfig();
636
+ const orgQuery = projectConfig?.orgId ? `?orgId=${projectConfig.orgId}` : '';
637
+
638
+ const makeRequest = async (_isRetry = false) => {
639
+ const token = readCredentials()?.token;
640
+ const res = await cloudFetch(
641
+ `/api/cli/courses/${encodeURIComponent(slug)}/status${orgQuery}`,
642
+ {},
643
+ token
644
+ );
645
+ return handleResponse(res, { retryFn: makeRequest, _isRetry });
646
+ };
647
+
648
+ const data = await makeRequest();
649
+
650
+ console.log(`\n${data.slug} — ${data.name} (${data.orgName})\n`);
651
+
652
+ if (data.lastDeploy) {
653
+ console.log(`Last deploy: ${formatDate(data.lastDeploy)} (${data.lastDeployFileCount} files, ${formatBytes(data.lastDeploySize)})`);
654
+ } else {
655
+ console.log('Last deploy: Never');
656
+ }
657
+
658
+ if (data.errorCount24h != null) console.log(`Errors (24h): ${data.errorCount24h}`);
659
+ if (data.launchCount24h != null) console.log(`Launches (24h): ${data.launchCount24h}`);
660
+
661
+ if (data.previewUrl) {
662
+ console.log(`Preview: ${data.previewUrl}`);
663
+ if (data.previewExpiresAt) console.log(` Expires ${formatDate(data.previewExpiresAt)}`);
664
+ }
665
+
666
+ console.log('');
667
+ }
668
+
669
+ // =============================================================================
670
+ // ZIP HELPER
671
+ // =============================================================================
672
+
673
+ /**
674
+ * Zip a directory's contents using the system `zip` command.
675
+ * Falls back to a tar+gzip approach if zip isn't available.
676
+ */
677
+ function zipDirectory(sourceDir, outputPath) {
678
+ return new Promise((resolve, reject) => {
679
+ // Use system zip: cd into dir so paths are relative
680
+ exec(
681
+ `cd "${sourceDir}" && zip -r -q "${outputPath}" .`,
682
+ (error) => {
683
+ if (error) {
684
+ reject(new Error(`Failed to create zip: ${error.message}. Ensure 'zip' is installed.`));
685
+ } else {
686
+ resolve();
687
+ }
688
+ }
689
+ );
690
+ });
691
+ }