@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidvornholt/standards",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Bootstrap, synchronize, and validate davidvornholt/standards consumers",
5
5
  "license": "MIT",
6
6
  "repository": {
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
- const resolveSource = (src: string): Source => {
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
- execFileSync('git', ['clone', '--depth', '1', '--branch', 'main', url, dir], {
185
- stdio: 'ignore',
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 `just sync-standards`, or move your change upstream.',
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
- command = setCommand(command, 'check');
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: command ?? 'sync',
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 `just sync-standards` to update.',
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 \`just sync-standards\` first`,
102
+ `${CANONICAL_SETTINGS_FILE} not found; run \`bun standards sync\` first`,
103
103
  ],
104
104
  };
105
105
  }
@@ -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 `just sync-standards github --apply` (admin auth), or fix the declaration.',
81
+ 'Converge with `bun standards github --apply` (admin auth), or fix the declaration.',
82
82
  );
83
83
  return false;
84
84
  }