create-glyph-extension 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -0
  3. package/dist/index.js +1408 -0
  4. package/package.json +39 -0
package/dist/index.js ADDED
@@ -0,0 +1,1408 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { execSync, spawn } from "child_process";
5
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
6
+ import { basename, join, resolve } from "path";
7
+ import prompts from "prompts";
8
+ import pc from "picocolors";
9
+
10
+ // src/templates/package-json.ts
11
+ function packageJson(ctx) {
12
+ return JSON.stringify(
13
+ {
14
+ name: ctx.projectName,
15
+ version: "1.0.0",
16
+ private: true,
17
+ type: "module",
18
+ engines: { node: ">=20" },
19
+ scripts: {
20
+ dev: "glyph dev",
21
+ build: "glyph build",
22
+ test: "glyph test",
23
+ validate: "glyph validate",
24
+ format: 'prettier --write "sources/**/*.ts"'
25
+ },
26
+ devDependencies: {
27
+ "@glyphmoe/sdk": "~0.1.9",
28
+ "@glyphmoe/cli": "^1.0.0",
29
+ cheerio: "^1.0.0",
30
+ typescript: "^5.0.0",
31
+ vitest: "^3.0.0",
32
+ prettier: "^3.0.0"
33
+ }
34
+ },
35
+ null,
36
+ 2
37
+ ) + "\n";
38
+ }
39
+
40
+ // src/templates/repo-json.ts
41
+ function repoJson(ctx) {
42
+ const repo = {
43
+ name: ctx.projectName,
44
+ author: ctx.author,
45
+ description: "My custom Glyph extension repository."
46
+ };
47
+ if (ctx.repoUrl) {
48
+ repo.website = ctx.repoUrl;
49
+ const match = ctx.repoUrl.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
50
+ if (match) {
51
+ repo.url = `https://${match[1]}.github.io/${match[2]}`;
52
+ } else {
53
+ repo.url = ctx.repoUrl;
54
+ }
55
+ } else {
56
+ repo.website = "";
57
+ repo.url = "";
58
+ }
59
+ return JSON.stringify(repo, null, 2) + "\n";
60
+ }
61
+
62
+ // src/templates/tsconfig-json.ts
63
+ function tsconfigJson() {
64
+ return JSON.stringify(
65
+ {
66
+ compilerOptions: {
67
+ target: "ES2022",
68
+ module: "ES2022",
69
+ moduleResolution: "bundler",
70
+ lib: ["ES2022"],
71
+ strict: true,
72
+ esModuleInterop: true,
73
+ skipLibCheck: true,
74
+ declaration: true,
75
+ outDir: "./dist"
76
+ },
77
+ include: ["sources/**/*"],
78
+ exclude: ["node_modules", "dist"]
79
+ },
80
+ null,
81
+ 2
82
+ ) + "\n";
83
+ }
84
+
85
+ // src/templates/vitest-config.ts
86
+ function vitestConfig() {
87
+ return `import { defineConfig } from 'vitest/config'
88
+
89
+ // Note: \`glyph test\` uses its own vitest config that injects the SDK test runtime.
90
+ // This file is only used when running \`npx vitest\` directly.
91
+ export default defineConfig({
92
+ test: {
93
+ testTimeout: 10000,
94
+ },
95
+ })
96
+ `;
97
+ }
98
+
99
+ // src/templates/gitignore.ts
100
+ function gitignore() {
101
+ return `node_modules
102
+ dist
103
+ .DS_Store
104
+ `;
105
+ }
106
+
107
+ // src/templates/prettierrc.ts
108
+ function prettierrc() {
109
+ return JSON.stringify(
110
+ {
111
+ semi: false,
112
+ singleQuote: true,
113
+ trailingComma: "all",
114
+ printWidth: 100,
115
+ tabWidth: 2,
116
+ arrowParens: "avoid"
117
+ },
118
+ null,
119
+ 2
120
+ ) + "\n";
121
+ }
122
+
123
+ // src/templates/index-html.ts
124
+ function indexHtml() {
125
+ return `<!doctype html>
126
+ <html lang="en">
127
+ <head>
128
+ <meta charset="utf-8" />
129
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
130
+ <meta name="theme-color" content="#0E0E12" />
131
+ <title>Glyph Extensions</title>
132
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
133
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
134
+ <link
135
+ href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap"
136
+ rel="stylesheet"
137
+ />
138
+ <style>
139
+ :root {
140
+ --bg: #0e0e12;
141
+ --bg-surface: #16161d;
142
+ --bg-surface-hover: #1e1e28;
143
+ --border: rgba(255, 255, 255, 0.06);
144
+ --text: #e8e6e3;
145
+ --text-secondary: #8a8a96;
146
+ --accent: #ef9f27;
147
+ --accent-dim: rgba(239, 159, 39, 0.12);
148
+ --green: #34c759;
149
+ --red: #da3633;
150
+ --radius: 12px;
151
+ --font: "Space Grotesk", system-ui, -apple-system, sans-serif;
152
+ }
153
+ *,
154
+ *::before,
155
+ *::after {
156
+ box-sizing: border-box;
157
+ margin: 0;
158
+ padding: 0;
159
+ }
160
+ body {
161
+ font-family: var(--font);
162
+ background: var(--bg);
163
+ color: var(--text);
164
+ line-height: 1.6;
165
+ min-height: 100vh;
166
+ -webkit-font-smoothing: antialiased;
167
+ -moz-osx-font-smoothing: grayscale;
168
+ }
169
+ a {
170
+ color: inherit;
171
+ text-decoration: none;
172
+ }
173
+
174
+ .container {
175
+ max-width: 600px;
176
+ margin: 0 auto;
177
+ padding: 24px 20px 64px;
178
+ }
179
+
180
+ /* Header */
181
+ .header {
182
+ text-align: center;
183
+ padding: 56px 0 32px;
184
+ }
185
+ .header-logo {
186
+ margin-bottom: 20px;
187
+ }
188
+ .header h1 {
189
+ font-size: 26px;
190
+ font-weight: 700;
191
+ letter-spacing: -0.02em;
192
+ margin-bottom: 4px;
193
+ }
194
+ .header .author {
195
+ color: var(--accent);
196
+ font-size: 13px;
197
+ font-weight: 500;
198
+ margin-bottom: 8px;
199
+ }
200
+ .header .author:empty {
201
+ display: none;
202
+ }
203
+ .header p {
204
+ color: var(--text-secondary);
205
+ font-size: 15px;
206
+ max-width: 400px;
207
+ margin: 0 auto;
208
+ }
209
+
210
+ /* Add button */
211
+ .add-btn {
212
+ display: inline-flex;
213
+ align-items: center;
214
+ gap: 8px;
215
+ background: var(--accent);
216
+ color: var(--bg);
217
+ font-family: var(--font);
218
+ font-size: 14px;
219
+ font-weight: 600;
220
+ padding: 12px 28px;
221
+ border-radius: 10px;
222
+ cursor: pointer;
223
+ transition:
224
+ opacity 0.15s,
225
+ transform 0.1s;
226
+ margin: 24px 0 8px;
227
+ text-decoration: none;
228
+ }
229
+ .add-btn:hover {
230
+ opacity: 0.85;
231
+ }
232
+ .add-btn:active {
233
+ transform: scale(0.97);
234
+ }
235
+ .add-btn svg {
236
+ width: 18px;
237
+ height: 18px;
238
+ fill: currentColor;
239
+ }
240
+
241
+ /* Fallback */
242
+ .fallback {
243
+ display: none;
244
+ background: var(--bg-surface);
245
+ border: 1px solid var(--border);
246
+ border-radius: var(--radius);
247
+ padding: 16px 20px;
248
+ margin-top: 16px;
249
+ font-size: 13px;
250
+ color: var(--text-secondary);
251
+ text-align: center;
252
+ }
253
+ .fallback.visible {
254
+ display: block;
255
+ }
256
+ .fallback strong {
257
+ color: var(--text);
258
+ }
259
+
260
+ /* Sources */
261
+ .sources-header {
262
+ display: flex;
263
+ align-items: center;
264
+ justify-content: space-between;
265
+ margin: 40px 0 16px;
266
+ padding-bottom: 10px;
267
+ border-bottom: 1px solid var(--border);
268
+ }
269
+ .sources-header h2 {
270
+ font-size: 16px;
271
+ font-weight: 600;
272
+ letter-spacing: -0.01em;
273
+ }
274
+ .sources-header .count {
275
+ font-size: 12px;
276
+ color: var(--text-secondary);
277
+ background: var(--bg-surface);
278
+ padding: 4px 10px;
279
+ border-radius: 20px;
280
+ }
281
+ .source-list {
282
+ list-style: none;
283
+ }
284
+ .source-item {
285
+ display: flex;
286
+ align-items: center;
287
+ gap: 14px;
288
+ padding: 14px 16px;
289
+ border-radius: var(--radius);
290
+ background: var(--bg-surface);
291
+ margin-bottom: 8px;
292
+ border: 1px solid var(--border);
293
+ transition:
294
+ border-color 0.2s,
295
+ background 0.2s;
296
+ }
297
+ .source-item:hover {
298
+ border-color: rgba(255, 255, 255, 0.1);
299
+ background: var(--bg-surface-hover);
300
+ }
301
+ .source-icon {
302
+ width: 40px;
303
+ height: 40px;
304
+ border-radius: 10px;
305
+ background: rgba(255, 255, 255, 0.04);
306
+ display: flex;
307
+ align-items: center;
308
+ justify-content: center;
309
+ flex-shrink: 0;
310
+ overflow: hidden;
311
+ font-size: 18px;
312
+ color: var(--text-secondary);
313
+ }
314
+ .source-icon img {
315
+ width: 100%;
316
+ height: 100%;
317
+ object-fit: cover;
318
+ }
319
+ .source-info {
320
+ flex: 1;
321
+ min-width: 0;
322
+ }
323
+ .source-name {
324
+ font-weight: 600;
325
+ font-size: 14px;
326
+ letter-spacing: -0.01em;
327
+ }
328
+ .source-meta {
329
+ display: flex;
330
+ gap: 8px;
331
+ align-items: center;
332
+ font-size: 12px;
333
+ color: var(--text-secondary);
334
+ margin-top: 3px;
335
+ }
336
+ .source-meta .lang {
337
+ background: rgba(255, 255, 255, 0.06);
338
+ padding: 2px 6px;
339
+ border-radius: 4px;
340
+ text-transform: uppercase;
341
+ font-weight: 600;
342
+ font-size: 10px;
343
+ letter-spacing: 0.05em;
344
+ }
345
+ .source-meta .nsfw-badge {
346
+ background: var(--red);
347
+ color: #fff;
348
+ padding: 2px 6px;
349
+ border-radius: 4px;
350
+ font-weight: 700;
351
+ font-size: 9px;
352
+ letter-spacing: 0.05em;
353
+ text-transform: uppercase;
354
+ }
355
+
356
+ /* Loading */
357
+ .status {
358
+ text-align: center;
359
+ padding: 48px 0;
360
+ color: var(--text-secondary);
361
+ font-size: 13px;
362
+ }
363
+ .spinner {
364
+ width: 20px;
365
+ height: 20px;
366
+ border: 2px solid rgba(255, 255, 255, 0.08);
367
+ border-top-color: var(--accent);
368
+ border-radius: 50%;
369
+ animation: spin 0.8s linear infinite;
370
+ margin: 0 auto 12px;
371
+ }
372
+ @keyframes spin {
373
+ to {
374
+ transform: rotate(360deg);
375
+ }
376
+ }
377
+
378
+ /* Footer */
379
+ .footer {
380
+ text-align: center;
381
+ padding-top: 48px;
382
+ font-size: 12px;
383
+ color: var(--text-secondary);
384
+ }
385
+ .footer a {
386
+ color: var(--accent);
387
+ transition: opacity 0.2s;
388
+ }
389
+ .footer a:hover {
390
+ opacity: 0.7;
391
+ }
392
+
393
+ /* Playground */
394
+ .playground { margin-top: 40px; padding-top: 32px; border-top: 1px solid var(--border); }
395
+ .playground-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
396
+ .playground-header h2 { font-size: 16px; font-weight: 600; }
397
+ .pg-select { background: var(--bg-surface); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 6px 12px; font-family: var(--font); font-size: 13px; cursor: pointer; }
398
+ .pg-tabs { display: flex; gap: 4px; margin-bottom: 12px; }
399
+ .pg-tab { background: none; border: 1px solid var(--border); color: var(--text-secondary); font-family: var(--font); font-size: 13px; font-weight: 500; padding: 8px 16px; border-radius: 8px; cursor: pointer; transition: all 0.15s; }
400
+ .pg-tab:hover { color: var(--text); border-color: rgba(255,255,255,0.15); }
401
+ .pg-tab.active { background: var(--accent-dim); color: var(--accent); border-color: var(--accent); }
402
+ .pg-input { display: flex; gap: 8px; margin-bottom: 16px; }
403
+ .pg-input input { flex: 1; background: var(--bg-surface); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; font-family: var(--font); font-size: 14px; outline: none; }
404
+ .pg-input input:focus { border-color: var(--accent); }
405
+ .pg-input input[type=number] { flex: none; text-align: center; }
406
+ .pg-run { background: var(--accent); color: var(--bg); border: none; border-radius: 8px; padding: 10px 20px; font-family: var(--font); font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; transition: opacity 0.15s; }
407
+ .pg-run:hover { opacity: 0.85; }
408
+ .pg-run:disabled { opacity: 0.5; cursor: not-allowed; }
409
+ .pg-results-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
410
+ .pg-result-tabs { display: flex; gap: 4px; }
411
+ .pg-rtab { background: none; border: none; color: var(--text-secondary); font-family: var(--font); font-size: 12px; font-weight: 600; padding: 4px 12px; border-radius: 6px; cursor: pointer; transition: all 0.15s; }
412
+ .pg-rtab.active { background: var(--accent-dim); color: var(--accent); }
413
+ .pg-duration { font-size: 12px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
414
+ .pg-preview { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; min-height: 100px; }
415
+ .pg-json { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; font-family: 'SF Mono','Fira Code',monospace; font-size: 12px; line-height: 1.6; overflow-x: auto; white-space: pre-wrap; word-break: break-all; max-height: 500px; overflow-y: auto; }
416
+
417
+ /* Novel cards in search results */
418
+ .pg-card { display: flex; gap: 14px; padding: 12px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 8px; cursor: pointer; transition: border-color 0.2s, background 0.2s; }
419
+ .pg-card:hover { border-color: rgba(255,255,255,0.15); background: var(--bg-surface-hover); }
420
+ .pg-cover { width: 56px; height: 76px; border-radius: 6px; background: rgba(255,255,255,0.04); flex-shrink: 0; overflow: hidden; }
421
+ .pg-cover img { width: 100%; height: 100%; object-fit: cover; }
422
+ .pg-card-info { flex: 1; min-width: 0; }
423
+ .pg-card-title { font-size: 14px; font-weight: 600; margin-bottom: 2px; }
424
+ .pg-card-author { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; }
425
+ .pg-card-tags { display: flex; flex-wrap: wrap; gap: 4px; }
426
+ .pg-tag { background: var(--accent-dim); color: var(--accent); font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 4px; }
427
+ .pg-card-arrow { color: var(--accent); font-size: 13px; font-weight: 600; align-self: center; flex-shrink: 0; }
428
+ .pg-next-page { display: block; text-align: center; padding: 10px; border: 1px dashed var(--border); border-radius: 8px; color: var(--accent); font-size: 13px; font-weight: 500; cursor: pointer; margin-top: 8px; transition: border-color 0.2s; }
429
+ .pg-next-page:hover { border-color: var(--accent); }
430
+
431
+ /* Novel detail view */
432
+ .pg-detail-header { display: flex; gap: 20px; margin-bottom: 20px; }
433
+ .pg-detail-cover { width: 120px; height: 170px; border-radius: 10px; background: rgba(255,255,255,0.04); flex-shrink: 0; overflow: hidden; }
434
+ .pg-detail-cover img { width: 100%; height: 100%; object-fit: cover; }
435
+ .pg-detail-info h3 { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
436
+ .pg-detail-author { font-size: 13px; color: var(--text-secondary); margin-bottom: 8px; }
437
+ .pg-detail-desc { font-size: 13px; color: var(--text-secondary); line-height: 1.5; margin-bottom: 10px; max-height: 80px; overflow: hidden; }
438
+ .pg-detail-status { display: inline-block; font-size: 11px; font-weight: 600; padding: 3px 8px; border-radius: 4px; background: rgba(255,255,255,0.06); color: var(--text-secondary); text-transform: capitalize; margin-bottom: 8px; }
439
+ .pg-chapter-count { font-size: 13px; color: var(--text-secondary); margin-bottom: 8px; border-top: 1px solid var(--border); padding-top: 12px; }
440
+ .pg-ch-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-radius: 8px; cursor: pointer; font-size: 13px; transition: background 0.15s; }
441
+ .pg-ch-item:hover { background: rgba(255,255,255,0.04); }
442
+ .pg-ch-num { color: var(--text-secondary); font-size: 12px; min-width: 40px; }
443
+ .pg-ch-title { flex: 1; }
444
+ .pg-ch-date { color: var(--text-secondary); font-size: 11px; }
445
+ .pg-ch-list { max-height: 400px; overflow-y: auto; }
446
+
447
+ /* Chapter reader */
448
+ .pg-reader { font-family: Georgia, 'Times New Roman', serif; font-size: 16px; line-height: 1.8; color: var(--text); max-width: 600px; }
449
+ .pg-reader img { max-width: 100%; height: auto; }
450
+
451
+ /* Discover sections */
452
+ .pg-section-card { padding: 14px 16px; border: 1px solid var(--border); border-radius: 10px; margin-bottom: 8px; cursor: pointer; transition: border-color 0.2s; }
453
+ .pg-section-card:hover { border-color: var(--accent); }
454
+ .pg-section-type { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--accent); margin-bottom: 4px; }
455
+ .pg-section-title { font-size: 14px; font-weight: 600; }
456
+ .pg-section-subtitle { font-size: 12px; color: var(--text-secondary); }
457
+
458
+ /* States */
459
+ .pg-loading { text-align: center; padding: 32px; color: var(--text-secondary); font-size: 13px; }
460
+ .pg-error { background: rgba(218,54,51,0.1); border: 1px solid var(--red); border-radius: var(--radius); padding: 14px 18px; font-size: 13px; color: var(--red); margin-top: 12px; }
461
+ .pg-empty { text-align: center; padding: 32px; color: var(--text-secondary); font-size: 13px; }
462
+
463
+ /* JSON syntax coloring */
464
+ .pg-json .key { color: #6ec6ff; }
465
+ .pg-json .str { color: #34c759; }
466
+ .pg-json .num { color: #ef9f27; }
467
+ .pg-json .bool { color: #ef9f27; }
468
+ .pg-json .null { color: #8a8a96; }
469
+ .pg-json details { margin-left: 16px; }
470
+ .pg-json summary { cursor: pointer; color: var(--text-secondary); }
471
+ .pg-json summary:hover { color: var(--text); }
472
+ </style>
473
+ </head>
474
+ <body>
475
+ <div class="container">
476
+ <div class="header">
477
+ <div class="header-logo">
478
+ <svg width="36" height="36" viewBox="0 0 48 48" fill="none">
479
+ <polygon
480
+ points="24,8 38,16 38,32 24,40 10,32 10,16"
481
+ stroke="white"
482
+ stroke-width="2"
483
+ fill="none"
484
+ />
485
+ <polygon
486
+ points="24,14 33,19 33,29 24,34 15,29 15,19"
487
+ stroke="white"
488
+ stroke-width="1"
489
+ fill="none"
490
+ opacity="0.4"
491
+ />
492
+ <line
493
+ x1="24"
494
+ y1="8"
495
+ x2="24"
496
+ y2="14"
497
+ stroke="white"
498
+ stroke-width="1.5"
499
+ />
500
+ <line
501
+ x1="24"
502
+ y1="34"
503
+ x2="24"
504
+ y2="40"
505
+ stroke="white"
506
+ stroke-width="1.5"
507
+ />
508
+ <line
509
+ x1="10"
510
+ y1="16"
511
+ x2="15"
512
+ y2="19"
513
+ stroke="white"
514
+ stroke-width="1.5"
515
+ />
516
+ <line
517
+ x1="38"
518
+ y1="16"
519
+ x2="33"
520
+ y2="19"
521
+ stroke="white"
522
+ stroke-width="1.5"
523
+ />
524
+ <line
525
+ x1="10"
526
+ y1="32"
527
+ x2="15"
528
+ y2="29"
529
+ stroke="white"
530
+ stroke-width="1.5"
531
+ />
532
+ <line
533
+ x1="38"
534
+ y1="32"
535
+ x2="33"
536
+ y2="29"
537
+ stroke="white"
538
+ stroke-width="1.5"
539
+ />
540
+ <circle
541
+ cx="24"
542
+ cy="24"
543
+ r="6"
544
+ stroke="#FAC775"
545
+ stroke-width="0.75"
546
+ fill="none"
547
+ opacity="0.35"
548
+ />
549
+ <circle cx="24" cy="24" r="3.5" fill="#EF9F27" />
550
+ </svg>
551
+ </div>
552
+ <h1 id="repo-name">Glyph Extensions</h1>
553
+ <p id="repo-author" class="author"></p>
554
+ <p id="repo-desc">
555
+ Browse and install novel sources for the Glyph app.
556
+ </p>
557
+ </div>
558
+
559
+ <div style="text-align: center">
560
+ <a class="add-btn" id="add-btn" href="#">
561
+ <svg viewBox="0 0 24 24">
562
+ <path
563
+ d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm1 15h-2v-4H7v-2h4V7h2v4h4v2h-4v4z"
564
+ />
565
+ </svg>
566
+ Add to Glyph
567
+ </a>
568
+ <div class="fallback" id="fallback">
569
+ <strong>Glyph</strong> is not installed on this device.<br />
570
+ Install the app first, then tap the button again.
571
+ </div>
572
+ </div>
573
+
574
+ <div id="sources-section" style="display: none">
575
+ <div class="sources-header">
576
+ <h2>Sources</h2>
577
+ <span class="count" id="source-count"></span>
578
+ </div>
579
+ <ul class="source-list" id="source-list"></ul>
580
+ </div>
581
+
582
+ <div class="playground" id="playground" style="display:none">
583
+ <div class="playground-header">
584
+ <h2>Playground</h2>
585
+ <select id="pg-source" class="pg-select"></select>
586
+ </div>
587
+
588
+ <div class="pg-tabs" id="pg-tabs">
589
+ <button class="pg-tab active" data-method="searchNovels">Search</button>
590
+ <button class="pg-tab" data-method="fetchNovelDetails">Novel Details</button>
591
+ <button class="pg-tab" data-method="fetchChapterContent">Chapter</button>
592
+ <button class="pg-tab" data-method="discover">Discover</button>
593
+ </div>
594
+
595
+ <div class="pg-input" id="pg-input-searchNovels">
596
+ <input type="text" id="pg-search-query" placeholder="Search query..." />
597
+ <input type="number" id="pg-search-page" value="1" min="1" style="width:70px" />
598
+ <button class="pg-run" id="pg-run-search">Run</button>
599
+ </div>
600
+ <div class="pg-input" id="pg-input-fetchNovelDetails" style="display:none">
601
+ <input type="text" id="pg-novel-url" placeholder="Novel URL..." style="flex:1" />
602
+ <button class="pg-run" id="pg-run-novel">Run</button>
603
+ </div>
604
+ <div class="pg-input" id="pg-input-fetchChapterContent" style="display:none">
605
+ <input type="text" id="pg-chapter-url" placeholder="Chapter URL..." style="flex:1" />
606
+ <button class="pg-run" id="pg-run-chapter">Run</button>
607
+ </div>
608
+ <div class="pg-input" id="pg-input-discover" style="display:none">
609
+ <button class="pg-run" id="pg-run-discover" style="width:100%">Load Discover Sections</button>
610
+ </div>
611
+
612
+ <div id="pg-results" style="display:none">
613
+ <div class="pg-results-bar">
614
+ <div class="pg-result-tabs">
615
+ <button class="pg-rtab active" data-view="preview">Preview</button>
616
+ <button class="pg-rtab" data-view="json">JSON</button>
617
+ </div>
618
+ <span class="pg-duration" id="pg-duration"></span>
619
+ </div>
620
+ <div id="pg-preview" class="pg-preview"></div>
621
+ <div id="pg-json" class="pg-json" style="display:none"></div>
622
+ </div>
623
+
624
+ <div id="pg-loading" class="pg-loading" style="display:none">
625
+ <div class="spinner"></div>Running...
626
+ </div>
627
+ <div id="pg-error" class="pg-error" style="display:none"></div>
628
+ </div>
629
+
630
+ <div class="status" id="status">
631
+ <div class="spinner"></div>
632
+ Loading sources...
633
+ </div>
634
+
635
+ <div class="footer">
636
+ Powered by <a href="https://glyph.moe" target="_blank">Glyph</a>
637
+ </div>
638
+ </div>
639
+
640
+ <script>
641
+ (function () {
642
+ "use strict";
643
+
644
+ var jsonUrl = getJsonUrl();
645
+ var deepLink =
646
+ "glyph://add-repo?url=" + encodeURIComponent(jsonUrl);
647
+
648
+ var addBtn = document.getElementById("add-btn");
649
+ addBtn.href = deepLink;
650
+ addBtn.addEventListener("click", function (e) {
651
+ e.preventDefault();
652
+ window.location.href = deepLink;
653
+ setTimeout(function () {
654
+ document
655
+ .getElementById("fallback")
656
+ .classList.add("visible");
657
+ }, 2000);
658
+ });
659
+
660
+ fetch(jsonUrl)
661
+ .then(function (r) {
662
+ if (!r.ok) throw new Error("HTTP " + r.status);
663
+ return r.json();
664
+ })
665
+ .then(function (data) {
666
+ renderRepo(data);
667
+ })
668
+ .catch(function (err) {
669
+ document.getElementById("status").innerHTML =
670
+ '<p style="color:' +
671
+ getComputedStyle(
672
+ document.documentElement,
673
+ ).getPropertyValue("--red") +
674
+ '">Failed to load sources: ' +
675
+ esc(err.message) +
676
+ "</p>";
677
+ });
678
+
679
+ function getJsonUrl() {
680
+ var params = new URLSearchParams(window.location.search);
681
+ if (params.has("url")) {
682
+ var u = params.get("url");
683
+ try {
684
+ var parsed = new URL(u, window.location.href);
685
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") {
686
+ return parsed.href;
687
+ }
688
+ } catch (e) { /* ignore malformed */ }
689
+ }
690
+ var base = window.location.href.replace(/\\/[^/]*$/, "/");
691
+ return base + "dist/index.json";
692
+ }
693
+
694
+ function renderRepo(data) {
695
+ var repoName =
696
+ data.name ||
697
+ (data.repository || "Extensions").split("/").pop() ||
698
+ "Extensions";
699
+ document.getElementById("repo-name").textContent = repoName;
700
+ document.title = repoName + " \u2014 Glyph";
701
+
702
+ if (data.author) {
703
+ document.getElementById("repo-author").textContent =
704
+ "by " + data.author;
705
+ }
706
+ if (data.description) {
707
+ document.getElementById("repo-desc").textContent =
708
+ data.description;
709
+ }
710
+
711
+ var sources = data.sources || [];
712
+ document.getElementById("status").style.display = "none";
713
+ document.getElementById("sources-section").style.display =
714
+ "block";
715
+ document.getElementById("source-count").textContent =
716
+ sources.length +
717
+ " source" +
718
+ (sources.length !== 1 ? "s" : "");
719
+
720
+ var list = document.getElementById("source-list");
721
+ sources.forEach(function (src) {
722
+ var li = document.createElement("li");
723
+ li.className = "source-item";
724
+
725
+ var iconDiv = document.createElement("div");
726
+ iconDiv.className = "source-icon";
727
+ if (src.icon) {
728
+ var img = document.createElement("img");
729
+ img.src = src.icon;
730
+ img.alt = src.name;
731
+ img.onerror = function () {
732
+ this.parentElement.textContent = "\u{1F4D6}";
733
+ };
734
+ iconDiv.appendChild(img);
735
+ } else {
736
+ iconDiv.textContent = "\u{1F4D6}";
737
+ }
738
+
739
+ var info = document.createElement("div");
740
+ info.className = "source-info";
741
+ var nsfwBadge = src.nsfw
742
+ ? '<span class="nsfw-badge">18+</span>'
743
+ : "";
744
+ info.innerHTML =
745
+ '<div class="source-name">' +
746
+ esc(src.name) +
747
+ "</div>" +
748
+ '<div class="source-meta">' +
749
+ '<span class="lang">' +
750
+ esc(src.language || "??") +
751
+ "</span>" +
752
+ nsfwBadge +
753
+ "<span>v" +
754
+ esc(src.version || "?") +
755
+ "</span>" +
756
+ "</div>";
757
+
758
+ li.appendChild(iconDiv);
759
+ li.appendChild(info);
760
+ list.appendChild(li);
761
+ });
762
+
763
+ initPlayground(sources);
764
+ }
765
+
766
+ function esc(s) {
767
+ var d = document.createElement("div");
768
+ d.textContent = s;
769
+ return d.innerHTML;
770
+ }
771
+ })();
772
+ </script>
773
+
774
+ <script>
775
+ (function () {
776
+ "use strict";
777
+
778
+ var pgSources = [];
779
+ var currentSource = "";
780
+ var currentMethod = "searchNovels";
781
+ var currentPage = 1;
782
+ var lastSearchQuery = "";
783
+ var lastResult = null;
784
+
785
+ window.initPlayground = function (sources) {
786
+ pgSources = sources;
787
+ if (!sources || sources.length === 0) return;
788
+
789
+ // Only show playground on local dev server (not GitHub Pages)
790
+ var host = window.location.hostname;
791
+ var isLocal = host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]"
792
+ || host.endsWith(".local")
793
+ || host.startsWith("192.168.") || host.startsWith("10.")
794
+ || /^172\\.(1[6-9]|2\\d|3[01])\\./.test(host)
795
+ || /^100\\.(6[4-9]|[7-9]\\d|1[01]\\d|12[0-7])\\./.test(host)
796
+ || host.startsWith("fe80:") || host.startsWith("[fe80:");
797
+ if (!isLocal) return;
798
+
799
+ var sel = document.getElementById("pg-source");
800
+ sources.forEach(function (src) {
801
+ var opt = document.createElement("option");
802
+ opt.value = src.id;
803
+ opt.textContent = src.name;
804
+ sel.appendChild(opt);
805
+ });
806
+ currentSource = sources[0].id;
807
+ sel.addEventListener("change", function () {
808
+ currentSource = this.value;
809
+ });
810
+
811
+ document.getElementById("playground").style.display = "block";
812
+
813
+ var tabs = document.querySelectorAll("#pg-tabs .pg-tab");
814
+ for (var i = 0; i < tabs.length; i++) {
815
+ tabs[i].addEventListener("click", function () {
816
+ switchTab(this.getAttribute("data-method"));
817
+ });
818
+ }
819
+
820
+ var rtabs = document.querySelectorAll(".pg-rtab");
821
+ for (var j = 0; j < rtabs.length; j++) {
822
+ rtabs[j].addEventListener("click", function () {
823
+ var view = this.getAttribute("data-view");
824
+ for (var k = 0; k < rtabs.length; k++) {
825
+ rtabs[k].classList.remove("active");
826
+ }
827
+ this.classList.add("active");
828
+ document.getElementById("pg-preview").style.display = view === "preview" ? "block" : "none";
829
+ document.getElementById("pg-json").style.display = view === "json" ? "block" : "none";
830
+ });
831
+ }
832
+
833
+ document.getElementById("pg-run-search").addEventListener("click", runSearch);
834
+ document.getElementById("pg-run-novel").addEventListener("click", runNovel);
835
+ document.getElementById("pg-run-chapter").addEventListener("click", runChapter);
836
+ document.getElementById("pg-run-discover").addEventListener("click", runDiscover);
837
+
838
+ document.getElementById("pg-search-query").addEventListener("keydown", function (e) {
839
+ if (e.key === "Enter") runSearch();
840
+ });
841
+ document.getElementById("pg-search-page").addEventListener("keydown", function (e) {
842
+ if (e.key === "Enter") runSearch();
843
+ });
844
+ document.getElementById("pg-novel-url").addEventListener("keydown", function (e) {
845
+ if (e.key === "Enter") runNovel();
846
+ });
847
+ document.getElementById("pg-chapter-url").addEventListener("keydown", function (e) {
848
+ if (e.key === "Enter") runChapter();
849
+ });
850
+ };
851
+
852
+ function runSearch() {
853
+ var query = document.getElementById("pg-search-query").value.trim();
854
+ var page = parseInt(document.getElementById("pg-search-page").value) || 1;
855
+ if (!query) return;
856
+ lastSearchQuery = query;
857
+ currentPage = page;
858
+ pgCall(currentSource, "searchNovels", [query, page]);
859
+ }
860
+
861
+ function runNovel() {
862
+ var url = document.getElementById("pg-novel-url").value.trim();
863
+ if (!url) return;
864
+ pgCall(currentSource, "fetchNovelDetails", [url]);
865
+ }
866
+
867
+ function runChapter() {
868
+ var url = document.getElementById("pg-chapter-url").value.trim();
869
+ if (!url) return;
870
+ pgCall(currentSource, "fetchChapterContent", [url]);
871
+ }
872
+
873
+ function runDiscover() {
874
+ pgCall(currentSource, "getDiscoverSections", []);
875
+ }
876
+
877
+ function switchTab(method) {
878
+ currentMethod = method;
879
+ var tabs = document.querySelectorAll("#pg-tabs .pg-tab");
880
+ for (var i = 0; i < tabs.length; i++) {
881
+ var m = tabs[i].getAttribute("data-method");
882
+ if (m === method) {
883
+ tabs[i].classList.add("active");
884
+ } else {
885
+ tabs[i].classList.remove("active");
886
+ }
887
+ }
888
+ var methods = ["searchNovels", "fetchNovelDetails", "fetchChapterContent", "discover"];
889
+ for (var j = 0; j < methods.length; j++) {
890
+ var el = document.getElementById("pg-input-" + methods[j]);
891
+ if (el) el.style.display = methods[j] === method ? "flex" : "none";
892
+ }
893
+ hideResults();
894
+ hideError();
895
+ }
896
+
897
+ function pgCall(sourceId, method, args) {
898
+ showLoading();
899
+ hideError();
900
+ hideResults();
901
+ fetch("/api/call", {
902
+ method: "POST",
903
+ headers: { "Content-Type": "application/json" },
904
+ body: JSON.stringify({ sourceId: sourceId, method: method, args: args }),
905
+ })
906
+ .then(function (resp) { return resp.json(); })
907
+ .then(function (data) {
908
+ hideLoading();
909
+ if (!data.ok) {
910
+ showError(data.error || "Unknown error");
911
+ return;
912
+ }
913
+ lastResult = data.result;
914
+ showResults(data.result, data.duration, method);
915
+ })
916
+ .catch(function (err) {
917
+ hideLoading();
918
+ showError(err.message);
919
+ });
920
+ }
921
+
922
+ function showLoading() {
923
+ document.getElementById("pg-loading").style.display = "block";
924
+ }
925
+ function hideLoading() {
926
+ document.getElementById("pg-loading").style.display = "none";
927
+ }
928
+ function showError(msg) {
929
+ var el = document.getElementById("pg-error");
930
+ el.textContent = msg;
931
+ el.style.display = "block";
932
+ }
933
+ function hideError() {
934
+ document.getElementById("pg-error").style.display = "none";
935
+ }
936
+ function hideResults() {
937
+ document.getElementById("pg-results").style.display = "none";
938
+ }
939
+
940
+ function showResults(result, duration, method) {
941
+ var resultsEl = document.getElementById("pg-results");
942
+ resultsEl.style.display = "block";
943
+ document.getElementById("pg-duration").textContent = duration ? duration + "ms" : "";
944
+
945
+ var rtabs = document.querySelectorAll(".pg-rtab");
946
+ for (var i = 0; i < rtabs.length; i++) {
947
+ rtabs[i].classList.toggle("active", rtabs[i].getAttribute("data-view") === "preview");
948
+ }
949
+ document.getElementById("pg-preview").style.display = "block";
950
+ document.getElementById("pg-json").style.display = "none";
951
+
952
+ var previewEl = document.getElementById("pg-preview");
953
+ if (method === "searchNovels") {
954
+ previewEl.innerHTML = renderSearchPreview(result);
955
+ } else if (method === "fetchNovelDetails") {
956
+ previewEl.innerHTML = renderNovelPreview(result);
957
+ } else if (method === "fetchChapterContent") {
958
+ previewEl.innerHTML = renderChapterPreview(result);
959
+ } else if (method === "getDiscoverSections") {
960
+ previewEl.innerHTML = renderDiscoverPreview(result);
961
+ } else if (method === "getDiscoverSectionItems") {
962
+ previewEl.innerHTML = renderSearchPreview(result);
963
+ } else {
964
+ previewEl.innerHTML = '<div class="pg-empty">No preview available</div>';
965
+ }
966
+
967
+ document.getElementById("pg-json").innerHTML = renderJson(result);
968
+ }
969
+
970
+ function renderSearchPreview(result) {
971
+ var html = "";
972
+ var items = result.items || [];
973
+ if (Array.isArray(result)) {
974
+ items = result;
975
+ }
976
+ items.forEach(function (item) {
977
+ // Normalize fields: Discover items use novelUrl/imageUrl, Search items use url/cover
978
+ var itemUrl = item.url || item.novelUrl || item.id || "";
979
+ var itemCover = item.cover || item.imageUrl || "";
980
+ var itemTitle = item.title || item.name || "";
981
+ var itemAuthor = item.author || item.subtitle || "";
982
+ html += '<div class="pg-card" data-nav="fetchNovelDetails" data-url="' + escAttr(itemUrl) + '">';
983
+ html += '<div class="pg-cover">' + (itemCover ? '<img src="/api/image-proxy?url=' + encodeURIComponent(itemCover) + '" onerror="this.style.display=&#39;none&#39;">' : '') + '</div>';
984
+ html += '<div class="pg-card-info">';
985
+ html += '<div class="pg-card-title">' + esc(itemTitle) + '</div>';
986
+ html += '<div class="pg-card-author">' + (itemAuthor ? esc(itemAuthor) : '') + '</div>';
987
+ html += '<div class="pg-card-tags">' + (item.tags || []).map(function (t) { return '<span class="pg-tag">' + esc(t) + '</span>'; }).join('') + '</div>';
988
+ html += '</div>';
989
+ html += '<span class="pg-card-arrow">&rarr;</span>';
990
+ html += '</div>';
991
+ });
992
+ if (result.hasNextPage) {
993
+ html += '<div class="pg-next-page" data-nextpage="true">Load page ' + (currentPage + 1) + ' &rarr;</div>';
994
+ }
995
+ if (!items || items.length === 0) {
996
+ html += '<div class="pg-empty">No results</div>';
997
+ }
998
+ return html;
999
+ }
1000
+
1001
+ function renderNovelPreview(result) {
1002
+ var html = '<div class="pg-detail-header">';
1003
+ html += '<div class="pg-detail-cover">' + (result.cover ? '<img src="/api/image-proxy?url=' + encodeURIComponent(result.cover) + '" onerror="this.style.display=&#39;none&#39;">' : '') + '</div>';
1004
+ html += '<div class="pg-detail-info">';
1005
+ html += '<h3>' + esc(result.title || "") + '</h3>';
1006
+ html += '<div class="pg-detail-author">' + (result.author ? 'by ' + esc(result.author) : '') + '</div>';
1007
+ if (result.status) {
1008
+ html += '<span class="pg-detail-status">' + esc(result.status) + '</span>';
1009
+ }
1010
+ if (result.description) {
1011
+ html += '<div class="pg-detail-desc">' + esc(result.description) + '</div>';
1012
+ }
1013
+ if (result.tags && result.tags.length) {
1014
+ html += '<div class="pg-card-tags">' + result.tags.map(function (t) { return '<span class="pg-tag">' + esc(t) + '</span>'; }).join('') + '</div>';
1015
+ }
1016
+ html += '</div></div>';
1017
+
1018
+ var chapters = result.chapters || [];
1019
+ if (chapters.length > 0) {
1020
+ html += '<div class="pg-chapter-count">' + chapters.length + ' chapter' + (chapters.length !== 1 ? 's' : '') + '</div>';
1021
+ html += '<div class="pg-ch-list">';
1022
+ chapters.forEach(function (ch, idx) {
1023
+ html += '<div class="pg-ch-item" data-nav="fetchChapterContent" data-url="' + escAttr(ch.url || ch.id || "") + '">';
1024
+ html += '<span class="pg-ch-num">' + (idx + 1) + '</span>';
1025
+ html += '<span class="pg-ch-title">' + esc(ch.title || 'Chapter ' + (idx + 1)) + '</span>';
1026
+ if (ch.date) {
1027
+ html += '<span class="pg-ch-date">' + esc(ch.date) + '</span>';
1028
+ }
1029
+ html += '</div>';
1030
+ });
1031
+ html += '</div>';
1032
+ }
1033
+ return html;
1034
+ }
1035
+
1036
+ function renderChapterPreview(result) {
1037
+ var content = result.content || result.html || result.text || "";
1038
+ if (typeof result === "string") content = result;
1039
+ var srcdoc = '<!DOCTYPE html><html><head><style>body{font-family:Georgia,serif;font-size:16px;line-height:1.8;color:#e8e6e3;background:#16161d;padding:16px;margin:0;}img{max-width:100%;height:auto;}</style></head><body>' + content + '</body></html>';
1040
+ var iframe = document.createElement("iframe");
1041
+ iframe.sandbox = "";
1042
+ iframe.setAttribute("srcdoc", srcdoc);
1043
+ iframe.style.cssText = "width:100%;min-height:400px;border:none;border-radius:8px;background:var(--bg-surface);";
1044
+ var wrapper = document.createElement("div");
1045
+ wrapper.appendChild(iframe);
1046
+ return wrapper.innerHTML;
1047
+ }
1048
+
1049
+ function renderDiscoverPreview(result) {
1050
+ var sections = Array.isArray(result) ? result : (result.sections || []);
1051
+ if (sections.length === 0) {
1052
+ return '<div class="pg-empty">No discover sections</div>';
1053
+ }
1054
+ var html = "";
1055
+ sections.forEach(function (sec) {
1056
+ html += '<div class="pg-section-card" data-discover="' + escAttr(sec.id || "") + '">';
1057
+ if (sec.type) {
1058
+ html += '<div class="pg-section-type">' + esc(sec.type) + '</div>';
1059
+ }
1060
+ html += '<div class="pg-section-title">' + esc(sec.title || "Untitled") + '</div>';
1061
+ if (sec.subtitle) {
1062
+ html += '<div class="pg-section-subtitle">' + esc(sec.subtitle) + '</div>';
1063
+ }
1064
+ html += '</div>';
1065
+ });
1066
+ return html;
1067
+ }
1068
+
1069
+ function renderJson(obj) {
1070
+ return '<pre>' + syntaxHighlight(JSON.stringify(obj, null, 2)) + '</pre>';
1071
+ }
1072
+
1073
+ function syntaxHighlight(json) {
1074
+ if (!json) return "";
1075
+ json = json.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1076
+ return json.replace(/("(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\"])*"(\\s*:)?|\b(true|false|null)\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g, function (match) {
1077
+ var cls = "num";
1078
+ if (/^"/.test(match)) {
1079
+ if (/:$/.test(match)) {
1080
+ cls = "key";
1081
+ } else {
1082
+ cls = "str";
1083
+ }
1084
+ } else if (/true|false/.test(match)) {
1085
+ cls = "bool";
1086
+ } else if (/null/.test(match)) {
1087
+ cls = "null";
1088
+ }
1089
+ return '<span class="' + cls + '">' + match + '</span>';
1090
+ });
1091
+ }
1092
+
1093
+ function esc(s) {
1094
+ if (s == null) return "";
1095
+ var d = document.createElement("div");
1096
+ d.textContent = String(s);
1097
+ return d.innerHTML;
1098
+ }
1099
+
1100
+ function escAttr(s) {
1101
+ return esc(s).replace(/'/g, "&#39;").replace(/"/g, "&quot;");
1102
+ }
1103
+
1104
+ function pgNav(method, value) {
1105
+ switchTab(method);
1106
+ if (method === "fetchNovelDetails") {
1107
+ document.getElementById("pg-novel-url").value = value;
1108
+ }
1109
+ if (method === "fetchChapterContent") {
1110
+ document.getElementById("pg-chapter-url").value = value;
1111
+ }
1112
+ pgCall(currentSource, method, [value]);
1113
+ }
1114
+
1115
+ function pgNextPage() {
1116
+ currentPage = currentPage + 1;
1117
+ document.getElementById("pg-search-page").value = currentPage;
1118
+ pgCall(currentSource, "searchNovels", [lastSearchQuery, currentPage]);
1119
+ }
1120
+
1121
+ function pgDiscoverSection(sectionId) {
1122
+ pgCall(currentSource, "getDiscoverSectionItems", [sectionId, 1]);
1123
+ }
1124
+
1125
+ document.getElementById("pg-preview").addEventListener("click", function(e) {
1126
+ var target = e.target;
1127
+ while (target && target !== this) {
1128
+ if (target.getAttribute("data-nav")) {
1129
+ pgNav(target.getAttribute("data-nav"), target.getAttribute("data-url"));
1130
+ return;
1131
+ }
1132
+ if (target.getAttribute("data-discover")) {
1133
+ pgDiscoverSection(target.getAttribute("data-discover"));
1134
+ return;
1135
+ }
1136
+ if (target.getAttribute("data-nextpage")) {
1137
+ pgNextPage();
1138
+ return;
1139
+ }
1140
+ target = target.parentElement;
1141
+ }
1142
+ });
1143
+ })();
1144
+ </script>
1145
+ </body>
1146
+ </html>
1147
+ `;
1148
+ }
1149
+
1150
+ // src/templates/source-package-json.ts
1151
+ function sourcePackageJson(ctx) {
1152
+ return JSON.stringify(
1153
+ {
1154
+ name: `glyph-source-${ctx.sourceId}`,
1155
+ version: "1.0.0",
1156
+ private: true
1157
+ },
1158
+ null,
1159
+ 2
1160
+ ) + "\n";
1161
+ }
1162
+
1163
+ // src/templates/source-tsconfig-json.ts
1164
+ function sourceTsconfigJson() {
1165
+ return JSON.stringify(
1166
+ {
1167
+ extends: "../../tsconfig.json",
1168
+ compilerOptions: {
1169
+ rootDir: "src",
1170
+ outDir: "../../dist"
1171
+ },
1172
+ include: ["src/**/*"],
1173
+ exclude: ["src/**/*.test.ts"]
1174
+ },
1175
+ null,
1176
+ 2
1177
+ ) + "\n";
1178
+ }
1179
+
1180
+ // src/templates/main-ts.ts
1181
+ function mainTs(ctx) {
1182
+ return `import { createSource, get, buildUrl, RateLimit } from '@glyphmoe/sdk'
1183
+ import { Parser } from './parser'
1184
+
1185
+ const BASE = 'https://example.com' // TODO: replace with your site URL
1186
+ const parser = new Parser(BASE)
1187
+
1188
+ export default createSource({
1189
+ id: ${JSON.stringify(ctx.sourceId)},
1190
+ name: ${JSON.stringify(ctx.sourceName)},
1191
+ version: '1.0.0',
1192
+ baseUrl: BASE,
1193
+ icon: \`\${BASE}/favicon.ico\`,
1194
+ language: ${JSON.stringify(ctx.language)},
1195
+ dev: ${JSON.stringify(ctx.author)},
1196
+ rateLimit: RateLimit.balanced,
1197
+
1198
+ async searchNovels(query, page) {
1199
+ // TODO: implement search
1200
+ const html = await get(buildUrl(BASE, '/search', { q: query, page }))
1201
+ return parser.parseSearchResults(html)
1202
+ },
1203
+
1204
+ async fetchNovelDetails(novelUrl) {
1205
+ // TODO: implement novel details
1206
+ const html = await get(novelUrl)
1207
+ return parser.parseNovelDetails(html, novelUrl)
1208
+ },
1209
+
1210
+ async fetchChapterContent(chapterUrl) {
1211
+ // TODO: implement chapter content
1212
+ const html = await get(chapterUrl)
1213
+ return parser.parseChapterContent(html)
1214
+ },
1215
+ })
1216
+ `;
1217
+ }
1218
+
1219
+ // src/templates/parser-ts.ts
1220
+ function parserTs() {
1221
+ return `import { load } from '@glyphmoe/sdk'
1222
+ import type { Novel, Chapter, PagedResults } from '@glyphmoe/sdk'
1223
+
1224
+ export class Parser {
1225
+ constructor(private baseUrl: string) {}
1226
+
1227
+ parseSearchResults(html: string): PagedResults<Novel> {
1228
+ const $ = load(html)
1229
+ // TODO: implement
1230
+ return { items: [], hasNextPage: false }
1231
+ }
1232
+
1233
+ parseNovelDetails(html: string, novelUrl: string): Novel & { chapters: Chapter[] } {
1234
+ const $ = load(html)
1235
+ // TODO: implement
1236
+ return { id: novelUrl, title: '', url: novelUrl, chapters: [] }
1237
+ }
1238
+
1239
+ parseChapterContent(html: string): string {
1240
+ const $ = load(html)
1241
+ // TODO: implement
1242
+ return $('body').html() ?? ''
1243
+ }
1244
+ }
1245
+ `;
1246
+ }
1247
+
1248
+ // src/templates/test-ts.ts
1249
+ function testTs(ctx) {
1250
+ return `import { describe, it, expect, beforeEach } from 'vitest'
1251
+ import { clearMocks } from '@glyphmoe/sdk/testing'
1252
+
1253
+ describe(${JSON.stringify(ctx.sourceId)}, () => {
1254
+ beforeEach(() => clearMocks())
1255
+
1256
+ it('should parse search results', async () => {
1257
+ // TODO: add mock HTML and test parser
1258
+ })
1259
+ })
1260
+ `;
1261
+ }
1262
+
1263
+ // src/index.ts
1264
+ async function main() {
1265
+ const argName = process.argv[2];
1266
+ if (argName === "--version" || argName === "-v") {
1267
+ console.log(`create-glyph-extension ${"1.0.0"}`);
1268
+ process.exit(0);
1269
+ }
1270
+ if (argName === "--help" || argName === "-h") {
1271
+ console.log("Usage: npx create-glyph-extension [project-name]");
1272
+ console.log("\nScaffolds a new Glyph extension project with SDK, build scripts, and an example source.");
1273
+ process.exit(0);
1274
+ }
1275
+ let gitUser = "";
1276
+ try {
1277
+ gitUser = execSync("git config user.name", { encoding: "utf-8" }).trim();
1278
+ } catch {
1279
+ }
1280
+ const response = await prompts(
1281
+ [
1282
+ {
1283
+ type: "text",
1284
+ name: "projectName",
1285
+ message: "Project name",
1286
+ initial: argName || "my-glyph-extensions",
1287
+ validate: (v) => {
1288
+ const trimmed = v.trim();
1289
+ if (!trimmed) return "Project name is required";
1290
+ if (trimmed !== basename(trimmed)) return "Project name cannot contain path separators";
1291
+ if (trimmed === "." || trimmed === "..") return "Invalid project name";
1292
+ if (!/^[a-zA-Z0-9._-]+$/.test(trimmed)) return "Project name can only contain letters, numbers, dots, hyphens, and underscores";
1293
+ return true;
1294
+ }
1295
+ },
1296
+ {
1297
+ type: "text",
1298
+ name: "sourceId",
1299
+ message: 'First source ID (lowercase, e.g. "royalroad")',
1300
+ validate: (v) => /^[a-z][a-z0-9-]*$/.test(v.trim()) ? true : "Must start with a letter, lowercase alphanumeric and hyphens only"
1301
+ },
1302
+ {
1303
+ type: "text",
1304
+ name: "sourceName",
1305
+ message: 'Source display name (e.g. "Royal Road")',
1306
+ validate: (v) => v.trim() ? true : "Display name is required"
1307
+ },
1308
+ {
1309
+ type: "text",
1310
+ name: "language",
1311
+ message: "Language code",
1312
+ initial: "en"
1313
+ },
1314
+ {
1315
+ type: "text",
1316
+ name: "author",
1317
+ message: "Author",
1318
+ initial: gitUser
1319
+ },
1320
+ {
1321
+ type: "text",
1322
+ name: "repoUrl",
1323
+ message: "GitHub repo URL (optional)",
1324
+ initial: ""
1325
+ }
1326
+ ],
1327
+ {
1328
+ onCancel: () => {
1329
+ console.log("\nAborted.");
1330
+ process.exit(130);
1331
+ }
1332
+ }
1333
+ );
1334
+ if (!response.projectName || !response.sourceId || !response.sourceName) {
1335
+ console.log("\nAborted.");
1336
+ process.exit(1);
1337
+ }
1338
+ const ctx = {
1339
+ projectName: response.projectName.trim(),
1340
+ sourceId: response.sourceId.trim(),
1341
+ sourceName: response.sourceName.trim(),
1342
+ language: (response.language || "en").trim(),
1343
+ author: (response.author || "").trim(),
1344
+ repoUrl: (response.repoUrl || "").trim()
1345
+ };
1346
+ const projectDir = resolve(process.cwd(), ctx.projectName);
1347
+ console.log(`
1348
+ Creating ${pc.bold(ctx.projectName)}...
1349
+ `);
1350
+ if (existsSync(projectDir)) {
1351
+ console.error(pc.red(`Directory "${ctx.projectName}" already exists.`));
1352
+ process.exit(1);
1353
+ }
1354
+ const sourceDir = join(projectDir, "sources", ctx.sourceId);
1355
+ const srcDir = join(sourceDir, "src");
1356
+ const staticDir = join(sourceDir, "static");
1357
+ mkdirSync(srcDir, { recursive: true });
1358
+ mkdirSync(staticDir, { recursive: true });
1359
+ const files = [
1360
+ [join(projectDir, "package.json"), packageJson(ctx)],
1361
+ [join(projectDir, "repo.json"), repoJson(ctx)],
1362
+ [join(projectDir, "tsconfig.json"), tsconfigJson()],
1363
+ [join(projectDir, "vitest.config.ts"), vitestConfig()],
1364
+ [join(projectDir, ".gitignore"), gitignore()],
1365
+ [join(projectDir, ".prettierrc"), prettierrc()],
1366
+ [join(projectDir, "index.html"), indexHtml()],
1367
+ // Source files
1368
+ [join(sourceDir, "package.json"), sourcePackageJson(ctx)],
1369
+ [join(sourceDir, "tsconfig.json"), sourceTsconfigJson()],
1370
+ [join(srcDir, "main.ts"), mainTs(ctx)],
1371
+ [join(srcDir, "parser.ts"), parserTs()],
1372
+ [join(srcDir, `${ctx.sourceId}.test.ts`), testTs(ctx)],
1373
+ // Static dir gitkeep
1374
+ [join(staticDir, ".gitkeep"), ""]
1375
+ ];
1376
+ for (const [filePath, content] of files) {
1377
+ writeFileSync(filePath, content, "utf-8");
1378
+ }
1379
+ console.log(` ${pc.green("\u2713")} Created ${files.length} files`);
1380
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
1381
+ try {
1382
+ await new Promise((resolvePromise, reject) => {
1383
+ const child = spawn(npmCmd, ["install"], {
1384
+ cwd: projectDir,
1385
+ stdio: "inherit"
1386
+ });
1387
+ child.on("close", (code) => {
1388
+ if (code === 0) resolvePromise();
1389
+ else reject(new Error(`npm install exited with code ${code}`));
1390
+ });
1391
+ child.on("error", reject);
1392
+ });
1393
+ console.log(` ${pc.green("\u2713")} Installed dependencies`);
1394
+ } catch {
1395
+ console.log();
1396
+ console.log(pc.yellow(" Warning: npm install failed. You can retry manually:"));
1397
+ console.log(` cd ${ctx.projectName} && npm install`);
1398
+ }
1399
+ console.log();
1400
+ console.log(`Next steps:`);
1401
+ console.log(` ${pc.cyan(`cd ${ctx.projectName}`)}`);
1402
+ console.log(` ${pc.cyan("npm run dev")}`);
1403
+ console.log();
1404
+ }
1405
+ main().catch((err) => {
1406
+ console.error(pc.red(err.message));
1407
+ process.exit(1);
1408
+ });