@mo7yw4ng/openape 1.0.6 → 2.0.3

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 (152) hide show
  1. package/bin/openape +29 -0
  2. package/bin/openape.js +29 -0
  3. package/package.json +22 -28
  4. package/LICENSE +0 -21
  5. package/README.md +0 -138
  6. package/esm/_dnt.polyfills.d.ts +0 -101
  7. package/esm/_dnt.polyfills.d.ts.map +0 -1
  8. package/esm/_dnt.polyfills.js +0 -127
  9. package/esm/_dnt.shims.d.ts +0 -6
  10. package/esm/_dnt.shims.d.ts.map +0 -1
  11. package/esm/_dnt.shims.js +0 -61
  12. package/esm/deno.d.ts +0 -25
  13. package/esm/deno.d.ts.map +0 -1
  14. package/esm/deno.js +0 -23
  15. package/esm/package.json +0 -3
  16. package/esm/src/commands/announcements.d.ts +0 -3
  17. package/esm/src/commands/announcements.d.ts.map +0 -1
  18. package/esm/src/commands/announcements.js +0 -71
  19. package/esm/src/commands/assignments.d.ts +0 -3
  20. package/esm/src/commands/assignments.d.ts.map +0 -1
  21. package/esm/src/commands/assignments.js +0 -229
  22. package/esm/src/commands/auth.d.ts +0 -3
  23. package/esm/src/commands/auth.d.ts.map +0 -1
  24. package/esm/src/commands/auth.js +0 -290
  25. package/esm/src/commands/calendar.d.ts +0 -3
  26. package/esm/src/commands/calendar.d.ts.map +0 -1
  27. package/esm/src/commands/calendar.js +0 -127
  28. package/esm/src/commands/courses.d.ts +0 -3
  29. package/esm/src/commands/courses.d.ts.map +0 -1
  30. package/esm/src/commands/courses.js +0 -312
  31. package/esm/src/commands/forums.d.ts +0 -3
  32. package/esm/src/commands/forums.d.ts.map +0 -1
  33. package/esm/src/commands/forums.js +0 -190
  34. package/esm/src/commands/grades.d.ts +0 -3
  35. package/esm/src/commands/grades.d.ts.map +0 -1
  36. package/esm/src/commands/grades.js +0 -84
  37. package/esm/src/commands/materials.d.ts +0 -3
  38. package/esm/src/commands/materials.d.ts.map +0 -1
  39. package/esm/src/commands/materials.js +0 -402
  40. package/esm/src/commands/quizzes.d.ts +0 -3
  41. package/esm/src/commands/quizzes.d.ts.map +0 -1
  42. package/esm/src/commands/quizzes.js +0 -236
  43. package/esm/src/commands/skills.d.ts +0 -3
  44. package/esm/src/commands/skills.d.ts.map +0 -1
  45. package/esm/src/commands/skills.js +0 -106
  46. package/esm/src/commands/upload.d.ts +0 -3
  47. package/esm/src/commands/upload.d.ts.map +0 -1
  48. package/esm/src/commands/upload.js +0 -55
  49. package/esm/src/commands/videos.d.ts +0 -3
  50. package/esm/src/commands/videos.d.ts.map +0 -1
  51. package/esm/src/commands/videos.js +0 -266
  52. package/esm/src/index.d.ts +0 -28
  53. package/esm/src/index.d.ts.map +0 -1
  54. package/esm/src/index.js +0 -164
  55. package/esm/src/lib/auth.d.ts +0 -66
  56. package/esm/src/lib/auth.d.ts.map +0 -1
  57. package/esm/src/lib/auth.js +0 -286
  58. package/esm/src/lib/config.d.ts +0 -6
  59. package/esm/src/lib/config.d.ts.map +0 -1
  60. package/esm/src/lib/config.js +0 -36
  61. package/esm/src/lib/logger.d.ts +0 -3
  62. package/esm/src/lib/logger.d.ts.map +0 -1
  63. package/esm/src/lib/logger.js +0 -26
  64. package/esm/src/lib/moodle.d.ts +0 -447
  65. package/esm/src/lib/moodle.d.ts.map +0 -1
  66. package/esm/src/lib/moodle.js +0 -1353
  67. package/esm/src/lib/session.d.ts +0 -8
  68. package/esm/src/lib/session.d.ts.map +0 -1
  69. package/esm/src/lib/session.js +0 -42
  70. package/esm/src/lib/token.d.ts +0 -38
  71. package/esm/src/lib/token.d.ts.map +0 -1
  72. package/esm/src/lib/token.js +0 -178
  73. package/esm/src/lib/types.d.ts +0 -189
  74. package/esm/src/lib/types.d.ts.map +0 -1
  75. package/esm/src/lib/types.js +0 -2
  76. package/esm/src/lib/utils.d.ts +0 -52
  77. package/esm/src/lib/utils.d.ts.map +0 -1
  78. package/esm/src/lib/utils.js +0 -122
  79. package/script/_dnt.polyfills.d.ts +0 -101
  80. package/script/_dnt.polyfills.d.ts.map +0 -1
  81. package/script/_dnt.polyfills.js +0 -130
  82. package/script/_dnt.shims.d.ts +0 -6
  83. package/script/_dnt.shims.d.ts.map +0 -1
  84. package/script/_dnt.shims.js +0 -65
  85. package/script/deno.d.ts +0 -25
  86. package/script/deno.d.ts.map +0 -1
  87. package/script/deno.js +0 -25
  88. package/script/package.json +0 -3
  89. package/script/src/commands/announcements.d.ts +0 -3
  90. package/script/src/commands/announcements.d.ts.map +0 -1
  91. package/script/src/commands/announcements.js +0 -74
  92. package/script/src/commands/assignments.d.ts +0 -3
  93. package/script/src/commands/assignments.d.ts.map +0 -1
  94. package/script/src/commands/assignments.js +0 -268
  95. package/script/src/commands/auth.d.ts +0 -3
  96. package/script/src/commands/auth.d.ts.map +0 -1
  97. package/script/src/commands/auth.js +0 -296
  98. package/script/src/commands/calendar.d.ts +0 -3
  99. package/script/src/commands/calendar.d.ts.map +0 -1
  100. package/script/src/commands/calendar.js +0 -133
  101. package/script/src/commands/courses.d.ts +0 -3
  102. package/script/src/commands/courses.d.ts.map +0 -1
  103. package/script/src/commands/courses.js +0 -315
  104. package/script/src/commands/forums.d.ts +0 -3
  105. package/script/src/commands/forums.d.ts.map +0 -1
  106. package/script/src/commands/forums.js +0 -193
  107. package/script/src/commands/grades.d.ts +0 -3
  108. package/script/src/commands/grades.d.ts.map +0 -1
  109. package/script/src/commands/grades.js +0 -87
  110. package/script/src/commands/materials.d.ts +0 -3
  111. package/script/src/commands/materials.d.ts.map +0 -1
  112. package/script/src/commands/materials.js +0 -408
  113. package/script/src/commands/quizzes.d.ts +0 -3
  114. package/script/src/commands/quizzes.d.ts.map +0 -1
  115. package/script/src/commands/quizzes.js +0 -239
  116. package/script/src/commands/skills.d.ts +0 -3
  117. package/script/src/commands/skills.d.ts.map +0 -1
  118. package/script/src/commands/skills.js +0 -112
  119. package/script/src/commands/upload.d.ts +0 -3
  120. package/script/src/commands/upload.d.ts.map +0 -1
  121. package/script/src/commands/upload.js +0 -61
  122. package/script/src/commands/videos.d.ts +0 -3
  123. package/script/src/commands/videos.d.ts.map +0 -1
  124. package/script/src/commands/videos.js +0 -272
  125. package/script/src/index.d.ts +0 -28
  126. package/script/src/index.d.ts.map +0 -1
  127. package/script/src/index.js +0 -171
  128. package/script/src/lib/auth.d.ts +0 -66
  129. package/script/src/lib/auth.d.ts.map +0 -1
  130. package/script/src/lib/auth.js +0 -296
  131. package/script/src/lib/config.d.ts +0 -6
  132. package/script/src/lib/config.d.ts.map +0 -1
  133. package/script/src/lib/config.js +0 -42
  134. package/script/src/lib/logger.d.ts +0 -3
  135. package/script/src/lib/logger.d.ts.map +0 -1
  136. package/script/src/lib/logger.js +0 -29
  137. package/script/src/lib/moodle.d.ts +0 -447
  138. package/script/src/lib/moodle.d.ts.map +0 -1
  139. package/script/src/lib/moodle.js +0 -1425
  140. package/script/src/lib/session.d.ts +0 -8
  141. package/script/src/lib/session.d.ts.map +0 -1
  142. package/script/src/lib/session.js +0 -45
  143. package/script/src/lib/token.d.ts +0 -38
  144. package/script/src/lib/token.d.ts.map +0 -1
  145. package/script/src/lib/token.js +0 -189
  146. package/script/src/lib/types.d.ts +0 -189
  147. package/script/src/lib/types.d.ts.map +0 -1
  148. package/script/src/lib/types.js +0 -3
  149. package/script/src/lib/utils.d.ts +0 -52
  150. package/script/src/lib/utils.d.ts.map +0 -1
  151. package/script/src/lib/utils.js +0 -167
  152. package/skills/openape/SKILL.md +0 -115
