@canra/cli 0.5.0 → 0.7.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 (3) hide show
  1. package/README.md +4 -2
  2. package/canra.js +39 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -24,8 +24,8 @@ canra search json
24
24
  | `canra search <query>` | Search the registry |
25
25
  | `canra info <owner>/<name>` | Show a spec's metadata |
26
26
  | `canra fetch <owner>/<name>[@version]` | Download a spec (latest or a pinned version), verify its SHA-256, and save it to `.canra/specs/<owner>/<name>/` |
27
- | `canra outdated [--json]` | Check fetched specs for newer versions; exits 1 if any is outdated |
28
- | `canra update [<owner>/<name>]` | Re-fetch the latest version of every outdated spec (or the named one) |
27
+ | `canra outdated [--json]` | Check fetched specs for newer versions; exits 1 if any is outdated or quarantined |
28
+ | `canra update [<owner>/<name>]` | Re-fetch the latest version of every outdated spec (or the named one); exits 1 if any spec is quarantined or fails to update |
29
29
  | `canra remove <owner>/<name>` | Delete a fetched spec from `.canra/specs/` |
30
30
  | `canra login` | Save a registry URL and API token |
31
31
  | `canra whoami` | Show which account your saved token belongs to |
@@ -34,6 +34,8 @@ canra search json
34
34
  | `canra logout` | Remove saved credentials |
35
35
  | `canra version` / `canra help` | Version and usage |
36
36
 
37
+ `<owner>` is a user or organization name in any case: `Canra/json` and `canra/json` are the same spec, saved under the lowercase owner in `.canra/specs/`.
38
+
37
39
  ## Vendoring specs
38
40
 
39
41
  Commit `.canra/` to source control. The saved specs are the record of which spec versions your project implemented (a version is exact and immutable). After `canra update`, `git diff .canra/` shows what changed in each spec.
