@popoverai/dotrequirements 0.39.4 → 0.39.5
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/dist/commands/solution/create.d.ts +3 -1
- package/dist/commands/solution/create.js +53 -2
- package/dist/commands/solution/image.d.ts +5 -2
- package/dist/commands/solution/image.js +7 -4
- package/dist/commands/solution/index.js +1 -1
- package/dist/commands/solution/upload.d.ts +3 -2
- package/dist/commands/solution/upload.js +8 -4
- package/package.json +1 -1
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* JSON), its title, its description, its plan in the stored shape, and the
|
|
5
5
|
* inputs that already exist. `--description` says what the solution is
|
|
6
6
|
* beyond its title, in Markdown, and wins over the file's. The backend validates the plan and refuses an invalid one
|
|
7
|
-
* with the rules it broke.
|
|
7
|
+
* with the rules it broke. `--idea` names where the idea is: an image is
|
|
8
|
+
* read and uploaded first, so the solution is created only once each one
|
|
9
|
+
* has (PLAN-13).
|
|
8
10
|
*/
|
|
9
11
|
export declare function createCommand(file: string | undefined, options?: {
|
|
10
12
|
title?: string;
|
|
@@ -2,18 +2,24 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { api } from "../../convex.js";
|
|
4
4
|
import { solutionClient } from "./client.js";
|
|
5
|
+
import { readImage } from "./image.js";
|
|
5
6
|
import { planPayload, readPlanFile } from "./plan-file.js";
|
|
6
7
|
import { parseThing } from "./things.js";
|
|
8
|
+
import { uploadToApp } from "./upload.js";
|
|
9
|
+
const NOT_CREATED = "no solution was created";
|
|
7
10
|
/**
|
|
8
11
|
* `dotreq solution create [file]`: with no file, a solution from the
|
|
9
12
|
* project's default plan, titled with `--title`; with a plan file (YAML or
|
|
10
13
|
* JSON), its title, its description, its plan in the stored shape, and the
|
|
11
14
|
* inputs that already exist. `--description` says what the solution is
|
|
12
15
|
* beyond its title, in Markdown, and wins over the file's. The backend validates the plan and refuses an invalid one
|
|
13
|
-
* with the rules it broke.
|
|
16
|
+
* with the rules it broke. `--idea` names where the idea is: an image is
|
|
17
|
+
* read and uploaded first, so the solution is created only once each one
|
|
18
|
+
* has (PLAN-13).
|
|
14
19
|
*/
|
|
15
20
|
export async function createCommand(file, options = {}) {
|
|
16
|
-
const
|
|
21
|
+
const references = options.idea?.map(parseThing);
|
|
22
|
+
const images = readIdeaImages(references ?? []);
|
|
17
23
|
const described = options.description?.trim() || undefined;
|
|
18
24
|
if (!file) {
|
|
19
25
|
const title = options.title?.trim();
|
|
@@ -21,6 +27,7 @@ export async function createCommand(file, options = {}) {
|
|
|
21
27
|
throw new Error("A solution from the project's default plan needs a title: --title <title>.");
|
|
22
28
|
}
|
|
23
29
|
const client = solutionClient();
|
|
30
|
+
const idea = await ideaThings(client, references, images);
|
|
24
31
|
const id = await client.mutation(api.solutions.mutations.create, {
|
|
25
32
|
project: client.project,
|
|
26
33
|
title,
|
|
@@ -37,6 +44,7 @@ export async function createCommand(file, options = {}) {
|
|
|
37
44
|
throw new Error(`${file} needs a plan, and a title in the file or as --title (docs/working/plan-surfaces.md).`);
|
|
38
45
|
}
|
|
39
46
|
const client = solutionClient();
|
|
47
|
+
const idea = await ideaThings(client, references, images);
|
|
40
48
|
const id = await client.mutation(api.solutions.mutations.create, {
|
|
41
49
|
project: client.project,
|
|
42
50
|
title,
|
|
@@ -49,4 +57,47 @@ export async function createCommand(file, options = {}) {
|
|
|
49
57
|
});
|
|
50
58
|
console.log(`Created "${title}": ${id}`);
|
|
51
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Each image the idea names, read and checked before anything is uploaded
|
|
62
|
+
* or created: a path with no file, a format a browser can't show (PLAN-13.2,
|
|
63
|
+
* 13.3), or two of one file name, which the solution can't tell apart
|
|
64
|
+
* (13.4, SIGHT-10).
|
|
65
|
+
*/
|
|
66
|
+
function readIdeaImages(references) {
|
|
67
|
+
const images = new Map();
|
|
68
|
+
const named = new Map();
|
|
69
|
+
for (const thing of references) {
|
|
70
|
+
if (thing.kind !== "image")
|
|
71
|
+
continue;
|
|
72
|
+
const image = readImage(thing.path, NOT_CREATED);
|
|
73
|
+
const earlier = named.get(image.name);
|
|
74
|
+
if (earlier !== undefined) {
|
|
75
|
+
throw new Error(`${earlier} and ${thing.path} are both called ${image.name}, so ${NOT_CREATED}. Rename one of them.`);
|
|
76
|
+
}
|
|
77
|
+
named.set(image.name, thing.path);
|
|
78
|
+
images.set(thing, image);
|
|
79
|
+
}
|
|
80
|
+
return images;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The idea as the backend keeps it, in the order given: each image uploaded
|
|
84
|
+
* for the project, as a rehearsal's input image is, and kept as its stored
|
|
85
|
+
* copy's id and name (PLAN-13.0, 13.1). One that doesn't upload stops the
|
|
86
|
+
* command before the solution is created (13.5).
|
|
87
|
+
*/
|
|
88
|
+
async function ideaThings(client, references, images) {
|
|
89
|
+
if (!references)
|
|
90
|
+
return undefined;
|
|
91
|
+
const idea = [];
|
|
92
|
+
for (const thing of references) {
|
|
93
|
+
const image = images.get(thing);
|
|
94
|
+
if (!image) {
|
|
95
|
+
idea.push(thing);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const storageId = await uploadToApp(client, api.solutions.images.inputUploadUrl, { project: client.project }, image, `so ${NOT_CREATED}. Run the command again.`);
|
|
99
|
+
idea.push({ kind: "image", storageId, name: image.name });
|
|
100
|
+
}
|
|
101
|
+
return idea;
|
|
102
|
+
}
|
|
52
103
|
//# sourceMappingURL=create.js.map
|
|
@@ -27,8 +27,11 @@ export type Image = {
|
|
|
27
27
|
contentType: ImageContentType;
|
|
28
28
|
bytes: Uint8Array<ArrayBuffer>;
|
|
29
29
|
};
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The image at a path, or a refusal naming the accepted formats
|
|
32
|
+
* (SIGHT-9.0.0) and what came of it: by default, that nothing was submitted.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readImage(path: string, outcome?: string): Image;
|
|
32
35
|
/** What image format the bytes hold, read from the file's signature. */
|
|
33
36
|
export declare function imageFormat(bytes: Uint8Array): ImageFormat;
|
|
34
37
|
/**
|
|
@@ -18,16 +18,19 @@ import { uploadToApp } from "./upload.js";
|
|
|
18
18
|
* with the backend's `ACCEPTED_IMAGE_FORMATS` (convex/solutions/images.ts).
|
|
19
19
|
*/
|
|
20
20
|
export const ACCEPTED_IMAGE_FORMATS = "a PNG, JPEG, GIF, WebP or AVIF image";
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The image at a path, or a refusal naming the accepted formats
|
|
23
|
+
* (SIGHT-9.0.0) and what came of it: by default, that nothing was submitted.
|
|
24
|
+
*/
|
|
25
|
+
export function readImage(path, outcome = "nothing was submitted") {
|
|
23
26
|
const full = resolve(path);
|
|
24
27
|
if (!existsSync(full) || !statSync(full).isFile()) {
|
|
25
|
-
throw new Error(`There is no file at ${path}, so
|
|
28
|
+
throw new Error(`There is no file at ${path}, so ${outcome}.`);
|
|
26
29
|
}
|
|
27
30
|
const bytes = new Uint8Array(readFileSync(full));
|
|
28
31
|
const format = imageFormat(bytes);
|
|
29
32
|
if ("refused" in format) {
|
|
30
|
-
throw new Error(`${path} isn't an image a browser can show (${format.refused}), so
|
|
33
|
+
throw new Error(`${path} isn't an image a browser can show (${format.refused}), so ${outcome}. Submit ${ACCEPTED_IMAGE_FORMATS}.`);
|
|
31
34
|
}
|
|
32
35
|
return { name: basename(full), contentType: format.contentType, bytes };
|
|
33
36
|
}
|
|
@@ -200,7 +200,7 @@ export function registerSolution(program) {
|
|
|
200
200
|
.description("Create a solution from the project's default plan, or from a plan file (YAML or JSON: title, plan, inputs)")
|
|
201
201
|
.option("--title <title>", "The solution's title: required without a file, else when the file has none")
|
|
202
202
|
.option("--description <text>", "What the solution is beyond its title, in Markdown: wins over the file's")
|
|
203
|
-
.option("--idea <reference...>", "Where the idea is written down, for the outcome that makes the solution exist: document:<id>, file:<path>[@<branch>], link:<url>")
|
|
203
|
+
.option("--idea <reference...>", "Where the idea is written down, for the outcome that makes the solution exist: document:<id>, file:<path>[@<branch>], link:<url>, image:<path>")
|
|
204
204
|
.action(wrap(createCommand));
|
|
205
205
|
whereOptions(solution
|
|
206
206
|
.command("status")
|
|
@@ -12,7 +12,8 @@ export type Upload = {
|
|
|
12
12
|
* the solution it is for (none for an image a rehearsal gives an input,
|
|
13
13
|
* REHEARSE-4.7), the file POSTed to it with its type, and the stored copy's
|
|
14
14
|
* id back. Convex sets no size
|
|
15
|
-
* limit on these uploads; the POST has two minutes.
|
|
15
|
+
* limit on these uploads; the POST has two minutes. A failure names the
|
|
16
|
+
* file, then says what came of it: by default, that nothing was submitted.
|
|
16
17
|
*/
|
|
17
18
|
export declare function uploadToApp(client: ReturnType<typeof solutionClient>, uploadUrl: FunctionReference<"mutation">,
|
|
18
19
|
/** The upload URL endpoint's arguments: the solution, or the project for a rehearsal's input. */
|
|
@@ -20,5 +21,5 @@ args: {
|
|
|
20
21
|
solutionId: string;
|
|
21
22
|
} | {
|
|
22
23
|
project: string;
|
|
23
|
-
}, file: Upload): Promise<string>;
|
|
24
|
+
}, file: Upload, outcome?: string): Promise<string>;
|
|
24
25
|
//# sourceMappingURL=upload.d.ts.map
|
|
@@ -4,23 +4,27 @@
|
|
|
4
4
|
* the solution it is for (none for an image a rehearsal gives an input,
|
|
5
5
|
* REHEARSE-4.7), the file POSTed to it with its type, and the stored copy's
|
|
6
6
|
* id back. Convex sets no size
|
|
7
|
-
* limit on these uploads; the POST has two minutes.
|
|
7
|
+
* limit on these uploads; the POST has two minutes. A failure names the
|
|
8
|
+
* file, then says what came of it: by default, that nothing was submitted.
|
|
8
9
|
*/
|
|
9
10
|
export async function uploadToApp(client, uploadUrl,
|
|
10
11
|
/** The upload URL endpoint's arguments: the solution, or the project for a rehearsal's input. */
|
|
11
|
-
args, file) {
|
|
12
|
+
args, file, outcome = "so nothing was submitted. Submit it again.") {
|
|
12
13
|
const url = await client.mutation(uploadUrl, args);
|
|
13
14
|
const response = await fetch(url, {
|
|
14
15
|
method: "POST",
|
|
15
16
|
headers: { "Content-Type": file.contentType },
|
|
16
17
|
body: file.bytes,
|
|
18
|
+
}).catch((error) => {
|
|
19
|
+
// The connection dropped, or never opened.
|
|
20
|
+
throw new Error(`Uploading ${file.name} failed (${error instanceof Error ? error.message : String(error)}), ${outcome}`);
|
|
17
21
|
});
|
|
18
22
|
if (!response.ok) {
|
|
19
|
-
throw new Error(`Uploading ${file.name} failed (${response.status} ${response.statusText}),
|
|
23
|
+
throw new Error(`Uploading ${file.name} failed (${response.status} ${response.statusText}), ${outcome}`);
|
|
20
24
|
}
|
|
21
25
|
const { storageId } = (await response.json());
|
|
22
26
|
if (!storageId) {
|
|
23
|
-
throw new Error(`Uploading ${file.name} returned no id,
|
|
27
|
+
throw new Error(`Uploading ${file.name} returned no id, ${outcome}`);
|
|
24
28
|
}
|
|
25
29
|
return storageId;
|
|
26
30
|
}
|
package/package.json
CHANGED