@canra/cli 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 (3) hide show
  1. package/README.md +2 -1
  2. package/canra.js +80 -4
  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.
@@ -324,13 +325,21 @@ async function cmdPublish(args) {
324
325
  // belong to instead of your own scope.
325
326
  const ownerAt = args.indexOf("--owner");
326
327
  const owner = ownerAt >= 0 ? args[ownerAt + 1] : null;
327
- 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
+ );
328
333
  if (ownerAt >= 0 && (!owner || !OWNER_RE.test(owner))) {
329
334
  console.error("Error: --owner needs a valid organization name");
330
335
  process.exit(1);
331
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
+ }
332
341
  if (!filePath) {
333
- console.error("Usage: canra publish <file> [--owner <org>]");
342
+ console.error("Usage: canra publish <file> [--owner <org>] [--accepts <suggestion-id>]");
334
343
  process.exit(1);
335
344
  }
336
345
 
@@ -347,6 +356,7 @@ async function cmdPublish(args) {
347
356
  const content = fs.readFileSync(filePath, "utf8");
348
357
  const registry = getRegistry();
349
358
  const headers = authHeaders();
359
+ if (accepts) headers["X-Suggestion"] = accepts;
350
360
 
351
361
  const target = owner ? `${registry}/api/specs?owner=${encodeURIComponent(owner)}` : `${registry}/api/specs`;
352
362
  const res = await fetchRegistry(target, {
@@ -362,6 +372,65 @@ async function cmdPublish(args) {
362
372
 
363
373
  const spec = await res.json();
364
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)}`);
365
434
  }
366
435
 
367
436
  async function cmdLogin() {
@@ -703,9 +772,13 @@ Commands:
703
772
  Download a spec (latest, or a pinned version) and
704
773
  save it, hash-verified, to .canra/specs/
705
774
  search <query> Search registry, print formatted results
706
- publish <file> [--owner <org>]
775
+ publish <file> [--owner <org>] [--accepts <suggestion-id>]
707
776
  Publish a spec file (requires auth token); --owner
708
- 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
709
782
  login Prompt for registry URL and token
710
783
  logout Remove saved credentials
711
784
  whoami Print the user your saved token authenticates as
@@ -742,6 +815,9 @@ async function main() {
742
815
  case "publish":
743
816
  await cmdPublish(args.slice(1));
744
817
  break;
818
+ case "suggest":
819
+ await cmdSuggest(args.slice(1));
820
+ break;
745
821
  case "login":
746
822
  await cmdLogin();
747
823
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canra/cli",
3
- "version": "0.4.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",