@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.
- 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 +1 -1
- package/dist/skill-content.generated.d.ts.map +1 -1
- package/dist/skill-content.generated.js +1 -1
- 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 +1 -1
- package/src/slug.ts +26 -0
- package/src/terminal.ts +14 -0
- package/src/version.ts +1 -1
- 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
|
-
|
|
238
|
-
|
|
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
|
+
};
|
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:
|