@canra/cli 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/README.md +2 -1
- package/canra.js +101 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,8 @@ canra search json
|
|
|
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 |
|
|
32
|
-
| `canra publish <file.spec.md
|
|
32
|
+
| `canra publish <file.spec.md> [--accepts <suggestion-id>]` | Publish a spec (needs a token); `--accepts` marks that suggestion on the spec accepted |
|
|
33
|
+
| `canra suggest <owner>/<name> --title "..." [--body "..."] [--file <path.spec.md>]` | Suggest a change to a spec you don't own (needs a token); `--file` attaches a full edited spec, which the owner reviews as a diff |
|
|
33
34
|
| `canra logout` | Remove saved credentials |
|
|
34
35
|
| `canra version` / `canra help` | Version and usage |
|
|
35
36
|
|
package/canra.js
CHANGED
|
@@ -95,6 +95,7 @@ async function fetchRegistry(url, opts) {
|
|
|
95
95
|
// meant to be driven by agents whose arguments may come from untrusted content.
|
|
96
96
|
const OWNER_RE = /^[a-z][a-z0-9_-]{1,38}[a-z0-9]$/;
|
|
97
97
|
const NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
98
|
+
const SUGGESTION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
98
99
|
|
|
99
100
|
// Specs are namespaced by (owner, name), like GitHub repos. Parse an
|
|
100
101
|
// "<owner>/<name>[@version]" argument, exiting with usage help on anything else.
|
|
@@ -306,9 +307,13 @@ async function cmdSearch(query) {
|
|
|
306
307
|
console.log(`${"-".repeat(nameWidth)} ${"-".repeat(versionWidth)} ${"-".repeat(descWidth)}`);
|
|
307
308
|
|
|
308
309
|
for (const spec of specs) {
|
|
309
|
-
const name = (spec.name
|
|
310
|
-
const version = (spec.version
|
|
311
|
-
const desc = (
|
|
310
|
+
const name = printable(spec.name).padEnd(nameWidth).slice(0, nameWidth);
|
|
311
|
+
const version = printable(spec.version).padEnd(versionWidth).slice(0, versionWidth);
|
|
312
|
+
const desc = printable(
|
|
313
|
+
String(spec.description || spec.title || "")
|
|
314
|
+
.replace(/\s+/g, " ")
|
|
315
|
+
.trim(),
|
|
316
|
+
).slice(0, descWidth);
|
|
312
317
|
console.log(`${name} ${version} ${desc}`);
|
|
313
318
|
}
|
|
314
319
|
|
|
@@ -320,13 +325,21 @@ async function cmdPublish(args) {
|
|
|
320
325
|
// belong to instead of your own scope.
|
|
321
326
|
const ownerAt = args.indexOf("--owner");
|
|
322
327
|
const owner = ownerAt >= 0 ? args[ownerAt + 1] : null;
|
|
323
|
-
const
|
|
328
|
+
const acceptsAt = args.indexOf("--accepts");
|
|
329
|
+
const accepts = acceptsAt >= 0 ? args[acceptsAt + 1] : null;
|
|
330
|
+
const filePath = args.find(
|
|
331
|
+
(a, i) => !a.startsWith("--") && (ownerAt < 0 || i !== ownerAt + 1) && (acceptsAt < 0 || i !== acceptsAt + 1),
|
|
332
|
+
);
|
|
324
333
|
if (ownerAt >= 0 && (!owner || !OWNER_RE.test(owner))) {
|
|
325
334
|
console.error("Error: --owner needs a valid organization name");
|
|
326
335
|
process.exit(1);
|
|
327
336
|
}
|
|
337
|
+
if (acceptsAt >= 0 && (!accepts || accepts.startsWith("--") || !SUGGESTION_ID_RE.test(accepts))) {
|
|
338
|
+
console.error("Error: --accepts needs a suggestion id");
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
328
341
|
if (!filePath) {
|
|
329
|
-
console.error("Usage: canra publish <file> [--owner <org>]");
|
|
342
|
+
console.error("Usage: canra publish <file> [--owner <org>] [--accepts <suggestion-id>]");
|
|
330
343
|
process.exit(1);
|
|
331
344
|
}
|
|
332
345
|
|
|
@@ -343,6 +356,7 @@ async function cmdPublish(args) {
|
|
|
343
356
|
const content = fs.readFileSync(filePath, "utf8");
|
|
344
357
|
const registry = getRegistry();
|
|
345
358
|
const headers = authHeaders();
|
|
359
|
+
if (accepts) headers["X-Suggestion"] = accepts;
|
|
346
360
|
|
|
347
361
|
const target = owner ? `${registry}/api/specs?owner=${encodeURIComponent(owner)}` : `${registry}/api/specs`;
|
|
348
362
|
const res = await fetchRegistry(target, {
|
|
@@ -352,12 +366,71 @@ async function cmdPublish(args) {
|
|
|
352
366
|
});
|
|
353
367
|
if (!res.ok) {
|
|
354
368
|
const body = await res.text();
|
|
355
|
-
console.error(`Error: ${res.status} - ${body}`);
|
|
369
|
+
console.error(`Error: ${res.status} - ${printable(body)}`);
|
|
356
370
|
process.exit(1);
|
|
357
371
|
}
|
|
358
372
|
|
|
359
373
|
const spec = await res.json();
|
|
360
|
-
console.log(`Published ${spec.name}@${spec.version}`);
|
|
374
|
+
console.log(`Published ${printable(spec.name)}@${printable(spec.version)}`);
|
|
375
|
+
if (accepts) {
|
|
376
|
+
const id = printable(spec.suggestion?.id ?? accepts);
|
|
377
|
+
if (spec.suggestion?.status === "accepted") console.log(`Suggestion ${id} accepted`);
|
|
378
|
+
else console.warn(`Warning: Suggestion ${id} was not accepted (not open or not for this spec)`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function cmdSuggest(args) {
|
|
383
|
+
const usage = 'canra suggest <owner>/<name> --title "..." [--body "..."] [--file <path.spec.md>]';
|
|
384
|
+
const flags = { "--title": null, "--body": null, "--file": null };
|
|
385
|
+
let target = null;
|
|
386
|
+
for (let i = 0; i < args.length; i++) {
|
|
387
|
+
if (Object.hasOwn(flags, args[i])) {
|
|
388
|
+
const value = args[i + 1];
|
|
389
|
+
if (value === undefined || value.startsWith("--")) {
|
|
390
|
+
console.error(`Error: ${args[i]} needs a value`);
|
|
391
|
+
console.error(`Usage: ${usage}`);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
flags[args[i]] = value;
|
|
395
|
+
i++;
|
|
396
|
+
} else if (!args[i].startsWith("--") && target === null) {
|
|
397
|
+
target = args[i];
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const title = flags["--title"]?.trim();
|
|
401
|
+
if (!target || !title) {
|
|
402
|
+
console.error(`Usage: ${usage}`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
const ref = parseSpecRef(target, usage);
|
|
406
|
+
if (ref.version !== null) {
|
|
407
|
+
console.error("Error: suggestions apply to the latest version; leave off @<version>");
|
|
408
|
+
console.error(`Usage: ${usage}`);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const payload = { title, body: flags["--body"] ?? "" };
|
|
413
|
+
if (flags["--file"]) {
|
|
414
|
+
if (!fs.existsSync(flags["--file"])) {
|
|
415
|
+
console.error(`Error: file not found: ${flags["--file"]}`);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
payload.content = fs.readFileSync(flags["--file"], "utf8");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const registry = getRegistry();
|
|
422
|
+
const res = await fetchRegistry(`${specApiUrl(registry, ref)}/suggestions`, {
|
|
423
|
+
method: "POST",
|
|
424
|
+
body: JSON.stringify(payload),
|
|
425
|
+
headers: { ...authHeaders(), "Content-Type": "application/json" },
|
|
426
|
+
});
|
|
427
|
+
if (!res.ok) {
|
|
428
|
+
const body = await res.text();
|
|
429
|
+
console.error(`Error: ${res.status} - ${printable(body)}`);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
const created = await res.json();
|
|
433
|
+
console.log(`Suggestion filed: ${printable(registry)}${printable(created.url)}`);
|
|
361
434
|
}
|
|
362
435
|
|
|
363
436
|
async function cmdLogin() {
|
|
@@ -419,9 +492,9 @@ async function cmdWhoami() {
|
|
|
419
492
|
process.exit(1);
|
|
420
493
|
}
|
|
421
494
|
const me = await res.json();
|
|
422
|
-
console.log(me.user);
|
|
495
|
+
console.log(printable(me.user));
|
|
423
496
|
console.log(` Registry: ${registry}`);
|
|
424
|
-
if (me.scopes?.length) console.log(` Scopes: ${me.scopes.join(", ")}`);
|
|
497
|
+
if (me.scopes?.length) console.log(` Scopes: ${printable(me.scopes.join(", "))}`);
|
|
425
498
|
}
|
|
426
499
|
|
|
427
500
|
async function cmdInfo(specArg) {
|
|
@@ -446,16 +519,16 @@ async function cmdInfo(specArg) {
|
|
|
446
519
|
}
|
|
447
520
|
|
|
448
521
|
const spec = await res.json();
|
|
449
|
-
console.log(`${ref.owner}/${ref.name}@${spec.version}`);
|
|
450
|
-
if (spec.title) console.log(` Title: ${spec.title}`);
|
|
451
|
-
if (spec.status) console.log(` Status: ${spec.status}`);
|
|
452
|
-
if (spec.difficulty) console.log(` Difficulty: ${spec.difficulty}`);
|
|
453
|
-
if (spec.tags?.length) console.log(` Tags: ${spec.tags.join(", ")}`);
|
|
454
|
-
if (spec.functions?.length) console.log(` Functions: ${spec.functions.join(", ")}`);
|
|
455
|
-
if (spec.types?.length) console.log(` Types: ${spec.types.join(", ")}`);
|
|
456
|
-
if (spec.test_vectors_inline) console.log(` Test vectors: ${spec.test_vectors_inline} inline`);
|
|
457
|
-
if (spec.owner) console.log(` Owner: ${spec.owner}`);
|
|
458
|
-
if (spec.updated_at) console.log(` Updated: ${spec.updated_at}`);
|
|
522
|
+
console.log(`${ref.owner}/${ref.name}@${printable(spec.version)}`);
|
|
523
|
+
if (spec.title) console.log(` Title: ${printable(spec.title)}`);
|
|
524
|
+
if (spec.status) console.log(` Status: ${printable(spec.status)}`);
|
|
525
|
+
if (spec.difficulty) console.log(` Difficulty: ${printable(spec.difficulty)}`);
|
|
526
|
+
if (spec.tags?.length) console.log(` Tags: ${printable(spec.tags.join(", "))}`);
|
|
527
|
+
if (spec.functions?.length) console.log(` Functions: ${printable(spec.functions.join(", "))}`);
|
|
528
|
+
if (spec.types?.length) console.log(` Types: ${printable(spec.types.join(", "))}`);
|
|
529
|
+
if (spec.test_vectors_inline) console.log(` Test vectors: ${printable(spec.test_vectors_inline)} inline`);
|
|
530
|
+
if (spec.owner) console.log(` Owner: ${printable(spec.owner)}`);
|
|
531
|
+
if (spec.updated_at) console.log(` Updated: ${printable(spec.updated_at)}`);
|
|
459
532
|
}
|
|
460
533
|
|
|
461
534
|
// The server rejects more than MAX_REFS (website/outdated.js) entries per request.
|
|
@@ -699,9 +772,13 @@ Commands:
|
|
|
699
772
|
Download a spec (latest, or a pinned version) and
|
|
700
773
|
save it, hash-verified, to .canra/specs/
|
|
701
774
|
search <query> Search registry, print formatted results
|
|
702
|
-
publish <file> [--owner <org>]
|
|
775
|
+
publish <file> [--owner <org>] [--accepts <suggestion-id>]
|
|
703
776
|
Publish a spec file (requires auth token); --owner
|
|
704
|
-
publishes into an organization you belong to
|
|
777
|
+
publishes into an organization you belong to;
|
|
778
|
+
--accepts marks that suggestion accepted
|
|
779
|
+
suggest <owner>/<name> --title "..." [--body "..."] [--file <path.spec.md>]
|
|
780
|
+
Suggest a change to a spec (requires auth token);
|
|
781
|
+
--file proposes an edited spec as a diff
|
|
705
782
|
login Prompt for registry URL and token
|
|
706
783
|
logout Remove saved credentials
|
|
707
784
|
whoami Print the user your saved token authenticates as
|
|
@@ -738,6 +815,9 @@ async function main() {
|
|
|
738
815
|
case "publish":
|
|
739
816
|
await cmdPublish(args.slice(1));
|
|
740
817
|
break;
|
|
818
|
+
case "suggest":
|
|
819
|
+
await cmdSuggest(args.slice(1));
|
|
820
|
+
break;
|
|
741
821
|
case "login":
|
|
742
822
|
await cmdLogin();
|
|
743
823
|
break;
|