coursecode 0.1.54 → 0.1.55

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
@@ -235,7 +235,8 @@ coursecode preview --export
235
235
 
236
236
  | Command | Description |
237
237
  |---------|-------------|
238
- | `coursecode create <name>` | Create a new course project |
238
+ | `coursecode create <name>` | Create a new course project; use `.` to initialize the current directory |
239
+ | `coursecode init [name]` | Initialize the current directory, optionally with a course title |
239
240
  | `coursecode preview` | Preview your course locally |
240
241
  | `coursecode convert` | Convert PDFs, Word, PowerPoint to markdown |
241
242
  | `coursecode mcp` | Start the MCP server for AI integration |
package/bin/cli.js CHANGED
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * Commands:
7
7
  * coursecode create <name> - Create a new course project
8
+ * coursecode init [name] - Initialize the current directory
8
9
  * coursecode dev - Start development server
9
10
  * coursecode build - Build course package
10
11
  * coursecode upgrade - Upgrade framework in current project
@@ -79,7 +80,7 @@ program
79
80
  // Create command
80
81
  program
81
82
  .command('create <name>')
82
- .description('Create a new course project')
83
+ .description('Create a new course project (use "." for the current directory)')
83
84
  .option('--blank', 'Create without example slides (clean starter)')
84
85
  .option('--no-install', 'Skip npm install')
85
86
  .option('--start', 'Auto-start dev server after creation (skip prompt)')
@@ -88,6 +89,17 @@ program
88
89
  await create(name, options);
89
90
  });
90
91
 
92
+ program
93
+ .command('init [name]')
94
+ .description('Initialize the current directory as a CourseCode project')
95
+ .option('--blank', 'Create without example content (clean starter)')
96
+ .option('--no-install', 'Skip npm install')
97
+ .option('--start', 'Auto-start dev server after creation (skip prompt)')
98
+ .action(async (name, options) => {
99
+ const { create } = await import('../lib/create.js');
100
+ await create(name || path.basename(process.cwd()), { ...options, currentDirectory: true });
101
+ });
102
+
91
103
  // Dev command
92
104
  program
93
105
  .command('dev')
@@ -16,6 +16,8 @@
16
16
  |---------|-------------|
17
17
  | `coursecode create <name>` | Create a new course project (includes example slides) |
18
18
  | `coursecode create <name> --blank` | Create a blank project (no example content) |
19
+ | `coursecode create .` | Initialize the current directory using its folder name as the course title |
20
+ | `coursecode init [name]` | Initialize the current directory, optionally with a specific course title |
19
21
  | `coursecode clean` | Remove example files and reset config to minimal starter |
20
22
  | `coursecode new slide <id>` | Create a new slide file in `course/slides/` |
21
23
  | `coursecode new assessment <id>` | Create a new assessment file in `course/slides/` |
@@ -253,7 +255,7 @@ coursecode export-content --format json -o content.json
253
255
  3. **Create slide** in `course/slides/intro.js`:
254
256
  ```javascript
255
257
  export const slide = {
256
- render(root, context) {
258
+ render(_root, _context) {
257
259
  const container = document.createElement('div');
258
260
  container.innerHTML = `<h1>Welcome</h1><p>Content here</p>`;
259
261
  return container; // Must return a DOM element
@@ -261,6 +263,8 @@ coursecode export-content --format json -o content.json
261
263
  };
262
264
  ```
263
265
 
266
+ The first render argument is reserved and currently receives `null`. Keep it as `_root` for API compatibility. Create and return a new root `HTMLElement`; use the second argument only when the slide needs navigation or assessment context.
267
+
264
268
  > **⚠️ No import statements.** Components, interactions, CSS classes, and icons are all globally available at runtime. The only valid `import` is for local assets (images, SVGs). Access framework APIs via `const { createXxxQuestion } = CourseCode;` (destructure from global, **not** an import).
265
269
 
266
270
  4. **Add styles** to `course/theme.css` (only for custom branding - use framework utility classes first, see CSS Quick Reference section below)
@@ -310,7 +310,7 @@ Separates data, presentation, logic (used in `app/`, `navigation/`):
310
310
 
311
311
  - **Single ViewManager** in `main.js` controls slide navigation
312
312
  - **No caching** - views render fresh each `showView()` to prevent stale data
