@tq1086/urpf-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2267 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/pack.ts
7
+ import path2 from "path";
8
+ import fs3 from "fs/promises";
9
+
10
+ // src/core/scanner/file-scanner.ts
11
+ import * as fs from "fs/promises";
12
+ import * as path from "path";
13
+
14
+ // src/core/scanner/ignore/ignore-engine.ts
15
+ var IgnoreEngine = class {
16
+ constructor(options) {
17
+ this.rules = [];
18
+ this.options = {
19
+ caseSensitive: false,
20
+ pathSeparator: "/",
21
+ gitignoreExtended: true
22
+ };
23
+ if (options) {
24
+ this.options = { ...this.options, ...options };
25
+ }
26
+ }
27
+ /**
28
+ * 添加忽略规则
29
+ * @param rule 忽略规则
30
+ */
31
+ addRule(rule) {
32
+ this.rules.push(rule);
33
+ }
34
+ /**
35
+ * 添加多个忽略规则
36
+ * @param rules 忽略规则列表
37
+ */
38
+ addRules(rules) {
39
+ this.rules.push(...rules);
40
+ }
41
+ /**
42
+ * 移除所有规则
43
+ */
44
+ clear() {
45
+ this.rules = [];
46
+ }
47
+ /**
48
+ * 获取所有规则
49
+ * @returns 规则列表
50
+ */
51
+ getRules() {
52
+ return [...this.rules];
53
+ }
54
+ /**
55
+ * 匹配文件路径
56
+ * @param filePath 文件路径
57
+ * @param isDirectory 是否为目录
58
+ * @returns 匹配结果
59
+ */
60
+ match(filePath, isDirectory = false) {
61
+ const normalizedPath = this.normalizePath(filePath);
62
+ let lastMatchedRuleIndex;
63
+ let lastMatchedRule;
64
+ for (let i = 0; i < this.rules.length; i++) {
65
+ const rule = this.rules[i];
66
+ if (rule.isDirectoryOnly && !isDirectory) {
67
+ continue;
68
+ }
69
+ if (this.matchRule(normalizedPath, rule, isDirectory)) {
70
+ lastMatchedRuleIndex = i;
71
+ lastMatchedRule = rule;
72
+ }
73
+ }
74
+ if (lastMatchedRule) {
75
+ return {
76
+ matched: !lastMatchedRule.isNegation,
77
+ ruleIndex: lastMatchedRuleIndex,
78
+ rule: lastMatchedRule
79
+ };
80
+ }
81
+ return { matched: false };
82
+ }
83
+ /**
84
+ * 批量匹配文件路径
85
+ * @param filePaths 文件路径列表
86
+ * @param isDirectory 是否为目录
87
+ * @returns 匹配结果列表
88
+ */
89
+ matchBatch(filePaths, isDirectory = false) {
90
+ return filePaths.map((filePath) => this.match(filePath, isDirectory));
91
+ }
92
+ /**
93
+ * 匹配单个规则
94
+ * @param filePath 文件路径
95
+ * @param rule 忽略规则
96
+ * @param isDirectory 是否为目录
97
+ * @returns 是否匹配
98
+ */
99
+ matchRule(filePath, rule, isDirectory) {
100
+ const pattern = rule.pattern;
101
+ if (rule.isAbsolute) {
102
+ return this.matchPattern(filePath, pattern, isDirectory, true);
103
+ }
104
+ const parts = filePath.split(this.options.pathSeparator);
105
+ for (let i = 0; i < parts.length; i++) {
106
+ const subPath = parts.slice(i).join(this.options.pathSeparator);
107
+ if (this.matchPattern(subPath, pattern, isDirectory, false)) {
108
+ return true;
109
+ }
110
+ }
111
+ return false;
112
+ }
113
+ /**
114
+ * 匹配通配符模式
115
+ * @param filePath 文件路径
116
+ * @param pattern 模式
117
+ * @param isDirectory 是否为目录
118
+ * @param fromStart 是否从开头匹配
119
+ * @returns 是否匹配
120
+ */
121
+ matchPattern(filePath, pattern, isDirectory, fromStart) {
122
+ const regex = this.patternToRegex(pattern, fromStart);
123
+ if (regex.test(filePath)) {
124
+ return true;
125
+ }
126
+ if (isDirectory && !filePath.endsWith(this.options.pathSeparator)) {
127
+ const dirPath = filePath + this.options.pathSeparator;
128
+ return regex.test(dirPath);
129
+ }
130
+ return false;
131
+ }
132
+ /**
133
+ * 将通配符模式转换为正则表达式
134
+ * @param pattern 通配符模式
135
+ * @param fromStart 是否从开头匹配
136
+ * @returns 正则表达式
137
+ */
138
+ patternToRegex(pattern, fromStart) {
139
+ let regexPattern = "";
140
+ const specialChars = /[.+?^${}()|[\]\\]/g;
141
+ regexPattern = pattern.replace(specialChars, "\\$&");
142
+ regexPattern = regexPattern.replace(/\*\*/g, ".*");
143
+ regexPattern = regexPattern.replace(/(?<!\.)\*(?!\*)/g, "[^/]*");
144
+ regexPattern = regexPattern.replace(/\?/g, "[^/]");
145
+ regexPattern = regexPattern.replace(/\[!\]/g, "\\[!\\]");
146
+ regexPattern = regexPattern.replace(/\[([^\]]+)\]/g, (match, content) => {
147
+ if (content.startsWith("!")) {
148
+ return `[^${content.substring(1)}]`;
149
+ }
150
+ return `[${content}]`;
151
+ });
152
+ if (fromStart) {
153
+ regexPattern = `^${regexPattern}$`;
154
+ } else {
155
+ regexPattern = `(^|/)${regexPattern}$`;
156
+ }
157
+ const flags = this.options.caseSensitive ? "" : "i";
158
+ return new RegExp(regexPattern, flags);
159
+ }
160
+ /**
161
+ * 规范化路径
162
+ * @param filePath 文件路径
163
+ * @returns 规范化后的路径
164
+ */
165
+ normalizePath(filePath) {
166
+ let normalized = filePath;
167
+ normalized = normalized.replace(/\\/g, this.options.pathSeparator);
168
+ if (normalized.startsWith(this.options.pathSeparator)) {
169
+ normalized = normalized.substring(1);
170
+ }
171
+ if (normalized.endsWith(this.options.pathSeparator)) {
172
+ normalized = normalized.slice(0, -1);
173
+ }
174
+ return normalized;
175
+ }
176
+ };
177
+
178
+ // src/core/scanner/file-scanner.ts
179
+ var FileScanner = class {
180
+ /**
181
+ * 扫描目录
182
+ * @param rootPath 根目录路径
183
+ * @param options 扫描选项
184
+ * @returns 扫描结果
185
+ */
186
+ static async scanDirectory(rootPath, options = {}) {
187
+ const startTime = Date.now();
188
+ const files = [];
189
+ const directories = [];
190
+ const skippedFiles = [];
191
+ try {
192
+ const absoluteRootPath = path.resolve(rootPath);
193
+ const ignoreEngine = options.ignoreRules ? new IgnoreEngine() : void 0;
194
+ if (ignoreEngine && options.ignoreRules) {
195
+ ignoreEngine.addRules(options.ignoreRules);
196
+ }
197
+ await this.scanRecursive(
198
+ absoluteRootPath,
199
+ "",
200
+ 0,
201
+ options,
202
+ files,
203
+ directories,
204
+ skippedFiles,
205
+ ignoreEngine
206
+ );
207
+ const endTime = Date.now();
208
+ const duration = endTime - startTime;
209
+ return {
210
+ rootPath: absoluteRootPath,
211
+ files,
212
+ directories,
213
+ skippedFiles,
214
+ startTime,
215
+ endTime,
216
+ duration
217
+ };
218
+ } catch (error) {
219
+ throw new Error(`\u626B\u63CF\u76EE\u5F55\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
220
+ }
221
+ }
222
+ /**
223
+ * 递归扫描目录
224
+ * @param currentPath 当前路径
225
+ * @param relativePath 相对路径
226
+ * @param currentDepth 当前深度
227
+ * @param options 扫描选项
228
+ * @param files 文件列表
229
+ * @param directories 目录列表
230
+ * @param skippedFiles 被忽略的文件列表
231
+ */
232
+ static async scanRecursive(currentPath, relativePath, currentDepth, options, files, directories, skippedFiles, ignoreEngine) {
233
+ if (options.maxDepth !== void 0 && options.maxDepth > 0 && currentDepth >= options.maxDepth) {
234
+ return;
235
+ }
236
+ try {
237
+ const entries = await fs.readdir(currentPath, { withFileTypes: true });
238
+ for (const entry of entries) {
239
+ const entryName = entry.name;
240
+ const entryPath = path.join(currentPath, entryName);
241
+ const entryRelativePath = relativePath ? path.join(relativePath, entryName) : entryName;
242
+ if (!options.includeHidden && entryName.startsWith(".")) {
243
+ skippedFiles.push(entryRelativePath);
244
+ continue;
245
+ }
246
+ try {
247
+ const stats = await fs.stat(entryPath);
248
+ const metadata = {
249
+ path: entryRelativePath,
250
+ name: entryName,
251
+ isDirectory: entry.isDirectory(),
252
+ isSymlink: entry.isSymbolicLink(),
253
+ size: stats.size,
254
+ permissions: this.modeToPermissions(stats.mode),
255
+ mtime: stats.mtimeMs,
256
+ ctime: stats.ctimeMs,
257
+ encoding: "utf-8",
258
+ newlineType: "lf",
259
+ isBinary: false
260
+ };
261
+ if (entry.isSymbolicLink()) {
262
+ if (!options.followSymlinks) {
263
+ skippedFiles.push(entryRelativePath);
264
+ continue;
265
+ }
266
+ const realStats = await fs.stat(entryPath);
267
+ metadata.isDirectory = realStats.isDirectory();
268
+ metadata.size = realStats.size;
269
+ }
270
+ if (ignoreEngine) {
271
+ const matchResult = ignoreEngine.match(entryRelativePath, metadata.isDirectory);
272
+ if (matchResult.matched) {
273
+ skippedFiles.push(entryRelativePath);
274
+ continue;
275
+ }
276
+ }
277
+ if (options.fileFilter && !options.fileFilter(metadata)) {
278
+ skippedFiles.push(entryRelativePath);
279
+ continue;
280
+ }
281
+ if (metadata.isDirectory) {
282
+ directories.push(metadata);
283
+ await this.scanRecursive(
284
+ entryPath,
285
+ entryRelativePath,
286
+ currentDepth + 1,
287
+ options,
288
+ files,
289
+ directories,
290
+ skippedFiles,
291
+ ignoreEngine
292
+ );
293
+ } else {
294
+ files.push(metadata);
295
+ }
296
+ } catch (error) {
297
+ skippedFiles.push(entryRelativePath);
298
+ }
299
+ }
300
+ } catch (error) {
301
+ throw new Error(`\u8BFB\u53D6\u76EE\u5F55\u5931\u8D25: ${currentPath} - ${error instanceof Error ? error.message : String(error)}`);
302
+ }
303
+ }
304
+ /**
305
+ * 将文件模式转换为权限字符串
306
+ * @param mode 文件模式
307
+ * @returns 权限字符串(如 "0644")
308
+ */
309
+ static modeToPermissions(mode) {
310
+ return (mode & 511).toString(8).padStart(3, "0");
311
+ }
312
+ /**
313
+ * 计算扫描统计信息
314
+ * @param result 扫描结果
315
+ * @returns 统计信息
316
+ */
317
+ static calculateStatistics(result) {
318
+ const totalFiles = result.files.length;
319
+ const totalDirectories = result.directories.length;
320
+ const ignoredFiles = result.skippedFiles.length;
321
+ const symlinks = result.files.filter((f) => f.isSymlink).length + result.directories.filter((d) => d.isSymlink).length;
322
+ const totalSize = result.files.reduce((sum, f) => sum + f.size, 0);
323
+ return {
324
+ totalFiles,
325
+ totalDirectories,
326
+ ignoredFiles,
327
+ symlinks,
328
+ totalSize
329
+ };
330
+ }
331
+ /**
332
+ * 过滤文件列表
333
+ * @param files 文件列表
334
+ * @param rules 忽略规则
335
+ * @returns 过滤后的文件列表
336
+ */
337
+ static filterFiles(files, rules) {
338
+ if (rules.length === 0) {
339
+ return files;
340
+ }
341
+ return files;
342
+ }
343
+ };
344
+
345
+ // src/core/scanner/ignore/ignore-parser.ts
346
+ var IgnoreRuleParser = class {
347
+ /**
348
+ * 解析忽略规则字符串
349
+ * @param pattern 规则模式字符串
350
+ * @param sourceFile 规则来源文件(可选)
351
+ * @returns 忽略规则
352
+ */
353
+ parse(pattern, sourceFile) {
354
+ const trimmedPattern = pattern.trim();
355
+ if (!trimmedPattern || trimmedPattern.startsWith("#")) {
356
+ throw new Error("\u65E0\u6548\u7684\u5FFD\u7565\u89C4\u5219\uFF1A\u7A7A\u884C\u6216\u6CE8\u91CA");
357
+ }
358
+ let rulePattern = trimmedPattern;
359
+ let isNegation = false;
360
+ let isDirectoryOnly = false;
361
+ let isAbsolute = false;
362
+ if (rulePattern.startsWith("!")) {
363
+ isNegation = true;
364
+ rulePattern = rulePattern.substring(1).trim();
365
+ }
366
+ if (rulePattern.endsWith("/")) {
367
+ isDirectoryOnly = true;
368
+ rulePattern = rulePattern.slice(0, -1);
369
+ }
370
+ if (rulePattern.startsWith("/")) {
371
+ isAbsolute = true;
372
+ rulePattern = rulePattern.substring(1);
373
+ }
374
+ return {
375
+ pattern: rulePattern,
376
+ isNegation,
377
+ isDirectoryOnly,
378
+ isAbsolute,
379
+ sourceFile
380
+ };
381
+ }
382
+ /**
383
+ * 解析忽略文件内容
384
+ * @param content 文件内容
385
+ * @param sourceFile 规则来源文件(可选)
386
+ * @returns 忽略规则列表
387
+ */
388
+ parseFile(content, sourceFile) {
389
+ const rules = [];
390
+ const lines = content.split("\n");
391
+ for (let i = 0; i < lines.length; i++) {
392
+ const line = lines[i].trim();
393
+ if (!line || line.startsWith("#")) {
394
+ continue;
395
+ }
396
+ try {
397
+ const rule = this.parse(line, sourceFile);
398
+ rules.push(rule);
399
+ } catch (error) {
400
+ console.warn(`\u7B2C ${i + 1} \u884C: \u65E0\u6548\u7684\u5FFD\u7565\u89C4\u5219 "${line}"`);
401
+ }
402
+ }
403
+ return rules;
404
+ }
405
+ /**
406
+ * 解析忽略文件(从文件系统)
407
+ * @param filePath 文件路径
408
+ * @returns 忽略规则列表
409
+ */
410
+ async parseFileFromPath(filePath) {
411
+ const fs7 = await import("fs/promises");
412
+ const content = await fs7.readFile(filePath, "utf-8");
413
+ return this.parseFile(content, filePath);
414
+ }
415
+ /**
416
+ * 从多个文件加载忽略规则
417
+ * @param filePaths 文件路径列表
418
+ * @returns 忽略规则列表
419
+ */
420
+ async parseMultipleFiles(filePaths) {
421
+ const allRules = [];
422
+ for (const filePath of filePaths) {
423
+ try {
424
+ const rules = await this.parseFileFromPath(filePath);
425
+ allRules.push(...rules);
426
+ } catch (error) {
427
+ console.warn(`\u65E0\u6CD5\u8BFB\u53D6\u5FFD\u7565\u6587\u4EF6: ${filePath}`);
428
+ }
429
+ }
430
+ return allRules;
431
+ }
432
+ /**
433
+ * 查找并解析项目根目录的忽略文件
434
+ * @param rootPath 项目根目录
435
+ * @param ignoreFileNames 忽略文件名列表
436
+ * @returns 忽略规则列表
437
+ */
438
+ async findAndParseIgnoreFiles(rootPath, ignoreFileNames = [".gitignore", ".iflowignore", ".npmignore"]) {
439
+ const fs7 = await import("fs/promises");
440
+ const path6 = await import("path");
441
+ const rules = [];
442
+ for (const fileName of ignoreFileNames) {
443
+ const filePath = path6.join(rootPath, fileName);
444
+ try {
445
+ await fs7.access(filePath);
446
+ const fileRules = await this.parseFileFromPath(filePath);
447
+ rules.push(...fileRules);
448
+ } catch {
449
+ }
450
+ }
451
+ return rules;
452
+ }
453
+ };
454
+
455
+ // src/core/generator/urpf-generator.ts
456
+ import crypto from "crypto";
457
+ var URPFGenerator = class {
458
+ constructor(options = {}) {
459
+ this.state = {
460
+ boundaryToken: options.boundaryToken || this.generateBoundaryToken(),
461
+ includeBoundaries: options.includeBoundaries !== false,
462
+ newlineType: options.newlineType || "lf"
463
+ };
464
+ }
465
+ /**
466
+ * 生成随机边界令牌(8位十六进制)
467
+ */
468
+ generateBoundaryToken() {
469
+ return crypto.randomBytes(4).toString("hex");
470
+ }
471
+ /**
472
+ * 获取换行符
473
+ */
474
+ getNewline() {
475
+ return this.state.newlineType === "lf" ? "\n" : "\r\n";
476
+ }
477
+ /**
478
+ * 生成开始边界
479
+ */
480
+ generateStartBoundary() {
481
+ return `--URPF-BOUNDARY-${this.state.boundaryToken}--`;
482
+ }
483
+ /**
484
+ * 生成结束边界
485
+ */
486
+ generateEndBoundary() {
487
+ return `--URPF-BOUNDARY-${this.state.boundaryToken}--`;
488
+ }
489
+ /**
490
+ * 生成分隔符
491
+ */
492
+ generateDelimiter() {
493
+ return `--URPF-BOUNDARY-${this.state.boundaryToken}--`;
494
+ }
495
+ /**
496
+ * 生成属性部分
497
+ */
498
+ generatePropertySection(properties) {
499
+ const newline = this.getNewline();
500
+ const lines = [];
501
+ for (const prop of properties) {
502
+ if (prop.name.includes(" ")) {
503
+ lines.push(`\${${prop.name}}=${prop.value}`);
504
+ } else {
505
+ lines.push(`$${prop.name}=${prop.value}`);
506
+ }
507
+ }
508
+ return lines.join(newline);
509
+ }
510
+ /**
511
+ * 生成资源头部
512
+ */
513
+ generateResourceHeader(resource) {
514
+ const header = `@${resource.header.udrsReference} ${resource.header.encoding} ${resource.header.permissions} ${resource.header.newlineType}`;
515
+ return header;
516
+ }
517
+ /**
518
+ * 生成资源部分
519
+ */
520
+ generateResourceSection(resource) {
521
+ const newline = this.getNewline();
522
+ const header = this.generateResourceHeader(resource);
523
+ const content = resource.content;
524
+ return `${header}${newline}${content}`;
525
+ }
526
+ /**
527
+ * 生成 URPF 文档
528
+ */
529
+ async generate(document) {
530
+ const startTime = Date.now();
531
+ const newline = this.getNewline();
532
+ const parts = [];
533
+ if (this.state.includeBoundaries) {
534
+ parts.push(this.generateStartBoundary());
535
+ }
536
+ if (document.properties && document.properties.length > 0) {
537
+ parts.push(this.generatePropertySection(document.properties));
538
+ parts.push(this.generateDelimiter());
539
+ }
540
+ for (const resource of document.resources) {
541
+ parts.push(this.generateResourceSection(resource));
542
+ parts.push(this.generateDelimiter());
543
+ }
544
+ if (this.state.includeBoundaries) {
545
+ parts.pop();
546
+ parts.push(this.generateEndBoundary());
547
+ } else {
548
+ parts.pop();
549
+ }
550
+ const content = parts.join(newline);
551
+ const duration = Date.now() - startTime;
552
+ return {
553
+ content,
554
+ size: Buffer.byteLength(content, "utf-8"),
555
+ resourceCount: document.resources.length,
556
+ duration
557
+ };
558
+ }
559
+ /**
560
+ * 获取当前边界令牌
561
+ */
562
+ getBoundaryToken() {
563
+ return this.state.boundaryToken;
564
+ }
565
+ /**
566
+ * 设置边界令牌
567
+ */
568
+ setBoundaryToken(token) {
569
+ if (!/^[0-9a-fA-F]{8}$/.test(token)) {
570
+ throw new Error("\u8FB9\u754C\u4EE4\u724C\u5FC5\u987B\u662F8\u4F4D\u5341\u516D\u8FDB\u5236\u6570\u5B57");
571
+ }
572
+ this.state.boundaryToken = token;
573
+ }
574
+ };
575
+
576
+ // src/core/generator/resource-serializer.ts
577
+ import fs2 from "fs/promises";
578
+ var ResourceSerializer = class {
579
+ constructor(options = {}) {
580
+ this.options = {
581
+ encoding: options.encoding || "utf-8",
582
+ detectEncoding: options.detectEncoding !== false,
583
+ detectNewline: options.detectNewline !== false
584
+ };
585
+ }
586
+ /**
587
+ * 检测文件编码
588
+ */
589
+ async detectFileEncoding(buffer) {
590
+ if (buffer.length >= 3 && buffer[0] === 239 && buffer[1] === 187 && buffer[2] === 191) {
591
+ return "utf-8";
592
+ }
593
+ if (buffer.length >= 2 && buffer[0] === 255 && buffer[1] === 254) {
594
+ return "utf-16le";
595
+ }
596
+ if (buffer.length >= 2 && buffer[0] === 254 && buffer[1] === 255) {
597
+ return "utf-16be";
598
+ }
599
+ try {
600
+ Buffer.from(buffer.toString("utf-8"), "utf-8");
601
+ return "utf-8";
602
+ } catch {
603
+ try {
604
+ Buffer.from(buffer.toString("gbk"), "gbk");
605
+ return "gbk";
606
+ } catch {
607
+ return "binary";
608
+ }
609
+ }
610
+ }
611
+ /**
612
+ * 检测换行符类型
613
+ */
614
+ detectNewlineType(buffer) {
615
+ const content = buffer.toString("utf-8");
616
+ let crlfCount = 0;
617
+ let lfCount = 0;
618
+ let crCount = 0;
619
+ for (let i = 0; i < content.length; i++) {
620
+ if (content[i] === "\r" && content[i + 1] === "\n") {
621
+ crlfCount++;
622
+ i++;
623
+ } else if (content[i] === "\n") {
624
+ lfCount++;
625
+ } else if (content[i] === "\r") {
626
+ crCount++;
627
+ }
628
+ }
629
+ if (crlfCount >= lfCount && crlfCount >= crCount) {
630
+ return "crlf";
631
+ } else if (lfCount >= crCount) {
632
+ return "lf";
633
+ } else {
634
+ return "cr";
635
+ }
636
+ }
637
+ /**
638
+ * 转换文件权限为八进制字符串
639
+ */
640
+ convertPermissions(mode) {
641
+ const permissions = mode & 511;
642
+ return permissions.toString(8).padStart(3, "0");
643
+ }
644
+ /**
645
+ * 读取文件内容
646
+ */
647
+ async readFile(filePath, encoding) {
648
+ if (encoding === "binary") {
649
+ const buffer = await fs2.readFile(filePath);
650
+ return buffer.toString("base64");
651
+ } else {
652
+ return await fs2.readFile(filePath, encoding);
653
+ }
654
+ }
655
+ /**
656
+ * 序列化文件为资源部分
657
+ */
658
+ async serializeFile(filePath, metadata, udrsReference) {
659
+ let encoding = this.options.encoding || "utf-8";
660
+ let newlineType = "lf";
661
+ if (this.options.detectEncoding) {
662
+ const buffer = await fs2.readFile(filePath);
663
+ encoding = await this.detectFileEncoding(buffer);
664
+ }
665
+ if (this.options.detectNewline && encoding !== "binary") {
666
+ const buffer = await fs2.readFile(filePath);
667
+ newlineType = this.detectNewlineType(buffer);
668
+ }
669
+ const content = await this.readFile(filePath, encoding);
670
+ const permissions = this.convertPermissions(metadata.permissions);
671
+ const header = {
672
+ udrsReference,
673
+ encoding,
674
+ permissions,
675
+ newlineType
676
+ };
677
+ return {
678
+ header,
679
+ content
680
+ };
681
+ }
682
+ /**
683
+ * 批量序列化文件
684
+ */
685
+ async serializeFiles(files) {
686
+ const resources = [];
687
+ for (const file of files) {
688
+ const resource = await this.serializeFile(file.filePath, file.metadata, file.udrsReference);
689
+ resources.push(resource);
690
+ }
691
+ return resources;
692
+ }
693
+ };
694
+
695
+ // src/commands/pack.ts
696
+ var PackCommand = class {
697
+ /**
698
+ * 执行 pack 命令
699
+ */
700
+ async execute(inputPath, options = {}) {
701
+ const startTime = Date.now();
702
+ try {
703
+ const resolvedPath = path2.resolve(inputPath);
704
+ await this.validatePath(resolvedPath);
705
+ const outputPath = options.output || this.generateOutputPath(resolvedPath);
706
+ const scanResult = await this.scanFiles(resolvedPath, options);
707
+ const urpfDocument = await this.generateURPFDocument(scanResult, resolvedPath);
708
+ const urpfResult = await this.saveURPFDocument(urpfDocument, outputPath, options);
709
+ const duration = Date.now() - startTime;
710
+ if (options.verbose) {
711
+ this.logVerbose(scanResult, urpfResult, outputPath, duration);
712
+ }
713
+ return {
714
+ success: true,
715
+ outputPath,
716
+ fileCount: scanResult.files.length,
717
+ directoryCount: scanResult.directories.length,
718
+ skippedCount: scanResult.skippedFiles.length,
719
+ urpfSize: urpfResult.size,
720
+ duration
721
+ };
722
+ } catch (err) {
723
+ const duration = Date.now() - startTime;
724
+ const errorMessage = err instanceof Error ? err.message : String(err);
725
+ if (options.verbose) {
726
+ console.error(`\u9519\u8BEF: ${errorMessage}`);
727
+ }
728
+ return {
729
+ success: false,
730
+ outputPath: "",
731
+ fileCount: 0,
732
+ directoryCount: 0,
733
+ skippedCount: 0,
734
+ urpfSize: 0,
735
+ duration,
736
+ error: errorMessage
737
+ };
738
+ }
739
+ }
740
+ /**
741
+ * 验证输入路径
742
+ */
743
+ async validatePath(filePath) {
744
+ try {
745
+ const stats = await fs3.stat(filePath);
746
+ if (!stats.isFile() && !stats.isDirectory()) {
747
+ throw new Error(`\u8DEF\u5F84\u4E0D\u662F\u6587\u4EF6\u6216\u76EE\u5F55: ${filePath}`);
748
+ }
749
+ } catch {
750
+ throw new Error(`\u65E0\u6CD5\u8BBF\u95EE\u8DEF\u5F84: ${filePath}`);
751
+ }
752
+ }
753
+ /**
754
+ * 生成输出路径
755
+ */
756
+ generateOutputPath(inputPath) {
757
+ return `${inputPath}.urpf`;
758
+ }
759
+ /**
760
+ * 扫描文件
761
+ */
762
+ async scanFiles(rootPath, options) {
763
+ const stats = await fs3.stat(rootPath);
764
+ if (stats.isFile()) {
765
+ const metadata = await this.collectFileMetadata(rootPath);
766
+ return {
767
+ files: [metadata],
768
+ directories: [],
769
+ skippedFiles: [],
770
+ statistics: {
771
+ totalFiles: 1,
772
+ totalDirectories: 0,
773
+ skippedFiles: 0
774
+ }
775
+ };
776
+ }
777
+ let ignoreRules = [];
778
+ if (options.ignoreFile) {
779
+ const parser = new IgnoreRuleParser();
780
+ const content = await fs3.readFile(options.ignoreFile, "utf-8");
781
+ ignoreRules = parser.parseFile(content);
782
+ } else {
783
+ const defaultIgnoreFiles = [".gitignore", ".iflowignore"];
784
+ for (const ignoreFile of defaultIgnoreFiles) {
785
+ const ignorePath = path2.join(rootPath, ignoreFile);
786
+ try {
787
+ await fs3.access(ignorePath);
788
+ const parser = new IgnoreRuleParser();
789
+ const content = await fs3.readFile(ignorePath, "utf-8");
790
+ ignoreRules = [...ignoreRules, ...parser.parseFile(content)];
791
+ break;
792
+ } catch {
793
+ }
794
+ }
795
+ }
796
+ const effectiveMaxDepth = options.noRecurse ? 1 : options.maxDepth;
797
+ return await FileScanner.scanDirectory(rootPath, {
798
+ ignoreRules,
799
+ followSymlinks: options.followSymlinks || false,
800
+ maxDepth: effectiveMaxDepth
801
+ // undefined 或 1
802
+ });
803
+ }
804
+ /**
805
+ * 收集文件元数据
806
+ */
807
+ async collectFileMetadata(filePath) {
808
+ const stats = await fs3.stat(filePath);
809
+ return {
810
+ path: filePath,
811
+ name: path2.basename(filePath),
812
+ size: stats.size,
813
+ permissions: stats.mode,
814
+ mtime: stats.mtime,
815
+ encoding: "utf-8",
816
+ newlineType: "lf"
817
+ };
818
+ }
819
+ /**
820
+ * 生成 URPF 文档
821
+ */
822
+ async generateURPFDocument(scanResult, rootPath) {
823
+ const serializer = new ResourceSerializer();
824
+ const files = scanResult.files.map((file) => {
825
+ const isAbsolutePath = path2.isAbsolute(file.path);
826
+ const filePath = isAbsolutePath ? file.path : path2.join(rootPath, file.path);
827
+ const relativePath = isAbsolutePath ? path2.relative(rootPath, file.path) : file.path;
828
+ return {
829
+ filePath,
830
+ metadata: file,
831
+ udrsReference: `file://${relativePath}`
832
+ };
833
+ });
834
+ const resources = await serializer.serializeFiles(files);
835
+ return {
836
+ boundaryToken: "",
837
+ // 将由生成器自动生成
838
+ properties: [],
839
+ resources
840
+ };
841
+ }
842
+ /**
843
+ * 保存 URPF 文件
844
+ */
845
+ async saveURPFDocument(document, outputPath, options) {
846
+ const generator = new URPFGenerator();
847
+ const result = await generator.generate(document);
848
+ const outputDir = path2.dirname(outputPath);
849
+ await fs3.mkdir(outputDir, { recursive: true });
850
+ try {
851
+ await fs3.access(outputPath);
852
+ if (!options.force) {
853
+ throw new Error(`\u8F93\u51FA\u6587\u4EF6\u5DF2\u5B58\u5728: ${outputPath}\u3002\u8BF7\u4F7F\u7528 --force \u9009\u9879\u5F3A\u5236\u8986\u76D6\uFF0C\u6216\u6307\u5B9A\u4E0D\u540C\u7684\u8F93\u51FA\u8DEF\u5F84\u3002`);
854
+ }
855
+ } catch (error) {
856
+ if (error.code !== "ENOENT") {
857
+ throw error;
858
+ }
859
+ }
860
+ await fs3.writeFile(outputPath, result.content, "utf-8");
861
+ return result;
862
+ }
863
+ /**
864
+ * 输出详细日志
865
+ */
866
+ logVerbose(scanResult, urpfResult, outputPath, duration) {
867
+ console.log("URPF \u6253\u5305\u5B8C\u6210:");
868
+ console.log(` \u8F93\u51FA\u6587\u4EF6: ${outputPath}`);
869
+ console.log(` \u6587\u4EF6\u6570\u91CF: ${scanResult.files.length}`);
870
+ console.log(` \u76EE\u5F55\u6570\u91CF: ${scanResult.directories.length}`);
871
+ console.log(` \u8DF3\u8FC7\u6587\u4EF6: ${scanResult.skippedFiles.length}`);
872
+ console.log(` URPF \u5927\u5C0F: ${urpfResult.size} \u5B57\u8282`);
873
+ console.log(` \u8D44\u6E90\u6570\u91CF: ${urpfResult.resourceCount}`);
874
+ console.log(` \u6267\u884C\u8017\u65F6: ${duration}ms`);
875
+ }
876
+ };
877
+
878
+ // src/commands/unpack.ts
879
+ import path3 from "path";
880
+ import fs4 from "fs/promises";
881
+
882
+ // src/core/parser/types.ts
883
+ var URPFParseError = class extends Error {
884
+ constructor(message, line, column) {
885
+ super(message);
886
+ this.line = line;
887
+ this.column = column;
888
+ this.name = "URPFParseError";
889
+ }
890
+ };
891
+
892
+ // src/core/parser/boundary-parser.ts
893
+ var BoundaryParser = class _BoundaryParser {
894
+ static {
895
+ /** 边界前缀 */
896
+ this.BOUNDARY_PREFIX = "--URPF-BOUNDARY-";
897
+ }
898
+ static {
899
+ /** 边界后缀 */
900
+ this.BOUNDARY_SUFFIX = "--";
901
+ }
902
+ static {
903
+ /** 边界令牌正则表达式(8位十六进制) */
904
+ this.TOKEN_REGEX = /^[0-9a-fA-F]{8}$/;
905
+ }
906
+ static {
907
+ /** 完整边界正则表达式 */
908
+ this.BOUNDARY_REGEX = /^--URPF-BOUNDARY-([0-9a-fA-F]{8})--$/;
909
+ }
910
+ /**
911
+ * 验证边界令牌格式
912
+ * @param token 边界令牌
913
+ * @returns 是否有效
914
+ */
915
+ static isValidToken(token) {
916
+ return _BoundaryParser.TOKEN_REGEX.test(token);
917
+ }
918
+ /**
919
+ * 提取边界令牌
920
+ * @param line 边界行
921
+ * @returns 边界令牌,如果无效则返回 null
922
+ */
923
+ static extractToken(line) {
924
+ const match = line.match(_BoundaryParser.BOUNDARY_REGEX);
925
+ return match ? match[1].toLowerCase() : null;
926
+ }
927
+ /**
928
+ * 判断是否为边界行
929
+ * @param line 文本行
930
+ * @returns 是否为边界行
931
+ */
932
+ static isBoundaryLine(line) {
933
+ return _BoundaryParser.BOUNDARY_REGEX.test(line);
934
+ }
935
+ /**
936
+ * 生成边界行
937
+ * @param token 边界令牌
938
+ * @param type 边界类型
939
+ * @returns 边界行字符串
940
+ */
941
+ static generateBoundaryLine(token, type) {
942
+ return `${_BoundaryParser.BOUNDARY_PREFIX}${token}${_BoundaryParser.BOUNDARY_SUFFIX}`;
943
+ }
944
+ /**
945
+ * 生成随机边界令牌
946
+ * @returns 随机边界令牌
947
+ */
948
+ static generateRandomToken() {
949
+ let token = "";
950
+ const hexChars = "0123456789abcdef";
951
+ for (let i = 0; i < 8; i++) {
952
+ token += hexChars[Math.floor(Math.random() * 16)];
953
+ }
954
+ return token;
955
+ }
956
+ /**
957
+ * 查找所有边界行
958
+ * @param content 内容字符串
959
+ * @returns 边界匹配结果列表
960
+ */
961
+ static findBoundaries(content) {
962
+ const lines = content.split("\n");
963
+ const boundaries = [];
964
+ lines.forEach((line, index) => {
965
+ if (_BoundaryParser.isBoundaryLine(line)) {
966
+ const token = _BoundaryParser.extractToken(line);
967
+ if (token) {
968
+ const lineNumber = index + 1;
969
+ let type;
970
+ if (index === 0) {
971
+ type = "start" /* START */;
972
+ } else if (index === lines.length - 1) {
973
+ type = "end" /* END */;
974
+ } else {
975
+ type = "delimiter" /* DELIMITER */;
976
+ }
977
+ boundaries.push({ type, token, line: lineNumber });
978
+ }
979
+ }
980
+ });
981
+ return boundaries;
982
+ }
983
+ /**
984
+ * 验证边界一致性
985
+ * @param boundaries 边界列表
986
+ * @throws {URPFParseError} 如果边界不一致
987
+ */
988
+ static validateBoundaries(boundaries) {
989
+ if (boundaries.length < 2) {
990
+ throw new URPFParseError(
991
+ "URPF \u6587\u6863\u5FC5\u987B\u5305\u542B\u81F3\u5C11\u5F00\u59CB\u8FB9\u754C\u548C\u7ED3\u675F\u8FB9\u754C"
992
+ );
993
+ }
994
+ if (boundaries[0].type !== "start" /* START */) {
995
+ throw new URPFParseError(
996
+ `\u7B2C ${boundaries[0].line} \u884C: \u6587\u6863\u5FC5\u987B\u4EE5\u5F00\u59CB\u8FB9\u754C\u5F00\u5934`,
997
+ boundaries[0].line
998
+ );
999
+ }
1000
+ if (boundaries[boundaries.length - 1].type !== "end" /* END */) {
1001
+ throw new URPFParseError(
1002
+ `\u7B2C ${boundaries[boundaries.length - 1].line} \u884C: \u6587\u6863\u5FC5\u987B\u4EE5\u7ED3\u675F\u8FB9\u754C\u7ED3\u5C3E`,
1003
+ boundaries[boundaries.length - 1].line
1004
+ );
1005
+ }
1006
+ const token = boundaries[0].token;
1007
+ for (let i = 1; i < boundaries.length; i++) {
1008
+ if (boundaries[i].token !== token) {
1009
+ throw new URPFParseError(
1010
+ `\u7B2C ${boundaries[i].line} \u884C: \u8FB9\u754C\u4EE4\u724C\u4E0D\u4E00\u81F4\uFF0C\u671F\u671B "${token}"\uFF0C\u5B9E\u9645 "${boundaries[i].token}"`,
1011
+ boundaries[i].line
1012
+ );
1013
+ }
1014
+ }
1015
+ }
1016
+ /**
1017
+ * 根据边界分割内容
1018
+ * @param content 内容字符串
1019
+ * @param boundaries 边界列表
1020
+ * @returns 分割后的内容段列表
1021
+ */
1022
+ static splitByBoundaries(content, boundaries) {
1023
+ const lines = content.split("\n");
1024
+ const sections = [];
1025
+ for (let i = 0; i < boundaries.length - 1; i++) {
1026
+ const startLine = boundaries[i].line - 1;
1027
+ const endLine = boundaries[i + 1].line - 1;
1028
+ if (endLine > startLine + 1) {
1029
+ const sectionLines = lines.slice(startLine + 1, endLine);
1030
+ sections.push(sectionLines.join("\n"));
1031
+ } else {
1032
+ sections.push("");
1033
+ }
1034
+ }
1035
+ return sections;
1036
+ }
1037
+ };
1038
+
1039
+ // src/core/parser/property-parser.ts
1040
+ var PropertyParser = class _PropertyParser {
1041
+ static {
1042
+ /** 简单变量名正则表达式($name) */
1043
+ this.SIMPLE_NAME_REGEX = /^\$([a-zA-Z0-9_]+)$/;
1044
+ }
1045
+ static {
1046
+ /** 带空格变量名正则表达式(${full name}) */
1047
+ this.EXTENDED_NAME_REGEX = /^\$\{([a-zA-Z0-9_ ]+)\}$/;
1048
+ }
1049
+ static {
1050
+ /** 属性行正则表达式 */
1051
+ this.PROPERTY_LINE_REGEX = /^(.+?)=(.*)$/;
1052
+ }
1053
+ /**
1054
+ * 解析属性行
1055
+ * @param line 属性行
1056
+ * @param lineNo 行号
1057
+ * @returns 属性项
1058
+ * @throws {URPFParseError} 如果格式无效
1059
+ */
1060
+ static parsePropertyLine(line, lineNo) {
1061
+ const trimmedLine = line.trim();
1062
+ if (!trimmedLine || trimmedLine.startsWith("#")) {
1063
+ throw new URPFParseError(`\u7B2C ${lineNo} \u884C: \u7A7A\u884C\u6216\u6CE8\u91CA`, lineNo);
1064
+ }
1065
+ const match = trimmedLine.match(_PropertyParser.PROPERTY_LINE_REGEX);
1066
+ if (!match) {
1067
+ throw new URPFParseError(
1068
+ `\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684\u5C5E\u6027\u884C\u683C\u5F0F\uFF0C\u5E94\u4E3A "name=value"`,
1069
+ lineNo
1070
+ );
1071
+ }
1072
+ const namePart = match[1].trim();
1073
+ const valuePart = match[2];
1074
+ const simpleMatch = namePart.match(_PropertyParser.SIMPLE_NAME_REGEX);
1075
+ const extendedMatch = namePart.match(_PropertyParser.EXTENDED_NAME_REGEX);
1076
+ let name;
1077
+ let isExtended = false;
1078
+ if (simpleMatch) {
1079
+ name = simpleMatch[1];
1080
+ isExtended = false;
1081
+ } else if (extendedMatch) {
1082
+ name = extendedMatch[1];
1083
+ isExtended = true;
1084
+ } else {
1085
+ throw new URPFParseError(
1086
+ `\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684\u53D8\u91CF\u540D\u683C\u5F0F\uFF0C\u5E94\u4E3A "$name" \u6216 "\${name with space}"`,
1087
+ lineNo
1088
+ );
1089
+ }
1090
+ return {
1091
+ name,
1092
+ value: valuePart,
1093
+ isExtended
1094
+ };
1095
+ }
1096
+ /**
1097
+ * 解析属性部分
1098
+ * @param content 属性部分内容
1099
+ * @param startLineNo 起始行号
1100
+ * @returns 属性部分
1101
+ * @throws {URPFParseError} 如果解析失败
1102
+ */
1103
+ static parsePropertySection(content, startLineNo = 1) {
1104
+ const lines = content.split("\n");
1105
+ const properties = [];
1106
+ const nameSet = /* @__PURE__ */ new Set();
1107
+ lines.forEach((line, index) => {
1108
+ const lineNo = startLineNo + index;
1109
+ const trimmedLine = line.trim();
1110
+ if (!trimmedLine || trimmedLine.startsWith("#")) {
1111
+ return;
1112
+ }
1113
+ const property = _PropertyParser.parsePropertyLine(line, lineNo);
1114
+ if (nameSet.has(property.name)) {
1115
+ throw new URPFParseError(
1116
+ `\u7B2C ${lineNo} \u884C: \u53D8\u91CF\u540D "${property.name}" \u91CD\u590D\u5B9A\u4E49`,
1117
+ lineNo
1118
+ );
1119
+ }
1120
+ nameSet.add(property.name);
1121
+ properties.push(property);
1122
+ });
1123
+ return { properties };
1124
+ }
1125
+ /**
1126
+ * 检查内容是否为属性部分
1127
+ * @param content 内容
1128
+ * @returns 是否为属性部分
1129
+ */
1130
+ static isPropertySection(content) {
1131
+ const lines = content.split("\n");
1132
+ for (const line of lines) {
1133
+ const trimmedLine = line.trim();
1134
+ if (!trimmedLine || trimmedLine.startsWith("#")) {
1135
+ continue;
1136
+ }
1137
+ return trimmedLine.startsWith("$") || trimmedLine.startsWith("${");
1138
+ }
1139
+ return false;
1140
+ }
1141
+ /**
1142
+ * 生成属性行
1143
+ * @param name 变量名
1144
+ * @param value 变量值
1145
+ * @param isExtended 是否为带空格格式
1146
+ * @returns 属性行字符串
1147
+ */
1148
+ static generatePropertyLine(name, value, isExtended = false) {
1149
+ const namePart = isExtended ? `\${${name}}` : `$${name}`;
1150
+ return `${namePart}=${value}`;
1151
+ }
1152
+ /**
1153
+ * 生成属性部分内容
1154
+ * @param propertySection 属性部分
1155
+ * @returns 属性部分内容字符串
1156
+ */
1157
+ static generatePropertySectionContent(propertySection) {
1158
+ return propertySection.properties.map((p) => _PropertyParser.generatePropertyLine(p.name, p.value, p.isExtended)).join("\n");
1159
+ }
1160
+ };
1161
+
1162
+ // src/core/parser/resource-parser.ts
1163
+ var VALID_ENCODINGS = [
1164
+ "utf-8",
1165
+ "ascii",
1166
+ "gbk",
1167
+ "gb2312",
1168
+ "iso-8859-1",
1169
+ "binary"
1170
+ ];
1171
+ var VALID_NEWLINES = ["lf", "cr", "crlf"];
1172
+ var ResourceParser = class _ResourceParser {
1173
+ static {
1174
+ /** 资源头部正则表达式 */
1175
+ this.RESOURCE_HEADER_REGEX = /^@(\S+)\s+(\S+)\s+(\d{3})\s+(lf|cr|crlf)$/i;
1176
+ }
1177
+ /**
1178
+ * 解析资源头部
1179
+ * @param line 资源头部行
1180
+ * @param lineNo 行号
1181
+ * @returns 资源头部
1182
+ * @throws {URPFParseError} 如果格式无效
1183
+ */
1184
+ static parseResourceHeader(line, lineNo) {
1185
+ const trimmedLine = line.trim();
1186
+ if (!trimmedLine.startsWith("@")) {
1187
+ throw new URPFParseError(
1188
+ `\u7B2C ${lineNo} \u884C: \u8D44\u6E90\u5934\u90E8\u5FC5\u987B\u4EE5 '@' \u5F00\u5934`,
1189
+ lineNo
1190
+ );
1191
+ }
1192
+ const match = trimmedLine.match(_ResourceParser.RESOURCE_HEADER_REGEX);
1193
+ if (!match) {
1194
+ throw new URPFParseError(
1195
+ `\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684\u8D44\u6E90\u5934\u90E8\u683C\u5F0F\uFF0C\u5E94\u4E3A "@<udrs-reference> <encoding> <permissions> <newline-type>"`,
1196
+ lineNo
1197
+ );
1198
+ }
1199
+ const udrsRefString = match[1];
1200
+ const encoding = match[2].toLowerCase();
1201
+ const permissions = match[3];
1202
+ const newlineType = match[4].toLowerCase();
1203
+ if (!VALID_ENCODINGS.includes(encoding)) {
1204
+ throw new URPFParseError(
1205
+ `\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684\u7F16\u7801\u7C7B\u578B "${encoding}"\uFF0C\u6709\u6548\u503C\u4E3A: ${VALID_ENCODINGS.join(", ")}`,
1206
+ lineNo
1207
+ );
1208
+ }
1209
+ if (!VALID_NEWLINES.includes(newlineType)) {
1210
+ throw new URPFParseError(
1211
+ `\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684\u6362\u884C\u7B26\u7C7B\u578B "${newlineType}"\uFF0C\u6709\u6548\u503C\u4E3A: ${VALID_NEWLINES.join(", ")}`,
1212
+ lineNo
1213
+ );
1214
+ }
1215
+ const udrsReference = _ResourceParser.parseUDRSReference(udrsRefString, lineNo);
1216
+ return {
1217
+ udrsReference,
1218
+ encoding,
1219
+ permissions,
1220
+ newlineType
1221
+ };
1222
+ }
1223
+ /**
1224
+ * 解析 UDRS 引用
1225
+ * @param udrsRefString UDRS 引用字符串
1226
+ * @param lineNo 行号
1227
+ * @returns UDRS 引用
1228
+ * @throws {URPFParseError} 如果格式无效
1229
+ */
1230
+ static parseUDRSReference(udrsRefString, lineNo) {
1231
+ const protocolMatch = udrsRefString.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/(.*)$/);
1232
+ if (!protocolMatch) {
1233
+ throw new URPFParseError(
1234
+ `\u7B2C ${lineNo} \u884C: \u65E0\u6548\u7684 UDRS \u5F15\u7528\u683C\u5F0F "${udrsRefString}"\uFF0C\u5E94\u4E3A "<protocol>://<uri>[#<fragment>]"`,
1235
+ lineNo
1236
+ );
1237
+ }
1238
+ const protocol = protocolMatch[1].toLowerCase();
1239
+ const uriAndFragment = protocolMatch[2];
1240
+ const fragmentIndex = uriAndFragment.indexOf("#");
1241
+ const uri = fragmentIndex !== -1 ? uriAndFragment.substring(0, fragmentIndex) : uriAndFragment;
1242
+ const fragment = fragmentIndex !== -1 ? uriAndFragment.substring(fragmentIndex + 1) : void 0;
1243
+ let fragmentType;
1244
+ if (fragment) {
1245
+ if (fragment.startsWith("::line=")) {
1246
+ fragmentType = "raw-line";
1247
+ } else if (fragment.startsWith("::byte=")) {
1248
+ fragmentType = "raw-byte";
1249
+ } else {
1250
+ fragmentType = "structured";
1251
+ }
1252
+ }
1253
+ return {
1254
+ protocol,
1255
+ uri,
1256
+ fragment,
1257
+ fragmentType
1258
+ };
1259
+ }
1260
+ /**
1261
+ * 解析资源内容
1262
+ * @param content 资源内容字符串
1263
+ * @param encoding 编码类型
1264
+ * @param newlineType 换行符类型
1265
+ * @returns 资源内容(Buffer 或 string)
1266
+ */
1267
+ static parseResourceContent(content, encoding, newlineType) {
1268
+ if (encoding === "binary") {
1269
+ return Buffer.from(content, "binary");
1270
+ }
1271
+ let normalizedContent = content;
1272
+ switch (newlineType) {
1273
+ case "lf":
1274
+ break;
1275
+ case "cr":
1276
+ normalizedContent = content.replace(/\r\n/g, "\r").replace(/\n/g, "\r");
1277
+ break;
1278
+ case "crlf":
1279
+ normalizedContent = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n/g, "\r\n");
1280
+ break;
1281
+ }
1282
+ return normalizedContent;
1283
+ }
1284
+ /**
1285
+ * 解析资源部分
1286
+ * @param content 资源部分内容
1287
+ * @param startLineNo 起始行号
1288
+ * @returns 资源部分
1289
+ * @throws {URPFParseError} 如果解析失败
1290
+ */
1291
+ static parseResourceSection(content, startLineNo = 1) {
1292
+ const lines = content.split("\n");
1293
+ if (lines.length === 0) {
1294
+ throw new URPFParseError(
1295
+ `\u7B2C ${startLineNo} \u884C: \u8D44\u6E90\u90E8\u5206\u4E0D\u80FD\u4E3A\u7A7A`,
1296
+ startLineNo
1297
+ );
1298
+ }
1299
+ const header = _ResourceParser.parseResourceHeader(lines[0], startLineNo);
1300
+ const contentLines = lines.slice(1);
1301
+ const contentString = contentLines.join("\n");
1302
+ const parsedContent = _ResourceParser.parseResourceContent(
1303
+ contentString,
1304
+ header.encoding,
1305
+ header.newlineType
1306
+ );
1307
+ return {
1308
+ header,
1309
+ content: parsedContent
1310
+ };
1311
+ }
1312
+ /**
1313
+ * 检查内容是否为资源部分
1314
+ * @param content 内容
1315
+ * @returns 是否为资源部分
1316
+ */
1317
+ static isResourceSection(content) {
1318
+ const lines = content.split("\n");
1319
+ if (lines.length === 0) {
1320
+ return false;
1321
+ }
1322
+ return lines[0].trim().startsWith("@");
1323
+ }
1324
+ /**
1325
+ * 生成资源头部行
1326
+ * @param header 资源头部
1327
+ * @returns 资源头部行字符串
1328
+ */
1329
+ static generateResourceHeaderLine(header) {
1330
+ const udrsRefString = _ResourceParser.generateUDRSReferenceString(header.udrsReference);
1331
+ return `@${udrsRefString} ${header.encoding} ${header.permissions} ${header.newlineType}`;
1332
+ }
1333
+ /**
1334
+ * 生成 UDRS 引用字符串
1335
+ * @param udrsReference UDRS 引用
1336
+ * @returns UDRS 引用字符串
1337
+ */
1338
+ static generateUDRSReferenceString(udrsReference) {
1339
+ let result = `${udrsReference.protocol}://${udrsReference.uri}`;
1340
+ if (udrsReference.fragment) {
1341
+ result += `#${udrsReference.fragment}`;
1342
+ }
1343
+ return result;
1344
+ }
1345
+ /**
1346
+ * 生成资源部分内容
1347
+ * @param resourceSection 资源部分
1348
+ * @returns 资源部分内容字符串
1349
+ */
1350
+ static generateResourceSectionContent(resourceSection) {
1351
+ const headerLine = _ResourceParser.generateResourceHeaderLine(resourceSection.header);
1352
+ let content = "";
1353
+ if (typeof resourceSection.content === "string") {
1354
+ content = resourceSection.content;
1355
+ } else {
1356
+ content = resourceSection.content.toString("binary");
1357
+ }
1358
+ return `${headerLine}
1359
+ ${content}`;
1360
+ }
1361
+ };
1362
+
1363
+ // src/core/parser/variable-replacer.ts
1364
+ var VariableReplacer = class _VariableReplacer {
1365
+ static {
1366
+ /** 变量引用正则表达式(${name} 或 ${full name}) */
1367
+ this.VARIABLE_REF_REGEX = /\$\{([a-zA-Z0-9_ ]+)\}/g;
1368
+ }
1369
+ /**
1370
+ * 创建变量上下文
1371
+ * @param properties 属性列表
1372
+ * @returns 变量上下文
1373
+ */
1374
+ static createContext(properties) {
1375
+ const map = /* @__PURE__ */ new Map();
1376
+ properties.forEach((p) => map.set(p.name, p.value));
1377
+ return {
1378
+ get(name) {
1379
+ return map.get(name);
1380
+ },
1381
+ has(name) {
1382
+ return map.has(name);
1383
+ },
1384
+ keys() {
1385
+ return Array.from(map.keys());
1386
+ }
1387
+ };
1388
+ }
1389
+ /**
1390
+ * 替换字符串中的变量引用
1391
+ * @param input 输入字符串
1392
+ * @param context 变量上下文
1393
+ * @param lineNo 行号(用于错误报告)
1394
+ * @returns 替换后的字符串
1395
+ * @throws {URPFParseError} 如果变量未定义
1396
+ */
1397
+ static replace(input, context, lineNo) {
1398
+ return input.replace(_VariableReplacer.VARIABLE_REF_REGEX, (match, name) => {
1399
+ const trimmedName = name.trim();
1400
+ if (!context.has(trimmedName)) {
1401
+ const lineInfo = lineNo !== void 0 ? `\u7B2C ${lineNo} \u884C: ` : "";
1402
+ throw new URPFParseError(
1403
+ `${lineInfo}\u672A\u5B9A\u4E49\u7684\u53D8\u91CF: "${trimmedName}"`,
1404
+ lineNo
1405
+ );
1406
+ }
1407
+ const value = context.get(trimmedName);
1408
+ return value || "";
1409
+ });
1410
+ }
1411
+ /**
1412
+ * 检查字符串中是否包含变量引用
1413
+ * @param input 输入字符串
1414
+ * @returns 是否包含变量引用
1415
+ */
1416
+ static hasVariableReferences(input) {
1417
+ return _VariableReplacer.VARIABLE_REF_REGEX.test(input);
1418
+ }
1419
+ /**
1420
+ * 提取字符串中的所有变量名
1421
+ * @param input 输入字符串
1422
+ * @returns 变量名列表
1423
+ */
1424
+ static extractVariableNames(input) {
1425
+ const names = [];
1426
+ let match;
1427
+ while ((match = _VariableReplacer.VARIABLE_REF_REGEX.exec(input)) !== null) {
1428
+ names.push(match[1].trim());
1429
+ }
1430
+ return [...new Set(names)];
1431
+ }
1432
+ /**
1433
+ * 验证所有变量引用是否已定义
1434
+ * @param input 输入字符串
1435
+ * @param context 变量上下文
1436
+ * @param lineNo 行号(用于错误报告)
1437
+ * @returns 是否所有变量都已定义
1438
+ */
1439
+ static validateVariables(input, context, lineNo) {
1440
+ const names = _VariableReplacer.extractVariableNames(input);
1441
+ for (const name of names) {
1442
+ if (!context.has(name)) {
1443
+ const lineInfo = lineNo !== void 0 ? `\u7B2C ${lineNo} \u884C: ` : "";
1444
+ throw new URPFParseError(
1445
+ `${lineInfo}\u672A\u5B9A\u4E49\u7684\u53D8\u91CF: "${name}"`,
1446
+ lineNo
1447
+ );
1448
+ }
1449
+ }
1450
+ return true;
1451
+ }
1452
+ };
1453
+
1454
+ // src/core/parser/urpf-parser.ts
1455
+ var URPFParser = class _URPFParser {
1456
+ /**
1457
+ * 解析 URPF 文档
1458
+ * @param content URPF 文档内容
1459
+ * @param options 解析选项
1460
+ * @returns 解析结果
1461
+ * @throws {URPFParseError} 如果解析失败
1462
+ */
1463
+ static parse(content, options = {}) {
1464
+ const warnings = [];
1465
+ const strict = options.strict !== false;
1466
+ try {
1467
+ const boundaries = BoundaryParser.findBoundaries(content);
1468
+ if (strict) {
1469
+ BoundaryParser.validateBoundaries(boundaries);
1470
+ } else {
1471
+ try {
1472
+ BoundaryParser.validateBoundaries(boundaries);
1473
+ } catch (error) {
1474
+ if (error instanceof URPFParseError) {
1475
+ warnings.push(error.message);
1476
+ }
1477
+ }
1478
+ }
1479
+ if (boundaries.length < 2) {
1480
+ throw new URPFParseError("URPF \u6587\u6863\u5FC5\u987B\u5305\u542B\u81F3\u5C11\u5F00\u59CB\u8FB9\u754C\u548C\u7ED3\u675F\u8FB9\u754C");
1481
+ }
1482
+ const boundaryToken = boundaries[0].token;
1483
+ const sections = BoundaryParser.splitByBoundaries(content, boundaries);
1484
+ if (sections.length === 0) {
1485
+ throw new URPFParseError("URPF \u6587\u6863\u4E0D\u5305\u542B\u4EFB\u4F55\u90E8\u5206");
1486
+ }
1487
+ const document = {
1488
+ boundaryToken,
1489
+ resourceSections: []
1490
+ };
1491
+ let startSectionIndex = 0;
1492
+ if (sections.length > 0 && PropertyParser.isPropertySection(sections[0])) {
1493
+ try {
1494
+ document.propertySection = PropertyParser.parsePropertySection(sections[0], boundaries[0].line + 1);
1495
+ const variableContext = VariableReplacer.createContext(document.propertySection.properties);
1496
+ for (let i = 1; i < sections.length; i++) {
1497
+ try {
1498
+ const resourceSection = ResourceParser.parseResourceSection(sections[i], boundaries[i].line + 1);
1499
+ const udrsRefString = ResourceParser.generateUDRSReferenceString(resourceSection.header.udrsReference);
1500
+ const replacedRefString = VariableReplacer.replace(udrsRefString, variableContext);
1501
+ resourceSection.header.udrsReference = ResourceParser.parseUDRSReference(replacedRefString, boundaries[i].line + 1);
1502
+ document.resourceSections.push(resourceSection);
1503
+ } catch (error) {
1504
+ if (strict) {
1505
+ throw error;
1506
+ } else if (error instanceof Error) {
1507
+ warnings.push(`\u7B2C ${boundaries[i].line + 1} \u884C: ${error.message}`);
1508
+ }
1509
+ }
1510
+ }
1511
+ startSectionIndex = 1;
1512
+ } catch (error) {
1513
+ if (strict) {
1514
+ throw error;
1515
+ } else if (error instanceof Error) {
1516
+ warnings.push(`\u7B2C ${boundaries[0].line + 1} \u884C: ${error.message}`);
1517
+ }
1518
+ }
1519
+ }
1520
+ if (startSectionIndex === 0) {
1521
+ for (let i = startSectionIndex; i < sections.length; i++) {
1522
+ try {
1523
+ const resourceSection = ResourceParser.parseResourceSection(sections[i], boundaries[i].line + 1);
1524
+ document.resourceSections.push(resourceSection);
1525
+ } catch (error) {
1526
+ if (strict) {
1527
+ throw error;
1528
+ } else if (error instanceof Error) {
1529
+ warnings.push(`\u7B2C ${boundaries[i].line + 1} \u884C: ${error.message}`);
1530
+ }
1531
+ }
1532
+ }
1533
+ }
1534
+ if (document.resourceSections.length === 0) {
1535
+ warnings.push("URPF \u6587\u6863\u4E0D\u5305\u542B\u4EFB\u4F55\u8D44\u6E90\u90E8\u5206");
1536
+ }
1537
+ return {
1538
+ document,
1539
+ warnings
1540
+ };
1541
+ } catch (error) {
1542
+ if (error instanceof URPFParseError) {
1543
+ throw error;
1544
+ }
1545
+ throw new URPFParseError(`\u89E3\u6790\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
1546
+ }
1547
+ }
1548
+ /**
1549
+ * 解析 URPF 文档(从文件)
1550
+ * @param filePath 文件路径
1551
+ * @param options 解析选项
1552
+ * @returns 解析结果
1553
+ * @throws {URPFParseError} 如果解析失败
1554
+ */
1555
+ static async parseFile(filePath, options = {}) {
1556
+ const fs7 = await import("fs/promises");
1557
+ const content = await fs7.readFile(filePath, "utf-8");
1558
+ return _URPFParser.parse(content, options);
1559
+ }
1560
+ /**
1561
+ * 生成 URPF 文档
1562
+ * @param document URPF 文档
1563
+ * @returns URPF 文档字符串
1564
+ */
1565
+ static generate(document) {
1566
+ const lines = [];
1567
+ lines.push(BoundaryParser.generateBoundaryLine(document.boundaryToken, "start" /* START */));
1568
+ if (document.propertySection) {
1569
+ lines.push(PropertyParser.generatePropertySectionContent(document.propertySection));
1570
+ lines.push(BoundaryParser.generateBoundaryLine(document.boundaryToken, "delimiter" /* DELIMITER */));
1571
+ }
1572
+ document.resourceSections.forEach((resourceSection, index) => {
1573
+ lines.push(ResourceParser.generateResourceSectionContent(resourceSection));
1574
+ if (index < document.resourceSections.length - 1) {
1575
+ lines.push(BoundaryParser.generateBoundaryLine(document.boundaryToken, "delimiter" /* DELIMITER */));
1576
+ }
1577
+ });
1578
+ lines.push(BoundaryParser.generateBoundaryLine(document.boundaryToken, "end" /* END */));
1579
+ return lines.join("\n");
1580
+ }
1581
+ /**
1582
+ * 验证 URPF 文档
1583
+ * @param document URPF 文档
1584
+ * @returns 验证结果
1585
+ */
1586
+ static validate(document) {
1587
+ const errors = [];
1588
+ if (!BoundaryParser.isValidToken(document.boundaryToken)) {
1589
+ errors.push(`\u65E0\u6548\u7684\u8FB9\u754C\u4EE4\u724C: ${document.boundaryToken}`);
1590
+ }
1591
+ if (document.propertySection) {
1592
+ const propertyNames = /* @__PURE__ */ new Set();
1593
+ for (const property of document.propertySection.properties) {
1594
+ if (propertyNames.has(property.name)) {
1595
+ errors.push(`\u91CD\u590D\u7684\u53D8\u91CF\u540D: ${property.name}`);
1596
+ }
1597
+ propertyNames.add(property.name);
1598
+ }
1599
+ }
1600
+ if (document.resourceSections.length === 0) {
1601
+ errors.push("URPF \u6587\u6863\u5FC5\u987B\u5305\u542B\u81F3\u5C11\u4E00\u4E2A\u8D44\u6E90\u90E8\u5206");
1602
+ }
1603
+ return {
1604
+ valid: errors.length === 0,
1605
+ errors
1606
+ };
1607
+ }
1608
+ };
1609
+
1610
+ // src/commands/unpack.ts
1611
+ var UnpackCommand = class {
1612
+ /**
1613
+ * 执行 unpack 命令
1614
+ */
1615
+ async execute(urpfFilePath, options = {}) {
1616
+ const startTime = Date.now();
1617
+ try {
1618
+ const resolvedPath = path3.resolve(urpfFilePath);
1619
+ await this.validateURPFFile(resolvedPath);
1620
+ const outputDir = options.output ? path3.resolve(options.output) : process.cwd();
1621
+ await fs4.mkdir(outputDir, { recursive: true });
1622
+ const parseResult = await URPFParser.parseFile(resolvedPath);
1623
+ const results = [];
1624
+ let totalBytes = 0;
1625
+ for (const resource of parseResult.document.resourceSections) {
1626
+ const result = await this.extractResource(
1627
+ resource,
1628
+ outputDir,
1629
+ options
1630
+ );
1631
+ if (result.success && result.action !== "skipped") {
1632
+ totalBytes += this.getFileSize(resource);
1633
+ }
1634
+ results.push(result);
1635
+ }
1636
+ const duration = Date.now() - startTime;
1637
+ const createdCount = results.filter((r) => r.action === "created").length;
1638
+ const overwrittenCount = results.filter((r) => r.action === "overwritten").length;
1639
+ const skippedCount = results.filter((r) => r.action === "skipped").length;
1640
+ const failedCount = results.filter((r) => r.action === "error").length;
1641
+ if (options.verbose) {
1642
+ this.logVerbose(results, outputDir, duration);
1643
+ }
1644
+ return {
1645
+ success: failedCount === 0,
1646
+ outputDir,
1647
+ files: results,
1648
+ createdCount,
1649
+ overwrittenCount,
1650
+ skippedCount,
1651
+ failedCount,
1652
+ totalBytes,
1653
+ duration
1654
+ };
1655
+ } catch (err) {
1656
+ const duration = Date.now() - startTime;
1657
+ const errorMessage = err instanceof Error ? err.message : String(err);
1658
+ if (options.verbose) {
1659
+ console.error(`\u9519\u8BEF: ${errorMessage}`);
1660
+ }
1661
+ return {
1662
+ success: false,
1663
+ outputDir: options.output ? path3.resolve(options.output) : process.cwd(),
1664
+ files: [],
1665
+ createdCount: 0,
1666
+ overwrittenCount: 0,
1667
+ skippedCount: 0,
1668
+ failedCount: 0,
1669
+ totalBytes: 0,
1670
+ duration,
1671
+ error: errorMessage
1672
+ };
1673
+ }
1674
+ }
1675
+ /**
1676
+ * 验证 URPF 文件
1677
+ */
1678
+ async validateURPFFile(filePath) {
1679
+ try {
1680
+ const stats = await fs4.stat(filePath);
1681
+ if (!stats.isFile()) {
1682
+ throw new Error(`\u8DEF\u5F84\u4E0D\u662F\u6587\u4EF6: ${filePath}`);
1683
+ }
1684
+ } catch (error) {
1685
+ if (error.code === "ENOENT") {
1686
+ throw new Error(`\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}`);
1687
+ }
1688
+ throw error;
1689
+ }
1690
+ }
1691
+ /**
1692
+ * 提取资源到文件系统
1693
+ */
1694
+ async extractResource(resource, outputDir, options) {
1695
+ try {
1696
+ const filePath = this.extractFilePath(resource.header.udrsReference.uri);
1697
+ const fullPath = path3.join(outputDir, filePath);
1698
+ const parentDir = path3.dirname(fullPath);
1699
+ await fs4.mkdir(parentDir, { recursive: true });
1700
+ const fileExists = await this.fileExists(fullPath);
1701
+ if (fileExists) {
1702
+ if (options.force) {
1703
+ await this.writeFile(resource, fullPath, options);
1704
+ return {
1705
+ path: filePath,
1706
+ success: true,
1707
+ action: "overwritten"
1708
+ };
1709
+ } else if (options.forceWhenNewer) {
1710
+ const sourceMtime = this.extractMtime(resource);
1711
+ const targetMtime = await this.getFileMtime(fullPath);
1712
+ if (sourceMtime > targetMtime) {
1713
+ await this.writeFile(resource, fullPath, options);
1714
+ return {
1715
+ path: filePath,
1716
+ success: true,
1717
+ action: "overwritten"
1718
+ };
1719
+ } else {
1720
+ if (options.verbose) {
1721
+ console.log(`\u8DF3\u8FC7\uFF08\u76EE\u6807\u6587\u4EF6\u66F4\u65B0\uFF09: ${filePath}`);
1722
+ }
1723
+ return {
1724
+ path: filePath,
1725
+ success: true,
1726
+ action: "skipped"
1727
+ };
1728
+ }
1729
+ } else {
1730
+ throw new Error(`\u6587\u4EF6\u5DF2\u5B58\u5728: ${filePath}\u3002\u8BF7\u4F7F\u7528 --force \u9009\u9879\u5F3A\u5236\u8986\u76D6\uFF0C\u6216\u4F7F\u7528 --force-when-newer \u9009\u9879\u4EC5\u8986\u76D6\u66F4\u65B0\u7684\u6587\u4EF6\u3002`);
1731
+ }
1732
+ } else {
1733
+ await this.writeFile(resource, fullPath, options);
1734
+ return {
1735
+ path: filePath,
1736
+ success: true,
1737
+ action: "created"
1738
+ };
1739
+ }
1740
+ } catch (error) {
1741
+ const errorMessage = error instanceof Error ? error.message : String(error);
1742
+ return {
1743
+ path: this.extractFilePath(resource.header.udrsReference.uri),
1744
+ success: false,
1745
+ action: "error",
1746
+ error: errorMessage
1747
+ };
1748
+ }
1749
+ }
1750
+ /**
1751
+ * 从 UDRS 引用中提取文件路径
1752
+ */
1753
+ extractFilePath(uri) {
1754
+ let filePath = uri.replace(/^file:\/\//, "");
1755
+ if (filePath.startsWith("/")) {
1756
+ filePath = filePath.substring(1);
1757
+ }
1758
+ if (/^[A-Za-z]:/.test(filePath)) {
1759
+ filePath = filePath;
1760
+ }
1761
+ return filePath;
1762
+ }
1763
+ /**
1764
+ * 检查文件是否存在
1765
+ */
1766
+ async fileExists(filePath) {
1767
+ try {
1768
+ await fs4.access(filePath);
1769
+ return true;
1770
+ } catch {
1771
+ return false;
1772
+ }
1773
+ }
1774
+ /**
1775
+ * 获取文件修改时间
1776
+ */
1777
+ async getFileMtime(filePath) {
1778
+ const stats = await fs4.stat(filePath);
1779
+ return stats.mtime;
1780
+ }
1781
+ /**
1782
+ * 从资源中提取修改时间
1783
+ * 注意:URPF 规范中不直接存储 mtime,这里我们使用一个合理的时间戳
1784
+ * 在实际应用中,可能需要从 URPF 文件的属性部分读取
1785
+ */
1786
+ extractMtime(resource) {
1787
+ return new Date(Date.now() - 36e5);
1788
+ }
1789
+ /**
1790
+ * 获取文件大小
1791
+ */
1792
+ getFileSize(resource) {
1793
+ if (Buffer.isBuffer(resource.content)) {
1794
+ return resource.content.length;
1795
+ }
1796
+ return Buffer.byteLength(resource.content, "utf-8");
1797
+ }
1798
+ /**
1799
+ * 写入文件
1800
+ */
1801
+ async writeFile(resource, filePath, options) {
1802
+ let content;
1803
+ if (Buffer.isBuffer(resource.content)) {
1804
+ content = resource.content;
1805
+ } else {
1806
+ content = resource.content;
1807
+ }
1808
+ if (Buffer.isBuffer(content)) {
1809
+ await fs4.writeFile(filePath, content);
1810
+ } else {
1811
+ await fs4.writeFile(filePath, content, {
1812
+ encoding: resource.header.encoding
1813
+ });
1814
+ }
1815
+ if (resource.header.permissions) {
1816
+ const permissions = parseInt(resource.header.permissions, 8);
1817
+ await fs4.chmod(filePath, permissions);
1818
+ }
1819
+ if (options.preserveMtime) {
1820
+ const mtime = this.extractMtime(resource);
1821
+ await fs4.utimes(filePath, mtime, mtime);
1822
+ }
1823
+ if (options.verbose) {
1824
+ console.log(`\u5DF2${await this.fileExists(filePath) ? "\u8986\u76D6" : "\u521B\u5EFA"}: ${filePath}`);
1825
+ }
1826
+ }
1827
+ /**
1828
+ * 输出详细日志
1829
+ */
1830
+ logVerbose(results, outputDir, duration) {
1831
+ console.log("URPF \u89E3\u5305\u5B8C\u6210:");
1832
+ console.log(` \u8F93\u51FA\u76EE\u5F55: ${outputDir}`);
1833
+ console.log(` \u6587\u4EF6\u603B\u6570: ${results.length}`);
1834
+ const createdCount = results.filter((r) => r.action === "created").length;
1835
+ const overwrittenCount = results.filter((r) => r.action === "overwritten").length;
1836
+ const skippedCount = results.filter((r) => r.action === "skipped").length;
1837
+ const failedCount = results.filter((r) => r.action === "error").length;
1838
+ console.log(` \u521B\u5EFA: ${createdCount}`);
1839
+ console.log(` \u8986\u76D6: ${overwrittenCount}`);
1840
+ console.log(` \u8DF3\u8FC7: ${skippedCount}`);
1841
+ console.log(` \u5931\u8D25: ${failedCount}`);
1842
+ console.log(` \u6267\u884C\u8017\u65F6: ${duration}ms`);
1843
+ if (failedCount > 0) {
1844
+ console.log("\n\u5931\u8D25\u7684\u6587\u4EF6:");
1845
+ for (const result of results) {
1846
+ if (result.action === "error") {
1847
+ console.log(` - ${result.path}: ${result.error}`);
1848
+ }
1849
+ }
1850
+ }
1851
+ }
1852
+ };
1853
+
1854
+ // src/commands/pack-add.ts
1855
+ import path4 from "path";
1856
+ import fs5 from "fs/promises";
1857
+ var PackAddCommand = class {
1858
+ /**
1859
+ * 执行 pack-add 命令
1860
+ */
1861
+ async execute(urpfFilePath, targetPath, options = {}) {
1862
+ const startTime = Date.now();
1863
+ try {
1864
+ const resolvedUrpfPath = path4.resolve(urpfFilePath);
1865
+ await this.validatePath(resolvedUrpfPath);
1866
+ const resolvedTargetPath = path4.resolve(targetPath);
1867
+ await this.validatePath(resolvedTargetPath);
1868
+ const urpfContent = await fs5.readFile(resolvedUrpfPath, "utf-8");
1869
+ const parseResult = URPFParser.parse(urpfContent, { strict: false });
1870
+ const document = parseResult.document;
1871
+ const variableContext = document.propertySection ? VariableReplacer.createContext(document.propertySection.properties) : void 0;
1872
+ const scanResult = await this.scanFiles(resolvedTargetPath, options, variableContext);
1873
+ if (scanResult.files.length === 0) {
1874
+ return {
1875
+ success: true,
1876
+ urpfFilePath: resolvedUrpfPath,
1877
+ addedCount: 0,
1878
+ skippedCount: scanResult.skippedFiles.length,
1879
+ totalBytes: 0,
1880
+ duration: Date.now() - startTime
1881
+ };
1882
+ }
1883
+ const serializer = new ResourceSerializer();
1884
+ const filesToAdd = scanResult.files.map((file) => {
1885
+ const isAbsolutePath = path4.isAbsolute(file.path);
1886
+ const filePath = isAbsolutePath ? file.path : path4.join(resolvedTargetPath, file.path);
1887
+ const relativePath = isAbsolutePath ? path4.relative(resolvedTargetPath, file.path) : file.path;
1888
+ return {
1889
+ filePath,
1890
+ metadata: file,
1891
+ udrsReference: `file://${relativePath}`
1892
+ };
1893
+ });
1894
+ const newResources = await serializer.serializeFiles(filesToAdd);
1895
+ const generatorDoc = {
1896
+ boundaryToken: document.boundaryToken,
1897
+ properties: document.propertySection?.properties.map((p) => ({
1898
+ name: p.name,
1899
+ value: p.value,
1900
+ isExtended: p.isExtended
1901
+ })),
1902
+ resources: document.resourceSections.map((rs) => ({
1903
+ header: {
1904
+ udrsReference: rs.header.udrsReference.protocol + "://" + rs.header.udrsReference.uri + (rs.header.udrsReference.fragment ? "#" + rs.header.udrsReference.fragment : ""),
1905
+ encoding: rs.header.encoding,
1906
+ permissions: rs.header.permissions,
1907
+ newlineType: rs.header.newlineType
1908
+ },
1909
+ content: rs.content
1910
+ }))
1911
+ };
1912
+ generatorDoc.resources = [...generatorDoc.resources, ...newResources];
1913
+ const generator = new URPFGenerator({
1914
+ boundaryToken: generatorDoc.boundaryToken,
1915
+ includeBoundaries: true
1916
+ });
1917
+ const result = await generator.generate(generatorDoc);
1918
+ await fs5.writeFile(resolvedUrpfPath, result.content, "utf-8");
1919
+ const duration = Date.now() - startTime;
1920
+ if (options.verbose) {
1921
+ console.log(`Successfully added ${newResources.length} files to URPF package: ${resolvedUrpfPath}`);
1922
+ console.log(` Added files: ${newResources.length}`);
1923
+ console.log(` Skipped files: ${scanResult.skippedFiles.length}`);
1924
+ console.log(` Total bytes: ${scanResult.files.reduce((sum, f) => sum + f.size, 0)}`);
1925
+ console.log(` Duration: ${duration}ms`);
1926
+ }
1927
+ return {
1928
+ success: true,
1929
+ urpfFilePath: resolvedUrpfPath,
1930
+ addedCount: newResources.length,
1931
+ skippedCount: scanResult.skippedFiles.length,
1932
+ totalBytes: scanResult.files.reduce((sum, f) => sum + f.size, 0),
1933
+ duration
1934
+ };
1935
+ } catch (err) {
1936
+ const duration = Date.now() - startTime;
1937
+ const errorMessage = err instanceof Error ? err.message : String(err);
1938
+ if (options.verbose) {
1939
+ console.error(`Error: ${errorMessage}`);
1940
+ }
1941
+ return {
1942
+ success: false,
1943
+ urpfFilePath: path4.resolve(urpfFilePath),
1944
+ addedCount: 0,
1945
+ skippedCount: 0,
1946
+ totalBytes: 0,
1947
+ duration,
1948
+ error: errorMessage
1949
+ };
1950
+ }
1951
+ }
1952
+ /**
1953
+ * 验证路径
1954
+ */
1955
+ async validatePath(filePath) {
1956
+ try {
1957
+ const stats = await fs5.stat(filePath);
1958
+ if (!stats.isFile() && !stats.isDirectory()) {
1959
+ throw new Error(`Path is not a file or directory: ${filePath}`);
1960
+ }
1961
+ } catch {
1962
+ throw new Error(`Cannot access path: ${filePath}`);
1963
+ }
1964
+ }
1965
+ /**
1966
+ * 扫描文件
1967
+ */
1968
+ async scanFiles(rootPath, options, variableContext) {
1969
+ const stats = await fs5.stat(rootPath);
1970
+ if (stats.isFile()) {
1971
+ const metadata = await this.collectFileMetadata(rootPath);
1972
+ return {
1973
+ files: [metadata],
1974
+ directories: [],
1975
+ skippedFiles: [],
1976
+ statistics: {
1977
+ totalFiles: 1,
1978
+ totalDirectories: 0,
1979
+ skippedFiles: 0
1980
+ }
1981
+ };
1982
+ }
1983
+ let ignoreRules = [];
1984
+ if (options.ignoreFile) {
1985
+ const parser = new IgnoreRuleParser();
1986
+ const content = await fs5.readFile(options.ignoreFile, "utf-8");
1987
+ ignoreRules = parser.parseFile(content);
1988
+ } else {
1989
+ const defaultIgnoreFiles = [".gitignore", ".iflowignore"];
1990
+ for (const ignoreFile of defaultIgnoreFiles) {
1991
+ const ignorePath = path4.join(rootPath, ignoreFile);
1992
+ try {
1993
+ await fs5.access(ignorePath);
1994
+ const parser = new IgnoreRuleParser();
1995
+ const content = await fs5.readFile(ignorePath, "utf-8");
1996
+ ignoreRules = [...ignoreRules, ...parser.parseFile(content)];
1997
+ break;
1998
+ } catch {
1999
+ }
2000
+ }
2001
+ }
2002
+ const effectiveMaxDepth = options.noRecurse ? 1 : options.maxDepth;
2003
+ return await FileScanner.scanDirectory(rootPath, {
2004
+ ignoreRules,
2005
+ followSymlinks: options.followSymlinks || false,
2006
+ maxDepth: effectiveMaxDepth
2007
+ });
2008
+ }
2009
+ /**
2010
+ * 收集文件元数据
2011
+ */
2012
+ async collectFileMetadata(filePath) {
2013
+ const stats = await fs5.stat(filePath);
2014
+ return {
2015
+ path: filePath,
2016
+ name: path4.basename(filePath),
2017
+ size: stats.size,
2018
+ permissions: stats.mode,
2019
+ mtime: stats.mtime,
2020
+ encoding: "utf-8",
2021
+ newlineType: "lf"
2022
+ };
2023
+ }
2024
+ };
2025
+
2026
+ // src/commands/pack-remove.ts
2027
+ import path5 from "path";
2028
+ import fs6 from "fs/promises";
2029
+ var PackRemoveCommand = class {
2030
+ /**
2031
+ * 执行 pack-remove 命令
2032
+ */
2033
+ async execute(urpfFilePath, filenamePattern, options = {}) {
2034
+ const startTime = Date.now();
2035
+ try {
2036
+ const resolvedUrpfPath = path5.resolve(urpfFilePath);
2037
+ await this.validateUrpfFile(resolvedUrpfPath);
2038
+ const urpfContent = await fs6.readFile(resolvedUrpfPath, "utf-8");
2039
+ const parseResult = URPFParser.parse(urpfContent, { strict: false });
2040
+ const document = parseResult.document;
2041
+ const variableContext = document.propertySection ? VariableReplacer.createContext(document.propertySection.properties) : void 0;
2042
+ let processedPattern = filenamePattern;
2043
+ if (options.applyVariables !== false && variableContext) {
2044
+ try {
2045
+ processedPattern = VariableReplacer.replace(filenamePattern, variableContext);
2046
+ if (options.verbose) {
2047
+ console.log(`Variable replacement: ${filenamePattern} -> ${processedPattern}`);
2048
+ }
2049
+ } catch (error) {
2050
+ if (error instanceof Error) {
2051
+ throw new Error(`Variable replacement failed: ${error.message}`);
2052
+ }
2053
+ throw error;
2054
+ }
2055
+ }
2056
+ const generatorDoc = {
2057
+ boundaryToken: document.boundaryToken,
2058
+ properties: document.propertySection?.properties.map((p) => ({
2059
+ name: p.name,
2060
+ value: p.value,
2061
+ isExtended: p.isExtended
2062
+ })),
2063
+ resources: document.resourceSections.map((rs) => ({
2064
+ header: {
2065
+ udrsReference: rs.header.udrsReference.protocol + "://" + rs.header.udrsReference.uri + (rs.header.udrsReference.fragment ? "#" + rs.header.udrsReference.fragment : ""),
2066
+ encoding: rs.header.encoding,
2067
+ permissions: rs.header.permissions,
2068
+ newlineType: rs.header.newlineType
2069
+ },
2070
+ content: rs.content
2071
+ }))
2072
+ };
2073
+ const { removedResources, notFoundCount } = this.removeResources(generatorDoc, processedPattern);
2074
+ if (removedResources.length === 0) {
2075
+ return {
2076
+ success: true,
2077
+ urpfFilePath: resolvedUrpfPath,
2078
+ removedCount: 0,
2079
+ notFoundCount,
2080
+ duration: Date.now() - startTime
2081
+ };
2082
+ }
2083
+ const generator = new URPFGenerator({
2084
+ boundaryToken: generatorDoc.boundaryToken,
2085
+ includeBoundaries: true
2086
+ });
2087
+ const result = await generator.generate(generatorDoc);
2088
+ await fs6.writeFile(resolvedUrpfPath, result.content, "utf-8");
2089
+ const duration = Date.now() - startTime;
2090
+ if (options.verbose) {
2091
+ console.log(`Successfully removed ${removedResources.length} files: ${resolvedUrpfPath}`);
2092
+ console.log(` Removed files:`);
2093
+ removedResources.forEach((resource) => {
2094
+ console.log(` - ${resource.header.udrsReference}`);
2095
+ });
2096
+ console.log(` Not found count: ${notFoundCount}`);
2097
+ console.log(` Duration: ${duration}ms`);
2098
+ }
2099
+ return {
2100
+ success: true,
2101
+ urpfFilePath: resolvedUrpfPath,
2102
+ removedCount: removedResources.length,
2103
+ notFoundCount,
2104
+ duration
2105
+ };
2106
+ } catch (err) {
2107
+ const duration = Date.now() - startTime;
2108
+ const errorMessage = err instanceof Error ? err.message : String(err);
2109
+ if (options.verbose) {
2110
+ console.error(`Error: ${errorMessage}`);
2111
+ }
2112
+ return {
2113
+ success: false,
2114
+ urpfFilePath: path5.resolve(urpfFilePath),
2115
+ removedCount: 0,
2116
+ notFoundCount: 0,
2117
+ duration,
2118
+ error: errorMessage
2119
+ };
2120
+ }
2121
+ }
2122
+ /**
2123
+ * 验证 URPF 文件
2124
+ */
2125
+ async validateUrpfFile(filePath) {
2126
+ try {
2127
+ const stats = await fs6.stat(filePath);
2128
+ if (!stats.isFile()) {
2129
+ throw new Error(`Path is not a file: ${filePath}`);
2130
+ }
2131
+ } catch {
2132
+ throw new Error(`Cannot access URPF file: ${filePath}`);
2133
+ }
2134
+ }
2135
+ /**
2136
+ * 从文档中移除匹配的资源
2137
+ */
2138
+ removeResources(document, pattern) {
2139
+ const removedResources = [];
2140
+ let notFoundCount = 0;
2141
+ const normalizedPattern = pattern.startsWith("/") || pattern.startsWith("\\") ? pattern.slice(1) : pattern;
2142
+ const normalizedPatternWithSlash = normalizedPattern.replace(/\\/g, "/");
2143
+ for (let i = document.resources.length - 1; i >= 0; i--) {
2144
+ const resource = document.resources[i];
2145
+ const udrsRefString = resource.header.udrsReference;
2146
+ if (this.matchResource(udrsRefString, normalizedPatternWithSlash)) {
2147
+ removedResources.push(resource);
2148
+ document.resources.splice(i, 1);
2149
+ }
2150
+ }
2151
+ if (removedResources.length === 0) {
2152
+ notFoundCount = 1;
2153
+ }
2154
+ return { removedResources, notFoundCount };
2155
+ }
2156
+ /**
2157
+ * 检查资源是否匹配模式
2158
+ */
2159
+ matchResource(udrsRefString, pattern) {
2160
+ const match = udrsRefString.match(/^file:\/\/(.+)$/);
2161
+ if (!match) {
2162
+ return false;
2163
+ }
2164
+ const resourcePath = match[1];
2165
+ const normalizedResourcePath = resourcePath.replace(/\\/g, "/");
2166
+ if (normalizedResourcePath === pattern) {
2167
+ return true;
2168
+ }
2169
+ const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
2170
+ const regex = new RegExp(`^${regexPattern}$`);
2171
+ return regex.test(normalizedResourcePath);
2172
+ }
2173
+ };
2174
+
2175
+ // src/index.ts
2176
+ var program = new Command();
2177
+ program.name("urpfcli").description("URPF CLI \u5DE5\u5177 - \u57FA\u4E8E URPF \u89C4\u8303\u7684\u547D\u4EE4\u884C\u6253\u5305\u5DE5\u5177").version("0.1.0");
2178
+ program.command("pack").description("\u5C06\u6587\u4EF6\u6216\u76EE\u5F55\u6253\u5305\u6210 URPF \u683C\u5F0F").argument("<file|dir>", "\u8981\u6253\u5305\u7684\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84").option("-o, --output <path>", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84\uFF08\u9ED8\u8BA4\uFF1A\u8F93\u5165\u8DEF\u5F84.urpf\uFF09").option("-v, --verbose", "\u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7").option("-i, --ignore <path>", "\u6307\u5B9A\u5FFD\u7565\u89C4\u5219\u6587\u4EF6\u8DEF\u5F84").option("-f, --follow-symlinks", "\u8DDF\u968F\u7B26\u53F7\u94FE\u63A5").option("-d, --max-depth <number>", "\u6700\u5927\u626B\u63CF\u6DF1\u5EA6\uFF080 \u8868\u793A\u65E0\u9650\u5236\uFF09").option("--no-recurse", "\u4E0D\u9012\u5F52\u626B\u63CF\u5B50\u76EE\u5F55").option("--force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u5B58\u5728\u7684\u8F93\u51FA\u6587\u4EF6").action(async (file, options) => {
2179
+ try {
2180
+ const packCommand = new PackCommand();
2181
+ const result = await packCommand.execute(file, options);
2182
+ if (result.success) {
2183
+ console.log(`\u2705 URPF \u6587\u4EF6\u5DF2\u751F\u6210: ${result.outputPath}`);
2184
+ if (!options.verbose) {
2185
+ console.log(` \u6587\u4EF6\u6570\u91CF: ${result.fileCount}`);
2186
+ console.log(` \u76EE\u5F55\u6570\u91CF: ${result.directoryCount}`);
2187
+ console.log(` \u8DF3\u8FC7\u6587\u4EF6: ${result.skippedCount}`);
2188
+ console.log(` URPF \u5927\u5C0F: ${result.urpfSize} \u5B57\u8282`);
2189
+ console.log(` \u6267\u884C\u8017\u65F6: ${result.duration}ms`);
2190
+ }
2191
+ } else {
2192
+ console.error(`\u274C \u6253\u5305\u5931\u8D25: ${result.error}`);
2193
+ process.exit(1);
2194
+ }
2195
+ } catch (error) {
2196
+ const errorMessage = error instanceof Error ? error.message : String(error);
2197
+ console.error(`\u274C \u9519\u8BEF: ${errorMessage}`);
2198
+ process.exit(1);
2199
+ }
2200
+ });
2201
+ program.command("unpack").description("\u89E3\u5305 URPF \u6587\u4EF6\u5230\u6307\u5B9A\u76EE\u5F55").argument("<file.urpf>", "\u8981\u89E3\u5305\u7684 URPF \u6587\u4EF6\u8DEF\u5F84").option("-o, --output <path>", "\u8F93\u51FA\u76EE\u5F55\u8DEF\u5F84\uFF08\u9ED8\u8BA4\uFF1A\u5F53\u524D\u76EE\u5F55\uFF09").option("-v, --verbose", "\u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7").option("--force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u5B58\u5728\u7684\u6587\u4EF6").option("--force-when-newer", "\u4EC5\u5F53\u6E90\u6587\u4EF6\u6BD4\u76EE\u6807\u6587\u4EF6\u66F4\u65B0\u65F6\u624D\u8986\u76D6").option("--preserve-mtime", "\u4FDD\u7559\u539F\u59CB\u4FEE\u6539\u65F6\u95F4").action(async (file, options) => {
2202
+ try {
2203
+ const unpackCommand = new UnpackCommand();
2204
+ const result = await unpackCommand.execute(file, options);
2205
+ if (result.success) {
2206
+ console.log(`\u2705 URPF \u6587\u4EF6\u5DF2\u89E3\u5305\u5230: ${result.outputDir}`);
2207
+ if (!options.verbose) {
2208
+ console.log(` \u521B\u5EFA: ${result.createdCount}`);
2209
+ console.log(` \u8986\u76D6: ${result.overwrittenCount}`);
2210
+ console.log(` \u8DF3\u8FC7: ${result.skippedCount}`);
2211
+ console.log(` \u603B\u5B57\u8282: ${result.totalBytes}`);
2212
+ console.log(` \u6267\u884C\u8017\u65F6: ${result.duration}ms`);
2213
+ }
2214
+ } else {
2215
+ console.error(`\u274C \u89E3\u5305\u5931\u8D25: ${result.error}`);
2216
+ process.exit(1);
2217
+ }
2218
+ } catch (error) {
2219
+ const errorMessage = error instanceof Error ? error.message : String(error);
2220
+ console.error(`\u274C \u9519\u8BEF: ${errorMessage}`);
2221
+ process.exit(1);
2222
+ }
2223
+ });
2224
+ program.command("pack-add").description("\u5411\u5DF2\u6709 URPF \u5305\u6DFB\u52A0\u6587\u4EF6\u6216\u76EE\u5F55").argument("<file.urpf>", "URPF \u6587\u4EF6\u8DEF\u5F84").argument("<file|dir>", "\u8981\u6DFB\u52A0\u7684\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84").option("-v, --verbose", "\u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7").option("-i, --ignore <path>", "\u6307\u5B9A\u5FFD\u7565\u89C4\u5219\u6587\u4EF6\u8DEF\u5F84").option("-f, --follow-symlinks", "\u8DDF\u968F\u7B26\u53F7\u94FE\u63A5").option("-d, --max-depth <number>", "\u6700\u5927\u626B\u63CF\u6DF1\u5EA6\uFF080 \u8868\u793A\u65E0\u9650\u5236\uFF09").option("--no-recurse", "\u4E0D\u9012\u5F52\u626B\u63CF\u5B50\u76EE\u5F55").action(async (urpfFile, target, options) => {
2225
+ try {
2226
+ const packAddCommand = new PackAddCommand();
2227
+ const result = await packAddCommand.execute(urpfFile, target, options);
2228
+ if (result.success) {
2229
+ console.log(`\u2705 \u5DF2\u6210\u529F\u6DFB\u52A0\u6587\u4EF6\u5230 URPF \u5305: ${result.urpfFilePath}`);
2230
+ if (!options.verbose) {
2231
+ console.log(` \u6DFB\u52A0\u6587\u4EF6: ${result.addedCount}`);
2232
+ console.log(` \u8DF3\u8FC7\u6587\u4EF6: ${result.skippedCount}`);
2233
+ console.log(` \u603B\u5B57\u8282: ${result.totalBytes}`);
2234
+ console.log(` \u6267\u884C\u8017\u65F6: ${result.duration}ms`);
2235
+ }
2236
+ } else {
2237
+ console.error(`\u274C \u6DFB\u52A0\u5931\u8D25: ${result.error}`);
2238
+ process.exit(1);
2239
+ }
2240
+ } catch (error) {
2241
+ const errorMessage = error instanceof Error ? error.message : String(error);
2242
+ console.error(`\u274C \u9519\u8BEF: ${errorMessage}`);
2243
+ process.exit(1);
2244
+ }
2245
+ });
2246
+ program.command("pack-remove").description("\u4ECE URPF \u5305\u4E2D\u79FB\u9664\u6587\u4EF6\uFF08\u652F\u6301\u53D8\u91CF\u66FF\u6362\uFF09").argument("<file.urpf>", "URPF \u6587\u4EF6\u8DEF\u5F84").argument("<filename>", "\u8981\u79FB\u9664\u7684\u6587\u4EF6\u540D\u6216\u6A21\u5F0F\uFF08\u652F\u6301\u901A\u914D\u7B26 * \u548C ?\uFF09").option("-v, --verbose", "\u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7").option("--no-apply-variables", "\u4E0D\u5E94\u7528\u53D8\u91CF\u66FF\u6362").action(async (urpfFile, filename, options) => {
2247
+ try {
2248
+ const packRemoveCommand = new PackRemoveCommand();
2249
+ const result = await packRemoveCommand.execute(urpfFile, filename, options);
2250
+ if (result.success) {
2251
+ console.log(`\u2705 \u5DF2\u6210\u529F\u4ECE URPF \u5305\u79FB\u9664\u6587\u4EF6: ${result.urpfFilePath}`);
2252
+ if (!options.verbose) {
2253
+ console.log(` \u79FB\u9664\u6587\u4EF6: ${result.removedCount}`);
2254
+ console.log(` \u672A\u627E\u5230: ${result.notFoundCount}`);
2255
+ console.log(` \u6267\u884C\u8017\u65F6: ${result.duration}ms`);
2256
+ }
2257
+ } else {
2258
+ console.error(`\u274C \u79FB\u9664\u5931\u8D25: ${result.error}`);
2259
+ process.exit(1);
2260
+ }
2261
+ } catch (error) {
2262
+ const errorMessage = error instanceof Error ? error.message : String(error);
2263
+ console.error(`\u274C \u9519\u8BEF: ${errorMessage}`);
2264
+ process.exit(1);
2265
+ }
2266
+ });
2267
+ program.parse();