@davidvornholt/standards 0.4.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.ts +50 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidvornholt/standards",
3
- "version": "0.4.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
@@ -81,6 +81,7 @@ type CliOptions = {
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
@@ -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"');
@@ -748,6 +757,7 @@ Commands:
748
757
  Options:
749
758
  --dir <path> Consumer directory to operate on (default: current directory)
750
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)
751
761
  --dry-run Preview a sync without writing anything
752
762
  --check With github: compare live settings to the declaration (default)
753
763
  --apply With github: converge the live repository (needs admin auth)`;
@@ -791,6 +801,7 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
791
801
  let consumer = process.cwd();
792
802
  let dryRun = false;
793
803
  let from: string | undefined;
804
+ let ref: string | undefined;
794
805
  let checkFlag = false;
795
806
  let apply = false;
796
807
 
@@ -814,6 +825,10 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
814
825
  from = nextOptionValue(argv, index);
815
826
  index += 1;
816
827
  break;
828
+ case '--ref':
829
+ ref = nextOptionValue(argv, index);
830
+ index += 1;
831
+ break;
817
832
  case '--help':
818
833
  case '-h':
819
834
  command = setCommand(command, 'help');
@@ -832,12 +847,16 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
832
847
  if (apply && checkFlag) {
833
848
  throw new Error('github accepts exactly one of --check or --apply');
834
849
  }
850
+ if (ref !== undefined && command !== 'init' && command !== 'sync') {
851
+ throw new Error('--ref is only valid with the init and sync commands');
852
+ }
835
853
 
836
854
  return {
837
855
  command,
838
856
  consumer: resolve(consumer),
839
857
  dryRun,
840
858
  from,
859
+ ref,
841
860
  apply,
842
861
  };
843
862
  };
@@ -857,6 +876,7 @@ const runCheckCommand = async (consumer: string): Promise<boolean> => {
857
876
  const runInitCommand = async (
858
877
  consumer: string,
859
878
  from: string | undefined,
879
+ ref: string | undefined,
860
880
  ): Promise<void> => {
861
881
  // Refuse before cloning upstream: re-initializing skips the lock, so it
862
882
  // would silently overwrite local canonical edits and orphan files that
@@ -868,7 +888,7 @@ const runInitCommand = async (
868
888
  process.exitCode = 1;
869
889
  return;
870
890
  }
871
- const source = resolveSource(from ?? DEFAULT_UPSTREAM);
891
+ const source = resolveSource(from ?? DEFAULT_UPSTREAM, ref);
872
892
  try {
873
893
  const manifest = await loadManifest(
874
894
  join(source.dir, 'sync-standards.json'),
@@ -882,12 +902,13 @@ const runInitCommand = async (
882
902
  const runSyncCommand = async (
883
903
  consumer: string,
884
904
  from: string | undefined,
905
+ ref: string | undefined,
885
906
  dryRun: boolean,
886
907
  ): Promise<void> => {
887
908
  const consumerManifest = await loadManifest(
888
909
  join(consumer, 'sync-standards.json'),
889
910
  );
890
- const source = resolveSource(from ?? consumerManifest.upstream);
911
+ const source = resolveSource(from ?? consumerManifest.upstream, ref);
891
912
  try {
892
913
  const manifest = await loadManifest(
893
914
  join(source.dir, 'sync-standards.json'),
@@ -899,7 +920,7 @@ const runSyncCommand = async (
899
920
  };
900
921
 
901
922
  const main = async (): Promise<void> => {
902
- const { command, consumer, dryRun, from, apply } = parseArgs(
923
+ const { command, consumer, dryRun, from, ref, apply } = parseArgs(
903
924
  process.argv.slice(2),
904
925
  );
905
926
 
@@ -940,12 +961,12 @@ const main = async (): Promise<void> => {
940
961
  }
941
962
 
942
963
  if (command === 'init') {
943
- await runInitCommand(consumer, from);
964
+ await runInitCommand(consumer, from, ref);
944
965
  return;
945
966
  }
946
967
 
947
968
  if (command === 'sync') {
948
- await runSyncCommand(consumer, from, dryRun);
969
+ await runSyncCommand(consumer, from, ref, dryRun);
949
970
  }
950
971
  };
951
972