mcp-perforce-server 2.0.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +20 -20
  2. package/MCP_CONFIG_EXAMPLES.md +222 -222
  3. package/README.md +130 -515
  4. package/dist/p4/config.d.ts +2 -0
  5. package/dist/p4/config.d.ts.map +1 -1
  6. package/dist/p4/config.js +60 -31
  7. package/dist/p4/config.js.map +1 -1
  8. package/dist/p4/parse.d.ts +56 -28
  9. package/dist/p4/parse.d.ts.map +1 -1
  10. package/dist/p4/parse.js +462 -29
  11. package/dist/p4/parse.js.map +1 -1
  12. package/dist/p4/runner.d.ts +2 -0
  13. package/dist/p4/runner.d.ts.map +1 -1
  14. package/dist/p4/runner.js +266 -35
  15. package/dist/p4/runner.js.map +1 -1
  16. package/dist/p4/security.d.ts +2 -0
  17. package/dist/p4/security.d.ts.map +1 -1
  18. package/dist/p4/security.js +61 -29
  19. package/dist/p4/security.js.map +1 -1
  20. package/dist/performance.d.ts +47 -0
  21. package/dist/performance.d.ts.map +1 -0
  22. package/dist/performance.js +67 -0
  23. package/dist/performance.js.map +1 -0
  24. package/dist/server.d.ts +15 -0
  25. package/dist/server.d.ts.map +1 -1
  26. package/dist/server.js +418 -135
  27. package/dist/server.js.map +1 -1
  28. package/dist/tools/advanced.js +1 -1
  29. package/dist/tools/advanced.js.map +1 -1
  30. package/dist/tools/basic.d.ts +58 -0
  31. package/dist/tools/basic.d.ts.map +1 -1
  32. package/dist/tools/basic.js +492 -60
  33. package/dist/tools/basic.js.map +1 -1
  34. package/dist/tools/changelist.d.ts +1 -1
  35. package/dist/tools/changelist.d.ts.map +1 -1
  36. package/dist/tools/changelist.js +36 -6
  37. package/dist/tools/changelist.js.map +1 -1
  38. package/dist/tools/index.d.ts +1 -1
  39. package/dist/tools/index.d.ts.map +1 -1
  40. package/dist/tools/index.js +8 -1
  41. package/dist/tools/index.js.map +1 -1
  42. package/dist/tools/utils.d.ts.map +1 -1
  43. package/dist/tools/utils.js +24 -3
  44. package/dist/tools/utils.js.map +1 -1
  45. package/package.json +114 -114
package/dist/p4/parse.js CHANGED
@@ -10,6 +10,8 @@ exports.parseChangesOutput = parseChangesOutput;
10
10
  exports.parseFilelogOutput = parseFilelogOutput;
11
11
  exports.parseClientsOutput = parseClientsOutput;
12
12
  exports.parseDiffOutput = parseDiffOutput;
13
+ exports.parseDiff2Output = parseDiff2Output;
14
+ exports.parseDescribeOutput = parseDescribeOutput;
13
15
  exports.parseSyncOutput = parseSyncOutput;
14
16
  exports.parseResolveOutput = parseResolveOutput;
15
17
  exports.parseShelveOutput = parseShelveOutput;
@@ -31,12 +33,21 @@ exports.parseLabelOutput = parseLabelOutput;
31
33
  exports.parseSizesOutput = parseSizesOutput;
32
34
  exports.parseHaveOutput = parseHaveOutput;
33
35
  exports.parseWhereOutput = parseWhereOutput;
36
+ exports.parseFstatOutput = parseFstatOutput;
37
+ exports.parseStreamsOutput = parseStreamsOutput;
38
+ exports.parseStreamOutput = parseStreamOutput;
39
+ exports.parsePrintOutput = parsePrintOutput;
40
+ exports.parseIntegrateOutput = parseIntegrateOutput;
34
41
  /**
35
42
  * Parse p4 -ztag output into structured data
36
43
  */
