|
6 | 6 | * |
7 | 7 | * Usage: node scripts/fix-standard-track-exceptions.js |
8 | 8 | * |
9 | | - * Walks through each exception and prompts for an action: |
| 9 | + * Walks through each exception and prompts for an action. Spec_url commands |
| 10 | + * accept any space-separated mix of URLs and suggestion numbers (1-N): |
10 | 11 | * |
11 | | - * https://... — Add the URL as spec_url (space-separated for multiple) |
12 | | - * f / false — Set standard_track to false (includes all subfeatures) |
| 12 | + * <items> — Set spec_url to listed URLs/suggestions (e.g. `1 2 https://x`) |
13 | 13 | * p — Copy the parent feature's spec_url |
14 | | - * p=https://.. — Set spec_url on both the parent and this subfeature |
| 14 | + * p <items> — Parent spec_url + extra URLs/suggestions on this subfeature |
| 15 | + * p=<items> — Set items on both the parent and this subfeature |
| 16 | + * f / false — Set standard_track to false (includes all subfeatures) |
15 | 17 | * (empty) — Skip this entry |
16 | 18 | * /foo — Skip ahead until the next entry containing "foo" |
17 | 19 | * o — Open ancestor spec_url(s) in the browser |
@@ -216,13 +218,12 @@ const fetchXrefSuggestions = async ( |
216 | 218 |
|
217 | 219 | const instructions = ` |
218 | 220 | ${styleText('bold', 'Actions:')} |
219 | | - ${styleText('cyan', 'https://...')} Add the URL as spec_url (space-separated for multiple) |
220 | | - ${styleText('cyan', '1-9')} Accept a numbered suggestion |
| 221 | + ${styleText('cyan', '<items>')} URL(s) and/or suggestion #(s), space-separated |
221 | 222 | ${styleText('cyan', 'x')} Fetch xref suggestions for the current feature |
222 | 223 | ${styleText('cyan', 'x <term>')} Fetch xref suggestions for a custom search term |
223 | 224 | ${styleText('cyan', 'p')} Use parent feature's spec_url |
224 | | - ${styleText('cyan', 'p https://...')} Use parent spec_url + extra URL on this subfeature |
225 | | - ${styleText('cyan', 'p=https://...')} Set spec_url on parent + this subfeature |
| 225 | + ${styleText('cyan', 'p <items>')} Parent spec_url + extra items on this subfeature |
| 226 | + ${styleText('cyan', 'p=<items>')} Set items on parent + this subfeature |
226 | 227 | ${styleText('cyan', 'f')} Set standard_track to false (+ all subfeatures) |
227 | 228 | ${styleText('cyan', 'r')} Repeat the previous action |
228 | 229 | ${styleText('cyan', '(Enter)')} Skip this entry |
@@ -273,7 +274,7 @@ const rl = readline.createInterface({ |
273 | 274 | output: process.stdout, |
274 | 275 | }); |
275 | 276 |
|
276 | | -const prompt = ` ${styleText('dim', 'URL, 1-9 (suggestion), x [term] (xref), p, f, r (repeat), /skip-to, or ?')} |
| 277 | +const prompt = ` ${styleText('dim', 'items (URL/1-9), x [term] (xref), p [items], p=items, f, r (repeat), /skip-to, or ?')} |
277 | 278 | > `; |
278 | 279 |
|
279 | 280 | /** Save progress and exit */ |
@@ -520,150 +521,121 @@ while (idx < exceptions.length) { |
520 | 521 | break; |
521 | 522 | } |
522 | 523 |
|
| 524 | + // Spec_url command: optional `p` / `p=` prefix followed by space-separated |
| 525 | + // tokens that are either URLs or suggestion numbers (1-N). |
| 526 | + let parentMode = null; |
| 527 | + let remainder; |
523 | 528 | if (answer === 'p') { |
524 | | - if (!ancestor) { |
| 529 | + parentMode = 'add'; |
| 530 | + remainder = ''; |
| 531 | + } else if (answer.startsWith('p ')) { |
| 532 | + parentMode = 'add'; |
| 533 | + remainder = answer.slice(2); |
| 534 | + } else if (answer.startsWith('p=')) { |
| 535 | + parentMode = 'set'; |
| 536 | + remainder = answer.slice(2); |
| 537 | + } else { |
| 538 | + remainder = answer; |
| 539 | + } |
| 540 | + const tokens = remainder.trim().split(/\s+/).filter(Boolean); |
| 541 | + /** |
| 542 | + * Whether a token looks like a spec_url command token (URL or number). |
| 543 | + * @param {string} t Token to test |
| 544 | + * @returns {boolean} True if token is a URL or numeric suggestion index |
| 545 | + */ |
| 546 | + const looksLikeSpecToken = (t) => |
| 547 | + t.startsWith('https://') || /^\d+$/.test(t); |
| 548 | + const isSpecCommand = |
| 549 | + parentMode !== null || |
| 550 | + (tokens.length > 0 && looksLikeSpecToken(tokens[0])); |
| 551 | + |
| 552 | + if (isSpecCommand) { |
| 553 | + if (parentMode === 'add' && !ancestor) { |
525 | 554 | console.log(styleText('red', ' No ancestor with spec_url found.')); |
526 | 555 | continue; |
527 | 556 | } |
528 | | - updateFeatures([featurePath], (c) => { |
529 | | - c.spec_url = ancestor.spec_url; |
530 | | - return c; |
531 | | - }); |
532 | | - recordSpecUrl(featurePath, ancestor.spec_url); |
533 | | - remaining.delete(featurePath); |
534 | | - lastAction = { |
535 | | - index: i, |
536 | | - /** |
537 | | - * |
538 | | - */ |
539 | | - undo: () => { |
540 | | - updateFeatures([featurePath], (c) => { |
541 | | - delete c.spec_url; |
542 | | - return c; |
543 | | - }); |
544 | | - remaining.add(featurePath); |
545 | | - }, |
546 | | - }; |
547 | | - lastInput = answer; |
548 | | - break; |
549 | | - } |
550 | | - |
551 | | - if (answer.startsWith('p https://')) { |
552 | | - if (!ancestor) { |
553 | | - console.log(styleText('red', ' No ancestor with spec_url found.')); |
| 557 | + if (parentMode === 'set' && tokens.length === 0) { |
| 558 | + console.log( |
| 559 | + styleText('red', ' Need at least one URL or suggestion after p=.'), |
| 560 | + ); |
554 | 561 | continue; |
555 | 562 | } |
556 | | - const extra = answer.slice(2).trim().split(/\s+/); |
557 | | - const specUrl = [...[ancestor.spec_url].flat(), ...extra]; |
558 | | - updateFeatures([featurePath], (c) => { |
559 | | - c.spec_url = specUrl; |
560 | | - return c; |
561 | | - }); |
562 | | - recordSpecUrl(featurePath, specUrl); |
563 | | - remaining.delete(featurePath); |
564 | | - lastAction = { |
565 | | - index: i, |
566 | | - /** |
567 | | - * |
568 | | - */ |
569 | | - undo: () => { |
570 | | - updateFeatures([featurePath], (c) => { |
571 | | - delete c.spec_url; |
572 | | - return c; |
573 | | - }); |
574 | | - remaining.add(featurePath); |
575 | | - }, |
576 | | - }; |
577 | | - lastInput = answer; |
578 | | - break; |
579 | | - } |
580 | 563 |
|
581 | | - if (answer.startsWith('p=https://')) { |
582 | | - const urls = answer.slice(2).trim().split(/\s+/); |
583 | | - const specUrl = urls.length === 1 ? urls[0] : urls; |
584 | | - const parentPath = featurePath.split('.').slice(0, -1).join('.'); |
585 | | - updateFeatures([parentPath], (c) => { |
586 | | - c.spec_url = specUrl; |
587 | | - return c; |
588 | | - }); |
589 | | - updateFeatures([featurePath], (c) => { |
590 | | - c.spec_url = specUrl; |
591 | | - return c; |
592 | | - }); |
593 | | - recordSpecUrl(featurePath, specUrl); |
594 | | - remaining.delete(featurePath); |
595 | | - lastAction = { |
596 | | - index: i, |
597 | | - /** |
598 | | - * |
599 | | - */ |
600 | | - undo: () => { |
601 | | - updateFeatures([parentPath], (c) => { |
602 | | - delete c.spec_url; |
603 | | - return c; |
604 | | - }); |
605 | | - updateFeatures([featurePath], (c) => { |
606 | | - delete c.spec_url; |
607 | | - return c; |
608 | | - }); |
609 | | - remaining.add(featurePath); |
610 | | - }, |
611 | | - }; |
612 | | - lastInput = answer; |
613 | | - break; |
614 | | - } |
615 | | - |
616 | | - const suggestionNum = Number(answer); |
617 | | - if ( |
618 | | - Number.isInteger(suggestionNum) && |
619 | | - suggestionNum >= 1 && |
620 | | - suggestionNum <= suggestions.length |
621 | | - ) { |
622 | | - const specUrl = suggestions[suggestionNum - 1]; |
623 | | - updateFeatures([featurePath], (c) => { |
624 | | - c.spec_url = specUrl; |
625 | | - return c; |
626 | | - }); |
627 | | - remaining.delete(featurePath); |
628 | | - lastAction = { |
629 | | - index: i, |
630 | | - /** |
631 | | - * |
632 | | - */ |
633 | | - undo: () => { |
634 | | - updateFeatures([featurePath], (c) => { |
635 | | - delete c.spec_url; |
636 | | - return c; |
637 | | - }); |
638 | | - remaining.add(featurePath); |
639 | | - }, |
640 | | - }; |
641 | | - lastInput = answer; |
642 | | - break; |
643 | | - } |
| 564 | + // Resolve tokens to URLs |
| 565 | + const resolved = []; |
| 566 | + let invalid = null; |
| 567 | + for (const t of tokens) { |
| 568 | + if (t.startsWith('https://')) { |
| 569 | + resolved.push(t); |
| 570 | + } else { |
| 571 | + const num = Number(t); |
| 572 | + if (Number.isInteger(num) && num >= 1 && num <= suggestions.length) { |
| 573 | + resolved.push(suggestions[num - 1]); |
| 574 | + } else { |
| 575 | + invalid = t; |
| 576 | + break; |
| 577 | + } |
| 578 | + } |
| 579 | + } |
| 580 | + if (invalid !== null) { |
| 581 | + console.log( |
| 582 | + styleText('yellow', ` Invalid token: ${invalid}. Type ? for help.`), |
| 583 | + ); |
| 584 | + continue; |
| 585 | + } |
644 | 586 |
|
645 | | - if (answer.startsWith('https://')) { |
646 | | - const urls = answer.split(/\s+/); |
647 | | - const specUrl = urls.length === 1 ? urls[0] : urls; |
648 | | - updateFeatures([featurePath], (c) => { |
649 | | - c.spec_url = specUrl; |
650 | | - return c; |
651 | | - }); |
652 | | - recordSpecUrl(featurePath, specUrl); |
653 | | - remaining.delete(featurePath); |
654 | | - lastAction = { |
655 | | - index: i, |
656 | | - /** |
657 | | - * |
658 | | - */ |
659 | | - undo: () => { |
660 | | - updateFeatures([featurePath], (c) => { |
661 | | - delete c.spec_url; |
662 | | - return c; |
663 | | - }); |
664 | | - remaining.add(featurePath); |
665 | | - }, |
666 | | - }; |
| 587 | + const allUrls = |
| 588 | + parentMode === 'add' && ancestor |
| 589 | + ? [...[ancestor.spec_url].flat(), ...resolved] |
| 590 | + : resolved; |
| 591 | + const specUrl = allUrls.length === 1 ? allUrls[0] : allUrls; |
| 592 | + |
| 593 | + if (parentMode === 'set') { |
| 594 | + const parentPath = featurePath.split('.').slice(0, -1).join('.'); |
| 595 | + updateFeatures([parentPath], (c) => { |
| 596 | + c.spec_url = specUrl; |
| 597 | + return c; |
| 598 | + }); |
| 599 | + updateFeatures([featurePath], (c) => { |
| 600 | + c.spec_url = specUrl; |
| 601 | + return c; |
| 602 | + }); |
| 603 | + recordSpecUrl(featurePath, specUrl); |
| 604 | + remaining.delete(featurePath); |
| 605 | + lastAction = { |
| 606 | + index: i, |
| 607 | + /** Revert spec_url on parent + this feature. */ |
| 608 | + undo: () => { |
| 609 | + updateFeatures([parentPath], (c) => { |
| 610 | + delete c.spec_url; |
| 611 | + return c; |
| 612 | + }); |
| 613 | + updateFeatures([featurePath], (c) => { |
| 614 | + delete c.spec_url; |
| 615 | + return c; |
| 616 | + }); |
| 617 | + remaining.add(featurePath); |
| 618 | + }, |
| 619 | + }; |
| 620 | + } else { |
| 621 | + updateFeatures([featurePath], (c) => { |
| 622 | + c.spec_url = specUrl; |
| 623 | + return c; |
| 624 | + }); |
| 625 | + recordSpecUrl(featurePath, specUrl); |
| 626 | + remaining.delete(featurePath); |
| 627 | + lastAction = { |
| 628 | + index: i, |
| 629 | + /** Revert spec_url on this feature. */ |
| 630 | + undo: () => { |
| 631 | + updateFeatures([featurePath], (c) => { |
| 632 | + delete c.spec_url; |
| 633 | + return c; |
| 634 | + }); |
| 635 | + remaining.add(featurePath); |
| 636 | + }, |
| 637 | + }; |
| 638 | + } |
667 | 639 | lastInput = answer; |
668 | 640 | break; |
669 | 641 | } |
|
0 commit comments