@popoverai/dotrequirements 0.33.0 → 0.34.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 +2 -2
- package/dist/cli.js +13 -1
- package/dist/cloud.d.ts +5 -1
- package/dist/cloud.js +6 -2
- package/dist/commands/acceptance-test.js +1 -1
- package/dist/commands/board/index.d.ts +13 -0
- package/dist/commands/board/index.js +96 -0
- package/dist/commands/branch-merge.d.ts +78 -0
- package/dist/commands/branch-merge.js +129 -0
- package/dist/commands/diff.js +15 -5
- package/dist/commands/mockup/index.d.ts +11 -0
- package/dist/commands/mockup/index.js +82 -0
- package/dist/commands/scores/index.d.ts +50 -0
- package/dist/commands/scores/index.js +190 -0
- package/dist/commands/solution/ask.d.ts +9 -0
- package/dist/commands/solution/ask.js +27 -0
- package/dist/commands/solution/author.d.ts +19 -0
- package/dist/commands/solution/author.js +202 -0
- package/dist/commands/solution/branch.d.ts +103 -0
- package/dist/commands/solution/branch.js +341 -0
- package/dist/commands/solution/brief.d.ts +81 -0
- package/dist/commands/solution/brief.js +47 -0
- package/dist/commands/solution/checkout.d.ts +27 -0
- package/dist/commands/solution/checkout.js +66 -0
- package/dist/commands/solution/client.d.ts +16 -0
- package/dist/commands/solution/client.js +46 -0
- package/dist/commands/solution/contract.d.ts +9 -0
- package/dist/commands/solution/contract.js +23 -0
- package/dist/commands/solution/create.d.ts +14 -0
- package/dist/commands/solution/create.js +52 -0
- package/dist/commands/solution/demo-script.d.ts +28 -0
- package/dist/commands/solution/demo-script.js +43 -0
- package/dist/commands/solution/image.d.ts +60 -0
- package/dist/commands/solution/image.js +148 -0
- package/dist/commands/solution/index.d.ts +12 -0
- package/dist/commands/solution/index.js +282 -0
- package/dist/commands/solution/look-up.d.ts +9 -0
- package/dist/commands/solution/look-up.js +69 -0
- package/dist/commands/solution/next.d.ts +12 -0
- package/dist/commands/solution/next.js +48 -0
- package/dist/commands/solution/plan-file.d.ts +22 -0
- package/dist/commands/solution/plan-file.js +33 -0
- package/dist/commands/solution/preview-screenshot.d.ts +21 -0
- package/dist/commands/solution/preview-screenshot.js +56 -0
- package/dist/commands/solution/pull-request.d.ts +25 -0
- package/dist/commands/solution/pull-request.js +117 -0
- package/dist/commands/solution/pushing.d.ts +66 -0
- package/dist/commands/solution/pushing.js +185 -0
- package/dist/commands/solution/rehearse.d.ts +10 -0
- package/dist/commands/solution/rehearse.js +24 -0
- package/dist/commands/solution/request.d.ts +3 -0
- package/dist/commands/solution/request.js +13 -0
- package/dist/commands/solution/review.d.ts +27 -0
- package/dist/commands/solution/review.js +104 -0
- package/dist/commands/solution/run-file.d.ts +43 -0
- package/dist/commands/solution/run-file.js +69 -0
- package/dist/commands/solution/share.d.ts +62 -0
- package/dist/commands/solution/share.js +346 -0
- package/dist/commands/solution/stall.d.ts +14 -0
- package/dist/commands/solution/stall.js +41 -0
- package/dist/commands/solution/start.d.ts +14 -0
- package/dist/commands/solution/start.js +31 -0
- package/dist/commands/solution/status.d.ts +39 -0
- package/dist/commands/solution/status.js +145 -0
- package/dist/commands/solution/summary.d.ts +13 -0
- package/dist/commands/solution/summary.js +5 -0
- package/dist/commands/solution/things.d.ts +118 -0
- package/dist/commands/solution/things.js +78 -0
- package/dist/commands/solution/thread.d.ts +13 -0
- package/dist/commands/solution/thread.js +53 -0
- package/dist/commands/solution/upload.d.ts +24 -0
- package/dist/commands/solution/upload.js +27 -0
- package/dist/commands/solution/video.d.ts +88 -0
- package/dist/commands/solution/video.js +364 -0
- package/dist/commands/solution/working-folder.d.ts +44 -0
- package/dist/commands/solution/working-folder.js +64 -0
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.js +21 -5
- package/dist/convex.d.ts +54 -0
- package/dist/convex.js +64 -0
- package/dist/push/core.js +7 -2
- package/dist/rehearsal/derive.d.ts +23 -0
- package/dist/rehearsal/derive.js +112 -0
- package/dist/rehearsal/driver.d.ts +44 -0
- package/dist/rehearsal/driver.js +337 -0
- package/dist/rehearsal/earlier.d.ts +25 -0
- package/dist/rehearsal/earlier.js +138 -0
- package/dist/rehearsal/index.d.ts +5 -0
- package/dist/rehearsal/index.js +4 -0
- package/dist/rehearsal/live.d.ts +72 -0
- package/dist/rehearsal/live.js +301 -0
- package/dist/rehearsal/report.d.ts +4 -0
- package/dist/rehearsal/report.js +69 -0
- package/dist/rehearsal/types.d.ts +377 -0
- package/dist/rehearsal/types.js +8 -0
- package/dist/requirements/index.d.ts +6 -0
- package/dist/requirements/index.js +24 -0
- package/dist/schema/browser.d.ts +2 -1
- package/dist/schema/browser.js +6 -1
- package/dist/schema/file-writer.d.ts +5 -0
- package/dist/schema/file-writer.js +4 -0
- package/dist/schema/parser.d.ts +11 -2
- package/dist/schema/parser.js +11 -7
- package/dist/schema/provenance.d.ts +27 -0
- package/dist/schema/provenance.js +66 -0
- package/dist/schema/schemas.d.ts +40 -0
- package/dist/schema/schemas.js +4 -0
- package/dist/sync/execute.d.ts +10 -1
- package/dist/sync/execute.js +68 -5
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/publish.d.ts +7 -0
- package/dist/sync/segment.js +4 -1
- package/dist/sync/snapshot.d.ts +23 -1
- package/dist/sync/snapshot.js +75 -3
- package/dist/sync/solution-documents.d.ts +74 -0
- package/dist/sync/solution-documents.js +135 -0
- package/dist/sync/types.d.ts +20 -0
- package/package.json +3 -2
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { api } from "../../convex.js";
|
|
4
|
+
import { uploadToApp } from "./upload.js";
|
|
5
|
+
/**
|
|
6
|
+
* Videos on an outcome (SIGHT-7): the drafter submits a video file from its
|
|
7
|
+
* checkout with the script it was recorded from. The video is uploaded to
|
|
8
|
+
* the app and never committed; the script travels as text, and the outcome's
|
|
9
|
+
* reviewer reads it in its brief.
|
|
10
|
+
*
|
|
11
|
+
* A browser plays MP4 with H.264 video and WebM with VP8 or VP9 video
|
|
12
|
+
* everywhere we support (Chrome, Edge, Firefox, Safari on macOS and iOS).
|
|
13
|
+
* HEVC and AV1 play only on some hardware, and QuickTime and Matroska only
|
|
14
|
+
* in some browsers, so they are refused. The codec is read from the file
|
|
15
|
+
* itself, since an extension says nothing about it. Kept in step with the
|
|
16
|
+
* backend's `ACCEPTED_VIDEO_FORMATS` (convex/solutions/videos.ts).
|
|
17
|
+
*/
|
|
18
|
+
export const ACCEPTED_VIDEO_FORMATS = "an MP4 with H.264 video, or a WebM with VP8 or VP9 video";
|
|
19
|
+
/** The video at a path, or a refusal naming the accepted formats (SIGHT-7.0.0). */
|
|
20
|
+
export function readVideo(path) {
|
|
21
|
+
const full = resolve(path);
|
|
22
|
+
if (!existsSync(full) || !statSync(full).isFile()) {
|
|
23
|
+
throw new Error(`There is no file at ${path}, so nothing was submitted.`);
|
|
24
|
+
}
|
|
25
|
+
const bytes = new Uint8Array(readFileSync(full));
|
|
26
|
+
const format = videoFormat(bytes);
|
|
27
|
+
if ("refused" in format) {
|
|
28
|
+
throw new Error(`${path} isn't a video a browser can play (${format.refused}), so nothing was submitted. Submit ${ACCEPTED_VIDEO_FORMATS}.`);
|
|
29
|
+
}
|
|
30
|
+
return { name: basename(full), contentType: format.contentType, bytes };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The script a video was recorded from (SIGHT-7.0, 7.0.1): agentic-demo's
|
|
34
|
+
* steps file, JSON holding the browser commands for each step and the
|
|
35
|
+
* sentence said over it, as its text. Anything else is refused, so a
|
|
36
|
+
* reviewer never reads the wrong file as the demo's script.
|
|
37
|
+
*/
|
|
38
|
+
export function readScript(path) {
|
|
39
|
+
const full = resolve(path);
|
|
40
|
+
if (!existsSync(full) || !statSync(full).isFile()) {
|
|
41
|
+
throw new Error(`There is no script at ${path}, so nothing was submitted. Submit the steps file the video was recorded from with it.`);
|
|
42
|
+
}
|
|
43
|
+
const text = readFileSync(full, "utf8");
|
|
44
|
+
if (!isStepsFile(text)) {
|
|
45
|
+
throw new Error(`${path} isn't the steps file a video is recorded from, so nothing was submitted. Submit the steps file you recorded the video from, with its steps and what is said over each.`);
|
|
46
|
+
}
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Where each step of the script begins in the video, in seconds
|
|
51
|
+
* (SIGHT-7.4): read from the summary `agentic-demo record` writes beside
|
|
52
|
+
* the video it records, `final.json` (@popoverai/browser-automation 0.15.4
|
|
53
|
+
* on), one segment per step: `index`, the step's 0-based place in the steps
|
|
54
|
+
* file, and `startSeconds`, where the step's first frame is in the final
|
|
55
|
+
* video, taken as given. The starts are kept in the steps file's order, so
|
|
56
|
+
* step n of a link, counted from 1 as the recorder prints it ("step 1/6"),
|
|
57
|
+
* is the start at n − 1 (SOLDOC-15.1.1). None when there is no such file,
|
|
58
|
+
* as beside a video recorded before the recorder wrote one, so a link to a
|
|
59
|
+
* step of the script opens the video at its start (SOLDOC-15.1.1.0); none,
|
|
60
|
+
* with a word on stderr, when the file isn't the recorder's summary of this
|
|
61
|
+
* script, one start for each of its steps.
|
|
62
|
+
*/
|
|
63
|
+
export function readStepStarts(videoPath, script) {
|
|
64
|
+
const summary = join(dirname(resolve(videoPath)), "final.json");
|
|
65
|
+
if (!existsSync(summary))
|
|
66
|
+
return undefined;
|
|
67
|
+
let segments;
|
|
68
|
+
try {
|
|
69
|
+
segments = JSON.parse(readFileSync(summary, "utf8"))?.segments;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
segments = undefined;
|
|
73
|
+
}
|
|
74
|
+
const starts = [];
|
|
75
|
+
if (Array.isArray(segments)) {
|
|
76
|
+
segments.forEach((segment, at) => {
|
|
77
|
+
const index = Number.isInteger(segment?.index)
|
|
78
|
+
? segment.index
|
|
79
|
+
: at;
|
|
80
|
+
starts[index] = segment?.startSeconds;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const steps = JSON.parse(script).steps.length;
|
|
84
|
+
const fits = starts.length === steps &&
|
|
85
|
+
// Array.from gives a step no segment named `undefined`, which fails.
|
|
86
|
+
Array.from(starts).every((start) => typeof start === "number" && Number.isFinite(start) && start >= 0);
|
|
87
|
+
if (!fits) {
|
|
88
|
+
console.error(`${summary} doesn't give where each of the script's ${steps} steps begins, so the video is submitted without them; a link to a step of it opens at its start. Record it again from this script to keep them.`);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return starts;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A demo's script, as the drafter submits it (SIGHT-13.0): the steps file
|
|
95
|
+
* the agent reviewing it records the video from, as its text.
|
|
96
|
+
*/
|
|
97
|
+
export function readDemoScript(path) {
|
|
98
|
+
const full = resolve(path);
|
|
99
|
+
if (!existsSync(full) || !statSync(full).isFile()) {
|
|
100
|
+
throw new Error(`There is no demo script at ${path}, so nothing was submitted. Submit the steps file the demo is recorded from, as demo-script:<steps file>.`);
|
|
101
|
+
}
|
|
102
|
+
const script = readFileSync(full, "utf8");
|
|
103
|
+
if (!isStepsFile(script)) {
|
|
104
|
+
throw new Error(`${path} isn't a steps file a demo is recorded from, so nothing was submitted. Submit the steps file with its steps and what is said over each; \`agentic-demo validate ${path}\` checks it.`);
|
|
105
|
+
}
|
|
106
|
+
return { name: basename(full), script };
|
|
107
|
+
}
|
|
108
|
+
/** JSON with a list of steps, each saying what is said over it. */
|
|
109
|
+
function isStepsFile(text) {
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(text);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const steps = parsed?.steps;
|
|
118
|
+
return (Array.isArray(steps) &&
|
|
119
|
+
steps.length > 0 &&
|
|
120
|
+
steps.every((step) => typeof step?.narrate === "string"));
|
|
121
|
+
}
|
|
122
|
+
/** What container and video codec the bytes hold, read from the file itself. */
|
|
123
|
+
export function videoFormat(bytes) {
|
|
124
|
+
if (fourcc(bytes, 4) === "ftyp")
|
|
125
|
+
return mp4Format(bytes);
|
|
126
|
+
if (readUint(bytes, 0, 4) === 0x1a45dfa3)
|
|
127
|
+
return webmFormat(bytes);
|
|
128
|
+
return { refused: "it isn't an MP4 or a WebM file" };
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// MP4: a tree of boxes, each a 32-bit size and a four-letter type. The video
|
|
132
|
+
// track's codec is the first sample entry of its `stsd`, under
|
|
133
|
+
// moov > trak > mdia > minf > stbl, and a track is video when its `hdlr`
|
|
134
|
+
// says `vide`.
|
|
135
|
+
const H264 = new Set(["avc1", "avc3"]);
|
|
136
|
+
const MP4_CODEC_NAMES = {
|
|
137
|
+
hvc1: "HEVC",
|
|
138
|
+
hev1: "HEVC",
|
|
139
|
+
av01: "AV1",
|
|
140
|
+
vp09: "VP9 in an MP4",
|
|
141
|
+
vp08: "VP8 in an MP4",
|
|
142
|
+
mp4v: "MPEG-4 Part 2",
|
|
143
|
+
};
|
|
144
|
+
function mp4Format(bytes) {
|
|
145
|
+
const top = boxes(bytes, 0, bytes.length);
|
|
146
|
+
if (!top)
|
|
147
|
+
return { refused: "its MP4 structure is damaged" };
|
|
148
|
+
const ftyp = top.find((box) => box.type === "ftyp");
|
|
149
|
+
if (ftyp && fourcc(bytes, ftyp.body) === "qt ") {
|
|
150
|
+
return { refused: "it is a QuickTime movie" };
|
|
151
|
+
}
|
|
152
|
+
const moov = top.find((box) => box.type === "moov");
|
|
153
|
+
if (!moov)
|
|
154
|
+
return { refused: "it has no movie header" };
|
|
155
|
+
const codecs = [];
|
|
156
|
+
for (const trak of children(bytes, moov, "trak")) {
|
|
157
|
+
const mdia = children(bytes, trak, "mdia")[0];
|
|
158
|
+
if (!mdia)
|
|
159
|
+
continue;
|
|
160
|
+
const hdlr = children(bytes, mdia, "hdlr")[0];
|
|
161
|
+
// hdlr: version and flags, pre_defined, then the handler type.
|
|
162
|
+
if (!hdlr || fourcc(bytes, hdlr.body + 8) !== "vide")
|
|
163
|
+
continue;
|
|
164
|
+
const minf = children(bytes, mdia, "minf")[0];
|
|
165
|
+
const stbl = minf && children(bytes, minf, "stbl")[0];
|
|
166
|
+
const stsd = stbl && children(bytes, stbl, "stsd")[0];
|
|
167
|
+
if (!stsd)
|
|
168
|
+
continue;
|
|
169
|
+
// stsd: version and flags, an entry count, then the entries as boxes.
|
|
170
|
+
const entries = boxes(bytes, stsd.body + 8, stsd.end);
|
|
171
|
+
if (entries?.[0])
|
|
172
|
+
codecs.push(entries[0].type);
|
|
173
|
+
}
|
|
174
|
+
if (codecs.length === 0)
|
|
175
|
+
return { refused: "it has no video track" };
|
|
176
|
+
const other = codecs.find((codec) => !H264.has(codec));
|
|
177
|
+
if (other) {
|
|
178
|
+
return { refused: `its video is ${MP4_CODEC_NAMES[other] ?? other}` };
|
|
179
|
+
}
|
|
180
|
+
return { contentType: "video/mp4" };
|
|
181
|
+
}
|
|
182
|
+
function children(bytes, parent, type) {
|
|
183
|
+
return (boxes(bytes, parent.body, parent.end) ?? []).filter((box) => box.type === type);
|
|
184
|
+
}
|
|
185
|
+
/** The boxes between two offsets, or null when a size runs past the end. */
|
|
186
|
+
function boxes(bytes, start, end) {
|
|
187
|
+
const found = [];
|
|
188
|
+
let at = start;
|
|
189
|
+
while (at + 8 <= end) {
|
|
190
|
+
let size = readUint(bytes, at, 4);
|
|
191
|
+
let header = 8;
|
|
192
|
+
if (size === 1) {
|
|
193
|
+
if (at + 16 > end)
|
|
194
|
+
return null;
|
|
195
|
+
size = readUint(bytes, at + 8, 8);
|
|
196
|
+
header = 16;
|
|
197
|
+
}
|
|
198
|
+
else if (size === 0) {
|
|
199
|
+
size = end - at;
|
|
200
|
+
}
|
|
201
|
+
if (size < header || at + size > end)
|
|
202
|
+
return null;
|
|
203
|
+
found.push({
|
|
204
|
+
type: fourcc(bytes, at + 4),
|
|
205
|
+
body: at + header,
|
|
206
|
+
end: at + size,
|
|
207
|
+
});
|
|
208
|
+
at += size;
|
|
209
|
+
}
|
|
210
|
+
return found;
|
|
211
|
+
}
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// WebM: EBML elements, each an ID and a size written as variable-length
|
|
214
|
+
// integers. The header's DocType says webm or matroska; the video track's
|
|
215
|
+
// CodecID is under Segment > Tracks > TrackEntry, where TrackType 1 is video.
|
|
216
|
+
const EBML = 0x1a45dfa3;
|
|
217
|
+
const DOC_TYPE = 0x4282;
|
|
218
|
+
const SEGMENT = 0x18538067;
|
|
219
|
+
const CLUSTER = 0x1f43b675;
|
|
220
|
+
const TRACKS = 0x1654ae6b;
|
|
221
|
+
const TRACK_ENTRY = 0xae;
|
|
222
|
+
const TRACK_TYPE = 0x83;
|
|
223
|
+
const CODEC_ID = 0x86;
|
|
224
|
+
const WEBM_VIDEO = new Set(["V_VP8", "V_VP9"]);
|
|
225
|
+
const WEBM_CODEC_NAMES = {
|
|
226
|
+
V_AV1: "AV1",
|
|
227
|
+
"V_MPEG4/ISO/AVC": "H.264 in a Matroska file",
|
|
228
|
+
"V_MPEGH/ISO/HEVC": "HEVC",
|
|
229
|
+
};
|
|
230
|
+
function webmFormat(bytes) {
|
|
231
|
+
const top = elements(bytes, 0, bytes.length);
|
|
232
|
+
const header = top.find((element) => element.id === EBML);
|
|
233
|
+
const docType = header
|
|
234
|
+
? elements(bytes, header.body, header.end).find((element) => element.id === DOC_TYPE)
|
|
235
|
+
: undefined;
|
|
236
|
+
const kind = docType ? text(bytes, docType) : "";
|
|
237
|
+
if (kind !== "webm") {
|
|
238
|
+
return {
|
|
239
|
+
refused: kind === "matroska"
|
|
240
|
+
? "it is a Matroska file"
|
|
241
|
+
: "its WebM header is damaged",
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const segment = top.find((element) => element.id === SEGMENT);
|
|
245
|
+
const tracks = segment
|
|
246
|
+
? elements(bytes, segment.body, segment.end).find((element) => element.id === TRACKS)
|
|
247
|
+
: undefined;
|
|
248
|
+
if (!tracks)
|
|
249
|
+
return { refused: "it has no track list" };
|
|
250
|
+
const codecs = [];
|
|
251
|
+
for (const entry of elements(bytes, tracks.body, tracks.end)) {
|
|
252
|
+
if (entry.id !== TRACK_ENTRY)
|
|
253
|
+
continue;
|
|
254
|
+
const fields = elements(bytes, entry.body, entry.end);
|
|
255
|
+
const type = fields.find((field) => field.id === TRACK_TYPE);
|
|
256
|
+
if (!type || readUint(bytes, type.body, type.end - type.body) !== 1) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const codec = fields.find((field) => field.id === CODEC_ID);
|
|
260
|
+
if (codec)
|
|
261
|
+
codecs.push(text(bytes, codec));
|
|
262
|
+
}
|
|
263
|
+
if (codecs.length === 0)
|
|
264
|
+
return { refused: "it has no video track" };
|
|
265
|
+
const other = codecs.find((codec) => !WEBM_VIDEO.has(codec));
|
|
266
|
+
if (other) {
|
|
267
|
+
return { refused: `its video is ${WEBM_CODEC_NAMES[other] ?? other}` };
|
|
268
|
+
}
|
|
269
|
+
return { contentType: "video/webm" };
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The elements between two offsets. An element of unknown size, as a
|
|
273
|
+
* Segment written while recording is, runs to the end; a Cluster ends the
|
|
274
|
+
* walk, since the tracks come before the frames.
|
|
275
|
+
*/
|
|
276
|
+
function elements(bytes, start, end) {
|
|
277
|
+
const found = [];
|
|
278
|
+
let at = start;
|
|
279
|
+
while (at < end) {
|
|
280
|
+
const id = vint(bytes, at, true);
|
|
281
|
+
if (!id || id.value === null)
|
|
282
|
+
break;
|
|
283
|
+
const size = vint(bytes, at + id.length, false);
|
|
284
|
+
if (!size)
|
|
285
|
+
break;
|
|
286
|
+
const body = at + id.length + size.length;
|
|
287
|
+
if (id.value === CLUSTER)
|
|
288
|
+
break;
|
|
289
|
+
const elementEnd = size.value === null ? end : Math.min(end, body + size.value);
|
|
290
|
+
found.push({ id: id.value, body, end: elementEnd });
|
|
291
|
+
at = elementEnd;
|
|
292
|
+
}
|
|
293
|
+
return found;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* A variable-length integer: the leading zero bits say how many bytes
|
|
297
|
+
* follow. An ID keeps its length marker; a size drops it, and a size of all
|
|
298
|
+
* ones is unknown.
|
|
299
|
+
*/
|
|
300
|
+
function vint(bytes, at, keepMarker) {
|
|
301
|
+
const first = bytes[at];
|
|
302
|
+
if (first === undefined || first === 0)
|
|
303
|
+
return null;
|
|
304
|
+
let length = 1;
|
|
305
|
+
while (length <= 8 && !(first & (0x80 >> (length - 1))))
|
|
306
|
+
length++;
|
|
307
|
+
if (at + length > bytes.length)
|
|
308
|
+
return null;
|
|
309
|
+
let value = keepMarker ? first : first & (0xff >> length);
|
|
310
|
+
let allOnes = value === 0xff >> length;
|
|
311
|
+
for (let i = 1; i < length; i++) {
|
|
312
|
+
value = value * 256 + bytes[at + i];
|
|
313
|
+
if (bytes[at + i] !== 0xff)
|
|
314
|
+
allOnes = false;
|
|
315
|
+
}
|
|
316
|
+
return { value: !keepMarker && allOnes ? null : value, length };
|
|
317
|
+
}
|
|
318
|
+
function text(bytes, element) {
|
|
319
|
+
return String.fromCharCode(...bytes.subarray(element.body, element.end))
|
|
320
|
+
.replace(/\0+$/, "")
|
|
321
|
+
.trim();
|
|
322
|
+
}
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
function fourcc(bytes, at) {
|
|
325
|
+
return at + 4 <= bytes.length
|
|
326
|
+
? String.fromCharCode(...bytes.subarray(at, at + 4))
|
|
327
|
+
: "";
|
|
328
|
+
}
|
|
329
|
+
function readUint(bytes, at, length) {
|
|
330
|
+
let value = 0;
|
|
331
|
+
for (let i = 0; i < length; i++)
|
|
332
|
+
value = value * 256 + (bytes[at + i] ?? 0);
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* A video and its script, read and checked before anything is uploaded or
|
|
337
|
+
* recorded (SIGHT-7.0.0, 7.0.1), with its steps' starts (7.4).
|
|
338
|
+
*/
|
|
339
|
+
export function readRecording(path, scriptPath) {
|
|
340
|
+
const video = readVideo(path);
|
|
341
|
+
const script = readScript(scriptPath);
|
|
342
|
+
const stepStarts = readStepStarts(path, script);
|
|
343
|
+
return { video, script, ...(stepStarts ? { stepStarts } : {}) };
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* A recording as it is submitted (SIGHT-7.0, 7.4, 13.2): the app's copy of
|
|
347
|
+
* the video, by the file's name, with the script's text and its steps'
|
|
348
|
+
* starts. Nothing goes on the branch.
|
|
349
|
+
*/
|
|
350
|
+
export async function submittedRecording(client, solutionId, { video, script, stepStarts }) {
|
|
351
|
+
const storageId = await uploadVideo(client, solutionId, video);
|
|
352
|
+
return {
|
|
353
|
+
kind: "video",
|
|
354
|
+
storageId,
|
|
355
|
+
name: video.name,
|
|
356
|
+
script,
|
|
357
|
+
...(stepStarts ? { stepStarts } : {}),
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/** Upload a video to the app (SIGHT-7.0), and the stored copy's id back. */
|
|
361
|
+
export function uploadVideo(client, solutionId, video) {
|
|
362
|
+
return uploadToApp(client, api.solutions.mutations.videoUploadUrl, { solutionId }, video);
|
|
363
|
+
}
|
|
364
|
+
//# sourceMappingURL=video.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Reference } from "./things.js";
|
|
2
|
+
/**
|
|
3
|
+
* A document submitted by its file, as `solutions/mutations:complete` takes
|
|
4
|
+
* it: on a project with no repository connected the backend saves it on the
|
|
5
|
+
* solution's branch (BRANCH-1.0); with one, the file is the thing submitted.
|
|
6
|
+
*/
|
|
7
|
+
export type SubmittedFile = {
|
|
8
|
+
path: string;
|
|
9
|
+
markdownContent: string;
|
|
10
|
+
documentId?: string;
|
|
11
|
+
defaultPrefix?: string;
|
|
12
|
+
};
|
|
13
|
+
/** A document the backend saved on the branch, for its id to go into the file. */
|
|
14
|
+
export type SavedFile = {
|
|
15
|
+
path: string;
|
|
16
|
+
documentId: string;
|
|
17
|
+
version: number;
|
|
18
|
+
};
|
|
19
|
+
/** A document `next` lays out in the working folder (BRANCH-4.0). */
|
|
20
|
+
export type LaidOutFile = {
|
|
21
|
+
documentId: string;
|
|
22
|
+
path: string;
|
|
23
|
+
markdownContent: string;
|
|
24
|
+
defaultPrefix?: string;
|
|
25
|
+
version: number;
|
|
26
|
+
solution?: {
|
|
27
|
+
solutionId: string;
|
|
28
|
+
outcome: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The text of each document among the `file:` things submitted: a Markdown
|
|
33
|
+
* file that exists, with its frontmatter taken off and what it says about the
|
|
34
|
+
* document kept beside it. Anything else stays a plain file reference.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readSubmittedFiles(made: Reference[]): SubmittedFile[];
|
|
37
|
+
/**
|
|
38
|
+
* After a save on the branch, each file carries its document's id, so submitting
|
|
39
|
+
* the same file again saves that document's next version (BRANCH-4.1).
|
|
40
|
+
*/
|
|
41
|
+
export declare function recordSavedIds(files: SubmittedFile[], saved: SavedFile[]): void;
|
|
42
|
+
/** Write the documents an outcome works from into the working folder (BRANCH-4.0). */
|
|
43
|
+
export declare function layOut(files: LaidOutFile[]): void;
|
|
44
|
+
//# sourceMappingURL=working-folder.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
import { writeRequirementsFile } from "../../schema/file-writer.js";
|
|
5
|
+
import { extractFrontmatterBlock, stripFrontmatterBlock, } from "../../schema/parser-core.js";
|
|
6
|
+
/**
|
|
7
|
+
* The text of each document among the `file:` things submitted: a Markdown
|
|
8
|
+
* file that exists, with its frontmatter taken off and what it says about the
|
|
9
|
+
* document kept beside it. Anything else stays a plain file reference.
|
|
10
|
+
*/
|
|
11
|
+
export function readSubmittedFiles(made) {
|
|
12
|
+
const files = [];
|
|
13
|
+
for (const thing of made) {
|
|
14
|
+
if (thing.kind !== "file" || thing.branch !== undefined)
|
|
15
|
+
continue;
|
|
16
|
+
if (!thing.path.endsWith(".md") || !fs.existsSync(thing.path))
|
|
17
|
+
continue;
|
|
18
|
+
const content = fs.readFileSync(thing.path, "utf-8");
|
|
19
|
+
const yaml = extractFrontmatterBlock(content);
|
|
20
|
+
const frontmatter = (yaml ? parse(yaml) : null);
|
|
21
|
+
const id = frontmatter?.document?.id;
|
|
22
|
+
const prefix = frontmatter?.document?.defaultPrefix;
|
|
23
|
+
files.push({
|
|
24
|
+
path: thing.path,
|
|
25
|
+
markdownContent: stripFrontmatterBlock(content),
|
|
26
|
+
...(typeof id === "string" && id ? { documentId: id } : {}),
|
|
27
|
+
...(typeof prefix === "string" && prefix
|
|
28
|
+
? { defaultPrefix: prefix }
|
|
29
|
+
: {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return files;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* After a save on the branch, each file carries its document's id, so submitting
|
|
36
|
+
* the same file again saves that document's next version (BRANCH-4.1).
|
|
37
|
+
*/
|
|
38
|
+
export function recordSavedIds(files, saved) {
|
|
39
|
+
for (const entry of saved) {
|
|
40
|
+
const file = files.find((f) => f.path === entry.path);
|
|
41
|
+
if (!file)
|
|
42
|
+
continue;
|
|
43
|
+
writeRequirementsFile(entry.path, {
|
|
44
|
+
documentId: entry.documentId,
|
|
45
|
+
defaultPrefix: "keep",
|
|
46
|
+
version: entry.version,
|
|
47
|
+
}, file.markdownContent);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Write the documents an outcome works from into the working folder (BRANCH-4.0). */
|
|
51
|
+
export function layOut(files) {
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
fs.mkdirSync(path.dirname(file.path), { recursive: true });
|
|
54
|
+
writeRequirementsFile(file.path, {
|
|
55
|
+
documentId: file.documentId,
|
|
56
|
+
defaultPrefix: file.defaultPrefix
|
|
57
|
+
? { set: file.defaultPrefix }
|
|
58
|
+
: "keep",
|
|
59
|
+
version: file.version,
|
|
60
|
+
...(file.solution ? { belonging: file.solution } : {}),
|
|
61
|
+
}, file.markdownContent);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=working-folder.js.map
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* direction (`--cloud-contributes` / `--repo-contributes`) or assign authority
|
|
5
5
|
* (`--repo-wins` / `--cloud-wins`).
|
|
6
6
|
*/
|
|
7
|
+
import { type BranchMergeFlags } from "./branch-merge.js";
|
|
7
8
|
import { type SyncAuthOptions } from "./sync-common.js";
|
|
8
|
-
interface SyncOptions extends SyncAuthOptions {
|
|
9
|
+
interface SyncOptions extends SyncAuthOptions, BranchMergeFlags {
|
|
9
10
|
cloudContributes?: boolean;
|
|
10
11
|
repoContributes?: boolean;
|
|
11
12
|
cloudWins?: boolean;
|
package/dist/commands/sync.js
CHANGED
|
@@ -8,9 +8,10 @@ import * as path from "node:path";
|
|
|
8
8
|
import * as readline from "node:readline";
|
|
9
9
|
import { attachmentSyncNeeded } from "../sync/attachments.js";
|
|
10
10
|
import { executePlan } from "../sync/execute.js";
|
|
11
|
-
import { acquireCloudSnapshot, acquireLocalSnapshot, compareSnapshots, duplicateIdAbortMessage, filterByScope, resolutionHint, } from "../sync/index.js";
|
|
11
|
+
import { acquireCloudSnapshot, acquireLocalSnapshot, compareSnapshots, duplicateIdAbortMessage, filterByScope, projectSolutions, resolutionHint, } from "../sync/index.js";
|
|
12
12
|
import { buildPlan, resolveMode, } from "../sync/plan.js";
|
|
13
13
|
import { brand } from "../utils/brand.js";
|
|
14
|
+
import { branchMergeCommand, branchMergeOf, } from "./branch-merge.js";
|
|
14
15
|
import { resolveCloudAuth } from "./sync-common.js";
|
|
15
16
|
/**
|
|
16
17
|
* The CLI entry point: exits non-zero on unresolved trouble (SYNC-MODE-1.5,
|
|
@@ -19,6 +20,12 @@ import { resolveCloudAuth } from "./sync-common.js";
|
|
|
19
20
|
* the whole command (the report lines still print).
|
|
20
21
|
*/
|
|
21
22
|
export async function syncCliAction(scope, options) {
|
|
23
|
+
// BRANCH-8: a branch merge is a sync of its own, with no local files.
|
|
24
|
+
const merge = branchMergeOf(scope, options);
|
|
25
|
+
if (merge) {
|
|
26
|
+
await branchMergeCommand(merge);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
22
29
|
const result = await syncCommand(scope, options);
|
|
23
30
|
if (result === "trouble") {
|
|
24
31
|
process.exitCode = 1;
|
|
@@ -38,9 +45,14 @@ export async function syncCommand(scope, options) {
|
|
|
38
45
|
mode = resolveMode(options);
|
|
39
46
|
}
|
|
40
47
|
const auth = resolveCloudAuth(options);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
// SOLACCESS-2.2: whether the project's team has solutions decides which
|
|
49
|
+
// files the sync carries.
|
|
50
|
+
const cloud = await acquireCloudSnapshot(auth);
|
|
51
|
+
const [local, solutions] = await Promise.all([
|
|
52
|
+
acquireLocalSnapshot(workspaceRoot, undefined, {
|
|
53
|
+
solutions: cloud.solutions === true,
|
|
54
|
+
}),
|
|
55
|
+
projectSolutions(auth, cloud),
|
|
44
56
|
]);
|
|
45
57
|
// SYNC-MODE-4.4 (and 4.4.0 for --yes): an empty repo is indistinguishable from a wrong working
|
|
46
58
|
// directory or a failed checkout — never treat it as intent to delete every
|
|
@@ -122,7 +134,7 @@ export async function syncCommand(scope, options) {
|
|
|
122
134
|
return "clean";
|
|
123
135
|
}
|
|
124
136
|
console.log(`\nSyncing with ${brand} cloud...`);
|
|
125
|
-
const outcome = await executePlan(plan, cloud, auth, workspaceRoot, mode);
|
|
137
|
+
const outcome = await executePlan(plan, cloud, auth, workspaceRoot, mode, solutions);
|
|
126
138
|
// Report.
|
|
127
139
|
if (outcome.cloudCreated)
|
|
128
140
|
console.log(` Created in cloud: ${outcome.cloudCreated}`);
|
|
@@ -179,6 +191,10 @@ export async function syncCommand(scope, options) {
|
|
|
179
191
|
for (const { name, warning } of outcome.publishWarnings) {
|
|
180
192
|
console.log(` ⚠ ${name}: ${warning}`);
|
|
181
193
|
}
|
|
194
|
+
// SOLDOC-5: a file's path and frontmatter disagree about where it belongs.
|
|
195
|
+
for (const warning of outcome.belongingWarnings ?? []) {
|
|
196
|
+
console.log(` ⚠ ${warning}`);
|
|
197
|
+
}
|
|
182
198
|
// SYNC-CLI-EDIT-3.1: the cloud stores the web editor's notation, and files
|
|
183
199
|
// written another way converge to it on push. Each file NAMED, not counted —
|
|
184
200
|
// a count leaves Jaime to find out which files changed from `git diff`,
|
package/dist/convex.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare const api: {
|
|
|
20
20
|
exportDocument: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
21
21
|
exportProject: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
22
22
|
exportProjectForShare: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
23
|
+
foreignDocumentIds: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
23
24
|
};
|
|
24
25
|
mutations: {
|
|
25
26
|
deleteForSync: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
@@ -68,6 +69,59 @@ export declare const api: {
|
|
|
68
69
|
ensureProjectShareToken: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
69
70
|
};
|
|
70
71
|
};
|
|
72
|
+
scores: {
|
|
73
|
+
mutations: {
|
|
74
|
+
create: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
75
|
+
update: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
mockups: {
|
|
79
|
+
mutations: {
|
|
80
|
+
create: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
solutions: {
|
|
84
|
+
queries: {
|
|
85
|
+
getPlan: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
86
|
+
getThread: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
87
|
+
getThings: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
88
|
+
defaultPlan: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
89
|
+
readPlanFile: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
90
|
+
getOutcomePage: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
91
|
+
getAgentLogs: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
92
|
+
forSync: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
93
|
+
rehearsalsOf: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
94
|
+
rehearsalHistory: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
95
|
+
};
|
|
96
|
+
mutations: {
|
|
97
|
+
create: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
98
|
+
start: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
99
|
+
next: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
100
|
+
complete: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
101
|
+
ask: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
102
|
+
stall: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
103
|
+
answer: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
104
|
+
approveCheckpoint: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
105
|
+
removeRehearsal: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
106
|
+
videoUploadUrl: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
107
|
+
};
|
|
108
|
+
shares: {
|
|
109
|
+
forCheckout: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
110
|
+
push: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
111
|
+
done: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
112
|
+
};
|
|
113
|
+
images: {
|
|
114
|
+
uploadUrl: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
115
|
+
inputUploadUrl: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
116
|
+
copies: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
117
|
+
};
|
|
118
|
+
branches: {
|
|
119
|
+
branchOf: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
120
|
+
claim: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
121
|
+
recordPush: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
122
|
+
removal: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
71
125
|
lib: {
|
|
72
126
|
authActions: {
|
|
73
127
|
exchangeAuthorizationCode: import("convex/server").FunctionReference<"action", "public", any, any, string | undefined>;
|