@coldtea/pr-lens-cli 0.6.1 → 0.8.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 (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 +2 -2
  54. package/dist/skill-content.generated.d.ts.map +1 -1
  55. package/dist/skill-content.generated.js +2 -2
  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 +2 -2
  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 +31 -4
@@ -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: