@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.
- package/README.md +2 -1
- package/canra.js +80 -4
- 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.
|
|
@@ -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
|
|
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;
|