@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.
- package/README.md +4 -2
- package/canra.js +39 -15
- 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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|