coursecode 0.1.46 → 0.1.48

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/README.md CHANGED
@@ -175,12 +175,27 @@ The preview server provides:
175
175
 
176
176
  When ready, deploy:
177
177
 
178
- **With [CourseCode Cloud](https://coursecodecloud.com)**: Push your course and get a live link. Cloud handles hosting, generates any LMS format on demand, and gives you sharable preview links with optional password protection. No ZIP files, no manual uploads.
178
+ **With [CourseCode Cloud](https://coursecodecloud.com)**: Push your course and get a live link. Cloud handles hosting, generates any LMS format on demand, and gives you a shareable preview link with optional password protection. No ZIP files, no manual uploads.
179
179
 
180
180
  ```bash
181
181
  coursecode deploy
182
182
  ```
183
183
 
184
+ For stakeholder review, deploy a preview-only version and password-protect the preview link:
185
+
186
+ ```bash
187
+ coursecode deploy --preview --password
188
+ ```
189
+
190
+ You can inspect recent deployments and move pointers without rebuilding:
191
+
192
+ ```bash
193
+ coursecode deployments
194
+ coursecode promote --preview
195
+ coursecode promote --production
196
+ coursecode preview-link --password
197
+ ```
198
+
184
199
  If the cloud course was deleted but the project still has the old local binding, redeploy with:
185
200
 
186
201
  ```bash
@@ -212,6 +227,9 @@ coursecode preview --export
212
227
  | `coursecode lint` | Validate course structure and content |
213
228
  | `coursecode build` | Build a package for LMS upload |
214
229
  | `coursecode deploy` | Build and deploy to CourseCode Cloud |
230
+ | `coursecode deployments` | List recent Cloud deployments |
231
+ | `coursecode promote` | Move the Production or Preview pointer |
232
+ | `coursecode preview-link` | Manage the Cloud preview link |
215
233
  | `coursecode narration` | Generate audio narration from text |
216
234
 
217
235
  For the full command list and deployment options, see the [User Guide](framework/docs/USER_GUIDE.md#sharing-and-deploying) or run `coursecode --help`.
package/bin/cli.js CHANGED
@@ -365,7 +365,7 @@ program
365
365
  .option('--promote', 'Force-promote: always move production pointer regardless of deploy_mode setting. Mutually exclusive with --stage.')
366
366
  .option('--stage', 'Force-stage: never move production pointer regardless of deploy_mode setting. Mutually exclusive with --promote.')
367
367
  .option('--repair-binding', 'Clear a stale local Cloud binding if the remote course was deleted, then continue')
368
- .option('--password', 'Password-protect preview (interactive prompt, requires --preview)')
368
+ .option('--password [password]', 'Password-protect preview. Prompts if no value is provided.')
369
369
  .option('-m, --message <message>', 'Deploy reason (e.g. "Fixed accessibility issues")')
370
370
  .option('--local', 'Use local Cloud instance (http://localhost:3000)')
371
371
  .option('--json', 'Emit machine-readable JSON result')
@@ -404,6 +404,17 @@ program
404
404
  await status({ json: options.json, repairBinding: options.repairBinding });
405
405
  });
406
406
 
407
+ program
408
+ .command('deployments')
409
+ .description('List recent Cloud deployments for current course')
410
+ .option('--local', 'Use local Cloud instance (http://localhost:3000)')
411
+ .option('--json', 'Output raw JSON')
412
+ .action(async (options) => {
413
+ const { deployments, setLocalMode } = await import('../lib/cloud.js');
414
+ if (options.local) setLocalMode();
415
+ await deployments({ json: options.json });
416
+ });
417
+
407
418
  program
408
419
  .command('preview-link')
409
420
  .description('Show or update the Cloud preview link for the current course')
@@ -741,18 +741,42 @@ Open the URL in any browser, log in with your CourseCode account, and enter the
741
741
  - **Preview pointer** — the version served on the cloud preview link (for stakeholder review).
742
742
  - **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).
743
743
  - `--promote` and `--stage` are mutually exclusive.
744
+ - `--password` can be combined with `--preview` to create or update the main preview link password. If you omit the password value in an interactive terminal, the CLI prompts for it. In `--json` mode you must pass the value explicitly.
744
745
  - **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.
745
746
  - 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`.
746
747
 
748
+ **Managing previews and pointers after deploy:**
749
+
750
+ Use these commands when you want to change Cloud state without rebuilding the course:
751
+
752
+ ```bash
753
+ coursecode status
754
+ coursecode deployments
755
+ coursecode promote --preview
756
+ coursecode promote --production
757
+ coursecode preview-link --enable
758
+ coursecode preview-link --password
759
+ coursecode preview-link --remove-password
760
+ coursecode preview-link --expires-in-days 7
761
+ coursecode preview-link --disable
762
+ ```
763
+
764
+ - `coursecode deployments` lists recent immutable deployments and marks the current Production and Preview pointers.
765
+ - `coursecode promote --preview` moves the Preview pointer to an existing deployment. If you do not pass `--deployment <id>`, the CLI prompts you to pick from recent deployments.
766
+ - `coursecode promote --production` moves the Production pointer. Preview-only deployments cannot be promoted to Production.
767
+ - `coursecode preview-link` manages the main preview link. That link follows the Preview pointer, so the URL can stay the same while you choose which deployment reviewers see.
768
+ - Cloud can also create additional pinned preview links for specific deployments in the web app. The main CLI preview link is the pointer-following link.
769
+
747
770
  **Typical Cloud workflow:**
748
771
  1. Run `coursecode login` once, open the URL shown, and enter the code.
749
- 2. Run `coursecode deploy` from your project folder.
772
+ 2. Run `coursecode deploy` from your project folder, or `coursecode deploy --preview --password` for a password-protected review build.
750
773
  3. Open the CourseCode Cloud dashboard link shown after deploy.
751
- 4. Use Cloud preview links for review.
752
- 5. Download the LMS format you need from Cloud when you're ready to deliver.
774
+ 4. Use the main preview link for review.
775
+ 5. Move the Preview or Production pointer when needed.
776
+ 6. Download the LMS format you need from Cloud when you're ready to deliver.
753
777
 
754
778
  **Prefer a GUI instead of the terminal?**
755
- - Use **CourseCode Desktop** for the same project workflow with buttons for Preview / Export / Deploy.
779
+ - Use **CourseCode Desktop** for the same project workflow with buttons for Preview / Export / Deploy, plus a focused Cloud Deployments panel for preview-link password/expiry management, recent deployments, and Production/Preview pointer changes.
756
780
  - Desktop docs: `coursecode-desktop/USER_GUIDE.md`
757
781
 
758
782
  **When to use Cloud vs local export:**
@@ -762,7 +786,7 @@ Open the URL in any browser, log in with your CourseCode account, and enter the
762
786
  **Benefits:**
763
787
  - **No format decisions** — download the right ZIP for any LMS directly from the cloud
764
788
  - **Instant updates** — redeploy and all future launches get the new version
765
- - **Preview sharing** — cloud provides a shareable preview link automatically
789
+ - **Preview sharing** — cloud provides a shareable preview link that can be password-protected and pointed at the review deployment you choose
766
790
 
767
791
  ### Exporting Content for Review
768
792
 
package/lib/cloud.js CHANGED
@@ -1036,6 +1036,28 @@ export async function deploy(options = {}) {
1036
1036
  const promoteMode = options.promote ? 'promote' : options.stage ? 'stage' : 'auto';
1037
1037
  const previewForce = !!options.preview;
1038
1038
  const previewOnly = previewForce && promoteMode === 'auto';
1039
+ let previewPassword;
1040
+ if (options.password !== undefined) {
1041
+ if (!previewForce) {
1042
+ logErr('\n❌ --password requires --preview.\n');
1043
+ if (options.json) process.stdout.write(JSON.stringify({ success: false, error: '--password requires --preview' }) + '\n');
1044
+ process.exit(1);
1045
+ }
1046
+ previewPassword = options.password;
1047
+ if (previewPassword === true || previewPassword === '') {
1048
+ if (options.json) {
1049
+ logErr('\n❌ --password requires a value when using --json.\n');
1050
+ process.stdout.write(JSON.stringify({ success: false, error: '--password requires a value when using --json' }) + '\n');
1051
+ process.exit(1);
1052
+ }
1053
+ previewPassword = await prompt(' Preview password: ');
1054
+ }
1055
+ if (!previewPassword) {
1056
+ logErr('\n❌ Preview password cannot be empty.\n');
1057
+ if (options.json) process.stdout.write(JSON.stringify({ success: false, error: 'Preview password cannot be empty' }) + '\n');
1058
+ process.exit(1);
1059
+ }
1060
+ }
1039
1061
 
1040
1062
  log('\n📦 Building...\n');
1041
1063
  emitProgress('building');
@@ -1196,6 +1218,7 @@ export async function deploy(options = {}) {
1196
1218
  previewForce,
1197
1219
  previewOnly,
1198
1220
  };
1221
+ if (previewPassword) finalizeBody.previewPassword = previewPassword;
1199
1222
  if (options.message) finalizeBody.message = options.message;
1200
1223
 
1201
1224
  const finalizeRes = await cloudFetch(
@@ -1544,6 +1567,51 @@ export async function status(options = {}) {
1544
1567
  console.log('');
1545
1568
  }
1546
1569
 
1570
+ /**
1571
+ * coursecode deployments — list recent deployments for the current course
1572
+ */
1573
+ export async function deployments(options = {}) {
1574
+ await ensureAuthenticated();
1575
+ const slug = resolveSlug();
1576
+ const rcConfig = readRcConfig();
1577
+ const orgQuery = rcConfig?.orgId ? `?orgId=${rcConfig.orgId}` : '';
1578
+
1579
+ const makeRequest = async (_isRetry = false) => {
1580
+ const token = readCredentials()?.token;
1581
+ const res = await cloudFetch(
1582
+ `/api/cli/courses/${encodeURIComponent(slug)}/versions${orgQuery}`,
1583
+ {},
1584
+ token
1585
+ );
1586
+ return handleResponse(res, { retryFn: makeRequest, _isRetry });
1587
+ };
1588
+
1589
+ const data = await makeRequest();
1590
+
1591
+ if (options.json) {
1592
+ process.stdout.write(JSON.stringify(data) + '\n');
1593
+ return;
1594
+ }
1595
+
1596
+ const rows = data.deployments ?? [];
1597
+ if (rows.length === 0) {
1598
+ console.log('\nNo deployments found for this course.\n');
1599
+ return;
1600
+ }
1601
+
1602
+ console.log(`\n${slug} — Recent Deployments\n`);
1603
+ rows.forEach((deployment, index) => {
1604
+ const marker = deployment.id === data.production_deployment_id
1605
+ ? ' [production]'
1606
+ : deployment.id === data.preview_deployment_id
1607
+ ? ' [preview]'
1608
+ : '';
1609
+ const flags = deployment.preview_only ? 'preview-only' : deployment.source;
1610
+ console.log(` ${index + 1}. ${formatDate(deployment.created_at)} — ${deployment.file_count} files, ${formatBytes(deployment.total_size)} — ${flags}${marker}`);
1611
+ });
1612
+ console.log('');
1613
+ }
1614
+
1547
1615
  /**
1548
1616
  * coursecode preview-link — show or update the current preview link
1549
1617
  */
@@ -1794,4 +1862,3 @@ export async function deleteCourse(options = {}) {
1794
1862
  }
1795
1863
  console.log('');
1796
1864
  }
1797
-
package/lib/create.js CHANGED
@@ -10,6 +10,44 @@ import { spawn } from 'child_process';
10
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
11
  const PACKAGE_ROOT = path.join(__dirname, '..');
12
12
 
13
+ export function toProjectDirectoryName(name) {
14
+ return String(name || '')
15
+ .trim()
16
+ .normalize('NFKD')
17
+ .replace(/[\u0300-\u036f]/g, '')
18
+ .toLowerCase()
19
+ .replace(/['’]/g, '')
20
+ .replace(/[^a-z0-9]+/g, '_')
21
+ .replace(/^_+|_+$/g, '');
22
+ }
23
+
24
+ function escapeSingleQuotedValue(value) {
25
+ return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
26
+ }
27
+
28
+ export function stampCourseTitle(configContent, title) {
29
+ const titleValue = escapeSingleQuotedValue(title);
30
+ let nextContent = configContent.replace(
31
+ /^(\s*)title\s*:\s*['"`][^'"`]*['"`](\s*,?)/m,
32
+ `$1title: '${titleValue}'$2`
33
+ );
34
+
35
+ nextContent = nextContent.replace(
36
+ /^(\s*)courseTitle\s*:\s*['"`][^'"`]*['"`](\s*,?)/m,
37
+ `$1courseTitle: '${titleValue}'$2`
38
+ );
39
+
40
+ return nextContent;
41
+ }
42
+
43
+ function writeCourseTitle(projectDir, title) {
44
+ const configPath = path.join(projectDir, 'course', 'course-config.js');
45
+ if (!fs.existsSync(configPath)) return;
46
+
47
+ const current = fs.readFileSync(configPath, 'utf-8');
48
+ fs.writeFileSync(configPath, stampCourseTitle(current, title), 'utf-8');
49
+ }
50
+
13
51
  /**
14
52
  * Copy directory recursively
15
53
  */
@@ -85,15 +123,31 @@ function gitInit(cwd) {
85
123
  }
86
124
 
87
125
  export async function create(name, options = {}) {
88
- const targetDir = path.resolve(process.cwd(), name);
126
+ const displayName = String(name || '').trim();
127
+ const directoryName = toProjectDirectoryName(displayName);
128
+
129
+ if (!displayName) {
130
+ console.error('\n❌ Course name is required.\n');
131
+ process.exit(1);
132
+ }
133
+
134
+ if (!directoryName) {
135
+ console.error('\n❌ Course name must include letters or numbers.\n');
136
+ process.exit(1);
137
+ }
138
+
139
+ const targetDir = path.resolve(process.cwd(), directoryName);
89
140
 
90
141
  // Check if directory already exists
91
142
  if (fs.existsSync(targetDir)) {
92
- console.error(`\n❌ Directory "${name}" already exists.\n`);
143
+ console.error(`\n❌ Directory "${directoryName}" already exists.\n`);
93
144
  process.exit(1);
94
145
  }
95
146
 
96
- console.log(`\n🚀 Creating CourseCode project: ${name}\n`);
147
+ console.log(`\n🚀 Creating CourseCode project: ${displayName}\n`);
148
+ if (directoryName !== displayName) {
149
+ console.log(` Project folder: ${directoryName}`);
150
+ }
97
151
 
98
152
  // Create project directory
99
153
  fs.mkdirSync(targetDir, { recursive: true });
@@ -138,7 +192,7 @@ export async function create(name, options = {}) {
138
192
  // Read and customize package.json
139
193
  const pkgPath = path.join(targetDir, 'package.json');
140
194
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
141
- pkg.name = name;
195
+ pkg.name = directoryName;
142
196
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
143
197
 
144
198
  // Create .coursecoderc.json to track framework version
@@ -161,6 +215,8 @@ export async function create(name, options = {}) {
161
215
  clean({ basePath: targetDir });
162
216
  }
163
217
 
218
+ writeCourseTitle(targetDir, displayName);
219
+
164
220
  // Install dependencies
165
221
  if (options.install !== false) {
166
222
  console.log('\n Installing dependencies...\n');
@@ -184,7 +240,7 @@ export async function create(name, options = {}) {
184
240
  // Print success message
185
241
  if (options.blank) {
186
242
  console.log(`
187
- ✅ CourseCode project "${name}" created (blank starter)!
243
+ ✅ CourseCode project "${displayName}" created (blank starter)!
188
244
 
189
245
  Course files:
190
246
  - course/course-config.js - Course metadata & structure (minimal)
@@ -199,7 +255,7 @@ export async function create(name, options = {}) {
199
255
  `);
200
256
  } else {
201
257
  console.log(`
202
- ✅ CourseCode project "${name}" created successfully!
258
+ ✅ CourseCode project "${displayName}" created successfully!
203
259
 
204
260
  Course files:
205
261
  - course/course-config.js - Course metadata & structure
@@ -228,9 +284,15 @@ export async function create(name, options = {}) {
228
284
 
229
285
  child.on('error', () => {
230
286
  console.warn(' ⚠️ Failed to start dev server. Run manually:');
231
- console.log(` cd ${name} && coursecode dev\n`);
287
+ console.log(` cd ${directoryName} && coursecode dev\n`);
232
288
  });
233
289
  } else {
234
- console.log(`\n To start developing:\n\n cd ${name}\n coursecode dev\n`);
290
+ console.log(`\n To start developing:\n\n cd ${directoryName}\n coursecode dev\n`);
235
291
  }
292
+
293
+ return {
294
+ displayName,
295
+ directoryName,
296
+ targetDir
297
+ };
236
298
  }
package/lib/import.js CHANGED
@@ -179,9 +179,9 @@ export async function importPresentation(source, options = {}) {
179
179
 
180
180
  // Create blank project (no example slides — they'd just be deleted)
181
181
  console.log(' ⏳ Creating course project...\n');
182
- await create(name, { blank: true, install: options.install });
182
+ const createdProject = await create(name, { blank: true, install: options.install });
183
183
 
184
- const targetDir = path.resolve(process.cwd(), name);
184
+ const targetDir = createdProject?.targetDir || path.resolve(process.cwd(), name);
185
185
  const courseDir = path.join(targetDir, 'course');
186
186
 
187
187
  // Import presentation in-place
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecode",
3
- "version": "0.1.46",
3
+ "version": "0.1.48",
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": {