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 +19 -1
- package/bin/cli.js +12 -1
- package/framework/docs/USER_GUIDE.md +29 -5
- package/lib/cloud.js +68 -1
- package/lib/create.js +70 -8
- package/lib/import.js +2 -2
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
752
|
-
5.
|
|
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
|
|
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
|
|
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 "${
|
|
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: ${
|
|
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 =
|
|
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 "${
|
|
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 "${
|
|
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 ${
|
|
287
|
+
console.log(` cd ${directoryName} && coursecode dev\n`);
|
|
232
288
|
});
|
|
233
289
|
} else {
|
|
234
|
-
console.log(`\n To start developing:\n\n cd ${
|
|
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
|