313
- - Slide signature: `render(root, context)` where `root` is framework-provided container
313
+ - Slide signature: `render(_root, context)`. The first argument is reserved and currently `null`; slides create and return their own root `HTMLElement`. The second argument carries navigation/render context.
314
314
  - Components with sub-views (assessments) create own ViewManager
315
315
 
316
316
  ### Event Delegation
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.1.54",
2
+ "version": "0.1.55",
3
3
  "name": "CourseCode Framework",
4
4
  "description": "Multi-format course authoring and learner runtime framework",
5
- "released": "2026-07-10",
5
+ "released": "2026-07-11",
6
6
  "formats": [
7
7
  "SCORM 2004 4th Edition",
8
8
  "SCORM 1.2",
package/lib/create.js CHANGED
@@ -77,6 +77,42 @@ function copyDir(src, dest, options = {}) {
77
77
  }
78
78
  }
79
79
 
80
+ function mergeProjectControlFile(sourcePath, destinationPath) {
81
+ const source = fs.readFileSync(sourcePath, 'utf-8').trimEnd();
82
+ if (!fs.existsSync(destinationPath)) {
83
+ fs.writeFileSync(destinationPath, `${source}\n`, 'utf-8');
84
+ return;
85
+ }
86
+
87
+ const current = fs.readFileSync(destinationPath, 'utf-8');
88
+ const existingLines = new Set(current.split(/\r?\n/).map(line => line.trim()));
89
+ const missingRules = source
90
+ .split(/\r?\n/)
91
+ .filter(line => line.trim() && !line.trim().startsWith('#') && !existingLines.has(line.trim()));
92
+
93
+ if (missingRules.length === 0) return;
94
+
95
+ const separator = current.endsWith('\n') ? '\n' : '\n\n';
96
+ fs.appendFileSync(
97
+ destinationPath,
98
+ `${separator}# CourseCode project defaults\n${missingRules.join('\n')}\n`,
99
+ 'utf-8'
100
+ );
101
+ }
102
+
103
+ function assertCurrentDirectoryCanBeInitialized(targetDir, templateDir) {
104
+ const managedPaths = fs.readdirSync(templateDir)
105
+ .filter(name => !['.DS_Store', 'gitignore', 'gitattributes'].includes(name))
106
+ .concat(['framework', 'schemas', 'lib', '.coursecoderc.json']);
107
+ const conflicts = managedPaths.filter(name => fs.existsSync(path.join(targetDir, name)));
108
+
109
+ if (conflicts.length > 0) {
110
+ throw new Error(
111
+ `Cannot initialize the current directory because CourseCode-managed paths already exist: ${conflicts.join(', ')}`
112
+ );
113
+ }
114
+ }
115
+
80
116
  /**
81
117
  * Run npm install in directory
82
118
  */
@@ -123,7 +159,10 @@ function gitInit(cwd) {
123
159
  }
124
160
 
