coursecode 0.1.19 → 0.1.21

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.
@@ -123,7 +123,7 @@ The browser only downloads the one chunk matching the meta tag. Unused driver ch
123
123
  | `coursecode build` | `dist/` | Universal build + format manifest + meta tag stamped |
124
124
  | `coursecode build` (with `PACKAGE=true`) | `dist/` + ZIP | Same + format-specific ZIP for LMS upload |
125
125
  | `coursecode preview --export` | `course-preview/` | Copy of `dist/` wrapped in stub player (for Netlify/GitHub Pages) |
126
- | `coursecode deploy` | Uploads `dist/` | Cloud hosts universal build, assembles format ZIPs on demand. Flags: `--promote` (force live), `--stage` (force staged), `--preview` (preview-only: production untouched, preview always moved), `--repair-binding` (clear stale local cloud binding first if the remote course was deleted). `--promote`/`--stage` are mutually exclusive; `--preview` stacks with either. |
126
+ | `coursecode deploy` | Uploads `dist/` | Cloud hosts universal build, assembles format ZIPs on demand. Flags: `--promote` (force live), `--stage` (force staged), `--preview` (preview-only: production untouched, preview always moved), `--repair-binding` (clear stale local cloud binding first if the remote course was deleted). `--promote`/`--stage` are mutually exclusive; `--preview` stacks with either. **GitHub-linked courses**: production deploy blocked; only `--preview` allowed (see GitHub Source Guard below). |
127
127
 
128
128
  The ZIP never includes preview/stub player assets. Preview is a separate concern (see below).
129
129
 
@@ -610,6 +610,20 @@ User: coursecode build → uploads dist/
610
610
 
611
611
  **Security boundary:** The cloud never executes `course-config.js` or any user-authored JavaScript. The meta tag and manifest are the only format-specific artifacts, and both are generated from trusted framework source code.
612
612
 
613
+ #### GitHub Source Guard
614
+
615
+ Courses linked to GitHub for production deploys are protected from accidental CLI overwrites via two layers:
616
+
617
+ | Layer | Mechanism | Key |
618
+ |-------|-----------|-----|
619
+ | **CLI (local)** | `deploy()` reads `sourceType` from `.coursecoderc.json` | Fast fail before build — no wasted time |
620
+ | **Server (safety net)** | Deploy endpoint rejects non-preview uploads for `source_type = 'github'` | Can't bypass via old CLI or `curl` |
621
+
622
+ - **`--preview` always allowed** — useful for testing without touching production pointer
623
+ - **`.coursecoderc.json`** carries `sourceType` and `githubRepo` (committed to repo by cloud's GitHub integration), so anyone cloning gets the guard automatically
624
+ - **Self-healing on unlink:** If the cloud course is unlinked from GitHub, both `status()` and `deploy()` reconcile — they detect the server no longer reports `source_type: 'github'` and clear the local `sourceType`/`githubRepo` from `.coursecoderc.json`. No manual cleanup or repo commit needed.
625
+ - CLI error code: `github_source_blocked` (structured JSON for Desktop/CI consumers)
626
+
613
627
  ---
614
628
 
615
629
  ## Course Validation
@@ -740,6 +740,7 @@ Open the URL in any browser, log in with your CourseCode account, and enter the
740
740
  - **Preview pointer** — the version served on the cloud preview link (for stakeholder review).
741
741
  - **deploy_mode** — a per-course or org setting in the Cloud dashboard. Default is auto-promote (new uploads immediately go live). Can be set to staged (new uploads require a manual promote step).
742
742
  - `--promote` and `--stage` are mutually exclusive.
743
+ - **GitHub-linked courses:** If your course is connected to a GitHub repo in the Cloud dashboard, production deploys happen via `git push` — the CLI blocks direct production uploads. Use `coursecode deploy --preview` to push a preview build for stakeholder review.
743
744
  - If a cloud deployment was deleted outside the CLI and this project still has the old local binding, rerun with `coursecode deploy --repair-binding`. To clear the stale binding without deploying yet, run `coursecode status --repair-binding`.
744
745
 
745
746
  **Typical Cloud workflow:**
package/lib/cloud.js CHANGED
@@ -982,6 +982,45 @@ export async function deploy(options = {}) {
982
982
  if (handled) return;
983
983
  } else if (!statusRes.ok) {
984
984
  await handleResponse(statusRes);
985
+ } else {
986
+ // Reconcile local sourceType with cloud truth (handles unlink-via-dashboard)
987
+ try {
988
+ const statusData = JSON.parse(await statusRes.text());
989
+ const serverSourceType = statusData.source?.type || statusData.source_type;
990
+ const localRc = readRcConfig();
991
+ if (localRc?.sourceType === 'github' && serverSourceType !== 'github') {
992
+ updateRcConfig((rc) => {
993
+ delete rc.sourceType;
994
+ delete rc.githubRepo;
995
+ return rc;
996
+ });
997
+ log(' ℹ️ GitHub link removed on Cloud — updated local config.\n');
998
+ }
999
+ } catch { /* non-critical — guard will use whatever rcConfig has */ }
1000
+ }
1001
+ }
1002
+
1003
+ // Block production deploys for GitHub-linked courses
1004
+ const rcConfig = readRcConfig();
1005
+ if (rcConfig?.sourceType === 'github') {
1006
+ if (options.preview) {
1007
+ log(' ℹ️ GitHub-linked course — deploying preview only.\n');
1008
+ } else {
1009
+ const repo = rcConfig.githubRepo || 'unknown';
1010
+ logErr(`\n❌ This course deploys to production via GitHub, not CLI.`);
1011
+ logErr(` Repo: ${repo}`);
1012
+ logErr(' Push to your repo to trigger a production deploy.');
1013
+ logErr(' Use --preview to deploy a preview build via CLI.\n');
1014
+ if (options.json) {
1015
+ process.stdout.write(JSON.stringify({
1016
+ success: false,
1017
+ error: 'Production deploy blocked — course is GitHub-linked',
1018
+ errorCode: 'github_source_blocked',
1019
+ githubRepo: repo,
1020
+ hint: 'Use --preview for preview deploys, or push to GitHub for production.',
1021
+ }) + '\n');
1022
+ }
1023
+ process.exit(1);
985
1024
  }
986
1025
  }
