offcourse 0.0.2 → 1.0.0

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.
Files changed (284) hide show
  1. package/.github/workflows/ci.yml +50 -0
  2. package/.husky/commit-msg +2 -0
  3. package/.husky/pre-commit +1 -0
  4. package/.husky/pre-push +3 -0
  5. package/.prettierrc +8 -0
  6. package/.release-it.json +23 -0
  7. package/ARCHITECTURE.md +233 -0
  8. package/CHANGELOG.md +78 -0
  9. package/README.md +255 -20
  10. package/commitlint.config.js +4 -0
  11. package/dist/ai/openRouter.d.ts +47 -0
  12. package/dist/ai/openRouter.d.ts.map +1 -0
  13. package/dist/ai/openRouter.js +116 -0
  14. package/dist/ai/openRouter.js.map +1 -0
  15. package/dist/ai/transcriptPolisher.d.ts +24 -0
  16. package/dist/ai/transcriptPolisher.d.ts.map +1 -0
  17. package/dist/ai/transcriptPolisher.js +89 -0
  18. package/dist/ai/transcriptPolisher.js.map +1 -0
  19. package/dist/cli/commands/config.d.ts +13 -0
  20. package/dist/cli/commands/config.d.ts.map +1 -0
  21. package/dist/cli/commands/config.js +66 -0
  22. package/dist/cli/commands/config.js.map +1 -0
  23. package/dist/cli/commands/enrich.d.ts +14 -0
  24. package/dist/cli/commands/enrich.d.ts.map +1 -0
  25. package/dist/cli/commands/enrich.js +271 -0
  26. package/dist/cli/commands/enrich.js.map +1 -0
  27. package/dist/cli/commands/inspect.d.ts +11 -0
  28. package/dist/cli/commands/inspect.d.ts.map +1 -0
  29. package/dist/cli/commands/inspect.js +365 -0
  30. package/dist/cli/commands/inspect.js.map +1 -0
  31. package/dist/cli/commands/login.d.ts +12 -0
  32. package/dist/cli/commands/login.d.ts.map +1 -0
  33. package/dist/cli/commands/login.js +55 -0
  34. package/dist/cli/commands/login.js.map +1 -0
  35. package/dist/cli/commands/status.d.ts +15 -0
  36. package/dist/cli/commands/status.d.ts.map +1 -0
  37. package/dist/cli/commands/status.js +118 -0
  38. package/dist/cli/commands/status.js.map +1 -0
  39. package/dist/cli/commands/sync.d.ts +16 -0
  40. package/dist/cli/commands/sync.d.ts.map +1 -0
  41. package/dist/cli/commands/sync.js +922 -0
  42. package/dist/cli/commands/sync.js.map +1 -0
  43. package/dist/cli/commands/syncGhl.d.ts +20 -0
  44. package/dist/cli/commands/syncGhl.d.ts.map +1 -0
  45. package/dist/cli/commands/syncGhl.js +483 -0
  46. package/dist/cli/commands/syncGhl.js.map +1 -0
  47. package/dist/cli/commands/syncHighLevel.d.ts +24 -0
  48. package/dist/cli/commands/syncHighLevel.d.ts.map +1 -0
  49. package/dist/cli/commands/syncHighLevel.js +483 -0
  50. package/dist/cli/commands/syncHighLevel.js.map +1 -0
  51. package/dist/cli/commands/syncHighLevel.test.d.ts +2 -0
  52. package/dist/cli/commands/syncHighLevel.test.d.ts.map +1 -0
  53. package/dist/cli/commands/syncHighLevel.test.js +102 -0
  54. package/dist/cli/commands/syncHighLevel.test.js.map +1 -0
  55. package/dist/cli/index.d.ts +3 -0
  56. package/dist/cli/index.d.ts.map +1 -0
  57. package/dist/cli/index.js +106 -0
  58. package/dist/cli/index.js.map +1 -0
  59. package/dist/config/configManager.d.ts +31 -0
  60. package/dist/config/configManager.d.ts.map +1 -0
  61. package/dist/config/configManager.js +64 -0
  62. package/dist/config/configManager.js.map +1 -0
  63. package/dist/config/paths.d.ts +21 -0
  64. package/dist/config/paths.d.ts.map +1 -0
  65. package/dist/config/paths.js +33 -0
  66. package/dist/config/paths.js.map +1 -0
  67. package/dist/config/paths.test.d.ts +2 -0
  68. package/dist/config/paths.test.d.ts.map +1 -0
  69. package/dist/config/paths.test.js +70 -0
  70. package/dist/config/paths.test.js.map +1 -0
  71. package/dist/config/schema.d.ts +60 -0
  72. package/dist/config/schema.d.ts.map +1 -0
  73. package/dist/config/schema.js +50 -0
  74. package/dist/config/schema.js.map +1 -0
  75. package/dist/config/schema.test.d.ts +2 -0
  76. package/dist/config/schema.test.d.ts.map +1 -0
  77. package/dist/config/schema.test.js +151 -0
  78. package/dist/config/schema.test.js.map +1 -0
  79. package/dist/downloader/hlsDownloader.d.ts +58 -0
  80. package/dist/downloader/hlsDownloader.d.ts.map +1 -0
  81. package/dist/downloader/hlsDownloader.js +254 -0
  82. package/dist/downloader/hlsDownloader.js.map +1 -0
  83. package/dist/downloader/hlsDownloader.test.d.ts +2 -0
  84. package/dist/downloader/hlsDownloader.test.d.ts.map +1 -0
  85. package/dist/downloader/hlsDownloader.test.js +116 -0
  86. package/dist/downloader/hlsDownloader.test.js.map +1 -0
  87. package/dist/downloader/hlsValidator.d.ts +35 -0
  88. package/dist/downloader/hlsValidator.d.ts.map +1 -0
  89. package/dist/downloader/hlsValidator.js +148 -0
  90. package/dist/downloader/hlsValidator.js.map +1 -0
  91. package/dist/downloader/index.d.ts +26 -0
  92. package/dist/downloader/index.d.ts.map +1 -0
  93. package/dist/downloader/index.js +52 -0
  94. package/dist/downloader/index.js.map +1 -0
  95. package/dist/downloader/loomDownloader.d.ts +56 -0
  96. package/dist/downloader/loomDownloader.d.ts.map +1 -0
  97. package/dist/downloader/loomDownloader.js +559 -0
  98. package/dist/downloader/loomDownloader.js.map +1 -0
  99. package/dist/downloader/loomDownloader.test.d.ts +2 -0
  100. package/dist/downloader/loomDownloader.test.d.ts.map +1 -0
  101. package/dist/downloader/loomDownloader.test.js +36 -0
  102. package/dist/downloader/loomDownloader.test.js.map +1 -0
  103. package/dist/downloader/queue.d.ts +56 -0
  104. package/dist/downloader/queue.d.ts.map +1 -0
  105. package/dist/downloader/queue.js +88 -0
  106. package/dist/downloader/queue.js.map +1 -0
  107. package/dist/downloader/queue.test.d.ts +2 -0
  108. package/dist/downloader/queue.test.d.ts.map +1 -0
  109. package/dist/downloader/queue.test.js +158 -0
  110. package/dist/downloader/queue.test.js.map +1 -0
  111. package/dist/downloader/videoDownloader.d.ts +32 -0
  112. package/dist/downloader/videoDownloader.d.ts.map +1 -0
  113. package/dist/downloader/videoDownloader.js +173 -0
  114. package/dist/downloader/videoDownloader.js.map +1 -0
  115. package/dist/downloader/vimeoDownloader.d.ts +52 -0
  116. package/dist/downloader/vimeoDownloader.d.ts.map +1 -0
  117. package/dist/downloader/vimeoDownloader.js +565 -0
  118. package/dist/downloader/vimeoDownloader.js.map +1 -0
  119. package/dist/downloader/vimeoDownloader.test.d.ts +2 -0
  120. package/dist/downloader/vimeoDownloader.test.d.ts.map +1 -0
  121. package/dist/downloader/vimeoDownloader.test.js +51 -0
  122. package/dist/downloader/vimeoDownloader.test.js.map +1 -0
  123. package/dist/scraper/auth.d.ts +29 -0
  124. package/dist/scraper/auth.d.ts.map +1 -0
  125. package/dist/scraper/auth.js +115 -0
  126. package/dist/scraper/auth.js.map +1 -0
  127. package/dist/scraper/extractor.d.ts +49 -0
  128. package/dist/scraper/extractor.d.ts.map +1 -0
  129. package/dist/scraper/extractor.js +627 -0
  130. package/dist/scraper/extractor.js.map +1 -0
  131. package/dist/scraper/extractor.test.d.ts +2 -0
  132. package/dist/scraper/extractor.test.d.ts.map +1 -0
  133. package/dist/scraper/extractor.test.js +65 -0
  134. package/dist/scraper/extractor.test.js.map +1 -0
  135. package/dist/scraper/ghl/auth.d.ts +25 -0
  136. package/dist/scraper/ghl/auth.d.ts.map +1 -0
  137. package/dist/scraper/ghl/auth.js +187 -0
  138. package/dist/scraper/ghl/auth.js.map +1 -0
  139. package/dist/scraper/ghl/extractor.d.ts +96 -0
  140. package/dist/scraper/ghl/extractor.d.ts.map +1 -0
  141. package/dist/scraper/ghl/extractor.js +345 -0
  142. package/dist/scraper/ghl/extractor.js.map +1 -0
  143. package/dist/scraper/ghl/index.d.ts +4 -0
  144. package/dist/scraper/ghl/index.d.ts.map +1 -0
  145. package/dist/scraper/ghl/index.js +4 -0
  146. package/dist/scraper/ghl/index.js.map +1 -0
  147. package/dist/scraper/ghl/navigator.d.ts +93 -0
  148. package/dist/scraper/ghl/navigator.d.ts.map +1 -0
  149. package/dist/scraper/ghl/navigator.js +447 -0
  150. package/dist/scraper/ghl/navigator.js.map +1 -0
  151. package/dist/scraper/highlevel/auth.d.ts +25 -0
  152. package/dist/scraper/highlevel/auth.d.ts.map +1 -0
  153. package/dist/scraper/highlevel/auth.js +189 -0
  154. package/dist/scraper/highlevel/auth.js.map +1 -0
  155. package/dist/scraper/highlevel/extractor.d.ts +97 -0
  156. package/dist/scraper/highlevel/extractor.d.ts.map +1 -0
  157. package/dist/scraper/highlevel/extractor.js +386 -0
  158. package/dist/scraper/highlevel/extractor.js.map +1 -0
  159. package/dist/scraper/highlevel/extractor.test.d.ts +2 -0
  160. package/dist/scraper/highlevel/extractor.test.d.ts.map +1 -0
  161. package/dist/scraper/highlevel/extractor.test.js +101 -0
  162. package/dist/scraper/highlevel/extractor.test.js.map +1 -0
  163. package/dist/scraper/highlevel/index.d.ts +3 -0
  164. package/dist/scraper/highlevel/index.d.ts.map +1 -0
  165. package/dist/scraper/highlevel/index.js +3 -0
  166. package/dist/scraper/highlevel/index.js.map +1 -0
  167. package/dist/scraper/highlevel/navigator.d.ts +93 -0
  168. package/dist/scraper/highlevel/navigator.d.ts.map +1 -0
  169. package/dist/scraper/highlevel/navigator.js +492 -0
  170. package/dist/scraper/highlevel/navigator.js.map +1 -0
  171. package/dist/scraper/highlevel/navigator.test.d.ts +2 -0
  172. package/dist/scraper/highlevel/navigator.test.d.ts.map +1 -0
  173. package/dist/scraper/highlevel/navigator.test.js +78 -0
  174. package/dist/scraper/highlevel/navigator.test.js.map +1 -0
  175. package/dist/scraper/navigator.d.ts +65 -0
  176. package/dist/scraper/navigator.d.ts.map +1 -0
  177. package/dist/scraper/navigator.js +300 -0
  178. package/dist/scraper/navigator.js.map +1 -0
  179. package/dist/scraper/navigator.test.d.ts +2 -0
  180. package/dist/scraper/navigator.test.d.ts.map +1 -0
  181. package/dist/scraper/navigator.test.js +63 -0
  182. package/dist/scraper/navigator.test.js.map +1 -0
  183. package/dist/scraper/skoolApi.d.ts +17 -0
  184. package/dist/scraper/skoolApi.d.ts.map +1 -0
  185. package/dist/scraper/skoolApi.js +72 -0
  186. package/dist/scraper/skoolApi.js.map +1 -0
  187. package/dist/scraper/videoInterceptor.d.ts +19 -0
  188. package/dist/scraper/videoInterceptor.d.ts.map +1 -0
  189. package/dist/scraper/videoInterceptor.js +315 -0
  190. package/dist/scraper/videoInterceptor.js.map +1 -0
  191. package/dist/shared/auth.d.ts +58 -0
  192. package/dist/shared/auth.d.ts.map +1 -0
  193. package/dist/shared/auth.js +211 -0
  194. package/dist/shared/auth.js.map +1 -0
  195. package/dist/shared/fs.d.ts +31 -0
  196. package/dist/shared/fs.d.ts.map +1 -0
  197. package/dist/shared/fs.js +73 -0
  198. package/dist/shared/fs.js.map +1 -0
  199. package/dist/shared/http.d.ts +15 -0
  200. package/dist/shared/http.d.ts.map +1 -0
  201. package/dist/shared/http.js +31 -0
  202. package/dist/shared/http.js.map +1 -0
  203. package/dist/shared/index.d.ts +4 -0
  204. package/dist/shared/index.d.ts.map +1 -0
  205. package/dist/shared/index.js +4 -0
  206. package/dist/shared/index.js.map +1 -0
  207. package/dist/state/database.d.ts +245 -0
  208. package/dist/state/database.d.ts.map +1 -0
  209. package/dist/state/database.js +676 -0
  210. package/dist/state/database.js.map +1 -0
  211. package/dist/state/database.test.d.ts +2 -0
  212. package/dist/state/database.test.d.ts.map +1 -0
  213. package/dist/state/database.test.js +34 -0
  214. package/dist/state/database.test.js.map +1 -0
  215. package/dist/state/index.d.ts +2 -0
  216. package/dist/state/index.d.ts.map +1 -0
  217. package/dist/state/index.js +2 -0
  218. package/dist/state/index.js.map +1 -0
  219. package/dist/storage/fileSystem.d.ts +56 -0
  220. package/dist/storage/fileSystem.d.ts.map +1 -0
  221. package/dist/storage/fileSystem.js +121 -0
  222. package/dist/storage/fileSystem.js.map +1 -0
  223. package/dist/transcription/whisperService.d.ts +27 -0
  224. package/dist/transcription/whisperService.d.ts.map +1 -0
  225. package/dist/transcription/whisperService.js +102 -0
  226. package/dist/transcription/whisperService.js.map +1 -0
  227. package/eslint.config.js +55 -0
  228. package/package.json +68 -11
  229. package/src/__fixtures__/highlevel-post-response.json +68 -0
  230. package/src/__fixtures__/hls-master-playlist.m3u8 +24 -0
  231. package/src/cli/commands/__snapshots__/syncHighLevel.test.ts.snap +38 -0
  232. package/src/cli/commands/config.ts +74 -0
  233. package/src/cli/commands/inspect.ts +441 -0
  234. package/src/cli/commands/login.ts +68 -0
  235. package/src/cli/commands/status.ts +147 -0
  236. package/src/cli/commands/sync.ts +1235 -0
  237. package/src/cli/commands/syncHighLevel.test.ts +144 -0
  238. package/src/cli/commands/syncHighLevel.ts +639 -0
  239. package/src/cli/index.ts +121 -0
  240. package/src/config/configManager.ts +75 -0
  241. package/src/config/paths.test.ts +83 -0
  242. package/src/config/paths.ts +36 -0
  243. package/src/config/schema.test.ts +173 -0
  244. package/src/config/schema.ts +65 -0
  245. package/src/downloader/hlsDownloader.test.ts +148 -0
  246. package/src/downloader/hlsDownloader.ts +327 -0
  247. package/src/downloader/hlsValidator.ts +196 -0
  248. package/src/downloader/index.ts +122 -0
  249. package/src/downloader/loomDownloader.test.ts +43 -0
  250. package/src/downloader/loomDownloader.ts +742 -0
  251. package/src/downloader/queue.test.ts +199 -0
  252. package/src/downloader/queue.ts +118 -0
  253. package/src/downloader/vimeoDownloader.test.ts +62 -0
  254. package/src/downloader/vimeoDownloader.ts +722 -0
  255. package/src/scraper/extractor.test.ts +124 -0
  256. package/src/scraper/extractor.ts +757 -0
  257. package/src/scraper/highlevel/__snapshots__/extractor.test.ts.snap +41 -0
  258. package/src/scraper/highlevel/extractor.test.ts +134 -0
  259. package/src/scraper/highlevel/extractor.ts +537 -0
  260. package/src/scraper/highlevel/index.ts +2 -0
  261. package/src/scraper/highlevel/navigator.test.ts +110 -0
  262. package/src/scraper/highlevel/navigator.ts +668 -0
  263. package/src/scraper/highlevel/schemas.ts +183 -0
  264. package/src/scraper/navigator.test.ts +122 -0
  265. package/src/scraper/navigator.ts +355 -0
  266. package/src/scraper/schemas.ts +177 -0
  267. package/src/scraper/videoInterceptor.ts +435 -0
  268. package/src/shared/auth.test.ts +58 -0
  269. package/src/shared/auth.ts +251 -0
  270. package/src/shared/firebase.ts +151 -0
  271. package/src/shared/fs.ts +80 -0
  272. package/src/shared/http.ts +34 -0
  273. package/src/shared/index.ts +6 -0
  274. package/src/shared/slug.ts +26 -0
  275. package/src/shared/url.test.ts +122 -0
  276. package/src/shared/url.ts +57 -0
  277. package/src/state/database.test.ts +49 -0
  278. package/src/state/database.ts +919 -0
  279. package/src/state/index.ts +14 -0
  280. package/src/storage/fileSystem.test.ts +64 -0
  281. package/src/storage/fileSystem.ts +175 -0
  282. package/tsconfig.json +28 -0
  283. package/vitest.config.ts +29 -0
  284. package/cli.js +0 -45
