@davidvornholt/standards 0.3.0 → 0.5.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/package.json +1 -1
- package/src/cli.ts +90 -37
- package/src/github-api.ts +1 -1
- package/src/github-commands.ts +1 -1
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -74,13 +74,14 @@ type Source = {
|
|
|
74
74
|
readonly cleanup: () => void;
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
-
type Command = 'check' | 'doctor' | 'github' | 'init' | 'sync';
|
|
77
|
+
type Command = 'check' | 'doctor' | 'github' | 'help' | 'init' | 'sync';
|
|
78
78
|
|
|
79
79
|
type CliOptions = {
|
|
80
|
-
readonly command: Command;
|
|
80
|
+
readonly command: Command | undefined;
|
|
81
81
|
readonly consumer: string;
|
|
82
82
|
readonly dryRun: boolean;
|
|
83
83
|
readonly from: string | undefined;
|
|
84
|
+
readonly ref: string | undefined;
|
|
84
85
|
readonly apply: boolean;
|
|
85
86
|
};
|
|
86
87
|
|
|
@@ -163,9 +164,15 @@ const writeLock = async (dir: string, lock: Lock): Promise<void> => {
|
|
|
163
164
|
|
|
164
165
|
// Fetch the template into a working directory. Accepts a local path (used to
|
|
165
166
|
// prove the engine before the public repo exists and in tests), a github:
|
|
166
|
-
// shorthand, or any git URL.
|
|
167
|
-
|
|
167
|
+
// shorthand, or any git URL. Remote sources default to `main`; `ref` pins a
|
|
168
|
+
// tag, branch, or full commit sha instead (`git fetch` accepts all three).
|
|
169
|
+
const resolveSource = (src: string, ref: string | undefined): Source => {
|
|
168
170
|
if (existsSync(src)) {
|
|
171
|
+
if (ref !== undefined) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`--ref requires a git URL source; a local path is used as-is: ${src}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
169
176
|
let sha = 'local';
|
|
170
177
|
try {
|
|
171
178
|
sha = execFileSync('git', ['-C', src, 'rev-parse', 'HEAD'], {
|
|
@@ -180,18 +187,34 @@ const resolveSource = (src: string): Source => {
|
|
|
180
187
|
const url = src.startsWith(GITHUB_PREFIX)
|
|
181
188
|
? `https://github.com/${src.slice(GITHUB_PREFIX.length)}.git`
|
|
182
189
|
: src;
|
|
190
|
+
const target = ref ?? 'main';
|
|
183
191
|
const dir = mkdtempSync(join(tmpdir(), 'standards-'));
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
192
|
+
const cleanup = (): void => rmSync(dir, { recursive: true, force: true });
|
|
193
|
+
try {
|
|
194
|
+
// init + fetch instead of `clone --branch` so a full commit sha works as a
|
|
195
|
+
// ref, not only tags and branches (GitHub serves reachable sha fetches).
|
|
196
|
+
execFileSync('git', ['init', '--quiet', dir], { stdio: 'ignore' });
|
|
197
|
+
execFileSync(
|
|
198
|
+
'git',
|
|
199
|
+
['-C', dir, 'fetch', '--quiet', '--depth', '1', '--', url, target],
|
|
200
|
+
{ stdio: 'ignore' },
|
|
201
|
+
);
|
|
202
|
+
execFileSync(
|
|
203
|
+
'git',
|
|
204
|
+
['-C', dir, 'checkout', '--quiet', '--detach', 'FETCH_HEAD'],
|
|
205
|
+
{ stdio: 'ignore' },
|
|
206
|
+
);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
cleanup();
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Cannot fetch "${target}" from ${url}; expected a tag, branch, or full commit sha reachable on the remote`,
|
|
211
|
+
{ cause: error },
|
|
212
|
+
);
|
|
213
|
+
}
|
|
187
214
|
const sha = execFileSync('git', ['-C', dir, 'rev-parse', 'HEAD'], {
|
|
188
215
|
encoding: 'utf8',
|
|
189
216
|
}).trim();
|
|
190
|
-
return {
|
|
191
|
-
dir,
|
|
192
|
-
sha,
|
|
193
|
-
cleanup: () => rmSync(dir, { recursive: true, force: true }),
|
|
194
|
-
};
|
|
217
|
+
return { dir, sha, cleanup };
|
|
195
218
|
};
|
|
196
219
|
|
|
197
220
|
// Recursively collect files under `abs`, keyed by their POSIX path relative to
|
|
@@ -461,7 +484,7 @@ const runCheck = async (consumer: string): Promise<boolean> => {
|
|
|
461
484
|
);
|
|
462
485
|
console.error(problems.join('\n'));
|
|
463
486
|
console.error(
|
|
464
|
-
'These files are read-only. Restore them with `
|
|
487
|
+
'These files are read-only. Restore them with `bun standards sync`, or move your change upstream.',
|
|
465
488
|
);
|
|
466
489
|
return false;
|
|
467
490
|
}
|
|
@@ -474,15 +497,6 @@ const runCheck = async (consumer: string): Promise<boolean> => {
|
|
|
474
497
|
const readTextIfPresent = async (path: string): Promise<string | null> =>
|
|
475
498
|
existsSync(path) ? readFile(path, 'utf8') : null;
|
|
476
499
|
|
|
477
|
-
const hasStandardsImport = (justfile: string): boolean =>
|
|
478
|
-
justfile.split('\n').some((line) => {
|
|
479
|
-
const trimmed = line.trim();
|
|
480
|
-
return (
|
|
481
|
-
trimmed === "import 'standards.just'" ||
|
|
482
|
-
trimmed === 'import "standards.just"'
|
|
483
|
-
);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
500
|
const DEPENDABOT_BASELINE_ECOSYSTEMS = ['bun', 'github-actions'] as const;
|
|
487
501
|
const DEPENDABOT_SCHEDULE_INTERVALS = new Set([
|
|
488
502
|
'daily',
|
|
@@ -679,11 +693,6 @@ const inspectPackageJson = (packageRaw: string): ReadonlyArray<string> => {
|
|
|
679
693
|
|
|
680
694
|
const runDoctor = async (consumer: string): Promise<boolean> => {
|
|
681
695
|
const problems: Array<string> = [];
|
|
682
|
-
const justfile = await readTextIfPresent(join(consumer, 'justfile'));
|
|
683
|
-
if (justfile === null || !hasStandardsImport(justfile)) {
|
|
684
|
-
problems.push("justfile must import 'standards.just'");
|
|
685
|
-
}
|
|
686
|
-
|
|
687
696
|
const biome = await readTextIfPresent(join(consumer, 'biome.jsonc'));
|
|
688
697
|
if (biome === null || !biome.includes('"./biome.base.jsonc"')) {
|
|
689
698
|
problems.push('biome.jsonc must extend "./biome.base.jsonc"');
|
|
@@ -735,11 +744,30 @@ const runDoctor = async (consumer: string): Promise<boolean> => {
|
|
|
735
744
|
return true;
|
|
736
745
|
};
|
|
737
746
|
|
|
747
|
+
const USAGE = `Usage: standards <command> [options]
|
|
748
|
+
|
|
749
|
+
Commands:
|
|
750
|
+
init Bootstrap a consumer repo: seed repo-owned files, mirror canonical files, write the lock
|
|
751
|
+
sync Mirror canonical files from upstream and rewrite the lock
|
|
752
|
+
check Verify canonical files, extension seams, and GitHub settings
|
|
753
|
+
doctor Validate extension seams only
|
|
754
|
+
github Compare (--check) or converge (--apply) live GitHub settings
|
|
755
|
+
help Show this help
|
|
756
|
+
|
|
757
|
+
Options:
|
|
758
|
+
--dir <path> Consumer directory to operate on (default: current directory)
|
|
759
|
+
--from <src> Upstream override for init/sync (GitHub repo or local path)
|
|
760
|
+
--ref <ref> Upstream tag, branch, or full commit sha for init/sync (remote Git/GitHub sources only; default: main)
|
|
761
|
+
--dry-run Preview a sync without writing anything
|
|
762
|
+
--check With github: compare live settings to the declaration (default)
|
|
763
|
+
--apply With github: converge the live repository (needs admin auth)`;
|
|
764
|
+
|
|
738
765
|
const commandFromArg = (arg: string): Command => {
|
|
739
766
|
if (
|
|
740
767
|
arg === 'check' ||
|
|
741
768
|
arg === 'doctor' ||
|
|
742
769
|
arg === 'github' ||
|
|
770
|
+
arg === 'help' ||
|
|
743
771
|
arg === 'init' ||
|
|
744
772
|
arg === 'sync'
|
|
745
773
|
) {
|
|
@@ -773,6 +801,7 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
|
773
801
|
let consumer = process.cwd();
|
|
774
802
|
let dryRun = false;
|
|
775
803
|
let from: string | undefined;
|
|
804
|
+
let ref: string | undefined;
|
|
776
805
|
let checkFlag = false;
|
|
777
806
|
let apply = false;
|
|
778
807
|
|
|
@@ -796,15 +825,21 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
|
796
825
|
from = nextOptionValue(argv, index);
|
|
797
826
|
index += 1;
|
|
798
827
|
break;
|
|
828
|
+
case '--ref':
|
|
829
|
+
ref = nextOptionValue(argv, index);
|
|
830
|
+
index += 1;
|
|
831
|
+
break;
|
|
832
|
+
case '--help':
|
|
833
|
+
case '-h':
|
|
834
|
+
command = setCommand(command, 'help');
|
|
835
|
+
break;
|
|
799
836
|
default:
|
|
800
837
|
command = setCommand(command, commandFromArg(arg));
|
|
801
838
|
}
|
|
802
839
|
}
|
|
803
840
|
|
|
804
|
-
// `--check` doubles as the legacy spelling of the check command and as the
|
|
805
|
-
// explicit (default) mode of `github`.
|
|
806
841
|
if (checkFlag && command !== 'github') {
|
|
807
|
-
|
|
842
|
+
throw new Error('--check is only valid with the github command');
|
|
808
843
|
}
|
|
809
844
|
if (apply && command !== 'github') {
|
|
810
845
|
throw new Error('--apply is only valid with the github command');
|
|
@@ -812,12 +847,16 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
|
|
|
812
847
|
if (apply && checkFlag) {
|
|
813
848
|
throw new Error('github accepts exactly one of --check or --apply');
|
|
814
849
|
}
|
|
850
|
+
if (ref !== undefined && command !== 'init' && command !== 'sync') {
|
|
851
|
+
throw new Error('--ref is only valid with the init and sync commands');
|
|
852
|
+
}
|
|
815
853
|
|
|
816
854
|
return {
|
|
817
|
-
command
|
|
855
|
+
command,
|
|
818
856
|
consumer: resolve(consumer),
|
|
819
857
|
dryRun,
|
|
820
858
|
from,
|
|
859
|
+
ref,
|
|
821
860
|
apply,
|
|
822
861
|
};
|
|
823
862
|
};
|
|
@@ -837,18 +876,19 @@ const runCheckCommand = async (consumer: string): Promise<boolean> => {
|
|
|
837
876
|
const runInitCommand = async (
|
|
838
877
|
consumer: string,
|
|
839
878
|
from: string | undefined,
|
|
879
|
+
ref: string | undefined,
|
|
840
880
|
): Promise<void> => {
|
|
841
881
|
// Refuse before cloning upstream: re-initializing skips the lock, so it
|
|
842
882
|
// would silently overwrite local canonical edits and orphan files that
|
|
843
883
|
// upstream deleted (they leave the lock and no future sync removes them).
|
|
844
884
|
if (existsSync(join(consumer, 'sync-standards.lock'))) {
|
|
845
885
|
console.error(
|
|
846
|
-
'standards: already initialized (sync-standards.lock exists). Use `
|
|
886
|
+
'standards: already initialized (sync-standards.lock exists). Use `bun standards sync` to update.',
|
|
847
887
|
);
|
|
848
888
|
process.exitCode = 1;
|
|
849
889
|
return;
|
|
850
890
|
}
|
|
851
|
-
const source = resolveSource(from ?? DEFAULT_UPSTREAM);
|
|
891
|
+
const source = resolveSource(from ?? DEFAULT_UPSTREAM, ref);
|
|
852
892
|
try {
|
|
853
893
|
const manifest = await loadManifest(
|
|
854
894
|
join(source.dir, 'sync-standards.json'),
|
|
@@ -862,12 +902,13 @@ const runInitCommand = async (
|
|
|
862
902
|
const runSyncCommand = async (
|
|
863
903
|
consumer: string,
|
|
864
904
|
from: string | undefined,
|
|
905
|
+
ref: string | undefined,
|
|
865
906
|
dryRun: boolean,
|
|
866
907
|
): Promise<void> => {
|
|
867
908
|
const consumerManifest = await loadManifest(
|
|
868
909
|
join(consumer, 'sync-standards.json'),
|
|
869
910
|
);
|
|
870
|
-
const source = resolveSource(from ?? consumerManifest.upstream);
|
|
911
|
+
const source = resolveSource(from ?? consumerManifest.upstream, ref);
|
|
871
912
|
try {
|
|
872
913
|
const manifest = await loadManifest(
|
|
873
914
|
join(source.dir, 'sync-standards.json'),
|
|
@@ -879,10 +920,22 @@ const runSyncCommand = async (
|
|
|
879
920
|
};
|
|
880
921
|
|
|
881
922
|
const main = async (): Promise<void> => {
|
|
882
|
-
const { command, consumer, dryRun, from, apply } = parseArgs(
|
|
923
|
+
const { command, consumer, dryRun, from, ref, apply } = parseArgs(
|
|
883
924
|
process.argv.slice(2),
|
|
884
925
|
);
|
|
885
926
|
|
|
927
|
+
if (command === undefined) {
|
|
928
|
+
console.error('standards: a command is required\n');
|
|
929
|
+
console.error(USAGE);
|
|
930
|
+
process.exitCode = 1;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (command === 'help') {
|
|
935
|
+
console.log(USAGE);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
886
939
|
if (command === 'check') {
|
|
887
940
|
if (!(await runCheckCommand(consumer))) {
|
|
888
941
|
process.exitCode = 1;
|
|
@@ -908,12 +961,12 @@ const main = async (): Promise<void> => {
|
|
|
908
961
|
}
|
|
909
962
|
|
|
910
963
|
if (command === 'init') {
|
|
911
|
-
await runInitCommand(consumer, from);
|
|
964
|
+
await runInitCommand(consumer, from, ref);
|
|
912
965
|
return;
|
|
913
966
|
}
|
|
914
967
|
|
|
915
968
|
if (command === 'sync') {
|
|
916
|
-
await runSyncCommand(consumer, from, dryRun);
|
|
969
|
+
await runSyncCommand(consumer, from, ref, dryRun);
|
|
917
970
|
}
|
|
918
971
|
};
|
|
919
972
|
|
package/src/github-api.ts
CHANGED
|
@@ -99,7 +99,7 @@ export const loadDeclared = async (
|
|
|
99
99
|
return {
|
|
100
100
|
merged: null,
|
|
101
101
|
problems: [
|
|
102
|
-
`${CANONICAL_SETTINGS_FILE} not found; run \`
|
|
102
|
+
`${CANONICAL_SETTINGS_FILE} not found; run \`bun standards sync\` first`,
|
|
103
103
|
],
|
|
104
104
|
};
|
|
105
105
|
}
|
package/src/github-commands.ts
CHANGED
|
@@ -78,7 +78,7 @@ export const runGithubCheck = async (consumer: string): Promise<boolean> => {
|
|
|
78
78
|
if (problems.length > 0) {
|
|
79
79
|
reportProblems(problems);
|
|
80
80
|
console.error(
|
|
81
|
-
'Converge with `
|
|
81
|
+
'Converge with `bun standards github --apply` (admin auth), or fix the declaration.',
|
|
82
82
|
);
|
|
83
83
|
return false;
|
|
84
84
|
}
|