package/canra.js CHANGED
@@ -89,11 +89,11 @@ async function fetchRegistry(url, opts) {
89
89
 
90
90
  // --- Spec references ---
91
91
 
92
- // Mirror the server's naming rules (route-utils.js USERNAME_RE, specs-api.js
92
+ // Mirror the server's naming rules (username.js USERNAME_RE and grandfathered names, specs-api.js
93
93
  // NAME_RE). Owner and name become local path segments under .canra/specs/, so
94
94
  // anything path-like ("..", ".", backslashes) must be rejected here — the CLI is
95
95
  // meant to be driven by agents whose arguments may come from untrusted content.
96
- const OWNER_RE = /^[a-z][a-z0-9_-]{1,38}[a-z0-9]$/;
96
+ const OWNER_RE = /^(?:(?=.{1,39}$)[a-z0-9]+(?:-[a-z0-9]+)*|[a-z][a-z0-9_-]{1,38}[a-z0-9])$/i;
97
97
  const NAME_RE = /^[a-z][a-z0-9-]*$/;
98
98
  const SUGGESTION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
99
99
 
@@ -197,7 +197,7 @@ function refuseSymlinks(paths) {
197
197
 
198
198
  function localSpecPaths(owner, name) {
199
199
  const specsDir = path.join(LOCAL_DIR, "specs");
200
- const ownerDir = path.join(specsDir, owner);
200
+ const ownerDir = path.join(specsDir, owner.toLowerCase());
201
201
  return [LOCAL_DIR, specsDir, ownerDir, path.join(ownerDir, name)];
202
202
  }
203
203
 
@@ -207,8 +207,10 @@ function frontmatterFunctionCount(text) {
207
207
  return list ? (list[1].match(/"(?:[^"\\]|\\.)*"/g) || []).length : 0;
208
208
  }
209
209
 
210
+ // Owner directories are lowercased everywhere (here, localSpecPaths, deleteLocalSpec) so Canra/json and
211
+ // canra/json are one local spec on case-sensitive and case-insensitive filesystems alike.
210
212
  function localSpecDir(owner, name) {
211
- return path.join(LOCAL_DIR, "specs", owner, name);
213
+ return path.join(LOCAL_DIR, "specs", owner.toLowerCase(), name);
212
214
  }
213
215
 
214
216
  async function downloadSpec(registry, { owner, name, version }) {
@@ -643,6 +645,20 @@ function needsUpdate(row) {
643
645
  return row.status === "outdated" || (row.status === "current" && Boolean(row.moved_to));
644
646
  }
645
647
 
648
+ function isQuarantined(row) {
649
+ return row.status === "quarantined";
650
+ }
651
+
652
+ function warnQuarantined(results) {
653
+ const quarantined = results.filter(isQuarantined);
654
+ for (const r of quarantined) {
655
+ console.warn(
656
+ `Warning: ${printable(r.owner)}/${printable(r.name)} is quarantined on the registry and cannot be updated. Remove it with canra remove, or wait for the registry to restore it.`,
657
+ );
658
+ }
659
+ return quarantined.length;
660
+ }
661
+
646
662
  async function cmdOutdated(args) {
647
663
  const asJson = args.includes("--json");
648
664
  const refs = scanLocalSpecs();
@@ -653,19 +669,22 @@ async function cmdOutdated(args) {
653
669
 
654
670
  const results = await checkRemote(getRegistry(), refs);
655
671
  const outdated = results.filter(needsUpdate).length;
672
+ const quarantined = results.filter(isQuarantined).length;
656
673
 
674
+ // --json prints no warnings, so the output stays machine-readable; the exit code carries the quarantine signal.
657
675
  if (asJson) {
658
676
  console.log(JSON.stringify(results, null, 2));
659
677
  } else {
660
678
  printOutdatedTable(results);
661
- if (results.some(needsUpdate)) {
679
+ warnQuarantined(results);
680
+ if (outdated > 0) {
662
681
  console.log(`\n${outdated} outdated spec${outdated !== 1 ? "s" : ""}.`);
663
682
  console.log("Run canra update, then git diff .canra/ to review the changes.");
664
- } else {
683
+ } else if (quarantined === 0) {
665
684
  console.log("\nAll specs are up to date.");
666
685
  }
667
686
  }
668
- if (outdated > 0) process.exitCode = 1;
687
+ if (outdated > 0 || quarantined > 0) process.exitCode = 1;
669
688
  }
670
689
 
671
690
  function deleteLocalSpec(owner, name) {
@@ -674,7 +693,7 @@ function deleteLocalSpec(owner, name) {
674
693
  if (!dir.startsWith(specsRoot + path.sep)) throw new Error(`unexpected local path for ${owner}/${name}`);
675
694
  refuseSymlinks(localSpecPaths(owner, name));
676
695
  fs.rmSync(dir, { recursive: true, force: true });
677
- const ownerDir = path.join(LOCAL_DIR, "specs", owner);
696
+ const ownerDir = path.join(LOCAL_DIR, "specs", owner.toLowerCase());
678
697
  if (fs.readdirSync(ownerDir).length === 0) fs.rmdirSync(ownerDir);
679
698
  }
680
699
 
@@ -710,7 +729,7 @@ async function cmdUpdate(specArg) {
710
729
 
711
730
  let refs = scanLocalSpecs();
712
731
  if (only) {
713
- refs = refs.filter((r) => r.owner === only.owner && r.name === only.name);
732
+ refs = refs.filter((r) => r.owner.toLowerCase() === only.owner.toLowerCase() && r.name === only.name);
714
733
  if (refs.length === 0) {
715
734
  console.error(`Error: ${only.owner}/${only.name} is not fetched locally. Run \`canra fetch\` first.`);
716
735
  process.exit(1);
@@ -722,9 +741,12 @@ async function cmdUpdate(specArg) {
722
741
  }
723
742
 
724
743
  const registry = getRegistry();
725
- const targets = (await checkRemote(registry, refs)).filter(needsUpdate);
744
+ const results = await checkRemote(registry, refs);
745
+ const quarantined = warnQuarantined(results);
746
+ const targets = results.filter(needsUpdate);
726
747
  if (targets.length === 0) {
727
- console.log("All specs are up to date.");
748
+ if (quarantined === 0) console.log("All specs are up to date.");
749
+ else process.exitCode = 1;
728
750
  return;
729
751
  }
730
752
 
@@ -741,7 +763,7 @@ async function cmdUpdate(specArg) {
741
763
  if (!OWNER_RE.test(toOwner) || (target && target[2] !== row.name)) {
742
764
  throw new Error(`unexpected move target '${printable(row.moved_to)}'`);
743
765
  }
744
- const moving = toOwner !== row.owner;
766
+ const moving = toOwner.toLowerCase() !== row.owner.toLowerCase();
745
767
  if (moving && fs.existsSync(localSpecDir(toOwner, row.name))) {
746
768
  throw new Error(`${toOwner}/${row.name} is already fetched locally; remove one of the two copies`);
747
769
  }
@@ -759,7 +781,7 @@ async function cmdUpdate(specArg) {
759
781
  }
760
782
  }
761
783
  if (updated > 0) console.log("\nRun git diff .canra/ to review the changes.");
762
- if (failed > 0) process.exitCode = 1;
784
+ if (failed > 0 || quarantined > 0) process.exitCode = 1;
763
785
  }
764
786
 
765
787
  function cmdHelp() {
@@ -784,9 +806,11 @@ Commands:
784
806
  whoami Print the user your saved token authenticates as
785
807
  info <owner>/<name> Print spec metadata
786
808
  outdated [--json] Check .canra/specs/ for newer versions; exits 1 if
787
- any spec is outdated; --json prints the results
809
+ any spec is outdated or quarantined; --json prints
810
+ the results
788
811
  update [<owner>/<name>] Re-fetch the latest version of every outdated spec
789
- (or just the named one); review with git diff .canra/
812
+ (or just the named one); review with git diff .canra/;
813
+ exits 1 if any spec is quarantined or fails
790
814
  remove <owner>/<name> Delete a fetched spec from .canra/specs/
791
815
  help Print this help message
792
816
  version Print CLI version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canra/cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "CLI for Canra, the registry of machine-readable specs: fetch, search, and publish specs.",
5
5
  "keywords": [
6
6
  "canra",