mcp-perforce-server 2.1.0 → 2.1.2

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/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,6 +33,11 @@ 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
  */
@@ -40,7 +47,7 @@ function parseZtagOutput(output) {
40
47
  if (!output || typeof output !== 'string') {
41
48
  return results;
42
49
  }
43
- const lines = output.split('\n');
50
+ const lines = getNormalizedLines(output);
44
51
  let currentRecord = {};
45
52
  for (const line of lines) {
46
53
  const trimmedLine = line.trim();
@@ -74,7 +81,7 @@ function parseInfoOutput(output) {
74
81
  if (!output || typeof output !== 'string') {
75
82
  return result;
76
83
  }
77
- const lines = output.split('\n');
84
+ const lines = getNormalizedLines(output);
78
85
  for (const line of lines) {
79
86
  const trimmedLine = line.trim();
80
87
  if (!trimmedLine)
@@ -99,7 +106,7 @@ function parseOpenedOutput(output) {
99
106
  if (!output || typeof output !== 'string') {
100
107
  return results;
101
108
  }
102
- const lines = output.split('\n').filter(line => line.trim());
109
+ const lines = getNormalizedLines(output, { removeEmpty: true });
103
110
  for (const line of lines) {
104
111
  // Format: "//depot/path#revision - action by user@client (change) type"
105
112
  const match = line.match(/^(.+?)#(\d+)\s+-\s+(\w+)\s+by\s+(.+?)@(.+?)\s+\((.+?)\)(?:\s+(.+))?/);
@@ -134,10 +141,12 @@ function parseChangesOutput(output) {
134
141
  if (!output || typeof output !== 'string') {
135
142
  return results;
136
143
  }
137
- const lines = output.split('\n').filter(line => line.trim());
144
+ const lines = getNormalizedLines(output, { trim: true, removeEmpty: true });
138
145
  for (const line of lines) {
146
+ // Remove "info: " prefix if present (from -s flag output)
147
+ const cleanLine = line.replace(/^info:\s*/, '');
139
148
  // Format: "Change 12345 on 2023/01/01 by user@client 'Description...'"
140
- 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+'(.*)'/);
141
150
  if (match) {
142
151
  const [, change, date, user, client, description] = match;
143
152
  results.push({
@@ -160,7 +169,7 @@ function parseFilelogOutput(output) {
160
169
  if (!output || typeof output !== 'string') {
161
170
  return results;
162
171
  }
163
- const lines = output.split('\n');
172
+ const lines = getNormalizedLines(output);
164
173
  let currentFile = null;
165
174
  let currentRevision = null;
166
175
  for (const line of lines) {
@@ -211,7 +220,7 @@ function parseClientsOutput(output) {
211
220
  if (!output || typeof output !== 'string') {
212
221
  return results;
213
222
  }
214
- const lines = output.split('\n').filter(line => line.trim());
223
+ const lines = getNormalizedLines(output, { removeEmpty: true });
215
224
  for (const line of lines) {
216
225
  // Format: "Client clientname 2023/01/01 root /path/to/root 'Description...'"
217
226
  const match = line.match(/^Client\s+(\S+)\s+(\S+)\s+root\s+(.+?)\s+'(.*)'/);
@@ -240,7 +249,7 @@ function parseDiffOutput(output) {
240
249
  totalRemovedLines: 0
241
250
  };
242
251
  }
243
- const lines = output.split('\n');
252
+ const lines = getNormalizedLines(output);
244
253
  const files = [];
245
254
  let currentFile = null;
246
255
  let diffLines = [];
@@ -292,6 +301,163 @@ function parseDiffOutput(output) {
292
301
  totalRemovedLines: files.reduce((sum, file) => sum + (file.removedLines || 0), 0),
293
302
  };
294
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
+ }
295
461
  /**
296
462
  * Parse p4 sync output into sync records
297
463
  */
@@ -301,7 +467,7 @@ function parseSyncOutput(output) {
301
467
  if (!output || typeof output !== 'string') {
302
468
  return results;
303
469
  }
304
- const lines = output.split('\n').filter(line => line.trim());
470
+ const lines = getNormalizedLines(output, { removeEmpty: true });
305
471
  for (const line of lines) {
306
472
  // Format: "//depot/path#revision - updating /local/path"
307
473
  // Format: "//depot/path#revision - added as /local/path"
@@ -328,7 +494,7 @@ function parseResolveOutput(output) {
328
494
  if (!output || typeof output !== 'string') {
329
495
  return results;
330
496
  }
331
- const lines = output.split('\n').filter(line => line.trim());
497
+ const lines = getNormalizedLines(output, { removeEmpty: true });
332
498
  for (const line of lines) {
333
499
  // Format: "C /path/to/file - merging //depot/path#123"
334
500
  const match = line.match(/^([A-Z])\s+(.+?)\s+-\s+(.+)/);
@@ -353,7 +519,7 @@ function parseShelveOutput(output) {
353
519
  if (!output || typeof output !== 'string') {
354
520
  return results;
355
521
  }
356
- const lines = output.split('\n').filter(line => line.trim());
522
+ const lines = getNormalizedLines(output, { removeEmpty: true });
357
523
  for (const line of lines) {
358
524
  // Format: "change 12345 shelved"
359
525
  const match = line.match(/^change\s+(\d+)\s+shelved/);
@@ -376,7 +542,7 @@ function parseUnshelveOutput(output) {
376
542
  if (!output || typeof output !== 'string') {
377
543
  return results;
378
544
  }
379
- const lines = output.split('\n').filter(line => line.trim());
545
+ const lines = getNormalizedLines(output, { removeEmpty: true });
380
546
  for (const line of lines) {
381
547
  // Format: "//depot/path#123 - unshelved"
382
548
  const match = line.match(/^(.+?)#(\d+)\s+-\s+unshelved/);
@@ -400,7 +566,7 @@ function parseBlameOutput(output) {
400
566
  if (!output || typeof output !== 'string') {
401
567
  return results;
402
568
  }
403
- const lines = output.split('\n').filter(line => line.trim());
569
+ const lines = getNormalizedLines(output, { removeEmpty: true });
404
570
  for (const line of lines) {
405
571
  // Format: "12345: user 2023/01/01 12:34:56: content"
406
572
  const match = line.match(/^(\d+):\s+(.+?)\s+(\d{4}\/\d{2}\/\d{2})\s+(\d{2}:\d{2}:\d{2}):\s*(.*)/);
@@ -426,7 +592,7 @@ function parseCopyOutput(output) {
426
592
  if (!output || typeof output !== 'string') {
427
593
  return results;
428
594
  }
429
- const lines = output.split('\n').filter(line => line.trim());
595
+ const lines = getNormalizedLines(output, { removeEmpty: true });
430
596
  for (const line of lines) {
431
597
  // Format: "//depot/src#123 - copied to //depot/dst#124"
432
598
  const match = line.match(/^(.+?)#(\d+)\s+-\s+copied\s+to\s+(.+?)#(\d+)/);
@@ -452,7 +618,7 @@ function parseMoveOutput(output) {
452
618
  if (!output || typeof output !== 'string') {
453
619
  return results;
454
620
  }
455
- const lines = output.split('\n').filter(line => line.trim());
621
+ const lines = getNormalizedLines(output, { removeEmpty: true });
456
622
  for (const line of lines) {
457
623
  // Format: "//depot/old#123 - moved to //depot/new#124"
458
624
  const match = line.match(/^(.+?)#(\d+)\s+-\s+moved\s+to\s+(.+?)#(\d+)/);
@@ -478,7 +644,7 @@ function parseGrepOutput(output) {
478
644
  if (!output || typeof output !== 'string') {
479
645
  return results;
480
646
  }
481
- const lines = output.split('\n').filter(line => line.trim());
647
+ const lines = getNormalizedLines(output, { removeEmpty: true });
482
648
  for (const line of lines) {
483
649
  // Format: "//depot/path:123:matched text here"
484
650
  const match = line.match(/^(.+?):(\d+):(.*)/);
@@ -502,7 +668,7 @@ function parseFilesOutput(output) {
502
668
  if (!output || typeof output !== 'string') {
503
669
  return results;
504
670
  }
505
- const lines = output.split('\n').filter(line => line.trim());
671
+ const lines = getNormalizedLines(output, { removeEmpty: true });
506
672
  for (const line of lines) {
507
673
  // Format: "//depot/path#123 - action change 456 (type text)"
508
674
  const match = line.match(/^(.+?)#(\d+)\s+-\s+(\w+)\s+change\s+(\d+)\s+\((.+?)\)/);
@@ -528,7 +694,7 @@ function parseDirsOutput(output) {
528
694
  if (!output || typeof output !== 'string') {
529
695
  return results;
530
696
  }
531
- const lines = output.split('\n').filter(line => line.trim());
697
+ const lines = getNormalizedLines(output, { removeEmpty: true });
532
698
  for (const line of lines) {
533
699
  // Directories are listed one per line
534
700
  if (line.startsWith('//')) {
@@ -548,7 +714,7 @@ function parseUsersOutput(output) {
548
714
  if (!output || typeof output !== 'string') {
549
715
  return results;
550
716
  }
551
- const lines = output.split('\n').filter(line => line.trim());
717
+ const lines = getNormalizedLines(output, { removeEmpty: true });
552
718
  for (const line of lines) {
553
719
  // Format: "user <user@domain> (realname) accessed 2023/01/01 12:34:56"
554
720
  const match = line.match(/^(\w+)\s+<([^>]+)>\s+\(([^)]+)\)\s+accessed\s+(\d{4}\/\d{2}\/\d{2})\s+(\d{2}:\d{2}:\d{2})/);
@@ -573,7 +739,7 @@ function parseUserOutput(output) {
573
739
  if (!output || typeof output !== 'string') {
574
740
  return result;
575
741
  }
576
- const lines = output.split('\n');
742
+ const lines = getNormalizedLines(output);
577
743
  for (const line of lines) {
578
744
  const colonIndex = line.indexOf(': ');
579
745
  if (colonIndex > 0) {
@@ -593,7 +759,7 @@ function parseClientOutput(output) {
593
759
  if (!output || typeof output !== 'string') {
594
760
  return result;
595
761
  }
596
- const lines = output.split('\n');
762
+ const lines = getNormalizedLines(output);
597
763
  for (const line of lines) {
598
764
  const colonIndex = line.indexOf(': ');
599
765
  if (colonIndex > 0) {
@@ -613,7 +779,7 @@ function parseJobsOutput(output) {
613
779
  if (!output || typeof output !== 'string') {
614
780
  return results;
615
781
  }
616
- const lines = output.split('\n').filter(line => line.trim());
782
+ const lines = getNormalizedLines(output, { removeEmpty: true });
617
783
  for (const line of lines) {
618
784
  // Format: "job000001 on 2023/01/01 by user *open* 'Job description'"
619
785
  const match = line.match(/^(\w+)\s+on\s+(\d{4}\/\d{2}\/\d{2})\s+by\s+(\w+)\s+(\w+)\s+'(.*)'/);
@@ -639,7 +805,7 @@ function parseJobOutput(output) {
639
805
  if (!output || typeof output !== 'string') {
640
806
  return result;
641
807
  }
642
- const lines = output.split('\n');
808
+ const lines = getNormalizedLines(output);
643
809
  for (const line of lines) {
644
810
  const colonIndex = line.indexOf(': ');
645
811
  if (colonIndex > 0) {
@@ -659,7 +825,7 @@ function parseFixesOutput(output) {
659
825
  if (!output || typeof output !== 'string') {
660
826
  return results;
661
827
  }
662
- const lines = output.split('\n').filter(line => line.trim());
828
+ const lines = getNormalizedLines(output, { removeEmpty: true });
663
829
  for (const line of lines) {
664
830
  // Format: "job000001 fixed by change 12345 on 2023/01/01 by user@client"
665
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+(.+)$/);
@@ -684,7 +850,7 @@ function parseLabelsOutput(output) {
684
850
  if (!output || typeof output !== 'string') {
685
851
  return results;
686
852
  }
687
- const lines = output.split('\n').filter(line => line.trim());
853
+ const lines = getNormalizedLines(output, { removeEmpty: true });
688
854
  for (const line of lines) {
689
855
  // Format: "Label labelname 2023/01/01 'Label description'"
690
856
  const match = line.match(/^Label\s+(\w+)\s+(\d{4}\/\d{2}\/\d{2})\s+'(.*)'/);
@@ -708,7 +874,7 @@ function parseLabelOutput(output) {
708
874
  if (!output || typeof output !== 'string') {
709
875
  return result;
710
876
  }
711
- const lines = output.split('\n');
877
+ const lines = getNormalizedLines(output);
712
878
  for (const line of lines) {
713
879
  const colonIndex = line.indexOf(': ');
714
880
  if (colonIndex > 0) {
@@ -732,7 +898,7 @@ function parseSizesOutput(output) {
732
898
  };
733
899
  }
734
900
  const result = {};
735
- const lines = output.split('\n').filter(line => line.trim());
901
+ const lines = getNormalizedLines(output, { removeEmpty: true });
736
902
  for (const line of lines) {
737
903
  if (line.includes('files,')) {
738
904
  // Format: "12345 files, 67890123 bytes"
@@ -754,7 +920,7 @@ function parseHaveOutput(output) {
754
920
  if (!output || typeof output !== 'string') {
755
921
  return results;
756
922
  }
757
- const lines = output.split('\n').filter(line => line.trim());
923
+ const lines = getNormalizedLines(output, { removeEmpty: true });
758
924
  for (const line of lines) {
759
925
  // Format: "//depot/path#123 - /local/path"
760
926
  const match = line.match(/^(.+?)#(\d+)\s+-\s+(.+)/);
@@ -778,7 +944,7 @@ function parseWhereOutput(output) {
778
944
  if (!output || typeof output !== 'string') {
779
945
  return results;
780
946
  }
781
- const lines = output.split('\n').filter(line => line.trim());
947
+ const lines = getNormalizedLines(output, { removeEmpty: true });
782
948
  for (const line of lines) {
783
949
  // Format: "//depot/path /local/path //depot/path"
784
950
  const parts = line.split(/\s+/);
@@ -792,6 +958,105 @@ function parseWhereOutput(output) {
792
958
  }
793
959
  return results;
794
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
+ }
795
1060
  /**
796
1061
  * Helper function to convert resolve status to action
797
1062
  */
@@ -804,6 +1069,53 @@ function getResolveAction(status) {
804
1069
  default: return 'unknown';
805
1070
  }
806
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
+ }
807
1119
  /**
808
1120
  * Parse individual values with type conversion
809
1121
  */