@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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/canra.js +101 -21
  3. 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>` | Publish a spec (needs a token) |
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 || "").padEnd(nameWidth).slice(0, nameWidth);
310
- const version = (spec.version || "").padEnd(versionWidth).slice(0, versionWidth);
311
- const desc = (spec.description || spec.title || "").slice(0, descWidth).replace(/\n/g, " ");
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 filePath = args.find((a, i) => !a.startsWith("--") && (ownerAt < 0 || i !== ownerAt + 1));
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canra/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "CLI for Canra, the registry of machine-readable specs: fetch, search, and publish specs.",
5
5
  "keywords": [
6
6
  "canra",