@coldtea/pr-lens-cli 0.3.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 +13 -4
- package/dist/canvas/api.d.ts +5 -1
- package/dist/canvas/api.d.ts.map +1 -1
- package/dist/canvas/api.js +3 -2
- package/dist/canvas/api.js.map +1 -1
- package/dist/canvas/delete.d.ts +3 -0
- package/dist/canvas/delete.d.ts.map +1 -0
- package/dist/canvas/delete.js +25 -0
- package/dist/canvas/delete.js.map +1 -0
- package/dist/canvas/registry.d.ts +1 -0
- package/dist/canvas/registry.d.ts.map +1 -1
- package/dist/canvas/registry.js +3 -2
- package/dist/canvas/registry.js.map +1 -1
- package/dist/canvas/write.d.ts +15 -0
- package/dist/canvas/write.d.ts.map +1 -0
- package/dist/canvas/write.js +82 -0
- package/dist/canvas/write.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/canvas.d.ts.map +1 -1
- package/dist/commands/canvas.js +22 -89
- package/dist/commands/canvas.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +3 -2
- package/dist/commands/validate.js.map +1 -1
- package/dist/skill-content.generated.d.ts +5 -5
- package/dist/skill-content.generated.d.ts.map +1 -1
- package/dist/skill-content.generated.js +5 -5
- package/dist/skill-content.generated.js.map +1 -1
- package/dist/snapshot.d.ts +3 -2
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +4 -2
- package/dist/snapshot.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/canvas/api.ts +15 -3
- package/src/canvas/delete.ts +39 -0
- package/src/canvas/registry.ts +8 -3
- package/src/canvas/write.ts +128 -0
- package/src/cli.ts +2 -1
- package/src/commands/canvas.ts +22 -128
- package/src/commands/validate.ts +4 -2
- package/src/skill-content.generated.ts +5 -5
- package/src/snapshot.ts +4 -2
- package/src/version.ts +1 -1
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coldtea/pr-lens-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The PR Lens command line: turn a pull request diff into a schema-valid graph with your own model key, render it, and compose the comment.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Coldtea AI",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"yaml": "^2.8.1",
|
|
42
42
|
"zod": "^4.4.3",
|
|
43
|
-
"@coldtea/pr-lens-renderer": "^0.
|
|
44
|
-
"@coldtea/pr-lens-schema": "^0.1
|
|
43
|
+
"@coldtea/pr-lens-renderer": "^0.2.3",
|
|
44
|
+
"@coldtea/pr-lens-schema": "^0.2.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^20.19.0",
|
package/src/canvas/api.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import {
|
|
2
3
|
assertNever,
|
|
3
4
|
safeParseGraphDoc,
|
|
4
5
|
type GraphDoc,
|
|
5
6
|
} from "@coldtea/pr-lens-schema";
|
|
6
|
-
|
|
7
|
-
import { PrLensCliError } from "../errors.js";
|
|
7
|
+
|
|
8
8
|
import { CLI_VERSION } from "../version.js";
|
|
9
|
+
import { PrLensCliError } from "../errors.js";
|
|
9
10
|
|
|
10
11
|
const REQUEST_TIMEOUT_MS = 60_000;
|
|
11
12
|
|
|
@@ -87,7 +88,7 @@ const Refusal = z.discriminatedUnion("code", [
|
|
|
87
88
|
]);
|
|
88
89
|
|
|
89
90
|
type Request = {
|
|
90
|
-
method: "GET" | "POST" | "PUT";
|
|
91
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
91
92
|
path: string;
|
|
92
93
|
/** Undefined when minting, so a 404 there is not blamed on a canvas. */
|
|
93
94
|
canvas: string | undefined;
|
|
@@ -310,3 +311,14 @@ export const rotateCanvas = (
|
|
|
310
311
|
},
|
|
311
312
|
Rotated,
|
|
312
313
|
);
|
|
314
|
+
|
|
315
|
+
export const deleteCanvas = (
|
|
316
|
+
api: string,
|
|
317
|
+
id: string,
|
|
318
|
+
token: string,
|
|
319
|
+
): Promise<{ id: string; deleted: true }> =>
|
|
320
|
+
call(
|
|
321
|
+
api,
|
|
322
|
+
{ method: "DELETE", path: canvasPath(id), canvas: id, token },
|
|
323
|
+
z.object({ id: z.literal(id), deleted: z.literal(true) }),
|
|
324
|
+
);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { deleteCanvas } from "./api.js";
|
|
2
|
+
import { usageError } from "../errors.js";
|
|
3
|
+
import type { Terminal } from "../terminal.js";
|
|
4
|
+
import { parseOptions, readString } from "../args.js";
|
|
5
|
+
import { selectCanvas, readRegistry, updateRegistry } from "./registry.js";
|
|
6
|
+
import { readApi, requireWriteToken, settlePendingRotation } from "./write.js";
|
|
7
|
+
|
|
8
|
+
export const deleteCommand = async (
|
|
9
|
+
args: readonly string[],
|
|
10
|
+
terminal: Terminal,
|
|
11
|
+
env: Record<string, string | undefined>,
|
|
12
|
+
): Promise<void> => {
|
|
13
|
+
const { values, positionals } = parseOptions(args, {
|
|
14
|
+
canvas: { type: "string" },
|
|
15
|
+
api: { type: "string" },
|
|
16
|
+
});
|
|
17
|
+
if (positionals.length > 0)
|
|
18
|
+
throw usageError(
|
|
19
|
+
`delete takes no positional arguments, got ${positionals.join(" ")}`,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const api = readApi(values.api, env);
|
|
23
|
+
|
|
24
|
+
const registry = await readRegistry();
|
|
25
|
+
|
|
26
|
+
const ref = readString(values.canvas, "canvas");
|
|
27
|
+
const selected = selectCanvas(registry, ref);
|
|
28
|
+
|
|
29
|
+
const target = await settlePendingRotation(api, selected, terminal);
|
|
30
|
+
await deleteCanvas(api, target.id, requireWriteToken(target));
|
|
31
|
+
|
|
32
|
+
await updateRegistry((current) => {
|
|
33
|
+
if (current.canvases[target.id]?.api === api)
|
|
34
|
+
delete current.canvases[target.id];
|
|
35
|
+
}, terminal);
|
|
36
|
+
terminal.out(
|
|
37
|
+
`✓ deleted ${target.id} from ${api}; local graph and SVG files kept`,
|
|
38
|
+
);
|
|
39
|
+
};
|
package/src/canvas/registry.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import {
|
|
2
3
|
access,
|
|
3
4
|
link,
|
|
@@ -9,14 +10,12 @@ import {
|
|
|
9
10
|
readFile,
|
|
10
11
|
writeFile,
|
|
11
12
|
} from "node:fs/promises";
|
|
12
|
-
import { z } from "zod";
|
|
13
13
|
import type { Stats } from "node:fs";
|
|
14
14
|
import { randomBytes } from "node:crypto";
|
|
15
|
+
import { assertNever } from "@coldtea/pr-lens-schema";
|
|
15
16
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
16
17
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
17
18
|
|
|
18
|
-
import { assertNever } from "@coldtea/pr-lens-schema";
|
|
19
|
-
|
|
20
19
|
import { git } from "../git.js";
|
|
21
20
|
import type { Terminal } from "../terminal.js";
|
|
22
21
|
import { PrLensCliError, usageError } from "../errors.js";
|
|
@@ -477,3 +476,9 @@ export const onlyCanvas = (registry: CanvasRegistry): Registered => {
|
|
|
477
476
|
|
|
478
477
|
return only;
|
|
479
478
|
};
|
|
479
|
+
|
|
480
|
+
export const selectCanvas = (
|
|
481
|
+
registry: CanvasRegistry,
|
|
482
|
+
ref: string | undefined,
|
|
483
|
+
): Registered =>
|
|
484
|
+
ref === undefined ? onlyCanvas(registry) : findCanvas(registry, ref);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readString } from "../args.js";
|
|
2
|
+
import { rotateCanvas } from "./api.js";
|
|
3
|
+
import type { Terminal } from "../terminal.js";
|
|
4
|
+
import { PrLensCliError, usageError } from "../errors.js";
|
|
5
|
+
import { updateRegistry, type Registered } from "./registry.js";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_API = "https://prlens.dev";
|
|
8
|
+
export const API_ENV = "PR_LENS_API_URL";
|
|
9
|
+
|
|
10
|
+
export const readApi = (
|
|
11
|
+
value: unknown,
|
|
12
|
+
env: Record<string, string | undefined>,
|
|
13
|
+
): string => {
|
|
14
|
+
const api = readString(value, "api") ?? env[API_ENV] ?? DEFAULT_API;
|
|
15
|
+
try {
|
|
16
|
+
new URL(api);
|
|
17
|
+
} catch {
|
|
18
|
+
throw usageError(`--api needs a URL, got ${JSON.stringify(api)}`);
|
|
19
|
+
}
|
|
20
|
+
return api.replace(/\/+$/, "");
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Another app's 404 says nothing about this entry, so it must not change it. */
|
|
24
|
+
export const requireSameApi = (
|
|
25
|
+
api: string,
|
|
26
|
+
{ id, entry }: Registered,
|
|
27
|
+
): void => {
|
|
28
|
+
if (entry.api === api) return;
|
|
29
|
+
throw new PrLensCliError(
|
|
30
|
+
"CANVAS_UNREGISTERED",
|
|
31
|
+
`${id} is registered against ${entry.api}, not ${api}`,
|
|
32
|
+
`pass --api ${entry.api}, or push the document without --canvas to mint a canvas here`,
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const requireWriteToken = ({ id, entry }: Registered): string => {
|
|
37
|
+
if (entry.writeToken !== undefined) return entry.writeToken;
|
|
38
|
+
throw new PrLensCliError(
|
|
39
|
+
"CANVAS_UNREGISTERED",
|
|
40
|
+
`this checkout can read ${id} but holds no write token for it`,
|
|
41
|
+
"pull its edit link, the one with #w= at the end, and the token comes with it",
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const unfinishedRotation = (error: PrLensCliError): PrLensCliError =>
|
|
46
|
+
new PrLensCliError(
|
|
47
|
+
error.code,
|
|
48
|
+
`${error.message}; the rotation is not finished`,
|
|
49
|
+
[
|
|
50
|
+
error.details,
|
|
51
|
+
"run pr-lens canvas rotate again to finish it: the new token is kept until the app confirms it",
|
|
52
|
+
]
|
|
53
|
+
.filter((line) => line !== undefined && line !== "")
|
|
54
|
+
.join("\n"),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
/** Asking again with the same pair is safe: the app answers "rotated" once the token is on record. */
|
|
58
|
+
export const settleRotation = async (
|
|
59
|
+
api: string,
|
|
60
|
+
{ id, entry }: Registered,
|
|
61
|
+
nextToken: string,
|
|
62
|
+
terminal: Terminal,
|
|
63
|
+
): Promise<{ registered: Registered; editUrl: string }> => {
|
|
64
|
+
const rotated = await rotateCanvas(
|
|
65
|
+
api,
|
|
66
|
+
id,
|
|
67
|
+
requireWriteToken({ id, entry }),
|
|
68
|
+
nextToken,
|
|
69
|
+
).catch(async (error: unknown) => {
|
|
70
|
+
if (!(error instanceof PrLensCliError)) throw error;
|
|
71
|
+
if (error.code !== "CANVAS_UNKNOWN") throw unfinishedRotation(error);
|
|
72
|
+
|
|
73
|
+
// Final: a pending token that was current would have been answered
|
|
74
|
+
// "rotated". Drop it now, or a token imported later would carry it out.
|
|
75
|
+
await updateRegistry((registry) => {
|
|
76
|
+
const current = registry.canvases[id];
|
|
77
|
+
if (
|
|
78
|
+
current === undefined ||
|
|
79
|
+
current.pending !== nextToken ||
|
|
80
|
+
current.api !== api
|
|
81
|
+
)
|
|
82
|
+
return;
|
|
83
|
+
registry.canvases[id] = { ...current, pending: undefined };
|
|
84
|
+
}, terminal);
|
|
85
|
+
throw new PrLensCliError(
|
|
86
|
+
error.code,
|
|
87
|
+
`${error.message}; the pending rotation was dropped`,
|
|
88
|
+
error.details,
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// A different token pending by now belongs to a later rotation.
|
|
93
|
+
await updateRegistry((registry) => {
|
|
94
|
+
const current = registry.canvases[id];
|
|
95
|
+
if (
|
|
96
|
+
current === undefined ||
|
|
97
|
+
current.pending !== nextToken ||
|
|
98
|
+
current.api !== api
|
|
99
|
+
)
|
|
100
|
+
return;
|
|
101
|
+
registry.canvases[id] = {
|
|
102
|
+
...current,
|
|
103
|
+
writeToken: nextToken,
|
|
104
|
+
pending: undefined,
|
|
105
|
+
};
|
|
106
|
+
}, terminal);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
registered: {
|
|
110
|
+
id,
|
|
111
|
+
entry: { ...entry, writeToken: nextToken, pending: undefined },
|
|
112
|
+
},
|
|
113
|
+
editUrl: rotated.editUrl,
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const settlePendingRotation = async (
|
|
118
|
+
api: string,
|
|
119
|
+
registered: Registered,
|
|
120
|
+
terminal: Terminal,
|
|
121
|
+
): Promise<Registered> => {
|
|
122
|
+
requireSameApi(api, registered);
|
|
123
|
+
requireWriteToken(registered);
|
|
124
|
+
const pending = registered.entry.pending;
|
|
125
|
+
return pending === undefined
|
|
126
|
+
? registered
|
|
127
|
+
: (await settleRotation(api, registered, pending, terminal)).registered;
|
|
128
|
+
};
|
package/src/cli.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertNever } from "@coldtea/pr-lens-schema";
|
|
2
|
+
|
|
2
3
|
import { CLI_VERSION } from "./version.js";
|
|
3
4
|
import type { Terminal } from "./terminal.js";
|
|
4
5
|
import { formatError, PrLensCliError } from "./errors.js";
|
|
@@ -23,7 +24,7 @@ const HELP = `pr-lens — review what actually matters
|
|
|
23
24
|
pr-lens comment --graph --manifest the pull request comment, as markdown
|
|
24
25
|
pr-lens validate <file...> any PR Lens document, checked against the contract
|
|
25
26
|
pr-lens export <graph.json> the merged state, as a map worth committing
|
|
26
|
-
pr-lens canvas push | pull | rotate that document, kept on prlens.dev as a page and an embed
|
|
27
|
+
pr-lens canvas push | pull | rotate | delete that document, kept on prlens.dev as a page and an embed
|
|
27
28
|
pr-lens skill diagram instructions for coding agents
|
|
28
29
|
|
|
29
30
|
pr-lens <command> --help what a command takes
|
package/src/commands/canvas.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { assertNever } from "@coldtea/pr-lens-schema";
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import {
|
|
4
4
|
fetchCanvas,
|
|
5
5
|
mintCanvas,
|
|
6
6
|
pushCanvas,
|
|
7
|
-
rotateCanvas,
|
|
8
7
|
verifyWriteToken,
|
|
9
8
|
} from "../canvas/api.js";
|
|
10
9
|
import {
|
|
@@ -13,7 +12,7 @@ import {
|
|
|
13
12
|
findCanvas,
|
|
14
13
|
isCanvasId,
|
|
15
14
|
mintWriteToken,
|
|
16
|
-
|
|
15
|
+
selectCanvas,
|
|
17
16
|
isReservedRegistryTarget,
|
|
18
17
|
readRegistry,
|
|
19
18
|
REGISTRY_PATH,
|
|
@@ -25,27 +24,28 @@ import {
|
|
|
25
24
|
type CanvasRegistry,
|
|
26
25
|
type Registered,
|
|
27
26
|
} from "../canvas/registry.js";
|
|
28
|
-
import { readGraphDoc } from "../document.js";
|
|
29
|
-
import { PrLensCliError, usageError } from "../errors.js";
|
|
30
27
|
import { writeJsonFile } from "../io.js";
|
|
28
|
+
import { readGraphDoc } from "../document.js";
|
|
31
29
|
import type { Terminal } from "../terminal.js";
|
|
32
30
|
import { WORKSPACE_DIR } from "../workspace.js";
|
|
31
|
+
import { deleteCommand } from "../canvas/delete.js";
|
|
32
|
+
import { parseOptions, readString } from "../args.js";
|
|
33
|
+
import { PrLensCliError, usageError } from "../errors.js";
|
|
34
|
+
import { DEFAULT_API, API_ENV, readApi, requireSameApi, requireWriteToken, settleRotation, settlePendingRotation } from "../canvas/write.js";
|
|
33
35
|
|
|
34
|
-
const DEFAULT_API = "https://prlens.dev";
|
|
35
|
-
const API_ENV = "PR_LENS_API_URL";
|
|
36
36
|
const DEFAULT_SOURCE = `${WORKSPACE_DIR}/drawn.graph.json`;
|
|
37
37
|
const DEFAULT_OUT = `${WORKSPACE_DIR}/graph.json`;
|
|
38
38
|
|
|
39
|
-
const SUBCOMMANDS = ["push", "pull", "rotate"] as const;
|
|
39
|
+
const SUBCOMMANDS = ["push", "pull", "rotate", "delete"] as const;
|
|
40
40
|
type Subcommand = (typeof SUBCOMMANDS)[number];
|
|
41
41
|
|
|
42
42
|
const isSubcommand = (value: string): value is Subcommand =>
|
|
43
43
|
SUBCOMMANDS.some((subcommand) => subcommand === value);
|
|
44
44
|
|
|
45
|
-
export const USAGE = `pr-lens canvas <push | pull | rotate> [options]
|
|
45
|
+
export const USAGE = `pr-lens canvas <push | pull | rotate | delete> [options]
|
|
46
46
|
|
|
47
|
-
Keeps a graph document on the PR Lens app as a canvas: a page anyone
|
|
48
|
-
|
|
47
|
+
Keeps a graph document on the PR Lens app as a canvas: a page anyone you share
|
|
48
|
+
it with can read, and an SVG a README can embed. The write token lands in
|
|
49
49
|
${REGISTRY_PATH}, which git ignores; the edit link carries the same token
|
|
50
50
|
in its fragment, so share the view link and keep the edit link to yourself.
|
|
51
51
|
|
|
@@ -61,20 +61,10 @@ in its fragment, so share the view link and keep the edit link to yourself.
|
|
|
61
61
|
pr-lens canvas rotate mint a new write token; the old edit link stops working
|
|
62
62
|
--canvas <id|name> which canvas (default the checkout's only canvas)
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
pr-lens canvas delete permanently delete the hosted canvas; keep local graph and SVG files
|
|
65
|
+
--canvas <id|name> which canvas (default the checkout's only canvas)
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
value: unknown,
|
|
68
|
-
env: Record<string, string | undefined>,
|
|
69
|
-
): string => {
|
|
70
|
-
const api = readString(value, "api") ?? env[API_ENV] ?? DEFAULT_API;
|
|
71
|
-
try {
|
|
72
|
-
new URL(api);
|
|
73
|
-
} catch {
|
|
74
|
-
throw usageError(`--api needs a URL, got ${JSON.stringify(api)}`);
|
|
75
|
-
}
|
|
76
|
-
return api.replace(/\/+$/, "");
|
|
77
|
-
};
|
|
67
|
+
--api <url> the PR Lens app (default $${API_ENV}, else ${DEFAULT_API})`;
|
|
78
68
|
|
|
79
69
|
type CanvasRef = {
|
|
80
70
|
id: string;
|
|
@@ -114,100 +104,9 @@ const readCanvasRef = (value: string): CanvasRef => {
|
|
|
114
104
|
return { id, origin: url.origin, writeToken };
|
|
115
105
|
};
|
|
116
106
|
|
|
117
|
-
/** Another app's 404 says nothing about this entry, so it must not change it. */
|
|
118
|
-
const requireSameApi = (api: string, { id, entry }: Registered): void => {
|
|
119
|
-
if (entry.api === api) return;
|
|
120
|
-
throw new PrLensCliError(
|
|
121
|
-
"CANVAS_UNREGISTERED",
|
|
122
|
-
`${id} is registered against ${entry.api}, not ${api}`,
|
|
123
|
-
`pass --api ${entry.api}, or push the document without --canvas to mint a canvas here`,
|
|
124
|
-
);
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const requireWriteToken = ({ id, entry }: Registered): string => {
|
|
128
|
-
if (entry.writeToken !== undefined) return entry.writeToken;
|
|
129
|
-
throw new PrLensCliError(
|
|
130
|
-
"CANVAS_UNREGISTERED",
|
|
131
|
-
`this checkout can read ${id} but holds no write token for it`,
|
|
132
|
-
"pull its edit link, the one with #w= at the end, and the token comes with it",
|
|
133
|
-
);
|
|
134
|
-
};
|
|
135
|
-
|
|
136
107
|
const countDiagrams = (count: number): string =>
|
|
137
108
|
`${count} ${count === 1 ? "diagram" : "diagrams"}`;
|
|
138
109
|
|
|
139
|
-
const unfinishedRotation = (error: PrLensCliError): PrLensCliError =>
|
|
140
|
-
new PrLensCliError(
|
|
141
|
-
error.code,
|
|
142
|
-
`${error.message}; the rotation is not finished`,
|
|
143
|
-
[
|
|
144
|
-
error.details,
|
|
145
|
-
"run pr-lens canvas rotate again to finish it: the new token is kept until the app confirms it",
|
|
146
|
-
]
|
|
147
|
-
.filter((line) => line !== undefined && line !== "")
|
|
148
|
-
.join("\n"),
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
/** Asking again with the same pair is safe: the app answers "rotated" once the token is on record. */
|
|
152
|
-
const settleRotation = async (
|
|
153
|
-
api: string,
|
|
154
|
-
{ id, entry }: Registered,
|
|
155
|
-
nextToken: string,
|
|
156
|
-
terminal: Terminal,
|
|
157
|
-
): Promise<{ registered: Registered; editUrl: string }> => {
|
|
158
|
-
const rotated = await rotateCanvas(
|
|
159
|
-
api,
|
|
160
|
-
id,
|
|
161
|
-
requireWriteToken({ id, entry }),
|
|
162
|
-
nextToken,
|
|
163
|
-
).catch(async (error: unknown) => {
|
|
164
|
-
if (!(error instanceof PrLensCliError)) throw error;
|
|
165
|
-
if (error.code !== "CANVAS_UNKNOWN") throw unfinishedRotation(error);
|
|
166
|
-
|
|
167
|
-
// Final: a pending token that was current would have been answered
|
|
168
|
-
// "rotated". Drop it now, or a token imported later would carry it out.
|
|
169
|
-
await updateRegistry((registry) => {
|
|
170
|
-
const current = registry.canvases[id];
|
|
171
|
-
if (
|
|
172
|
-
current === undefined ||
|
|
173
|
-
current.pending !== nextToken ||
|
|
174
|
-
current.api !== api
|
|
175
|
-
)
|
|
176
|
-
return;
|
|
177
|
-
registry.canvases[id] = { ...current, pending: undefined };
|
|
178
|
-
}, terminal);
|
|
179
|
-
throw new PrLensCliError(
|
|
180
|
-
error.code,
|
|
181
|
-
`${error.message}; the pending rotation was dropped`,
|
|
182
|
-
error.details,
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// A different token pending by now belongs to a later rotation.
|
|
187
|
-
await updateRegistry((registry) => {
|
|
188
|
-
const current = registry.canvases[id];
|
|
189
|
-
if (
|
|
190
|
-
current === undefined ||
|
|
191
|
-
current.pending !== nextToken ||
|
|
192
|
-
current.api !== api
|
|
193
|
-
)
|
|
194
|
-
return;
|
|
195
|
-
registry.canvases[id] = {
|
|
196
|
-
...current,
|
|
197
|
-
writeToken: nextToken,
|
|
198
|
-
pending: undefined,
|
|
199
|
-
};
|
|
200
|
-
}, terminal);
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
registered: {
|
|
204
|
-
id,
|
|
205
|
-
entry: { ...entry, writeToken: nextToken, pending: undefined },
|
|
206
|
-
},
|
|
207
|
-
editUrl: rotated.editUrl,
|
|
208
|
-
};
|
|
209
|
-
};
|
|
210
|
-
|
|
211
110
|
type Recorded = "imported" | "kept" | "overtaken" | "refused" | "elsewhere";
|
|
212
111
|
|
|
213
112
|
type PullRecord = {
|
|
@@ -355,12 +254,7 @@ const push = async (
|
|
|
355
254
|
return { id: minted.id, entry };
|
|
356
255
|
}));
|
|
357
256
|
|
|
358
|
-
|
|
359
|
-
const pending = registered.entry.pending;
|
|
360
|
-
const target =
|
|
361
|
-
pending === undefined
|
|
362
|
-
? registered
|
|
363
|
-
: (await settleRotation(api, registered, pending, terminal)).registered;
|
|
257
|
+
const target = await settlePendingRotation(api, registered, terminal);
|
|
364
258
|
|
|
365
259
|
const pushed = await pushCanvas(
|
|
366
260
|
api,
|
|
@@ -381,8 +275,9 @@ const push = async (
|
|
|
381
275
|
terminal.out(
|
|
382
276
|
`✓ ${pushed.viewUrl} — rev ${pushed.rev} · ${countDiagrams(pushed.tiles.length)}`,
|
|
383
277
|
);
|
|
384
|
-
terminal.out(
|
|
278
|
+
terminal.out(" unlisted: anyone you share it with can open it, no sign-in needed");
|
|
385
279
|
terminal.out(` README embed: ${pushed.embedUrl}`);
|
|
280
|
+
terminal.out(" remove: pr-lens canvas delete");
|
|
386
281
|
};
|
|
387
282
|
|
|
388
283
|
const pull = async (
|
|
@@ -408,9 +303,7 @@ const pull = async (
|
|
|
408
303
|
positional !== undefined
|
|
409
304
|
? readCanvasRef(positional)
|
|
410
305
|
: {
|
|
411
|
-
...(ref
|
|
412
|
-
? onlyCanvas(registry)
|
|
413
|
-
: findCanvas(registry, ref)),
|
|
306
|
+
...selectCanvas(registry, ref),
|
|
414
307
|
origin: undefined,
|
|
415
308
|
writeToken: undefined,
|
|
416
309
|
};
|
|
@@ -487,8 +380,7 @@ const rotate = async (
|
|
|
487
380
|
await ensureRegistryHome(terminal);
|
|
488
381
|
const registry = await readRegistry();
|
|
489
382
|
const ref = readString(values.canvas, "canvas");
|
|
490
|
-
const { id } =
|
|
491
|
-
ref === undefined ? onlyCanvas(registry) : findCanvas(registry, ref);
|
|
383
|
+
const { id } = selectCanvas(registry, ref);
|
|
492
384
|
|
|
493
385
|
// Saved before the request, so a lost answer cannot lose it; chosen under
|
|
494
386
|
// the lock, so two rotations at once finish the same one.
|
|
@@ -532,7 +424,7 @@ export const canvasCommand = async (
|
|
|
532
424
|
): Promise<void> => {
|
|
533
425
|
const [name, ...rest] = args;
|
|
534
426
|
if (name === undefined)
|
|
535
|
-
throw usageError("canvas needs a subcommand: push, pull or
|
|
427
|
+
throw usageError("canvas needs a subcommand: push, pull, rotate or delete");
|
|
536
428
|
if (!isSubcommand(name))
|
|
537
429
|
throw usageError(`unknown canvas subcommand ${JSON.stringify(name)}`);
|
|
538
430
|
|
|
@@ -541,6 +433,8 @@ export const canvasCommand = async (
|
|
|
541
433
|
return push(rest, terminal, env);
|
|
542
434
|
case "pull":
|
|
543
435
|
return pull(rest, terminal, env);
|
|
436
|
+
case "delete":
|
|
437
|
+
return deleteCommand(rest, terminal, env);
|
|
544
438
|
case "rotate":
|
|
545
439
|
return rotate(rest, terminal, env);
|
|
546
440
|
default:
|
package/src/commands/validate.ts
CHANGED
|
@@ -10,8 +10,10 @@ const count = (n: number, singular: string): string =>
|
|
|
10
10
|
const describe = (validated: ValidatedDocument): string => {
|
|
11
11
|
switch (validated.kind) {
|
|
12
12
|
case "graph": {
|
|
13
|
-
const { lanes, nodes, edges, flows, lenses } = validated.document;
|
|
14
|
-
|
|
13
|
+
const { lanes, nodes, edges, flows, lenses, walkthrough } = validated.document;
|
|
14
|
+
const tour =
|
|
15
|
+
walkthrough === undefined ? "" : ` · ${count(walkthrough.steps.length, "walkthrough step")}`;
|
|
16
|
+
return `graph document · ${count(lanes.length, "lane")}, ${count(nodes.length, "node")}, ${count(edges.length, "edge")}, ${count(flows.length, "flow")} · ${lenses.join(", ")}${tour}`;
|
|
15
17
|
}
|
|
16
18
|
case "patch":
|
|
17
19
|
return `patch document · ${count(validated.document.ops.length, "operation")} · ${validated.document.target.fromSha.slice(0, 7)} → ${validated.document.target.toSha.slice(0, 7)}`;
|