@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.
- package/README.md +31 -3
- package/dist/account.d.ts +76 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +211 -0
- package/dist/account.js.map +1 -0
- package/dist/auth.d.ts +40 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +121 -0
- package/dist/auth.js.map +1 -0
- package/dist/canvas/api.d.ts +35 -1
- package/dist/canvas/api.d.ts.map +1 -1
- package/dist/canvas/api.js +57 -1
- package/dist/canvas/api.js.map +1 -1
- package/dist/canvas/claim.d.ts +3 -0
- package/dist/canvas/claim.d.ts.map +1 -0
- package/dist/canvas/claim.js +84 -0
- package/dist/canvas/claim.js.map +1 -0
- package/dist/canvas/delete.js +3 -3
- package/dist/canvas/delete.js.map +1 -1
- package/dist/canvas/registry.d.ts +8 -2
- package/dist/canvas/registry.d.ts.map +1 -1
- package/dist/canvas/registry.js +15 -2
- package/dist/canvas/registry.js.map +1 -1
- package/dist/canvas/write.d.ts +11 -2
- package/dist/canvas/write.d.ts.map +1 -1
- package/dist/canvas/write.js +27 -8
- package/dist/canvas/write.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +369 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/canvas.d.ts.map +1 -1
- package/dist/commands/canvas.js +162 -21
- package/dist/commands/canvas.js.map +1 -1
- package/dist/commands/render.d.ts +1 -1
- package/dist/commands/render.d.ts.map +1 -1
- package/dist/commands/render.js +6 -3
- package/dist/commands/render.js.map +1 -1
- package/dist/config-home.d.ts +4 -0
- package/dist/config-home.d.ts.map +1 -0
- package/dist/config-home.js +38 -0
- package/dist/config-home.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/install.d.ts +5 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +83 -0
- package/dist/install.js.map +1 -0
- package/dist/skill-content.generated.d.ts +2 -2
- package/dist/skill-content.generated.d.ts.map +1 -1
- package/dist/skill-content.generated.js +2 -2
- package/dist/skill-content.generated.js.map +1 -1
- package/dist/slug.d.ts +7 -0
- package/dist/slug.d.ts.map +1 -0
- package/dist/slug.js +23 -0
- package/dist/slug.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +7 -0
- package/dist/terminal.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace.d.ts +4 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +26 -4
- package/dist/workspace.js.map +1 -1
- package/package.json +3 -3
- package/src/account.ts +302 -0
- package/src/auth.ts +183 -0
- package/src/canvas/api.ts +114 -2
- package/src/canvas/claim.ts +127 -0
- package/src/canvas/delete.ts +3 -3
- package/src/canvas/registry.ts +24 -2
- package/src/canvas/write.ts +37 -6
- package/src/cli.ts +8 -2
- package/src/commands/auth.ts +560 -0
- package/src/commands/canvas.ts +238 -18
- package/src/commands/render.ts +7 -3
- package/src/config-home.ts +45 -0
- package/src/errors.ts +5 -1
- package/src/install.ts +107 -0
- package/src/skill-content.generated.ts +2 -2
- package/src/slug.ts +26 -0
- package/src/terminal.ts +14 -0
- package/src/version.ts +1 -1
- 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
|
+
};
|
package/src/canvas/delete.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
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)
|
package/src/canvas/registry.ts
CHANGED
|
@@ -36,7 +36,8 @@ export const mintWriteToken = (): string =>
|
|
|
36
36
|
|
|
37
37
|
const Entry = z.object({
|
|
38
38
|
name: z.string(),
|
|
39
|
-
|
|
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;
|
package/src/canvas/write.ts
CHANGED
|
@@ -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]
|
|
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
|
-
|
|
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
|
-
|
|
154
|
+
|
|
124
155
|
const pending = registered.entry.pending;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
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:
|