987
1026
 
@@ -1294,6 +1333,20 @@ export async function status(options = {}) {
1294
1333
 
1295
1334
  const data = await handleResponse(firstRes, { retryFn: makeRequest, _isRetry: false });
1296
1335
 
1336
+ // Reconcile local sourceType with cloud truth (handles unlink-via-dashboard)
1337
+ const localRc = readRcConfig();
1338
+ const serverSourceType = data.source?.type || data.source_type;
1339
+ if (localRc?.sourceType === 'github' && serverSourceType !== 'github') {
1340
+ updateRcConfig((rc) => {
1341
+ delete rc.sourceType;
1342
+ delete rc.githubRepo;
1343
+ return rc;
1344
+ });
1345
+ if (!options.json) {
1346
+ console.log(' ℹ️ GitHub link removed on Cloud — updated local config.\n');
1347
+ }
1348
+ }
1349
+
1297
1350
  if (options.json) {
1298
1351
  process.stdout.write(JSON.stringify(data) + '\n');
1299
1352
  return;
@@ -1301,14 +1354,13 @@ export async function status(options = {}) {
1301
1354
 
1302
1355
  console.log(`\n${data.slug} — ${data.name} (${data.orgName})\n`);
1303
1356
 
1304
- if (data.source?.type === 'github' && data.source?.githubRepo) {
1305
- console.log(`Source: GitHub ${data.source.githubRepo}`);
1306
- console.log(' direct production deploy disabled; preview deploy allowed');
1307
- } else if (data.source_type === 'github' && data.github_repo) {
1308
- console.log(`Source: GitHub ${data.github_repo}`);
1309
- console.log(` (changes deploy via GitHub, not direct upload)`);
1310
- } else if (data.source_type) {
1311
- console.log(`Source: ${data.source_type}`);
1357
+ const sourceType = data.source?.type || data.source_type;
1358
+ const githubRepo = data.source?.githubRepo || data.github_repo;
1359
+ if (sourceType === 'github' && githubRepo) {
1360
+ console.log(`Source: GitHub ${githubRepo}`);
1361
+ console.log(' production=github | preview=cli+github');
1362
+ } else if (sourceType) {
1363
+ console.log(`Source: ${sourceType}`);
1312
1364
  }
1313
1365
 
1314
1366
  if (data.courseId) console.log(`Course ID: ${data.courseId}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecode",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Multi-format course authoring framework with CLI tools (SCORM 2004, SCORM 1.2, cmi5, LTI 1.3)",
5
5
  "type": "module",
6
6
  "bin": {