37
44
  function parseZtagOutput(output) {
38
45
  const results = [];
39
- const lines = output.split('\n');
46
+ // Handle non-string inputs safely
47
+ if (!output || typeof output !== 'string') {
48
+ return results;
49
+ }
50
+ const lines = getNormalizedLines(output);
40
51
  let currentRecord = {};
41
52
  for (const line of lines) {
42
53
  const trimmedLine = line.trim();
@@ -66,7 +77,11 @@ function parseZtagOutput(output) {
66
77
  */
67
78
  function parseInfoOutput(output) {
68
79
  const result = {};
69
- const lines = output.split('\n');
80
+ // Handle non-string inputs safely
81
+ if (!output || typeof output !== 'string') {
82
+ return result;
83
+ }
84
+ const lines = getNormalizedLines(output);
70
85
  for (const line of lines) {
71
86
  const trimmedLine = line.trim();
72
87
  if (!trimmedLine)
@@ -87,7 +102,11 @@ function parseInfoOutput(output) {
87
102
  */
88
103
  function parseOpenedOutput(output) {
89
104
  const results = [];
90
- const lines = output.split('\n').filter(line => line.trim());
105
+ // Handle non-string inputs safely
106
+ if (!output || typeof output !== 'string') {
107
+ return results;
108
+ }
109
+ const lines = getNormalizedLines(output, { removeEmpty: true });
91
110
  for (const line of lines) {
92
111
  // Format: "//depot/path#revision - action by user@client (change) type"
93
112
  const match = line.match(/^(.+?)#(\d+)\s+-\s+(\w+)\s+by\s+(.+?)@(.+?)\s+\((.+?)\)(?:\s+(.+))?/);
@@ -118,10 +137,16 @@ function parseOpenedOutput(output) {
118
137
  */
119
138
  function parseChangesOutput(output) {
120
139
  const results = [];
121
- const lines = output.split('\n').filter(line => line.trim());
140
+ // Handle non-string inputs safely
141
+ if (!output || typeof output !== 'string') {
142
+ return results;
143
+ }
144
+ const lines = getNormalizedLines(output, { trim: true, removeEmpty: true });
122
145
  for (const line of lines) {
146
+ // Remove "info: " prefix if present (from -s flag output)
147
+ const cleanLine = line.replace(/^info:\s*/, '');
123
148
  // Format: "Change 12345 on 2023/01/01 by user@client 'Description...'"
124
- const match = line.match(/^Change\s+(\d+)\s+on\s+(\S+)\s+by\s+(.+?)@(.+?)\s+'(.*)'/);
149
+ const match = cleanLine.match(/^Change\s+(\d+)\s+on\s+(\S+)\s+by\s+(.+?)@(.+?)\s+'(.*)'/);
125
150
  if (match) {
126
151
  const [, change, date, user, client, description] = match;
127
152
  results.push({
@@ -140,7 +165,11 @@ function parseChangesOutput(output) {
140
165
  */
141
166
  function parseFilelogOutput(output) {
142
167
  const results = [];
143
- const lines = output.split('\n');
168
+ // Handle non-string inputs safely
169
+ if (!output || typeof output !== 'string') {
170
+ return results;
171
+ }
172
+ const lines = getNormalizedLines(output);
144
173
  let currentFile = null;
145
174
  let currentRevision = null;
146
175
  for (const line of lines) {
@@ -187,7 +216,11 @@ function parseFilelogOutput(output) {
187
216
  */
188
217
  function parseClientsOutput(output) {
189
218
  const results = [];
190
- const lines = output.split('\n').filter(line => line.trim());
219
+ // Handle non-string inputs safely
220
+ if (!output || typeof output !== 'string') {
221
+ return results;
222
+ }
223
+ const lines = getNormalizedLines(output, { removeEmpty: true });
191
224
  for (const line of lines) {
192
225
  // Format: "Client clientname 2023/01/01 root /path/to/root 'Description...'"
193
226
  const match = line.match(/^Client\s+(\S+)\s+(\S+)\s+root\s+(.+?)\s+'(.*)'/);
@@ -207,7 +240,16 @@ function parseClientsOutput(output) {
207
240
  * Parse p4 diff output into structured diff information
208
241
  */
209
242
  function parseDiffOutput(output) {
210
- const lines = output.split('\n');
243
+ // Handle non-string inputs safely
244
+ if (!output || typeof output !== 'string') {
245
+ return {
246
+ files: [],
247
+ totalFiles: 0,
248
+ totalAddedLines: 0,
249
+ totalRemovedLines: 0
250
+ };
251
+ }
252
+ const lines = getNormalizedLines(output);
211
253
  const files = [];
212
254
  let currentFile = null;
213
255
  let diffLines = [];
@@ -259,12 +301,173 @@ function parseDiffOutput(output) {
259
301
  totalRemovedLines: files.reduce((sum, file) => sum + (file.removedLines || 0), 0),
260
302
  };
261
303
  }
304
+ /**
305
+ * Parse p4 diff2 output for depot-to-depot comparisons
306
+ */
307
+ function parseDiff2Output(output, summaryOnly = true) {
308
+ const emptyResult = {
309
+ differences: [],
310
+ totalDifferences: 0,
311
+ summaryOnly,
312
+ };
313
+ if (!output || typeof output !== 'string') {
314
+ return emptyResult;
315
+ }
316
+ const lines = getNormalizedLines(output);
317
+ const differences = [];
318
+ let currentDiff = null;
319
+ let currentDiffLines = [];
320
+ const finalizeCurrentDiff = () => {
321
+ if (!currentDiff)
322
+ return;
323
+ if (!summaryOnly) {
324
+ const diffText = currentDiffLines.join('\n').trim();
325
+ if (diffText) {
326
+ currentDiff.diff = diffText;
327
+ }
328
+ }
329
+ differences.push(currentDiff);
330
+ currentDiff = null;
331
+ currentDiffLines = [];
332
+ };
333
+ for (const line of lines) {
334
+ const trimmed = line.trim();
335
+ if (!trimmed) {
336
+ continue;
337
+ }
338
+ const headerMatch = trimmed.match(/^====\s+(.+?)\s+-\s+(.+?)\s+====(?:\s+(.*))?$/);
339
+ if (headerMatch) {
340
+ finalizeCurrentDiff();
341
+ const [, sourceRaw, targetRaw, differenceTypeRaw] = headerMatch;
342
+ const sourceInfo = parseDiff2Side(sourceRaw);
343
+ const targetInfo = parseDiff2Side(targetRaw);
344
+ currentDiff = {
345
+ sourceFile: sourceInfo.depotFile,
346
+ sourceRevision: sourceInfo.revision,
347
+ sourceType: sourceInfo.type,
348
+ targetFile: targetInfo.depotFile,
349
+ targetRevision: targetInfo.revision,
350
+ targetType: targetInfo.type,
351
+ differenceType: differenceTypeRaw ? differenceTypeRaw.trim() : 'content',
352
+ };
353
+ continue;
354
+ }
355
+ if (summaryOnly) {
356
+ const pairMatch = trimmed.match(/^(.+?#\S+(?:\s+\(.+?\))?)\s+-\s+(.+?#\S+(?:\s+\(.+?\))?)(?:\s+(.*))?$/);
357
+ if (pairMatch) {
358
+ finalizeCurrentDiff();
359
+ const [, sourceRaw, targetRaw, differenceTypeRaw] = pairMatch;
360
+ const sourceInfo = parseDiff2Side(sourceRaw);
361
+ const targetInfo = parseDiff2Side(targetRaw);
362
+ differences.push({
363
+ sourceFile: sourceInfo.depotFile,
364
+ sourceRevision: sourceInfo.revision,
365
+ sourceType: sourceInfo.type,
366
+ targetFile: targetInfo.depotFile,
367
+ targetRevision: targetInfo.revision,
368
+ targetType: targetInfo.type,
369
+ differenceType: differenceTypeRaw ? differenceTypeRaw.trim() : 'different',
370
+ });
371
+ }
372
+ continue;
373
+ }
374
+ if (!summaryOnly && currentDiff) {
375
+ currentDiffLines.push(line);
376
+ }
377
+ }
378
+ finalizeCurrentDiff();
379
+ return {
380
+ differences: differences,
381
+ totalDifferences: differences.length,
382
+ summaryOnly,
383
+ };
384
+ }
385
+ /**
386
+ * Parse p4 describe -s output into structured changelist data
387
+ */
388
+ function parseDescribeOutput(output) {
389
+ const result = {
390
+ files: [],
391
+ description: '',
392
+ rawText: '',
393
+ };
394
+ if (!output || typeof output !== 'string') {
395
+ return result;
396
+ }
397
+ const lines = getNormalizedLines(output);
398
+ result.rawText = lines
399
+ .filter((line) => line.trim())
400
+ .join('\n');
401
+ let inDescription = false;
402
+ let inFiles = false;
403
+ const descriptionLines = [];
404
+ const files = [];
405
+ for (const line of lines) {
406
+ const trimmed = line.trim();
407
+ if (!trimmed) {
408
+ if (inDescription && !inFiles && descriptionLines.length > 0 && descriptionLines[descriptionLines.length - 1] !== '') {
409
+ descriptionLines.push('');
410
+ }
411
+ continue;
412
+ }
413
+ const headerMatch = trimmed.match(/^Change\s+(\d+)\s+by\s+(.+?)@(\S+)\s+on\s+(.+?)(?:\s+\*(\w+)\*)?$/);
414
+ if (headerMatch) {
415
+ const [, change, user, client, date, status] = headerMatch;
416
+ result.change = parseInt(change, 10);
417
+ result.user = user;
418
+ result.client = client;
419
+ result.date = date.trim();
420
+ result.status = status ? status.toLowerCase() : 'submitted';
421
+ inDescription = true;
422
+ inFiles = false;
423
+ continue;
424
+ }
425
+ if (/^Affected files\s+\.\.\.$/i.test(trimmed)) {
426
+ inDescription = false;
427
+ inFiles = true;
428
+ continue;
429
+ }
430
+ if (/^Differences\s+\.\.\.$/i.test(trimmed)) {
431
+ inDescription = false;
432
+ inFiles = false;
433
+ continue;
434
+ }
435
+ if (inFiles && trimmed.startsWith('... ')) {
436
+ const fileMatch = trimmed.match(/^\.{3}\s+(.+?)#([^\s]+)\s+([^\s]+)(?:\s+\((.+?)\))?$/);
437
+ if (fileMatch) {
438
+ const [, depotFile, revisionRaw, action, type] = fileMatch;
439
+ const numericRevision = parseInt(revisionRaw, 10);
440
+ files.push({
441
+ depotFile,
442
+ revision: Number.isNaN(numericRevision) ? revisionRaw : numericRevision,
443
+ action,
444
+ type: type || undefined,
445
+ });
446
+ }
447
+ continue;
448
+ }
449
+ if (inDescription) {
450
+ descriptionLines.push(trimmed);
451
+ }
452
+ }
453
+ while (descriptionLines.length > 0 && descriptionLines[descriptionLines.length - 1] === '') {
454
+ descriptionLines.pop();
455
+ }
456
+ result.description = descriptionLines.join('\n');
457
+ result.files = files;
458
+ result.fileCount = files.length;
459
+ return result;
460
+ }
262
461
  /**
263
462
  * Parse p4 sync output into sync records
264
463
  */
265
464
  function parseSyncOutput(output) {
266
465
  const results = [];
267
- const lines = output.split('\n').filter(line => line.trim());
466
+ // Handle non-string inputs safely
467
+ if (!output || typeof output !== 'string') {
468
+ return results;
469
+ }
470
+ const lines = getNormalizedLines(output, { removeEmpty: true });
268
471
  for (const line of lines) {
269
472
  // Format: "//depot/path#revision - updating /local/path"
270
473
  // Format: "//depot/path#revision - added as /local/path"
@@ -287,7 +490,11 @@ function parseSyncOutput(output) {
287
490
  */
288
491
  function parseResolveOutput(output) {
289
492
  const results = [];
290
- const lines = output.split('\n').filter(line => line.trim());
493
+ // Handle non-string inputs safely
494
+ if (!output || typeof output !== 'string') {
495
+ return results;
496
+ }
497
+ const lines = getNormalizedLines(output, { removeEmpty: true });
291
498
  for (const line of lines) {
292
499
  // Format: "C /path/to/file - merging //depot/path#123"
293
500
  const match = line.match(/^([A-Z])\s+(.+?)\s+-\s+(.+)/);
@@ -308,7 +515,11 @@ function parseResolveOutput(output) {
308
515
  */
309
516
  function parseShelveOutput(output) {
310
517
  const results = [];
311
- const lines = output.split('\n').filter(line => line.trim());
518
+ // Handle non-string inputs safely
519
+ if (!output || typeof output !== 'string') {
520
+ return results;
521
+ }
522
+ const lines = getNormalizedLines(output, { removeEmpty: true });
312
523
  for (const line of lines) {
313
524
  // Format: "change 12345 shelved"
314
525
  const match = line.match(/^change\s+(\d+)\s+shelved/);
@@ -327,7 +538,11 @@ function parseShelveOutput(output) {
327
538
  */
328
539
  function parseUnshelveOutput(output) {
329
540
  const results = [];
330
- const lines = output.split('\n').filter(line => line.trim());
541
+ // Handle non-string inputs safely
542
+ if (!output || typeof output !== 'string') {
543
+ return results;
544
+ }
545
+ const lines = getNormalizedLines(output, { removeEmpty: true });
331
546
  for (const line of lines) {
332
547
  // Format: "//depot/path#123 - unshelved"
333
548
  const match = line.match(/^(.+?)#(\d+)\s+-\s+unshelved/);
@@ -347,7 +562,11 @@ function parseUnshelveOutput(output) {
347
562
  */
348
563
  function parseBlameOutput(output) {
349
564
  const results = [];
350
- const lines = output.split('\n').filter(line => line.trim());
565
+ // Handle non-string inputs safely
566
+ if (!output || typeof output !== 'string') {
567
+ return results;
568
+ }
569
+ const lines = getNormalizedLines(output, { removeEmpty: true });
351
570
  for (const line of lines) {
352
571
  // Format: "12345: user 2023/01/01 12:34:56: content"
353
572
  const match = line.match(/^(\d+):\s+(.+?)\s+(\d{4}\/\d{2}\/\d{2})\s+(\d{2}:\d{2}:\d{2}):\s*(.*)/);
@@ -369,7 +588,11 @@ function parseBlameOutput(output) {
369
588
  */
370
589
  function parseCopyOutput(output) {
371
590
  const results = [];
372
- const lines = output.split('\n').filter(line => line.trim());
591
+ // Handle non-string inputs safely
592
+ if (!output || typeof output !== 'string') {
593
+ return results;
594
+ }
595
+ const lines = getNormalizedLines(output, { removeEmpty: true });
373
596
  for (const line of lines) {
374
597
  // Format: "//depot/src#123 - copied to //depot/dst#124"
375
598
  const match = line.match(/^(.+?)#(\d+)\s+-\s+copied\s+to\s+(.+?)#(\d+)/);
@@ -391,7 +614,11 @@ function parseCopyOutput(output) {
391
614
  */
392
615
  function parseMoveOutput(output) {
393
616
  const results = [];
394
- const lines = output.split('\n').filter(line => line.trim());
617
+ // Handle non-string inputs safely
618
+ if (!output || typeof output !== 'string') {
619
+ return results;
620
+ }
621
+ const lines = getNormalizedLines(output, { removeEmpty: true });
395
622
  for (const line of lines) {
396
623
  // Format: "//depot/old#123 - moved to //depot/new#124"
397
624
  const match = line.match(/^(.+?)#(\d+)\s+-\s+moved\s+to\s+(.+?)#(\d+)/);
@@ -413,7 +640,11 @@ function parseMoveOutput(output) {
413
640
  */
414
641
  function parseGrepOutput(output) {
415
642
  const results = [];
416
- const lines = output.split('\n').filter(line => line.trim());
643
+ // Handle non-string inputs safely
644
+ if (!output || typeof output !== 'string') {
645
+ return results;
646
+ }
647
+ const lines = getNormalizedLines(output, { removeEmpty: true });
417
648
  for (const line of lines) {
418
649
  // Format: "//depot/path:123:matched text here"
419
650
  const match = line.match(/^(.+?):(\d+):(.*)/);
@@ -433,7 +664,11 @@ function parseGrepOutput(output) {
433
664
  */
434
665
  function parseFilesOutput(output) {
435
666
  const results = [];
436
- const lines = output.split('\n').filter(line => line.trim());
667
+ // Handle non-string inputs safely
668
+ if (!output || typeof output !== 'string') {
669
+ return results;
670
+ }
671
+ const lines = getNormalizedLines(output, { removeEmpty: true });
437
672
  for (const line of lines) {
438
673
  // Format: "//depot/path#123 - action change 456 (type text)"
439
674
  const match = line.match(/^(.+?)#(\d+)\s+-\s+(\w+)\s+change\s+(\d+)\s+\((.+?)\)/);
@@ -455,7 +690,11 @@ function parseFilesOutput(output) {
455
690
  */
456
691
  function parseDirsOutput(output) {
457
692
  const results = [];
458
- const lines = output.split('\n').filter(line => line.trim());
693
+ // Handle non-string inputs safely
694
+ if (!output || typeof output !== 'string') {
695
+ return results;
696
+ }
697
+ const lines = getNormalizedLines(output, { removeEmpty: true });
459
698
  for (const line of lines) {
460
699
  // Directories are listed one per line
461
700
  if (line.startsWith('//')) {
@@ -471,7 +710,11 @@ function parseDirsOutput(output) {
471
710
  */
472
711
  function parseUsersOutput(output) {
473
712
  const results = [];
474
- const lines = output.split('\n').filter(line => line.trim());
713
+ // Handle non-string inputs safely
714
+ if (!output || typeof output !== 'string') {
715
+ return results;
716
+ }
717
+ const lines = getNormalizedLines(output, { removeEmpty: true });
475
718
  for (const line of lines) {
476
719
  // Format: "user <user@domain> (realname) accessed 2023/01/01 12:34:56"
477
720
  const match = line.match(/^(\w+)\s+<([^>]+)>\s+\(([^)]+)\)\s+accessed\s+(\d{4}\/\d{2}\/\d{2})\s+(\d{2}:\d{2}:\d{2})/);
@@ -492,7 +735,11 @@ function parseUsersOutput(output) {
492
735
  */
493
736
  function parseUserOutput(output) {
494
737
  const result = {};
495
- const lines = output.split('\n');
738
+ // Handle non-string inputs safely
739
+ if (!output || typeof output !== 'string') {
740
+ return result;
741
+ }
742
+ const lines = getNormalizedLines(output);
496
743
  for (const line of lines) {
497
744
  const colonIndex = line.indexOf(': ');
498
745
  if (colonIndex > 0) {
@@ -508,7 +755,11 @@ function parseUserOutput(output) {
508
755
  */
509
756
  function parseClientOutput(output) {
510
757
  const result = {};
511
- const lines = output.split('\n');
758
+ // Handle non-string inputs safely
759
+ if (!output || typeof output !== 'string') {
760
+ return result;
761
+ }
762
+ const lines = getNormalizedLines(output);
512
763
  for (const line of lines) {
513
764
  const colonIndex = line.indexOf(': ');
514
765
  if (colonIndex > 0) {
@@ -524,7 +775,11 @@ function parseClientOutput(output) {
524
775
  */
525
776
  function parseJobsOutput(output) {
526
777
  const results = [];
527
- const lines = output.split('\n').filter(line => line.trim());
778
+ // Handle non-string inputs safely
779
+ if (!output || typeof output !== 'string') {
780
+ return results;
781
+ }
782
+ const lines = getNormalizedLines(output, { removeEmpty: true });
528
783
  for (const line of lines) {
529
784
  // Format: "job000001 on 2023/01/01 by user *open* 'Job description'"
530
785
  const match = line.match(/^(\w+)\s+on\s+(\d{4}\/\d{2}\/\d{2})\s+by\s+(\w+)\s+(\w+)\s+'(.*)'/);
@@ -546,7 +801,11 @@ function parseJobsOutput(output) {
546
801
  */
547
802
  function parseJobOutput(output) {
548
803
  const result = {};
549
- const lines = output.split('\n');
804
+ // Handle non-string inputs safely
805
+ if (!output || typeof output !== 'string') {
806
+ return result;
807
+ }
808
+ const lines = getNormalizedLines(output);
550
809
  for (const line of lines) {
551
810
  const colonIndex = line.indexOf(': ');
552
811
  if (colonIndex > 0) {
@@ -562,7 +821,11 @@ function parseJobOutput(output) {
562
821
  */
563
822
  function parseFixesOutput(output) {
564
823
  const results = [];
565
- const lines = output.split('\n').filter(line => line.trim());
824
+ // Handle non-string inputs safely
825
+ if (!output || typeof output !== 'string') {
826
+ return results;
827
+ }
828
+ const lines = getNormalizedLines(output, { removeEmpty: true });
566
829
  for (const line of lines) {
567
830
  // Format: "job000001 fixed by change 12345 on 2023/01/01 by user@client"
568
831
  const match = line.match(/^(\w+)\s+fixed\s+by\s+change\s+(\d+)\s+on\s+(\d{4}\/\d{2}\/\d{2})\s+by\s+(.+)$/);
@@ -583,7 +846,11 @@ function parseFixesOutput(output) {
583
846
  */
584
847
  function parseLabelsOutput(output) {
585
848
  const results = [];
586
- const lines = output.split('\n').filter(line => line.trim());
849
+ // Handle non-string inputs safely
850
+ if (!output || typeof output !== 'string') {
851
+ return results;
852
+ }
853
+ const lines = getNormalizedLines(output, { removeEmpty: true });
587
854
  for (const line of lines) {
588
855
  // Format: "Label labelname 2023/01/01 'Label description'"
589
856
  const match = line.match(/^Label\s+(\w+)\s+(\d{4}\/\d{2}\/\d{2})\s+'(.*)'/);
@@ -603,7 +870,11 @@ function parseLabelsOutput(output) {
603
870
  */
604
871
  function parseLabelOutput(output) {
605
872
  const result = {};
606
- const lines = output.split('\n');
873
+ // Handle non-string inputs safely
874
+ if (!output || typeof output !== 'string') {
875
+ return result;
876
+ }
877
+ const lines = getNormalizedLines(output);
607
878
  for (const line of lines) {
608
879
  const colonIndex = line.indexOf(': ');
609
880
  if (colonIndex > 0) {
@@ -618,8 +889,16 @@ function parseLabelOutput(output) {
618
889
  * Parse p4 sizes output into size statistics
619
890
  */
620
891
  function parseSizesOutput(output) {
892
+ // Handle non-string inputs safely
893
+ if (!output || typeof output !== 'string') {
894
+ return {
895
+ totalSize: 0,
896
+ fileCount: 0,
897
+ files: []
898
+ };
899
+ }
621
900
  const result = {};
622
- const lines = output.split('\n').filter(line => line.trim());
901
+ const lines = getNormalizedLines(output, { removeEmpty: true });
623
902
  for (const line of lines) {
624
903
  if (line.includes('files,')) {
625
904
  // Format: "12345 files, 67890123 bytes"
@@ -637,7 +916,11 @@ function parseSizesOutput(output) {
637
916
  */
638
917
  function parseHaveOutput(output) {
639
918
  const results = [];
640
- const lines = output.split('\n').filter(line => line.trim());
919
+ // Handle non-string inputs safely
920
+ if (!output || typeof output !== 'string') {
921
+ return results;
922
+ }
923
+ const lines = getNormalizedLines(output, { removeEmpty: true });
641
924
  for (const line of lines) {
642
925
  // Format: "//depot/path#123 - /local/path"
643
926
  const match = line.match(/^(.+?)#(\d+)\s+-\s+(.+)/);
@@ -657,7 +940,11 @@ function parseHaveOutput(output) {
657
940
  */
658
941
  function parseWhereOutput(output) {
659
942
  const results = [];
660
- const lines = output.split('\n').filter(line => line.trim());
943
+ // Handle non-string inputs safely
944
+ if (!output || typeof output !== 'string') {
945
+ return results;
946
+ }
947
+ const lines = getNormalizedLines(output, { removeEmpty: true });
661
948
  for (const line of lines) {
662
949
  // Format: "//depot/path /local/path //depot/path"
663
950
  const parts = line.split(/\s+/);
@@ -671,6 +958,105 @@ function parseWhereOutput(output) {
671
958
  }
672
959
  return results;
673
960
  }
961
+ /**
962
+ * Parse p4 fstat output into records
963
+ */
964
+ function parseFstatOutput(output) {
965
+ const records = parseZtagOutput(output);
966
+ return records.map((record) => ({ ...record }));
967
+ }
968
+ /**
969
+ * Parse p4 streams output into stream records
970
+ */
971
+ function parseStreamsOutput(output) {
972
+ const results = [];
973
+ if (!output || typeof output !== 'string') {
974
+ return results;
975
+ }
976
+ const lines = getNormalizedLines(output, { removeEmpty: true });
977
+ for (const line of lines) {
978
+ // Format: "Stream //Depot/main 2025/01/01 owner 'description'"
979
+ const match = line.match(/^Stream\s+(\S+)\s+(\S+)\s+(\S+)\s+'(.*)'$/);
980
+ if (match) {
981
+ const [, stream, date, owner, description] = match;
982
+ results.push({
983
+ stream,
984
+ date,
985
+ owner,
986
+ description,
987
+ });
988
+ }
989
+ else if (line.startsWith('//')) {
990
+ results.push({ stream: line.trim() });
991
+ }
992
+ }
993
+ return results;
994
+ }
995
+ /**
996
+ * Parse p4 stream output into a stream spec key/value object
997
+ */
998
+ function parseStreamOutput(output) {
999
+ const result = {};
1000
+ if (!output || typeof output !== 'string') {
1001
+ return result;
1002
+ }
1003
+ const lines = getNormalizedLines(output);
1004
+ for (const line of lines) {
1005
+ const colonIndex = line.indexOf(':');
1006
+ if (colonIndex > 0) {
1007
+ const key = line.substring(0, colonIndex).trim();
1008
+ const value = line.substring(colonIndex + 1).trim();
1009
+ if (key) {
1010
+ result[key] = value;
1011
+ }
1012
+ }
1013
+ }
1014
+ return result;
1015
+ }
1016
+ /**
1017
+ * Parse p4 print output content
1018
+ */
1019
+ function parsePrintOutput(output) {
1020
+ if (!output || typeof output !== 'string') {
1021
+ return { content: '' };
1022
+ }
1023
+ return {
1024
+ content: getNormalizedLines(output).join('\n').trim(),
1025
+ };
1026
+ }
1027
+ /**
1028
+ * Parse p4 integrate/merge output into operation records
1029
+ */
1030
+ function parseIntegrateOutput(output) {
1031
+ const results = [];
1032
+ if (!output || typeof output !== 'string') {
1033
+ return results;
1034
+ }
1035
+ const lines = getNormalizedLines(output, { removeEmpty: true });
1036
+ for (const line of lines) {
1037
+ // Common formats:
1038
+ // //depot/target#5 - integrate from //depot/source#7
1039
+ // //depot/target#none - branch from //depot/source#1
1040
+ const match = line.match(/^(.+?)#([^\s]+)\s+-\s+(.+?)(?:\s+from\s+(.+?)#([^\s]+))?$/);
1041
+ if (match) {
1042
+ const [, targetFile, targetRevisionRaw, action, sourceFile, sourceRevisionRaw] = match;
1043
+ const targetRevisionNum = parseInt(targetRevisionRaw, 10);
1044
+ const sourceRevisionNum = sourceRevisionRaw ? parseInt(sourceRevisionRaw, 10) : NaN;
1045
+ results.push({
1046
+ targetFile,
1047
+ targetRevision: Number.isNaN(targetRevisionNum) ? targetRevisionRaw : targetRevisionNum,
1048
+ action: action.trim(),
1049
+ sourceFile: sourceFile || undefined,
1050
+ sourceRevision: sourceRevisionRaw
1051
+ ? Number.isNaN(sourceRevisionNum)
1052
+ ? sourceRevisionRaw
1053
+ : sourceRevisionNum
1054
+ : undefined,
1055
+ });
1056
+ }
1057
+ }
1058
+ return results;
1059
+ }
674
1060
  /**
675
1061
  * Helper function to convert resolve status to action
676
1062
  */
@@ -683,6 +1069,53 @@ function getResolveAction(status) {
683
1069
  default: return 'unknown';
684
1070
  }
685
1071
  }
1072
+ /**
1073
+ * Strip p4 -s script mode prefixes (info1:, error:, exit:, etc.) from output lines
1074
+ */
1075
+ function stripScriptPrefix(line) {
1076
+ const match = line.match(/^(info\d*|text|warning|error|exit):\s?(.*)$/i);
1077
+ if (!match) {
1078
+ return line;
1079
+ }
1080
+ const [, prefix, value] = match;
1081
+ if (prefix.toLowerCase() === 'exit') {
1082
+ return '';
1083
+ }
1084
+ return value;
1085
+ }
1086
+ function getNormalizedLines(output, options = {}) {
1087
+ if (!output || typeof output !== 'string') {
1088
+ return [];
1089
+ }
1090
+ const { trim = false, removeEmpty = false } = options;
1091
+ const lines = output.split('\n');
1092
+ const normalized = [];
1093
+ for (const rawLine of lines) {
1094
+ const stripped = stripScriptPrefix(rawLine.replace(/\r$/, ''));
1095
+ const value = trim ? stripped.trim() : stripped;
1096
+ if (removeEmpty && value.trim().length === 0) {
1097
+ continue;
1098
+ }
1099
+ normalized.push(value);
1100
+ }
1101
+ return normalized;
1102
+ }
1103
+ /**
1104
+ * Parse a single side of a diff2 header line
1105
+ */
1106
+ function parseDiff2Side(side) {
1107
+ const match = side.trim().match(/^(.+?)#([^\s]+)(?:\s+\((.+?)\))?$/);
1108
+ if (!match) {
1109
+ return { depotFile: side.trim(), revision: 'unknown' };
1110
+ }
1111
+ const [, depotFile, revisionRaw, type] = match;
1112
+ const numericRevision = parseInt(revisionRaw, 10);
1113
+ return {
1114
+ depotFile,
1115
+ revision: Number.isNaN(numericRevision) ? revisionRaw : numericRevision,
1116
+ type: type || undefined,
1117
+ };
1118
+ }
686
1119
  /**
687
1120
  * Parse individual values with type conversion
688
1121
  */