@davidvornholt/standards 0.9.0 → 0.10.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.
- package/README.md +9 -4
- package/package.json +12 -2
- package/src/cli.ts +224 -187
- package/src/dependabot-compose-input.ts +136 -0
- package/src/dependabot-compose.ts +188 -0
- package/src/dependabot-inspect.ts +181 -0
- package/src/dependabot-update.ts +156 -0
- package/src/github-api.ts +47 -0
- package/src/github-apply.ts +20 -4
- package/src/github-command-shared.ts +36 -2
- package/src/github-commands.ts +12 -78
- package/src/github-diff.ts +5 -5
- package/src/github-live-drift.ts +99 -0
- package/src/github-settings-parse.ts +3 -0
- package/src/structure-profile.ts +2 -0
- package/src/yaml-emit.ts +67 -0
- package/src/yaml-parse.ts +25 -0
package/src/cli.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// davidvornholt/standards template into a consumer repo and detects local
|
|
5
5
|
// tampering with them. See the standards repository README for the design.
|
|
6
6
|
//
|
|
7
|
-
// This
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// repo's Effect standard
|
|
7
|
+
// This bootstrap executable keeps its runtime surface minimal and does not use
|
|
8
|
+
// Effect: `bunx` must be able to execute the published package before a
|
|
9
|
+
// consumer has dependencies. Its strict YAML parser is a declared runtime
|
|
10
|
+
// dependency. This is the documented exception to the repo's Effect standard
|
|
11
|
+
// for standalone bootstrap tooling.
|
|
11
12
|
|
|
12
13
|
import { execFileSync } from 'node:child_process';
|
|
13
14
|
import { createHash } from 'node:crypto';
|
|
@@ -32,15 +33,21 @@ import {
|
|
|
32
33
|
sep,
|
|
33
34
|
} from 'node:path';
|
|
34
35
|
import process from 'node:process';
|
|
36
|
+
import {
|
|
37
|
+
composeDependabot,
|
|
38
|
+
DEPENDABOT_BASE_FILE,
|
|
39
|
+
DEPENDABOT_FILE,
|
|
40
|
+
DEPENDABOT_LOCAL_FILE,
|
|
41
|
+
} from './dependabot-compose';
|
|
42
|
+
import { inspectDependabot } from './dependabot-inspect';
|
|
35
43
|
import { CANONICAL_SETTINGS_FILE, LOCAL_SETTINGS_FILE } from './github-api';
|
|
36
44
|
import { runGithubApply, runGithubCheck } from './github-commands';
|
|
37
45
|
import { loadGithubSettings } from './github-settings';
|
|
46
|
+
import { isNonEmptyString, isRecord } from './github-settings-parse';
|
|
38
47
|
import { collectStructureProblems } from './structure-check';
|
|
39
48
|
import type { StructureProfile } from './structure-profile';
|
|
40
49
|
import { hasSafeCommand } from './structure-script';
|
|
41
50
|
|
|
42
|
-
const { YAML: BunYaml } = await import('bun');
|
|
43
|
-
|
|
44
51
|
const DEFAULT_UPSTREAM = 'github:davidvornholt/standards';
|
|
45
52
|
|
|
46
53
|
// Characters of a sha256 hex digest shown in drift reports; enough to identify.
|
|
@@ -79,6 +86,7 @@ type Source = {
|
|
|
79
86
|
|
|
80
87
|
type Command =
|
|
81
88
|
| 'check'
|
|
89
|
+
| 'dependabot'
|
|
82
90
|
| 'doctor'
|
|
83
91
|
| 'github'
|
|
84
92
|
| 'help'
|
|
@@ -93,6 +101,7 @@ type CliOptions = {
|
|
|
93
101
|
readonly from: string | undefined;
|
|
94
102
|
readonly ref: string | undefined;
|
|
95
103
|
readonly apply: boolean;
|
|
104
|
+
readonly write: boolean;
|
|
96
105
|
readonly profile: StructureProfile;
|
|
97
106
|
};
|
|
98
107
|
|
|
@@ -403,6 +412,12 @@ const runInit = async (
|
|
|
403
412
|
): Promise<void> => {
|
|
404
413
|
const seeds = await seedTargets(src.dir, manifest.seedDir);
|
|
405
414
|
assertDisjoint(manifest.paths, [...seeds.keys()]);
|
|
415
|
+
const prospectiveDependabot = await prepareProspectiveDependabot(
|
|
416
|
+
manifest,
|
|
417
|
+
src.dir,
|
|
418
|
+
consumer,
|
|
419
|
+
seeds.get(DEPENDABOT_LOCAL_FILE) ?? null,
|
|
420
|
+
);
|
|
406
421
|
await Promise.all(
|
|
407
422
|
[...seeds].map(async ([rel, abs]) => {
|
|
408
423
|
const dest = join(consumer, rel);
|
|
@@ -423,6 +438,7 @@ const runInit = async (
|
|
|
423
438
|
dryRun: false,
|
|
424
439
|
});
|
|
425
440
|
reportMirror(result, false);
|
|
441
|
+
await applyProspectiveDependabot(consumer, prospectiveDependabot, false);
|
|
426
442
|
await writeLock(consumer, {
|
|
427
443
|
upstream: manifest.upstream,
|
|
428
444
|
sha: src.sha,
|
|
@@ -441,6 +457,12 @@ const runSync = async (
|
|
|
441
457
|
): Promise<void> => {
|
|
442
458
|
const seeds = await seedTargets(src.dir, manifest.seedDir);
|
|
443
459
|
assertDisjoint(manifest.paths, [...seeds.keys()]);
|
|
460
|
+
const prospectiveDependabot = await prepareProspectiveDependabot(
|
|
461
|
+
manifest,
|
|
462
|
+
src.dir,
|
|
463
|
+
consumer,
|
|
464
|
+
null,
|
|
465
|
+
);
|
|
444
466
|
const lock = await readLock(consumer);
|
|
445
467
|
const result = await mirror({
|
|
446
468
|
manifest,
|
|
@@ -450,6 +472,7 @@ const runSync = async (
|
|
|
450
472
|
dryRun,
|
|
451
473
|
});
|
|
452
474
|
reportMirror(result, dryRun);
|
|
475
|
+
await applyProspectiveDependabot(consumer, prospectiveDependabot, dryRun);
|
|
453
476
|
if (dryRun) {
|
|
454
477
|
return;
|
|
455
478
|
}
|
|
@@ -510,177 +533,159 @@ const runCheck = async (consumer: string): Promise<boolean> => {
|
|
|
510
533
|
const readTextIfPresent = async (path: string): Promise<string | null> =>
|
|
511
534
|
existsSync(path) ? readFile(path, 'utf8') : null;
|
|
512
535
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
'monthly',
|
|
518
|
-
'quarterly',
|
|
519
|
-
'semiannually',
|
|
520
|
-
'yearly',
|
|
521
|
-
'cron',
|
|
522
|
-
]);
|
|
523
|
-
|
|
524
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
525
|
-
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
526
|
-
|
|
527
|
-
const isNonEmptyString = (value: unknown): value is string =>
|
|
528
|
-
typeof value === 'string' && value.length > 0;
|
|
529
|
-
|
|
530
|
-
type DependabotUpdateInspection = {
|
|
531
|
-
readonly problems: ReadonlyArray<string>;
|
|
532
|
-
readonly rootEcosystem: string | null;
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
const inspectDependabotSchedule = (
|
|
536
|
-
schedule: unknown,
|
|
537
|
-
label: string,
|
|
538
|
-
): ReadonlyArray<string> => {
|
|
539
|
-
if (!(isRecord(schedule) && isNonEmptyString(schedule.interval))) {
|
|
540
|
-
return [`${label} must define schedule.interval`];
|
|
541
|
-
}
|
|
542
|
-
if (!DEPENDABOT_SCHEDULE_INTERVALS.has(schedule.interval)) {
|
|
543
|
-
return [`${label} has an unsupported schedule.interval`];
|
|
544
|
-
}
|
|
545
|
-
if (schedule.interval === 'cron' && !isNonEmptyString(schedule.cronjob)) {
|
|
546
|
-
return [`${label} must define schedule.cronjob for a cron interval`];
|
|
547
|
-
}
|
|
548
|
-
return [];
|
|
536
|
+
type DependabotSources = {
|
|
537
|
+
readonly base: string | null;
|
|
538
|
+
readonly local: string | null;
|
|
539
|
+
readonly current: string | null;
|
|
549
540
|
};
|
|
550
541
|
|
|
551
|
-
|
|
542
|
+
const readDependabotSources = async (
|
|
543
|
+
consumer: string,
|
|
544
|
+
): Promise<DependabotSources> => ({
|
|
545
|
+
base: await readTextIfPresent(join(consumer, DEPENDABOT_BASE_FILE)),
|
|
546
|
+
local: await readTextIfPresent(join(consumer, DEPENDABOT_LOCAL_FILE)),
|
|
547
|
+
current: await readTextIfPresent(join(consumer, DEPENDABOT_FILE)),
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Compose the generated Dependabot config from its on-disk sources, folding
|
|
551
|
+
// Dependabot semantic validation into the same problem list.
|
|
552
|
+
const composedDependabot = (
|
|
553
|
+
sources: DependabotSources,
|
|
554
|
+
): {
|
|
555
|
+
readonly composed: string | null;
|
|
552
556
|
readonly problems: ReadonlyArray<string>;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const inspectDependabotGroups = (
|
|
557
|
-
groups: unknown,
|
|
558
|
-
): DependabotGroupInspection => {
|
|
559
|
-
if (groups === undefined) {
|
|
560
|
-
return { problems: [], scheduledGroups: new Set() };
|
|
561
|
-
}
|
|
562
|
-
if (!isRecord(groups)) {
|
|
557
|
+
} => {
|
|
558
|
+
if (sources.base === null) {
|
|
563
559
|
return {
|
|
560
|
+
composed: null,
|
|
564
561
|
problems: [
|
|
565
|
-
|
|
562
|
+
`${DEPENDABOT_BASE_FILE} must exist; run \`bun standards sync\` to mirror it in`,
|
|
566
563
|
],
|
|
567
|
-
scheduledGroups: new Set(),
|
|
568
564
|
};
|
|
569
565
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
for (const [name, group] of Object.entries(groups)) {
|
|
574
|
-
const label = `.github/dependabot.yml multi-ecosystem-groups.${name}`;
|
|
575
|
-
const groupProblems = isRecord(group)
|
|
576
|
-
? inspectDependabotSchedule(group.schedule, label)
|
|
577
|
-
: [`${label} must be a mapping`];
|
|
578
|
-
problems.push(...groupProblems);
|
|
579
|
-
if (groupProblems.length === 0) {
|
|
580
|
-
scheduledGroups.add(name);
|
|
581
|
-
}
|
|
566
|
+
const result = composeDependabot(sources.base, sources.local);
|
|
567
|
+
if (result.composed === null) {
|
|
568
|
+
return result;
|
|
582
569
|
}
|
|
583
|
-
|
|
570
|
+
const problems = inspectDependabot(result.composed);
|
|
571
|
+
return problems.length > 0 ? { composed: null, problems } : result;
|
|
584
572
|
};
|
|
585
573
|
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
return { problems: [`${label} must be a mapping`], rootEcosystem: null };
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const {
|
|
597
|
-
directory,
|
|
598
|
-
directories,
|
|
599
|
-
schedule,
|
|
600
|
-
'multi-ecosystem-group': multiEcosystemGroup,
|
|
601
|
-
'package-ecosystem': ecosystem,
|
|
602
|
-
} = update;
|
|
603
|
-
const problems: Array<string> = [];
|
|
604
|
-
if (!isNonEmptyString(ecosystem)) {
|
|
605
|
-
problems.push(`${label} must define package-ecosystem`);
|
|
574
|
+
const dependabotProblems = async (
|
|
575
|
+
consumer: string,
|
|
576
|
+
): Promise<ReadonlyArray<string>> => {
|
|
577
|
+
const sources = await readDependabotSources(consumer);
|
|
578
|
+
const { composed, problems } = composedDependabot(sources);
|
|
579
|
+
if (composed === null) {
|
|
580
|
+
return problems;
|
|
606
581
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
directories.length > 0 &&
|
|
612
|
-
directories.every(isNonEmptyString);
|
|
613
|
-
if (hasDirectory === hasDirectories) {
|
|
614
|
-
problems.push(
|
|
615
|
-
`${label} must define exactly one of directory or directories`,
|
|
616
|
-
);
|
|
582
|
+
if (sources.current !== composed) {
|
|
583
|
+
return [
|
|
584
|
+
`${DEPENDABOT_FILE} does not match its composed sources; regenerate it with \`bun standards dependabot --write\``,
|
|
585
|
+
];
|
|
617
586
|
}
|
|
587
|
+
return [];
|
|
588
|
+
};
|
|
618
589
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
590
|
+
const writeComposedDependabot = async (
|
|
591
|
+
consumer: string,
|
|
592
|
+
composed: string,
|
|
593
|
+
): Promise<void> => {
|
|
594
|
+
const dest = join(consumer, DEPENDABOT_FILE);
|
|
595
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
596
|
+
await writeFile(dest, composed);
|
|
597
|
+
};
|
|
628
598
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
599
|
+
const composeProblemsError = (problems: ReadonlyArray<string>): Error =>
|
|
600
|
+
new Error(
|
|
601
|
+
[
|
|
602
|
+
`cannot compose ${DEPENDABOT_FILE}:`,
|
|
603
|
+
...problems.map((problem) => ` - ${problem}`),
|
|
604
|
+
].join('\n'),
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
type ProspectiveDependabot = {
|
|
608
|
+
readonly composed: string;
|
|
609
|
+
readonly current: string | null;
|
|
637
610
|
};
|
|
638
611
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
612
|
+
// Validate the incoming canonical base against the effective local overlay
|
|
613
|
+
// before init/sync mutates any consumer-owned, canonical, generated, or lock
|
|
614
|
+
// file. For init, an existing overlay wins; otherwise the overlay seed is what
|
|
615
|
+
// the command is about to install.
|
|
616
|
+
const prepareProspectiveDependabot = async (
|
|
617
|
+
manifest: Manifest,
|
|
618
|
+
srcDir: string,
|
|
619
|
+
consumer: string,
|
|
620
|
+
localSeed: string | null,
|
|
621
|
+
): Promise<ProspectiveDependabot> => {
|
|
622
|
+
const incoming = await listManaged(srcDir, manifest.paths);
|
|
623
|
+
const basePath = incoming.get(DEPENDABOT_BASE_FILE);
|
|
624
|
+
if (basePath === undefined) {
|
|
625
|
+
throw new Error(
|
|
626
|
+
`source content must manage ${DEPENDABOT_BASE_FILE}; @davidvornholt/standards 0.10.1 requires a 0.10.1-compatible content ref`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
const existingLocal = await readTextIfPresent(
|
|
630
|
+
join(consumer, DEPENDABOT_LOCAL_FILE),
|
|
631
|
+
);
|
|
632
|
+
const local =
|
|
633
|
+
existingLocal ??
|
|
634
|
+
(localSeed === null ? null : await readFile(localSeed, 'utf8'));
|
|
635
|
+
const current = await readTextIfPresent(join(consumer, DEPENDABOT_FILE));
|
|
636
|
+
const sources = {
|
|
637
|
+
base: await readFile(basePath, 'utf8'),
|
|
638
|
+
local,
|
|
639
|
+
current,
|
|
640
|
+
};
|
|
641
|
+
const { composed, problems } = composedDependabot(sources);
|
|
642
|
+
if (composed === null) {
|
|
643
|
+
throw composeProblemsError(problems);
|
|
646
644
|
}
|
|
645
|
+
return { composed, current };
|
|
646
|
+
};
|
|
647
647
|
|
|
648
|
-
|
|
649
|
-
|
|
648
|
+
const applyProspectiveDependabot = async (
|
|
649
|
+
consumer: string,
|
|
650
|
+
prospective: ProspectiveDependabot,
|
|
651
|
+
dryRun: boolean,
|
|
652
|
+
): Promise<void> => {
|
|
653
|
+
if (prospective.current === prospective.composed) {
|
|
654
|
+
return;
|
|
650
655
|
}
|
|
651
|
-
if (
|
|
652
|
-
|
|
656
|
+
if (dryRun) {
|
|
657
|
+
console.log(` would generate ${DEPENDABOT_FILE}`);
|
|
658
|
+
return;
|
|
653
659
|
}
|
|
660
|
+
await writeComposedDependabot(consumer, prospective.composed);
|
|
661
|
+
console.log(` generated ${DEPENDABOT_FILE}`);
|
|
662
|
+
};
|
|
654
663
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
664
|
+
const runDependabotCheck = async (consumer: string): Promise<boolean> => {
|
|
665
|
+
const problems = await dependabotProblems(consumer);
|
|
666
|
+
if (problems.length > 0) {
|
|
667
|
+
console.error(`standards dependabot: ${problems.length} problem(s):`);
|
|
668
|
+
console.error(problems.map((problem) => ` - ${problem}`).join('\n'));
|
|
669
|
+
return false;
|
|
659
670
|
}
|
|
671
|
+
console.log(
|
|
672
|
+
`standards dependabot: ${DEPENDABOT_FILE} matches its composed sources`,
|
|
673
|
+
);
|
|
674
|
+
return true;
|
|
675
|
+
};
|
|
660
676
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
update,
|
|
667
|
-
index,
|
|
668
|
-
groupInspection.scheduledGroups,
|
|
669
|
-
);
|
|
670
|
-
problems.push(...inspection.problems);
|
|
671
|
-
if (inspection.rootEcosystem !== null) {
|
|
672
|
-
rootEcosystems.add(inspection.rootEcosystem);
|
|
673
|
-
}
|
|
677
|
+
const runDependabotWrite = async (consumer: string): Promise<void> => {
|
|
678
|
+
const sources = await readDependabotSources(consumer);
|
|
679
|
+
const { composed, problems } = composedDependabot(sources);
|
|
680
|
+
if (composed === null) {
|
|
681
|
+
throw composeProblemsError(problems);
|
|
674
682
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
problems.push(
|
|
679
|
-
`.github/dependabot.yml must include a root-directory ${ecosystem} ecosystem`,
|
|
680
|
-
);
|
|
681
|
-
}
|
|
683
|
+
if (sources.current === composed) {
|
|
684
|
+
console.log(`standards dependabot: ${DEPENDABOT_FILE} is up to date`);
|
|
685
|
+
return;
|
|
682
686
|
}
|
|
683
|
-
|
|
687
|
+
await writeComposedDependabot(consumer, composed);
|
|
688
|
+
console.log(`standards dependabot: generated ${DEPENDABOT_FILE}`);
|
|
684
689
|
};
|
|
685
690
|
|
|
686
691
|
const inspectPackageJson = (packageRaw: string): ReadonlyArray<string> => {
|
|
@@ -726,14 +731,7 @@ const runDoctor = async (consumer: string): Promise<boolean> => {
|
|
|
726
731
|
problems.push('AGENTS.local.md must exist for project-specific guidance');
|
|
727
732
|
}
|
|
728
733
|
|
|
729
|
-
|
|
730
|
-
join(consumer, '.github/dependabot.yml'),
|
|
731
|
-
);
|
|
732
|
-
if (dependabot === null) {
|
|
733
|
-
problems.push('.github/dependabot.yml must exist');
|
|
734
|
-
} else {
|
|
735
|
-
problems.push(...inspectDependabot(dependabot));
|
|
736
|
-
}
|
|
734
|
+
problems.push(...(await dependabotProblems(consumer)));
|
|
737
735
|
|
|
738
736
|
const packagePath = join(consumer, 'package.json');
|
|
739
737
|
const packageRaw = await readTextIfPresent(packagePath);
|
|
@@ -773,13 +771,14 @@ const runDoctor = async (consumer: string): Promise<boolean> => {
|
|
|
773
771
|
const USAGE = `Usage: standards <command> [options]
|
|
774
772
|
|
|
775
773
|
Commands:
|
|
776
|
-
init
|
|
777
|
-
sync
|
|
778
|
-
check
|
|
779
|
-
doctor
|
|
780
|
-
structure
|
|
781
|
-
|
|
782
|
-
|
|
774
|
+
init Bootstrap a consumer repo: seed repo-owned files, mirror canonical files, write the lock
|
|
775
|
+
sync Mirror canonical files from upstream, regenerate the composed Dependabot config, and rewrite the lock
|
|
776
|
+
check Verify canonical files, extension seams, monorepo structure, and GitHub settings
|
|
777
|
+
doctor Validate extension seams only
|
|
778
|
+
structure Validate monorepo structure rules only
|
|
779
|
+
dependabot Verify (--check) or regenerate (--write) the composed .github/dependabot.yml
|
|
780
|
+
github Compare (--check) or converge (--apply) live GitHub settings
|
|
781
|
+
help Show this help
|
|
783
782
|
|
|
784
783
|
Options:
|
|
785
784
|
--dir <path> Consumer directory to operate on (default: current directory)
|
|
@@ -787,12 +786,14 @@ Options:
|
|
|
787
786
|
--from <src> Upstream override for init/sync (GitHub repo or local path)
|
|
788
787
|
--ref <ref> Upstream tag, branch, or full commit sha for init/sync (remote Git/GitHub sources only; default: main)
|
|
789
788
|
--dry-run Preview a sync without writing anything
|
|
790
|
-
--check With github: compare
|
|
791
|
-
--apply With github: converge the live repository (needs admin auth)
|
|
789
|
+
--check With github/dependabot: compare against the declared sources (default)
|
|
790
|
+
--apply With github: converge the live repository (needs admin auth)
|
|
791
|
+
--write With dependabot: regenerate the composed .github/dependabot.yml`;
|
|
792
792
|
|
|
793
793
|
const commandFromArg = (arg: string): Command => {
|
|
794
794
|
if (
|
|
795
795
|
arg === 'check' ||
|
|
796
|
+
arg === 'dependabot' ||
|
|
796
797
|
arg === 'doctor' ||
|
|
797
798
|
arg === 'github' ||
|
|
798
799
|
arg === 'help' ||
|
|
@@ -832,6 +833,44 @@ const nextOptionValue = (
|
|
|
832
833
|
return value;
|
|
833
834
|
};
|
|
834
835
|
|
|
836
|
+
type ParsedFlags = {
|
|
837
|
+
readonly command: Command | undefined;
|
|
838
|
+
readonly checkFlag: boolean;
|
|
839
|
+
readonly apply: boolean;
|
|
840
|
+
readonly write: boolean;
|
|
841
|
+
readonly ref: string | undefined;
|
|
842
|
+
readonly profile: StructureProfile | undefined;
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// Every option is only meaningful with specific commands; reject the rest so a
|
|
846
|
+
// typo fails loudly instead of silently doing the default thing.
|
|
847
|
+
const assertOptionUsage = (flags: ParsedFlags): void => {
|
|
848
|
+
const { command, checkFlag, apply, write, ref, profile } = flags;
|
|
849
|
+
if (checkFlag && command !== 'github' && command !== 'dependabot') {
|
|
850
|
+
throw new Error(
|
|
851
|
+
'--check is only valid with the github and dependabot commands',
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
if (apply && command !== 'github') {
|
|
855
|
+
throw new Error('--apply is only valid with the github command');
|
|
856
|
+
}
|
|
857
|
+
if (apply && checkFlag) {
|
|
858
|
+
throw new Error('github accepts exactly one of --check or --apply');
|
|
859
|
+
}
|
|
860
|
+
if (write && command !== 'dependabot') {
|
|
861
|
+
throw new Error('--write is only valid with the dependabot command');
|
|
862
|
+
}
|
|
863
|
+
if (write && checkFlag) {
|
|
864
|
+
throw new Error('dependabot accepts exactly one of --check or --write');
|
|
865
|
+
}
|
|
866
|
+
if (ref !== undefined && command !== 'init' && command !== 'sync') {
|
|
867
|
+
throw new Error('--ref is only valid with the init and sync commands');
|
|
868
|
+
}
|
|
869
|
+
if (profile !== undefined && command !== 'structure') {
|
|
870
|
+
throw new Error('--profile is only valid with the structure command');
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
|
|
835
874
|
const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
836
875
|
let command: Command | undefined;
|
|
837
876
|
let consumer = process.cwd();
|
|
@@ -840,6 +879,7 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
|
840
879
|
let ref: string | undefined;
|
|
841
880
|
let checkFlag = false;
|
|
842
881
|
let apply = false;
|
|
882
|
+
let write = false;
|
|
843
883
|
let profile: StructureProfile | undefined;
|
|
844
884
|
|
|
845
885
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -851,6 +891,9 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
|
851
891
|
case '--check':
|
|
852
892
|
checkFlag = true;
|
|
853
893
|
break;
|
|
894
|
+
case '--write':
|
|
895
|
+
write = true;
|
|
896
|
+
break;
|
|
854
897
|
case '--dir':
|
|
855
898
|
consumer = nextOptionValue(argv, index);
|
|
856
899
|
index += 1;
|
|
@@ -879,21 +922,7 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
|
879
922
|
}
|
|
880
923
|
}
|
|
881
924
|
|
|
882
|
-
|
|
883
|
-
throw new Error('--check is only valid with the github command');
|
|
884
|
-
}
|
|
885
|
-
if (apply && command !== 'github') {
|
|
886
|
-
throw new Error('--apply is only valid with the github command');
|
|
887
|
-
}
|
|
888
|
-
if (apply && checkFlag) {
|
|
889
|
-
throw new Error('github accepts exactly one of --check or --apply');
|
|
890
|
-
}
|
|
891
|
-
if (ref !== undefined && command !== 'init' && command !== 'sync') {
|
|
892
|
-
throw new Error('--ref is only valid with the init and sync commands');
|
|
893
|
-
}
|
|
894
|
-
if (profile !== undefined && command !== 'structure') {
|
|
895
|
-
throw new Error('--profile is only valid with the structure command');
|
|
896
|
-
}
|
|
925
|
+
assertOptionUsage({ command, checkFlag, apply, write, ref, profile });
|
|
897
926
|
|
|
898
927
|
return {
|
|
899
928
|
command,
|
|
@@ -902,6 +931,7 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
|
902
931
|
from,
|
|
903
932
|
ref,
|
|
904
933
|
apply,
|
|
934
|
+
write,
|
|
905
935
|
profile: profile ?? 'consumer',
|
|
906
936
|
};
|
|
907
937
|
};
|
|
@@ -1069,7 +1099,7 @@ const runSyncCommand = async (
|
|
|
1069
1099
|
|
|
1070
1100
|
// Commands whose success is reported through the exit code.
|
|
1071
1101
|
const runGateCommand = (
|
|
1072
|
-
command: 'check' | 'doctor' | 'github' | 'structure',
|
|
1102
|
+
command: 'check' | 'dependabot' | 'doctor' | 'github' | 'structure',
|
|
1073
1103
|
consumer: string,
|
|
1074
1104
|
apply: boolean,
|
|
1075
1105
|
profile: StructureProfile,
|
|
@@ -1077,6 +1107,9 @@ const runGateCommand = (
|
|
|
1077
1107
|
if (command === 'check') {
|
|
1078
1108
|
return runCheckCommand(consumer);
|
|
1079
1109
|
}
|
|
1110
|
+
if (command === 'dependabot') {
|
|
1111
|
+
return runDependabotCheck(consumer);
|
|
1112
|
+
}
|
|
1080
1113
|
if (command === 'doctor') {
|
|
1081
1114
|
return runDoctor(consumer);
|
|
1082
1115
|
}
|
|
@@ -1087,9 +1120,8 @@ const runGateCommand = (
|
|
|
1087
1120
|
};
|
|
1088
1121
|
|
|
1089
1122
|
const main = async (): Promise<void> => {
|
|
1090
|
-
const { command, consumer, dryRun, from, ref, apply, profile } =
|
|
1091
|
-
process.argv.slice(2)
|
|
1092
|
-
);
|
|
1123
|
+
const { command, consumer, dryRun, from, ref, apply, write, profile } =
|
|
1124
|
+
parseArgs(process.argv.slice(2));
|
|
1093
1125
|
|
|
1094
1126
|
if (command === undefined) {
|
|
1095
1127
|
console.error('standards: a command is required\n');
|
|
@@ -1113,6 +1145,11 @@ const main = async (): Promise<void> => {
|
|
|
1113
1145
|
return;
|
|
1114
1146
|
}
|
|
1115
1147
|
|
|
1148
|
+
if (command === 'dependabot' && write) {
|
|
1149
|
+
await runDependabotWrite(consumer);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1116
1153
|
if (!(await runGateCommand(command, consumer, apply, profile))) {
|
|
1117
1154
|
process.exitCode = 1;
|
|
1118
1155
|
}
|