@@ -0,0 +1,38 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`formatHighLevelMarkdown > snapshot: complete lesson with all parts 1`] = `
4
+ "# Complete Module: Advanced Techniques
5
+
6
+ In this comprehensive module, you'll learn advanced strategies.
7
+
8
+ ## Video
9
+
10
+ Video URL: https://cdn.example.com/videos/advanced-techniques.mp4
11
+
12
+ ---
13
+
14
+ Let's dive into the key concepts:
15
+
16
+ - Concept A
17
+ - Concept B
18
+ - Concept C
19
+ Remember to practice daily!
20
+ "
21
+ `;
22
+
23
+ exports[`formatHighLevelMarkdown > snapshot: lesson with HTML entities and special chars 1`] = `
24
+ "# Q&A Session: Common Questions
25
+
26
+ Your questions answered!
27
+
28
+ ---
29
+
30
+ Q: What's the best approach?
31
+
32
+ A: It depends on your goals & resources.
33
+
34
+
35
+
36
+ <script> tags are blocked
37
+ "
38
+ `;
@@ -0,0 +1,74 @@
1
+ import chalk from "chalk";
2
+ import { getConfigValue, loadConfig, updateConfig } from "../../config/configManager.js";
3
+ import { CONFIG_FILE } from "../../config/paths.js";
4
+ import type { Config } from "../../config/schema.js";
5
+ import { configSchema } from "../../config/schema.js";
6
+
7
+ /**
8
+ * Shows all current configuration values.
9
+ */
10
+ export function configShowCommand(): void {
11
+ const config = loadConfig();
12
+
13
+ console.log(chalk.blue("\nāš™ļø Configuration\n"));
14
+ console.log(chalk.gray(` File: ${CONFIG_FILE}\n`));
15
+
16
+ for (const [key, value] of Object.entries(config)) {
17
+ console.log(` ${chalk.cyan(key)}: ${chalk.white(String(value))}`);
18
+ }
19
+ console.log();
20
+ }
21
+
22
+ /**
23
+ * Sets a configuration value.
24
+ */
25
+ export function configSetCommand(key: string, value: string): void {
26
+ const validKeys = Object.keys(configSchema.shape) as (keyof Config)[];
27
+
28
+ if (!validKeys.includes(key as keyof Config)) {
29
+ console.log(chalk.red(`\nāŒ Unknown config key: ${key}`));
30
+ console.log(chalk.gray(` Valid keys: ${validKeys.join(", ")}\n`));
31
+ process.exit(1);
32
+ }
33
+
34
+ // Parse value based on expected type
35
+ const currentValue = getConfigValue(key as keyof Config);
36
+ let parsedValue: string | number | boolean;
37
+
38
+ if (typeof currentValue === "boolean") {
39
+ parsedValue = value === "true" || value === "1";
40
+ } else if (typeof currentValue === "number") {
41
+ parsedValue = parseInt(value, 10);
42
+ if (isNaN(parsedValue)) {
43
+ console.log(chalk.red(`\nāŒ Invalid number: ${value}\n`));
44
+ process.exit(1);
45
+ }
46
+ } else {
47
+ parsedValue = value;
48
+ }
49
+
50
+ try {
51
+ updateConfig({ [key]: parsedValue });
52
+ console.log(chalk.green(`\nāœ… Set ${key} = ${parsedValue}\n`));
53
+ } catch (error) {
54
+ console.log(chalk.red(`\nāŒ Invalid value for ${key}: ${value}`));
55
+ console.log(chalk.gray(` ${String(error)}\n`));
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Gets a specific configuration value.
62
+ */
63
+ export function configGetCommand(key: string): void {
64
+ const validKeys = Object.keys(configSchema.shape) as (keyof Config)[];
65
+
66
+ if (!validKeys.includes(key as keyof Config)) {
67
+ console.log(chalk.red(`\nāŒ Unknown config key: ${key}`));
68
+ console.log(chalk.gray(` Valid keys: ${validKeys.join(", ")}\n`));
69
+ process.exit(1);
70
+ }
71
+
72
+ const value = getConfigValue(key as keyof Config);
73
+ console.log(String(value));
74
+ }
@@ -0,0 +1,441 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { join } from "node:path";
4
+ import { loadConfig } from "../../config/configManager.js";
5
+ import { expandPath } from "../../config/paths.js";
6
+ import { getAuthenticatedSession, isSkoolLoginPage } from "../../shared/auth.js";
7
+ import { ensureDir, outputFile } from "../../shared/fs.js";
8
+
9
+ const SKOOL_DOMAIN = "www.skool.com";
10
+ const SKOOL_LOGIN_URL = "https://www.skool.com/login";
11
+
12
+ interface InspectOptions {
13
+ output?: string;
14
+ full?: boolean;
15
+ click?: boolean;
16
+ }
17
+
18
+ /**
19
+ * Inspects the page structure and logs useful debugging info.
20
+ */
21
+ export async function inspectCommand(url: string, options: InspectOptions): Promise<void> {
22
+ console.log(chalk.blue("\nšŸ” Page Inspector\n"));
23
+
24
+ const config = loadConfig();
25
+ const spinner = ora("Connecting...").start();
26
+
27
+ let browser;
28
+ let session;
29
+
30
+ try {
31
+ const result = await getAuthenticatedSession(
32
+ {
33
+ domain: SKOOL_DOMAIN,
34
+ loginUrl: SKOOL_LOGIN_URL,
35
+ isLoginPage: isSkoolLoginPage,
36
+ },
37
+ { headless: false } // Always visible for inspection
38
+ );
39
+ browser = result.browser;
40
+ session = result.session;
41
+ spinner.succeed("Connected");
42
+ } catch {
43
+ spinner.fail("Failed to connect");
44
+ console.log(chalk.red("\nāŒ Please run: course-grab login\n"));
45
+ process.exit(1);
46
+ }
47
+
48
+ try {
49
+ // Collect network requests to find video URLs
50
+ const videoRequests: { url: string; resourceType: string }[] = [];
51
+ session.page.on("request", (request) => {
52
+ const reqUrl = request.url();
53
+ const resourceType = request.resourceType();
54
+ if (
55
+ resourceType === "media" ||
56
+ reqUrl.includes(".mp4") ||
57
+ reqUrl.includes(".m3u8") ||
58
+ reqUrl.includes(".webm") ||
59
+ reqUrl.includes("vimeo") ||
60
+ reqUrl.includes("wistia") ||
61
+ reqUrl.includes("mux.com") ||
62
+ reqUrl.includes("cloudflare") ||
63
+ reqUrl.includes("stream")
64
+ ) {
65
+ videoRequests.push({ url: reqUrl, resourceType });
66
+ }
67
+ });
68
+
69
+ const pageSpinner = ora("Loading page...").start();
70
+ await session.page.goto(url, { timeout: 60000 });
71
+ // Use domcontentloaded instead of networkidle - some pages never stop loading
72
+ await session.page.waitForLoadState("domcontentloaded");
73
+ // Give it a moment for JS to render
74
+ await session.page.waitForTimeout(2000);
75
+ pageSpinner.succeed("Page loaded");
76
+
77
+ console.log(chalk.cyan("\nšŸ“„ Page Info:\n"));
78
+ console.log(` URL: ${session.page.url()}`);
79
+ console.log(` Title: ${await session.page.title()}`);
80
+
81
+ // Look for video preview/placeholder elements
82
+ const previewInfo = await session.page.evaluate(() => {
83
+ const previews: {
84
+ selector: string;
85
+ description: string;
86
+ element: string;
87
+ }[] = [];
88
+
89
+ // Common video preview patterns
90
+ const previewSelectors = [
91
+ // Play buttons
92
+ '[class*="play"]',
93
+ '[class*="Play"]',
94
+ 'button[class*="video"]',
95
+ '[aria-label*="play" i]',
96
+ '[aria-label*="Play" i]',
97
+ // Thumbnail overlays
98
+ '[class*="thumbnail"]',
99
+ '[class*="poster"]',
100
+ '[class*="preview"]',
101
+ '[class*="cover"]',
102
+ // SVG play icons
103
+ 'svg[class*="play"]',
104
+ // Clickable video containers
105
+ '[class*="video-container"]',
106
+ '[class*="player-container"]',
107
+ '[class*="video-wrapper"]',
108
+ // Data attributes
109
+ "[data-video]",
110
+ "[data-video-id]",
111
+ "[data-src]",
112
+ ];
113
+
114
+ for (const selector of previewSelectors) {
115
+ const elements = document.querySelectorAll(selector);
116
+ elements.forEach((el) => {
117
+ const rect = el.getBoundingClientRect();
118
+ // Only consider visible, reasonably sized elements
119
+ if (rect.width > 50 && rect.height > 50) {
120
+ previews.push({
121
+ selector,
122
+ description: `<${el.tagName.toLowerCase()}> class="${el.className}" (${Math.round(rect.width)}x${Math.round(rect.height)})`,
123
+ element: el.outerHTML.substring(0, 200),
124
+ });
125
+ }
126
+ });
127
+ }
128
+
129
+ return previews;
130
+ });
131
+
132
+ if (previewInfo.length > 0) {
133
+ console.log(chalk.yellow("\nšŸŽ¬ Potential Video Previews/Placeholders:\n"));
134
+ const seen = new Set<string>();
135
+ for (const preview of previewInfo) {
136
+ if (seen.has(preview.description)) continue;
137
+ seen.add(preview.description);
138
+ console.log(` ${preview.description}`);
139
+ console.log(chalk.gray(` selector: ${preview.selector}`));
140
+ console.log(chalk.gray(` html: ${preview.element.substring(0, 100)}...`));
141
+ }
142
+ }
143
+
144
+ // If --click flag, try to click on video preview
145
+ if (options.click) {
146
+ console.log(chalk.cyan("\nšŸ‘† Attempting to click video preview...\n"));
147
+
148
+ const clicked = await session.page.evaluate(() => {
149
+ // Try various selectors for play button
150
+ const playSelectors = [
151
+ '[class*="play"]',
152
+ '[class*="Play"]',
153
+ 'button[class*="video"]',
154
+ '[class*="poster"]',
155
+ '[class*="thumbnail"]',
156
+ '[class*="video-container"]',
157
+ '[class*="player"]',
158
+ ];
159
+
160
+ for (const selector of playSelectors) {
161
+ const el = document.querySelector(selector);
162
+ if (el && el instanceof HTMLElement) {
163
+ const rect = el.getBoundingClientRect();
164
+ if (rect.width > 50 && rect.height > 50) {
165
+ el.click();
166
+ return { clicked: true, selector, element: el.outerHTML.substring(0, 100) };
167
+ }
168
+ }
169
+ }
170
+ return { clicked: false };
171
+ });
172
+
173
+ if (clicked.clicked) {
174
+ console.log(chalk.green(` āœ“ Clicked on: ${clicked.selector}`));
175
+ console.log(chalk.gray(` ${clicked.element}...`));
176
+
177
+ // Wait for video element to appear
178
+ console.log(chalk.gray(" Waiting for video element..."));
179
+ try {
180
+ await session.page.waitForSelector(
181
+ "video, iframe[src*='vimeo'], iframe[src*='wistia'], iframe[src*='youtube']",
182
+ {
183
+ timeout: 5000,
184
+ }
185
+ );
186
+ console.log(chalk.green(" āœ“ Video element appeared!"));
187
+ } catch {
188
+ console.log(chalk.yellow(" ⚠ No video element detected after click"));
189
+ }
190
+
191
+ // Small delay for any animations/loading
192
+ await session.page.waitForTimeout(1000);
193
+ } else {
194
+ console.log(chalk.yellow(" ⚠ No clickable preview found"));
195
+ }
196
+ }
197
+
198
+ // Analyze page structure
199
+ const analysis = await session.page.evaluate(() => {
200
+ const result: Record<string, unknown> = {};
201
+
202
+ // Find all iframes (potential video embeds)
203
+ const iframes = document.querySelectorAll("iframe");
204
+ result.iframes = Array.from(iframes).map((iframe) => ({
205
+ src: iframe.src,
206
+ id: iframe.id,
207
+ className: iframe.className,
208
+ width: iframe.width,
209
+ height: iframe.height,
210
+ }));
211
+
212
+ // Find all video elements
213
+ const videos = document.querySelectorAll("video");
214
+ result.videos = Array.from(videos).map((video) => ({
215
+ src: video.src,
216
+ poster: video.poster,
217
+ className: video.className,
218
+ sources: Array.from(video.querySelectorAll("source")).map((s) => ({
219
+ src: s.src,
220
+ type: s.type,
221
+ })),
222
+ }));
223
+
224
+ // Find elements with video-related classes
225
+ const videoRelated = document.querySelectorAll(
226
+ '[class*="video"], [class*="player"], [class*="wistia"], [class*="vimeo"], [class*="embed"], [class*="media"]'
227
+ );
228
+ result.videoRelatedElements = Array.from(videoRelated).map((el) => ({
229
+ tagName: el.tagName,
230
+ className: el.className,
231
+ id: el.id,
232
+ dataAttributes: Object.fromEntries(
233
+ Array.from(el.attributes)
234
+ .filter((attr) => attr.name.startsWith("data-"))
235
+ .map((attr) => [attr.name, attr.value])
236
+ ),
237
+ // Include src attributes that might have video URLs
238
+ src: el.getAttribute("src"),
239
+ href: el.getAttribute("href"),
240
+ }));
241
+
242
+ // Look for any script tags that might contain video configuration
243
+ const scripts = document.querySelectorAll("script");
244
+ const videoScripts: string[] = [];
245
+ scripts.forEach((script) => {
246
+ const content = script.textContent || "";
247
+ if (
248
+ content.includes("vimeo") ||
249
+ content.includes("wistia") ||
250
+ content.includes("video") ||
251
+ content.includes("player") ||
252
+ content.includes("mux") ||
253
+ content.includes("cloudflare")
254
+ ) {
255
+ // Extract just the relevant parts
256
+ const matches = content.match(/(https?:\/\/[^\s"']+\.(mp4|m3u8|webm|mov)[^\s"']*)/gi);
257
+ if (matches) {
258
+ videoScripts.push(...matches);
259
+ }
260
+ // Also look for video IDs
261
+ const idMatches = content.match(/"(video[_-]?id|videoId|id)":\s*"([^"]+)"/gi);
262
+ if (idMatches) {
263
+ videoScripts.push(...idMatches);
264
+ }
265
+ }
266
+ });
267
+ result.videoScripts = [...new Set(videoScripts)];
268
+
269
+ // Find navigation/sidebar elements (for lessons)
270
+ const navElements = document.querySelectorAll(
271
+ 'nav, [class*="sidebar"], [class*="nav"], [class*="menu"], [class*="lesson"]'
272
+ );
273
+ result.navigationElements = Array.from(navElements)
274
+ .slice(0, 10)
275
+ .map((el) => ({
276
+ tagName: el.tagName,
277
+ className: el.className,
278
+ id: el.id,
279
+ childCount: el.children.length,
280
+ links: Array.from(el.querySelectorAll("a")).map((a) => ({
281
+ href: a.href,
282
+ text: a.textContent?.trim().substring(0, 50),
283
+ })),
284
+ }));
285
+
286
+ // Find main content area
287
+ const contentAreas = document.querySelectorAll(
288
+ 'main, article, [class*="content"], [class*="post"], [class*="body"]'
289
+ );
290
+ result.contentAreas = Array.from(contentAreas)
291
+ .slice(0, 5)
292
+ .map((el) => ({
293
+ tagName: el.tagName,
294
+ className: el.className,
295
+ id: el.id,
296
+ textLength: el.textContent?.length ?? 0,
297
+ hasVideo: el.querySelector("video, iframe") !== null,
298
+ }));
299
+
300
+ // Find all links to /classroom/
301
+ const classroomLinks = document.querySelectorAll('a[href*="/classroom/"]');
302
+ result.classroomLinks = Array.from(classroomLinks).map((a) => ({
303
+ href: (a as HTMLAnchorElement).href,
304
+ text: a.textContent?.trim().substring(0, 100),
305
+ className: a.className,
306
+ parentClass: a.parentElement?.className,
307
+ }));
308
+
309
+ // Get page structure overview
310
+ const getAllClasses = (el: Element, depth = 0): string[] => {
311
+ if (depth > 3) return [];
312
+ const classes: string[] = [];
313
+ if (el.className && typeof el.className === "string") {
314
+ classes.push(`${" ".repeat(depth)}${el.tagName}.${el.className.split(" ").join(".")}`);
315
+ }
316
+ Array.from(el.children).forEach((child) => {
317
+ classes.push(...getAllClasses(child, depth + 1));
318
+ });
319
+ return classes;
320
+ };
321
+
322
+ result.bodyStructure = getAllClasses(document.body).slice(0, 100);
323
+
324
+ return result;
325
+ });
326
+
327
+ // Output analysis
328
+ console.log(chalk.cyan("\nšŸŽ¬ Video Sources:\n"));
329
+ if ((analysis.iframes as { src: string }[]).length > 0) {
330
+ console.log(chalk.yellow(" Iframes:"));
331
+ for (const iframe of analysis.iframes as { src: string; className: string }[]) {
332
+ console.log(` - ${iframe.src}`);
333
+ console.log(chalk.gray(` class: ${iframe.className}`));
334
+ }
335
+ }
336
+ if ((analysis.videos as { src: string }[]).length > 0) {
337
+ console.log(chalk.yellow("\n Video elements:"));
338
+ for (const video of analysis.videos as {
339
+ src: string;
340
+ sources: { src: string }[];
341
+ }[]) {
342
+ console.log(` - ${video.src || "(no src)"}`);
343
+ for (const source of video.sources) {
344
+ console.log(` source: ${source.src}`);
345
+ }
346
+ }
347
+ }
348
+
349
+ // Show video URLs found in scripts
350
+ const videoScripts = analysis.videoScripts as string[];
351
+ if (videoScripts.length > 0) {
352
+ console.log(chalk.yellow("\n Video URLs from scripts:"));
353
+ for (const url of videoScripts.slice(0, 10)) {
354
+ console.log(chalk.green(` - ${url}`));
355
+ }
356
+ }
357
+
358
+ if ((analysis.videoRelatedElements as { className: string }[]).length > 0) {
359
+ console.log(chalk.yellow("\n Video-related elements:"));
360
+ for (const el of (
361
+ analysis.videoRelatedElements as {
362
+ tagName: string;
363
+ className: string;
364
+ dataAttributes: Record<string, string>;
365
+ src?: string;
366
+ }[]
367
+ ).slice(0, 10)) {
368
+ console.log(` - <${el.tagName.toLowerCase()}> class="${el.className}"`);
369
+ if (el.src) {
370
+ console.log(chalk.green(` src: ${el.src}`));
371
+ }
372
+ if (Object.keys(el.dataAttributes).length > 0) {
373
+ console.log(chalk.gray(` data: ${JSON.stringify(el.dataAttributes)}`));
374
+ }
375
+ }
376
+ }
377
+
378
+ console.log(chalk.cyan("\nšŸ“š Classroom Links:\n"));
379
+ for (const link of (
380
+ analysis.classroomLinks as { href: string; text: string; className: string }[]
381
+ ).slice(0, 15)) {
382
+ console.log(` - ${link.text || "(no text)"}`);
383
+ console.log(chalk.gray(` ${link.href}`));
384
+ console.log(chalk.gray(` class: ${link.className}`));
385
+ }
386
+
387
+ console.log(chalk.cyan("\n🧭 Navigation Elements:\n"));
388
+ for (const nav of analysis.navigationElements as {
389
+ tagName: string;
390
+ className: string;
391
+ links: { href: string; text: string }[];
392
+ }[]) {
393
+ console.log(` <${nav.tagName.toLowerCase()}> class="${nav.className}"`);
394
+ if (nav.links.length > 0) {
395
+ console.log(chalk.gray(` Links: ${nav.links.length}`));
396
+ for (const link of nav.links.slice(0, 5)) {
397
+ console.log(chalk.gray(` - ${link.text}: ${link.href}`));
398
+ }
399
+ }
400
+ }
401
+
402
+ // Save full analysis to file if requested
403
+ if (options.output || options.full) {
404
+ const outputDir = expandPath(options.output ?? config.outputDir);
405
+ await ensureDir(outputDir);
406
+
407
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
408
+ const filename = `inspect-${timestamp}.json`;
409
+ const filepath = join(outputDir, filename);
410
+
411
+ await outputFile(filepath, JSON.stringify(analysis, null, 2));
412
+ console.log(chalk.green(`\nšŸ“ Full analysis saved to: ${filepath}\n`));
413
+
414
+ // Also save HTML
415
+ if (options.full) {
416
+ const html = await session.page.content();
417
+ const htmlPath = join(outputDir, `inspect-${timestamp}.html`);
418
+ await outputFile(htmlPath, html);
419
+ console.log(chalk.green(`šŸ“ HTML saved to: ${htmlPath}\n`));
420
+ }
421
+ }
422
+
423
+ // Show network requests that looked like video
424
+ if (videoRequests.length > 0) {
425
+ console.log(chalk.cyan("\nšŸ“” Video-related Network Requests:\n"));
426
+ const seen = new Set<string>();
427
+ for (const req of videoRequests) {
428
+ if (seen.has(req.url)) continue;
429
+ seen.add(req.url);
430
+ console.log(chalk.green(` - ${req.url.substring(0, 120)}`));
431
+ console.log(chalk.gray(` type: ${req.resourceType}`));
432
+ }
433
+ }
434
+
435
+ console.log(chalk.gray("\nšŸ’” Tips:"));
436
+ console.log(chalk.gray(" - Use --click to trigger lazy-loaded video players"));
437
+ console.log(chalk.gray(" - Use --full to save complete HTML for offline analysis\n"));
438
+ } finally {
439
+ await browser.close();
440
+ }
441
+ }
@@ -0,0 +1,68 @@
1
+ import chalk from "chalk";
2
+ import {
3
+ clearSession,
4
+ getAuthenticatedSession,
5
+ hasValidSession,
6
+ isSkoolLoginPage,
7
+ } from "../../shared/auth.js";
8
+
9
+ const SKOOL_DOMAIN = "www.skool.com";
10
+ const SKOOL_LOGIN_URL = "https://www.skool.com/login";
11
+
12
+ /**
13
+ * Handles the login command.
14
+ * Opens a browser for the user to log in manually.
15
+ */
16
+ export async function loginCommand(options: { force?: boolean }): Promise<void> {
17
+ console.log(chalk.blue("\nšŸ” Skool.com Login\n"));
18
+
19
+ if ((await hasValidSession(SKOOL_DOMAIN)) && !options.force) {
20
+ console.log(chalk.yellow("āš ļø You already have an active session."));
21
+ console.log(chalk.gray(" Use --force to re-login anyway.\n"));
22
+ return;
23
+ }
24
+
25
+ if (options.force) {
26
+ await clearSession(SKOOL_DOMAIN);
27
+ console.log(chalk.gray(" Cleared existing session.\n"));
28
+ }
29
+
30
+ try {
31
+ const { browser } = await getAuthenticatedSession(
32
+ {
33
+ domain: SKOOL_DOMAIN,
34
+ loginUrl: SKOOL_LOGIN_URL,
35
+ isLoginPage: isSkoolLoginPage,
36
+ },
37
+ { headless: false }
38
+ );
39
+
40
+ // Close the browser after successful login
41
+ await browser.close();
42
+
43
+ console.log(chalk.green("āœ… Login successful!"));
44
+ console.log(chalk.gray(" Your session has been saved.\n"));
45
+ console.log(chalk.gray(" You can now use: offcourse sync <url>\n"));
46
+ } catch (error) {
47
+ if (error instanceof Error && error.message.includes("Timeout")) {
48
+ console.log(chalk.red("\nāŒ Login timed out."));
49
+ console.log(chalk.gray(" Please try again and complete the login within 5 minutes.\n"));
50
+ } else {
51
+ console.log(chalk.red("\nāŒ Login failed:"), error);
52
+ }
53
+ process.exit(1);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Handles the logout command.
59
+ */
60
+ export async function logoutCommand(): Promise<void> {
61
+ console.log(chalk.blue("\nšŸ”“ Logging out...\n"));
62
+
63
+ if (await clearSession(SKOOL_DOMAIN)) {
64
+ console.log(chalk.green("āœ… Session cleared successfully.\n"));
65
+ } else {
66
+ console.log(chalk.yellow("āš ļø No active session found.\n"));
67
+ }
68
+ }