@coldtea/pr-lens-cli 0.7.0 → 0.8.1

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 (90) hide show
  1. package/README.md +31 -3
  2. package/dist/account.d.ts +76 -0
  3. package/dist/account.d.ts.map +1 -0
  4. package/dist/account.js +211 -0
  5. package/dist/account.js.map +1 -0
  6. package/dist/auth.d.ts +40 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/auth.js +121 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/canvas/api.d.ts +35 -1
  11. package/dist/canvas/api.d.ts.map +1 -1
  12. package/dist/canvas/api.js +57 -1
  13. package/dist/canvas/api.js.map +1 -1
  14. package/dist/canvas/claim.d.ts +3 -0
  15. package/dist/canvas/claim.d.ts.map +1 -0
  16. package/dist/canvas/claim.js +84 -0
  17. package/dist/canvas/claim.js.map +1 -0
  18. package/dist/canvas/delete.js +3 -3
  19. package/dist/canvas/delete.js.map +1 -1
  20. package/dist/canvas/registry.d.ts +8 -2
  21. package/dist/canvas/registry.d.ts.map +1 -1
  22. package/dist/canvas/registry.js +15 -2
  23. package/dist/canvas/registry.js.map +1 -1
  24. package/dist/canvas/write.d.ts +11 -2
  25. package/dist/canvas/write.d.ts.map +1 -1
  26. package/dist/canvas/write.js +27 -8
  27. package/dist/canvas/write.js.map +1 -1
  28. package/dist/cli.d.ts.map +1 -1
  29. package/dist/cli.js +8 -2
  30. package/dist/cli.js.map +1 -1
  31. package/dist/commands/auth.d.ts +9 -0
  32. package/dist/commands/auth.d.ts.map +1 -0
  33. package/dist/commands/auth.js +369 -0
  34. package/dist/commands/auth.js.map +1 -0
  35. package/dist/commands/canvas.d.ts.map +1 -1
  36. package/dist/commands/canvas.js +162 -21
  37. package/dist/commands/canvas.js.map +1 -1
  38. package/dist/commands/render.d.ts +1 -1
  39. package/dist/commands/render.d.ts.map +1 -1
  40. package/dist/commands/render.js +6 -3
  41. package/dist/commands/render.js.map +1 -1
  42. package/dist/config-home.d.ts +4 -0
  43. package/dist/config-home.d.ts.map +1 -0
  44. package/dist/config-home.js +38 -0
  45. package/dist/config-home.js.map +1 -0
  46. package/dist/errors.d.ts +1 -1
  47. package/dist/errors.d.ts.map +1 -1
  48. package/dist/errors.js.map +1 -1
  49. package/dist/install.d.ts +5 -0
  50. package/dist/install.d.ts.map +1 -0
  51. package/dist/install.js +83 -0
  52. package/dist/install.js.map +1 -0
  53. package/dist/skill-content.generated.d.ts +1 -1
  54. package/dist/skill-content.generated.d.ts.map +1 -1
  55. package/dist/skill-content.generated.js +1 -1
  56. package/dist/skill-content.generated.js.map +1 -1
  57. package/dist/slug.d.ts +7 -0
  58. package/dist/slug.d.ts.map +1 -0
  59. package/dist/slug.js +23 -0
  60. package/dist/slug.js.map +1 -0
  61. package/dist/terminal.d.ts +5 -0
  62. package/dist/terminal.d.ts.map +1 -1
  63. package/dist/terminal.js +7 -0
  64. package/dist/terminal.js.map +1 -1
  65. package/dist/version.d.ts +1 -1
  66. package/dist/version.js +1 -1
  67. package/dist/workspace.d.ts +4 -0
  68. package/dist/workspace.d.ts.map +1 -1
  69. package/dist/workspace.js +26 -4
  70. package/dist/workspace.js.map +1 -1
  71. package/package.json +3 -3
  72. package/src/account.ts +302 -0
  73. package/src/auth.ts +183 -0
  74. package/src/canvas/api.ts +114 -2
  75. package/src/canvas/claim.ts +127 -0
  76. package/src/canvas/delete.ts +3 -3
  77. package/src/canvas/registry.ts +24 -2
  78. package/src/canvas/write.ts +37 -6
  79. package/src/cli.ts +8 -2
  80. package/src/commands/auth.ts +560 -0
  81. package/src/commands/canvas.ts +238 -18
  82. package/src/commands/render.ts +7 -3
  83. package/src/config-home.ts +45 -0
  84. package/src/errors.ts +5 -1
  85. package/src/install.ts +107 -0
  86. package/src/skill-content.generated.ts +1 -1
  87. package/src/slug.ts +26 -0
  88. package/src/terminal.ts +14 -0
  89. package/src/version.ts +1 -1
  90. package/src/workspace.ts +30 -4
