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.
- package/LICENSE +21 -0
- package/README.md +322 -0
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/bin/cli.js +331 -0
- package/framework/assets/logo-coursecode-black.svg +14 -0
- package/framework/assets/logo-coursecode-white.svg +14 -0
- package/framework/assets/logo-coursecode.svg +14 -0
- package/framework/css/01-base.css +160 -0
- package/framework/css/02-layout.css +499 -0
- package/framework/css/accessibility.css +834 -0
- package/framework/css/components/accordions.css +710 -0
- package/framework/css/components/assessments.css +520 -0
- package/framework/css/components/audio-player.css +570 -0
- package/framework/css/components/badges.css +80 -0
- package/framework/css/components/breadcrumbs.css +87 -0
- package/framework/css/components/buttons.css +707 -0
- package/framework/css/components/callouts.css +1280 -0
- package/framework/css/components/cards.css +475 -0
- package/framework/css/components/carousel.css +193 -0
- package/framework/css/components/checkbox-group.css +123 -0
- package/framework/css/components/checklist.css +203 -0
- package/framework/css/components/collapse.css +96 -0
- package/framework/css/components/comparison.css +33 -0
- package/framework/css/components/content-image.css +36 -0
- package/framework/css/components/document-gallery.css +425 -0
- package/framework/css/components/dropdown.css +115 -0
- package/framework/css/components/embed-frame.css +142 -0
- package/framework/css/components/engagement.css +412 -0
- package/framework/css/components/features.css +35 -0
- package/framework/css/components/flip-cards.css +253 -0
- package/framework/css/components/footer.css +353 -0
- package/framework/css/components/forms.css +294 -0
- package/framework/css/components/hero.css +216 -0
- package/framework/css/components/images.css +528 -0
- package/framework/css/components/interactive-timeline.css +274 -0
- package/framework/css/components/intro-cards.css +30 -0
- package/framework/css/components/lightbox.css +666 -0
- package/framework/css/components/loading.css +65 -0
- package/framework/css/components/modals.css +235 -0
- package/framework/css/components/notifications.css +107 -0
- package/framework/css/components/quote.css +150 -0
- package/framework/css/components/sidebar.css +684 -0
- package/framework/css/components/slide-header.css +52 -0
- package/framework/css/components/spinner.css +62 -0
- package/framework/css/components/stats.css +44 -0
- package/framework/css/components/steps.css +232 -0
- package/framework/css/components/tables.css +90 -0
- package/framework/css/components/tabs.css +347 -0
- package/framework/css/components/timeline.css +154 -0
- package/framework/css/components/toggle.css +95 -0
- package/framework/css/components/tooltip.css +226 -0
- package/framework/css/components/video-player.css +438 -0
- package/framework/css/design-tokens.css +707 -0
- package/framework/css/framework.css +86 -0
- package/framework/css/interactions/accessibility.css +75 -0
- package/framework/css/interactions/base.css +92 -0
- package/framework/css/interactions/drag-drop.css +295 -0
- package/framework/css/interactions/fill-in-the-blank.css +236 -0
- package/framework/css/interactions/hotspots.css +69 -0
- package/framework/css/interactions/index.css +45 -0
- package/framework/css/interactions/interactive-image.css +359 -0
- package/framework/css/interactions/likert.css +126 -0
- package/framework/css/interactions/matching.css +354 -0
- package/framework/css/interactions/numeric-input.css +78 -0
- package/framework/css/interactions/sequencing.css +378 -0
- package/framework/css/interactions/true-false.css +177 -0
- package/framework/css/layouts/article.css +258 -0
- package/framework/css/layouts/base.css +30 -0
- package/framework/css/layouts/canvas.css +38 -0
- package/framework/css/layouts/focused.css +236 -0
- package/framework/css/layouts/index.css +29 -0
- package/framework/css/layouts/presentation.css +191 -0
- package/framework/css/layouts/traditional.css +52 -0
- package/framework/css/responsive.css +439 -0
- package/framework/css/utilities/accessibility-utils.css +59 -0
- package/framework/css/utilities/animations.css +419 -0
- package/framework/css/utilities/borders.css +72 -0
- package/framework/css/utilities/colors.css +76 -0
- package/framework/css/utilities/container.css +46 -0
- package/framework/css/utilities/decorative.css +442 -0
- package/framework/css/utilities/display.css +257 -0
- package/framework/css/utilities/flexbox.css +80 -0
- package/framework/css/utilities/grid.css +69 -0
- package/framework/css/utilities/icons.css +534 -0
- package/framework/css/utilities/lists.css +190 -0
- package/framework/css/utilities/spacing.css +167 -0
- package/framework/css/utilities/tables.css +81 -0
- package/framework/css/utilities/typography.css +159 -0
- package/framework/css/utilities/visibility.css +117 -0
- package/framework/docs/COURSE_AUTHORING_GUIDE.md +1773 -0
- package/framework/docs/COURSE_OUTLINE_GUIDE.md +725 -0
- package/framework/docs/COURSE_OUTLINE_TEMPLATE.md +161 -0
- package/framework/docs/DATA_MODEL.md +409 -0
- package/framework/docs/FRAMEWORK_GUIDE.md +1088 -0
- package/framework/docs/USER_GUIDE.md +583 -0
- package/framework/docs/examples/cloudflare-channel-relay.js +169 -0
- package/framework/docs/examples/cloudflare-data-worker.js +102 -0
- package/framework/docs/examples/cloudflare-error-worker.js +228 -0
- package/framework/index.html +175 -0
- package/framework/js/app/AppActions.js +410 -0
- package/framework/js/app/AppState.js +225 -0
- package/framework/js/app/AppUI.js +616 -0
- package/framework/js/assessment/AssessmentActions.js +615 -0
- package/framework/js/assessment/AssessmentFactory.js +471 -0
- package/framework/js/assessment/AssessmentState.js +322 -0
- package/framework/js/assessment/AssessmentUI.js +451 -0
- package/framework/js/automation/api-engagement.js +196 -0
- package/framework/js/automation/api-interactions.js +167 -0
- package/framework/js/automation/api.js +242 -0
- package/framework/js/automation/index.js +41 -0
- package/framework/js/components/interactions/drag-drop.js +884 -0
- package/framework/js/components/interactions/fill-in.js +535 -0
- package/framework/js/components/interactions/hotspot.js +702 -0
- package/framework/js/components/interactions/interaction-base.js +511 -0
- package/framework/js/components/interactions/likert.js +301 -0
- package/framework/js/components/interactions/matching.js +699 -0
- package/framework/js/components/interactions/multiple-choice.js +377 -0
- package/framework/js/components/interactions/numeric.js +271 -0
- package/framework/js/components/interactions/sequencing.js +423 -0
- package/framework/js/components/interactions/true-false.js +241 -0
- package/framework/js/components/ui-components/accordion.js +442 -0
- package/framework/js/components/ui-components/alert.js +88 -0
- package/framework/js/components/ui-components/audio-player.js +1193 -0
- package/framework/js/components/ui-components/callout.js +121 -0
- package/framework/js/components/ui-components/carousel.js +145 -0
- package/framework/js/components/ui-components/checkbox-group.js +87 -0
- package/framework/js/components/ui-components/checklist.js +40 -0
- package/framework/js/components/ui-components/collapse.js +114 -0
- package/framework/js/components/ui-components/comparison.js +30 -0
- package/framework/js/components/ui-components/conditional-display.js +150 -0
- package/framework/js/components/ui-components/content-image.js +41 -0
- package/framework/js/components/ui-components/dropdown.js +262 -0
- package/framework/js/components/ui-components/embed-frame.js +274 -0
- package/framework/js/components/ui-components/features.js +33 -0
- package/framework/js/components/ui-components/flip-card.js +230 -0
- package/framework/js/components/ui-components/form-validator.js +76 -0
- package/framework/js/components/ui-components/hero.js +49 -0
- package/framework/js/components/ui-components/index.js +12 -0
- package/framework/js/components/ui-components/interactive-image.js +235 -0
- package/framework/js/components/ui-components/interactive-timeline.js +285 -0
- package/framework/js/components/ui-components/intro-cards.js +35 -0
- package/framework/js/components/ui-components/lightbox.js +652 -0
- package/framework/js/components/ui-components/modal.js +386 -0
- package/framework/js/components/ui-components/notifications.js +145 -0
- package/framework/js/components/ui-components/progress.js +88 -0
- package/framework/js/components/ui-components/quote.js +41 -0
- package/framework/js/components/ui-components/stats.js +33 -0
- package/framework/js/components/ui-components/steps.js +41 -0
- package/framework/js/components/ui-components/tabs.js +255 -0
- package/framework/js/components/ui-components/timeline.js +42 -0
- package/framework/js/components/ui-components/toggle-group.js +73 -0
- package/framework/js/components/ui-components/tooltip.js +458 -0
- package/framework/js/components/ui-components/value-display.js +133 -0
- package/framework/js/components/ui-components/video-player.js +686 -0
- package/framework/js/core/component-catalog.js +121 -0
- package/framework/js/core/event-bus.js +178 -0
- package/framework/js/core/interaction-catalog.js +149 -0
- package/framework/js/dev/runtime-linter.js +1725 -0
- package/framework/js/drivers/cmi5-driver.js +768 -0
- package/framework/js/drivers/driver-factory.js +77 -0
- package/framework/js/drivers/driver-interface.js +110 -0
- package/framework/js/drivers/http-driver-base.js +241 -0
- package/framework/js/drivers/lti-driver.js +508 -0
- package/framework/js/drivers/proxy-driver.js +444 -0
- package/framework/js/drivers/scorm-12-driver.js +560 -0
- package/framework/js/drivers/scorm-2004-driver.js +775 -0
- package/framework/js/drivers/scorm-driver-base.js +112 -0
- package/framework/js/engagement/engagement-manager.js +404 -0
- package/framework/js/engagement/engagement-progress.js +191 -0
- package/framework/js/engagement/engagement-trackers.js +215 -0
- package/framework/js/engagement/requirement-strategies.js +268 -0
- package/framework/js/main.js +727 -0
- package/framework/js/managers/accessibility-manager.js +499 -0
- package/framework/js/managers/assessment-manager.js +230 -0
- package/framework/js/managers/audio-manager.js +944 -0
- package/framework/js/managers/comment-manager.js +88 -0
- package/framework/js/managers/flag-manager.js +86 -0
- package/framework/js/managers/interaction-manager.js +254 -0
- package/framework/js/managers/interaction-registry.js +96 -0
- package/framework/js/managers/objective-manager.js +423 -0
- package/framework/js/managers/score-manager.js +441 -0
- package/framework/js/managers/video-manager.js +536 -0
- package/framework/js/navigation/Breadcrumbs.js +234 -0
- package/framework/js/navigation/NavigationActions.js +1132 -0
- package/framework/js/navigation/NavigationState.js +276 -0
- package/framework/js/navigation/NavigationUI.js +574 -0
- package/framework/js/navigation/document-gallery.js +357 -0
- package/framework/js/navigation/navigation-helpers.js +175 -0
- package/framework/js/navigation/navigation-validators.js +174 -0
- package/framework/js/state/index.js +8 -0
- package/framework/js/state/lms-connection.js +482 -0
- package/framework/js/state/lms-error-utils.js +58 -0
- package/framework/js/state/state-commits.js +200 -0
- package/framework/js/state/state-domains.js +86 -0
- package/framework/js/state/state-manager.js +502 -0
- package/framework/js/state/state-validation.js +311 -0
- package/framework/js/state/transaction-log.js +41 -0
- package/framework/js/state/xapi-statement-service.js +325 -0
- package/framework/js/utilities/access-control.js +99 -0
- package/framework/js/utilities/breakpoint-manager.js +315 -0
- package/framework/js/utilities/canvas-slide.js +35 -0
- package/framework/js/utilities/conditional-display.js +388 -0
- package/framework/js/utilities/course-channel.js +214 -0
- package/framework/js/utilities/course-helpers.js +420 -0
- package/framework/js/utilities/data-reporter.js +273 -0
- package/framework/js/utilities/error-reporter.js +313 -0
- package/framework/js/utilities/hotspot-helper.js +341 -0
- package/framework/js/utilities/icons.js +348 -0
- package/framework/js/utilities/logger.js +92 -0
- package/framework/js/utilities/markdown-renderer.js +45 -0
- package/framework/js/utilities/scroll-tracker.js +68 -0
- package/framework/js/utilities/ui-initializer.js +146 -0
- package/framework/js/utilities/utilities.js +293 -0
- package/framework/js/utilities/view-manager.js +227 -0
- package/framework/js/validation/html-validators.js +422 -0
- package/framework/js/validation/scorm-validators.js +438 -0
- package/framework/js/vendor/pipwerks.js +931 -0
- package/framework/scripts/generate-narration.js +629 -0
- package/framework/scripts/tts-providers/azure-provider.js +178 -0
- package/framework/scripts/tts-providers/base-provider.js +81 -0
- package/framework/scripts/tts-providers/deepgram-provider.js +135 -0
- package/framework/scripts/tts-providers/elevenlabs-provider.js +148 -0
- package/framework/scripts/tts-providers/google-provider.js +272 -0
- package/framework/scripts/tts-providers/index.js +158 -0
- package/framework/scripts/tts-providers/openai-provider.js +143 -0
- package/framework/version.json +63 -0
- package/lib/authoring-api.js +919 -0
- package/lib/build-linter.js +450 -0
- package/lib/build-packaging.js +186 -0
- package/lib/build.js +88 -0
- package/lib/cloud.js +691 -0
- package/lib/convert.js +341 -0
- package/lib/course-parser.js +936 -0
- package/lib/course-writer.js +258 -0
- package/lib/create.js +248 -0
- package/lib/css-index.js +237 -0
- package/lib/dev.js +51 -0
- package/lib/export-content.js +1246 -0
- package/lib/headless-browser.js +413 -0
- package/lib/import.js +377 -0
- package/lib/index.js +80 -0
- package/lib/info.js +79 -0
- package/lib/interaction-formatters.js +568 -0
- package/lib/manifest/cmi5-manifest.js +63 -0
- package/lib/manifest/lti-tool-config.js +53 -0
- package/lib/manifest/manifest-factory.js +99 -0
- package/lib/manifest/scorm-12-manifest.js +61 -0
- package/lib/manifest/scorm-2004-manifest.js +94 -0
- package/lib/manifest/scorm-proxy-manifest.js +104 -0
- package/lib/manifest-parser.js +96 -0
- package/lib/mcp-prompts.js +753 -0
- package/lib/mcp-server.js +316 -0
- package/lib/narration.js +53 -0
- package/lib/pdf-structure.js +142 -0
- package/lib/preview-export.js +231 -0
- package/lib/preview-routes-api.js +662 -0
- package/lib/preview-routes-editing.js +159 -0
- package/lib/preview-routes-lms.js +230 -0
- package/lib/preview-server.js +564 -0
- package/lib/project-utils.js +269 -0
- package/lib/proxy-templates/proxy.html +68 -0
- package/lib/proxy-templates/scorm-bridge.js +112 -0
- package/lib/scaffold.js +193 -0
- package/lib/schema-extractor.js +361 -0
- package/lib/slide-source-editor.js +586 -0
- package/lib/stub-player/app-viewer.js +195 -0
- package/lib/stub-player/app.js +370 -0
- package/lib/stub-player/catalog-panel.js +312 -0
- package/lib/stub-player/config-panel.js +1303 -0
- package/lib/stub-player/content-generator.js +586 -0
- package/lib/stub-player/content-viewer.js +173 -0
- package/lib/stub-player/debug-panel.js +420 -0
- package/lib/stub-player/edit-mode.js +922 -0
- package/lib/stub-player/edit-utils.js +400 -0
- package/lib/stub-player/header-bar.js +354 -0
- package/lib/stub-player/interaction-editor.js +210 -0
- package/lib/stub-player/interactions-panel.js +565 -0
- package/lib/stub-player/lms-api.js +1094 -0
- package/lib/stub-player/login-screen.js +74 -0
- package/lib/stub-player/outline-mode.js +689 -0
- package/lib/stub-player/styles/_assessments-panel.css +245 -0
- package/lib/stub-player/styles/_base.css +89 -0
- package/lib/stub-player/styles/_catalog-icons.css +96 -0
- package/lib/stub-player/styles/_catalog-panel.css +291 -0
- package/lib/stub-player/styles/_config-panel.css +636 -0
- package/lib/stub-player/styles/_content-viewer.css +834 -0
- package/lib/stub-player/styles/_debug-panel.css +576 -0
- package/lib/stub-player/styles/_edit-mode.css +128 -0
- package/lib/stub-player/styles/_header-bar.css +343 -0
- package/lib/stub-player/styles/_interaction-editor.css +140 -0
- package/lib/stub-player/styles/_interactions-panel.css +1038 -0
- package/lib/stub-player/styles/_login-screen.css +102 -0
- package/lib/stub-player/styles/_outline-mode.css +752 -0
- package/lib/stub-player/styles.css +15 -0
- package/lib/stub-player.js +160 -0
- package/lib/test-data-reporting.js +176 -0
- package/lib/test-error-reporting.js +146 -0
- package/lib/token.js +86 -0
- package/lib/upgrade.js +257 -0
- package/lib/validation-rules.js +517 -0
- package/lib/vite-plugin-content-discovery.js +296 -0
- package/package.json +108 -0
- package/schemas/XMLSchema.dtd +402 -0
- package/schemas/adlcp_v1p3.xsd +111 -0
- package/schemas/adlnav_v1p3.xsd +61 -0
- package/schemas/adlseq_v1p3.xsd +93 -0
- package/schemas/common/anyElement.xsd +27 -0
- package/schemas/common/dataTypes.xsd +138 -0
- package/schemas/common/elementNames.xsd +767 -0
- package/schemas/common/elementTypes.xsd +786 -0
- package/schemas/common/rootElement.xsd +31 -0
- package/schemas/common/vocabTypes.xsd +345 -0
- package/schemas/common/vocabValues.xsd +257 -0
- package/schemas/datatypes.dtd +203 -0
- package/schemas/ims_xml.xsd +35 -0
- package/schemas/imscp_v1p1.xsd +368 -0
- package/schemas/imsss_v1p0.xsd +67 -0
- package/schemas/imsss_v1p0auxresource.xsd +19 -0
- package/schemas/imsss_v1p0control.xsd +20 -0
- package/schemas/imsss_v1p0delivery.xsd +17 -0
- package/schemas/imsss_v1p0limit.xsd +47 -0
- package/schemas/imsss_v1p0objective.xsd +67 -0
- package/schemas/imsss_v1p0random.xsd +16 -0
- package/schemas/imsss_v1p0rollup.xsd +46 -0
- package/schemas/imsss_v1p0seqrule.xsd +108 -0
- package/schemas/imsss_v1p0util.xsd +94 -0
- package/schemas/license.txt +17 -0
- package/schemas/lom.xsd +102 -0
- package/schemas/lomCustom.xsd +62 -0
- package/schemas/lomLoose.xsd +62 -0
- package/schemas/lomStrict.xsd +62 -0
- package/schemas/xml.xsd +81 -0
- package/template/.env.example +92 -0
- package/template/course/assets/audio/example-intro.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--compact-player.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--demo-modal.mp3 +0 -0
- package/template/course/assets/audio/example-ui-demo--full-player.mp3 +0 -0
- package/template/course/assets/docs/example_md_1.md +39 -0
- package/template/course/assets/docs/example_md_2.md +41 -0
- package/template/course/assets/docs/example_pdf_1_thumbnail.png +0 -0
- package/template/course/assets/docs/example_pdf_2.pdf +0 -0
- package/template/course/assets/images/course-architecture.svg +36 -0
- package/template/course/assets/images/logo.svg +14 -0
- package/template/course/assets/widgets/counter-demo.html +190 -0
- package/template/course/assets/widgets/gravity-painter.html +384 -0
- package/template/course/course-config.js +539 -0
- package/template/course/icons.js +19 -0
- package/template/course/interactions/PLUGIN_GUIDE.md +97 -0
- package/template/course/slides/example-course-structure.js +138 -0
- package/template/course/slides/example-final-exam.js +144 -0
- package/template/course/slides/example-finishing.js +127 -0
- package/template/course/slides/example-interactions-showcase.js +615 -0
- package/template/course/slides/example-preview-tour.js +129 -0
- package/template/course/slides/example-remedial.js +143 -0
- package/template/course/slides/example-summary.js +103 -0
- package/template/course/slides/example-ui-showcase.js +1805 -0
- package/template/course/slides/example-welcome.js +123 -0
- package/template/course/slides/example-workflow.js +140 -0
- package/template/course/theme.css +165 -0
- package/template/eslint.config.js +47 -0
- package/template/package.json +28 -0
- 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
|
+
}
|