@@ -1,286 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { chromium } from "playwright-core";
4
- import { acquireWsToken, loadWsToken, saveWsToken } from "./token.js";
5
- import { createLogger } from "./logger.js";
6
- import { getOutputFormat, getSessionPath } from "./utils.js";
7
- import { extractSessionInfo } from "./session.js";
8
- /**
9
- * Find a Chromium-based browser executable on Windows, macOS, or Linux.
10
- * Priority: Edge → Chrome → Brave
11
- */
12
- export function findEdgePath() {
13
- const platform = process.platform;
14
- if (platform === "darwin") {
15
- const candidates = [
16
- "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
17
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
18
- "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
19
- ];
20
- for (const candidate of candidates) {
21
- if (fs.existsSync(candidate))
22
- return candidate;
23
- }
24
- }
25
- else if (platform === "linux") {
26
- const candidates = [
27
- "/usr/bin/microsoft-edge",
28
- "/usr/bin/google-chrome",
29
- "/usr/bin/chromium-browser",
30
- "/usr/bin/chromium",
31
- "/usr/bin/brave-browser",
32
- ];
33
- for (const candidate of candidates) {
34
- if (fs.existsSync(candidate))
35
- return candidate;
36
- }
37
- }
38
- else if (platform === "win32") {
39
- const roots = [
40
- process.env.PROGRAMFILES,
41
- process.env["PROGRAMFILES(X86)"],
42
- process.env.LOCALAPPDATA,
43
- ].filter(Boolean);
44
- const browsers = [
45
- { suffix: "Microsoft\\Edge\\Application\\msedge.exe" },
46
- { suffix: "Google\\Chrome\\Application\\chrome.exe" },
47
- { suffix: "BraveSoftware\\Brave-Browser\\Application\\brave.exe" },
48
- ];
49
- for (const { suffix } of browsers) {
50
- for (const root of roots) {
51
- const candidate = path.join(root, suffix);
52
- if (fs.existsSync(candidate))
53
- return candidate;
54
- }
55
- }
56
- }
57
- else {
58
- throw new Error(`不支援的作業系統:${platform}`);
59
- }
60
- throw new Error("找不到可用的瀏覽器(Edge / Chrome / Brave)。請確認已安裝其中一種。");
61
- }
62
- /**
63
- * Launch a browser and return an authenticated context.
64
- * Tries to restore a saved session first; falls back to fresh OAuth login.
65
- * Also acquires Moodle Web Service Token for API calls.
66
- */
67
- export async function launchAuthenticated(config, log) {
68
- const edgePath = findEdgePath();
69
- log.debug(`Using Edge: ${edgePath}`);
70
- // Wait a bit to ensure any previous browser process has fully terminated
71
- await new Promise(resolve => setTimeout(resolve, 1000));
72
- const browser = await chromium.launch({
73
- executablePath: edgePath,
74
- headless: config.headless,
75
- slowMo: config.slowMo,
76
- });
77
- // Try loading saved WS token first
78
- let wsToken = loadWsToken(config.authStatePath) ?? undefined;
79
- if (wsToken) {
80
- log.info("Loaded saved Web Service Token.");
81
- }
82
- // Try restoring a saved session
83
- const restored = await tryRestoreSession(browser, config, log);
84
- if (restored) {
85
- const page = restored.pages()[0] ?? (await restored.newPage());
86
- // If no saved WS token, try to acquire one
87
- if (!wsToken) {
88
- try {
89
- wsToken = await acquireWsToken(page, config, log);
90
- saveWsToken(config.authStatePath, wsToken);
91
- }
92
- catch {
93
- log.warn("Failed to acquire WS Token with restored session, continuing without it.");
94
- }
95
- }
96
- return { browser, context: restored, page, wsToken };
97
- }
98
- // Fresh login
99
- if (config.headless) {
100
- await browser.close().catch(() => { });
101
- throw new Error("找不到有效的 Session 或是 Session 已過期。\n" +
102
- "請先執行 `openape login` 進行手動登入,或是加上 `--headed` 參數執行目前的指令以開啟登入畫面。");
103
- }
104
- const context = await browser.newContext();
105
- const page = await context.newPage();
106
- await login(page, config, log);
107
- await saveSession(context, config.authStatePath, log);
108
- // Acquire WS Token after successful login
109
- if (!wsToken) {
110
- try {
111
- wsToken = await acquireWsToken(page, config, log);
112
- saveWsToken(config.authStatePath, wsToken);
113
- }
114
- catch {
115
- log.warn("Failed to acquire WS Token, continuing with sesskey-only auth.");
116
- }
117
- }
118
- return { browser, context, page, wsToken };
119
- }
120
- /**
121
- * Safely close browser and context with timeout.
122
- * Designed for AI agent usage - no human interaction needed.
123
- * If noWait is true, initiates cleanup but doesn't wait for completion.
124
- *
125
- * Note: Closes sequentially (context first, then browser) to avoid libuv
126
- * assertion failures on Windows when handles are closed concurrently.
127
- */
128
- export async function closeBrowserSafely(browser, context, timeoutMs = 5000, noWait = false) {
129
- const closePromises = [];
130
- // Close context first with error handling
131
- if (context) {
132
- closePromises.push(Promise.race([
133
- context.close().catch(() => { }),
134
- new Promise(resolve => setTimeout(() => resolve(), timeoutMs))
135
- ]));
136
- }
137
- // Close browser after context with error handling
138
- closePromises.push(Promise.race([
139
- browser.close().catch(() => { }),
140
- new Promise(resolve => setTimeout(() => resolve(), timeoutMs))
141
- ]));
142
- if (noWait) {
143
- // Fire and forget - don't wait for cleanup
144
- Promise.allSettled(closePromises);
145
- return;
146
- }
147
- // Wait sequentially to avoid libuv issues on Windows
148
- for (const promise of closePromises) {
149
- await promise.catch(() => { });
150
- }
151
- }
152
- /**
153
- * Attempt to restore a session from stored state.
154
- * Returns null if the stored state doesn't exist or the session is expired.
155
- */
156
- async function tryRestoreSession(browser, config, log) {
157
- const statePath = path.resolve(config.authStatePath);
158
- if (!fs.existsSync(statePath)) {
159
- log.debug("No saved session found, will perform fresh login.");
160
- return null;
161
- }
162
- log.info("Restoring saved session...");
163
- const context = await browser.newContext({ storageState: statePath });
164
- const page = await context.newPage();
165
- try {
166
- await page.goto(`${config.moodleBaseUrl}/my/`, {
167
- waitUntil: "domcontentloaded",
168
- timeout: 15000,
169
- });
170
- // If we got redirected to a login page, the session is expired
171
- const url = page.url();
172
- if (url.includes("login") || url.includes("microsoftonline")) {
173
- log.warn("Saved session expired, will re-authenticate.");
174
- await context.close();
175
- return null;
176
- }
177
- log.success("Session restored successfully.");
178
- return context;
179
- }
180
- catch {
181
- log.warn("Failed to restore session, will re-authenticate.");
182
- await context.close();
183
- return null;
184
- }
185
- }
186
- /**
187
- * Save the current session state to disk for future reuse.
188
- */
189
- async function saveSession(context, statePath, log) {
190
- try {
191
- await context.storageState({ path: statePath });
192
- log.debug("Session saved for future reuse.");
193
- }
194
- catch (err) {
195
- log.warn(`Failed to save session: ${err}`);
196
- }
197
- }
198
- /**
199
- * Perform Microsoft OAuth login flow.
200
- */
201
- async function login(page, config, log) {
202
- log.info("Starting Microsoft OAuth login...");
203
- await page.goto(`${config.moodleBaseUrl}/auth/oauth2/login.php`, {
204
- waitUntil: "domcontentloaded",
205
- timeout: 30000,
206
- });
207
- // Wait for Microsoft login page or redirect back to Moodle
208
- try {
209
- await page.waitForURL((url) => url.toString().includes("microsoftonline") ||
210
- url.toString().includes("login.microsoftonline") ||
211
- (url.toString().includes("ilearning.cycu.edu.tw") &&
212
- !url.toString().includes("auth/oauth2/login")), { timeout: 10000 });
213
- const url = page.url().toString();
214
- if (url.includes("microsoftonline") || url.includes("login.microsoftonline")) {
215
- log.info("Microsoft login page detected. Please complete login in the browser.");
216
- log.info("Waiting for redirect back to Moodle...");
217
- await page.waitForURL((u) => u.toString().includes("ilearning.cycu.edu.tw") &&
218
- !u.toString().includes("microsoftonline") &&
219
- !u.toString().includes("login.microsoftonline"), { timeout: 300000 });
220
- }
221
- }
222
- catch {
223
- // Already logged in or redirected
224
- }
225
- // Verify we're logged in
226
- const finalUrl = page.url().toString();
227
- if (finalUrl.includes("login") ||
228
- finalUrl.includes("microsoftonline") ||
229
- finalUrl === config.moodleBaseUrl + "/auth/oauth2/login.php") {
230
- throw new Error(`登入後未重新導向回 Moodle。目前 URL: ${finalUrl}`);
231
- }
232
- log.success("Login completed successfully.");
233
- }
234
- /**
235
- * Create API context for WS token operations (no browser required).
236
- * Returns null if session is invalid or WS token is missing.
237
- */
238
- export async function createApiContext(options, command) {
239
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
240
- const outputFormat = command ? getOutputFormat(command) : "json";
241
- const silent = outputFormat === "json" && !opts.verbose;
242
- const log = createLogger(opts.verbose, silent, outputFormat);
243
- const sessionPath = getSessionPath();
244
- const wsToken = loadWsToken(sessionPath);
245
- if (!wsToken) {
246
- log.error("未找到 WS token。請先執行 'openape login' 進行登入。");
247
- return null;
248
- }
249
- return {
250
- log,
251
- session: {
252
- wsToken,
253
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
254
- },
255
- };
256
- }
257
- /**
258
- * Create an authenticated browser context for commands that need page access.
259
- * Launches a browser, restores or creates a session, and extracts session info.
260
- */
261
- export async function createBrowserContext(options, command) {
262
- const opts = command?.optsWithGlobals ? command.optsWithGlobals() : options;
263
- const outputFormat = command ? getOutputFormat(command) : "json";
264
- const silent = outputFormat === "json" && !opts.verbose;
265
- const log = createLogger(opts.verbose, silent, outputFormat);
266
- const sessionPath = getSessionPath();
267
- const headed = "headed" in options ? options.headed : false;
268
- const config = {
269
- courseUrl: "",
270
- moodleBaseUrl: "https://ilearning.cycu.edu.tw",
271
- headless: !headed,
272
- slowMo: 0,
273
- authStatePath: sessionPath,
274
- ollamaBaseUrl: "",
275
- };
276
- try {
277
- log.info("啟動瀏覽器...");
278
- const { browser, context, page, wsToken } = await launchAuthenticated(config, log);
279
- const session = await extractSessionInfo(page, config, log, wsToken);
280
- return { log, page, session, browser, context };
281
- }
282
- catch (err) {
283
- log.error(err instanceof Error ? err.message : String(err));
284
- return null;
285
- }
286
- }
@@ -1,6 +0,0 @@
1
- import type { AppConfig } from "./types.js";
2
- /**
3
- * Load config from .env file (if it exists).
4
- */
5
- export declare function loadConfig(baseDir?: string): AppConfig;
6
- //# sourceMappingURL=config.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/src/lib/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAgBtD"}
@@ -1,36 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- /**
4
- * Load config from .env file (if it exists).
5
- */
6
- export function loadConfig(baseDir) {
7
- const envPath = baseDir ? path.resolve(baseDir, ".env") : path.resolve(".env");
8
- if (fs.existsSync(envPath)) {
9
- const envContent = fs.readFileSync(envPath, "utf8");
10
- for (const line of envContent.split("\n")) {
11
- const trimmed = line.trim();
12
- if (!trimmed || trimmed.startsWith("#"))
13
- continue;
14
- const eqIdx = trimmed.indexOf("=");
15
- if (eqIdx === -1)
16
- continue;
17
- const key = trimmed.slice(0, eqIdx).trim();
18
- const value = trimmed.slice(eqIdx + 1).trim();
19
- if (!process.env[key])
20
- process.env[key] = value;
21
- }
22
- }
23
- return buildConfig();
24
- }
25
- function buildConfig() {
26
- const moodleBaseUrl = (process.env.MOODLE_BASE_URL ?? "https://ilearning.cycu.edu.tw").replace(/\/$/, "");
27
- return {
28
- courseUrl: "",
29
- moodleBaseUrl,
30
- headless: process.env.HEADLESS !== "false",
31
- slowMo: parseInt(process.env.SLOW_MO ?? "0", 10),
32
- authStatePath: process.env.AUTH_STATE_PATH ?? ".auth/storage-state.json",
33
- ollamaModel: process.env.MODEL,
34
- ollamaBaseUrl: (process.env.OLLAMA_BASE_URL ?? "http://localhost:11434").replace(/\/$/, ""),
35
- };
36
- }
@@ -1,3 +0,0 @@
1
- import type { Logger, OutputFormat } from "./types.js";
2
- export declare function createLogger(verbose?: boolean, silent?: boolean, outputFormat?: OutputFormat): Logger;
3
- //# sourceMappingURL=logger.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../src/src/lib/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAOvD,wBAAgB,YAAY,CAAC,OAAO,UAAQ,EAAE,MAAM,UAAQ,EAAE,YAAY,GAAE,YAAqB,GAAG,MAAM,CAuBzG"}
@@ -1,26 +0,0 @@
1
- const NO_COLOR = !!process.env.NO_COLOR;
2
- const c = (code, text) => NO_COLOR ? text : `\x1b[${code}m${text}\x1b[0m`;
3
- export function createLogger(verbose = false, silent = false, outputFormat = "json") {
4
- const errorFn = outputFormat === "json"
5
- ? (msg) => console.error(JSON.stringify({ error: msg }))
6
- : (msg) => console.error(c("31", "[ERR]") + ` ${msg}`);
7
- if (silent) {
8
- return {
9
- info: (_msg) => { },
10
- success: (_msg) => { },
11
- warn: (_msg) => { },
12
- error: errorFn,
13
- debug: (_msg) => { },
14
- };
15
- }
16
- return {
17
- info: (msg) => console.error(c("36", "[INFO]") + ` ${msg}`),
18
- success: (msg) => console.error(c("32", "[OK]") + ` ${msg}`),
19
- warn: (msg) => console.error(c("33", "[WARN]") + ` ${msg}`),
20
- error: errorFn,
21
- debug: (msg) => {
22
- if (verbose)
23
- console.error(c("90", "[DBG]") + ` ${msg}`);
24
- },
25
- };
26
- }