coursecode 0.1.11 → 0.1.13
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/bin/cli.js +2 -1
- package/lib/cloud.js +85 -13
- package/lib/convert.js +4 -3
- package/lib/stub-player/lms-api.js +1 -1
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -166,12 +166,13 @@ program
|
|
|
166
166
|
// Convert command
|
|
167
167
|
program
|
|
168
168
|
.command('convert [source]')
|
|
169
|
-
.description('Convert docx, pptx, and pdf files to markdown for course authoring')
|
|
169
|
+
.description('Convert docx, pptx, and pdf files to markdown for course authoring (PDF JSON sidecars optional)')
|
|
170
170
|
.option('-o, --output <dir>', 'Output directory for converted files', './course/references/converted')
|
|
171
171
|
.option('-f, --format <type>', 'Limit to format: docx, pptx, pdf, or all', 'all')
|
|
172
172
|
.option('--dry-run', 'Show what would be converted without writing files')
|
|
173
173
|
.option('--overwrite', 'Overwrite existing markdown files')
|
|
174
174
|
.option('--flatten', 'Output all files to single directory (no subdirs)')
|
|
175
|
+
.option('--pdf-json', 'Also write PDF structure JSON sidecars (.json) next to converted markdown')
|
|
175
176
|
.action(async (source = './course/references', options) => {
|
|
176
177
|
const { convert } = await import('../lib/convert.js');
|
|
177
178
|
await convert(source, options);
|
package/lib/cloud.js
CHANGED
|
@@ -29,8 +29,11 @@ const DEFAULT_CLOUD_URL = 'https://coursecodecloud.com';
|
|
|
29
29
|
const FALLBACK_CLOUD_URL = 'https://coursecode-cloud-web.vercel.app';
|
|
30
30
|
const LOCAL_CLOUD_URL = 'http://localhost:3000';
|
|
31
31
|
let useLocal = false;
|
|
32
|
+
let activeCloudUrl = null;
|
|
32
33
|
const CREDENTIALS_DIR = path.join(os.homedir(), '.coursecode');
|
|
33
34
|
const CREDENTIALS_PATH = path.join(CREDENTIALS_DIR, 'credentials.json');
|
|
35
|
+
const PROJECT_CONFIG_DIR = '.coursecode';
|
|
36
|
+
const PROJECT_CONFIG_PATH = path.join(PROJECT_CONFIG_DIR, 'project.json');
|
|
34
37
|
|
|
35
38
|
const POLL_INTERVAL_MS = 2000;
|
|
36
39
|
const POLL_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes (device code expiry)
|
|
@@ -91,12 +94,19 @@ function writeCredentials(token, cloudUrl = DEFAULT_CLOUD_URL) {
|
|
|
91
94
|
fs.writeFileSync(getCredentialsPath(), data, { mode: 0o600 });
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
function updateCredentialsCloudUrl(cloudUrl) {
|
|
98
|
+
const creds = readCredentials();
|
|
99
|
+
if (!creds?.token) return;
|
|
100
|
+
writeCredentials(creds.token, cloudUrl);
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
function deleteCredentials() {
|
|
95
104
|
try { fs.unlinkSync(getCredentialsPath()); } catch { /* already gone */ }
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
function getCloudUrl() {
|
|
99
108
|
if (useLocal) return LOCAL_CLOUD_URL;
|
|
109
|
+
if (activeCloudUrl) return activeCloudUrl;
|
|
100
110
|
return readCredentials()?.cloud_url || DEFAULT_CLOUD_URL;
|
|
101
111
|
}
|
|
102
112
|
|
|
@@ -106,6 +116,32 @@ function getCloudUrl() {
|
|
|
106
116
|
*/
|
|
107
117
|
export function setLocalMode() {
|
|
108
118
|
useLocal = true;
|
|
119
|
+
activeCloudUrl = LOCAL_CLOUD_URL;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// PROJECT BINDING (local: .coursecode/project.json)
|
|
124
|
+
// =============================================================================
|
|
125
|
+
|
|
126
|
+
function readProjectConfig() {
|
|
127
|
+
try {
|
|
128
|
+
const fullPath = path.join(process.cwd(), PROJECT_CONFIG_PATH);
|
|
129
|
+
if (!fs.existsSync(fullPath)) return null;
|
|
130
|
+
return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function writeProjectConfig(data) {
|
|
137
|
+
const dir = path.join(process.cwd(), PROJECT_CONFIG_DIR);
|
|
138
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
139
|
+
const existing = readProjectConfig() || {};
|
|
140
|
+
Object.assign(existing, data);
|
|
141
|
+
fs.writeFileSync(
|
|
142
|
+
path.join(process.cwd(), PROJECT_CONFIG_PATH),
|
|
143
|
+
JSON.stringify(existing, null, 2) + '\n'
|
|
144
|
+
);
|
|
109
145
|
}
|
|
110
146
|
|
|
111
147
|
// =============================================================================
|
|
@@ -176,7 +212,10 @@ async function cloudFetch(urlPath, options = {}, token = null) {
|
|
|
176
212
|
// Primary unreachable — try fallback before giving up
|
|
177
213
|
if (!useLocal) {
|
|
178
214
|
const fallback = await attemptFetch(FALLBACK_CLOUD_URL);
|
|
179
|
-
if (fallback)
|
|
215
|
+
if (fallback) {
|
|
216
|
+
activeCloudUrl = FALLBACK_CLOUD_URL;
|
|
217
|
+
return fallback;
|
|
218
|
+
}
|
|
180
219
|
}
|
|
181
220
|
console.error('\n❌ Could not connect to CourseCode Cloud. Check your internet connection.\n');
|
|
182
221
|
process.exit(1);
|
|
@@ -185,11 +224,29 @@ async function cloudFetch(urlPath, options = {}, token = null) {
|
|
|
185
224
|
// Peek at the body: if it's an HTML block page, silently retry on the fallback.
|
|
186
225
|
// We must buffer the text here since Response bodies can only be read once.
|
|
187
226
|
const text = await res.text();
|
|
227
|
+
|
|
228
|
+
// Token may be valid on the alternate cloud origin. Before triggering re-auth,
|
|
229
|
+
// retry authenticated 401s once on the other known origin.
|
|
230
|
+
if (res.status === 401 && token && !useLocal) {
|
|
231
|
+
const alternateUrl = primaryUrl === FALLBACK_CLOUD_URL ? DEFAULT_CLOUD_URL : FALLBACK_CLOUD_URL;
|
|
232
|
+
const alternateRes = await attemptFetch(alternateUrl);
|
|
233
|
+
if (alternateRes) {
|
|
234
|
+
const alternateText = await alternateRes.text();
|
|
235
|
+
if (!isBlockPage(alternateText) && alternateRes.status !== 401) {
|
|
236
|
+
activeCloudUrl = alternateUrl;
|
|
237
|
+
updateCredentialsCloudUrl(alternateUrl);
|
|
238
|
+
return syntheticResponse(alternateText, alternateRes.status);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
188
243
|
if (isBlockPage(text) && !useLocal) {
|
|
189
244
|
const fallbackRes = await attemptFetch(FALLBACK_CLOUD_URL);
|
|
190
245
|
if (fallbackRes) {
|
|
191
246
|
const fallbackText = await fallbackRes.text();
|
|
192
247
|
if (!isBlockPage(fallbackText)) {
|
|
248
|
+
activeCloudUrl = FALLBACK_CLOUD_URL;
|
|
249
|
+
updateCredentialsCloudUrl(FALLBACK_CLOUD_URL);
|
|
193
250
|
// Fallback succeeded — return a synthetic Response with the buffered text
|
|
194
251
|
return syntheticResponse(fallbackText, fallbackRes.status);
|
|
195
252
|
}
|
|
@@ -200,6 +257,7 @@ async function cloudFetch(urlPath, options = {}, token = null) {
|
|
|
200
257
|
}
|
|
201
258
|
|
|
202
259
|
// Primary response is fine — return a synthetic Response with the buffered text
|
|
260
|
+
activeCloudUrl = primaryUrl;
|
|
203
261
|
return syntheticResponse(text, res.status);
|
|
204
262
|
}
|
|
205
263
|
|
|
@@ -435,7 +493,7 @@ async function runLoginFlow({ jsonMode = false } = {}) {
|
|
|
435
493
|
if (data.pending) continue;
|
|
436
494
|
|
|
437
495
|
if (data.token) {
|
|
438
|
-
writeCredentials(data.token, getCloudUrl());
|
|
496
|
+
writeCredentials(data.token, activeCloudUrl || getCloudUrl());
|
|
439
497
|
log(' ✓ Logged in successfully\n');
|
|
440
498
|
return data.token;
|
|
441
499
|
}
|
|
@@ -453,7 +511,7 @@ async function runLoginFlow({ jsonMode = false } = {}) {
|
|
|
453
511
|
*/
|
|
454
512
|
async function runLegacyLoginFlow() {
|
|
455
513
|
const nonce = crypto.randomBytes(32).toString('hex');
|
|
456
|
-
const
|
|
514
|
+
const initialCloudUrl = getCloudUrl();
|
|
457
515
|
|
|
458
516
|
console.log(' → Registering session...');
|
|
459
517
|
const createRes = await cloudFetch('/api/auth/connect', {
|
|
@@ -469,7 +527,8 @@ async function runLegacyLoginFlow() {
|
|
|
469
527
|
process.exit(1);
|
|
470
528
|
}
|
|
471
529
|
|
|
472
|
-
const
|
|
530
|
+
const effectiveCloudUrl = activeCloudUrl || initialCloudUrl;
|
|
531
|
+
const loginUrl = `${effectiveCloudUrl}/auth/connect?session=${nonce}`;
|
|
473
532
|
console.log(' → Opening browser for authentication...');
|
|
474
533
|
openBrowser(loginUrl);
|
|
475
534
|
|
|
@@ -490,7 +549,7 @@ async function runLegacyLoginFlow() {
|
|
|
490
549
|
if (data.pending) continue;
|
|
491
550
|
|
|
492
551
|
if (data.token) {
|
|
493
|
-
writeCredentials(data.token,
|
|
552
|
+
writeCredentials(data.token, activeCloudUrl || initialCloudUrl);
|
|
494
553
|
console.log(' ✓ Logged in successfully');
|
|
495
554
|
return data.token;
|
|
496
555
|
}
|
|
@@ -528,11 +587,21 @@ export async function ensureAuthenticated() {
|
|
|
528
587
|
* Returns { orgId, courseId, orgName } or prompts the user.
|
|
529
588
|
*/
|
|
530
589
|
async function resolveOrgAndCourse(slug, token) {
|
|
531
|
-
//
|
|
532
|
-
//
|
|
590
|
+
// Shared binding (committed): cloudId in .coursecoderc.json.
|
|
591
|
+
// Local binding (per-user): orgId/courseId in .coursecode/project.json.
|
|
592
|
+
// This keeps login global while allowing per-course auth context.
|
|
533
593
|
const rcConfig = readRcConfig();
|
|
534
|
-
|
|
535
|
-
|
|
594
|
+
const projectConfig = readProjectConfig();
|
|
595
|
+
const rcCloudId = rcConfig?.cloudId;
|
|
596
|
+
const localOrgId = projectConfig?.orgId;
|
|
597
|
+
const localCourseId = projectConfig?.courseId;
|
|
598
|
+
|
|
599
|
+
if (rcCloudId && localOrgId) {
|
|
600
|
+
return { orgId: localOrgId, courseId: rcCloudId };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (localOrgId && localCourseId) {
|
|
604
|
+
return { orgId: localOrgId, courseId: localCourseId };
|
|
536
605
|
}
|
|
537
606
|
|
|
538
607
|
// Call resolve endpoint
|
|
@@ -541,7 +610,8 @@ async function resolveOrgAndCourse(slug, token) {
|
|
|
541
610
|
|
|
542
611
|
// Found in exactly one org
|
|
543
612
|
if (data.found) {
|
|
544
|
-
|
|
613
|
+
writeProjectConfig({ slug, orgId: data.orgId, courseId: data.courseId });
|
|
614
|
+
writeRcConfig({ cloudId: data.courseId, orgId: data.orgId });
|
|
545
615
|
return { orgId: data.orgId, courseId: data.courseId, orgName: data.orgName };
|
|
546
616
|
}
|
|
547
617
|
|
|
@@ -558,7 +628,8 @@ async function resolveOrgAndCourse(slug, token) {
|
|
|
558
628
|
process.exit(1);
|
|
559
629
|
}
|
|
560
630
|
const match = data.matches[idx];
|
|
561
|
-
|
|
631
|
+
writeProjectConfig({ slug, orgId: match.orgId, courseId: match.courseId });
|
|
632
|
+
writeRcConfig({ cloudId: match.courseId, orgId: match.orgId });
|
|
562
633
|
return { orgId: match.orgId, courseId: match.courseId, orgName: match.orgName };
|
|
563
634
|
}
|
|
564
635
|
|
|
@@ -638,7 +709,7 @@ export async function login(options = {}) {
|
|
|
638
709
|
}
|
|
639
710
|
|
|
640
711
|
/**
|
|
641
|
-
* coursecode logout — delete credentials
|
|
712
|
+
* coursecode logout — delete Cloud credentials
|
|
642
713
|
*/
|
|
643
714
|
export async function logout(options = {}) {
|
|
644
715
|
deleteCredentials();
|
|
@@ -817,8 +888,9 @@ export async function deploy(options = {}) {
|
|
|
817
888
|
|
|
818
889
|
const result = await makeRequest();
|
|
819
890
|
|
|
820
|
-
// Step 6:
|
|
891
|
+
// Step 6: Persist per-user binding and stamp cloud identity into .coursecoderc.json
|
|
821
892
|
const finalCourseId = result.courseId || courseId;
|
|
893
|
+
writeProjectConfig({ slug, orgId: result.orgId || orgId, courseId: finalCourseId });
|
|
822
894
|
writeRcConfig({
|
|
823
895
|
cloudId: finalCourseId,
|
|
824
896
|
orgId: result.orgId || orgId,
|
package/lib/convert.js
CHANGED
|
@@ -18,7 +18,8 @@ export async function convert(source, options) {
|
|
|
18
18
|
format = 'all',
|
|
19
19
|
dryRun = false,
|
|
20
20
|
overwrite = false,
|
|
21
|
-
flatten = false
|
|
21
|
+
flatten = false,
|
|
22
|
+
pdfJson = false
|
|
22
23
|
} = options;
|
|
23
24
|
|
|
24
25
|
console.log('\n📄 Converting documents to markdown...\n');
|
|
@@ -111,8 +112,8 @@ export async function convert(source, options) {
|
|
|
111
112
|
// Write output
|
|
112
113
|
await fs.writeFile(outputFile, result.markdown, 'utf-8');
|
|
113
114
|
|
|
114
|
-
//
|
|
115
|
-
if (result.data) {
|
|
115
|
+
// PDF structure sidecar is optional because it is much larger/token-heavier than markdown.
|
|
116
|
+
if (result.data && pdfJson) {
|
|
116
117
|
const jsonPath = outputFile.replace(/\.md$/, '.json');
|
|
117
118
|
await fs.writeFile(jsonPath, JSON.stringify(result.data, null, 2), 'utf-8');
|
|
118
119
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Enforces real LMS behavior — lifecycle violations return 'false' with proper
|
|
10
10
|
* error codes, read-only elements reject SetValue, and format-specific rules
|
|
11
11
|
* are enforced. This catches bugs that silently pass in dev but explode in
|
|
12
|
-
* production LMSs like
|
|
12
|
+
* production SCORM/cmi5/LTI environments and LMSs like Moodle or Cornerstone.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const isBrowser = typeof window !== 'undefined';
|