package/src/canvas/api.ts CHANGED
@@ -85,6 +85,8 @@ const Refusal = z.discriminatedUnion("code", [
85
85
  retryAt: z.string(),
86
86
  }),
87
87
  z.object({ code: z.literal("TOO_LARGE"), message: z.string() }),
88
+ z.object({ code: z.literal("UNAUTHENTICATED"), message: z.string() }),
89
+ z.object({ code: z.literal("ALREADY_OWNED"), message: z.string() }),
88
90
  ]);
89
91
 
90
92
  type Request = {
@@ -93,6 +95,8 @@ type Request = {
93
95
  /** Undefined when minting, so a 404 there is not blamed on a canvas. */
94
96
  canvas: string | undefined;
95
97
  token?: string;
98
+ /** Lets a later sign-in claim what this machine pushed. */
99
+ install?: string;
96
100
  ifMatch?: number;
97
101
  body?: unknown;
98
102
  };
@@ -112,6 +116,25 @@ const unavailable = (
112
116
  details,
113
117
  );
114
118
 
119
+ /** Untrusted server content, so printable and short. */
120
+ const LOCATION_SHOWN = 200;
121
+
122
+ /** Printing the target turns an `http://` for `https://` typo into a fix. */
123
+ const redirected = (api: string, response: Response): PrLensCliError => {
124
+ const location = response.headers
125
+ .get("location")
126
+ ?.replace(/[^\x20-\x7e]/g, "")
127
+ .slice(0, LOCATION_SHOWN);
128
+
129
+ return unavailable(
130
+ api,
131
+ response.status,
132
+ location === undefined || location === ""
133
+ ? "the canvas API answers at the address it is given, and this one redirects"
134
+ : `the canvas API answers at the address it is given; this one points at ${location}, so pass that as --api`,
135
+ );
136
+ };
137
+
115
138
  const parseJson = (text: string): unknown => {
116
139
  try {
117
140
  return JSON.parse(text);
@@ -170,6 +193,21 @@ const refusal = (
170
193
  `${hostOf(api)} is rate limiting this client until ${error.retryAt}`,
171
194
  error.message,
172
195
  );
196
+ case "UNAUTHENTICATED":
197
+ return new PrLensCliError(
198
+ "AUTH_REQUIRED",
199
+ `${hostOf(api)} did not accept this sign-in`,
200
+ [error.message, "pr-lens auth login signs this machine in again"].join(
201
+ "\n",
202
+ ),
203
+ );
204
+ case "ALREADY_OWNED":
205
+ // The caller holds the write token, so naming the case leaks nothing.
206
+ return new PrLensCliError(
207
+ "CANVAS_OWNED",
208
+ `${request.canvas ?? "that canvas"} belongs to another account on ${hostOf(api)}`,
209
+ "the first claim wins, and somebody else's landed first",
210
+ );
173
211
  case "INVALID_REQUEST":
174
212
  case "TOO_LARGE":
175
213
  return unavailable(api, status, error.message);
@@ -191,6 +229,9 @@ const call = async <T>(
191
229
  if (request.token !== undefined)
192
230
  headers.authorization = `Bearer ${request.token}`;
193
231
 
232
+ if (request.install !== undefined)
233
+ headers["x-pr-lens-install"] = request.install;
234
+
194
235
  if (request.ifMatch !== undefined)
195
236
  headers["if-match"] = String(request.ifMatch);
196
237
 
@@ -200,6 +241,9 @@ const call = async <T>(
200
241
  method: request.method,
201
242
  headers,
202
243
  body: request.body === undefined ? undefined : JSON.stringify(request.body),
244
+ // A cross-origin redirect keeps every header but `authorization`, which
245
+ // would leak the install id to another host.
246
+ redirect: "manual",
203
247
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
204
248
  }).catch(() => {
205
249
  // The runtime's message names addresses and internals; keep it out.
@@ -210,6 +254,10 @@ const call = async <T>(
210
254
  );
211
255
  });
212
256
 
257
+ // A 3xx body is not an error envelope.
258
+ if (response.status >= 300 && response.status < 400)
259
+ throw redirected(api, response);
260
+
213
261
  // A body can fail after the headers arrived.
214
262
  const text = await response.text().catch(() => {
215
263
  throw unavailable(
@@ -234,8 +282,17 @@ const call = async <T>(
234
282
 
235
283
  const canvasPath = (id: string): string => `/api/canvas/${id}`;
236
284
 
237
- export const mintCanvas = (api: string): Promise<Minted> =>
238
- call(api, { method: "POST", path: "/api/canvas", canvas: undefined }, Minted);
285
+ /** The bearer is how CI attributes a mint: a runner's install id is never linked. */
286
+ export const mintCanvas = (
287
+ api: string,
288
+ install: string | undefined,
289
+ token: string | undefined,
290
+ ): Promise<Minted> =>
291
+ call(
292
+ api,
293
+ { method: "POST", path: "/api/canvas", canvas: undefined, install, token },
294
+ Minted,
295
+ );
239
296
 
240
297
  export const fetchCanvas = async (
241
298
  api: string,
@@ -322,3 +379,58 @@ export const deleteCanvas = (
322
379
  { method: "DELETE", path: canvasPath(id), canvas: id, token },
323
380
  z.object({ id: z.literal(id), deleted: z.literal(true) }),
324
381
  );
382
+
383
+ /** An unreadable revision must not read as an empty canvas. */
384
+ const Preview = z.discriminatedUnion("type", [
385
+ z.object({ type: z.literal("drawn"), title: z.string().min(1) }),
386
+ z.object({ type: z.literal("not_drawn") }),
387
+ z.object({ type: z.literal("unreadable") }),
388
+ ]);
389
+
390
+ const Owned = z.object({
391
+ id: z.string(),
392
+ rev: z.number().int(),
393
+ preview: Preview,
394
+ });
395
+
396
+ const Owning = z.object({ canvases: z.array(Owned) });
397
+
398
+ export type CanvasPreview = z.infer<typeof Preview>;
399
+ export type OwnedCanvas = z.infer<typeof Owned>;
400
+
401
+ /**
402
+ * Carries no write tokens: the app keeps only their hashes. A store without
403
+ * accounts 404s here, which reads as unavailable, not as a missing canvas.
404
+ */
405
+ export const listOwnedCanvases = (
406
+ api: string,
407
+ token: string,
408
+ ): Promise<OwnedCanvas[]> =>
409
+ call(
410
+ api,
411
+ { method: "GET", path: "/api/canvases", canvas: undefined, token },
412
+ Owning,
413
+ ).then(({ canvases }) => canvases);
414
+
415
+ /**
416
+ * The account token takes the header, so the write tokens go in the body.
417
+ * The caller mints the next token so a lost answer can be replayed.
418
+ */
419
+ export const claimCanvas = (
420
+ api: string,
421
+ id: string,
422
+ accountToken: string,
423
+ writeToken: string,
424
+ nextWriteToken: string,
425
+ ): Promise<Rotated> =>
426
+ call(
427
+ api,
428
+ {
429
+ method: "POST",
430
+ path: `${canvasPath(id)}/claim`,
431
+ canvas: id,
432
+ token: accountToken,
433
+ body: { writeToken, nextWriteToken },
434
+ },
435
+ Rotated,
436
+ );
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The write token is the only proof, and it rides in every edit link ever
3
+ * shared, so claiming retires it in the same step.
4
+ */
5
+ import { requireToken } from "../auth.js";
6
+ import type { Terminal } from "../terminal.js";
7
+ import { claimCanvas, listOwnedCanvases } from "./api.js";
8
+ import { expectOne, parseOptions } from "../args.js";
9
+ import { PrLensCliError } from "../errors.js";
10
+ import { readApi, requireWriteToken, settlePendingRotation } from "./write.js";
11
+ import {
12
+ findCanvas,
13
+ mintWriteToken,
14
+ readRegistry,
15
+ REGISTRY_PATH,
16
+ updateRegistry,
17
+ } from "./registry.js";
18
+
19
+ const unfinishedClaim = (error: PrLensCliError): PrLensCliError =>
20
+ new PrLensCliError(
21
+ error.code,
22
+ `${error.message}; the claim may have landed`,
23
+ [
24
+ error.details,
25
+ "run pr-lens canvas claim again: the token it minted is kept, and a second run finishes the claim",
26
+ ]
27
+ .filter((line) => line !== undefined && line !== "")
28
+ .join("\n"),
29
+ );
30
+
31
+ export const claimCommand = async (
32
+ args: readonly string[],
33
+ terminal: Terminal,
34
+ env: Record<string, string | undefined>,
35
+ ): Promise<void> => {
36
+ const { values, positionals } = parseOptions(args, {
37
+ api: { type: "string" },
38
+ });
39
+ const ref = expectOne(positionals, "the id or name of a canvas to claim");
40
+
41
+ const api = readApi(values.api, env);
42
+ const registry = await readRegistry();
43
+ const selected = findCanvas(registry, ref);
44
+
45
+ // Checked first, so a signed-out machine never costs a rotation.
46
+ const account = await requireToken(env, api);
47
+
48
+ // Also finishes a claim whose answer was lost: its token is still pending.
49
+ const settled = await settlePendingRotation(api, selected, terminal, env);
50
+ const id = settled.id;
51
+
52
+ // Unlike push and delete, ownership cannot stand in: claiming is how a
53
+ // canvas gets an owner.
54
+ requireWriteToken(settled);
55
+
56
+ // The app treats an owner's claim as a replay and rotates, so running the
57
+ // command twice would retire a token for nothing.
58
+ const mine = await listOwnedCanvases(api, account);
59
+ if (mine.some((canvas) => canvas.id === id))
60
+ throw new PrLensCliError(
61
+ "CANVAS_OWNED",
62
+ `${id} is already yours on ${new URL(api).host}`,
63
+ "claiming it again would retire its write token for nothing",
64
+ );
65
+
66
+ terminal.err("! Claiming rotates this canvas's write token.");
67
+ terminal.err(" Edit links you have already shared will stop working.");
68
+
69
+ // Saved first, so a lost answer does not lose the token.
70
+ const nextToken = mintWriteToken();
71
+ let saved = false;
72
+ await updateRegistry((current) => {
73
+ const entry = current.canvases[id];
74
+ if (entry === undefined || entry.api !== api) return;
75
+ current.canvases[id] = { ...entry, pending: nextToken };
76
+ saved = true;
77
+ }, terminal);
78
+ if (!saved)
79
+ throw new PrLensCliError(
80
+ "CANVAS_UNREGISTERED",
81
+ `${id} is no longer in ${REGISTRY_PATH}`,
82
+ );
83
+
84
+ const claimed = await claimCanvas(
85
+ api,
86
+ id,
87
+ account,
88
+ requireWriteToken(settled),
89
+ nextToken,
90
+ ).catch(async (error: unknown) => {
91
+ if (!(error instanceof PrLensCliError)) throw error;
92
+ // Only these two mean the app did not rotate. Anything else may have
93
+ // landed, so the pending token stays for the next run to finish.
94
+ if (error.code !== "CANVAS_UNKNOWN" && error.code !== "CANVAS_OWNED")
95
+ throw unfinishedClaim(error);
96
+
97
+ await updateRegistry((current) => {
98
+ const entry = current.canvases[id];
99
+ if (entry === undefined || entry.pending !== nextToken || entry.api !== api)
100
+ return;
101
+ current.canvases[id] = { ...entry, pending: undefined };
102
+ }, terminal);
103
+ throw error;
104
+ });
105
+
106
+ // A different token pending by now belongs to a later rotation.
107
+ await updateRegistry((current) => {
108
+ const entry = current.canvases[id];
109
+ if (entry === undefined || entry.pending !== nextToken || entry.api !== api)
110
+ return;
111
+ current.canvases[id] = {
112
+ ...entry,
113
+ writeToken: nextToken,
114
+ pending: undefined,
115
+ };
116
+ }, terminal);
117
+
118
+ const view = new URL(claimed.editUrl);
119
+ view.hash = "";
120
+ terminal.out(`✓ ${view.href} is yours`);
121
+ terminal.out(
122
+ ` Its new write token is in ${REGISTRY_PATH}, so this checkout can push to it.`,
123
+ );
124
+ terminal.out(
125
+ " Edit links you shared before this still open the page, and no longer edit it.",
126
+ );
127
+ };
@@ -3,7 +3,7 @@ import { usageError } from "../errors.js";
3
3
  import type { Terminal } from "../terminal.js";
4
4
  import { parseOptions, readString } from "../args.js";
5
5
  import { selectCanvas, readRegistry, updateRegistry } from "./registry.js";
6
- import { readApi, requireWriteToken, settlePendingRotation } from "./write.js";
6
+ import { readApi, settlePendingRotation, writeCredential } from "./write.js";
7
7
 
8
8
  export const deleteCommand = async (
9
9
  args: readonly string[],
@@ -26,8 +26,8 @@ export const deleteCommand = async (
26
26
  const ref = readString(values.canvas, "canvas");
27
27
  const selected = selectCanvas(registry, ref);
28
28
 
29
- const target = await settlePendingRotation(api, selected, terminal);
30
- await deleteCanvas(api, target.id, requireWriteToken(target));
29
+ const target = await settlePendingRotation(api, selected, terminal, env);
30
+ await deleteCanvas(api, target.id, await writeCredential(target, env, api));
31
31
 
32
32
  await updateRegistry((current) => {
33
33
  if (current.canvases[target.id]?.api === api)
@@ -36,7 +36,8 @@ export const mintWriteToken = (): string =>
36
36
 
37
37
  const Entry = z.object({
38
38
  name: z.string(),
39
- source: z.string(),
39
+ /** Absent for a canvas recorded from an edit link: no local file wrote it. */
40
+ source: z.string().optional(),
40
41
  /** Another app's 404 says nothing about this entry. */
41
42
  api: z.string(),
42
43
  /** Absent for a canvas pulled by its view link. */
@@ -446,7 +447,7 @@ export const findBySource = (
446
447
  ): Registered | undefined => {
447
448
  const key = sourceKey(path);
448
449
  const matching = entries(registry).filter(
449
- ({ entry }) => sourceKey(entry.source) === key,
450
+ ({ entry }) => entry.source !== undefined && sourceKey(entry.source) === key,
450
451
  );
451
452
  const [only, ...more] = matching;
452
453
  if (more.length > 0)
@@ -458,6 +459,27 @@ export const findBySource = (
458
459
  return only;
459
460
  };
460
461
 
462
+ /**
463
+ * Fallback after the path: a pull and the render after it write two files,
464
+ * and only one can be the recorded path. A path match wins, since a title
465
+ * can be edited.
466
+ */
467
+ export const findByTitle = (
468
+ registry: CanvasRegistry,
469
+ title: string,
470
+ ): Registered | undefined => {
471
+ const matching = entries(registry).filter(({ entry }) => entry.name === title);
472
+ const [only, ...more] = matching;
473
+
474
+ if (more.length > 0)
475
+ throw usageError(
476
+ `${matching.length} canvases are named ${JSON.stringify(title)}`,
477
+ `pass --canvas <id>: ${matching.map(describe).join(", ")}`,
478
+ );
479
+
480
+ return only;
481
+ };
482
+
461
483
  export const onlyCanvas = (registry: CanvasRegistry): Registered => {
462
484
  const all = entries(registry);
463
485
  const [only, ...more] = all;
@@ -1,4 +1,5 @@
1
1
  import { readString } from "../args.js";
2
+ import { readToken } from "../auth.js";
2
3
  import { rotateCanvas } from "./api.js";
3
4
  import type { Terminal } from "../terminal.js";
4
5
  import { PrLensCliError, usageError } from "../errors.js";
@@ -7,11 +8,15 @@ import { updateRegistry, type Registered } from "./registry.js";
7
8
  export const DEFAULT_API = "https://prlens.dev";
8
9
  export const API_ENV = "PR_LENS_API_URL";
9
10
 
11
+ /**
12
+ * `||` for the environment: a workflow cannot omit an `env` key, so it sets
13
+ * it to "". The flag keeps `??`, so a typed `--api ""` is reported.
14
+ */
10
15
  export const readApi = (
11
16
  value: unknown,
12
17
  env: Record<string, string | undefined>,
13
18
  ): string => {
14
- const api = readString(value, "api") ?? env[API_ENV] ?? DEFAULT_API;
19
+ const api = readString(value, "api") ?? (env[API_ENV] || DEFAULT_API);
15
20
  try {
16
21
  new URL(api);
17
22
  } catch {
@@ -42,6 +47,30 @@ export const requireWriteToken = ({ id, entry }: Registered): string => {
42
47
  );
43
48
  };
44
49
 
50
+ /**
51
+ * The write token first: it works signed out and on stores without accounts.
52
+ * The account token only works if the app finds the account owns the canvas.
53
+ */
54
+ export const writeCredential = async (
55
+ registered: Registered,
56
+ env: Record<string, string | undefined>,
57
+ api: string,
58
+ ): Promise<string> => {
59
+ if (registered.entry.writeToken !== undefined) return registered.entry.writeToken;
60
+
61
+ const account = await readToken(env, api);
62
+ if (account !== undefined) return account;
63
+
64
+ throw new PrLensCliError(
65
+ "CANVAS_UNREGISTERED",
66
+ `this checkout can read ${registered.id} but holds no write token for it`,
67
+ [
68
+ "pr-lens auth login signs this machine in, and an owner needs no token",
69
+ "or pull its edit link, the one with #w= at the end, and the token comes with it",
70
+ ].join("\n"),
71
+ );
72
+ };
73
+
45
74
  const unfinishedRotation = (error: PrLensCliError): PrLensCliError =>
46
75
  new PrLensCliError(
47
76
  error.code,
@@ -60,11 +89,12 @@ export const settleRotation = async (
60
89
  { id, entry }: Registered,
61
90
  nextToken: string,
62
91
  terminal: Terminal,
92
+ env: Record<string, string | undefined>,
63
93
  ): Promise<{ registered: Registered; editUrl: string }> => {
64
94
  const rotated = await rotateCanvas(
65
95
  api,
66
96
  id,
67
- requireWriteToken({ id, entry }),
97
+ await writeCredential({ id, entry }, env, api),
68
98
  nextToken,
69
99
  ).catch(async (error: unknown) => {
70
100
  if (!(error instanceof PrLensCliError)) throw error;
@@ -118,11 +148,12 @@ export const settlePendingRotation = async (
118
148
  api: string,
119
149
  registered: Registered,
120
150
  terminal: Terminal,
151
+ env: Record<string, string | undefined>,
121
152
  ): Promise<Registered> => {
122
153
  requireSameApi(api, registered);
123
- requireWriteToken(registered);
154
+
124
155
  const pending = registered.entry.pending;
125
- return pending === undefined
126
- ? registered
127
- : (await settleRotation(api, registered, pending, terminal)).registered;
156
+ if (pending === undefined) return registered;
157
+
158
+ return (await settleRotation(api, registered, pending, terminal, env)).registered;
128
159
  };
package/src/cli.ts CHANGED
@@ -3,6 +3,7 @@ import { assertNever } from "@coldtea/pr-lens-schema";
3
3
  import { CLI_VERSION } from "./version.js";
4
4
  import type { Terminal } from "./terminal.js";
5
5
  import { formatError, PrLensCliError } from "./errors.js";
6
+ import { authCommand, USAGE as AUTH_USAGE } from "./commands/auth.js";
6
7
  import { skillCommand, USAGE as SKILL_USAGE } from "./commands/skill.js";
7
8
  import { exportCommand, USAGE as EXPORT_USAGE } from "./commands/export.js";
8
9
  import { canvasCommand, USAGE as CANVAS_USAGE } from "./commands/canvas.js";
@@ -11,7 +12,7 @@ import { analyzeCommand, USAGE as ANALYZE_USAGE } from "./commands/analyze.js";
11
12
  import { commentCommand, USAGE as COMMENT_USAGE } from "./commands/comment.js";
12
13
  import { USAGE as VALIDATE_USAGE, validateCommand } from "./commands/validate.js";
13
14
 
14
- const COMMANDS = ["analyze", "render", "comment", "validate", "export", "canvas", "skill"] as const;
15
+ const COMMANDS = ["analyze", "render", "comment", "validate", "export", "canvas", "auth", "skill"] as const;
15
16
  type CommandName = (typeof COMMANDS)[number];
16
17
 
17
18
  const isCommand = (value: string): value is CommandName =>
@@ -24,7 +25,8 @@ const HELP = `pr-lens — review what actually matters
24
25
  pr-lens comment --graph --manifest the pull request comment, as markdown
25
26
  pr-lens validate <file...> any PR Lens document, checked against the contract
26
27
  pr-lens export <graph.json> the merged state, as a map worth committing
27
- pr-lens canvas push | pull | rotate | delete that document, kept on prlens.dev as a page and an embed
28
+ pr-lens canvas list | push | pull | claim | rotate | delete that document, kept on prlens.dev as a page and an embed
29
+ pr-lens auth login | status | logout sign this machine in, so the canvases it pushes are yours
28
30
  pr-lens skill diagram instructions for coding agents
29
31
 
30
32
  pr-lens <command> --help what a command takes
@@ -47,6 +49,8 @@ const usageFor = (command: CommandName): string => {
47
49
  return EXPORT_USAGE;
48
50
  case "canvas":
49
51
  return CANVAS_USAGE;
52
+ case "auth":
53
+ return AUTH_USAGE;
50
54
  case "skill":
51
55
  return SKILL_USAGE;
52
56
  default:
@@ -73,6 +77,8 @@ const dispatch = (
73
77
  return exportCommand(args, terminal);
74
78
  case "canvas":
75
79
  return canvasCommand(args, terminal, env);
80
+ case "auth":
81
+ return authCommand(args, terminal, env);
76
82
  case "skill":
77
83
  return skillCommand(args, terminal);
78
84
  default: