@stephenyang/stephen 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,4239 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { mkdirSync as mkdirSync2, realpathSync } from "fs";
5
+ import { dirname as dirname2 } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { createInterface } from "readline/promises";
8
+ import { stdin as input, stdout as output } from "process";
9
+ import envPaths from "env-paths";
10
+
11
+ // src/ak/command.ts
12
+ import { Command } from "commander";
13
+ import { ZodError, z as z8 } from "zod";
14
+ import { table as table7 } from "table";
15
+
16
+ // src/ak/output.ts
17
+ import { table } from "table";
18
+ function renderAkRecordsAsJson(records, limit) {
19
+ const serializedRecords = records.map((record) => ({
20
+ id: record.id,
21
+ env: record.env,
22
+ userId: record.userId,
23
+ userName: record.userName,
24
+ email: record.email,
25
+ phone: record.phone,
26
+ key: record.key,
27
+ createdAt: record.createdAt,
28
+ updatedAt: record.updatedAt
29
+ }));
30
+ return JSON.stringify(
31
+ {
32
+ ok: true,
33
+ data: serializedRecords,
34
+ meta: {
35
+ count: serializedRecords.length,
36
+ limit
37
+ }
38
+ },
39
+ null,
40
+ 2
41
+ );
42
+ }
43
+ function renderAkErrorAsJson(code, message, details) {
44
+ return JSON.stringify(
45
+ {
46
+ ok: false,
47
+ error: {
48
+ code,
49
+ ...details === void 0 ? {} : { details },
50
+ message
51
+ }
52
+ },
53
+ null,
54
+ 2
55
+ );
56
+ }
57
+ function renderAkRecordsAsTable(records) {
58
+ return table([
59
+ ["id", "env", "userId", "userName", "email", "phone", "key", "updatedAt"],
60
+ ...records.map((record) => [
61
+ record.id,
62
+ record.env,
63
+ record.userId ?? "",
64
+ record.userName ?? "",
65
+ record.email ?? "",
66
+ record.phone ?? "",
67
+ record.key,
68
+ record.updatedAt
69
+ ])
70
+ ]);
71
+ }
72
+
73
+ // src/ak/crypto.ts
74
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
75
+ var ENCRYPTION_ALGORITHM = "aes-256-gcm";
76
+ var IV_LENGTH = 12;
77
+ var AUTH_TAG_LENGTH = 16;
78
+ var PREFIX_LENGTH = 12;
79
+ function createAkId(key) {
80
+ return createHash("sha1").update(key).digest("hex");
81
+ }
82
+ function deriveAkSearchPrefix(key, length = PREFIX_LENGTH) {
83
+ return key.slice(0, length);
84
+ }
85
+ function encryptAkKey(masterKey, key) {
86
+ const iv = randomBytes(IV_LENGTH);
87
+ const cipher = createCipheriv(ENCRYPTION_ALGORITHM, masterKey, iv, {
88
+ authTagLength: AUTH_TAG_LENGTH
89
+ });
90
+ const encrypted = Buffer.concat([cipher.update(key, "utf8"), cipher.final()]);
91
+ const tag = cipher.getAuthTag();
92
+ return [iv.toString("base64url"), tag.toString("base64url"), encrypted.toString("base64url")].join(".");
93
+ }
94
+ function decryptAkKey(masterKey, payload) {
95
+ const [ivPart, tagPart, encryptedPart] = payload.split(".");
96
+ if (!ivPart || !tagPart || !encryptedPart) {
97
+ throw new Error("Invalid encrypted API key payload.");
98
+ }
99
+ const decipher = createDecipheriv(
100
+ ENCRYPTION_ALGORITHM,
101
+ masterKey,
102
+ Buffer.from(ivPart, "base64url"),
103
+ {
104
+ authTagLength: AUTH_TAG_LENGTH
105
+ }
106
+ );
107
+ decipher.setAuthTag(Buffer.from(tagPart, "base64url"));
108
+ return Buffer.concat([
109
+ decipher.update(Buffer.from(encryptedPart, "base64url")),
110
+ decipher.final()
111
+ ]).toString("utf8");
112
+ }
113
+
114
+ // src/ak/schema.ts
115
+ import { z } from "zod";
116
+
117
+ // src/ak/types.ts
118
+ var AK_RECOMMENDED_ENVS = [
119
+ "bzy-pre",
120
+ "bzy-prod",
121
+ "op-pre",
122
+ "op-prod",
123
+ "gitee",
124
+ "github",
125
+ "gitlab"
126
+ ];
127
+ var AK_QUERY_FIELDS = [
128
+ "userId",
129
+ "userName",
130
+ "email",
131
+ "phone",
132
+ "key"
133
+ ];
134
+
135
+ // src/ak/schema.ts
136
+ var envRecommendationMessage = `Recommended values: ${AK_RECOMMENDED_ENVS.join(", ")}.`;
137
+ var akEnvSchema = z.string().transform((value, context) => {
138
+ try {
139
+ return normalizeAkEnv(value);
140
+ } catch (error) {
141
+ context.addIssue({
142
+ code: "custom",
143
+ message: error.message
144
+ });
145
+ return z.NEVER;
146
+ }
147
+ });
148
+ function normalizeAkKey(value) {
149
+ const normalized = value.trim();
150
+ if (normalized.length === 0) {
151
+ throw new Error("API key cannot be empty.");
152
+ }
153
+ return normalized;
154
+ }
155
+ function normalizeAkEnv(value) {
156
+ const normalized = value.trim();
157
+ if (normalized.length === 0) {
158
+ throw new Error(`Environment is required. ${envRecommendationMessage}`);
159
+ }
160
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(normalized)) {
161
+ throw new Error(
162
+ `Environment must use letters, numbers, ".", "_" or "-". ${envRecommendationMessage}`
163
+ );
164
+ }
165
+ return normalized;
166
+ }
167
+ function maskKey(value) {
168
+ if (value.length <= 4) {
169
+ return "*".repeat(value.length);
170
+ }
171
+ if (value.length <= 8) {
172
+ return `${value.slice(0, 2)}${"*".repeat(value.length - 4)}${value.slice(-2)}`;
173
+ }
174
+ return `${value.slice(0, 4)}${"*".repeat(value.length - 8)}${value.slice(-4)}`;
175
+ }
176
+ var nullableTextField = z.string().trim().min(1).optional().transform((value) => value ?? void 0);
177
+ var addAkRecordInputSchema = z.object({
178
+ email: nullableTextField,
179
+ env: akEnvSchema,
180
+ key: z.string().transform(normalizeAkKey),
181
+ phone: nullableTextField,
182
+ userId: nullableTextField,
183
+ userName: nullableTextField
184
+ });
185
+ var listAkRecordsInputSchema = z.object({
186
+ env: akEnvSchema.optional(),
187
+ field: z.string().trim().optional(),
188
+ limit: z.number().int().positive().max(100).default(50),
189
+ query: z.string().trim().min(1).optional()
190
+ });
191
+ function parseAkQueryFields(value) {
192
+ if (!value) {
193
+ return [...AK_QUERY_FIELDS];
194
+ }
195
+ return value.split(",").map((field) => {
196
+ const normalized = field.trim();
197
+ if (!AK_QUERY_FIELDS.includes(normalized)) {
198
+ throw new Error(`Unsupported query field: ${normalized}.`);
199
+ }
200
+ return normalized;
201
+ });
202
+ }
203
+
204
+ // src/ak/service.ts
205
+ var AkServiceError = class extends Error {
206
+ code;
207
+ exitCode;
208
+ constructor(code, message, exitCode) {
209
+ super(message);
210
+ this.code = code;
211
+ this.exitCode = exitCode;
212
+ this.name = "AkServiceError";
213
+ }
214
+ };
215
+ var AkService = class {
216
+ #masterKey;
217
+ #now;
218
+ #repository;
219
+ constructor(dependencies) {
220
+ this.#masterKey = dependencies.masterKey;
221
+ this.#now = dependencies.now;
222
+ this.#repository = dependencies.repository;
223
+ }
224
+ add(input2) {
225
+ const parsed = addAkRecordInputSchema.parse(input2);
226
+ const key = normalizeAkKey(parsed.key);
227
+ const id = createAkId(key);
228
+ const now = this.#now();
229
+ this.#repository.insert({
230
+ createdAt: now,
231
+ email: normalizeNullableFieldForInsert(parsed.email),
232
+ env: parsed.env,
233
+ id,
234
+ keyCiphertext: encryptAkKey(this.#masterKey, key),
235
+ keySearchPrefix: deriveAkSearchPrefix(key),
236
+ phone: normalizeNullableFieldForInsert(parsed.phone),
237
+ updatedAt: now,
238
+ userId: normalizeNullableFieldForInsert(parsed.userId),
239
+ userName: normalizeNullableFieldForInsert(parsed.userName)
240
+ });
241
+ const record = this.#repository.getById(id);
242
+ if (!record) {
243
+ throw new AkServiceError("STORAGE_ERROR", "Failed to load the newly created record.", 6);
244
+ }
245
+ return this.#toView(record, Boolean(input2.rawKey));
246
+ }
247
+ get(input2) {
248
+ const record = this.#findRecord(input2);
249
+ if (!record) {
250
+ throw new AkServiceError("RECORD_NOT_FOUND", "No API key record matched the query.", 3);
251
+ }
252
+ return this.#toView(record, Boolean(input2.rawKey));
253
+ }
254
+ list(input2) {
255
+ const fields = parseFields(input2.field);
256
+ const limit = input2.limit ?? 50;
257
+ const filters = {
258
+ fields,
259
+ limit,
260
+ ...input2.env ? { env: input2.env } : {},
261
+ ...input2.query ? { query: input2.query } : {}
262
+ };
263
+ const results = this.#repository.list(filters);
264
+ return results.map((record) => this.#toView(record, Boolean(input2.rawKey)));
265
+ }
266
+ update(input2) {
267
+ const record = this.#findRecord(input2);
268
+ if (!record) {
269
+ throw new AkServiceError("RECORD_NOT_FOUND", "No API key record matched the query.", 3);
270
+ }
271
+ const normalizedEmail = normalizeNullableField(input2.email);
272
+ const normalizedPhone = normalizeNullableField(input2.phone);
273
+ const normalizedUserId = normalizeNullableField(input2.userId);
274
+ const normalizedUserName = normalizeNullableField(input2.userName);
275
+ const updated = this.#repository.updateMetadata({
276
+ ...normalizedEmail === void 0 ? {} : { email: normalizedEmail },
277
+ id: record.id,
278
+ ...normalizedPhone === void 0 ? {} : { phone: normalizedPhone },
279
+ updatedAt: this.#now(),
280
+ ...normalizedUserId === void 0 ? {} : { userId: normalizedUserId },
281
+ ...normalizedUserName === void 0 ? {} : { userName: normalizedUserName }
282
+ });
283
+ if (!updated) {
284
+ throw new AkServiceError("STORAGE_ERROR", "Failed to update the API key record.", 6);
285
+ }
286
+ return this.#toView(updated, Boolean(input2.rawKey));
287
+ }
288
+ delete(input2) {
289
+ const record = this.#findRecord(input2);
290
+ if (!record) {
291
+ throw new AkServiceError("RECORD_NOT_FOUND", "No API key record matched the query.", 3);
292
+ }
293
+ return this.#repository.deleteById(record.id);
294
+ }
295
+ #findRecord(input2) {
296
+ if (input2.id) {
297
+ return this.#repository.getById(input2.id);
298
+ }
299
+ if (input2.env && input2.key) {
300
+ const normalizedKey = normalizeAkKey(input2.key);
301
+ return this.#repository.getByEnvAndId(input2.env, createAkId(normalizedKey));
302
+ }
303
+ throw new AkServiceError(
304
+ "INVALID_ARGUMENT",
305
+ "Provide either --id or the pair of --env and --key.",
306
+ 2
307
+ );
308
+ }
309
+ #toView(record, rawKey) {
310
+ const key = decryptAkKey(this.#masterKey, record.keyCiphertext);
311
+ return {
312
+ createdAt: record.createdAt,
313
+ email: record.email,
314
+ env: record.env,
315
+ id: record.id,
316
+ key: rawKey ? key : maskKey(key),
317
+ phone: record.phone,
318
+ updatedAt: record.updatedAt,
319
+ userId: record.userId,
320
+ userName: record.userName
321
+ };
322
+ }
323
+ };
324
+ function normalizeNullableField(value) {
325
+ if (value === void 0) {
326
+ return void 0;
327
+ }
328
+ if (value === null) {
329
+ return null;
330
+ }
331
+ const normalized = value.trim();
332
+ return normalized.length === 0 ? null : normalized;
333
+ }
334
+ function normalizeNullableFieldForInsert(value) {
335
+ return normalizeNullableField(value) ?? null;
336
+ }
337
+ function parseFields(value) {
338
+ try {
339
+ return parseAkQueryFields(value);
340
+ } catch (error) {
341
+ const message = error instanceof Error ? error.message : "Invalid query fields.";
342
+ throw new AkServiceError("INVALID_ARGUMENT", message, 2);
343
+ }
344
+ }
345
+
346
+ // src/ak/runtime.ts
347
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
348
+ import { dirname, join } from "path";
349
+ import { z as z2 } from "zod";
350
+ var AK_DB_PATH_ENV_VAR = "STEPHEN_AK_DB_PATH";
351
+ var LEGACY_AK_DB_PATH_ENV_VAR = "STEPHEN_CLI_AK_DB_PATH";
352
+ var AK_CONFIG_FILE_NAME = "config.json";
353
+ var CONFIG_KEYS = ["ak.dbPath"];
354
+ var AkConfigError = class extends Error {
355
+ code = "CONFIG_ERROR";
356
+ exitCode = 2;
357
+ constructor(message) {
358
+ super(message);
359
+ this.name = "AkConfigError";
360
+ }
361
+ };
362
+ var AkStorageInitError = class extends Error {
363
+ code = "STORAGE_ERROR";
364
+ exitCode = 6;
365
+ constructor(path, cause) {
366
+ const detail = cause instanceof Error ? cause.message : "Unknown storage error.";
367
+ super(
368
+ `Failed to open the ak database at "${path}". Check ${formatAkDbPathEnvVarHelp()}, the local config file, and the parent directory permissions. Details: ${detail}`
369
+ );
370
+ this.name = "AkStorageInitError";
371
+ }
372
+ };
373
+ var akConfigSchema = z2.object({
374
+ ak: z2.object({
375
+ dbPath: z2.string().optional()
376
+ }).optional()
377
+ }).passthrough();
378
+ function getAkConfigFilePath(configDir) {
379
+ return join(configDir, AK_CONFIG_FILE_NAME);
380
+ }
381
+ function loadAkConfig(configDir) {
382
+ const configFilePath = getAkConfigFilePath(configDir);
383
+ if (!existsSync(configFilePath)) {
384
+ return {};
385
+ }
386
+ return parseAkConfig(readFileSync(configFilePath, "utf8"), configFilePath);
387
+ }
388
+ function saveAkConfig(configDir, config) {
389
+ const configFilePath = getAkConfigFilePath(configDir);
390
+ mkdirSync(dirname(configFilePath), { recursive: true });
391
+ writeFileSync(`${configFilePath}`, `${JSON.stringify(config, null, 2)}
392
+ `, "utf8");
393
+ }
394
+ function parseAkConfig(contents, configFilePath = AK_CONFIG_FILE_NAME) {
395
+ let parsedJson;
396
+ try {
397
+ parsedJson = JSON.parse(contents);
398
+ } catch {
399
+ throw new AkConfigError(
400
+ `Failed to parse the local config file at "${configFilePath}". Fix the JSON or set ${formatAkDbPathEnvVarHelp()} to the full ak.db path on this machine.`
401
+ );
402
+ }
403
+ const parsed = akConfigSchema.safeParse(parsedJson);
404
+ if (!parsed.success) {
405
+ throw new AkConfigError(
406
+ `Invalid ak config in "${configFilePath}". Expected ak.dbPath to be a string when provided. Fix the config file or set ${formatAkDbPathEnvVarHelp()} to the full ak.db path on this machine.`
407
+ );
408
+ }
409
+ return parsed.data;
410
+ }
411
+ function resolveAkDatabasePath(input2) {
412
+ const configPath = normalizeConfiguredPath(input2.config.ak?.dbPath, "ak.dbPath");
413
+ if (configPath) {
414
+ return {
415
+ path: configPath,
416
+ source: "config"
417
+ };
418
+ }
419
+ const envPath = getConfiguredAkDbPathFromEnv(input2.env);
420
+ if (envPath) {
421
+ return {
422
+ path: normalizeConfiguredPath(envPath.value, envPath.source),
423
+ source: "env"
424
+ };
425
+ }
426
+ return {
427
+ path: join(input2.defaultDataDir, "ak.db"),
428
+ source: "default"
429
+ };
430
+ }
431
+ function getConfigEntry(key, input2) {
432
+ assertSupportedConfigKey(key);
433
+ const config = loadAkConfig(input2.configDir);
434
+ const resolved = resolveAkDatabasePath({
435
+ config,
436
+ defaultDataDir: input2.dataDir,
437
+ env: input2.env
438
+ });
439
+ const envValue = normalizeResolvedValue(getConfiguredAkDbPathFromEnv(input2.env)?.value);
440
+ const fileValue = normalizeResolvedValue(config.ak?.dbPath);
441
+ return {
442
+ configFilePath: getAkConfigFilePath(input2.configDir),
443
+ defaultValue: join(input2.dataDir, "ak.db"),
444
+ envValue,
445
+ fileValue,
446
+ key,
447
+ source: resolved.source,
448
+ value: resolved.path
449
+ };
450
+ }
451
+ function listConfigEntries(input2) {
452
+ return CONFIG_KEYS.map((key) => getConfigEntry(key, input2));
453
+ }
454
+ function setConfigValue(key, value, input2) {
455
+ assertSupportedConfigKey(key);
456
+ const normalizedValue = normalizeConfiguredPath(value, key);
457
+ const config = loadAkConfig(input2.configDir);
458
+ const nextConfig = {
459
+ ...config,
460
+ ak: {
461
+ ...config.ak ?? {},
462
+ dbPath: normalizedValue
463
+ }
464
+ };
465
+ saveAkConfig(input2.configDir, nextConfig);
466
+ return nextConfig;
467
+ }
468
+ function assertSupportedConfigKey(key) {
469
+ if (!CONFIG_KEYS.includes(key)) {
470
+ throw new AkConfigError(
471
+ `Unsupported config key: ${key}. Supported keys: ${CONFIG_KEYS.join(", ")}.`
472
+ );
473
+ }
474
+ }
475
+ function normalizeConfiguredPath(value, source) {
476
+ if (value === void 0) {
477
+ return void 0;
478
+ }
479
+ const normalized = value.trim();
480
+ if (normalized.length === 0) {
481
+ throw new AkConfigError(
482
+ `${source} is configured but empty. Set it to the full ak.db path, or remove it to fall back to the next storage path source.`
483
+ );
484
+ }
485
+ return normalized;
486
+ }
487
+ function normalizeResolvedValue(value) {
488
+ if (value === void 0) {
489
+ return null;
490
+ }
491
+ return value.trim();
492
+ }
493
+ function getConfiguredAkDbPathFromEnv(env) {
494
+ const primaryValue = env[AK_DB_PATH_ENV_VAR];
495
+ if (primaryValue !== void 0) {
496
+ return {
497
+ source: AK_DB_PATH_ENV_VAR,
498
+ value: primaryValue
499
+ };
500
+ }
501
+ const legacyValue = env[LEGACY_AK_DB_PATH_ENV_VAR];
502
+ if (legacyValue !== void 0) {
503
+ return {
504
+ source: LEGACY_AK_DB_PATH_ENV_VAR,
505
+ value: legacyValue
506
+ };
507
+ }
508
+ return void 0;
509
+ }
510
+ function formatAkDbPathEnvVarHelp() {
511
+ return `${AK_DB_PATH_ENV_VAR} (preferred) or ${LEGACY_AK_DB_PATH_ENV_VAR} (legacy)`;
512
+ }
513
+
514
+ // src/disk/command.ts
515
+ import { z as z3 } from "zod";
516
+
517
+ // src/disk/output.ts
518
+ import { table as table2 } from "table";
519
+ function renderDiskCleanupReportAsJson(report) {
520
+ return JSON.stringify(
521
+ {
522
+ ok: true,
523
+ data: report,
524
+ meta: {
525
+ count: report.targets.length,
526
+ estimatedReclaimBytes: report.estimatedReclaimBytes,
527
+ estimatedReclaimGB: report.estimatedReclaimGB
528
+ }
529
+ },
530
+ null,
531
+ 2
532
+ );
533
+ }
534
+ function renderDiskCleanupReportAsTable(report) {
535
+ const targetTable = table2([
536
+ ["label", "status", "sizeGB", "exists", "requiresAdmin", "path"],
537
+ ...report.targets.map((target) => [
538
+ target.label,
539
+ target.status,
540
+ String(target.sizeGB),
541
+ target.exists ? "yes" : "no",
542
+ target.requiresAdministrator ? "yes" : "no",
543
+ target.path
544
+ ])
545
+ ]);
546
+ if (!report.downloads) {
547
+ return targetTable;
548
+ }
549
+ return `${targetTable}
550
+ Downloads top entries
551
+ ${table2([
552
+ ["kind", "sizeGB", "path"],
553
+ ...report.downloads.topEntries.map((entry) => [entry.kind, String(entry.sizeGB), entry.path])
554
+ ])}`;
555
+ }
556
+
557
+ // src/disk/command.ts
558
+ var diskCleanupOptionsSchema = z3.object({
559
+ apply: z3.boolean().default(false),
560
+ confirm: z3.boolean().default(false),
561
+ disableHibernate: z3.boolean().default(false),
562
+ format: z3.enum(["json", "table"]).default("json"),
563
+ level: z3.enum(["safe", "dev", "system", "deep"]).default("safe")
564
+ });
565
+ function registerDiskCommands(program, dependencies) {
566
+ const disk = program.command("disk").description("Inspect and clean conservative disk caches.");
567
+ disk.command("cleanup").option("--apply", "execute cleanup instead of previewing it").option("--confirm", "confirm apply for system or deep cleanup levels").option("--disable-hibernate", "disable Windows hibernation during apply mode").option("--format <format>", "output format", "json").option("--level <level>", "cleanup level: safe, dev, system, or deep", "safe").option("-t, --table", "render as a table").action(async (options) => {
568
+ const parsed = diskCleanupOptionsSchema.parse(applyDiskTableShortcut(options));
569
+ const report = await dependencies.createDiskCleanupService().cleanup({
570
+ apply: parsed.apply,
571
+ confirm: parsed.confirm,
572
+ disableHibernate: parsed.disableHibernate,
573
+ level: parsed.level
574
+ });
575
+ if (parsed.format === "table") {
576
+ dependencies.stdout(`${renderDiskCleanupReportAsTable(report)}
577
+ `);
578
+ return;
579
+ }
580
+ dependencies.stdout(`${renderDiskCleanupReportAsJson(report)}
581
+ `);
582
+ });
583
+ }
584
+ function applyDiskTableShortcut(options) {
585
+ if (options.table) {
586
+ return {
587
+ ...options,
588
+ format: "table"
589
+ };
590
+ }
591
+ if (options.format) {
592
+ return {
593
+ ...options,
594
+ format: options.format
595
+ };
596
+ }
597
+ throw new Error("Output format is required for this command.");
598
+ }
599
+
600
+ // src/disk/service.ts
601
+ import { win32 } from "path";
602
+ var DiskCleanupServiceError = class extends Error {
603
+ code;
604
+ details;
605
+ exitCode;
606
+ constructor(code, message, exitCode = 2, details) {
607
+ super(message);
608
+ this.code = code;
609
+ if (details) {
610
+ this.details = details;
611
+ }
612
+ this.exitCode = exitCode;
613
+ }
614
+ };
615
+ var DiskCleanupService = class {
616
+ runtime;
617
+ systemRoot;
618
+ userProfileRoot;
619
+ constructor(dependencies) {
620
+ this.runtime = dependencies.runtime;
621
+ this.systemRoot = dependencies.systemRoot;
622
+ this.userProfileRoot = dependencies.userProfileRoot;
623
+ }
624
+ async cleanup(options) {
625
+ if (requiresConfirmation(options.level) && options.apply && !options.confirm) {
626
+ throw new DiskCleanupServiceError(
627
+ "DISK_CLEANUP_CONFIRMATION_REQUIRED",
628
+ "Disk cleanup level requires --confirm before apply."
629
+ );
630
+ }
631
+ const targetDefinitions = this.getTargets(options.level);
632
+ const targets = [];
633
+ const failures = [];
634
+ let estimatedReclaimBytes = 0;
635
+ for (const target of targetDefinitions) {
636
+ const inspection = target.action === "run-command" ? { exists: true, isDirectory: false, sizeBytes: 0 } : await this.runtime.inspectPath(target.path);
637
+ estimatedReclaimBytes += inspection.sizeBytes;
638
+ let status = inspection.exists ? options.apply ? "cleaned" : "planned" : "missing";
639
+ let error;
640
+ if (options.apply && inspection.exists) {
641
+ try {
642
+ if (target.action === "clear-directory-contents") {
643
+ await this.runtime.clearDirectoryContents(target.path);
644
+ }
645
+ if (target.action === "run-command") {
646
+ await this.runtime.runCommand(target.command, target.args ?? []);
647
+ }
648
+ if (target.action === "inspect-only") {
649
+ status = "planned";
650
+ }
651
+ } catch (cause) {
652
+ error = getErrorMessage(cause);
653
+ failures.push(`${target.label}: ${error}`);
654
+ status = "failed";
655
+ }
656
+ }
657
+ targets.push({
658
+ action: target.action,
659
+ ...target.command ? { command: target.command } : {},
660
+ ...target.args ? { args: target.args } : {},
661
+ exists: inspection.exists,
662
+ label: target.label,
663
+ path: target.path,
664
+ requiresAdministrator: target.requiresAdministrator,
665
+ sizeBytes: inspection.sizeBytes,
666
+ sizeGB: bytesToGb(inspection.sizeBytes),
667
+ status,
668
+ ...error ? { error } : {}
669
+ });
670
+ }
671
+ const hibernation = {
672
+ requested: options.disableHibernate,
673
+ status: "skipped"
674
+ };
675
+ if (options.apply && options.disableHibernate) {
676
+ try {
677
+ await this.runtime.disableHibernation();
678
+ hibernation.status = "disabled";
679
+ } catch (error) {
680
+ hibernation.status = "failed";
681
+ hibernation.error = getErrorMessage(error);
682
+ failures.push(`hibernation: ${hibernation.error}`);
683
+ }
684
+ }
685
+ const report = {
686
+ ...options.level === "deep" ? { downloads: await this.getDownloadsReport() } : {},
687
+ estimatedReclaimBytes,
688
+ estimatedReclaimGB: bytesToGb(estimatedReclaimBytes),
689
+ hibernation,
690
+ level: options.level,
691
+ mode: options.apply ? "apply" : "preview",
692
+ systemRoot: this.systemRoot,
693
+ targets,
694
+ userProfileRoot: this.userProfileRoot
695
+ };
696
+ if (failures.length > 0) {
697
+ throw new DiskCleanupServiceError(
698
+ "DISK_CLEANUP_ERROR",
699
+ "Disk cleanup encountered failures.",
700
+ 2,
701
+ {
702
+ report
703
+ }
704
+ );
705
+ }
706
+ ;
707
+ return report;
708
+ }
709
+ async getDownloadsReport() {
710
+ const path = win32.join(this.userProfileRoot, "Downloads");
711
+ return {
712
+ path,
713
+ topEntries: await this.runtime.listTopEntriesBySize(path, 100)
714
+ };
715
+ }
716
+ getTargets(level) {
717
+ if (level === "safe") {
718
+ return this.getSafeTargets();
719
+ }
720
+ if (level === "dev") {
721
+ return dedupeTargets([...this.getSafeTargets(), ...this.getDevTargets()]);
722
+ }
723
+ if (level === "system") {
724
+ return dedupeTargets([...this.getSafeTargets(), ...this.getSystemTargets()]);
725
+ }
726
+ return dedupeTargets([...this.getSafeTargets(), ...this.getDevTargets(), ...this.getSystemTargets()]);
727
+ }
728
+ getSafeTargets() {
729
+ return [
730
+ {
731
+ action: "clear-directory-contents",
732
+ label: "npm cache",
733
+ path: win32.join(this.userProfileRoot, "AppData", "Local", "npm-cache"),
734
+ requiresAdministrator: false
735
+ },
736
+ {
737
+ action: "clear-directory-contents",
738
+ label: "NuGet cache",
739
+ path: win32.join(this.userProfileRoot, "AppData", "Local", "NuGet"),
740
+ requiresAdministrator: false
741
+ },
742
+ {
743
+ action: "clear-directory-contents",
744
+ label: "Generic cache",
745
+ path: win32.join(this.userProfileRoot, ".cache"),
746
+ requiresAdministrator: false
747
+ },
748
+ {
749
+ action: "clear-directory-contents",
750
+ label: "Maven repository cache",
751
+ path: win32.join(this.userProfileRoot, ".m2"),
752
+ requiresAdministrator: false
753
+ },
754
+ {
755
+ action: "clear-directory-contents",
756
+ label: "User temp",
757
+ path: win32.join(this.userProfileRoot, "AppData", "Local", "Temp"),
758
+ requiresAdministrator: false
759
+ },
760
+ {
761
+ action: "clear-directory-contents",
762
+ label: "Windows Update download cache",
763
+ path: win32.join(this.systemRoot, "SoftwareDistribution", "Download"),
764
+ requiresAdministrator: true
765
+ }
766
+ ];
767
+ }
768
+ getDevTargets() {
769
+ return [
770
+ {
771
+ action: "clear-directory-contents",
772
+ label: "Gradle cache",
773
+ path: win32.join(this.userProfileRoot, ".gradle", "caches"),
774
+ requiresAdministrator: false
775
+ },
776
+ {
777
+ action: "clear-directory-contents",
778
+ label: "pnpm store",
779
+ path: win32.join(this.userProfileRoot, ".pnpm-store"),
780
+ requiresAdministrator: false
781
+ },
782
+ {
783
+ action: "clear-directory-contents",
784
+ label: "pnpm local store",
785
+ path: win32.join(this.userProfileRoot, "AppData", "Local", "pnpm", "store"),
786
+ requiresAdministrator: false
787
+ },
788
+ {
789
+ action: "clear-directory-contents",
790
+ label: "Yarn cache",
791
+ path: win32.join(this.userProfileRoot, "AppData", "Local", "Yarn", "Cache"),
792
+ requiresAdministrator: false
793
+ },
794
+ {
795
+ action: "clear-directory-contents",
796
+ label: "pip cache",
797
+ path: win32.join(this.userProfileRoot, "AppData", "Local", "pip", "Cache"),
798
+ requiresAdministrator: false
799
+ }
800
+ ];
801
+ }
802
+ getSystemTargets() {
803
+ return [
804
+ {
805
+ action: "clear-directory-contents",
806
+ label: "Windows temp",
807
+ path: win32.join(this.systemRoot, "Temp"),
808
+ requiresAdministrator: true
809
+ },
810
+ {
811
+ action: "run-command",
812
+ args: ["/Online", "/Cleanup-Image", "/StartComponentCleanup"],
813
+ command: "dism.exe",
814
+ label: "DISM component cleanup",
815
+ path: "dism.exe /Online /Cleanup-Image /StartComponentCleanup",
816
+ requiresAdministrator: true
817
+ }
818
+ ];
819
+ }
820
+ };
821
+ function bytesToGb(value) {
822
+ return Number((value / 1024 ** 3).toFixed(2));
823
+ }
824
+ function getErrorMessage(error) {
825
+ return error instanceof Error ? error.message : "Unknown error";
826
+ }
827
+ function requiresConfirmation(level) {
828
+ return level === "system" || level === "deep";
829
+ }
830
+ function dedupeTargets(targets) {
831
+ const seen = /* @__PURE__ */ new Set();
832
+ const result = [];
833
+ for (const target of targets) {
834
+ const key = `${target.action}:${target.command ?? target.path}`;
835
+ if (seen.has(key)) {
836
+ continue;
837
+ }
838
+ seen.add(key);
839
+ result.push(target);
840
+ }
841
+ return result;
842
+ }
843
+
844
+ // src/video/command.ts
845
+ import { z as z4 } from "zod";
846
+
847
+ // src/video/sniff/browser-provider.ts
848
+ var BrowserVideoSniffProvider = class {
849
+ runtime;
850
+ constructor(dependencies) {
851
+ this.runtime = dependencies.runtime;
852
+ }
853
+ sniff(sourceUrl, options) {
854
+ return this.runtime.launchBrowserSniffer(sourceUrl, options);
855
+ }
856
+ };
857
+
858
+ // src/video/sniff/candidate.ts
859
+ function createVideoCandidate(type, url, origin, confidence = 0.5) {
860
+ return {
861
+ confidence,
862
+ mimeType: type === "m3u8" ? "application/vnd.apple.mpegurl" : "video/mp4",
863
+ origin,
864
+ type,
865
+ url
866
+ };
867
+ }
868
+ function rankVideoCandidates(candidates) {
869
+ const deduped = /* @__PURE__ */ new Map();
870
+ for (const candidate of candidates) {
871
+ const existing = deduped.get(candidate.url);
872
+ if (!existing || (candidate.confidence ?? 0) > (existing.confidence ?? 0)) {
873
+ deduped.set(candidate.url, candidate);
874
+ }
875
+ }
876
+ return [...deduped.values()].sort((left, right) => {
877
+ const confidenceDelta = (right.confidence ?? 0) - (left.confidence ?? 0);
878
+ if (confidenceDelta !== 0) {
879
+ return confidenceDelta;
880
+ }
881
+ if (left.type === right.type) {
882
+ return 0;
883
+ }
884
+ return left.type === "m3u8" ? -1 : 1;
885
+ });
886
+ }
887
+ function extractVideoCandidatesFromText(text, origin) {
888
+ const matches = text.matchAll(/https?:\/\/[^\s"'<>]+?\.(m3u8|mp4)(?:\?[^\s"'<>]*)?/gi);
889
+ return rankVideoCandidates(
890
+ [...matches].map(
891
+ (match) => createVideoCandidate(match[1]?.toLowerCase() === "m3u8" ? "m3u8" : "mp4", match[0], origin, origin === "html" ? 0.75 : 0.65)
892
+ )
893
+ );
894
+ }
895
+
896
+ // src/video/utils.ts
897
+ function optional(key, value) {
898
+ return value !== void 0 && value !== null && value !== "" ? { [key]: value } : {};
899
+ }
900
+
901
+ // src/video/sniff/service.ts
902
+ var VideoSniffService = class {
903
+ browserProvider;
904
+ httpProvider;
905
+ constructor(dependencies) {
906
+ this.browserProvider = dependencies.browserProvider;
907
+ this.httpProvider = dependencies.httpProvider;
908
+ }
909
+ async sniff(options) {
910
+ const providerOptions = buildProviderOptions(options);
911
+ if (options.mode === "http") {
912
+ return this.createResult("http", options.sourceUrl, await this.httpProvider(options.sourceUrl, providerOptions));
913
+ }
914
+ if (options.mode === "browser") {
915
+ return this.createResult(
916
+ "browser",
917
+ options.sourceUrl,
918
+ await this.browserProvider(options.sourceUrl, providerOptions)
919
+ );
920
+ }
921
+ try {
922
+ return this.createResult(
923
+ "browser",
924
+ options.sourceUrl,
925
+ await this.browserProvider(options.sourceUrl, providerOptions)
926
+ );
927
+ } catch (error) {
928
+ if (error instanceof Error && "recoverable" in error && error.recoverable === true) {
929
+ return this.createResult("http", options.sourceUrl, await this.httpProvider(options.sourceUrl, providerOptions));
930
+ }
931
+ throw error;
932
+ }
933
+ }
934
+ createResult(mode, sourceUrl, candidates) {
935
+ return {
936
+ candidates: rankVideoCandidates(candidates),
937
+ mode,
938
+ sourceUrl
939
+ };
940
+ }
941
+ };
942
+ function buildProviderOptions(options) {
943
+ return {
944
+ ...optional("noProxy", options.noProxy),
945
+ ...optional("proxyUrl", options.proxyUrl)
946
+ };
947
+ }
948
+
949
+ // src/video/types.ts
950
+ var VideoCommandError = class extends Error {
951
+ code;
952
+ details;
953
+ exitCode;
954
+ recoverable;
955
+ constructor(code, message, exitCode = 2, details, recoverable = false) {
956
+ super(message);
957
+ this.code = code;
958
+ if (details !== void 0) {
959
+ this.details = details;
960
+ }
961
+ this.exitCode = exitCode;
962
+ this.recoverable = recoverable;
963
+ }
964
+ };
965
+
966
+ // src/video/sniff/http-provider.ts
967
+ var HttpVideoSniffProvider = class {
968
+ runtime;
969
+ constructor(dependencies) {
970
+ this.runtime = dependencies.runtime;
971
+ }
972
+ async sniff(sourceUrl, options) {
973
+ const response = await this.runtime.fetch(sourceUrl);
974
+ if (!response.ok) {
975
+ throw new VideoCommandError(
976
+ "VIDEO_SNIFF_FAILED",
977
+ `Failed to inspect ${sourceUrl}. HTTP ${response.status}.`
978
+ );
979
+ }
980
+ const text = await response.text();
981
+ return extractVideoCandidatesFromText(text, "html");
982
+ }
983
+ };
984
+
985
+ // src/video/download/direct-driver.ts
986
+ import { basename, win32 as win322 } from "path";
987
+ import { SingleBar } from "cli-progress";
988
+
989
+ // src/video/download/media-request.ts
990
+ function createMediaRequestInit(sourceUrl) {
991
+ const url = new URL(sourceUrl);
992
+ return {
993
+ headers: {
994
+ referer: `${url.protocol}//${url.host}/`,
995
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0 Safari/537.36"
996
+ }
997
+ };
998
+ }
999
+
1000
+ // src/video/download/direct-driver.ts
1001
+ async function fetchWithProxy(url, runtime, options) {
1002
+ const init = createMediaRequestInit(url);
1003
+ if (options?.noProxy) {
1004
+ return await runtime.fetch(url, init);
1005
+ }
1006
+ const proxyUrl = options?.proxyUrl ?? process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY;
1007
+ if (!proxyUrl) {
1008
+ return await runtime.fetch(url, init);
1009
+ }
1010
+ try {
1011
+ const mod = await import("https-proxy-agent");
1012
+ const response = await runtime.fetch(url, { ...init, agent: new mod.HttpsProxyAgent(proxyUrl) });
1013
+ return response;
1014
+ } catch (error) {
1015
+ console.warn(`[stephen] Proxy connection failed (${proxyUrl}), falling back to direct connection: ${error instanceof Error ? error.message : String(error)}`);
1016
+ return await runtime.fetch(url, init);
1017
+ }
1018
+ }
1019
+ var DirectVideoDownloadDriver = class {
1020
+ runtime;
1021
+ constructor(dependencies) {
1022
+ this.runtime = dependencies.runtime;
1023
+ }
1024
+ async download(options) {
1025
+ let response;
1026
+ try {
1027
+ response = await fetchWithProxy(
1028
+ options.sourceUrl,
1029
+ this.runtime,
1030
+ buildProxyOptions(options)
1031
+ );
1032
+ } catch (error) {
1033
+ throw new VideoCommandError(
1034
+ "VIDEO_DOWNLOAD_FAILED",
1035
+ `Failed to download ${options.sourceUrl}.`,
1036
+ 2,
1037
+ {
1038
+ cause: error instanceof Error ? error.message : String(error)
1039
+ }
1040
+ );
1041
+ }
1042
+ if (!response.ok) {
1043
+ throw new VideoCommandError(
1044
+ "VIDEO_DOWNLOAD_FAILED",
1045
+ `Failed to download ${options.sourceUrl}. HTTP ${response.status}.`
1046
+ );
1047
+ }
1048
+ const outputPath = options.outputPath ?? win322.join(options.outputDir ?? ".", inferFileName(options.sourceUrl));
1049
+ const contentLength = response.headers.get("content-length");
1050
+ const totalBytes = contentLength ? parseInt(contentLength, 10) : void 0;
1051
+ let buffer;
1052
+ if (response.body && totalBytes && totalBytes > 0) {
1053
+ buffer = await this.downloadWithProgress(response.body, totalBytes, outputPath);
1054
+ } else {
1055
+ buffer = await this.downloadAsBuffer(response);
1056
+ }
1057
+ await this.runtime.writeFile(outputPath, buffer);
1058
+ const downloadedBytes = buffer.byteLength;
1059
+ if (downloadedBytes > 0) {
1060
+ console.error(`Downloaded ${formatBytes(downloadedBytes)} to ${outputPath}`);
1061
+ }
1062
+ return {
1063
+ bytesWritten: buffer.byteLength,
1064
+ mediaType: "mp4",
1065
+ outputPath,
1066
+ sourceUrl: options.sourceUrl
1067
+ };
1068
+ }
1069
+ /* c8 ignore start */
1070
+ async downloadWithProgress(body, totalBytes, _outputPath) {
1071
+ const reader = body.getReader();
1072
+ const chunks = [];
1073
+ let receivedBytes = 0;
1074
+ const startTime = Date.now();
1075
+ const progressBar = new SingleBar({
1076
+ format: createProgressFormatter(startTime, 24),
1077
+ barCompleteChar: "\u2588",
1078
+ barIncompleteChar: "\u2591",
1079
+ hideCursor: true,
1080
+ fps: 10,
1081
+ barsize: 24
1082
+ });
1083
+ progressBar.start(totalBytes, 0);
1084
+ try {
1085
+ while (true) {
1086
+ const { done, value } = await reader.read();
1087
+ if (done) break;
1088
+ chunks.push(value);
1089
+ receivedBytes += value.length;
1090
+ progressBar.update(receivedBytes);
1091
+ }
1092
+ } finally {
1093
+ progressBar.stop();
1094
+ reader.releaseLock();
1095
+ }
1096
+ return Buffer.concat(chunks);
1097
+ }
1098
+ async downloadAsBuffer(response) {
1099
+ const payload = await response.arrayBuffer();
1100
+ return payload instanceof Uint8Array ? Buffer.from(payload) : Buffer.from(payload);
1101
+ }
1102
+ };
1103
+ function createProgressFormatter(startTime, barsize) {
1104
+ return (_options, params) => {
1105
+ const elapsed = (Date.now() - startTime) / 1e3;
1106
+ const speed = elapsed > 0 ? params.value / elapsed : 0;
1107
+ const completeCount = Math.round(params.progress * barsize);
1108
+ const bar = "\u2588".repeat(completeCount) + "\u2591".repeat(barsize - completeCount);
1109
+ const percentage = (params.progress * 100).toFixed(1);
1110
+ return `Downloading ${bar} ${percentage}% | ${formatBytes(speed)}/s | ETA ${formatETA(params.eta)} | ${formatBytes(params.value)} / ${formatBytes(params.total)}`;
1111
+ };
1112
+ }
1113
+ function inferFileName(sourceUrl) {
1114
+ const name = basename(new URL(sourceUrl).pathname);
1115
+ return name || "video.mp4";
1116
+ }
1117
+ function formatBytes(bytes) {
1118
+ if (bytes === 0) return "0 B";
1119
+ const k = 1024;
1120
+ const sizes = ["B", "KB", "MB", "GB"];
1121
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1122
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
1123
+ }
1124
+ function formatETA(seconds) {
1125
+ if (!isFinite(seconds) || seconds <= 0) return "--:--";
1126
+ const s = Math.round(seconds);
1127
+ if (s >= 3600) {
1128
+ const h = Math.floor(s / 3600);
1129
+ const m2 = Math.floor(s % 3600 / 60);
1130
+ const sec2 = s % 60;
1131
+ return `${h}:${String(m2).padStart(2, "0")}:${String(sec2).padStart(2, "0")}`;
1132
+ }
1133
+ const m = Math.floor(s / 60);
1134
+ const sec = s % 60;
1135
+ return `${m}:${String(sec).padStart(2, "0")}`;
1136
+ }
1137
+ function buildProxyOptions(options) {
1138
+ return {
1139
+ ...options.noProxy === void 0 ? {} : { noProxy: options.noProxy },
1140
+ ...options.proxyUrl === void 0 ? {} : { proxyUrl: options.proxyUrl }
1141
+ };
1142
+ }
1143
+
1144
+ // src/video/download/hls-driver.ts
1145
+ import { basename as basename2, extname, win32 as win323 } from "path";
1146
+ var HlsVideoDownloadDriver = class {
1147
+ runtime;
1148
+ constructor(dependencies) {
1149
+ this.runtime = dependencies.runtime;
1150
+ }
1151
+ async download(options) {
1152
+ const playlistResponse = await this.runtime.fetch(
1153
+ options.sourceUrl,
1154
+ createMediaRequestInit(options.sourceUrl)
1155
+ );
1156
+ if (!playlistResponse.ok) {
1157
+ throw new VideoCommandError(
1158
+ "VIDEO_DOWNLOAD_FAILED",
1159
+ `Failed to download ${options.sourceUrl}. HTTP ${playlistResponse.status}.`
1160
+ );
1161
+ }
1162
+ const playlist = await playlistResponse.text();
1163
+ const segmentUrls = parsePlaylistResourceUrls(playlist, options.sourceUrl);
1164
+ const chunks = [];
1165
+ const segmentResponses = await Promise.all(
1166
+ segmentUrls.map((segmentUrl) => this.runtime.fetch(segmentUrl, createMediaRequestInit(segmentUrl)))
1167
+ );
1168
+ for (let i = 0; i < segmentResponses.length; i++) {
1169
+ const segmentResponse = segmentResponses[i];
1170
+ const segmentUrl = segmentUrls[i];
1171
+ if (!segmentResponse || !segmentUrl) {
1172
+ throw new VideoCommandError(
1173
+ "VIDEO_DOWNLOAD_FAILED",
1174
+ `Failed to download segment ${i}.`
1175
+ );
1176
+ }
1177
+ if (!segmentResponse.ok) {
1178
+ throw new VideoCommandError(
1179
+ "VIDEO_DOWNLOAD_FAILED",
1180
+ `Failed to download ${segmentUrl}. HTTP ${segmentResponse.status}.`
1181
+ );
1182
+ }
1183
+ const payload = await segmentResponse.arrayBuffer();
1184
+ chunks.push(payload instanceof Uint8Array ? Buffer.from(payload) : Buffer.from(payload));
1185
+ }
1186
+ const merged = Buffer.concat(chunks);
1187
+ const outputPath = options.outputPath ?? win323.join(options.outputDir ?? ".", `${stripExtension(basename2(new URL(options.sourceUrl).pathname))}.ts`);
1188
+ await this.runtime.writeFile(outputPath, merged);
1189
+ return {
1190
+ bytesWritten: merged.byteLength,
1191
+ mediaType: "m3u8",
1192
+ outputPath,
1193
+ sourceUrl: options.sourceUrl
1194
+ };
1195
+ }
1196
+ };
1197
+ function stripExtension(value) {
1198
+ const extension = extname(value);
1199
+ return extension.length === 0 ? value : value.slice(0, -extension.length);
1200
+ }
1201
+ function parsePlaylistResourceUrls(playlist, sourceUrl) {
1202
+ const urls = [];
1203
+ for (const rawLine of playlist.split(/\r?\n/u)) {
1204
+ const line = rawLine.trim();
1205
+ if (line.length === 0) {
1206
+ continue;
1207
+ }
1208
+ const mapUri = /^#EXT-X-MAP:.*URI="([^"]+)"/u.exec(line)?.[1];
1209
+ if (mapUri) {
1210
+ urls.push(new URL(mapUri, sourceUrl).toString());
1211
+ continue;
1212
+ }
1213
+ if (!line.startsWith("#")) {
1214
+ urls.push(new URL(line, sourceUrl).toString());
1215
+ }
1216
+ }
1217
+ return urls;
1218
+ }
1219
+
1220
+ // src/video/download/browser-driver.ts
1221
+ import { join as join2 } from "path";
1222
+ var BrowserDownloadDriver = class {
1223
+ async download(options) {
1224
+ const outputPath = options.outputPath ?? join2(options.outputDir ?? ".", this.inferFileName(options.sourceUrl));
1225
+ const { chromium } = await this.loadPlaywright();
1226
+ if (!chromium) {
1227
+ throw new VideoCommandError(
1228
+ "VIDEO_BROWSER_UNAVAILABLE",
1229
+ "Browser download requires Playwright Chromium support.",
1230
+ 2,
1231
+ void 0,
1232
+ true
1233
+ );
1234
+ }
1235
+ const browser = await chromium.launch({ headless: true });
1236
+ try {
1237
+ const context = await browser.newContext();
1238
+ const page = await context.newPage();
1239
+ const downloadPromise = page.waitForEvent("download");
1240
+ await page.goto(options.sourceUrl, { waitUntil: "load" });
1241
+ await page.waitForTimeout(2e3);
1242
+ const download = await downloadPromise;
1243
+ await download.saveAs(outputPath);
1244
+ return {
1245
+ mediaType: "mp4",
1246
+ outputPath,
1247
+ sourceUrl: options.sourceUrl
1248
+ };
1249
+ } finally {
1250
+ await browser.close();
1251
+ }
1252
+ }
1253
+ async loadPlaywright() {
1254
+ try {
1255
+ return await loadOptionalModule("playwright");
1256
+ } catch {
1257
+ throw new VideoCommandError(
1258
+ "VIDEO_BROWSER_UNAVAILABLE",
1259
+ "Browser download requires Playwright. Install it with: npm install playwright",
1260
+ 2,
1261
+ void 0,
1262
+ true
1263
+ );
1264
+ }
1265
+ }
1266
+ inferFileName(sourceUrl) {
1267
+ try {
1268
+ const url = new URL(sourceUrl);
1269
+ const name = url.pathname.split("/").pop();
1270
+ return name && name.length > 0 ? name : "video.mp4";
1271
+ } catch {
1272
+ return "video.mp4";
1273
+ }
1274
+ }
1275
+ };
1276
+ var loadOptionalModule = new Function(
1277
+ "specifier",
1278
+ "return import(specifier);"
1279
+ );
1280
+
1281
+ // src/video/download/resolver.ts
1282
+ function encodeUrlPath(pathname) {
1283
+ const segments = pathname.split("/");
1284
+ return segments.map((segment) => {
1285
+ if (segment.length > 0 && segment.charCodeAt(0) > 127) {
1286
+ return encodeURIComponent(segment);
1287
+ }
1288
+ return segment;
1289
+ }).join("/");
1290
+ }
1291
+ function classifyVideoInput(input2) {
1292
+ let url;
1293
+ try {
1294
+ url = new URL(input2);
1295
+ } catch {
1296
+ const urlObj = new URL(input2.startsWith("http") ? input2 : `https://${input2}`);
1297
+ urlObj.pathname = encodeUrlPath(urlObj.pathname);
1298
+ url = urlObj;
1299
+ }
1300
+ if (!["http:", "https:"].includes(url.protocol)) {
1301
+ throw new VideoCommandError("VIDEO_UNSUPPORTED_INPUT", `Unsupported video input: ${input2}`);
1302
+ }
1303
+ if (/\.m3u8(?:\?|$)/i.test(url.href)) {
1304
+ return {
1305
+ kind: "m3u8",
1306
+ url: url.href
1307
+ };
1308
+ }
1309
+ if (/\.mp4(?:\?|$)/i.test(url.href)) {
1310
+ return {
1311
+ kind: "mp4",
1312
+ url: url.href
1313
+ };
1314
+ }
1315
+ return {
1316
+ kind: "page",
1317
+ url: url.href
1318
+ };
1319
+ }
1320
+
1321
+ // src/video/download/service.ts
1322
+ var VideoDownloadService = class {
1323
+ directDriver;
1324
+ hlsDriver;
1325
+ sniffService;
1326
+ browserDriver;
1327
+ constructor(dependencies) {
1328
+ this.directDriver = dependencies.directDriver;
1329
+ this.hlsDriver = dependencies.hlsDriver;
1330
+ this.sniffService = dependencies.sniffService;
1331
+ this.browserDriver = dependencies.browserDriver;
1332
+ }
1333
+ async download(options) {
1334
+ const classified = classifyVideoInput(options.input);
1335
+ if (classified.kind === "page" && this.browserDriver) {
1336
+ const sniffed2 = await this.sniffService.sniff({
1337
+ mode: options.mode,
1338
+ sourceUrl: classified.url,
1339
+ ...buildProxyOptions2(options)
1340
+ });
1341
+ const candidate2 = sniffed2.candidates[0];
1342
+ if (!candidate2) {
1343
+ throw new VideoCommandError(
1344
+ "VIDEO_NO_CANDIDATE",
1345
+ "No supported media candidate was detected for the provided input."
1346
+ );
1347
+ }
1348
+ if (candidate2.type === "m3u8") {
1349
+ return this.hlsDriver.download(buildDownloadTargetOptions(options, candidate2.url));
1350
+ }
1351
+ return this.browserDriver.download(buildDownloadTargetOptions(options, candidate2.url));
1352
+ }
1353
+ if (classified.kind === "m3u8") {
1354
+ return this.hlsDriver.download(buildDownloadTargetOptions(options, classified.url));
1355
+ }
1356
+ if (this.browserDriver) {
1357
+ try {
1358
+ return await this.browserDriver.download(buildDownloadTargetOptions(options, classified.url));
1359
+ } catch (error) {
1360
+ if (error instanceof VideoCommandError && error.code === "VIDEO_BROWSER_UNAVAILABLE") {
1361
+ } else if (!isBrowserDownloadTimeout(error)) {
1362
+ throw error;
1363
+ }
1364
+ }
1365
+ }
1366
+ if (classified.kind === "mp4") {
1367
+ return this.directDriver.download(buildDownloadTargetOptions(options, classified.url));
1368
+ }
1369
+ const sniffed = await this.sniffService.sniff({
1370
+ mode: options.mode,
1371
+ sourceUrl: classified.url,
1372
+ ...buildProxyOptions2(options)
1373
+ });
1374
+ const candidate = sniffed.candidates[0];
1375
+ if (!candidate) {
1376
+ throw new VideoCommandError(
1377
+ "VIDEO_NO_CANDIDATE",
1378
+ "No supported media candidate was detected for the provided input."
1379
+ );
1380
+ }
1381
+ if (candidate.type === "mp4") {
1382
+ return this.directDriver.download(buildDownloadTargetOptions(options, candidate.url));
1383
+ }
1384
+ return this.hlsDriver.download(buildDownloadTargetOptions(options, candidate.url));
1385
+ }
1386
+ };
1387
+ function isBrowserDownloadTimeout(error) {
1388
+ return error instanceof Error && error.message.includes('waiting for event "download"');
1389
+ }
1390
+ function buildProxyOptions2(options) {
1391
+ return {
1392
+ ...options.noProxy === void 0 ? {} : { noProxy: options.noProxy },
1393
+ ...options.proxyUrl === void 0 ? {} : { proxyUrl: options.proxyUrl }
1394
+ };
1395
+ }
1396
+ function buildDownloadTargetOptions(options, sourceUrl) {
1397
+ return {
1398
+ ...optional("outputDir", options.outputDir),
1399
+ ...optional("outputPath", options.outputPath),
1400
+ ...optional("noProxy", options.noProxy),
1401
+ ...optional("proxyUrl", options.proxyUrl),
1402
+ sourceUrl
1403
+ };
1404
+ }
1405
+
1406
+ // src/video/compress/service.ts
1407
+ import { extname as extname2 } from "path";
1408
+
1409
+ // src/video/compress/preset.ts
1410
+ var VIDEO_COMPRESSION_DEFAULTS = {
1411
+ audioBitrate: "64k",
1412
+ audioCodec: "aac",
1413
+ container: "mp4",
1414
+ videoCodec: "libx265"
1415
+ };
1416
+
1417
+ // src/video/compress/ffmpeg.ts
1418
+ function buildFfmpegArgs(options) {
1419
+ const args = [
1420
+ "-i",
1421
+ options.inputPath,
1422
+ "-c:v",
1423
+ VIDEO_COMPRESSION_DEFAULTS.videoCodec
1424
+ ];
1425
+ if (options.videoBitrate) {
1426
+ args.push("-b:v", options.videoBitrate);
1427
+ }
1428
+ if (options.resolution) {
1429
+ const [width, height] = options.resolution.split("x");
1430
+ args.push("-vf", `scale=${width}:${height}`);
1431
+ }
1432
+ args.push(
1433
+ "-c:a",
1434
+ VIDEO_COMPRESSION_DEFAULTS.audioCodec,
1435
+ "-b:a",
1436
+ options.audioBitrate ?? VIDEO_COMPRESSION_DEFAULTS.audioBitrate,
1437
+ options.outputPath
1438
+ );
1439
+ return args;
1440
+ }
1441
+ function probeFfmpegResult(result) {
1442
+ return result.code === 0 && result.stdout.includes("ffmpeg version");
1443
+ }
1444
+
1445
+ // src/video/compress/service.ts
1446
+ var VideoCompressionService = class {
1447
+ runtime;
1448
+ constructor(dependencies) {
1449
+ this.runtime = dependencies.runtime;
1450
+ }
1451
+ async compress(options) {
1452
+ const probe = await this.runtime.execFile("ffmpeg", ["-version"]);
1453
+ if (!probeFfmpegResult(probe)) {
1454
+ throw new VideoCommandError(
1455
+ "VIDEO_FFMPEG_MISSING",
1456
+ "ffmpeg is required for video compression."
1457
+ );
1458
+ }
1459
+ const outputPath = options.outputPath ?? replaceExtension(options.inputPath, ".mp4");
1460
+ const execResult = await this.runtime.execFile(
1461
+ "ffmpeg",
1462
+ buildFfmpegArgs({
1463
+ inputPath: options.inputPath,
1464
+ outputPath,
1465
+ ...options.audioBitrate ? { audioBitrate: options.audioBitrate } : {},
1466
+ ...options.resolution ? { resolution: options.resolution } : {},
1467
+ ...options.videoBitrate ? { videoBitrate: options.videoBitrate } : {}
1468
+ })
1469
+ );
1470
+ if (execResult.code !== 0) {
1471
+ throw new VideoCommandError(
1472
+ "VIDEO_COMPRESSION_FAILED",
1473
+ execResult.stderr || "Video compression failed."
1474
+ );
1475
+ }
1476
+ return {
1477
+ audioBitrate: options.audioBitrate ?? VIDEO_COMPRESSION_DEFAULTS.audioBitrate,
1478
+ audioCodec: VIDEO_COMPRESSION_DEFAULTS.audioCodec,
1479
+ codec: VIDEO_COMPRESSION_DEFAULTS.videoCodec,
1480
+ inputPath: options.inputPath,
1481
+ outputPath,
1482
+ ...options.resolution ? { resolution: options.resolution } : {},
1483
+ ...options.videoBitrate ? { videoBitrate: options.videoBitrate } : {}
1484
+ };
1485
+ }
1486
+ };
1487
+ function replaceExtension(path, nextExtension) {
1488
+ const currentExtension = extname2(path);
1489
+ return currentExtension.length === 0 ? `${path}${nextExtension}` : `${path.slice(0, -currentExtension.length)}${nextExtension}`;
1490
+ }
1491
+
1492
+ // src/video/output.ts
1493
+ import { table as table3 } from "table";
1494
+ function renderVideoCandidatesAsJson(result) {
1495
+ return JSON.stringify(
1496
+ {
1497
+ ok: true,
1498
+ data: result,
1499
+ meta: {
1500
+ count: result.candidates.length
1501
+ }
1502
+ },
1503
+ null,
1504
+ 2
1505
+ );
1506
+ }
1507
+ function renderVideoDownloadResultAsJson(result) {
1508
+ return JSON.stringify(
1509
+ {
1510
+ ok: true,
1511
+ data: result
1512
+ },
1513
+ null,
1514
+ 2
1515
+ );
1516
+ }
1517
+ function renderVideoCompressionResultAsJson(result) {
1518
+ return JSON.stringify(
1519
+ {
1520
+ ok: true,
1521
+ data: result
1522
+ },
1523
+ null,
1524
+ 2
1525
+ );
1526
+ }
1527
+ function renderVideoCommandErrorAsJson(code, message, details) {
1528
+ return JSON.stringify(
1529
+ {
1530
+ ok: false,
1531
+ error: {
1532
+ code,
1533
+ message,
1534
+ ...details === void 0 ? {} : { details }
1535
+ }
1536
+ },
1537
+ null,
1538
+ 2
1539
+ );
1540
+ }
1541
+ function renderVideoCandidatesAsTable(result) {
1542
+ return renderVideoOperationAsTable([
1543
+ ["type", "url", "origin", "mode"],
1544
+ ...result.candidates.map((candidate) => [
1545
+ candidate.type,
1546
+ candidate.url,
1547
+ candidate.origin,
1548
+ result.mode
1549
+ ])
1550
+ ]);
1551
+ }
1552
+ function renderVideoOperationAsTable(rows) {
1553
+ return table3(rows);
1554
+ }
1555
+
1556
+ // src/video/command.ts
1557
+ var sniffOptionsSchema = z4.object({
1558
+ format: z4.enum(["json", "table"]).default("json"),
1559
+ mode: z4.enum(["auto", "browser", "http"]).default("auto"),
1560
+ proxy: z4.string().optional(),
1561
+ skipProxy: z4.boolean().default(false)
1562
+ });
1563
+ var downloadOptionsSchema = z4.object({
1564
+ format: z4.enum(["json", "table"]).default("json"),
1565
+ mode: z4.enum(["auto", "browser", "http"]).default("auto"),
1566
+ outputDir: z4.string().optional(),
1567
+ proxy: z4.string().optional(),
1568
+ skipProxy: z4.boolean().default(false)
1569
+ });
1570
+ var compressOptionsSchema = z4.object({
1571
+ audioBitrate: z4.string().optional(),
1572
+ format: z4.enum(["json", "table"]).default("json"),
1573
+ outputPath: z4.string().optional(),
1574
+ resolution: z4.string().optional(),
1575
+ videoBitrate: z4.string().optional()
1576
+ });
1577
+ function registerVideoCommands(program, dependencies) {
1578
+ const video = program.command("video").description("Inspect, download, and compress video media.");
1579
+ const browserProvider = new BrowserVideoSniffProvider({
1580
+ runtime: dependencies.runtime
1581
+ });
1582
+ const httpProvider = new HttpVideoSniffProvider({
1583
+ runtime: dependencies.runtime
1584
+ });
1585
+ const sniffService = new VideoSniffService({
1586
+ browserProvider: (sourceUrl, opts) => browserProvider.sniff(sourceUrl, opts),
1587
+ httpProvider: (sourceUrl, opts) => httpProvider.sniff(sourceUrl, opts)
1588
+ });
1589
+ const browserDriver = new BrowserDownloadDriver();
1590
+ const downloadService = new VideoDownloadService({
1591
+ directDriver: new DirectVideoDownloadDriver({
1592
+ runtime: dependencies.runtime
1593
+ }),
1594
+ hlsDriver: new HlsVideoDownloadDriver({
1595
+ runtime: dependencies.runtime
1596
+ }),
1597
+ sniffService,
1598
+ browserDriver
1599
+ });
1600
+ const compressionService = new VideoCompressionService({
1601
+ runtime: dependencies.runtime
1602
+ });
1603
+ video.command("sniff").argument("<input>", "page url or media url").option("--mode <mode>", "sniff mode", "auto").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").option("--proxy <proxy>", "proxy URL").option("--skip-proxy", "disable proxy").action(async (input2, options) => {
1604
+ const parsed = sniffOptionsSchema.parse(applyVideoTableShortcut(options));
1605
+ const result = await sniffService.sniff({
1606
+ mode: parsed.mode,
1607
+ sourceUrl: input2,
1608
+ ...parsed.proxy ? { proxyUrl: parsed.proxy } : {},
1609
+ ...parsed.skipProxy ? { noProxy: true } : {}
1610
+ });
1611
+ if (parsed.format === "table") {
1612
+ dependencies.stdout(`${renderVideoCandidatesAsTable(result)}
1613
+ `);
1614
+ return;
1615
+ }
1616
+ dependencies.stdout(`${renderVideoCandidatesAsJson(result)}
1617
+ `);
1618
+ });
1619
+ video.command("download").argument("<input>", "page url, m3u8 url, or mp4 url").option("--mode <mode>", "sniff mode", "auto").option("--output-dir <outputDir>", "output directory").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").option("--proxy <proxy>", "proxy URL").option("--skip-proxy", "disable proxy").action(async (input2, options) => {
1620
+ const parsed = downloadOptionsSchema.parse(applyVideoTableShortcut(options));
1621
+ const result = await downloadService.download({
1622
+ input: input2,
1623
+ mode: parsed.mode,
1624
+ ...parsed.outputDir ? { outputDir: parsed.outputDir } : {},
1625
+ ...parsed.proxy ? { proxyUrl: parsed.proxy } : {},
1626
+ ...parsed.skipProxy ? { noProxy: true } : {}
1627
+ });
1628
+ if (parsed.format === "table") {
1629
+ dependencies.stdout(
1630
+ `${renderVideoOperationAsTable([
1631
+ ["field", "value"],
1632
+ ["mediaType", result.mediaType],
1633
+ ["outputPath", result.outputPath]
1634
+ ])}
1635
+ `
1636
+ );
1637
+ return;
1638
+ }
1639
+ dependencies.stdout(`${renderVideoDownloadResultAsJson(result)}
1640
+ `);
1641
+ });
1642
+ video.command("compress").argument("<input>", "local input video path").option("--output-path <outputPath>", "output path").option("--resolution <resolution>", "target resolution, such as 1280x720").option("--video-bitrate <videoBitrate>", "target video bitrate").option("--audio-bitrate <audioBitrate>", "target audio bitrate").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action(async (input2, options) => {
1643
+ const parsed = compressOptionsSchema.parse(applyVideoTableShortcut(options));
1644
+ const result = await compressionService.compress({
1645
+ inputPath: input2,
1646
+ ...parsed.audioBitrate ? { audioBitrate: parsed.audioBitrate } : {},
1647
+ ...parsed.outputPath ? { outputPath: parsed.outputPath } : {},
1648
+ ...parsed.resolution ? { resolution: parsed.resolution } : {},
1649
+ ...parsed.videoBitrate ? { videoBitrate: parsed.videoBitrate } : {}
1650
+ });
1651
+ if (parsed.format === "table") {
1652
+ dependencies.stdout(
1653
+ `${renderVideoOperationAsTable([
1654
+ ["field", "value"],
1655
+ ["codec", result.codec],
1656
+ ["outputPath", result.outputPath]
1657
+ ])}
1658
+ `
1659
+ );
1660
+ return;
1661
+ }
1662
+ dependencies.stdout(`${renderVideoCompressionResultAsJson(result)}
1663
+ `);
1664
+ });
1665
+ }
1666
+ function handleVideoCommandError(error, dependencies) {
1667
+ if (error instanceof VideoCommandError) {
1668
+ const details = error.details;
1669
+ dependencies.stderr(
1670
+ `${renderVideoCommandErrorAsJson(
1671
+ error.code,
1672
+ error.message,
1673
+ details
1674
+ )}
1675
+ `
1676
+ );
1677
+ return error.exitCode;
1678
+ }
1679
+ return void 0;
1680
+ }
1681
+ function applyVideoTableShortcut(options) {
1682
+ if (options.table) {
1683
+ return {
1684
+ ...options,
1685
+ format: "table"
1686
+ };
1687
+ }
1688
+ if (options.format) {
1689
+ return {
1690
+ ...options,
1691
+ format: options.format
1692
+ };
1693
+ }
1694
+ throw new Error("Output format is required for this command.");
1695
+ }
1696
+
1697
+ // src/36kr/command.ts
1698
+ import { z as z5 } from "zod";
1699
+
1700
+ // src/36kr/output.ts
1701
+ import { table as table4 } from "table";
1702
+ function renderKr36ArticleAsJson(article) {
1703
+ return JSON.stringify(
1704
+ {
1705
+ ok: true,
1706
+ data: article
1707
+ },
1708
+ null,
1709
+ 2
1710
+ );
1711
+ }
1712
+ function renderKr36InformationListAsJson(list) {
1713
+ return JSON.stringify(
1714
+ {
1715
+ ok: true,
1716
+ data: list
1717
+ },
1718
+ null,
1719
+ 2
1720
+ );
1721
+ }
1722
+ function renderKr36InformationListAsTable(list) {
1723
+ return table4([
1724
+ ["id", "title", "author", "publishTime", "url"],
1725
+ ...list.items.map((item) => [
1726
+ String(item.id),
1727
+ item.title,
1728
+ item.authorName ?? "",
1729
+ item.publishTime?.local ?? "",
1730
+ item.url
1731
+ ])
1732
+ ]);
1733
+ }
1734
+ function renderKr36CommandErrorAsJson(code, message, details) {
1735
+ return JSON.stringify(
1736
+ {
1737
+ ok: false,
1738
+ error: {
1739
+ code,
1740
+ message,
1741
+ ...details === void 0 ? {} : { details }
1742
+ }
1743
+ },
1744
+ null,
1745
+ 2
1746
+ );
1747
+ }
1748
+
1749
+ // src/36kr/types.ts
1750
+ var Kr36CommandError = class extends Error {
1751
+ constructor(code, message, exitCode, details) {
1752
+ super(message);
1753
+ this.code = code;
1754
+ this.exitCode = exitCode;
1755
+ this.details = details;
1756
+ this.name = "Kr36CommandError";
1757
+ }
1758
+ };
1759
+
1760
+ // src/36kr/service.ts
1761
+ var KR36_BROWSER_HEADERS = {
1762
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
1763
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
1764
+ "Cache-Control": "no-cache",
1765
+ Pragma: "no-cache",
1766
+ Referer: "https://36kr.com/",
1767
+ "Sec-Fetch-Dest": "document",
1768
+ "Sec-Fetch-Mode": "navigate",
1769
+ "Sec-Fetch-Site": "same-origin",
1770
+ "Sec-Fetch-User": "?1",
1771
+ "Upgrade-Insecure-Requests": "1",
1772
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
1773
+ };
1774
+ var KR36_INFORMATION_ENDPOINT = "https://gateway.36kr.com/api/mis/nav/ifm/subNav/flow";
1775
+ var KR36_INFORMATION_PAGE_SIZE = 30;
1776
+ var KR36_SUPPORTED_CHANNELS = /* @__PURE__ */ new Set(["AI", "technology"]);
1777
+ var KR36_JSON_HEADERS = {
1778
+ Accept: "application/json, text/plain, */*",
1779
+ "Accept-Language": KR36_BROWSER_HEADERS["Accept-Language"],
1780
+ "Content-Type": "application/json;charset=UTF-8",
1781
+ Origin: "https://36kr.com",
1782
+ "User-Agent": KR36_BROWSER_HEADERS["User-Agent"]
1783
+ };
1784
+ var Kr36ArticleService = class {
1785
+ constructor(runtime) {
1786
+ this.runtime = runtime;
1787
+ }
1788
+ async getArticle(articleId) {
1789
+ assertArticleId(articleId);
1790
+ const request = buildKr36ArticleRequest(articleId);
1791
+ const html = await this.runtime.fetchArticleHtml(request);
1792
+ const initialState = parseInitialState(html);
1793
+ const articleDetail = initialState.articleDetail;
1794
+ const rawArticle = articleDetail?.articleDetailData?.data;
1795
+ if (!rawArticle) {
1796
+ throw new Kr36CommandError(
1797
+ "KR36_PARSE_ERROR",
1798
+ "36kr article data was not found in window.initialState.",
1799
+ 2
1800
+ );
1801
+ }
1802
+ const contentHtml = rawArticle.widgetContent ?? "";
1803
+ const recommend = articleDetail?.articleRecommendData ?? {};
1804
+ const publishTime = rawArticle.publishTime ?? 0;
1805
+ const article = {
1806
+ author: buildAuthor(rawArticle, recommend),
1807
+ content: {
1808
+ html: contentHtml,
1809
+ paragraphs: extractParagraphs(contentHtml)
1810
+ },
1811
+ id: String(rawArticle.itemId ?? articleId),
1812
+ imageSources: rawArticle.imgSources ?? [],
1813
+ images: extractImages(contentHtml),
1814
+ latestArticles: (articleDetail?.latestArticle?.articleLatestList ?? []).map((item) => ({
1815
+ id: item.id,
1816
+ title: item.title
1817
+ })),
1818
+ newestArticles: (recommend.newestItemList ?? []).map((item) => {
1819
+ const listItem = {
1820
+ id: item.itemId,
1821
+ title: item.itemTitle
1822
+ };
1823
+ if (item.itemContent !== void 0) {
1824
+ listItem.content = item.itemContent;
1825
+ }
1826
+ if (item.publishTime !== void 0) {
1827
+ listItem.publishTime = item.publishTime;
1828
+ }
1829
+ if (item.itemRoute !== void 0) {
1830
+ listItem.route = item.itemRoute;
1831
+ }
1832
+ return listItem;
1833
+ }),
1834
+ organizations: articleDetail?.organArticleData?.data?.organizationList ?? [],
1835
+ publishTime: {
1836
+ iso: publishTime ? new Date(publishTime).toISOString() : "",
1837
+ local: publishTime ? formatChinaTime(publishTime) : "",
1838
+ ms: publishTime
1839
+ },
1840
+ relatedArticles: (recommend.relateArticleList ?? []).map((item) => {
1841
+ const listItem = {
1842
+ id: item.itemId,
1843
+ title: item.widgetTitle
1844
+ };
1845
+ const author = item.author ?? item.authorName;
1846
+ if (author !== void 0) {
1847
+ listItem.author = author;
1848
+ }
1849
+ if (item.widgetImage !== void 0) {
1850
+ listItem.image = item.widgetImage;
1851
+ }
1852
+ if (item.route !== void 0) {
1853
+ listItem.route = item.route;
1854
+ }
1855
+ return listItem;
1856
+ }),
1857
+ request,
1858
+ stats: buildStats(articleDetail?.likeCount, articleDetail?.favoriteCount, recommend),
1859
+ summary: rawArticle.summary ?? "",
1860
+ title: rawArticle.widgetTitle ?? "",
1861
+ url: request.url
1862
+ };
1863
+ if (rawArticle.companyCertifyNick !== void 0) {
1864
+ article.companyCertifyNick = rawArticle.companyCertifyNick;
1865
+ }
1866
+ if (rawArticle.popinImage !== void 0) {
1867
+ article.coverImage = rawArticle.popinImage;
1868
+ }
1869
+ if (recommend.nextItem) {
1870
+ article.nextArticle = {
1871
+ id: recommend.nextItem.itemId,
1872
+ title: recommend.nextItem.itemTitle
1873
+ };
1874
+ if (recommend.nextItem.itemContent !== void 0) {
1875
+ article.nextArticle.content = recommend.nextItem.itemContent;
1876
+ }
1877
+ if (recommend.nextItem.publishTime !== void 0) {
1878
+ article.nextArticle.publishTime = recommend.nextItem.publishTime;
1879
+ }
1880
+ if (recommend.nextItem.itemRoute !== void 0) {
1881
+ article.nextArticle.route = recommend.nextItem.itemRoute;
1882
+ }
1883
+ }
1884
+ if (rawArticle.sourceType !== void 0) {
1885
+ article.sourceType = rawArticle.sourceType;
1886
+ }
1887
+ return article;
1888
+ }
1889
+ async getInformationList(options) {
1890
+ const channel = parseInformationChannel(options.channel);
1891
+ const pageLimit = normalizePages(options.pages);
1892
+ const firstPageRequest = buildKr36InformationFirstPageRequest(channel);
1893
+ const firstPageHtml = await this.runtime.fetchArticleHtml(firstPageRequest);
1894
+ const firstPageState = parseInitialState(firstPageHtml);
1895
+ const firstPageList = firstPageState.information?.informationList;
1896
+ if (!firstPageList) {
1897
+ throw new Kr36CommandError(
1898
+ "KR36_PARSE_ERROR",
1899
+ "36kr information list data was not found in window.initialState.",
1900
+ 2
1901
+ );
1902
+ }
1903
+ const items = [...mapInformationItems(firstPageList.itemList ?? [])];
1904
+ let pageCallback = firstPageList.pageCallback ?? "";
1905
+ let hasNextPage = firstPageList.hasNextPage ?? 0;
1906
+ let fetchedPages = 1;
1907
+ while (fetchedPages < pageLimit && hasNextPage && pageCallback) {
1908
+ const request = buildKr36InformationNextPageRequest(channel, pageCallback);
1909
+ const response = parseInformationFlowResponse(await this.runtime.fetchJson(request));
1910
+ const nextList = response.data ?? {};
1911
+ items.push(...mapInformationItems(nextList.itemList ?? []));
1912
+ pageCallback = nextList.pageCallback ?? "";
1913
+ hasNextPage = nextList.hasNextPage ?? 0;
1914
+ fetchedPages += 1;
1915
+ }
1916
+ return {
1917
+ channel,
1918
+ items,
1919
+ meta: {
1920
+ fetchedPages,
1921
+ hasNextPage,
1922
+ nextPageCallback: pageCallback,
1923
+ pageSize: KR36_INFORMATION_PAGE_SIZE,
1924
+ totalItems: items.length
1925
+ },
1926
+ request: {
1927
+ firstPage: firstPageRequest,
1928
+ nextPageEndpoint: KR36_INFORMATION_ENDPOINT
1929
+ }
1930
+ };
1931
+ }
1932
+ };
1933
+ function buildKr36ArticleUrl(articleId) {
1934
+ assertArticleId(articleId);
1935
+ return `https://36kr.com/p/${articleId}?f=rss`;
1936
+ }
1937
+ function buildKr36ArticleRequest(articleId) {
1938
+ return {
1939
+ headers: KR36_BROWSER_HEADERS,
1940
+ url: buildKr36ArticleUrl(articleId)
1941
+ };
1942
+ }
1943
+ function buildKr36InformationFirstPageRequest(channel) {
1944
+ return {
1945
+ headers: KR36_BROWSER_HEADERS,
1946
+ url: `https://36kr.com/information/${channel}/`
1947
+ };
1948
+ }
1949
+ function buildKr36InformationNextPageRequest(channel, pageCallback, timestamp = Date.now()) {
1950
+ return {
1951
+ body: {
1952
+ partner_id: "web",
1953
+ timestamp,
1954
+ param: {
1955
+ subnavType: 1,
1956
+ subnavNick: channel,
1957
+ pageSize: KR36_INFORMATION_PAGE_SIZE,
1958
+ pageEvent: 1,
1959
+ pageCallback,
1960
+ siteId: 1,
1961
+ platformId: 2
1962
+ }
1963
+ },
1964
+ headers: {
1965
+ ...KR36_JSON_HEADERS,
1966
+ Referer: `https://36kr.com/information/${channel}/`
1967
+ },
1968
+ url: KR36_INFORMATION_ENDPOINT
1969
+ };
1970
+ }
1971
+ function assertArticleId(articleId) {
1972
+ if (!/^\d+$/.test(articleId)) {
1973
+ throw new Kr36CommandError(
1974
+ "KR36_INVALID_ARTICLE_ID",
1975
+ "36kr article id must contain digits only.",
1976
+ 2,
1977
+ {
1978
+ articleId
1979
+ }
1980
+ );
1981
+ }
1982
+ }
1983
+ function parseInformationChannel(channel) {
1984
+ if (KR36_SUPPORTED_CHANNELS.has(channel)) {
1985
+ return channel;
1986
+ }
1987
+ throw new Kr36CommandError(
1988
+ "KR36_INVALID_CHANNEL",
1989
+ "36kr list only supports AI and technology channels.",
1990
+ 2,
1991
+ {
1992
+ channel,
1993
+ supportedChannels: [...KR36_SUPPORTED_CHANNELS]
1994
+ }
1995
+ );
1996
+ }
1997
+ function normalizePages(pages) {
1998
+ const value = pages ?? 1;
1999
+ if (!Number.isInteger(value) || value < 1 || value > 20) {
2000
+ throw new Kr36CommandError(
2001
+ "KR36_INVALID_PAGES",
2002
+ "36kr list pages must be an integer between 1 and 20.",
2003
+ 2,
2004
+ {
2005
+ pages: value
2006
+ }
2007
+ );
2008
+ }
2009
+ return value;
2010
+ }
2011
+ function parseInitialState(html) {
2012
+ const match = /window\.initialState=(\{[\s\S]*?\})\s*;?\s*<\/script>/.exec(html);
2013
+ if (!match?.[1]) {
2014
+ throw new Kr36CommandError(
2015
+ "KR36_PARSE_ERROR",
2016
+ "window.initialState was not found in the 36kr article page.",
2017
+ 2
2018
+ );
2019
+ }
2020
+ try {
2021
+ return JSON.parse(match[1]);
2022
+ } catch (error) {
2023
+ throw new Kr36CommandError(
2024
+ "KR36_PARSE_ERROR",
2025
+ "Failed to parse window.initialState from the 36kr article page.",
2026
+ 2,
2027
+ {
2028
+ cause: error instanceof Error ? error.message : String(error)
2029
+ }
2030
+ );
2031
+ }
2032
+ }
2033
+ function extractImages(html) {
2034
+ return [...html.matchAll(/<img\b[^>]*>/gi)].map((match, index) => {
2035
+ const tag = match[0];
2036
+ return {
2037
+ index: index + 1,
2038
+ size: getAttribute(tag, "data-img-size-val"),
2039
+ url: getAttribute(tag, "src") ?? ""
2040
+ };
2041
+ }).filter((image) => image.url);
2042
+ }
2043
+ function parseInformationFlowResponse(jsonText) {
2044
+ try {
2045
+ const response = JSON.parse(jsonText);
2046
+ if (response.code !== 0) {
2047
+ throw new Kr36CommandError(
2048
+ "KR36_REQUEST_FAILED",
2049
+ response.msg || "36kr information flow request failed.",
2050
+ 1,
2051
+ {
2052
+ code: response.code
2053
+ }
2054
+ );
2055
+ }
2056
+ return response;
2057
+ } catch (error) {
2058
+ if (error instanceof Kr36CommandError) {
2059
+ throw error;
2060
+ }
2061
+ throw new Kr36CommandError(
2062
+ "KR36_PARSE_ERROR",
2063
+ "Failed to parse 36kr information flow response.",
2064
+ 2,
2065
+ {
2066
+ cause: error instanceof Error ? error.message : String(error)
2067
+ }
2068
+ );
2069
+ }
2070
+ }
2071
+ function mapInformationItems(items) {
2072
+ return items.map((item) => {
2073
+ const material = item.templateMaterial ?? {};
2074
+ const mapped = {
2075
+ id: item.itemId,
2076
+ title: material.widgetTitle ?? "",
2077
+ url: `https://36kr.com/p/${item.itemId}`
2078
+ };
2079
+ if (material.authorName !== void 0) {
2080
+ mapped.authorName = material.authorName;
2081
+ }
2082
+ if (material.authorRoute !== void 0) {
2083
+ mapped.authorRoute = material.authorRoute;
2084
+ }
2085
+ if (material.widgetImage !== void 0) {
2086
+ mapped.image = material.widgetImage;
2087
+ }
2088
+ if (material.publishTime !== void 0) {
2089
+ mapped.publishTime = {
2090
+ iso: new Date(material.publishTime).toISOString(),
2091
+ local: formatChinaTime(material.publishTime),
2092
+ ms: material.publishTime
2093
+ };
2094
+ }
2095
+ if (item.route !== void 0) {
2096
+ mapped.route = item.route;
2097
+ }
2098
+ if (material.summary !== void 0) {
2099
+ mapped.summary = material.summary;
2100
+ }
2101
+ return mapped;
2102
+ });
2103
+ }
2104
+ function extractParagraphs(html) {
2105
+ return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, "\n").split(/\n+/).map((value) => decodeHtmlEntities(value).trim()).filter(Boolean);
2106
+ }
2107
+ function getAttribute(tag, name) {
2108
+ const match = new RegExp(`${name}=["']([^"']+)["']`, "i").exec(tag);
2109
+ return match?.[1] ?? null;
2110
+ }
2111
+ function decodeHtmlEntities(value) {
2112
+ return value.replace(/&nbsp;/g, " ").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
2113
+ }
2114
+ function buildStats(likeCount, favoriteCount, recommend) {
2115
+ return {
2116
+ authorArticleCount: recommend.statArticle ?? 0,
2117
+ collect: recommend.statCollect ?? 0,
2118
+ comment: recommend.statComment ?? 0,
2119
+ favoriteCount: favoriteCount ?? 0,
2120
+ likeCount: likeCount ?? 0,
2121
+ praise: recommend.statPraise ?? 0
2122
+ };
2123
+ }
2124
+ function buildAuthor(rawArticle, recommend) {
2125
+ const author = {
2126
+ name: rawArticle.author ?? recommend.authorName ?? ""
2127
+ };
2128
+ const face = rawArticle.authorFace ?? recommend.authorFace;
2129
+ if (face !== void 0) {
2130
+ author.face = face;
2131
+ }
2132
+ if (rawArticle.authorId !== void 0) {
2133
+ author.id = rawArticle.authorId;
2134
+ }
2135
+ if (rawArticle.authorRoute !== void 0) {
2136
+ author.route = rawArticle.authorRoute;
2137
+ }
2138
+ if (recommend.authorSummary !== void 0) {
2139
+ author.summary = recommend.authorSummary;
2140
+ }
2141
+ if (recommend.authorTitle !== void 0) {
2142
+ author.title = recommend.authorTitle;
2143
+ }
2144
+ return author;
2145
+ }
2146
+ function formatChinaTime(timestampMs) {
2147
+ const formatter = new Intl.DateTimeFormat("sv-SE", {
2148
+ day: "2-digit",
2149
+ hour: "2-digit",
2150
+ hour12: false,
2151
+ minute: "2-digit",
2152
+ month: "2-digit",
2153
+ second: "2-digit",
2154
+ timeZone: "Asia/Shanghai",
2155
+ year: "numeric"
2156
+ });
2157
+ return formatter.format(new Date(timestampMs));
2158
+ }
2159
+
2160
+ // src/36kr/command.ts
2161
+ var articleOptionsSchema = z5.object({
2162
+ format: z5.enum(["json"]).default("json")
2163
+ });
2164
+ var informationListOptionsSchema = z5.object({
2165
+ format: z5.enum(["json", "table"]).default("json"),
2166
+ pages: z5.number().int().min(1).max(20).default(1),
2167
+ table: z5.boolean().optional()
2168
+ });
2169
+ function registerKr36Commands(program, dependencies) {
2170
+ const kr36 = program.command("36kr").description("Fetch and parse 36kr content.");
2171
+ const articleService = new Kr36ArticleService(dependencies.runtime);
2172
+ kr36.command("article").argument("<articleId>", "36kr article id").option("--format <format>", "output format", "json").action(async (articleId, options) => {
2173
+ articleOptionsSchema.parse(options);
2174
+ const article = await articleService.getArticle(articleId);
2175
+ dependencies.stdout(`${renderKr36ArticleAsJson(article)}
2176
+ `);
2177
+ });
2178
+ kr36.command("list").argument("<channel>", "36kr information channel: AI or technology").option("--pages <pages>", "number of pages to fetch", parsePages, 1).option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action(async (channel, options) => {
2179
+ const parsed = informationListOptionsSchema.parse(applyTableShortcut(options));
2180
+ const list = await articleService.getInformationList({
2181
+ channel,
2182
+ pages: parsed.pages
2183
+ });
2184
+ dependencies.stdout(
2185
+ parsed.format === "table" ? `${renderKr36InformationListAsTable(list)}
2186
+ ` : `${renderKr36InformationListAsJson(list)}
2187
+ `
2188
+ );
2189
+ });
2190
+ }
2191
+ function handleKr36CommandError(error, dependencies) {
2192
+ if (error instanceof Kr36CommandError) {
2193
+ dependencies.stderr(
2194
+ `${renderKr36CommandErrorAsJson(error.code, error.message, error.details)}
2195
+ `
2196
+ );
2197
+ return error.exitCode;
2198
+ }
2199
+ return void 0;
2200
+ }
2201
+ function parsePages(value) {
2202
+ const parsed = Number(value);
2203
+ if (!Number.isInteger(parsed)) {
2204
+ throw new Kr36CommandError(
2205
+ "KR36_INVALID_PAGES",
2206
+ "36kr list pages must be an integer between 1 and 20.",
2207
+ 2,
2208
+ {
2209
+ pages: value
2210
+ }
2211
+ );
2212
+ }
2213
+ return parsed;
2214
+ }
2215
+ function applyTableShortcut(options) {
2216
+ if (options.table) {
2217
+ return {
2218
+ ...options,
2219
+ format: "table"
2220
+ };
2221
+ }
2222
+ return {
2223
+ ...options,
2224
+ format: options.format ?? "json"
2225
+ };
2226
+ }
2227
+
2228
+ // src/toutiao/command.ts
2229
+ import { z as z6 } from "zod";
2230
+
2231
+ // src/toutiao/output.ts
2232
+ import { table as table5 } from "table";
2233
+ function renderToutiaoListAsJson(result) {
2234
+ return JSON.stringify(
2235
+ {
2236
+ ok: true,
2237
+ data: result
2238
+ },
2239
+ null,
2240
+ 2
2241
+ );
2242
+ }
2243
+ function renderToutiaoListAsTable(result) {
2244
+ return table5([
2245
+ ["id", "title", "author", "publishTime", "comments", "url"],
2246
+ ...result.items.map((item) => [
2247
+ item.id,
2248
+ item.title,
2249
+ item.authorName ?? "",
2250
+ item.publishTime?.local ?? "",
2251
+ item.commentCount === void 0 ? "" : String(item.commentCount),
2252
+ item.url
2253
+ ])
2254
+ ]);
2255
+ }
2256
+ function renderToutiaoArticleAsJson(article) {
2257
+ return JSON.stringify(
2258
+ {
2259
+ ok: true,
2260
+ data: article
2261
+ },
2262
+ null,
2263
+ 2
2264
+ );
2265
+ }
2266
+ function renderToutiaoAuthorAsJson(result) {
2267
+ return JSON.stringify(
2268
+ {
2269
+ ok: true,
2270
+ data: result
2271
+ },
2272
+ null,
2273
+ 2
2274
+ );
2275
+ }
2276
+ function renderToutiaoAuthorAsTable(result) {
2277
+ return table5([
2278
+ ["id", "title", "author", "publishTime", "comments", "url"],
2279
+ ...result.items.map((item) => [
2280
+ item.id,
2281
+ item.title,
2282
+ item.authorName ?? "",
2283
+ item.publishTime?.local ?? "",
2284
+ item.commentCount === void 0 ? "" : String(item.commentCount),
2285
+ item.url
2286
+ ])
2287
+ ]);
2288
+ }
2289
+ function renderToutiaoCommandErrorAsJson(code, message, details) {
2290
+ return JSON.stringify(
2291
+ {
2292
+ ok: false,
2293
+ error: {
2294
+ code,
2295
+ message,
2296
+ ...details === void 0 ? {} : { details }
2297
+ }
2298
+ },
2299
+ null,
2300
+ 2
2301
+ );
2302
+ }
2303
+
2304
+ // src/toutiao/types.ts
2305
+ var ToutiaoCommandError = class extends Error {
2306
+ constructor(code, message, exitCode, details) {
2307
+ super(message);
2308
+ this.code = code;
2309
+ this.exitCode = exitCode;
2310
+ this.details = details;
2311
+ this.name = "ToutiaoCommandError";
2312
+ }
2313
+ };
2314
+
2315
+ // src/toutiao/service.ts
2316
+ var SUPPORTED_SOURCES = /* @__PURE__ */ new Set(["tech", "AI", "\u5149\u523B\u673A", "\u82AF\u7247", "\u534A\u5BFC\u4F53"]);
2317
+ var ToutiaoService = class {
2318
+ constructor(dependencies) {
2319
+ this.dependencies = dependencies;
2320
+ }
2321
+ async article(input2) {
2322
+ const url = buildToutiaoArticleUrl(input2);
2323
+ return this.dependencies.runtime.fetchArticle({
2324
+ input: input2,
2325
+ url
2326
+ });
2327
+ }
2328
+ async author(input2, options = {}) {
2329
+ const authorToken = buildToutiaoAuthorToken(input2);
2330
+ const pages = normalizePages2(options.pages);
2331
+ const url = buildToutiaoAuthorUrl(authorToken);
2332
+ const runtimeResult = await this.dependencies.runtime.fetchAuthorArticles({
2333
+ authorToken,
2334
+ pages,
2335
+ url
2336
+ });
2337
+ const withContent = options.withContent ?? false;
2338
+ const result = {
2339
+ ...runtimeResult,
2340
+ meta: {
2341
+ fetchedPages: pages,
2342
+ totalItems: runtimeResult.items.length,
2343
+ withContent
2344
+ },
2345
+ request: {
2346
+ input: input2,
2347
+ url
2348
+ }
2349
+ };
2350
+ if (withContent) {
2351
+ const articles = [];
2352
+ for (const item of runtimeResult.items) {
2353
+ articles.push(await this.dependencies.runtime.fetchArticle({
2354
+ input: item.id,
2355
+ url: buildToutiaoArticleUrl(item.url || item.id)
2356
+ }));
2357
+ }
2358
+ result.articles = articles;
2359
+ result.meta.totalArticles = articles.length;
2360
+ }
2361
+ return result;
2362
+ }
2363
+ async list(options) {
2364
+ const source = parseSource(options.source);
2365
+ const pages = normalizePages2(options.pages);
2366
+ const runtimeResult = source === "tech" ? await this.dependencies.runtime.fetchTechnologyChannel({ pages }) : await this.dependencies.runtime.fetchKeywordInformation({
2367
+ keyword: source,
2368
+ pages,
2369
+ source
2370
+ });
2371
+ return {
2372
+ ...runtimeResult,
2373
+ meta: {
2374
+ fetchedPages: pages,
2375
+ totalItems: runtimeResult.items.length
2376
+ }
2377
+ };
2378
+ }
2379
+ };
2380
+ function buildToutiaoAuthorToken(input2) {
2381
+ const trimmed = input2.trim();
2382
+ if (/^[A-Za-z0-9._-]+$/.test(trimmed) && trimmed.length >= 12) {
2383
+ return trimmed;
2384
+ }
2385
+ try {
2386
+ const url = new URL(trimmed);
2387
+ const token = /\/c\/user\/token\/([^/?#]+)/.exec(url.pathname)?.[1];
2388
+ if (token && isToutiaoHost(url.hostname)) {
2389
+ return decodeURIComponent(token);
2390
+ }
2391
+ } catch {
2392
+ }
2393
+ throw new ToutiaoCommandError(
2394
+ "TOUTIAO_INVALID_AUTHOR",
2395
+ "Toutiao author must be a user token or a /c/user/token/<token>/ homepage URL.",
2396
+ 2,
2397
+ {
2398
+ input: input2
2399
+ }
2400
+ );
2401
+ }
2402
+ function buildToutiaoAuthorUrl(authorToken) {
2403
+ return `https://www.toutiao.com/c/user/token/${encodeURIComponent(authorToken)}/`;
2404
+ }
2405
+ function buildToutiaoArticleUrl(input2) {
2406
+ const trimmed = input2.trim();
2407
+ if (/^\d+$/.test(trimmed)) {
2408
+ return `https://www.toutiao.com/article/${trimmed}/`;
2409
+ }
2410
+ try {
2411
+ const url = new URL(trimmed);
2412
+ const id = /(?:article|group)\/(\d+)/.exec(url.pathname)?.[1] ?? /^\/a(\d+)/.exec(url.pathname)?.[1];
2413
+ if (id && isToutiaoHost(url.hostname)) {
2414
+ return `https://www.toutiao.com/article/${id}/`;
2415
+ }
2416
+ const nextUrl = url.searchParams.get("url");
2417
+ if (nextUrl) {
2418
+ return buildToutiaoArticleUrl(nextUrl);
2419
+ }
2420
+ } catch {
2421
+ }
2422
+ throw new ToutiaoCommandError(
2423
+ "TOUTIAO_INVALID_ARTICLE",
2424
+ "Toutiao article must be a numeric id or a toutiao article/group URL.",
2425
+ 2,
2426
+ {
2427
+ input: input2
2428
+ }
2429
+ );
2430
+ }
2431
+ function isToutiaoHost(hostname) {
2432
+ return hostname === "www.toutiao.com" || hostname === "toutiao.com";
2433
+ }
2434
+ function parseSource(source) {
2435
+ if (SUPPORTED_SOURCES.has(source)) {
2436
+ return source;
2437
+ }
2438
+ throw new ToutiaoCommandError(
2439
+ "TOUTIAO_INVALID_SOURCE",
2440
+ "Toutiao list only supports tech, AI, \u5149\u523B\u673A, \u82AF\u7247, and \u534A\u5BFC\u4F53.",
2441
+ 2,
2442
+ {
2443
+ source,
2444
+ supportedSources: [...SUPPORTED_SOURCES]
2445
+ }
2446
+ );
2447
+ }
2448
+ function normalizePages2(pages) {
2449
+ const value = pages ?? 1;
2450
+ if (!Number.isInteger(value) || value < 1 || value > 5) {
2451
+ throw new ToutiaoCommandError(
2452
+ "TOUTIAO_INVALID_PAGES",
2453
+ "Toutiao list pages must be an integer between 1 and 5.",
2454
+ 2,
2455
+ {
2456
+ pages: value
2457
+ }
2458
+ );
2459
+ }
2460
+ return value;
2461
+ }
2462
+
2463
+ // src/toutiao/command.ts
2464
+ var listOptionsSchema = z6.object({
2465
+ format: z6.enum(["json", "table"]).default("json"),
2466
+ pages: z6.number().int().min(1).max(5).default(1),
2467
+ table: z6.boolean().optional()
2468
+ });
2469
+ var articleOptionsSchema2 = z6.object({
2470
+ format: z6.enum(["json"]).default("json")
2471
+ });
2472
+ var authorOptionsSchema = z6.object({
2473
+ format: z6.enum(["json", "table"]).default("json"),
2474
+ pages: z6.number().int().min(1).max(5).default(1),
2475
+ table: z6.boolean().optional(),
2476
+ withContent: z6.boolean().default(false)
2477
+ });
2478
+ function registerToutiaoCommands(program, dependencies) {
2479
+ const toutiao = program.command("toutiao").description("Fetch Toutiao channel and search content.");
2480
+ const service = new ToutiaoService({ runtime: dependencies.runtime });
2481
+ toutiao.command("article").argument("<article>", "Toutiao article id or URL").option("--format <format>", "output format", "json").action(async (article, options) => {
2482
+ articleOptionsSchema2.parse(options);
2483
+ const result = await service.article(article);
2484
+ dependencies.stdout(`${renderToutiaoArticleAsJson(result)}
2485
+ `);
2486
+ });
2487
+ toutiao.command("list").argument("<source>", "Toutiao source: tech, AI, \u5149\u523B\u673A, \u82AF\u7247, or \u534A\u5BFC\u4F53").option("--pages <pages>", "number of pages to fetch", parsePages2, 1).option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action(async (source, options) => {
2488
+ const parsed = listOptionsSchema.parse(applyTableShortcut2(options));
2489
+ const result = await service.list({
2490
+ pages: parsed.pages,
2491
+ source
2492
+ });
2493
+ dependencies.stdout(
2494
+ parsed.format === "table" ? `${renderToutiaoListAsTable(result)}
2495
+ ` : `${renderToutiaoListAsJson(result)}
2496
+ `
2497
+ );
2498
+ });
2499
+ toutiao.command("author").argument("<author>", "Toutiao author token or /c/user/token/<token>/ homepage URL").option("--pages <pages>", "number of pages to fetch", parsePages2, 1).option("--with-content", "also fetch detail content for each author article").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action(async (author, options) => {
2500
+ const parsed = authorOptionsSchema.parse(applyTableShortcut2(options));
2501
+ const result = await service.author(author, {
2502
+ pages: parsed.pages,
2503
+ withContent: parsed.withContent
2504
+ });
2505
+ dependencies.stdout(
2506
+ parsed.format === "table" ? `${renderToutiaoAuthorAsTable(result)}
2507
+ ` : `${renderToutiaoAuthorAsJson(result)}
2508
+ `
2509
+ );
2510
+ });
2511
+ }
2512
+ function handleToutiaoCommandError(error, dependencies) {
2513
+ if (error instanceof ToutiaoCommandError) {
2514
+ dependencies.stderr(
2515
+ `${renderToutiaoCommandErrorAsJson(error.code, error.message, error.details)}
2516
+ `
2517
+ );
2518
+ return error.exitCode;
2519
+ }
2520
+ return void 0;
2521
+ }
2522
+ function parsePages2(value) {
2523
+ const parsed = Number(value);
2524
+ if (!Number.isInteger(parsed)) {
2525
+ throw new ToutiaoCommandError(
2526
+ "TOUTIAO_INVALID_PAGES",
2527
+ "Toutiao list pages must be an integer between 1 and 5.",
2528
+ 2,
2529
+ {
2530
+ pages: value
2531
+ }
2532
+ );
2533
+ }
2534
+ return parsed;
2535
+ }
2536
+ function applyTableShortcut2(options) {
2537
+ if (options.table) {
2538
+ return {
2539
+ ...options,
2540
+ format: "table"
2541
+ };
2542
+ }
2543
+ return {
2544
+ ...options,
2545
+ format: options.format ?? "json"
2546
+ };
2547
+ }
2548
+
2549
+ // src/hn/command.ts
2550
+ import { z as z7 } from "zod";
2551
+
2552
+ // src/hn/output.ts
2553
+ import { table as table6 } from "table";
2554
+ function renderHackerNewsStoriesAsJson(result) {
2555
+ return JSON.stringify(
2556
+ {
2557
+ ok: true,
2558
+ data: result
2559
+ },
2560
+ null,
2561
+ 2
2562
+ );
2563
+ }
2564
+ function renderHackerNewsStoriesAsTable(result) {
2565
+ return table6([
2566
+ ["id", "title", "author", "score", "comments", "time", "url"],
2567
+ ...result.items.map((item) => [
2568
+ String(item.id),
2569
+ item.title,
2570
+ item.author ?? "",
2571
+ item.score === void 0 ? "" : String(item.score),
2572
+ item.commentCount === void 0 ? "" : String(item.commentCount),
2573
+ item.time?.iso ?? "",
2574
+ item.url
2575
+ ])
2576
+ ]);
2577
+ }
2578
+ function renderHackerNewsSearchAsJson(result) {
2579
+ return JSON.stringify(
2580
+ {
2581
+ ok: true,
2582
+ data: result
2583
+ },
2584
+ null,
2585
+ 2
2586
+ );
2587
+ }
2588
+ function renderHackerNewsSearchAsTable(result) {
2589
+ return table6([
2590
+ ["id", "title", "author", "score", "comments", "time", "url"],
2591
+ ...result.items.map((item) => [
2592
+ String(item.id),
2593
+ item.title,
2594
+ item.author ?? "",
2595
+ item.score === void 0 ? "" : String(item.score),
2596
+ item.commentCount === void 0 ? "" : String(item.commentCount),
2597
+ item.time?.iso ?? "",
2598
+ item.url
2599
+ ])
2600
+ ]);
2601
+ }
2602
+ function renderHackerNewsCommandErrorAsJson(code, message, details) {
2603
+ return JSON.stringify(
2604
+ {
2605
+ ok: false,
2606
+ error: {
2607
+ code,
2608
+ message,
2609
+ ...details === void 0 ? {} : { details }
2610
+ }
2611
+ },
2612
+ null,
2613
+ 2
2614
+ );
2615
+ }
2616
+
2617
+ // src/hn/types.ts
2618
+ var HackerNewsCommandError = class extends Error {
2619
+ constructor(code, message, exitCode, details) {
2620
+ super(message);
2621
+ this.code = code;
2622
+ this.exitCode = exitCode;
2623
+ this.details = details;
2624
+ this.name = "HackerNewsCommandError";
2625
+ }
2626
+ };
2627
+
2628
+ // src/hn/service.ts
2629
+ var SUPPORTED_SOURCES2 = /* @__PURE__ */ new Set(["top", "new", "best"]);
2630
+ var HackerNewsService = class {
2631
+ constructor(dependencies) {
2632
+ this.dependencies = dependencies;
2633
+ }
2634
+ async stories(options) {
2635
+ const source = parseSource2(options.source);
2636
+ const limit = normalizeLimit(options.limit);
2637
+ const runtimeResult = await this.dependencies.runtime.fetchStories({
2638
+ limit,
2639
+ source
2640
+ });
2641
+ return {
2642
+ ...runtimeResult,
2643
+ meta: {
2644
+ limit,
2645
+ totalItems: runtimeResult.items.length
2646
+ }
2647
+ };
2648
+ }
2649
+ async search(options) {
2650
+ const query = normalizeQuery(options.query);
2651
+ const limit = normalizeLimit(options.limit);
2652
+ const sort = options.sort ?? "relevance";
2653
+ const runtimeResult = await this.dependencies.runtime.fetchSearch({
2654
+ limit,
2655
+ query,
2656
+ sort
2657
+ });
2658
+ return {
2659
+ ...runtimeResult,
2660
+ meta: {
2661
+ limit,
2662
+ sort,
2663
+ totalItems: runtimeResult.items.length
2664
+ }
2665
+ };
2666
+ }
2667
+ };
2668
+ function parseSource2(source) {
2669
+ if (SUPPORTED_SOURCES2.has(source)) {
2670
+ return source;
2671
+ }
2672
+ throw new HackerNewsCommandError(
2673
+ "HN_INVALID_SOURCE",
2674
+ "Hacker News list only supports top, new, and best.",
2675
+ 2,
2676
+ {
2677
+ source,
2678
+ supportedSources: [...SUPPORTED_SOURCES2]
2679
+ }
2680
+ );
2681
+ }
2682
+ function normalizeLimit(limit) {
2683
+ const value = limit ?? 30;
2684
+ if (!Number.isInteger(value) || value < 1 || value > 100) {
2685
+ throw new HackerNewsCommandError(
2686
+ "HN_INVALID_LIMIT",
2687
+ "Hacker News limit must be an integer between 1 and 100.",
2688
+ 2,
2689
+ {
2690
+ limit: value
2691
+ }
2692
+ );
2693
+ }
2694
+ return value;
2695
+ }
2696
+ function normalizeQuery(query) {
2697
+ const value = query.trim();
2698
+ if (value.length === 0) {
2699
+ throw new HackerNewsCommandError(
2700
+ "HN_INVALID_QUERY",
2701
+ "Hacker News search query must not be empty.",
2702
+ 2
2703
+ );
2704
+ }
2705
+ return value;
2706
+ }
2707
+
2708
+ // src/hn/command.ts
2709
+ var storiesOptionsSchema = z7.object({
2710
+ format: z7.enum(["json", "table"]).default("json"),
2711
+ limit: z7.number().int().default(30),
2712
+ table: z7.boolean().optional()
2713
+ });
2714
+ var searchOptionsSchema = z7.object({
2715
+ format: z7.enum(["json", "table"]).default("json"),
2716
+ limit: z7.number().int().default(30),
2717
+ sort: z7.enum(["relevance", "date"]).default("relevance"),
2718
+ table: z7.boolean().optional()
2719
+ });
2720
+ function registerHackerNewsCommands(program, dependencies) {
2721
+ const hn = program.command("hn").description("Fetch Hacker News stories and search results.");
2722
+ const service = new HackerNewsService({ runtime: dependencies.runtime });
2723
+ for (const source of ["top", "new", "best"]) {
2724
+ hn.command(source).option("--limit <limit>", "number of stories to fetch", parseLimit, 30).option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action(async (options) => {
2725
+ const parsed = storiesOptionsSchema.parse(applyTableShortcut3(options));
2726
+ const result = await service.stories({
2727
+ limit: parsed.limit,
2728
+ source
2729
+ });
2730
+ dependencies.stdout(
2731
+ parsed.format === "table" ? `${renderHackerNewsStoriesAsTable(result)}
2732
+ ` : `${renderHackerNewsStoriesAsJson(result)}
2733
+ `
2734
+ );
2735
+ });
2736
+ }
2737
+ hn.command("search").argument("<query>", "search query").option("--limit <limit>", "number of search results to fetch", parseLimit, 30).option("--sort <sort>", "search sort: relevance or date", "relevance").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action(async (query, options) => {
2738
+ const parsed = searchOptionsSchema.parse(applyTableShortcut3(options));
2739
+ const result = await service.search({
2740
+ limit: parsed.limit,
2741
+ query,
2742
+ sort: parsed.sort
2743
+ });
2744
+ dependencies.stdout(
2745
+ parsed.format === "table" ? `${renderHackerNewsSearchAsTable(result)}
2746
+ ` : `${renderHackerNewsSearchAsJson(result)}
2747
+ `
2748
+ );
2749
+ });
2750
+ }
2751
+ function handleHackerNewsCommandError(error, dependencies) {
2752
+ if (error instanceof HackerNewsCommandError) {
2753
+ dependencies.stderr(
2754
+ `${renderHackerNewsCommandErrorAsJson(error.code, error.message, error.details)}
2755
+ `
2756
+ );
2757
+ return error.exitCode;
2758
+ }
2759
+ return void 0;
2760
+ }
2761
+ function parseLimit(value) {
2762
+ const parsed = Number(value);
2763
+ if (!Number.isInteger(parsed)) {
2764
+ throw new HackerNewsCommandError(
2765
+ "HN_INVALID_LIMIT",
2766
+ "Hacker News limit must be an integer between 1 and 100.",
2767
+ 2,
2768
+ {
2769
+ limit: value
2770
+ }
2771
+ );
2772
+ }
2773
+ return parsed;
2774
+ }
2775
+ function applyTableShortcut3(options) {
2776
+ if (options.table) {
2777
+ return {
2778
+ ...options,
2779
+ format: "table"
2780
+ };
2781
+ }
2782
+ return {
2783
+ ...options,
2784
+ format: options.format ?? "json"
2785
+ };
2786
+ }
2787
+
2788
+ // src/ak/command.ts
2789
+ var envSchema = z8.string().transform((value, context) => {
2790
+ try {
2791
+ return normalizeAkEnv(value);
2792
+ } catch (error) {
2793
+ context.addIssue({
2794
+ code: "custom",
2795
+ message: error.message
2796
+ });
2797
+ return z8.NEVER;
2798
+ }
2799
+ });
2800
+ var envDescription = `environment (recommended: ${AK_RECOMMENDED_ENVS.join(", ")}; custom values allowed)`;
2801
+ var addOptionsSchema = z8.object({
2802
+ email: z8.string().optional(),
2803
+ env: envSchema,
2804
+ key: z8.string(),
2805
+ phone: z8.string().optional(),
2806
+ rawKey: z8.boolean().optional(),
2807
+ userId: z8.string().optional(),
2808
+ userName: z8.string().optional()
2809
+ });
2810
+ var listOptionsSchema2 = z8.object({
2811
+ env: envSchema.optional(),
2812
+ field: z8.string().optional(),
2813
+ format: z8.enum(["json", "table"]).default("json"),
2814
+ limit: z8.number().int().positive().max(100).default(50),
2815
+ query: z8.string().optional(),
2816
+ rawKey: z8.boolean().optional()
2817
+ });
2818
+ var getOptionsSchema = z8.object({
2819
+ env: envSchema.optional(),
2820
+ format: z8.enum(["json", "table"]).default("json"),
2821
+ id: z8.string().optional(),
2822
+ key: z8.string().optional(),
2823
+ rawKey: z8.boolean().optional()
2824
+ });
2825
+ var updateOptionsSchema = z8.object({
2826
+ email: z8.string().optional(),
2827
+ env: envSchema.optional(),
2828
+ format: z8.enum(["json", "table"]).default("json"),
2829
+ id: z8.string().optional(),
2830
+ key: z8.string().optional(),
2831
+ phone: z8.string().optional(),
2832
+ rawKey: z8.boolean().optional(),
2833
+ userId: z8.string().optional(),
2834
+ userName: z8.string().optional()
2835
+ });
2836
+ var deleteOptionsSchema = z8.object({
2837
+ env: envSchema.optional(),
2838
+ format: z8.enum(["json", "table"]).default("json"),
2839
+ id: z8.string().optional(),
2840
+ key: z8.string().optional(),
2841
+ yes: z8.boolean().default(false)
2842
+ });
2843
+ var configFormatSchema = z8.object({
2844
+ format: z8.enum(["json", "table"]).default("json")
2845
+ });
2846
+ var configGetOptionsSchema = z8.object({
2847
+ format: z8.enum(["json", "table"]).default("json"),
2848
+ key: z8.string()
2849
+ });
2850
+ var configSetOptionsSchema = z8.object({
2851
+ format: z8.enum(["json", "table"]).default("json"),
2852
+ key: z8.string(),
2853
+ value: z8.string()
2854
+ });
2855
+ function createAkCli(dependencies) {
2856
+ const program = new Command();
2857
+ const createService = () => new AkService({
2858
+ masterKey: dependencies.masterKey,
2859
+ now: dependencies.now,
2860
+ repository: dependencies.getRepository()
2861
+ });
2862
+ const createDiskCleanupService = () => {
2863
+ const roots = dependencies.resolveDiskCleanupRoots();
2864
+ return new DiskCleanupService({
2865
+ runtime: dependencies.diskRuntime,
2866
+ systemRoot: roots.systemRoot,
2867
+ userProfileRoot: roots.userProfileRoot
2868
+ });
2869
+ };
2870
+ program.name("stephen").description("A personal TypeScript CLI for agent-friendly workflows.").showHelpAfterError().exitOverride();
2871
+ program.configureOutput({
2872
+ outputError: (value) => dependencies.stderr(value),
2873
+ writeErr: (value) => dependencies.stderr(value),
2874
+ writeOut: (value) => dependencies.stdout(value)
2875
+ });
2876
+ const ak = program.command("ak").description("Manage API key records.");
2877
+ const config = program.command("config").description("Manage local CLI configuration.");
2878
+ registerDiskCommands(program, {
2879
+ createDiskCleanupService,
2880
+ stdout: dependencies.stdout
2881
+ });
2882
+ registerVideoCommands(program, {
2883
+ runtime: dependencies.videoRuntime,
2884
+ stderr: dependencies.stderr,
2885
+ stdout: dependencies.stdout
2886
+ });
2887
+ registerKr36Commands(program, {
2888
+ runtime: dependencies.kr36Runtime,
2889
+ stderr: dependencies.stderr,
2890
+ stdout: dependencies.stdout
2891
+ });
2892
+ registerToutiaoCommands(program, {
2893
+ runtime: dependencies.toutiaoRuntime,
2894
+ stderr: dependencies.stderr,
2895
+ stdout: dependencies.stdout
2896
+ });
2897
+ registerHackerNewsCommands(program, {
2898
+ runtime: dependencies.hackerNewsRuntime,
2899
+ stderr: dependencies.stderr,
2900
+ stdout: dependencies.stdout
2901
+ });
2902
+ ak.command("add").requiredOption("-e, --env <env>", envDescription).requiredOption("-k, --key <key>", "api key").option("-u, --user-id <userId>", "user id").option("-n, --user-name <userName>", "user name").option("-m, --email <email>", "email").option("-p, --phone <phone>", "phone").option("--raw-key", "show the raw key in the output").action((options) => {
2903
+ const parsed = addOptionsSchema.parse(options);
2904
+ writeRecords([createService().add(parsed)], "json", parsed.rawKey ?? false, 50, dependencies);
2905
+ });
2906
+ ak.command("get").option("--id <id>", "record id").option("-e, --env <env>", envDescription).option("-k, --key <key>", "api key").option("--raw-key", "show the raw key in the output").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action((options) => {
2907
+ const parsed = getOptionsSchema.parse(applyTableShortcut4(options));
2908
+ writeRecords(
2909
+ [createService().get(parsed)],
2910
+ parsed.format,
2911
+ parsed.rawKey ?? false,
2912
+ 1,
2913
+ dependencies
2914
+ );
2915
+ });
2916
+ ak.command("list").option("-e, --env <env>", envDescription).option("-q, --query <query>", "fuzzy query").option("-f, --field <field>", "query fields").option("--limit <limit>", "result limit", parseLimit2, 50).option("--raw-key", "show the raw key in the output").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action((options) => {
2917
+ const parsed = listOptionsSchema2.parse(applyTableShortcut4(options));
2918
+ writeRecords(
2919
+ createService().list(parsed),
2920
+ parsed.format,
2921
+ parsed.rawKey ?? false,
2922
+ parsed.limit,
2923
+ dependencies
2924
+ );
2925
+ });
2926
+ ak.command("update").option("--id <id>", "record id").option("-e, --env <env>", envDescription).option("-k, --key <key>", "api key").option("-u, --user-id <userId>", "user id").option("-n, --user-name <userName>", "user name").option("-m, --email <email>", "email").option("-p, --phone <phone>", "phone").option("--raw-key", "show the raw key in the output").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action((options) => {
2927
+ const parsed = updateOptionsSchema.parse(applyTableShortcut4(options));
2928
+ writeRecords(
2929
+ [createService().update(parsed)],
2930
+ parsed.format,
2931
+ parsed.rawKey ?? false,
2932
+ 1,
2933
+ dependencies
2934
+ );
2935
+ });
2936
+ ak.command("delete").option("--id <id>", "record id").option("-e, --env <env>", envDescription).option("-k, --key <key>", "api key").option("--yes", "skip confirmation").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action(async (options) => {
2937
+ const parsed = deleteOptionsSchema.parse(applyTableShortcut4(options));
2938
+ if (!parsed.yes) {
2939
+ const confirmed = await dependencies.confirm("Delete the API key record?");
2940
+ if (!confirmed) {
2941
+ throw new AkServiceError("ABORTED", "Delete operation cancelled.", 2);
2942
+ }
2943
+ }
2944
+ const service = createService();
2945
+ const record = service.get(parsed);
2946
+ const deleted = service.delete(parsed);
2947
+ if (!deleted) {
2948
+ throw new AkServiceError("STORAGE_ERROR", "Failed to delete the API key record.", 6);
2949
+ }
2950
+ writeRecords([record], parsed.format, false, 1, dependencies);
2951
+ });
2952
+ config.command("list").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action((options) => {
2953
+ const parsed = configFormatSchema.parse(applyTableShortcut4(options));
2954
+ const entries = listConfigEntries({
2955
+ configDir: dependencies.paths.config,
2956
+ dataDir: dependencies.paths.data,
2957
+ env: dependencies.env
2958
+ });
2959
+ writeConfigEntries(entries, parsed.format, dependencies);
2960
+ });
2961
+ config.command("get").argument("<key>", "config key").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action((key, options) => {
2962
+ const parsed = configGetOptionsSchema.parse({
2963
+ ...applyTableShortcut4(options),
2964
+ key
2965
+ });
2966
+ assertSupportedConfigKey(parsed.key);
2967
+ const entry = getConfigEntry(parsed.key, {
2968
+ configDir: dependencies.paths.config,
2969
+ dataDir: dependencies.paths.data,
2970
+ env: dependencies.env
2971
+ });
2972
+ writeConfigEntries([entry], parsed.format, dependencies);
2973
+ });
2974
+ config.command("set").argument("<key>", "config key").argument("<value>", "config value").option("--format <format>", "output format", "json").option("-t, --table", "render as a table").action((key, value, options) => {
2975
+ const parsed = configSetOptionsSchema.parse({
2976
+ ...applyTableShortcut4(options),
2977
+ key,
2978
+ value
2979
+ });
2980
+ assertSupportedConfigKey(parsed.key);
2981
+ setConfigValue(parsed.key, parsed.value, {
2982
+ configDir: dependencies.paths.config
2983
+ });
2984
+ const entry = getConfigEntry(parsed.key, {
2985
+ configDir: dependencies.paths.config,
2986
+ dataDir: dependencies.paths.data,
2987
+ env: dependencies.env
2988
+ });
2989
+ writeConfigEntries([entry], parsed.format, dependencies);
2990
+ });
2991
+ return {
2992
+ run: async (args) => {
2993
+ try {
2994
+ await program.parseAsync(["node", "stephen", ...args], {
2995
+ from: "node"
2996
+ });
2997
+ return 0;
2998
+ } catch (error) {
2999
+ if (error instanceof Error && "code" in error && error.code === "commander.helpDisplayed") {
3000
+ return 0;
3001
+ }
3002
+ if (isCliError(error)) {
3003
+ dependencies.stderr(
3004
+ `${renderAkErrorAsJson(error.code, error.message, "details" in error ? error.details : void 0)}
3005
+ `
3006
+ );
3007
+ return error.exitCode;
3008
+ }
3009
+ const videoExitCode = handleVideoCommandError(error, {
3010
+ stderr: dependencies.stderr
3011
+ });
3012
+ if (videoExitCode !== void 0) {
3013
+ return videoExitCode;
3014
+ }
3015
+ const kr36ExitCode = handleKr36CommandError(error, {
3016
+ stderr: dependencies.stderr
3017
+ });
3018
+ if (kr36ExitCode !== void 0) {
3019
+ return kr36ExitCode;
3020
+ }
3021
+ const toutiaoExitCode = handleToutiaoCommandError(error, {
3022
+ stderr: dependencies.stderr
3023
+ });
3024
+ if (toutiaoExitCode !== void 0) {
3025
+ return toutiaoExitCode;
3026
+ }
3027
+ const hackerNewsExitCode = handleHackerNewsCommandError(error, {
3028
+ stderr: dependencies.stderr
3029
+ });
3030
+ if (hackerNewsExitCode !== void 0) {
3031
+ return hackerNewsExitCode;
3032
+ }
3033
+ if (error instanceof ZodError) {
3034
+ dependencies.stderr(
3035
+ `${renderAkErrorAsJson("INVALID_ARGUMENT", error.issues[0].message)}
3036
+ `
3037
+ );
3038
+ return 2;
3039
+ }
3040
+ if (error instanceof Error && "exitCode" in error && typeof error.exitCode === "number") {
3041
+ return error.exitCode;
3042
+ }
3043
+ const message = error instanceof Error ? error.message : "Unexpected error.";
3044
+ dependencies.stderr(`${renderAkErrorAsJson("UNEXPECTED_ERROR", message)}
3045
+ `);
3046
+ return 1;
3047
+ }
3048
+ }
3049
+ };
3050
+ }
3051
+ function isCliError(error) {
3052
+ return error instanceof Error && "code" in error && typeof error.code === "string" && "exitCode" in error && typeof error.exitCode === "number";
3053
+ }
3054
+ function parseLimit2(value) {
3055
+ const parsed = Number.parseInt(value, 10);
3056
+ if (Number.isNaN(parsed)) {
3057
+ throw new Error(`Invalid limit: '${value}' is not a number`);
3058
+ }
3059
+ return parsed;
3060
+ }
3061
+ function applyTableShortcut4(options) {
3062
+ if (options.table) {
3063
+ return {
3064
+ ...options,
3065
+ format: "table"
3066
+ };
3067
+ }
3068
+ if (options.format) {
3069
+ return {
3070
+ ...options,
3071
+ format: options.format
3072
+ };
3073
+ }
3074
+ throw new Error("Output format is required for this command.");
3075
+ }
3076
+ function writeRecords(records, format, _rawKey, limit, dependencies) {
3077
+ if (format === "table") {
3078
+ dependencies.stdout(`${renderAkRecordsAsTable(records)}
3079
+ `);
3080
+ return;
3081
+ }
3082
+ dependencies.stdout(`${renderAkRecordsAsJson(records, limit)}
3083
+ `);
3084
+ }
3085
+ function writeConfigEntries(entries, format, dependencies) {
3086
+ if (format === "table") {
3087
+ dependencies.stdout(`${renderConfigEntriesAsTable(entries)}
3088
+ `);
3089
+ return;
3090
+ }
3091
+ dependencies.stdout(`${renderConfigEntriesAsJson(entries)}
3092
+ `);
3093
+ }
3094
+ function renderConfigEntriesAsJson(entries) {
3095
+ return JSON.stringify(
3096
+ {
3097
+ ok: true,
3098
+ data: entries,
3099
+ meta: {
3100
+ count: entries.length
3101
+ }
3102
+ },
3103
+ null,
3104
+ 2
3105
+ );
3106
+ }
3107
+ function renderConfigEntriesAsTable(entries) {
3108
+ return table7([
3109
+ ["key", "value", "source", "fileValue", "envValue", "defaultValue"],
3110
+ ...entries.map((entry) => [
3111
+ entry.key,
3112
+ entry.value,
3113
+ entry.source,
3114
+ entry.fileValue ?? "",
3115
+ entry.envValue ?? "",
3116
+ entry.defaultValue
3117
+ ])
3118
+ ]);
3119
+ }
3120
+
3121
+ // src/ak/database.ts
3122
+ import Database from "better-sqlite3";
3123
+ function createAkDatabase(filename = ":memory:") {
3124
+ const database = new Database(filename);
3125
+ database.exec(`
3126
+ CREATE TABLE IF NOT EXISTS ak_records (
3127
+ id TEXT PRIMARY KEY,
3128
+ env TEXT NOT NULL,
3129
+ user_id TEXT,
3130
+ user_name TEXT,
3131
+ email TEXT,
3132
+ phone TEXT,
3133
+ key_ciphertext TEXT NOT NULL,
3134
+ key_search_prefix TEXT NOT NULL,
3135
+ created_at TEXT NOT NULL,
3136
+ updated_at TEXT NOT NULL
3137
+ );
3138
+
3139
+ CREATE INDEX IF NOT EXISTS idx_ak_env ON ak_records(env);
3140
+ CREATE INDEX IF NOT EXISTS idx_ak_user_id ON ak_records(user_id);
3141
+ CREATE INDEX IF NOT EXISTS idx_ak_user_name ON ak_records(user_name);
3142
+ CREATE INDEX IF NOT EXISTS idx_ak_email ON ak_records(email);
3143
+ CREATE INDEX IF NOT EXISTS idx_ak_phone ON ak_records(phone);
3144
+ CREATE INDEX IF NOT EXISTS idx_ak_key_search_prefix ON ak_records(key_search_prefix);
3145
+ CREATE INDEX IF NOT EXISTS idx_ak_updated_at ON ak_records(updated_at);
3146
+ `);
3147
+ return database;
3148
+ }
3149
+
3150
+ // src/ak/repository.ts
3151
+ import Database2 from "better-sqlite3";
3152
+ var AkDuplicateRecordError = class extends Error {
3153
+ constructor(id) {
3154
+ super(`API key record already exists: ${id}.`);
3155
+ this.name = "AkDuplicateRecordError";
3156
+ }
3157
+ };
3158
+ var AkRepository = class {
3159
+ #database;
3160
+ constructor(database) {
3161
+ this.#database = database;
3162
+ }
3163
+ insert(record) {
3164
+ try {
3165
+ this.#database.prepare(
3166
+ `INSERT INTO ak_records (
3167
+ id,
3168
+ env,
3169
+ user_id,
3170
+ user_name,
3171
+ email,
3172
+ phone,
3173
+ key_ciphertext,
3174
+ key_search_prefix,
3175
+ created_at,
3176
+ updated_at
3177
+ ) VALUES (
3178
+ @id,
3179
+ @env,
3180
+ @userId,
3181
+ @userName,
3182
+ @email,
3183
+ @phone,
3184
+ @keyCiphertext,
3185
+ @keySearchPrefix,
3186
+ @createdAt,
3187
+ @updatedAt
3188
+ )`
3189
+ ).run(record);
3190
+ } catch (error) {
3191
+ if (error instanceof Database2.SqliteError && error.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
3192
+ throw new AkDuplicateRecordError(record.id);
3193
+ }
3194
+ throw error;
3195
+ }
3196
+ }
3197
+ getById(id) {
3198
+ const row = this.#database.prepare("SELECT * FROM ak_records WHERE id = ?").get(id);
3199
+ return row ? mapRowToRecord(row) : null;
3200
+ }
3201
+ getByEnvAndId(env, id) {
3202
+ const row = this.#database.prepare("SELECT * FROM ak_records WHERE env = ? AND id = ?").get(env, id);
3203
+ return row ? mapRowToRecord(row) : null;
3204
+ }
3205
+ list(filters) {
3206
+ const conditions = [];
3207
+ const params = [];
3208
+ if (filters.env) {
3209
+ conditions.push("env = ?");
3210
+ params.push(filters.env);
3211
+ }
3212
+ if (filters.query && filters.fields && filters.fields.length > 0) {
3213
+ const normalizedQuery = filters.query.toLowerCase();
3214
+ const fieldConditions = filters.fields.map((field) => {
3215
+ if (field === "key") {
3216
+ params.push(`${filters.query}%`);
3217
+ return "key_search_prefix LIKE ?";
3218
+ }
3219
+ const column = fieldToColumn(field);
3220
+ params.push(`%${normalizedQuery}%`);
3221
+ return `LOWER(COALESCE(${column}, '')) LIKE ?`;
3222
+ });
3223
+ conditions.push(`(${fieldConditions.join(" OR ")})`);
3224
+ }
3225
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3226
+ params.push(filters.limit);
3227
+ const rows = this.#database.prepare(
3228
+ `SELECT * FROM ak_records
3229
+ ${whereClause}
3230
+ ORDER BY updated_at DESC
3231
+ LIMIT ?`
3232
+ ).all(...params);
3233
+ return rows.map(mapRowToRecord);
3234
+ }
3235
+ updateMetadata(input2) {
3236
+ const current = this.getById(input2.id);
3237
+ if (!current) {
3238
+ return null;
3239
+ }
3240
+ const row = this.#database.prepare(
3241
+ `UPDATE ak_records
3242
+ SET user_id = @userId,
3243
+ user_name = @userName,
3244
+ email = @email,
3245
+ phone = @phone,
3246
+ updated_at = @updatedAt
3247
+ WHERE id = @id
3248
+ RETURNING *`
3249
+ ).get({
3250
+ email: input2.email === void 0 ? current.email : input2.email,
3251
+ id: input2.id,
3252
+ phone: input2.phone === void 0 ? current.phone : input2.phone,
3253
+ updatedAt: input2.updatedAt,
3254
+ userId: input2.userId === void 0 ? current.userId : input2.userId,
3255
+ userName: input2.userName === void 0 ? current.userName : input2.userName
3256
+ });
3257
+ if (!row) {
3258
+ return null;
3259
+ }
3260
+ return mapRowToRecord(row);
3261
+ }
3262
+ deleteById(id) {
3263
+ const result = this.#database.prepare("DELETE FROM ak_records WHERE id = ?").run(id);
3264
+ return result.changes > 0;
3265
+ }
3266
+ };
3267
+ function fieldToColumn(field) {
3268
+ switch (field) {
3269
+ case "userId":
3270
+ return "user_id";
3271
+ case "userName":
3272
+ return "user_name";
3273
+ case "email":
3274
+ return "email";
3275
+ case "phone":
3276
+ return "phone";
3277
+ }
3278
+ }
3279
+ function mapRowToRecord(row) {
3280
+ return {
3281
+ createdAt: row.created_at,
3282
+ email: row.email,
3283
+ env: row.env,
3284
+ id: row.id,
3285
+ keyCiphertext: row.key_ciphertext,
3286
+ keySearchPrefix: row.key_search_prefix,
3287
+ phone: row.phone,
3288
+ updatedAt: row.updated_at,
3289
+ userId: row.user_id,
3290
+ userName: row.user_name
3291
+ };
3292
+ }
3293
+
3294
+ // src/disk/runtime.ts
3295
+ import { readdir, rm, stat } from "fs/promises";
3296
+ import { execFile } from "child_process";
3297
+ import { promisify } from "util";
3298
+ import { join as join3, win32 as win325 } from "path";
3299
+ var execFileAsync = promisify(execFile);
3300
+ function resolveDiskCleanupRoots(env) {
3301
+ const userProfileRoot = env.USERPROFILE ?? resolveHomeDrivePath(env);
3302
+ const systemRoot = env.SystemRoot ?? "C:\\Windows";
3303
+ if (!userProfileRoot) {
3304
+ throw new Error(
3305
+ "Windows user profile could not be resolved. Set USERPROFILE or HOMEDRIVE and HOMEPATH."
3306
+ );
3307
+ }
3308
+ return {
3309
+ systemRoot,
3310
+ userProfileRoot
3311
+ };
3312
+ }
3313
+ function createDiskCleanupRuntime(dependencies = {}) {
3314
+ const executeCommand = dependencies.executeCommand ?? /* v8 ignore next 3 */
3315
+ (async (file, args) => {
3316
+ await execFileAsync(file, args);
3317
+ });
3318
+ return {
3319
+ clearDirectoryContents: async (path) => {
3320
+ const entries = await readdir(path, { withFileTypes: true });
3321
+ await Promise.all(
3322
+ entries.map(
3323
+ (entry) => rm(join3(path, entry.name), {
3324
+ force: true,
3325
+ recursive: true
3326
+ })
3327
+ )
3328
+ );
3329
+ },
3330
+ disableHibernation: async () => {
3331
+ await executeCommand("powercfg", ["-h", "off"]);
3332
+ },
3333
+ inspectPath: async (path) => inspectPath(path),
3334
+ listTopEntriesBySize: async (path, limit) => listTopEntriesBySize(path, limit),
3335
+ runCommand: async (file, args) => {
3336
+ await executeCommand(file, args);
3337
+ }
3338
+ };
3339
+ }
3340
+ async function inspectPath(path) {
3341
+ try {
3342
+ const stats = await stat(path);
3343
+ if (!stats.isDirectory()) {
3344
+ return {
3345
+ exists: true,
3346
+ isDirectory: false,
3347
+ sizeBytes: stats.size
3348
+ };
3349
+ }
3350
+ const sizeBytes = await getDirectorySize(path);
3351
+ return {
3352
+ exists: true,
3353
+ isDirectory: true,
3354
+ sizeBytes
3355
+ };
3356
+ } catch (error) {
3357
+ if (isNotFoundError(error)) {
3358
+ return {
3359
+ exists: false,
3360
+ isDirectory: false,
3361
+ sizeBytes: 0
3362
+ };
3363
+ }
3364
+ throw error;
3365
+ }
3366
+ }
3367
+ async function getDirectorySize(path) {
3368
+ const entries = await readdir(path, { withFileTypes: true });
3369
+ let total = 0;
3370
+ for (const entry of entries) {
3371
+ const entryPath = join3(path, entry.name);
3372
+ if (entry.isDirectory()) {
3373
+ total += await getDirectorySize(entryPath);
3374
+ continue;
3375
+ }
3376
+ if (entry.isFile()) {
3377
+ total += (await stat(entryPath)).size;
3378
+ }
3379
+ }
3380
+ return total;
3381
+ }
3382
+ async function listTopEntriesBySize(path, limit) {
3383
+ try {
3384
+ const entries = await readdir(path, { withFileTypes: true });
3385
+ const measured = await Promise.all(
3386
+ entries.map(async (entry) => {
3387
+ const entryPath = join3(path, entry.name);
3388
+ if (entry.isDirectory()) {
3389
+ return {
3390
+ kind: "directory",
3391
+ name: entry.name,
3392
+ path: entryPath,
3393
+ sizeBytes: await getDirectorySize(entryPath),
3394
+ sizeGB: 0
3395
+ };
3396
+ }
3397
+ if (entry.isFile()) {
3398
+ const stats = await stat(entryPath);
3399
+ return {
3400
+ kind: "file",
3401
+ name: entry.name,
3402
+ path: entryPath,
3403
+ sizeBytes: stats.size,
3404
+ sizeGB: 0
3405
+ };
3406
+ }
3407
+ return null;
3408
+ })
3409
+ );
3410
+ return measured.filter((entry) => entry !== null).map((entry) => ({
3411
+ ...entry,
3412
+ sizeGB: bytesToGb2(entry.sizeBytes)
3413
+ })).sort((left, right) => right.sizeBytes - left.sizeBytes).slice(0, limit);
3414
+ } catch (error) {
3415
+ if (isNotFoundError(error)) {
3416
+ return [];
3417
+ }
3418
+ throw error;
3419
+ }
3420
+ }
3421
+ function bytesToGb2(value) {
3422
+ return Number((value / 1024 ** 3).toFixed(2));
3423
+ }
3424
+ function resolveHomeDrivePath(env) {
3425
+ if (!env.HOMEDRIVE || !env.HOMEPATH) {
3426
+ return null;
3427
+ }
3428
+ return win325.join(env.HOMEDRIVE, env.HOMEPATH);
3429
+ }
3430
+ function isNotFoundError(error) {
3431
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
3432
+ }
3433
+
3434
+ // src/video/runtime.ts
3435
+ import { execFile as nodeExecFile } from "child_process";
3436
+ import { writeFile as writeFileOnDisk } from "fs/promises";
3437
+ import { promisify as promisify2 } from "util";
3438
+ var execFileAsync2 = promisify2(nodeExecFile);
3439
+ function createDefaultVideoRuntime() {
3440
+ return {
3441
+ execFile: async (file, args) => {
3442
+ try {
3443
+ const result = await execFileAsync2(file, args, { encoding: "utf8" });
3444
+ return {
3445
+ code: 0,
3446
+ stderr: result.stderr,
3447
+ stdout: result.stdout
3448
+ };
3449
+ } catch (error) {
3450
+ const execError = error;
3451
+ return {
3452
+ code: typeof execError.code === "number" ? execError.code : 1,
3453
+ stderr: execError.stderr ?? execError.message,
3454
+ stdout: execError.stdout ?? ""
3455
+ };
3456
+ }
3457
+ },
3458
+ fetch: async (input2, init) => {
3459
+ const response = await fetch(input2, init);
3460
+ return response;
3461
+ },
3462
+ launchBrowserSniffer: async (url) => sniffWithBrowserRuntime(url),
3463
+ writeFile: async (path, data) => {
3464
+ await writeFileOnDisk(path, data);
3465
+ }
3466
+ };
3467
+ }
3468
+ async function sniffWithBrowserRuntime(url, loadModule = loadOptionalModule2) {
3469
+ let playwright;
3470
+ try {
3471
+ playwright = await loadModule("playwright");
3472
+ } catch {
3473
+ throw new VideoCommandError(
3474
+ "VIDEO_BROWSER_UNAVAILABLE",
3475
+ "Browser sniff mode requires Playwright to be installed.",
3476
+ 2,
3477
+ void 0,
3478
+ true
3479
+ );
3480
+ }
3481
+ const chromium = playwright.chromium;
3482
+ if (!chromium) {
3483
+ throw new VideoCommandError(
3484
+ "VIDEO_BROWSER_UNAVAILABLE",
3485
+ "Browser sniff mode requires Playwright Chromium support.",
3486
+ 2,
3487
+ void 0,
3488
+ true
3489
+ );
3490
+ }
3491
+ const browser = await chromium.launch({ headless: true });
3492
+ const candidates = [];
3493
+ try {
3494
+ const context = await browser.newContext();
3495
+ const page = await context.newPage();
3496
+ page.on("response", (response) => {
3497
+ const candidate = createCandidateFromBrowserResponse(response.url(), response.headers()["content-type"]);
3498
+ if (candidate) {
3499
+ candidates.push(candidate);
3500
+ }
3501
+ });
3502
+ await page.goto(url, { waitUntil: "load" });
3503
+ await page.waitForTimeout(6e4);
3504
+ await context.close();
3505
+ } finally {
3506
+ await browser.close();
3507
+ }
3508
+ return rankVideoCandidates(candidates);
3509
+ }
3510
+ var loadOptionalModule2 = new Function(
3511
+ "specifier",
3512
+ "return import(specifier);"
3513
+ );
3514
+ var VIDEO_RES_RE = /(?:^|\/)(?:avc1\/|vid\/)(?:\d+\/){0,2}(\d+)x(\d+)/i;
3515
+ var AUDIO_BITRATE_RE = /\/mp4a\/(\d+)/i;
3516
+ var AUDIO_RE = /\/mp4a\//i;
3517
+ function createCandidateFromBrowserResponse(url, contentType) {
3518
+ const isAudio = AUDIO_RE.test(url);
3519
+ let qualityBonus = 0;
3520
+ if (isAudio) {
3521
+ const bitrateMatch = AUDIO_BITRATE_RE.exec(url);
3522
+ if (bitrateMatch && bitrateMatch[1]) {
3523
+ const bitrate = parseInt(bitrateMatch[1], 10);
3524
+ qualityBonus = Math.min(bitrate / 192e3, 1) * 0.05;
3525
+ }
3526
+ } else {
3527
+ const resMatch = VIDEO_RES_RE.exec(url);
3528
+ if (resMatch && resMatch[1] && resMatch[2]) {
3529
+ const width = parseInt(resMatch[1], 10);
3530
+ const height = parseInt(resMatch[2], 10);
3531
+ const pixels = width * height;
3532
+ qualityBonus = Math.min(pixels / (1920 * 1080), 1) * 0.05;
3533
+ }
3534
+ }
3535
+ if (/\.m4s(?:\?|$)/i.test(url)) {
3536
+ return null;
3537
+ }
3538
+ if (/\.m3u8(?:\?|$)/i.test(url)) {
3539
+ if (isAudio) {
3540
+ return null;
3541
+ }
3542
+ return createVideoCandidate("m3u8", url, "network", qualityBonus);
3543
+ }
3544
+ if (/\.mp4(?:\?|$)/i.test(url) || contentType?.includes("video/mp4")) {
3545
+ if (isAudio) {
3546
+ return null;
3547
+ }
3548
+ return createVideoCandidate("mp4", url, "network", qualityBonus);
3549
+ }
3550
+ return null;
3551
+ }
3552
+
3553
+ // src/36kr/runtime.ts
3554
+ import { execFile as nodeExecFile2 } from "child_process";
3555
+ import { promisify as promisify3 } from "util";
3556
+ var execFileAsync3 = promisify3(nodeExecFile2);
3557
+ function createDefaultKr36Runtime() {
3558
+ return {
3559
+ fetchArticleHtml: async (request) => fetchWithCurl(request, "KR36_REQUEST_FAILED"),
3560
+ fetchJson: async (request) => fetchWithCurl(request, "KR36_REQUEST_FAILED")
3561
+ };
3562
+ }
3563
+ async function fetchWithCurl(request, errorCode) {
3564
+ const args = [
3565
+ "--silent",
3566
+ "--show-error",
3567
+ "--location",
3568
+ "--compressed",
3569
+ ...Object.entries(request.headers).flatMap(([name, value]) => ["--header", `${name}: ${value}`]),
3570
+ ..."body" in request ? ["--data-raw", JSON.stringify(request.body)] : [],
3571
+ request.url
3572
+ ];
3573
+ try {
3574
+ const result = await execFileAsync3("curl", args, {
3575
+ encoding: "utf8",
3576
+ maxBuffer: 20 * 1024 * 1024
3577
+ });
3578
+ return result.stdout;
3579
+ } catch (error) {
3580
+ const execError = error;
3581
+ throw new Kr36CommandError(
3582
+ errorCode,
3583
+ `Failed to fetch 36kr page: ${execError.stderr || execError.message}`,
3584
+ 1,
3585
+ {
3586
+ curlExitCode: execError.code,
3587
+ url: request.url
3588
+ }
3589
+ );
3590
+ }
3591
+ }
3592
+
3593
+ // src/toutiao/runtime.ts
3594
+ var TOUTIAO_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
3595
+ function createDefaultToutiaoRuntime() {
3596
+ return {
3597
+ fetchArticle: async (request) => fetchArticleWithBrowser(request),
3598
+ fetchAuthorArticles: async (options) => fetchAuthorArticlesWithBrowser(options),
3599
+ fetchKeywordInformation: async (options) => fetchKeywordInformationWithBrowser(options),
3600
+ fetchTechnologyChannel: async (options) => fetchTechnologyChannelWithBrowser(options)
3601
+ };
3602
+ }
3603
+ async function fetchArticleWithBrowser(request) {
3604
+ return withToutiaoPage(async (page) => {
3605
+ await page.goto(request.url, {
3606
+ timeout: 45e3,
3607
+ waitUntil: "domcontentloaded"
3608
+ });
3609
+ await page.waitForTimeout(5e3);
3610
+ const title = await page.locator("h1").first().innerText({ timeout: 5e3 }).catch(() => "");
3611
+ const paragraphs = await page.locator("article p").evaluateAll(
3612
+ (nodes) => nodes.map((node) => (node.textContent ?? "").trim()).filter(Boolean)
3613
+ );
3614
+ const bodyText = await page.locator("body").innerText({ timeout: 5e3 });
3615
+ const metadata = extractArticleMetadata(bodyText, title);
3616
+ const id = extractArticleId(request.url) ?? extractArticleId(request.input) ?? "";
3617
+ if (!id || !title || paragraphs.length === 0) {
3618
+ throw new ToutiaoCommandError(
3619
+ "TOUTIAO_PARSE_ERROR",
3620
+ "Toutiao article page did not expose title and content.",
3621
+ 2,
3622
+ {
3623
+ input: request.input,
3624
+ url: request.url
3625
+ }
3626
+ );
3627
+ }
3628
+ const article = {
3629
+ content: {
3630
+ paragraphs,
3631
+ text: paragraphs.join("\n")
3632
+ },
3633
+ id,
3634
+ request,
3635
+ title,
3636
+ url: page.url()
3637
+ };
3638
+ if (metadata.authorName !== void 0) {
3639
+ article.authorName = metadata.authorName;
3640
+ }
3641
+ if (metadata.publishTimeText !== void 0) {
3642
+ article.publishTimeText = metadata.publishTimeText;
3643
+ }
3644
+ return article;
3645
+ });
3646
+ }
3647
+ async function fetchTechnologyChannelWithBrowser(options) {
3648
+ return withToutiaoPage(async (page) => {
3649
+ const feedResponses = [];
3650
+ page.on("response", (response) => {
3651
+ if (!response.url().includes("/api/pc/list/feed")) {
3652
+ return;
3653
+ }
3654
+ void response.json().then((json) => {
3655
+ feedResponses.push(json);
3656
+ }).catch(() => void 0);
3657
+ });
3658
+ await page.goto("https://www.toutiao.com/?channel=tech&source=ch", {
3659
+ timeout: 45e3,
3660
+ waitUntil: "domcontentloaded"
3661
+ });
3662
+ await waitForFeedResponses(page, feedResponses, options.pages);
3663
+ const items = feedResponses.flatMap((response) => mapFeedItems(response.data ?? []));
3664
+ const lastResponse = feedResponses.at(-1);
3665
+ if (items.length === 0) {
3666
+ throw new ToutiaoCommandError(
3667
+ "TOUTIAO_PARSE_ERROR",
3668
+ "Toutiao technology channel did not return feed items.",
3669
+ 2
3670
+ );
3671
+ }
3672
+ const result = {
3673
+ hasMore: lastResponse?.has_more ?? false,
3674
+ items,
3675
+ source: "tech"
3676
+ };
3677
+ const next = {};
3678
+ if (lastResponse?.next?.max_behot_time !== void 0) {
3679
+ next.maxBehotTime = lastResponse.next.max_behot_time;
3680
+ }
3681
+ if (lastResponse?.offset !== void 0) {
3682
+ next.offset = lastResponse.offset;
3683
+ }
3684
+ if (Object.keys(next).length > 0) {
3685
+ result.next = next;
3686
+ }
3687
+ return result;
3688
+ });
3689
+ }
3690
+ async function fetchAuthorArticlesWithBrowser(options) {
3691
+ return withToutiaoPage(async (page) => {
3692
+ const feedResponses = [];
3693
+ page.on("response", (response) => {
3694
+ if (!isAuthorFeedResponseUrl(response.url(), options.authorToken)) {
3695
+ return;
3696
+ }
3697
+ void response.json().then((json) => {
3698
+ feedResponses.push(json);
3699
+ }).catch(() => void 0);
3700
+ });
3701
+ await page.goto(options.url, {
3702
+ timeout: 45e3,
3703
+ waitUntil: "domcontentloaded"
3704
+ });
3705
+ await waitForFeedResponses(page, feedResponses, options.pages);
3706
+ const seen = /* @__PURE__ */ new Set();
3707
+ const selectedResponses = feedResponses.slice(0, options.pages);
3708
+ const items = selectedResponses.flatMap((response) => mapFeedItems(response.data ?? [])).filter((item) => {
3709
+ if (seen.has(item.id)) {
3710
+ return false;
3711
+ }
3712
+ seen.add(item.id);
3713
+ return true;
3714
+ });
3715
+ const lastResponse = selectedResponses.at(-1);
3716
+ if (items.length === 0) {
3717
+ throw new ToutiaoCommandError(
3718
+ "TOUTIAO_PARSE_ERROR",
3719
+ "Toutiao author homepage did not return article feed items.",
3720
+ 2,
3721
+ {
3722
+ authorToken: options.authorToken,
3723
+ url: options.url
3724
+ }
3725
+ );
3726
+ }
3727
+ const result = {
3728
+ authorToken: options.authorToken,
3729
+ hasMore: lastResponse?.has_more ?? false,
3730
+ items
3731
+ };
3732
+ const next = {};
3733
+ if (lastResponse?.next?.max_behot_time !== void 0) {
3734
+ next.maxBehotTime = lastResponse.next.max_behot_time;
3735
+ }
3736
+ if (lastResponse?.offset !== void 0) {
3737
+ next.offset = lastResponse.offset;
3738
+ }
3739
+ if (Object.keys(next).length > 0) {
3740
+ result.next = next;
3741
+ }
3742
+ return result;
3743
+ });
3744
+ }
3745
+ async function fetchKeywordInformationWithBrowser(options) {
3746
+ return withToutiaoPage(async (page) => {
3747
+ const items = [];
3748
+ const seen = /* @__PURE__ */ new Set();
3749
+ let blocked = false;
3750
+ for (let pageIndex = 0; pageIndex < options.pages; pageIndex += 1) {
3751
+ const url = buildSearchUrl(options.keyword, pageIndex);
3752
+ await page.goto(url, {
3753
+ timeout: 45e3,
3754
+ waitUntil: "domcontentloaded"
3755
+ });
3756
+ await page.waitForTimeout(5e3);
3757
+ blocked = blocked || await page.locator("#pc_captcha").count() > 0;
3758
+ for (const item of await extractSearchItemsFromPage(page, options.keyword)) {
3759
+ if (seen.has(item.id)) {
3760
+ continue;
3761
+ }
3762
+ seen.add(item.id);
3763
+ items.push(item);
3764
+ }
3765
+ }
3766
+ if (blocked && items.length === 0) {
3767
+ throw new ToutiaoCommandError(
3768
+ "TOUTIAO_VERIFICATION_REQUIRED",
3769
+ "Toutiao search requires browser verification before returning usable results.",
3770
+ 1,
3771
+ {
3772
+ keyword: options.keyword
3773
+ }
3774
+ );
3775
+ }
3776
+ if (items.length === 0) {
3777
+ throw new ToutiaoCommandError(
3778
+ "TOUTIAO_PARSE_ERROR",
3779
+ "Toutiao search page did not expose result items.",
3780
+ 2,
3781
+ {
3782
+ keyword: options.keyword
3783
+ }
3784
+ );
3785
+ }
3786
+ return {
3787
+ hasMore: true,
3788
+ items,
3789
+ keyword: options.keyword,
3790
+ source: options.source
3791
+ };
3792
+ });
3793
+ }
3794
+ function buildSearchUrl(keyword, pageIndex) {
3795
+ const params = new URLSearchParams({
3796
+ action_type: pageIndex === 0 ? "search" : "pagination",
3797
+ cur_tab_title: "news",
3798
+ dvpf: "pc",
3799
+ from: "news",
3800
+ keyword,
3801
+ page_num: String(pageIndex),
3802
+ pd: "information",
3803
+ source: pageIndex === 0 ? "input" : "pagination"
3804
+ });
3805
+ return `https://so.toutiao.com/search?${params.toString()}`;
3806
+ }
3807
+ async function withToutiaoPage(callback) {
3808
+ let browser;
3809
+ let context;
3810
+ try {
3811
+ const { chromium } = await loadPlaywright();
3812
+ browser = await chromium.launch({ headless: true });
3813
+ context = await browser.newContext({
3814
+ locale: "zh-CN",
3815
+ userAgent: TOUTIAO_USER_AGENT
3816
+ });
3817
+ const page = await context.newPage();
3818
+ return await callback(page);
3819
+ } finally {
3820
+ await context?.close().catch(() => void 0);
3821
+ await browser?.close().catch(() => void 0);
3822
+ }
3823
+ }
3824
+ async function waitForFeedResponses(page, responses, pages) {
3825
+ const started = Date.now();
3826
+ while (responses.length < pages && Date.now() - started < 45e3) {
3827
+ await page.waitForTimeout(1e3);
3828
+ if (responses.length < pages) {
3829
+ await page.mouse.wheel(0, 4e3);
3830
+ }
3831
+ }
3832
+ }
3833
+ async function loadPlaywright() {
3834
+ try {
3835
+ return await loadOptionalModule3("playwright");
3836
+ } catch {
3837
+ throw new ToutiaoCommandError(
3838
+ "TOUTIAO_BROWSER_UNAVAILABLE",
3839
+ "Toutiao collection requires Playwright to be installed.",
3840
+ 2
3841
+ );
3842
+ }
3843
+ }
3844
+ function mapFeedItems(items) {
3845
+ return items.flatMap((item) => {
3846
+ const id = String(item.group_id ?? item.item_id ?? item.id ?? "");
3847
+ const title = item.title ?? item.Title ?? "";
3848
+ if (!id || !title) {
3849
+ return [];
3850
+ }
3851
+ const mapped = {
3852
+ id,
3853
+ title,
3854
+ url: normalizeArticleUrl(item.article_url ?? item.display_url ?? item.source_url, id)
3855
+ };
3856
+ const abstract = item.Abstract ?? item.abstract;
3857
+ if (abstract !== void 0) {
3858
+ mapped.abstract = abstract;
3859
+ }
3860
+ if (item.comment_count !== void 0) {
3861
+ mapped.commentCount = item.comment_count;
3862
+ }
3863
+ if (item.image_url !== void 0) {
3864
+ mapped.image = item.image_url;
3865
+ }
3866
+ const authorName = item.media_name ?? item.source;
3867
+ if (authorName !== void 0) {
3868
+ mapped.authorName = authorName;
3869
+ }
3870
+ if (item.source_url !== void 0) {
3871
+ mapped.sourceUrl = item.source_url;
3872
+ }
3873
+ if (item.behot_time !== void 0) {
3874
+ mapped.publishTime = {
3875
+ iso: new Date(item.behot_time * 1e3).toISOString(),
3876
+ local: formatChinaTime2(item.behot_time),
3877
+ seconds: item.behot_time
3878
+ };
3879
+ }
3880
+ return [mapped];
3881
+ });
3882
+ }
3883
+ function normalizeArticleUrl(value, id) {
3884
+ if (!value) {
3885
+ return `https://www.toutiao.com/article/${id}/`;
3886
+ }
3887
+ try {
3888
+ const url = new URL(value, "https://www.toutiao.com");
3889
+ return parseToutiaoArticleLink(url.toString())?.url ?? `https://www.toutiao.com/article/${id}/`;
3890
+ } catch {
3891
+ return `https://www.toutiao.com/article/${id}/`;
3892
+ }
3893
+ }
3894
+ async function extractSearchItemsFromPage(page, keyword) {
3895
+ const anchors = await page.locator("a").evaluateAll(
3896
+ (nodes) => nodes.map((node) => ({
3897
+ href: node.href,
3898
+ text: (node.textContent ?? "").trim().replace(/\s+/g, " ")
3899
+ }))
3900
+ );
3901
+ const seen = /* @__PURE__ */ new Set();
3902
+ const items = [];
3903
+ for (const anchor of anchors) {
3904
+ const articleLink = parseToutiaoArticleLink(anchor.href);
3905
+ if (!articleLink || !anchor.text || anchor.text.length < 6 || seen.has(articleLink.id)) {
3906
+ continue;
3907
+ }
3908
+ seen.add(articleLink.id);
3909
+ items.push({
3910
+ id: articleLink.id,
3911
+ title: anchor.text.slice(0, 180),
3912
+ url: articleLink.url
3913
+ });
3914
+ if (items.length >= 30) {
3915
+ break;
3916
+ }
3917
+ }
3918
+ if (items.length > 0) {
3919
+ return items;
3920
+ }
3921
+ return extractSearchItemsFromText(await page.locator("body").innerText({ timeout: 5e3 }), keyword);
3922
+ }
3923
+ function extractSearchItemsFromText(text, keyword) {
3924
+ const blockedPrefixes = /* @__PURE__ */ new Set([
3925
+ "\u65E0\u969C\u788D \u4ECA\u65E5\u5934\u6761\u9996\u9875 \u767B\u5F55 \u7EFC\u5408 \u8D44\u8BAF \u89C6\u9891 \u56FE\u7247 \u7528\u6237 \u5C0F\u89C6\u9891 \u5FAE\u5934\u6761 \u97F3\u4E50 \u53BB\u6296\u97F3\u641C"
3926
+ ]);
3927
+ const lines = text.split(/\n+/).map((line) => line.trim()).filter((line) => line.length >= 12 && !blockedPrefixes.has(line));
3928
+ const seen = /* @__PURE__ */ new Set();
3929
+ const items = [];
3930
+ for (const line of lines) {
3931
+ const normalized = line.replace(/\s+/g, " ");
3932
+ if (seen.has(normalized)) {
3933
+ continue;
3934
+ }
3935
+ seen.add(normalized);
3936
+ items.push({
3937
+ id: createSearchItemId(keyword, normalized, items.length),
3938
+ title: normalized.slice(0, 180),
3939
+ url: `https://so.toutiao.com/search?keyword=${encodeURIComponent(keyword)}&pd=information&dvpf=pc&source=input`
3940
+ });
3941
+ if (items.length >= 30) {
3942
+ break;
3943
+ }
3944
+ }
3945
+ return items;
3946
+ }
3947
+ function parseToutiaoArticleLink(href, depth = 0) {
3948
+ if (depth > 3) {
3949
+ return void 0;
3950
+ }
3951
+ const directId = extractArticleId(href);
3952
+ if (directId) {
3953
+ return {
3954
+ id: directId,
3955
+ url: `https://www.toutiao.com/article/${directId}/`
3956
+ };
3957
+ }
3958
+ try {
3959
+ const url = new URL(href);
3960
+ const nextUrl = url.searchParams.get("url");
3961
+ if (nextUrl) {
3962
+ return parseToutiaoArticleLink(nextUrl, depth + 1);
3963
+ }
3964
+ } catch {
3965
+ return void 0;
3966
+ }
3967
+ return void 0;
3968
+ }
3969
+ function extractArticleMetadata(bodyText, title) {
3970
+ const lines = bodyText.split(/\n+/).map((line) => line.trim()).filter(Boolean);
3971
+ const titleIndex = lines.findIndex((line) => line === title);
3972
+ const metadataLine = titleIndex >= 0 ? lines[titleIndex + 1] : void 0;
3973
+ const match = metadataLine?.match(/^(.+?)·(.+)$/);
3974
+ if (!match?.[1] || !match[2]) {
3975
+ return {};
3976
+ }
3977
+ return {
3978
+ authorName: match[2],
3979
+ publishTimeText: match[1]
3980
+ };
3981
+ }
3982
+ function extractArticleId(value) {
3983
+ return /(?:article|group)\/(\d+)/.exec(value)?.[1] ?? /\/a(\d+)/.exec(value)?.[1] ?? (/^\d+$/.test(value) ? value : void 0);
3984
+ }
3985
+ function isAuthorFeedResponseUrl(value, authorToken) {
3986
+ try {
3987
+ const url = new URL(value);
3988
+ const isProfileArticleFeed = url.pathname.includes("/api/pc/list/feed") && (url.searchParams.get("category") === "pc_profile_article" || url.searchParams.get("author_token") === authorToken);
3989
+ const isProfileAllFeed = url.pathname.includes("/api/pc/list/user/feed") && url.searchParams.get("category") === "profile_all" && url.searchParams.get("token") === authorToken;
3990
+ return isProfileArticleFeed || isProfileAllFeed;
3991
+ } catch {
3992
+ return value.includes("/api/pc/list/feed") && (value.includes("pc_profile_article") || value.includes(authorToken)) || value.includes("/api/pc/list/user/feed") && value.includes("profile_all") && value.includes(authorToken);
3993
+ }
3994
+ }
3995
+ function createSearchItemId(keyword, value, index) {
3996
+ let hash = 2166136261;
3997
+ const input2 = `${keyword}:${index}:${value}`;
3998
+ for (const char of input2) {
3999
+ hash ^= char.charCodeAt(0);
4000
+ hash = Math.imul(hash, 16777619);
4001
+ }
4002
+ return `search-${(hash >>> 0).toString(16)}`;
4003
+ }
4004
+ function formatChinaTime2(timestampSeconds) {
4005
+ return new Intl.DateTimeFormat("sv-SE", {
4006
+ day: "2-digit",
4007
+ hour: "2-digit",
4008
+ hour12: false,
4009
+ minute: "2-digit",
4010
+ month: "2-digit",
4011
+ second: "2-digit",
4012
+ timeZone: "Asia/Shanghai",
4013
+ year: "numeric"
4014
+ }).format(new Date(timestampSeconds * 1e3));
4015
+ }
4016
+ var loadOptionalModule3 = new Function(
4017
+ "specifier",
4018
+ "return import(specifier);"
4019
+ );
4020
+
4021
+ // src/hn/runtime.ts
4022
+ var FIREBASE_BASE_URL = "https://hacker-news.firebaseio.com/v0";
4023
+ var ALGOLIA_BASE_URL = "https://hn.algolia.com/api/v1";
4024
+ var SOURCE_TO_ENDPOINT = {
4025
+ best: "beststories",
4026
+ new: "newstories",
4027
+ top: "topstories"
4028
+ };
4029
+ function createDefaultHackerNewsRuntime() {
4030
+ return {
4031
+ fetchSearch: async (options) => fetchSearch(options),
4032
+ fetchStories: async (options) => fetchStories(options)
4033
+ };
4034
+ }
4035
+ async function fetchStories(options) {
4036
+ const endpoint = SOURCE_TO_ENDPOINT[options.source];
4037
+ const ids = await fetchJson(`${FIREBASE_BASE_URL}/${endpoint}.json`);
4038
+ const selectedIds = ids.slice(0, Math.max(options.limit * 2, options.limit));
4039
+ const rawItems = await Promise.all(
4040
+ selectedIds.map((id) => fetchJson(`${FIREBASE_BASE_URL}/item/${id}.json`))
4041
+ );
4042
+ const items = rawItems.flatMap((item) => mapFirebaseItem(item)).slice(0, options.limit);
4043
+ return {
4044
+ items,
4045
+ source: options.source
4046
+ };
4047
+ }
4048
+ async function fetchSearch(options) {
4049
+ const params = new URLSearchParams({
4050
+ hitsPerPage: String(options.limit),
4051
+ query: options.query,
4052
+ tags: "story"
4053
+ });
4054
+ const endpoint = options.sort === "date" ? "search_by_date" : "search";
4055
+ const response = await fetchJson(`${ALGOLIA_BASE_URL}/${endpoint}?${params.toString()}`);
4056
+ return {
4057
+ items: response.hits.flatMap(mapAlgoliaHit).slice(0, options.limit),
4058
+ query: options.query
4059
+ };
4060
+ }
4061
+ async function fetchJson(url) {
4062
+ let response;
4063
+ try {
4064
+ response = await fetch(url, {
4065
+ headers: {
4066
+ accept: "application/json",
4067
+ "user-agent": "stephen-cli/0.1 HackerNews collector"
4068
+ }
4069
+ });
4070
+ } catch (error) {
4071
+ throw new HackerNewsCommandError(
4072
+ "HN_FETCH_FAILED",
4073
+ "Failed to fetch Hacker News data.",
4074
+ 2,
4075
+ {
4076
+ cause: error instanceof Error ? error.message : String(error),
4077
+ url
4078
+ }
4079
+ );
4080
+ }
4081
+ if (!response.ok) {
4082
+ throw new HackerNewsCommandError(
4083
+ "HN_FETCH_FAILED",
4084
+ `Failed to fetch Hacker News data. HTTP ${response.status}.`,
4085
+ 2,
4086
+ {
4087
+ url
4088
+ }
4089
+ );
4090
+ }
4091
+ return await response.json();
4092
+ }
4093
+ function mapFirebaseItem(item) {
4094
+ if (!item || item.deleted || item.dead || item.type !== "story" || !item.id || !item.title) {
4095
+ return [];
4096
+ }
4097
+ return [
4098
+ createItem({
4099
+ id: item.id,
4100
+ title: item.title,
4101
+ url: item.url ?? `https://news.ycombinator.com/item?id=${item.id}`,
4102
+ ...optional("author", item.by),
4103
+ ...optional("commentCount", item.descendants),
4104
+ ...optional("score", item.score),
4105
+ ...optional("text", item.text),
4106
+ ...optional("timeSeconds", item.time)
4107
+ })
4108
+ ];
4109
+ }
4110
+ function mapAlgoliaHit(hit) {
4111
+ const id = hit.story_id ?? parseNumericId(hit.objectID);
4112
+ if (!id || !hit.title) {
4113
+ return [];
4114
+ }
4115
+ return [
4116
+ createItem({
4117
+ id,
4118
+ title: hit.title,
4119
+ url: hit.url ?? `https://news.ycombinator.com/item?id=${id}`,
4120
+ ...optional("author", hit.author),
4121
+ ...optional("commentCount", hit.num_comments),
4122
+ ...optional("score", hit.points),
4123
+ ...optional("timeSeconds", hit.created_at_i)
4124
+ })
4125
+ ];
4126
+ }
4127
+ function createItem(input2) {
4128
+ const item = {
4129
+ id: input2.id,
4130
+ title: input2.title,
4131
+ type: "story",
4132
+ url: input2.url
4133
+ };
4134
+ if (input2.author !== void 0) {
4135
+ item.author = input2.author;
4136
+ }
4137
+ if (input2.commentCount !== void 0) {
4138
+ item.commentCount = input2.commentCount;
4139
+ }
4140
+ if (input2.score !== void 0) {
4141
+ item.score = input2.score;
4142
+ }
4143
+ if (input2.text !== void 0) {
4144
+ item.text = input2.text;
4145
+ }
4146
+ if (input2.timeSeconds !== void 0) {
4147
+ item.time = {
4148
+ iso: new Date(input2.timeSeconds * 1e3).toISOString(),
4149
+ seconds: input2.timeSeconds
4150
+ };
4151
+ }
4152
+ return item;
4153
+ }
4154
+ function parseNumericId(value) {
4155
+ if (!value || !/^\d+$/.test(value)) {
4156
+ return void 0;
4157
+ }
4158
+ return Number(value);
4159
+ }
4160
+
4161
+ // src/index.ts
4162
+ var createDefaultReadline = () => createInterface({ input, output });
4163
+ function createCli(overrides = {}) {
4164
+ const paths = overrides.paths ?? envPaths("stephen");
4165
+ const runtimeEnv = overrides.env ?? process.env;
4166
+ const masterKey = overrides.masterKey ?? Buffer.from("0123456789abcdef0123456789abcdef", "utf8");
4167
+ const diskRuntime = overrides.diskRuntime ?? createDiskCleanupRuntime();
4168
+ const hackerNewsRuntime = overrides.hackerNewsRuntime ?? createDefaultHackerNewsRuntime();
4169
+ const kr36Runtime = overrides.kr36Runtime ?? createDefaultKr36Runtime();
4170
+ const toutiaoRuntime = overrides.toutiaoRuntime ?? createDefaultToutiaoRuntime();
4171
+ const videoRuntime = overrides.videoRuntime ?? createDefaultVideoRuntime();
4172
+ let repositoryCache = overrides.repository ? { promise: Promise.resolve(overrides.repository), repository: overrides.repository } : null;
4173
+ const getRepository = () => {
4174
+ if (repositoryCache && overrides.repository) {
4175
+ return overrides.repository;
4176
+ }
4177
+ if (!repositoryCache) {
4178
+ const config = loadAkConfig(paths.config);
4179
+ const resolvedDatabasePath = resolveAkDatabasePath({
4180
+ config,
4181
+ defaultDataDir: paths.data,
4182
+ env: runtimeEnv
4183
+ });
4184
+ mkdirSync2(dirname2(resolvedDatabasePath.path), { recursive: true });
4185
+ let repository;
4186
+ try {
4187
+ repository = new AkRepository(createAkDatabase(resolvedDatabasePath.path));
4188
+ } catch (error) {
4189
+ throw new AkStorageInitError(resolvedDatabasePath.path, error);
4190
+ }
4191
+ repositoryCache = { promise: Promise.resolve(repository), repository };
4192
+ }
4193
+ return repositoryCache.repository;
4194
+ };
4195
+ return createAkCli({
4196
+ confirm: overrides.confirm ?? defaultConfirm,
4197
+ diskRuntime,
4198
+ env: runtimeEnv,
4199
+ getRepository,
4200
+ hackerNewsRuntime,
4201
+ kr36Runtime,
4202
+ masterKey,
4203
+ now: overrides.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
4204
+ paths,
4205
+ resolveDiskCleanupRoots: () => resolveDiskCleanupRoots(runtimeEnv),
4206
+ stderr: overrides.stderr ?? ((value) => process.stderr.write(value)),
4207
+ stdout: overrides.stdout ?? ((value) => process.stdout.write(value)),
4208
+ toutiaoRuntime,
4209
+ videoRuntime
4210
+ });
4211
+ }
4212
+ async function defaultConfirm(message, createReadline = createDefaultReadline) {
4213
+ const readline = createReadline();
4214
+ try {
4215
+ const answer = await readline.question(`${message} [y/N] `);
4216
+ return answer.trim().toLowerCase() === "y";
4217
+ } finally {
4218
+ readline.close();
4219
+ }
4220
+ }
4221
+ function isMainEntrypoint(moduleUrl, argv) {
4222
+ const scriptPath = argv[1];
4223
+ if (!scriptPath) {
4224
+ return false;
4225
+ }
4226
+ return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(scriptPath);
4227
+ }
4228
+ if (isMainEntrypoint(import.meta.url, process.argv)) {
4229
+ const cli = createCli();
4230
+ void cli.run(process.argv.slice(2)).then((exitCode) => {
4231
+ process.exitCode = exitCode;
4232
+ });
4233
+ }
4234
+ export {
4235
+ createCli,
4236
+ defaultConfirm,
4237
+ isMainEntrypoint
4238
+ };
4239
+ //# sourceMappingURL=index.js.map