125
161
  export async function create(name, options = {}) {
126
- const displayName = String(name || '').trim();
162
+ const requestedName = String(name || '').trim();
163
+ const currentDirectory = options.currentDirectory === true || requestedName === '.';
164
+ const inferredName = path.basename(process.cwd()).replace(/[-_]+/g, ' ');
165
+ const displayName = currentDirectory && requestedName === '.' ? inferredName : requestedName;
127
166
  const directoryName = toProjectDirectoryName(displayName);
128
167
 
129
168
  if (!displayName) {
@@ -136,16 +175,18 @@ export async function create(name, options = {}) {
136
175
  process.exit(1);
137
176
  }
138
177
 
139
- const targetDir = path.resolve(process.cwd(), directoryName);
178
+ const targetDir = currentDirectory ? path.resolve(process.cwd()) : path.resolve(process.cwd(), directoryName);
179
+ const templateDir = path.join(PACKAGE_ROOT, 'template');
140
180
 
141
- // Check if directory already exists
142
- if (fs.existsSync(targetDir)) {
181
+ if (currentDirectory) {
182
+ assertCurrentDirectoryCanBeInitialized(targetDir, templateDir);
183
+ } else if (fs.existsSync(targetDir)) {
143
184
  console.error(`\n❌ Directory "${directoryName}" already exists.\n`);
144
185
  process.exit(1);
145
186
  }
146
187
 
147
188
  console.log(`\n🚀 Creating CourseCode project: ${displayName}\n`);
148
- if (directoryName !== displayName) {
189
+ if (!currentDirectory && directoryName !== displayName) {
149
190
  console.log(` Project folder: ${directoryName}`);
150
191
  }
151
192
 
@@ -153,11 +194,12 @@ export async function create(name, options = {}) {
153
194
  fs.mkdirSync(targetDir, { recursive: true });
154
195
 
155
196
  // Copy template files (course directory and vite config)
156
- const templateDir = path.join(PACKAGE_ROOT, 'template');
157
197
  console.log(' Copying template files...');
158
198
  copyDir(templateDir, targetDir, {
159
- exclude: [/^\.DS_Store$/, /^node_modules$/]
199
+ exclude: [/^\.DS_Store$/, /^node_modules$/, /^gitignore$/, /^gitattributes$/]
160
200
  });
201
+ mergeProjectControlFile(path.join(templateDir, 'gitignore'), path.join(targetDir, '.gitignore'));
202
+ mergeProjectControlFile(path.join(templateDir, 'gitattributes'), path.join(targetDir, '.gitattributes'));
161
203
 
162
204
  // Copy framework
163
205
  const frameworkSrc = path.join(PACKAGE_ROOT, 'framework');
@@ -189,14 +231,19 @@ export async function create(name, options = {}) {
189
231
  console.log(' Copying proxy packaging templates...');
190
232
  copyDir(proxyTemplatesSrc, proxyTemplatesDest);
191
233
 
192
- // Read and customize package.json
234
+ // Read and customize package.json. Pin the authoring/build CLI to the
235
+ // version that created the project so an isolated checkout can build.
236
+ const frameworkPkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
193
237
  const pkgPath = path.join(targetDir, 'package.json');
194
238
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
195
239
  pkg.name = directoryName;
240
+ pkg.devDependencies = {
241
+ ...pkg.devDependencies,
242
+ coursecode: `^${frameworkPkg.version}`
243
+ };
196
244
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
197
245
 
198
246
  // Create .coursecoderc.json to track framework version
199
- const frameworkPkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
200
247
  const coursecoderc = {
201
248
  frameworkVersion: frameworkPkg.version,
202
249
  createdAt: new Date().toISOString(),
@@ -212,7 +259,7 @@ export async function create(name, options = {}) {
212
259
  // If --blank, remove example files and reset config
213
260
  if (options.blank) {
214
261
  const { clean } = await import('./scaffold.js');
215
- clean({ basePath: targetDir });
262
+ clean({ basePath: targetDir, blank: true });
216
263
  }
217
264
 
218
265
  writeCourseTitle(targetDir, displayName);
@@ -229,12 +276,16 @@ export async function create(name, options = {}) {
229
276
  }
230
277
 
231
278
  // Initialize git repository
232
- console.log('\n Initializing git repository...');
233
- try {
234
- await gitInit(targetDir);
235
- console.log(' Git repository initialized');
236
- } catch (_error) {
237
- console.warn(' ⚠️ Git init failed. You can run "git init" manually.');
279
+ if (fs.existsSync(path.join(targetDir, '.git'))) {
280
+ console.log('\n ✅ Existing Git repository preserved');
281
+ } else {
282
+ console.log('\n Initializing git repository...');
283
+ try {
284
+ await gitInit(targetDir);
285
+ console.log(' ✅ Git repository initialized');
286
+ } catch (_error) {
287
+ console.warn(' ⚠️ Git init failed. You can run "git init" manually.');
288
+ }
238
289
  }
239
290
 
240
291
  // Print success message
@@ -287,12 +338,14 @@ export async function create(name, options = {}) {
287
338
  console.log(` cd ${directoryName} && coursecode dev\n`);
288
339
  });
289
340
  } else {
290
- console.log(`\n To start developing:\n\n cd ${directoryName}\n coursecode dev\n`);
341
+ const changeDirectory = currentDirectory ? '' : `cd ${directoryName}\n `;
342
+ console.log(`\n To start developing:\n\n ${changeDirectory}coursecode dev\n`);
291
343
  }
292
344
 
293
345
  return {
294
346
  displayName,
295
347
  directoryName,
296
- targetDir
348
+ targetDir,
349
+ currentDirectory
297
350
  };
298
351
  }
package/lib/scaffold.js CHANGED
@@ -34,13 +34,24 @@ const MINIMAL_CONFIG = `export const courseConfig = {
34
34
  };
35
35
  `;
36
36
 
37
+ const DEMO_ASSETS = [
38
+ 'docs/example_md_1.md',
39
+ 'docs/example_md_2.md',
40
+ 'docs/example_pdf_1_thumbnail.png',
41
+ 'docs/example_pdf_2.pdf',
42
+ 'images/course-architecture.svg',
43
+ 'images/logo.svg',
44
+ 'widgets/counter-demo.html',
45
+ 'widgets/gravity-painter.html'
46
+ ];
47
+
37
48
  /**
38
49
  * Minimal slide template
39
50
  */
40
51
  function slideTemplate(id) {
41
52
  const title = id.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
42
53
  return `export const slide = {
43
- render(root, context) {
54
+ render(_root, _context) {
44
55
  const container = document.createElement('div');
45
56
  container.innerHTML = \`
46
57
  <h1>${title}</h1>
@@ -83,9 +94,11 @@ const questions = [
83
94
  ];
84
95
 
85
96
  export const slide = {
86
- render(root) {
87
- const assessment = AssessmentManager.createAssessment(config, questions);
88
- return assessment.render(root);
97
+ render(_root, context = {}) {
98
+ const container = document.createElement('div');
99
+ const assessment = AssessmentManager.createAssessment({ ...config, questions });
100
+ assessment.render(container, context);
101
+ return container;
89
102
  }
90
103
  };
91
104
  `;
@@ -128,6 +141,22 @@ export function clean(options = {}) {
128
141
  }
129
142
  }
130
143
 
144
+ // Remove the known demo documents, images, and widgets without touching
145
+ // any author-created assets that may already exist in the project.
146
+ const assetsDir = path.join(coursePath, 'assets');
147
+ for (const relativePath of DEMO_ASSETS) {
148
+ const assetPath = path.join(assetsDir, relativePath);
149
+ if (fs.existsSync(assetPath)) {
150
+ fs.rmSync(assetPath, { force: true });
151
+ removed++;
152
+ }
153
+ }
154
+
155
+ // A freshly blank scaffold must not inherit the example narration cache.
156
+ if (options.blank) {
157
+ fs.rmSync(path.join(basePath, '.narration-cache.json'), { force: true });
158
+ }
159
+
131
160
  // Rewrite course-config.js to minimal starter
132
161
  const configPath = path.join(coursePath, 'course-config.js');
133
162
  if (fs.existsSync(configPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursecode",
3
- "version": "0.1.54",
3
+ "version": "0.1.55",
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": {
@@ -14,7 +14,6 @@
14
14
  "!framework/dist/**",
15
15
  "schemas",
16
16
  "template",
17
- "!template/.gitattributes",
18
17
  "!template/**/.gitkeep",
19
18
  "README.md",
20
19
  "THIRD_PARTY_NOTICES.md"
@@ -51,6 +50,7 @@
51
50
  "lint:responsive:structure": "node scripts/check-responsive-structure-scoping.mjs",
52
51
  "lint:fix": "eslint . --fix",
53
52
  "smoke:responsive": "node scripts/responsive-visual-smoke.mjs",
53
+ "smoke:packed-create": "node scripts/smoke-packed-create.mjs",
54
54
  "build": "npx vite build --config vite.framework-dev.config.js",
55
55
  "build:dev": "npx vite build --config vite.framework-dev.config.js",
56
56
  "build:scorm2004": "LMS_FORMAT=scorm2004 npx vite build --config vite.framework-dev.config.js",
@@ -69,7 +69,7 @@
69
69
  "test:cloud": "vitest run --config tests/vitest.cloud.config.js",
70
70
  "test:watch": "vitest --config tests/vitest.config.js",
71
71
  "test:coverage": "vitest run --config tests/vitest.config.js --coverage",
72
- "prerelease:check": "npm run lint && npm run lint:responsive && npm run lint:responsive:structure && npm run test:coverage && npm run build",
72
+ "prerelease:check": "npm run lint && npm run lint:responsive && npm run lint:responsive:structure && npm run test:coverage && npm run build && npm run smoke:packed-create",
73
73
  "prerelease:check:full": "npm run prerelease:check && npm run test:e2e && npm run test:drivers && npm run smoke:responsive -- --profile=expanded",
74
74
  "prepublishOnly": "npm run prerelease:check"
75
75
  },
@@ -0,0 +1,44 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
3
+
4
+ # Force LF line endings for text files
5
+ *.js text eol=lf
6
+ *.mjs text eol=lf
7
+ *.cjs text eol=lf
8
+ *.json text eol=lf
9
+ *.css text eol=lf
10
+ *.html text eol=lf
11
+ *.htm text eol=lf
12
+ *.xml text eol=lf
13
+ *.xsd text eol=lf
14
+ *.dtd text eol=lf
15
+ *.md text eol=lf
16
+ *.txt text eol=lf
17
+ *.yml text eol=lf
18
+ *.yaml text eol=lf
19
+ *.env text eol=lf
20
+
21
+ # Binary files
22
+ *.png binary
23
+ *.jpg binary
24
+ *.jpeg binary
25
+ *.gif binary
26
+ *.ico binary
27
+ *.webp binary
28
+ *.svg binary
29
+ *.mp3 binary
30
+ *.mp4 binary
31
+ *.wav binary
32
+ *.ogg binary
33
+ *.webm binary
34
+ *.woff binary
35
+ *.woff2 binary
36
+ *.ttf binary
37
+ *.eot binary
38
+ *.otf binary
39
+ *.pdf binary
40
+ *.zip binary
41
+
42
+ # Linguist overrides for GitHub stats
43
+ *.css linguist-detectable=false
44
+ *.xsd linguist-detectable=false
@@ -0,0 +1,38 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+ *.zip
7
+
8
+ # Preview export (comment this line to include in repo for GitHub Pages deployment)
9
+ course-preview/
10
+
11
+ # Environment
12
+ .env
13
+ .env.local
14
+
15
+ # Google Cloud service account credentials (NEVER commit these!)
16
+ **/service-account*.json
17
+ **/*-credentials.json
18
+ **/*-keyfile.json
19
+
20
+ # System files
21
+ .DS_Store
22
+ Thumbs.db
23
+
24
+ # IDE
25
+ .idea/
26
+ .vscode/
27
+ *.swp
28
+ *.swo
29
+
30
+ # CourseCode Cloud (project binding)
31
+ .coursecode/
32
+
33
+ # Narration cache
34
+ .narration-cache.json
35
+
36
+ # Logs
37
+ *.log
38
+ npm-debug.log*
@@ -16,6 +16,13 @@ const ROOT_DIR = path.resolve(__dirname);
16
16
  const DIST_DIR = path.join(ROOT_DIR, 'dist');
17
17
  const SUPPORTED_BROWSER_TARGETS = ['chrome111', 'edge111', 'firefox114', 'safari16.4'];
18
18
 
19
+ function directoryContainsFiles(directory) {
20
+ if (!fs.existsSync(directory)) return false;
21
+ return fs.readdirSync(directory, { withFileTypes: true }).some(entry => (
22
+ entry.isFile() || (entry.isDirectory() && directoryContainsFiles(path.join(directory, entry.name)))
23
+ ));
24
+ }
25
+
19
26
  async function loadBuildUtils() {
20
27
  if (
21
28
  generateManifest &&
@@ -382,7 +389,10 @@ export default defineConfig(async ({ mode }) => {
382
389
  { src: 'schemas/common/*', dest: 'common', rename: { stripBase: 2 } },
383
390
  // Publish runtime assets only. Never ship authoring references,
384
391
  // configuration source, assessment source, or hidden project files.
385
- { src: 'course/assets', dest: 'course', rename: { stripBase: 1 } },
392
+ // A blank course is valid and may not contain any assets yet.
393
+ ...(directoryContainsFiles(path.join(ROOT_DIR, 'course', 'assets'))
394
+ ? [{ src: 'course/assets', dest: 'course', rename: { stripBase: 1 } }]
395
+ : []),
386
396
  { src: 'framework/js/vendor/**/*', dest: 'js/vendor', rename: { stripBase: 3 } }
387
397
  ],
388
398
  watch: isDev ? { rerun: true } : undefined