@velven/cli 0.3.0 → 0.4.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/CHANGELOG.md +65 -4
- package/README.md +61 -17
- package/dist/velven.js +456 -59
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
- `velven create send` lets Velven pick how much to do when you pass neither `--effort` nor `--plan`/`--no-plan`:
|
|
6
|
+
a quick change, a full build it playtests and fixes, or a plan when you ask for one. Its line before sending reads "Velven picks how much to do: about 20 credits for a small change, at most
|
|
7
|
+
1500 held. You have 3200 available.", or on a project that never built "about 51 credits for a first build (its
|
|
8
|
+
pictures are made before the first preview)". With `--effort` or a plan flag the turn is as before; a later
|
|
9
|
+
message with neither flag is Velven's pick again (the help no longer says the flags are kept for every message).
|
|
10
|
+
- `--build` sends the message as Build it: no more questions, Velven fills in what the plan leaves open. Words beyond
|
|
11
|
+
a bare build (`--build "make it 2D"`) change the plan first, then it is built; `--build "Build it"` builds the plan as
|
|
12
|
+
it is, at once.
|
|
13
|
+
- `--ref <file>` (up to 4) uploads a reference picture (up to 10 MB) to the project and attaches it to the message:
|
|
14
|
+
the picture goes straight to Velven's storage through a link, then Velven keeps it. `--ref ref-3` sends a picture
|
|
15
|
+
the project already keeps, with no upload. A refused picture stops the CLI with Velven's sentence and exit 4, before
|
|
16
|
+
anything is sent; one over 10 MB is refused before it is uploaded.
|
|
17
|
+
- Following a turn Velven routes says "Started: Velven is choosing the work", then what it chose in words, with the
|
|
18
|
+
work and the reason on standard error. A question prints with its numbered choices and the commands that answer it.
|
|
19
|
+
- `velven create refs` lists the project's reference pictures (id, when added, size, how many messages carried each),
|
|
20
|
+
and `velven create refs rm <ref-n>` deletes one. A send whose picture finds the project's 16 full names both.
|
|
21
|
+
- A question Velven asks is printed once, with its choices: the turn's reply, the same question, is no longer printed
|
|
22
|
+
again at the end. `velven create watch <turn>` names the turn's own project in how to answer it, not the current one.
|
|
23
|
+
- A picture refused because the project has given out all 999 picture numbers stops with its own sentence and
|
|
24
|
+
`reference_numbers_used`, without the hint about removing a picture.
|
|
25
|
+
- `velven create stats` heads a routed group with its work ("quick fast first builds", "standard full later builds",
|
|
26
|
+
"quick interview turns", "quick routed plan turns"), and `velven create show --timeline` names a routed turn's work
|
|
27
|
+
and first build from its router step.
|
|
28
|
+
- `velven create new` without `--genre` makes an open game: Velven follows your idea instead of arcade's rules
|
|
29
|
+
(`--genre arcade` holds a project to them). Without `--engine`, Velven picks 2D (`phaser`) or 3D (`three`) from your
|
|
30
|
+
idea, 3D unless it is clearly 2D. A project made with `--engine` keeps its engine.
|
|
31
|
+
- A new game starts with a short conversation: Velven asks a question or two (2D or 3D, the style, at most four in
|
|
32
|
+
all), each with numbered choices and `Just build it: velven create send --build "Just build it"` (the style question
|
|
33
|
+
also offers `--ref <file>`), then sketches a short plan, printed as a card with `Build it: velven create send --build
|
|
34
|
+
"Build it"`. `velven create plan` prints the project's plan again (`--json` too).
|
|
35
|
+
- Following a build prints its plain steps ("Designing your game", "Painting the art", "Building the world", "Trying it
|
|
36
|
+
out", "Polishing", "Finishing up"), a first build's design once it is settled ("Designing <title>: <pitch>" with its
|
|
37
|
+
style and palette), and each picture as it lands ("Painted sky (1024 by 512)" and a link to it). The plan and the
|
|
38
|
+
brief as they are written are printed only with `watch --json`.
|
|
39
|
+
- A message sent while a turn runs on the project waits behind it instead of being refused (the CLI asks for that:
|
|
40
|
+
`queue=true` on the estimate, `queue: true` on the message; Velven still refuses a send that does not ask, as it
|
|
41
|
+
refused 0.3.0's): "Queued: it runs after the turn that is running. velven create queue lists what waits." (exit 0;
|
|
42
|
+
`--json` prints `{ ok, queued }`). It starts when that turn ends, and following that turn prints "Your next message
|
|
43
|
+
is running: velven create watch <turn>". `velven create queue` lists the waiting messages (and any Velven could not
|
|
44
|
+
start, with why); `velven create queue rm <id>` removes one.
|
|
45
|
+
- `velven create stats` heads a new game's plan turns "quick draft turns", and `velven create show --timeline` names a
|
|
46
|
+
draft or interview turn from its interviewer step.
|
|
47
|
+
- After a turn that ends with a playable game, following it prints "Where it could go next:" with two to four short
|
|
48
|
+
suggestions Velven wrote for this game, and the command that sends the first one; send any of them as an ordinary
|
|
49
|
+
message.
|
|
50
|
+
- `velven create usage` lists what each of your turns held and used, newest first (`--project <id>` for one project,
|
|
51
|
+
`--before <turn>` for the page before, `--json`).
|
|
52
|
+
- When Velven kept a turn quick because the day's credits would not cover more, its route line says so in words. A
|
|
53
|
+
reply may end "You're running low on credits for today." and, after a question, the CLI now prints what follows the
|
|
54
|
+
question in the reply.
|
|
55
|
+
- `velven create stats` adds a line under each group saying how the next message reacted to its turns (moved on,
|
|
56
|
+
corrected it, asked for checks), when any was labelled.
|
|
57
|
+
- On a project that never built, a message without `--build` is priced as what Velven answers it with first: "about 2
|
|
58
|
+
credits for a question or a short plan"; with `--build`, as a first build.
|
|
59
|
+
- A message sent while a turn runs that starts at once (the turn ended as it was queued) is followed as a turn ("Turn
|
|
60
|
+
<id> is on its way.", or "Turn <id> waits for room on Velven and starts on its own." when Velven's daily room holds
|
|
61
|
+
it back); one Velven refused when it came to start prints why and exits 4, instead of "Queued".
|
|
62
|
+
- "Your next message is running" names the project ("on project <id>") when the message that started waited on
|
|
63
|
+
another of your projects.
|
|
64
|
+
- When a new game's first build stops before it made the game (nothing saved, or only its design), the CLI prints
|
|
65
|
+
`Try again: velven create send --build "Build it"` under the reply.
|
|
66
|
+
|
|
67
|
+
## 0.3.0
|
|
4
68
|
|
|
5
69
|
- `velven create show --timeline` draws a turn's timeline as a text waterfall: every phase, step, model and tool call
|
|
6
70
|
with its bar, start and time, the critical path marked with `*`. `--turn <id>` picks the turn (the current project's
|
|
@@ -17,9 +81,6 @@
|
|
|
17
81
|
first build, `velven create show --timeline` calls such a turn "a quick first build", and `velven create stats` heads
|
|
18
82
|
its groups "quick first builds", "quick later builds", "quick build turns" (turns from before first builds were told
|
|
19
83
|
apart) and "quick plan turns". An older Velven's answers, without these fields, read as before.
|
|
20
|
-
|
|
21
|
-
## 0.3.0
|
|
22
|
-
|
|
23
84
|
- A version holds up to 512 MB and 5,000 files when you're signed in, so a game engine's web export fits. Without an
|
|
24
85
|
account it's still 50 MB and 1,000 files.
|
|
25
86
|
- A file over 32 MB is sent in parts of 32 MB, each through its own upload link. A dropped upload resumes where it
|
package/README.md
CHANGED
|
@@ -153,37 +153,80 @@ to pick another.
|
|
|
153
153
|
it and give you a preview link to play. It needs an account, since every turn spends credits.
|
|
154
154
|
|
|
155
155
|
```sh
|
|
156
|
-
velven create new "
|
|
157
|
-
velven create send "a
|
|
156
|
+
velven create new "Petal Drift"
|
|
157
|
+
velven create send "a cozy game where you grow a garden on a floating island"
|
|
158
158
|
```
|
|
159
159
|
|
|
160
160
|
| Command | What it does |
|
|
161
161
|
| --- | --- |
|
|
162
|
-
| `velven create new [title]` | Start a project. It becomes the current project. `--
|
|
163
|
-
| `velven create send <text>` | Send the current project a message. The CLI prints the estimate, asks before sending in a terminal, then follows the turn to its end |
|
|
162
|
+
| `velven create new [title]` | Start a project. It becomes the current project. Without `--genre` it is an open game: Velven follows your idea (`--genre arcade` holds it to arcade's rules). Without `--engine`, Velven picks 2D (`phaser`) or 3D (`three`) from your idea: 3D unless it is clearly 2D |
|
|
163
|
+
| `velven create send <text>` | Send the current project a message. Velven picks how much to do unless you pass `--effort` or a plan flag. The CLI prints the estimate, asks before sending in a terminal, then follows the turn to its end. `--build` builds now, `--ref <file>` attaches a picture (`--ref ref-3` one the project keeps). Sent while a turn runs on the project, the message waits and runs when that turn ends (followed at once when that turn ended as it was sent; exit 4 with why when Velven refused it) |
|
|
164
|
+
| `velven create plan` | A new game's short plan: what the game is, how it plays, its look, what is in it, how it ends. `--project <id>` picks another project |
|
|
165
|
+
| `velven create queue` | The messages waiting behind the running turn, in the order they run, and any Velven could not start with why. `--project <id>` picks another project |
|
|
166
|
+
| `velven create queue rm <id>` | Remove a waiting message before it runs |
|
|
164
167
|
| `velven create watch [turn]` | Follow a turn, by default the last one you sent or watched. `--from <n>` starts from that event |
|
|
165
168
|
| `velven create cancel [turn]` | Stop a turn. What it finished is saved, and unused credits come back |
|
|
166
169
|
| `velven create list` | Your projects. `*` marks the current one |
|
|
167
170
|
| `velven create show [project]` | A project, its newest turn and a fresh preview link (it works for 24 hours) |
|
|
168
171
|
| `velven create show --timeline` | The newest turn's timeline as a waterfall (see below). `--turn <id>` picks another turn, `--all` lists every tool call |
|
|
172
|
+
| `velven create refs` | The project's reference pictures: each id, when it was added, its size and how many messages carried it. `--project <id>` picks another project |
|
|
173
|
+
| `velven create refs rm <ref-n>` | Delete a reference picture, sent or not. The messages that carried it keep its number, and the number is never given to another picture |
|
|
169
174
|
| `velven create credits` | Your credits: today's allowance, your balance, what open turns hold, and the latest entries |
|
|
175
|
+
| `velven create usage` | What each of your turns held and used, newest first. `--project <id>` keeps one project's, `--before <turn>` reads the page before (the last line names it), `--json` prints the page |
|
|
170
176
|
| `velven create stats` | For Velven's admins: p50 and p90 over recent turns. `--days <n>` (1 to 90, 7 by default), `--origin <origin>` |
|
|
171
177
|
|
|
172
178
|
`velven create send` options:
|
|
173
179
|
|
|
174
|
-
- `--effort
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
- Without `--effort`, `--plan` or `--no-plan`, Velven picks the turn's work from your message: a quick change, a full
|
|
181
|
+
build that it also playtests and fixes, or a plan. The CLI prints "Velven picks how much to do: about 20 credits
|
|
182
|
+
for a small change, at most 1500 held." first (on a project that never built, the figure is a first build's, and
|
|
183
|
+
the line says so); what is not spent comes back when the turn ends. Effort and plan mode then stay as the project
|
|
184
|
+
had them.
|
|
185
|
+
- A new game starts with a short conversation: Velven asks what matters most and is still open (2D or 3D, the style,
|
|
186
|
+
at most four questions in all, fewer for a specific idea), then sketches a short plan. A question comes with
|
|
187
|
+
numbered choices and how to answer it: send a choice, your own words, or `--build "Just build it"`; the style
|
|
188
|
+
question also takes a picture with `--ref <file>`. The line names the turn's own project, also when you follow it
|
|
189
|
+
with `velven create watch` from another. The plan prints as a card (the title, the pitch, 2D or 3D and the camera,
|
|
190
|
+
how it plays, the look and palette, what is in it, how it ends, a score or not); send a change in words to reshape
|
|
191
|
+
it, or `--build "Build it"` to build it. `velven create plan` prints it again.
|
|
192
|
+
- `--build` builds now: no more questions, and Velven fills in what the plan leaves open (writing the plan first when
|
|
193
|
+
there is none). Words beyond a bare build change the plan first (`--build "make it 2D"`: the plan is written again
|
|
194
|
+
in 2D, then built); `--build "Build it"` builds the plan as it is. Velven still picks how much to do.
|
|
195
|
+
- `--ref <file>` attaches a reference picture (PNG, JPEG or WebP, up to 10 MB, up to 4 on a message): the CLI uploads
|
|
196
|
+
each to the project first (straight to Velven's storage through a link Velven gives, then Velven keeps it), then
|
|
197
|
+
sends the message with them. `--ref ref-3` sends a picture the project already keeps, by the id Velven printed when
|
|
198
|
+
it was attached, with no upload. Velven's agents read a picture's style, a character or a game's screen from it. A
|
|
199
|
+
picture Velven refuses, or one over 10 MB, stops the CLI with its sentence and exit 4, before the message is sent.
|
|
200
|
+
A project keeps up to 16 pictures. One no message carried goes a day after it was last uploaded; a sent one stays
|
|
201
|
+
until you delete it. When a project is full, the CLI says so and names `velven create refs` and
|
|
202
|
+
`velven create refs rm <ref-n>`. A project that has given out all 999 picture numbers takes no new one (exit 4,
|
|
203
|
+
`reference_numbers_used`): start a new project.
|
|
204
|
+
- `--effort quick|standard|deep` sets how much the turn may do, in place of Velven's pick. A quick turn writes and
|
|
205
|
+
builds the game. A standard turn also plays it and has its code reviewed. Deep is not available yet.
|
|
206
|
+
- `--plan` turns on plan mode, in place of Velven's pick: the turn writes or updates the project's design document and
|
|
207
|
+
changes no game files. `--no-plan` turns it off.
|
|
208
|
+
- The project keeps the effort and plan mode you last passed, for a later message that passes only one of them (with
|
|
209
|
+
`--effort` alone, a new standard or deep effort turns plan mode on, quick turns it off). A message with neither is
|
|
210
|
+
Velven's pick again, whatever you passed before; the CLI sends them only when you pass them.
|
|
179
211
|
- `--yes` sends without asking. Outside a terminal, the CLI never asks.
|
|
180
212
|
- `--detach` sends the message and returns at once. Follow the turn later with `velven create watch`.
|
|
181
213
|
- `--project <id>` sends to a project other than the current one.
|
|
214
|
+
- A message sent while a turn runs on the project waits behind it (at most five a project), holding no credits, and
|
|
215
|
+
starts on its own when that turn ends: the CLI prints "Queued: it runs after the turn that is running." and exits 0.
|
|
216
|
+
`velven create queue` lists what waits, `velven create queue rm <id>` removes one; cancelling the running turn
|
|
217
|
+
leaves the queue as it is. Following the turn that ends prints "Your next message is running: velven create watch
|
|
218
|
+
<turn>" (with "on project <id>" when the message that started waited on another of your projects; one waiting on
|
|
219
|
+
the same project starts first). A queued message that starts at once but waits for room on Velven says "Turn <id>
|
|
220
|
+
waits for room on Velven and starts on its own.", as any such send.
|
|
182
221
|
|
|
183
222
|
The CLI says what the turn does as it goes. What the agents write goes to standard output, and their progress goes to
|
|
184
|
-
standard error.
|
|
185
|
-
|
|
186
|
-
|
|
223
|
+
standard error. When Velven picked the work, it says so in words first ("I'll make the change."; when the day's
|
|
224
|
+
credits kept it from a fuller turn, "I'll make the change. I kept this one quick to save credits."), with the work it
|
|
225
|
+
picked and why on standard error. On a build it prints the plain step it is at ("Designing your game", "Painting the art", "Building the world", "Trying it out", "Polishing", "Finishing up"), and on a first build the design as it is settled ("Designing Petal Drift: ..." with its style and palette) and each picture once it lands ("Painted sky (1024 by 512)" with a link to look at it). You see what each step changed, the live link while the game is being built, each playtest's preview
|
|
226
|
+
link (the first playable one, before the turn ends), its screenshots and its clip. When the turn ends, it prints the reply (when the turn asked a question, which it already printed with its choices, only what follows it), then, after a turn that ended with a playable game, "Where it could go next:" with two to four short suggestions for this game and the command that sends the first (`velven create send "Add a boss at the end"`; each is an ordinary message), then the preview link and the credits used. A reply may end with one sentence on what the turn took from your pictures, and with "You're running low on credits for today." when what is left would not cover another small change. A turn that stops at
|
|
227
|
+
its credit cap or time limit saves what it finished and ends with exit 0. A failed turn costs nothing. When a new game's
|
|
228
|
+
first build stops before it made the game (it saved nothing, or only its design), the reply ends "Press Build it to try
|
|
229
|
+
again." and the CLI prints `Try again: velven create send --build "Build it"`; "try again" typed after it builds too.
|
|
187
230
|
|
|
188
231
|
When Velven is busy, a turn waits for room and starts on its own. The CLI prints "Still waiting for room on Velven."
|
|
189
232
|
and keeps following it, however long it waits. If the connection drops, the CLI reconnects and carries on where it
|
|
@@ -207,14 +250,15 @@ More than 20 tool calls or screenshots under one line fold into one; `--all` lis
|
|
|
207
250
|
newest turn of the current project, or of the project you name.
|
|
208
251
|
|
|
209
252
|
`velven create stats` is for Velven's admins (anyone else is told so, with exit 4). It prints one table for each effort,
|
|
210
|
-
plan mode and
|
|
253
|
+
plan mode, engine and, for the turns Velven routed, the work it picked ("quick fast first builds", "standard full later
|
|
254
|
+
builds", "quick interview turns", "quick draft turns" for a new game's short plan) over the turns created in the last `--days` days (`--origin` keeps the turns sent from one place:
|
|
211
255
|
`web`, `api`, `cli`, `mcp` or `benchmark`): how many finished and failed, p50 and p90 of the first live link, the first
|
|
212
256
|
change, the first playable preview and the whole turn, each phase, each job's cost in credits and USD cents and its model time, and the
|
|
213
|
-
single steps worth comparing, longest first.
|
|
257
|
+
single steps worth comparing, longest first. Under a group's heading, "the next message:" says how the message after its turns reacted to them, as Velven's router labelled it (moved on, corrected it, asked for checks), when any was labelled.
|
|
214
258
|
|
|
215
|
-
With `--json`, `new`, `send`, `list`, `show`, `credits` and `stats` print one JSON object, and `send` prints only the
|
|
216
|
-
finished turn; `show --timeline --json` prints the turn's id and its timeline (`started_at` and the spans). `watch --json`
|
|
217
|
-
is different: it prints each event of the turn as it comes, one JSON object per line.
|
|
259
|
+
With `--json`, `new`, `send`, `list`, `show`, `plan`, `queue`, `credits`, `usage` and `stats` print one JSON object, and `send` prints only the
|
|
260
|
+
finished turn (or the waiting message, `queued`, when it was queued); `show --timeline --json` prints the turn's id and its timeline (`started_at` and the spans). `watch --json`
|
|
261
|
+
is different: it prints each event of the turn as it comes, one JSON object per line (the plan and the brief as they are written included, which text mode leaves out until the last).
|
|
218
262
|
|
|
219
263
|
The current project and the last turn are saved in `~/.config/velven/create.json`, one set for each Velven
|
|
220
264
|
`VELVEN_API` points at.
|
package/dist/velven.js
CHANGED
|
@@ -23,7 +23,7 @@ class CliError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
// package.json
|
|
26
|
-
var version = "0.
|
|
26
|
+
var version = "0.4.0";
|
|
27
27
|
|
|
28
28
|
// src/version.ts
|
|
29
29
|
var VERSION = version;
|
|
@@ -76,16 +76,28 @@ function causeOf(e) {
|
|
|
76
76
|
return e instanceof Error && e.cause instanceof Error ? e.cause.message : e instanceof Error ? e.message : String(e);
|
|
77
77
|
}
|
|
78
78
|
async function call(api, method, path, body) {
|
|
79
|
+
return answerOf(api, path, body === undefined ? undefined : { type: "application/json", data: JSON.stringify(body) }, method);
|
|
80
|
+
}
|
|
81
|
+
async function putBytes(url, bytes, type, tooLarge) {
|
|
82
|
+
let response;
|
|
83
|
+
try {
|
|
84
|
+
response = await fetch(url, { method: "PUT", headers: { "content-type": type, "user-agent": USER_AGENT }, body: bytes });
|
|
85
|
+
} catch (e) {
|
|
86
|
+
throw new CliError(`Could not upload the picture (${causeOf(e)}).`, EXIT.failed, "network");
|
|
87
|
+
}
|
|
88
|
+
await response.text().catch(() => "");
|
|
89
|
+
if (response.status === 413)
|
|
90
|
+
throw new CliError(tooLarge, EXIT.refused, "too_large");
|
|
91
|
+
if (!response.ok)
|
|
92
|
+
throw new CliError(`The picture's upload was refused (HTTP ${response.status}). Try again in a moment.`, response.status >= 500 ? EXIT.failed : EXIT.refused, `http_${response.status}`);
|
|
93
|
+
}
|
|
94
|
+
async function answerOf(api, path, body, method) {
|
|
79
95
|
const headers = requestHeaders(api, "application/json");
|
|
80
96
|
if (body !== undefined)
|
|
81
|
-
headers["content-type"] =
|
|
97
|
+
headers["content-type"] = body.type;
|
|
82
98
|
let response;
|
|
83
99
|
try {
|
|
84
|
-
response = await fetch(`${api.base}${path}`, {
|
|
85
|
-
method,
|
|
86
|
-
headers,
|
|
87
|
-
body: body === undefined ? undefined : JSON.stringify(body)
|
|
88
|
-
});
|
|
100
|
+
response = await fetch(`${api.base}${path}`, { method, headers, body: body?.data });
|
|
89
101
|
} catch (e) {
|
|
90
102
|
throw new CliError(`Could not reach Velven at ${api.base} (${causeOf(e)}).`, EXIT.failed, "network");
|
|
91
103
|
}
|
|
@@ -408,7 +420,11 @@ function parseArgs(argv, spec) {
|
|
|
408
420
|
if (value === undefined || eq === -1 && value.startsWith("--")) {
|
|
409
421
|
throw new CliError(`--${name} needs a value.`, EXIT.usage, "usage");
|
|
410
422
|
}
|
|
411
|
-
|
|
423
|
+
if (kind === "strings") {
|
|
424
|
+
const given = flags[name];
|
|
425
|
+
flags[name] = [...Array.isArray(given) ? given : [], value];
|
|
426
|
+
} else
|
|
427
|
+
flags[name] = value;
|
|
412
428
|
}
|
|
413
429
|
return { positionals, flags };
|
|
414
430
|
}
|
|
@@ -416,6 +432,10 @@ function stringFlag(parsed, name) {
|
|
|
416
432
|
const value = parsed.flags[name];
|
|
417
433
|
return typeof value === "string" ? value : undefined;
|
|
418
434
|
}
|
|
435
|
+
function stringsFlag(parsed, name) {
|
|
436
|
+
const value = parsed.flags[name];
|
|
437
|
+
return Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
438
|
+
}
|
|
419
439
|
|
|
420
440
|
// src/context.ts
|
|
421
441
|
import { spawn } from "node:child_process";
|
|
@@ -467,6 +487,10 @@ function nodeContext() {
|
|
|
467
487
|
};
|
|
468
488
|
}
|
|
469
489
|
|
|
490
|
+
// src/create.ts
|
|
491
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
492
|
+
import { resolve } from "node:path";
|
|
493
|
+
|
|
470
494
|
// src/create-state.ts
|
|
471
495
|
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
472
496
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
@@ -512,7 +536,7 @@ var BAR_WIDTH = 60;
|
|
|
512
536
|
var FOLD_OVER = 20;
|
|
513
537
|
var LABEL_MAX = 44;
|
|
514
538
|
var CHAIN_SLACK_MS = 50;
|
|
515
|
-
var PHASE_ORDER = ["workspace", "planning", "coding", "building", "playtesting", "reviewing", "fixing", "saving", "previewing", "replying", "settle"];
|
|
539
|
+
var PHASE_ORDER = ["routing", "workspace", "planning", "coding", "building", "playtesting", "reviewing", "fixing", "saving", "previewing", "replying", "settle"];
|
|
516
540
|
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
517
541
|
function seconds(ms) {
|
|
518
542
|
return `${(ms / 1000).toFixed(1)} s`;
|
|
@@ -569,7 +593,11 @@ function spanFacts(span) {
|
|
|
569
593
|
fps_median: (v) => `${v} fps`,
|
|
570
594
|
memory_mb: (v) => `${v} MB memory`,
|
|
571
595
|
clocks: (v) => Number(v) > 1 ? "across two clocks" : null,
|
|
572
|
-
reasoning: (v) => Number(v) > 0 ? `reasoning ${count(Number(v))}` : null
|
|
596
|
+
reasoning: (v) => Number(v) > 0 ? `reasoning ${count(Number(v))}` : null,
|
|
597
|
+
route: (v) => `routed ${v}`,
|
|
598
|
+
route_by: (v) => v === "fallback" ? "the router did not answer" : null,
|
|
599
|
+
downgraded: (v) => v === true ? "full was over the hold" : null,
|
|
600
|
+
route_reason: (v) => `"${v}"`
|
|
573
601
|
};
|
|
574
602
|
if (span.kind === "model") {
|
|
575
603
|
const input = (num(a.input) ?? 0) + (num(a.cached) ?? 0);
|
|
@@ -707,7 +735,7 @@ function bar(row, total) {
|
|
|
707
735
|
return `|${" ".repeat(a)}${"#".repeat(b - a)}${" ".repeat(BAR_WIDTH - b)}|`;
|
|
708
736
|
}
|
|
709
737
|
function renderTimeline(turn, spans, options = {}) {
|
|
710
|
-
const kind =
|
|
738
|
+
const kind = turnKind(turn, spans);
|
|
711
739
|
if (spans.length === 0) {
|
|
712
740
|
const why = turn.status === "queued" || turn.status === "running" ? " A turn's timeline is written when it ends." : "";
|
|
713
741
|
return [`Turn ${turn.id}, a ${kind}, ${turn.status}: no timeline.${why}`];
|
|
@@ -768,7 +796,23 @@ function renderTimeline(turn, spans, options = {}) {
|
|
|
768
796
|
return lines;
|
|
769
797
|
}
|
|
770
798
|
function isFirstBuild(spans) {
|
|
771
|
-
return spans.some((s) => s.name === "beginTurn" && s.attrs?.first_build === true);
|
|
799
|
+
return spans.some((s) => (s.name === "beginTurn" || s.name === "askRouter") && s.attrs?.first_build === true);
|
|
800
|
+
}
|
|
801
|
+
function workOf(spans) {
|
|
802
|
+
const interview = spans.find((s) => s.name === "askInterviewer" && typeof s.attrs?.interview === "string")?.attrs?.interview;
|
|
803
|
+
if (interview === "ask")
|
|
804
|
+
return "interview";
|
|
805
|
+
if (interview === "draft")
|
|
806
|
+
return "draft";
|
|
807
|
+
const route = spans.find((s) => s.name === "askRouter" && typeof s.attrs?.route === "string")?.attrs?.route;
|
|
808
|
+
return typeof route === "string" ? route : null;
|
|
809
|
+
}
|
|
810
|
+
function turnKind(turn, spans) {
|
|
811
|
+
const work = workOf(spans);
|
|
812
|
+
if (work === "interview" || work === "draft")
|
|
813
|
+
return `${turn.effort} ${work} turn`;
|
|
814
|
+
const what = turn.plan ? "plan turn" : isFirstBuild(spans) ? "first build" : "build turn";
|
|
815
|
+
return `${turn.effort} ${work === "fast" || work === "full" ? `${work} ` : ""}${what}`;
|
|
772
816
|
}
|
|
773
817
|
function firsts(timing) {
|
|
774
818
|
if (!timing || timing.first_dev_ms === undefined && timing.first_write_ms === undefined && timing.first_preview_ms === undefined)
|
|
@@ -824,8 +868,11 @@ function table(rows, indent) {
|
|
|
824
868
|
var sec = (ms) => ms === null ? "-" : seconds(ms);
|
|
825
869
|
var pair = (p, f) => `${f(p.p50)} / ${f(p.p90)}`;
|
|
826
870
|
function groupTitle(g) {
|
|
871
|
+
if (g.work === "interview" || g.work === "draft")
|
|
872
|
+
return `${g.effort} ${g.work} turns, ${g.engine}`;
|
|
827
873
|
const what = g.plan ? "plan turns" : g.first_build === true ? "first builds" : g.first_build === false ? "later builds" : "build turns";
|
|
828
|
-
|
|
874
|
+
const work = g.work === "plan" ? "routed " : g.work ? `${g.work} ` : "";
|
|
875
|
+
return `${g.effort} ${work}${what}, ${g.engine}`;
|
|
829
876
|
}
|
|
830
877
|
function renderStats(groups, options) {
|
|
831
878
|
const over = `the last ${plural(options.days, "day")}${options.origin ? `, from ${options.origin}` : ""}`;
|
|
@@ -837,6 +884,9 @@ function renderStats(groups, options) {
|
|
|
837
884
|
const lines = [`Turns created in ${over}: p50 and p90 over the done turns.`];
|
|
838
885
|
for (const g of groups) {
|
|
839
886
|
lines.push("", `${groupTitle(g)}: ${plural(g.turns, "done turn")}, ${g.failed} failed`);
|
|
887
|
+
const reacted = REACTION_WORDS.filter(([key]) => (g.reactions?.[key] ?? 0) > 0).map(([key, words]) => `${words} ${g.reactions[key]}`);
|
|
888
|
+
if (reacted.length)
|
|
889
|
+
lines.push(` the next message: ${reacted.join(", ")}`);
|
|
840
890
|
const figures = [
|
|
841
891
|
["figure", "p50", "p90", "turns"],
|
|
842
892
|
["first live link", sec(g.first_dev_ms.p50), sec(g.first_dev_ms.p90), ""],
|
|
@@ -865,6 +915,12 @@ function renderStats(groups, options) {
|
|
|
865
915
|
}
|
|
866
916
|
return lines;
|
|
867
917
|
}
|
|
918
|
+
var REACTION_WORDS = [
|
|
919
|
+
["moved_on", "moved on"],
|
|
920
|
+
["corrected", "corrected it"],
|
|
921
|
+
["asked_checks", "asked for checks"],
|
|
922
|
+
["none", "no reaction"]
|
|
923
|
+
];
|
|
868
924
|
function rank(phase) {
|
|
869
925
|
const i = PHASE_ORDER.indexOf(phase);
|
|
870
926
|
return i === -1 ? PHASE_ORDER.length : i;
|
|
@@ -875,13 +931,24 @@ var CREATE_HELP = `velven create: describe a game, and Velven's agents build it.
|
|
|
875
931
|
|
|
876
932
|
Usage:
|
|
877
933
|
velven create new [title...] Start a project; it becomes the current one
|
|
878
|
-
--engine <id> The engine pack (default
|
|
879
|
-
--genre <id> The genre pack (default
|
|
880
|
-
|
|
934
|
+
--engine <id> The engine pack (default: Velven picks, 3D unless your idea is clearly 2D)
|
|
935
|
+
--genre <id> The genre pack (default none: an open game, Velven follows your idea;
|
|
936
|
+
--genre arcade holds it to arcade's rules)
|
|
937
|
+
velven create send <text...> Send the project a message: prints the estimate, asks, then follows the turn.
|
|
938
|
+
Velven picks how much to do (a quick change, a full build with a playtest,
|
|
939
|
+
a plan; on a new game a question or two, then a short plan to shape) unless
|
|
940
|
+
you pass --effort or a plan flag. Sent while a turn runs, it waits its turn
|
|
881
941
|
--project <id> A project other than the current one
|
|
882
|
-
--effort quick|standard|deep How much the turn may do
|
|
883
|
-
|
|
884
|
-
|
|
942
|
+
--effort quick|standard|deep How much the turn may do, instead of Velven's pick. The project keeps it
|
|
943
|
+
for a later message that passes only a plan flag; a message with neither
|
|
944
|
+
flag is Velven's pick again
|
|
945
|
+
--plan, --no-plan Plan mode, instead of Velven's pick: the turn updates the design document
|
|
946
|
+
and changes no game files. Kept as --effort is (with --effort alone, on for
|
|
947
|
+
a new standard or deep, off for quick)
|
|
948
|
+
--build Build it: no more questions, Velven fills in what the plan leaves open
|
|
949
|
+
(Velven still picks how much)
|
|
950
|
+
--ref <file|ref-n> A reference picture (PNG, JPEG or WebP, up to 10 MB) sent with the message, or
|
|
951
|
+
one the project keeps by its id (ref-3); up to 4
|
|
885
952
|
--yes, -y Do not ask before sending
|
|
886
953
|
--detach Send it and return at once; follow it later with watch
|
|
887
954
|
velven create watch [turn] Follow a turn to its end (default: the last one)
|
|
@@ -894,19 +961,31 @@ Usage:
|
|
|
894
961
|
--turn <id> With --timeline: this turn instead of the project's newest
|
|
895
962
|
--all With --timeline: list every tool call and screenshot (past 20 under one
|
|
896
963
|
step they fold into one line)
|
|
964
|
+
velven create refs The project's reference pictures: each id, when it was added and how many
|
|
965
|
+
messages carried it (a project keeps up to 16)
|
|
966
|
+
--project <id> A project other than the current one
|
|
967
|
+
velven create refs rm <ref-n> Delete a picture, sent or not; the messages that carried it keep its number
|
|
968
|
+
velven create plan The project's short plan: what the game is, how it plays, its look
|
|
969
|
+
--project <id> A project other than the current one
|
|
970
|
+
velven create queue The messages waiting behind the running turn, in the order they run
|
|
971
|
+
--project <id> A project other than the current one
|
|
972
|
+
velven create queue rm <id> Remove a waiting message
|
|
897
973
|
velven create credits Your credits and the latest entries
|
|
974
|
+
velven create usage What each of your turns held and used, newest first
|
|
975
|
+
--project <id> Only this project's turns
|
|
976
|
+
--before <turn> The page before this turn (the line at the end names it)
|
|
898
977
|
velven create stats Admins: p50 and p90 over recent turns, by effort, plan mode and engine
|
|
899
978
|
--days <n> Turns created in the last n days, 1 to 90 (default 7)
|
|
900
979
|
--origin <origin> Only turns sent from web, api, cli, mcp or benchmark
|
|
901
980
|
|
|
902
|
-
--json: new, send, list, show, credits and stats print one JSON object (send prints the finished turn,
|
|
981
|
+
--json: new, send, list, show, plan, queue, refs, credits, usage and stats print one JSON object (send prints the finished turn,
|
|
903
982
|
show --timeline the timeline); watch prints one event per line.
|
|
904
983
|
Ctrl+C stops following a turn; the turn goes on.
|
|
905
984
|
|
|
906
985
|
Exit codes:
|
|
907
986
|
0 done (also a turn that stopped at its credit cap or time limit) 1 failed, cancelled or no longer followed
|
|
908
987
|
2 bad command 3 needs sign-in 4 refused (no credits, closed, busy, too many turns, effort unavailable,
|
|
909
|
-
not found) 5 rate limited, try later`;
|
|
988
|
+
a picture refused, not found) 5 rate limited, try later`;
|
|
910
989
|
var CREATE_FLAGS = {
|
|
911
990
|
engine: "string",
|
|
912
991
|
genre: "string",
|
|
@@ -914,6 +993,8 @@ var CREATE_FLAGS = {
|
|
|
914
993
|
effort: "string",
|
|
915
994
|
plan: "boolean",
|
|
916
995
|
"no-plan": "boolean",
|
|
996
|
+
build: "boolean",
|
|
997
|
+
ref: "strings",
|
|
917
998
|
yes: "boolean",
|
|
918
999
|
detach: "boolean",
|
|
919
1000
|
from: "string",
|
|
@@ -922,19 +1003,26 @@ var CREATE_FLAGS = {
|
|
|
922
1003
|
turn: "string",
|
|
923
1004
|
all: "boolean",
|
|
924
1005
|
days: "string",
|
|
925
|
-
origin: "string"
|
|
1006
|
+
origin: "string",
|
|
1007
|
+
before: "string"
|
|
926
1008
|
};
|
|
927
1009
|
var SUBCOMMANDS = {
|
|
928
1010
|
new: ["engine", "genre", "json"],
|
|
929
|
-
send: ["project", "effort", "plan", "no-plan", "yes", "detach", "json"],
|
|
1011
|
+
send: ["project", "effort", "plan", "no-plan", "build", "ref", "yes", "detach", "json"],
|
|
930
1012
|
watch: ["from", "json"],
|
|
931
1013
|
cancel: [],
|
|
932
1014
|
list: ["json"],
|
|
933
1015
|
show: ["json", "timeline", "turn", "all"],
|
|
1016
|
+
refs: ["project", "json"],
|
|
1017
|
+
plan: ["project", "json"],
|
|
1018
|
+
queue: ["project", "json"],
|
|
934
1019
|
credits: ["json"],
|
|
1020
|
+
usage: ["project", "before", "json"],
|
|
935
1021
|
stats: ["days", "origin", "json"]
|
|
936
1022
|
};
|
|
937
1023
|
var EFFORTS = ["quick", "standard", "deep"];
|
|
1024
|
+
var REFS_PER_MESSAGE = 4;
|
|
1025
|
+
var REFS_PER_PROJECT = 16;
|
|
938
1026
|
var ORIGINS = ["web", "api", "cli", "mcp", "benchmark"];
|
|
939
1027
|
var ROOT = "/api/v1/create";
|
|
940
1028
|
async function create(ctx, parsed) {
|
|
@@ -974,6 +1062,14 @@ async function create(ctx, parsed) {
|
|
|
974
1062
|
return show(ctx, api, args[0], json);
|
|
975
1063
|
case "stats":
|
|
976
1064
|
return stats(ctx, api, stringFlag(parsed, "days"), stringFlag(parsed, "origin"), json);
|
|
1065
|
+
case "refs":
|
|
1066
|
+
return referencePictures(ctx, api, args, stringFlag(parsed, "project"), json);
|
|
1067
|
+
case "usage":
|
|
1068
|
+
return usage(ctx, api, stringFlag(parsed, "project"), stringFlag(parsed, "before"), json);
|
|
1069
|
+
case "plan":
|
|
1070
|
+
return showPlan(ctx, api, args, stringFlag(parsed, "project"), json);
|
|
1071
|
+
case "queue":
|
|
1072
|
+
return queue(ctx, api, args, stringFlag(parsed, "project"), json);
|
|
977
1073
|
default:
|
|
978
1074
|
return credits(ctx, api, json);
|
|
979
1075
|
}
|
|
@@ -993,7 +1089,11 @@ async function newProject(ctx, api, title, engine, genre, json) {
|
|
|
993
1089
|
return EXIT.ok;
|
|
994
1090
|
}
|
|
995
1091
|
ctx.out(`Started ${project.title ?? "a project"} (${project.id}): ${packs(project)}. It is now the current project.`);
|
|
996
|
-
|
|
1092
|
+
if (!project.genre)
|
|
1093
|
+
ctx.out("An open game: Velven follows your idea. --genre arcade holds a project to arcade's rules.");
|
|
1094
|
+
if (!engine)
|
|
1095
|
+
ctx.out("Velven picks the engine from your idea: 3D unless it is clearly 2D.");
|
|
1096
|
+
ctx.out('Tell it what to build: velven create send "a cozy game where you grow a garden on a floating island"');
|
|
997
1097
|
return EXIT.ok;
|
|
998
1098
|
}
|
|
999
1099
|
async function send(ctx, api, words, parsed, json) {
|
|
@@ -1008,17 +1108,32 @@ async function send(ctx, api, words, parsed, json) {
|
|
|
1008
1108
|
throw new CliError("Pass --plan or --no-plan, not both.", EXIT.usage, "usage");
|
|
1009
1109
|
}
|
|
1010
1110
|
const plan = parsed.flags.plan === true ? true : parsed.flags["no-plan"] === true ? false : undefined;
|
|
1111
|
+
const routed = effort === undefined && plan === undefined;
|
|
1112
|
+
const refs = stringsFlag(parsed, "ref");
|
|
1113
|
+
if (refs.length > REFS_PER_MESSAGE)
|
|
1114
|
+
throw new CliError(`A message takes up to ${REFS_PER_MESSAGE} pictures; ${refs.length} were given with --ref.`, EXIT.usage, "usage");
|
|
1115
|
+
const pictures = await Promise.all(refs.map((ref) => REF_ID.test(ref) ? ref : readPicture(ctx, ref)));
|
|
1011
1116
|
const project = await currentProject(ctx, api.base, stringFlag(parsed, "project"));
|
|
1012
1117
|
const say = json ? () => {} : (line) => ctx.out(line);
|
|
1013
1118
|
const query = new URLSearchParams;
|
|
1119
|
+
if (routed)
|
|
1120
|
+
query.set("routed", "true");
|
|
1121
|
+
if (routed && parsed.flags.build === true)
|
|
1122
|
+
query.set("intent", "build");
|
|
1014
1123
|
if (effort !== undefined)
|
|
1015
1124
|
query.set("effort", effort);
|
|
1016
1125
|
if (plan !== undefined)
|
|
1017
1126
|
query.set("plan", String(plan));
|
|
1127
|
+
query.set("queue", "true");
|
|
1018
1128
|
const search = query.toString();
|
|
1019
1129
|
const { estimate } = await call(api, "GET", `${ROOT}/projects/${seg(project)}/estimate${search ? `?${search}` : ""}`);
|
|
1020
|
-
|
|
1021
|
-
|
|
1130
|
+
if (routed) {
|
|
1131
|
+
const what = estimate.shaping === true ? "a question or a short plan" : estimate.first_build === true ? "a first build (its pictures are made before the first preview)" : "a small change";
|
|
1132
|
+
say(`Velven picks how much to do: about ${estimate.credits.typical} credits for ${what}, at most ${estimate.credits.cap} held. You have ${estimate.available} available.`);
|
|
1133
|
+
} else {
|
|
1134
|
+
const kind = estimate.plan ? `${estimate.effort} plan turn` : estimate.first_build === true ? `${estimate.effort} first build (its pictures are made before the first preview)` : `${estimate.effort} build turn`;
|
|
1135
|
+
say(`A ${kind}: about ${estimate.credits.typical} credits, at most ${estimate.credits.cap}. You have ${estimate.available} available.`);
|
|
1136
|
+
}
|
|
1022
1137
|
if (!estimate.enough) {
|
|
1023
1138
|
throw new CliError(estimate.reason ?? "Velven cannot start this turn now.", EXIT.refused, "not_enough");
|
|
1024
1139
|
}
|
|
@@ -1029,13 +1144,39 @@ async function send(ctx, api, words, parsed, json) {
|
|
|
1029
1144
|
return EXIT.failed;
|
|
1030
1145
|
}
|
|
1031
1146
|
}
|
|
1032
|
-
const body = { text };
|
|
1147
|
+
const body = { text, queue: true };
|
|
1033
1148
|
if (effort !== undefined)
|
|
1034
1149
|
body.effort = effort;
|
|
1035
1150
|
if (plan !== undefined)
|
|
1036
1151
|
body.plan = plan;
|
|
1152
|
+
if (parsed.flags.build === true)
|
|
1153
|
+
body.intent = "build";
|
|
1154
|
+
if (pictures.length) {
|
|
1155
|
+
const ids = [];
|
|
1156
|
+
for (const picture of pictures)
|
|
1157
|
+
ids.push(typeof picture === "string" ? picture : await uploadPicture(api, project, picture));
|
|
1158
|
+
body.references = ids;
|
|
1159
|
+
say(`Attached ${plural2(ids.length, "picture")}: ${ids.join(", ")}.`);
|
|
1160
|
+
}
|
|
1037
1161
|
const sent = await call(api, "POST", `${ROOT}/projects/${seg(project)}/messages`, body);
|
|
1038
|
-
|
|
1162
|
+
let turn;
|
|
1163
|
+
let stream2;
|
|
1164
|
+
if ("queued" in sent) {
|
|
1165
|
+
const queued = sent.queued;
|
|
1166
|
+
if (queued.state === "refused")
|
|
1167
|
+
throw new CliError(queued.refused ?? "Velven could not start this message.", EXIT.refused, "refused");
|
|
1168
|
+
if (queued.state === "waiting" || queued.turn === null) {
|
|
1169
|
+
if (json)
|
|
1170
|
+
ctx.out(JSON.stringify({ ok: true, queued }));
|
|
1171
|
+
else
|
|
1172
|
+
say(queuedLine(queued));
|
|
1173
|
+
return EXIT.ok;
|
|
1174
|
+
}
|
|
1175
|
+
turn = { id: queued.turn, ...queued.capped === true ? { queued: true } : {} };
|
|
1176
|
+
} else {
|
|
1177
|
+
turn = sent.turn;
|
|
1178
|
+
stream2 = sent.stream;
|
|
1179
|
+
}
|
|
1039
1180
|
await saveCreateState(ctx, api.base, { project, turn: turn.id });
|
|
1040
1181
|
if (turn.queued)
|
|
1041
1182
|
say(`Turn ${turn.id} waits for room on Velven and starts on its own.`);
|
|
@@ -1048,8 +1189,46 @@ async function send(ctx, api, words, parsed, json) {
|
|
|
1048
1189
|
say(`Follow it: velven create watch ${turn.id}; stop it: velven create cancel ${turn.id}`);
|
|
1049
1190
|
return EXIT.ok;
|
|
1050
1191
|
}
|
|
1051
|
-
const path =
|
|
1052
|
-
return follow(ctx, api, turn.id, path, undefined, json ? "final" : "text");
|
|
1192
|
+
const path = stream2?.startsWith("/") ? stream2 : `${ROOT}/turns/${seg(turn.id)}/stream`;
|
|
1193
|
+
return follow(ctx, api, turn.id, path, undefined, json ? "final" : "text", project);
|
|
1194
|
+
}
|
|
1195
|
+
var REF_ID = /^ref-[1-9][0-9]{0,2}$/;
|
|
1196
|
+
async function uploadPicture(api, project, picture) {
|
|
1197
|
+
const { upload } = await call(api, "POST", `${ROOT}/projects/${seg(project)}/references/uploads`);
|
|
1198
|
+
const tooLarge = `The picture ${picture.file} is larger than ${Math.floor(upload.max_bytes / (1024 * 1024))} MB. Send a smaller one.`;
|
|
1199
|
+
if (picture.bytes.length > upload.max_bytes)
|
|
1200
|
+
throw new CliError(tooLarge, EXIT.refused, "too_large");
|
|
1201
|
+
await putBytes(upload.url, picture.bytes, picture.type, tooLarge);
|
|
1202
|
+
try {
|
|
1203
|
+
const { reference } = await call(api, "POST", `${ROOT}/projects/${seg(project)}/references`, { upload: upload.id });
|
|
1204
|
+
return reference.id;
|
|
1205
|
+
} catch (e) {
|
|
1206
|
+
if (e instanceof CliError && e.code === "too_many_references") {
|
|
1207
|
+
const named = `velven create refs --project ${project}`;
|
|
1208
|
+
throw new CliError(`${e.message} See them: ${named}; remove one: velven create refs rm <ref-n> --project ${project}.`, e.exitCode, e.code);
|
|
1209
|
+
}
|
|
1210
|
+
throw e;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
var PICTURE_TYPES = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp" };
|
|
1214
|
+
async function readPicture(ctx, file) {
|
|
1215
|
+
let bytes;
|
|
1216
|
+
try {
|
|
1217
|
+
bytes = new Uint8Array(await readFile3(resolve(ctx.cwd, file)));
|
|
1218
|
+
} catch {
|
|
1219
|
+
throw new CliError(`Could not read the picture ${file}.`, EXIT.usage, "usage");
|
|
1220
|
+
}
|
|
1221
|
+
const ext = file.toLowerCase().split(".").pop() ?? "";
|
|
1222
|
+
return { file, bytes, type: PICTURE_TYPES[ext] ?? "application/octet-stream" };
|
|
1223
|
+
}
|
|
1224
|
+
async function turnProject(ctx, api, turn) {
|
|
1225
|
+
const { project: current } = await readCreateState(ctx, api.base);
|
|
1226
|
+
try {
|
|
1227
|
+
const { turn: view } = await call(api, "GET", `${ROOT}/turns/${seg(turn)}`);
|
|
1228
|
+
return view.project ?? current ?? null;
|
|
1229
|
+
} catch {
|
|
1230
|
+
return current ?? null;
|
|
1231
|
+
}
|
|
1053
1232
|
}
|
|
1054
1233
|
async function watch(ctx, api, arg, fromFlag, json) {
|
|
1055
1234
|
let from;
|
|
@@ -1059,9 +1238,10 @@ async function watch(ctx, api, arg, fromFlag, json) {
|
|
|
1059
1238
|
from = Number(fromFlag);
|
|
1060
1239
|
}
|
|
1061
1240
|
const turn = await turnId(ctx, api.base, arg, "watch");
|
|
1241
|
+
const named = json ? null : await turnProject(ctx, api, turn);
|
|
1062
1242
|
let code;
|
|
1063
1243
|
try {
|
|
1064
|
-
code = await follow(ctx, api, turn, `${ROOT}/turns/${seg(turn)}/stream`, from, json ? "events" : "text");
|
|
1244
|
+
code = await follow(ctx, api, turn, `${ROOT}/turns/${seg(turn)}/stream`, from, json ? "events" : "text", named);
|
|
1065
1245
|
} catch (e) {
|
|
1066
1246
|
if (e instanceof CliError && e.code === "stream_lost")
|
|
1067
1247
|
await saveCreateState(ctx, api.base, { turn });
|
|
@@ -1070,10 +1250,10 @@ async function watch(ctx, api, arg, fromFlag, json) {
|
|
|
1070
1250
|
await saveCreateState(ctx, api.base, { turn });
|
|
1071
1251
|
return code;
|
|
1072
1252
|
}
|
|
1073
|
-
async function follow(ctx, api, turn, path, from, mode) {
|
|
1253
|
+
async function follow(ctx, api, turn, path, from, mode, project) {
|
|
1074
1254
|
const controller = new AbortController;
|
|
1075
1255
|
ctx.untilStopped().then(() => controller.abort());
|
|
1076
|
-
const said = { held: null, engine: null };
|
|
1256
|
+
const said = { held: null, engine: null, project, asked: false, next: [] };
|
|
1077
1257
|
let final;
|
|
1078
1258
|
try {
|
|
1079
1259
|
final = await stream(api, path, { from, signal: controller.signal, sleep: ctx.sleep }, (event2) => {
|
|
@@ -1118,7 +1298,7 @@ async function follow(ctx, api, turn, path, from, mode) {
|
|
|
1118
1298
|
}
|
|
1119
1299
|
ctx.out(JSON.stringify({ ok: code === EXIT.ok, ...error, turn: view }));
|
|
1120
1300
|
} else if (mode === "text") {
|
|
1121
|
-
finish(ctx, turn, event);
|
|
1301
|
+
finish(ctx, turn, event, said);
|
|
1122
1302
|
}
|
|
1123
1303
|
return code;
|
|
1124
1304
|
}
|
|
@@ -1165,12 +1345,51 @@ function render(ctx, event, said) {
|
|
|
1165
1345
|
release(ctx, said);
|
|
1166
1346
|
switch (event.type) {
|
|
1167
1347
|
case "turn.started":
|
|
1168
|
-
|
|
1348
|
+
if (event.routed === true)
|
|
1349
|
+
ctx.err(`Started: Velven is choosing the work, ${plural2(event.credits_held, "credit")} held.`);
|
|
1350
|
+
else
|
|
1351
|
+
ctx.err(`Started: ${event.effort}${event.plan ? ", plan mode" : ""}, ${plural2(event.credits_held, "credit")} held.`);
|
|
1352
|
+
return;
|
|
1353
|
+
case "route":
|
|
1354
|
+
ctx.out(event.said);
|
|
1355
|
+
ctx.err(` (${event.work}, ${event.effort}${event.plan ? ", plan mode" : ""}, ${plural2(event.credits_held, "credit")} held: ${event.reason})`);
|
|
1356
|
+
return;
|
|
1357
|
+
case "ask": {
|
|
1358
|
+
ctx.out(`${event.question} (question ${event.index} of ${event.of})`);
|
|
1359
|
+
for (const [i, choice] of event.choices.entries())
|
|
1360
|
+
ctx.out(` ${i + 1}. ${choice}`);
|
|
1361
|
+
const send2 = sendCommand(said);
|
|
1362
|
+
ctx.out(`Answer: ${send2} "${event.choices[0]}", or in your own words.`);
|
|
1363
|
+
if (event.picture || event.topic === "style")
|
|
1364
|
+
ctx.out(`Or show the look: ${send2} --ref <file> "like this".`);
|
|
1365
|
+
ctx.out(`Just build it: ${send2} --build "Just build it"`);
|
|
1366
|
+
said.asked = true;
|
|
1169
1367
|
return;
|
|
1368
|
+
}
|
|
1369
|
+
case "plan":
|
|
1370
|
+
if (!event.done)
|
|
1371
|
+
return;
|
|
1372
|
+
for (const line of planCard(event.plan))
|
|
1373
|
+
ctx.out(line);
|
|
1374
|
+
ctx.out(`Build it: ${sendCommand(said)} --build "Build it"`);
|
|
1375
|
+
return;
|
|
1376
|
+
case "brief": {
|
|
1377
|
+
if (!event.done)
|
|
1378
|
+
return;
|
|
1379
|
+
if (event.title || event.pitch)
|
|
1380
|
+
ctx.out(`Designing ${event.title ?? "your game"}${event.pitch ? `: ${event.pitch}` : ""}`);
|
|
1381
|
+
if (event.style)
|
|
1382
|
+
ctx.out(` Style: ${event.style}`);
|
|
1383
|
+
if (event.palette.length)
|
|
1384
|
+
ctx.out(` Palette: ${event.palette.join(", ")}`);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1170
1387
|
case "turn.queued":
|
|
1171
1388
|
ctx.err("Still waiting for room on Velven.");
|
|
1172
1389
|
return;
|
|
1173
1390
|
case "phase":
|
|
1391
|
+
if (event.progress)
|
|
1392
|
+
ctx.out(event.progress.label);
|
|
1174
1393
|
ctx.err(`${capital(event.phase)}${event.detail ? `: ${event.detail}` : ""}`);
|
|
1175
1394
|
return;
|
|
1176
1395
|
case "job": {
|
|
@@ -1192,6 +1411,8 @@ function render(ctx, event, said) {
|
|
|
1192
1411
|
}
|
|
1193
1412
|
case "tool":
|
|
1194
1413
|
ctx.err(` ${event.name}: ${event.summary}${typeof event.ms === "number" ? ` (${duration(event.ms)})` : ""}`);
|
|
1414
|
+
if (event.picture?.url)
|
|
1415
|
+
ctx.out(`Painted ${event.picture.id} (${event.picture.width} by ${event.picture.height}) ${dim(ctx, event.picture.url)}`);
|
|
1195
1416
|
return;
|
|
1196
1417
|
case "text":
|
|
1197
1418
|
ctx.out(event.text);
|
|
@@ -1245,18 +1466,32 @@ function render(ctx, event, said) {
|
|
|
1245
1466
|
case "cost":
|
|
1246
1467
|
ctx.err(`${event.credits_used} of ${plural2(event.credits_held, "credit")} used so far.`);
|
|
1247
1468
|
return;
|
|
1469
|
+
case "next":
|
|
1470
|
+
said.next = event.choices;
|
|
1471
|
+
return;
|
|
1248
1472
|
default:
|
|
1249
1473
|
return;
|
|
1250
1474
|
}
|
|
1251
1475
|
}
|
|
1252
|
-
function finish(ctx, turn, event) {
|
|
1476
|
+
function finish(ctx, turn, event, said) {
|
|
1253
1477
|
if (event.type === "error") {
|
|
1254
1478
|
const tail = event.code === "turn_stopped" || event.code === "not_found" ? " Check it again later: velven create show" : ` The turn may go on. Watch it: velven create watch ${turn}; check it: velven create show`;
|
|
1255
1479
|
ctx.err(`${event.message || "The turn's stream answered with an error."}${tail}`);
|
|
1256
1480
|
return;
|
|
1257
1481
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1482
|
+
const reply = event.reply && said.asked ? event.reply.split(`
|
|
1483
|
+
|
|
1484
|
+
`).slice(1).join(`
|
|
1485
|
+
|
|
1486
|
+
`) : event.reply;
|
|
1487
|
+
if (reply)
|
|
1488
|
+
ctx.out(reply);
|
|
1489
|
+
if (said.next.length) {
|
|
1490
|
+
ctx.out("Where it could go next:");
|
|
1491
|
+
for (const [i, choice] of said.next.entries())
|
|
1492
|
+
ctx.out(` ${i + 1}. ${choice}`);
|
|
1493
|
+
ctx.out(`Send one: velven create send${said.project ? ` --project ${said.project}` : ""} "${said.next[0]}"`);
|
|
1494
|
+
}
|
|
1260
1495
|
if (event.preview)
|
|
1261
1496
|
ctx.out(`Play it: ${event.preview.url}`);
|
|
1262
1497
|
const back = event.refunded > 0 ? `; ${plural2(event.refunded, "credit")} came back` : "";
|
|
@@ -1266,13 +1501,56 @@ function finish(ctx, turn, event) {
|
|
|
1266
1501
|
ctx.out("It stopped at its credit cap. What it finished is saved: send another message to go on.");
|
|
1267
1502
|
if (event.stop_reason === "time")
|
|
1268
1503
|
ctx.out("It stopped at its time limit. What it finished is saved: send another message to go on.");
|
|
1269
|
-
|
|
1270
|
-
}
|
|
1271
|
-
if (event.status === "cancelled") {
|
|
1504
|
+
} else if (event.status === "cancelled") {
|
|
1272
1505
|
ctx.err(`Turn ${turn} was cancelled: ${plural2(event.credits_charged, "credit")} used${back}. What it finished is saved.`);
|
|
1273
|
-
|
|
1506
|
+
} else {
|
|
1507
|
+
ctx.err(`Turn ${turn} failed: ${event.error ?? "Velven could not finish it."} A failed turn costs nothing.`);
|
|
1274
1508
|
}
|
|
1275
|
-
|
|
1509
|
+
if (reply?.includes(BUILD_AGAIN))
|
|
1510
|
+
ctx.out(`Try again: ${sendCommand(said)} --build "Build it"`);
|
|
1511
|
+
if (event.next_turn) {
|
|
1512
|
+
const elsewhere = event.next_project && said.project && event.next_project !== said.project ? ` on project ${event.next_project}` : "";
|
|
1513
|
+
ctx.out(`Your next message${elsewhere} is running: velven create watch ${event.next_turn}`);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
var BUILD_AGAIN = "Press Build it to try again.";
|
|
1517
|
+
function sendCommand(said) {
|
|
1518
|
+
return `velven create send${said.project ? ` --project ${said.project}` : ""}`;
|
|
1519
|
+
}
|
|
1520
|
+
function dim(ctx, text) {
|
|
1521
|
+
return ctx.interactive && !ctx.env.NO_COLOR ? `\x1B[2m${text}\x1B[22m` : text;
|
|
1522
|
+
}
|
|
1523
|
+
function planCard(plan) {
|
|
1524
|
+
const lines = [];
|
|
1525
|
+
if (plan.title)
|
|
1526
|
+
lines.push(plan.title);
|
|
1527
|
+
if (plan.pitch)
|
|
1528
|
+
lines.push(plan.pitch);
|
|
1529
|
+
if (plan.dimension)
|
|
1530
|
+
lines.push(`${plan.dimension === "3d" ? "3D" : "2D"}${plan.camera ? `, ${plan.camera}` : ""}`);
|
|
1531
|
+
if (plan.play?.length) {
|
|
1532
|
+
lines.push("How it plays:");
|
|
1533
|
+
for (const [i, beat] of plan.play.entries())
|
|
1534
|
+
lines.push(` ${i + 1}. ${beat}`);
|
|
1535
|
+
}
|
|
1536
|
+
if (plan.look)
|
|
1537
|
+
lines.push(`Look: ${plan.look}`);
|
|
1538
|
+
if (plan.palette?.length)
|
|
1539
|
+
lines.push(`Palette: ${plan.palette.join(", ")}`);
|
|
1540
|
+
if (plan.contents?.length) {
|
|
1541
|
+
lines.push("What is in it:");
|
|
1542
|
+
for (const thing of plan.contents)
|
|
1543
|
+
lines.push(` - ${thing}`);
|
|
1544
|
+
}
|
|
1545
|
+
if (plan.ending)
|
|
1546
|
+
lines.push(`How it ends: ${plan.ending}`);
|
|
1547
|
+
if (plan.score !== undefined)
|
|
1548
|
+
lines.push(plan.score ? "It keeps a score." : "No score.");
|
|
1549
|
+
return lines;
|
|
1550
|
+
}
|
|
1551
|
+
function queuedLine(queued) {
|
|
1552
|
+
const ahead = queued.position !== null && queued.position > 1 ? ` and ${plural2(queued.position - 1, "message")} ahead of it` : "";
|
|
1553
|
+
return `Queued: it runs after the turn that is running${ahead}. velven create queue lists what waits.`;
|
|
1276
1554
|
}
|
|
1277
1555
|
async function cancel(ctx, api, arg) {
|
|
1278
1556
|
const turn = await turnId(ctx, api.base, arg, "cancel");
|
|
@@ -1383,6 +1661,125 @@ async function stats(ctx, api, daysFlag, origin, json) {
|
|
|
1383
1661
|
ctx.out(line);
|
|
1384
1662
|
return EXIT.ok;
|
|
1385
1663
|
}
|
|
1664
|
+
async function referencePictures(ctx, api, args, projectFlag, json) {
|
|
1665
|
+
const [action, id, ...rest] = args;
|
|
1666
|
+
if (action === "rm") {
|
|
1667
|
+
if (id === undefined || !REF_ID.test(id) || rest.length) {
|
|
1668
|
+
throw new CliError("Name one picture to remove by its id, like velven create refs rm ref-3.", EXIT.usage, "usage");
|
|
1669
|
+
}
|
|
1670
|
+
const project2 = await currentProject(ctx, api.base, projectFlag);
|
|
1671
|
+
await call(api, "DELETE", `${ROOT}/projects/${seg(project2)}/references/${seg(id)}`);
|
|
1672
|
+
if (json)
|
|
1673
|
+
ctx.out(JSON.stringify({ ok: true, deleted: id }));
|
|
1674
|
+
else
|
|
1675
|
+
ctx.out(`Removed ${id}. The messages that carried it keep its number, shown as removed.`);
|
|
1676
|
+
return EXIT.ok;
|
|
1677
|
+
}
|
|
1678
|
+
if (action !== undefined) {
|
|
1679
|
+
throw new CliError(`Unknown command "create refs ${action}". List the pictures with velven create refs, remove one with velven create refs rm ref-3.`, EXIT.usage, "usage");
|
|
1680
|
+
}
|
|
1681
|
+
const project = await currentProject(ctx, api.base, projectFlag);
|
|
1682
|
+
const { references } = await call(api, "GET", `${ROOT}/projects/${seg(project)}/references`);
|
|
1683
|
+
if (json) {
|
|
1684
|
+
ctx.out(JSON.stringify({ ok: true, references }));
|
|
1685
|
+
return EXIT.ok;
|
|
1686
|
+
}
|
|
1687
|
+
if (!references.length) {
|
|
1688
|
+
ctx.out('No reference pictures yet. Attach one: velven create send --ref <picture> "like this".');
|
|
1689
|
+
return EXIT.ok;
|
|
1690
|
+
}
|
|
1691
|
+
for (const r of references) {
|
|
1692
|
+
const carried = r.messages !== undefined ? r.messages ? `on ${plural2(r.messages, "message")}` : "not sent yet" : r.attached ? "sent" : "not sent yet";
|
|
1693
|
+
ctx.out(`${r.id.padEnd(7)} ${when(r.created_at).padEnd(20)} ${`${r.width} by ${r.height}`.padEnd(12)} ${carried}`);
|
|
1694
|
+
}
|
|
1695
|
+
ctx.out(`${references.length} of ${REFS_PER_PROJECT} kept. A picture no message carried goes a day after it was last uploaded.`);
|
|
1696
|
+
return EXIT.ok;
|
|
1697
|
+
}
|
|
1698
|
+
async function usage(ctx, api, project, before, json) {
|
|
1699
|
+
if (before !== undefined && (!/^\d+$/.test(before) || Number(before) === 0))
|
|
1700
|
+
throw new CliError(`--before is a turn number, like 12, not "${before}".`, EXIT.usage, "usage");
|
|
1701
|
+
const query = new URLSearchParams;
|
|
1702
|
+
if (project)
|
|
1703
|
+
query.set("project", project);
|
|
1704
|
+
if (before)
|
|
1705
|
+
query.set("before", before);
|
|
1706
|
+
const search = query.toString();
|
|
1707
|
+
const answer = await call(api, "GET", `${ROOT}/usage${search ? `?${search}` : ""}`);
|
|
1708
|
+
if (json) {
|
|
1709
|
+
ctx.out(JSON.stringify({ ok: true, ...answer }));
|
|
1710
|
+
return EXIT.ok;
|
|
1711
|
+
}
|
|
1712
|
+
if (!answer.turns.length) {
|
|
1713
|
+
ctx.out(before ? "No earlier turns." : "No turns yet.");
|
|
1714
|
+
return EXIT.ok;
|
|
1715
|
+
}
|
|
1716
|
+
for (const t of answer.turns) {
|
|
1717
|
+
const used = t.credits.charged !== null ? `${plural2(t.credits.charged, "credit")} used` : `${plural2(t.credits.held, "credit")} held`;
|
|
1718
|
+
const kind = t.work ?? `${t.effort}${t.plan ? " plan" : ""}`;
|
|
1719
|
+
ctx.out(`turn ${String(t.turn).padEnd(6)} ${when(t.created_at).padEnd(20)} ${(t.project.title ?? "Untitled").padEnd(24)} ${kind.padEnd(10)} ${t.status.padEnd(9)} ${used}`.trimEnd());
|
|
1720
|
+
}
|
|
1721
|
+
if (answer.next !== null)
|
|
1722
|
+
ctx.out(`More: velven create usage${project ? ` --project ${project}` : ""} --before ${answer.next}`);
|
|
1723
|
+
return EXIT.ok;
|
|
1724
|
+
}
|
|
1725
|
+
async function showPlan(ctx, api, args, projectFlag, json) {
|
|
1726
|
+
if (args.length)
|
|
1727
|
+
throw new CliError("velven create plan takes no words: name another project with --project <id>.", EXIT.usage, "usage");
|
|
1728
|
+
const id = await currentProject(ctx, api.base, projectFlag);
|
|
1729
|
+
const { project } = await call(api, "GET", `${ROOT}/projects/${seg(id)}`);
|
|
1730
|
+
const plan = project.game_plan ?? null;
|
|
1731
|
+
if (json) {
|
|
1732
|
+
ctx.out(JSON.stringify({ ok: true, plan }));
|
|
1733
|
+
return EXIT.ok;
|
|
1734
|
+
}
|
|
1735
|
+
if (!plan) {
|
|
1736
|
+
ctx.out('This project has no plan yet. Send your idea and Velven sketches one with you: velven create send "a cozy farming game".');
|
|
1737
|
+
return EXIT.ok;
|
|
1738
|
+
}
|
|
1739
|
+
for (const line of planCard(plan))
|
|
1740
|
+
ctx.out(line);
|
|
1741
|
+
if (!project.head)
|
|
1742
|
+
ctx.out(`Build it: velven create send${projectFlag ? ` --project ${id}` : ""} --build "Build it"`);
|
|
1743
|
+
return EXIT.ok;
|
|
1744
|
+
}
|
|
1745
|
+
async function queue(ctx, api, args, projectFlag, json) {
|
|
1746
|
+
const [action, id, ...rest] = args;
|
|
1747
|
+
if (action === "rm") {
|
|
1748
|
+
if (id === undefined || !/^\d+$/.test(id) || Number(id) === 0 || rest.length) {
|
|
1749
|
+
throw new CliError("Name one waiting message by its number, like velven create queue rm 12.", EXIT.usage, "usage");
|
|
1750
|
+
}
|
|
1751
|
+
const project2 = await currentProject(ctx, api.base, projectFlag);
|
|
1752
|
+
await call(api, "DELETE", `${ROOT}/projects/${seg(project2)}/queue/${seg(id)}`);
|
|
1753
|
+
if (json)
|
|
1754
|
+
ctx.out(JSON.stringify({ ok: true, removed: Number(id) }));
|
|
1755
|
+
else
|
|
1756
|
+
ctx.out(`Removed message ${id}: it will not run.`);
|
|
1757
|
+
return EXIT.ok;
|
|
1758
|
+
}
|
|
1759
|
+
if (action !== undefined) {
|
|
1760
|
+
throw new CliError(`Unknown command "create queue ${action}". List what waits with velven create queue, remove one with velven create queue rm 12.`, EXIT.usage, "usage");
|
|
1761
|
+
}
|
|
1762
|
+
const project = await currentProject(ctx, api.base, projectFlag);
|
|
1763
|
+
const { queued } = await call(api, "GET", `${ROOT}/projects/${seg(project)}/queue`);
|
|
1764
|
+
if (json) {
|
|
1765
|
+
ctx.out(JSON.stringify({ ok: true, queued }));
|
|
1766
|
+
return EXIT.ok;
|
|
1767
|
+
}
|
|
1768
|
+
if (!queued.length) {
|
|
1769
|
+
ctx.out("Nothing is waiting on this project.");
|
|
1770
|
+
return EXIT.ok;
|
|
1771
|
+
}
|
|
1772
|
+
for (const q of queued) {
|
|
1773
|
+
const at = q.position !== null ? `${q.position}.` : q.state === "refused" ? "x" : "-";
|
|
1774
|
+
const words = q.text.length > 60 ? `${q.text.slice(0, 59)}…` : q.text;
|
|
1775
|
+
ctx.out(`${at.padStart(3)} ${String(q.id).padEnd(6)} ${when(q.created_at).padEnd(20)} ${q.intent === "build" ? "[build] " : ""}${words}`);
|
|
1776
|
+
if (q.state === "refused" && q.refused)
|
|
1777
|
+
ctx.out(` Not sent: ${q.refused}`);
|
|
1778
|
+
}
|
|
1779
|
+
if (queued.some((q) => q.state === "waiting"))
|
|
1780
|
+
ctx.out("They run in this order when the running turn ends. Remove one: velven create queue rm <number>");
|
|
1781
|
+
return EXIT.ok;
|
|
1782
|
+
}
|
|
1386
1783
|
async function credits(ctx, api, json) {
|
|
1387
1784
|
const view = await call(api, "GET", `${ROOT}/credits`);
|
|
1388
1785
|
if (json) {
|
|
@@ -1428,9 +1825,9 @@ async function turnId(ctx, base, arg, verb) {
|
|
|
1428
1825
|
|
|
1429
1826
|
// src/dev.ts
|
|
1430
1827
|
import { createReadStream as createReadStream2 } from "node:fs";
|
|
1431
|
-
import { open as open2, readdir as readdir3, readFile as
|
|
1828
|
+
import { open as open2, readdir as readdir3, readFile as readFile6, realpath as realpath2, stat as stat2 } from "node:fs/promises";
|
|
1432
1829
|
import { createServer } from "node:http";
|
|
1433
|
-
import { extname, join as join5, relative as relative2, resolve, sep } from "node:path";
|
|
1830
|
+
import { extname, join as join5, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
1434
1831
|
import { createBrotliDecompress, createGunzip } from "node:zlib";
|
|
1435
1832
|
|
|
1436
1833
|
// ../scan-rules/src/each-limit.ts
|
|
@@ -1753,7 +2150,7 @@ function refusalSentence(findings) {
|
|
|
1753
2150
|
// src/files.ts
|
|
1754
2151
|
import { createHash } from "node:crypto";
|
|
1755
2152
|
import { createReadStream } from "node:fs";
|
|
1756
|
-
import { open, readdir as readdir2, readFile as
|
|
2153
|
+
import { open, readdir as readdir2, readFile as readFile5, realpath, stat } from "node:fs/promises";
|
|
1757
2154
|
import { join as join4, relative } from "node:path";
|
|
1758
2155
|
|
|
1759
2156
|
// src/ignore.ts
|
|
@@ -2058,7 +2455,7 @@ function mediaProblem(kind, path, size2, bytes, missing = "is not among the file
|
|
|
2058
2455
|
}
|
|
2059
2456
|
|
|
2060
2457
|
// src/project.ts
|
|
2061
|
-
import { readdir, readFile as
|
|
2458
|
+
import { readdir, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
2062
2459
|
import { join as join3, win32 } from "node:path";
|
|
2063
2460
|
var SPACE_TYPES = ["game", "world", "tool", "wonder"];
|
|
2064
2461
|
var DEVICES = ["desktop", "mobile", "vr"];
|
|
@@ -2078,7 +2475,7 @@ async function readProject(dir) {
|
|
|
2078
2475
|
const path = join3(dir, names.includes(PROJECT_FILE) ? PROJECT_FILE : names.find(isProjectName) ?? PROJECT_FILE);
|
|
2079
2476
|
let text;
|
|
2080
2477
|
try {
|
|
2081
|
-
text = await
|
|
2478
|
+
text = await readFile4(path, "utf8");
|
|
2082
2479
|
} catch {
|
|
2083
2480
|
return { path, exists: false, data: {} };
|
|
2084
2481
|
}
|
|
@@ -2243,7 +2640,7 @@ async function walk(root, skipped = () => {}) {
|
|
|
2243
2640
|
const realRoot = await realpath(root);
|
|
2244
2641
|
let rules = [];
|
|
2245
2642
|
try {
|
|
2246
|
-
rules = parseIgnore(await
|
|
2643
|
+
rules = parseIgnore(await readFile5(join4(root, ".velvenignore"), "utf8"));
|
|
2247
2644
|
} catch {
|
|
2248
2645
|
rules = [];
|
|
2249
2646
|
}
|
|
@@ -2364,7 +2761,7 @@ async function checkMedia(config, files) {
|
|
|
2364
2761
|
continue;
|
|
2365
2762
|
const path = mediaPath(raw);
|
|
2366
2763
|
const file = files.find((f) => f.path === path);
|
|
2367
|
-
const bytes = !file ? undefined : file.size <= MEDIA_RULES[kind].maxBytes ? new Uint8Array(await
|
|
2764
|
+
const bytes = !file ? undefined : file.size <= MEDIA_RULES[kind].maxBytes ? new Uint8Array(await readFile5(file.abs)) : await readHead(file.abs);
|
|
2368
2765
|
const problem = mediaProblem(kind, path, file?.size, bytes, "is not among the files this publish sends: check the path, and that .velvenignore does not leave it out");
|
|
2369
2766
|
if (problem)
|
|
2370
2767
|
problems.push(problem);
|
|
@@ -2376,9 +2773,9 @@ async function checkMedia(config, files) {
|
|
|
2376
2773
|
return out;
|
|
2377
2774
|
}
|
|
2378
2775
|
function hashFile(abs) {
|
|
2379
|
-
return new Promise((
|
|
2776
|
+
return new Promise((resolve2, reject) => {
|
|
2380
2777
|
const hash = createHash("sha256");
|
|
2381
|
-
createReadStream(abs).on("data", (chunk) => hash.update(chunk)).on("error", reject).on("end", () =>
|
|
2778
|
+
createReadStream(abs).on("data", (chunk) => hash.update(chunk)).on("error", reject).on("end", () => resolve2(hash.digest("hex")));
|
|
2382
2779
|
});
|
|
2383
2780
|
}
|
|
2384
2781
|
var HASH_CHUNK = 4 * MB;
|
|
@@ -2551,7 +2948,7 @@ function injectSdk(html, src) {
|
|
|
2551
2948
|
return tag + html;
|
|
2552
2949
|
}
|
|
2553
2950
|
async function ignoreRules(root) {
|
|
2554
|
-
return parseIgnore(await
|
|
2951
|
+
return parseIgnore(await readFile6(join5(root, ".velvenignore"), "utf8").catch(() => ""));
|
|
2555
2952
|
}
|
|
2556
2953
|
async function namedAsOnDisk(base, rel) {
|
|
2557
2954
|
const parts = rel.split("/").filter(Boolean);
|
|
@@ -2567,7 +2964,7 @@ async function statExact(base, rel) {
|
|
|
2567
2964
|
return info && await namedAsOnDisk(base, rel) ? info : null;
|
|
2568
2965
|
}
|
|
2569
2966
|
async function startDevServer(root, options) {
|
|
2570
|
-
const base =
|
|
2967
|
+
const base = resolve2(root);
|
|
2571
2968
|
const realBase = await realpath2(base);
|
|
2572
2969
|
const server = createServer(async (req, res) => {
|
|
2573
2970
|
const send2 = (status, body2, headers2 = {}) => {
|
|
@@ -2586,7 +2983,7 @@ async function startDevServer(root, options) {
|
|
|
2586
2983
|
if (!parts.every(servableSegment))
|
|
2587
2984
|
return send2(404, "Not found", { "content-type": "text/plain; charset=utf-8" });
|
|
2588
2985
|
let rel = parts.join("/");
|
|
2589
|
-
let file =
|
|
2986
|
+
let file = resolve2(base, rel);
|
|
2590
2987
|
if (file !== base && !file.startsWith(base + sep))
|
|
2591
2988
|
return send2(404, "Not found");
|
|
2592
2989
|
let info = await statExact(base, rel);
|
|
@@ -2615,7 +3012,7 @@ async function startDevServer(root, options) {
|
|
|
2615
3012
|
if (!real || !servableRelative(relative2(realBase, real))) {
|
|
2616
3013
|
return send2(404, "Not found", { "content-type": "text/plain; charset=utf-8" });
|
|
2617
3014
|
}
|
|
2618
|
-
let body = await
|
|
3015
|
+
let body = await readFile6(file);
|
|
2619
3016
|
const { type, encoding } = contentType(file, body);
|
|
2620
3017
|
if (rel === options.entry && options.sdkSrc && !encoding)
|
|
2621
3018
|
body = injectSdk(body.toString("utf8"), options.sdkSrc);
|
|
@@ -2638,7 +3035,7 @@ async function startDevServer(root, options) {
|
|
|
2638
3035
|
};
|
|
2639
3036
|
}
|
|
2640
3037
|
async function dev(ctx, dirArg, portFlag) {
|
|
2641
|
-
const dir =
|
|
3038
|
+
const dir = resolve2(ctx.cwd, dirArg ?? ".");
|
|
2642
3039
|
const info = await stat2(dir).catch(() => null);
|
|
2643
3040
|
if (!info?.isDirectory())
|
|
2644
3041
|
throw new CliError(`There is no folder at ${dir}.`, EXIT.usage, "usage");
|
|
@@ -2689,7 +3086,7 @@ async function dev(ctx, dirArg, portFlag) {
|
|
|
2689
3086
|
|
|
2690
3087
|
// src/publish.ts
|
|
2691
3088
|
import { stat as stat3 } from "node:fs/promises";
|
|
2692
|
-
import { resolve as
|
|
3089
|
+
import { resolve as resolve3 } from "node:path";
|
|
2693
3090
|
|
|
2694
3091
|
// src/upload.ts
|
|
2695
3092
|
import { open as open3 } from "node:fs/promises";
|
|
@@ -2796,7 +3193,7 @@ function fileEntry(f) {
|
|
|
2796
3193
|
}
|
|
2797
3194
|
var hashesOf = (f) => f.pieces ?? (f.sha256 ? [f.sha256] : []);
|
|
2798
3195
|
async function folderOf(ctx, arg) {
|
|
2799
|
-
const dir =
|
|
3196
|
+
const dir = resolve3(ctx.cwd, arg ?? ".");
|
|
2800
3197
|
const info = await stat3(dir).catch(() => null);
|
|
2801
3198
|
if (!info)
|
|
2802
3199
|
throw new CliError(`There is no folder at ${dir}.`, EXIT.usage, "usage");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velven/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Publish a folder to Velven, the community marketplace for spaces built with AI: a private preview first, then hosted and live on its own page in seconds.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://velven.ai/docs",
|