@gentbajko/slopify 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/alignment/protocol.js +11 -1
- package/dist/adapters/alignment/runner.js +4 -1
- package/dist/adapters/alignment/worker.js +28 -5
- package/dist/adapters/explain.js +93 -0
- package/dist/adapters/ffmpeg.js +3 -3
- package/dist/adapters/host-cli/index.js +9 -2
- package/dist/adapters/host-cli/transport.js +4 -4
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +12 -12
- package/dist/adapters/image/codex.js +24 -8
- package/dist/adapters/image/fal.js +12 -6
- package/dist/adapters/image/google.js +18 -8
- package/dist/adapters/image/models.js +8 -2
- package/dist/adapters/image/openai.js +15 -6
- package/dist/adapters/image/replicate.js +16 -7
- package/dist/adapters/llm/claude-code-models.js +1 -1
- package/dist/adapters/llm/claude-code.js +16 -7
- package/dist/adapters/llm/cli-login-error.js +2 -1
- package/dist/adapters/llm/codex-models.js +1 -1
- package/dist/adapters/llm/codex.js +5 -6
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-models.js +1 -1
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +15 -11
- package/dist/adapters/llm/openrouter.js +21 -9
- package/dist/adapters/llm/run-cli.js +14 -11
- package/dist/adapters/tts/cartesia.js +10 -3
- package/dist/adapters/tts/elevenlabs.js +18 -6
- package/dist/adapters/tts/inworld-async.js +47 -16
- package/dist/adapters/tts/inworld.js +25 -15
- package/dist/adapters/tts/openai.js +18 -6
- package/dist/catalog/registry.js +13 -10
- package/dist/catalog/store.js +1 -1
- package/dist/catalog/validate.js +13 -4
- package/dist/edge/cli.js +33 -7
- package/dist/edge/docker-launch.js +6 -6
- package/dist/edge/docker-projects/engine.js +34 -13
- package/dist/edge/docker-projects/install.js +8 -8
- package/dist/edge/docker-projects/state.js +6 -6
- package/dist/edge/docker-projects/tree.js +6 -6
- package/dist/edge/docker.js +13 -13
- package/dist/edge/http/actions.js +27 -25
- package/dist/edge/http/app.js +3 -3
- package/dist/edge/http/audio-preview.js +1 -1
- package/dist/edge/http/checkpoints.js +5 -1
- package/dist/edge/http/diagnostics.js +3 -1
- package/dist/edge/http/draft-files.js +2 -2
- package/dist/edge/http/draft-problem.js +11 -1
- package/dist/edge/http/files.js +4 -4
- package/dist/edge/http/folder-location.js +3 -3
- package/dist/edge/http/fonts.js +22 -10
- package/dist/edge/http/host-cli.js +34 -12
- package/dist/edge/http/multipart.js +7 -5
- package/dist/edge/http/open-folder.js +2 -2
- package/dist/edge/http/planning.js +4 -4
- package/dist/edge/http/problem.js +2 -2
- package/dist/edge/http/project-templates.js +7 -3
- package/dist/edge/http/projects.js +8 -8
- package/dist/edge/http/prompts.js +9 -4
- package/dist/edge/http/providers.js +15 -7
- package/dist/edge/http/revision-files.js +3 -3
- package/dist/edge/http/revisions.js +30 -4
- package/dist/edge/http/schedules.js +9 -9
- package/dist/edge/http/settings.js +15 -9
- package/dist/edge/http/staging.js +5 -5
- package/dist/edge/http/storage.js +4 -4
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/tutorial.js +2 -2
- package/dist/edge/http/update.js +3 -3
- package/dist/host-cli/install.js +28 -6
- package/dist/host-cli/paths.js +6 -6
- package/dist/host-cli/server.js +1 -1
- package/dist/host-cli/service.js +11 -11
- package/dist/host-cli/status.js +7 -3
- package/dist/kernel/config/index.js +6 -4
- package/dist/kernel/db/migrate.js +2 -1
- package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
- package/dist/kernel/lock.js +4 -3
- package/dist/kernel/ports/subtitles.js +15 -1
- package/dist/kernel/runner/attempt.js +26 -2
- package/dist/main.js +2 -2
- package/dist/slices/admission/rules.js +32 -20
- package/dist/slices/article/continuation.js +4 -2
- package/dist/slices/article/plain.js +2 -2
- package/dist/slices/article/provided-entries.js +3 -3
- package/dist/slices/article/run.js +2 -2
- package/dist/slices/article/segments.js +5 -3
- package/dist/slices/control/providers.js +20 -7
- package/dist/slices/fonts/catalog.js +1 -1
- package/dist/slices/fonts/upload.js +1 -1
- package/dist/slices/images/run.js +5 -5
- package/dist/slices/library/lint.js +13 -7
- package/dist/slices/library/slots.js +2 -2
- package/dist/slices/narration/concat.js +1 -1
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +2 -2
- package/dist/slices/narration/run.js +7 -7
- package/dist/slices/narration/steering.js +3 -3
- package/dist/slices/play-drafts/convert.js +5 -4
- package/dist/slices/play-drafts/readiness.js +12 -6
- package/dist/slices/play-drafts/review-inputs.js +7 -4
- package/dist/slices/play-drafts/review.js +3 -3
- package/dist/slices/play-drafts/service.js +3 -1
- package/dist/slices/play-drafts/uploads.js +1 -1
- package/dist/slices/rebuild/admission-repo.js +2 -2
- package/dist/slices/rebuild/recipe-legacy.js +1 -1
- package/dist/slices/rebuild/recipe-narration-text.js +1 -1
- package/dist/slices/rebuild/recipe-visual.js +1 -1
- package/dist/slices/rebuild/recovery.js +2 -2
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/runtime-admission.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-checkpoints.js +4 -4
- package/dist/slices/rebuild/runtime-export-inputs.js +6 -6
- package/dist/slices/rebuild/runtime-export.js +4 -4
- package/dist/slices/rebuild/runtime-local.js +6 -6
- package/dist/slices/rebuild/runtime-narration-text.js +46 -7
- package/dist/slices/rebuild/runtime-piece-label.js +62 -0
- package/dist/slices/rebuild/runtime-provider.js +6 -4
- package/dist/slices/rebuild/runtime-publication.js +3 -3
- package/dist/slices/rebuild/runtime-run.js +11 -1
- package/dist/slices/rebuild/runtime-subtitles.js +78 -15
- package/dist/slices/rebuild/transition-repo.js +6 -6
- package/dist/slices/reruns/index.js +1 -1
- package/dist/slices/research/run.js +9 -7
- package/dist/slices/research/synthesis.js +4 -2
- package/dist/slices/revisions/mutation-assets.js +26 -8
- package/dist/slices/revisions/mutation-cues.js +1 -1
- package/dist/slices/revisions/rules.js +18 -9
- package/dist/slices/revisions/schema.js +1 -1
- package/dist/slices/schedules/repo.js +55 -11
- package/dist/slices/schedules/scheduler.js +25 -8
- package/dist/slices/schedules/schema.js +19 -4
- package/dist/slices/schedules/service.js +2 -0
- package/dist/slices/settings/cli-paths.js +7 -4
- package/dist/slices/settings/models.js +4 -4
- package/dist/slices/settings/playback.js +2 -2
- package/dist/slices/storage/assets.js +5 -5
- package/dist/slices/storage/layout.js +1 -1
- package/dist/slices/subtitles/captions.js +1 -1
- package/dist/slices/subtitles/prepare.js +5 -5
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +8 -8
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/ffmpeg.js +6 -4
- package/dist/slices/video/plan.js +1 -1
- package/dist/slices/video/run.js +2 -2
- package/dist/updater/plan.js +1 -1
- package/dist/updater/service.js +4 -3
- package/dist/updater/worker.js +6 -4
- package/dist/web/assets/{index-CZNiHm8P.css → index-Bwtmu9ei.css} +1 -1
- package/dist/web/assets/index-DjhMJqMN.js +141 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DPgGLX5m.js +0 -137
|
@@ -15,7 +15,7 @@ async function main() {
|
|
|
15
15
|
const gid = process.getgid?.();
|
|
16
16
|
assertManagedDockerHost(process.platform, uid, gid);
|
|
17
17
|
if (gid === undefined)
|
|
18
|
-
throw new Error("
|
|
18
|
+
throw new Error("Could not read your user's group. Run the --docker launcher as your normal logged-in user.");
|
|
19
19
|
const c = dockerConfig(process.env, homedir(), process.cwd(), uid, gid);
|
|
20
20
|
await privateDirectory(c.root, uid);
|
|
21
21
|
if (process.argv[2] !== "--locked") {
|
|
@@ -24,7 +24,7 @@ async function main() {
|
|
|
24
24
|
try {
|
|
25
25
|
const s = await fd.stat();
|
|
26
26
|
if (!s.isFile() || s.nlink !== 1 || s.uid !== uid || (s.mode & 0o077) !== 0)
|
|
27
|
-
throw new Error(
|
|
27
|
+
throw new Error(`The launcher's lock file ${path} has the wrong owner or permissions. Delete it and run the launcher again.`);
|
|
28
28
|
const child = spawn("flock", [
|
|
29
29
|
"--nonblock",
|
|
30
30
|
"--no-fork",
|
|
@@ -36,13 +36,13 @@ async function main() {
|
|
|
36
36
|
"--locked",
|
|
37
37
|
], { stdio: "inherit" });
|
|
38
38
|
const code = await new Promise((resolve, reject) => {
|
|
39
|
-
child.once("error", () => reject(new Error("Install util-linux (
|
|
39
|
+
child.once("error", () => reject(new Error("The --docker launcher needs the flock command, which isn't installed. Install util-linux (for example sudo apt install util-linux, or sudo pacman -S util-linux) and try again.")));
|
|
40
40
|
child.once("exit", (status, signal) => signal
|
|
41
|
-
? reject(new Error("Docker setup was interrupted;
|
|
41
|
+
? reject(new Error("Docker setup was interrupted before it finished. Run the same command again; Slopify will finish or undo the half-done step."))
|
|
42
42
|
: resolve(status ?? 1));
|
|
43
43
|
});
|
|
44
44
|
if (code === 73)
|
|
45
|
-
throw new Error("Another
|
|
45
|
+
throw new Error("Another Slopify --docker launcher is already running and changing your installation. Wait for it to finish, then run the command again.");
|
|
46
46
|
process.exitCode = code;
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
@@ -51,7 +51,7 @@ async function main() {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
const controller = new AbortController();
|
|
54
|
-
const abort = () => controller.abort(new Error("Docker setup
|
|
54
|
+
const abort = () => controller.abort(new Error("Docker setup was stopped before it finished. Run the same command again; Slopify will finish or undo the half-done step."));
|
|
55
55
|
process.once("SIGINT", abort);
|
|
56
56
|
process.once("SIGTERM", abort);
|
|
57
57
|
try {
|
|
@@ -4,12 +4,27 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { containerSchema, digestSchema, } from "./state.js";
|
|
6
6
|
import { contains } from "./tree.js";
|
|
7
|
+
// Docker's exit code alone says nothing, so its last error line decides the advice: the daemon
|
|
8
|
+
// being stopped and the socket being off-limits are by far the most common first-run failures.
|
|
9
|
+
export function dockerFailure(args, result) {
|
|
10
|
+
const detail = (result.stderr ?? "").trim();
|
|
11
|
+
if (/permission denied[^\n]*docker\.sock|docker\.sock[^\n]*permission denied/i.test(detail))
|
|
12
|
+
return "Your user isn't allowed to use Docker. Give it access with sudo usermod -aG docker $USER, then log out and back in (or run newgrp docker) and try again.";
|
|
13
|
+
if (/cannot connect to the docker daemon|is the docker daemon running|error during connect/i.test(detail))
|
|
14
|
+
return "Docker is installed but not running. Start it (sudo systemctl start docker, or systemctl --user start docker for rootless Docker) and try again.";
|
|
15
|
+
if (/port is already allocated|address already in use/i.test(detail))
|
|
16
|
+
return "The port Slopify wants is already in use by another program. Stop that program, or start Slopify on another port: npx @gentbajko/slopify --docker --port 7070";
|
|
17
|
+
if (/no space left on device/i.test(detail))
|
|
18
|
+
return "The disk Docker uses is full. Free up space (for example docker system prune) and try again.";
|
|
19
|
+
const last = detail.split("\n").at(-1)?.trim().slice(0, 300);
|
|
20
|
+
return `Docker command "docker ${args[0] ?? ""}" failed (exit code ${result.code}${last ? `: ${last}` : ""}). Check that Docker works (docker info) and run the launcher again. Nothing was deleted, and any backup Slopify made is kept.`;
|
|
21
|
+
}
|
|
7
22
|
export function dockerEngine(runner, signal, env) {
|
|
8
23
|
const run = (args, localSignal = signal) => runner.exec("docker", args, localSignal);
|
|
9
24
|
async function command(args, localSignal = signal) {
|
|
10
25
|
const r = await run(args, localSignal);
|
|
11
26
|
if (r.code !== 0)
|
|
12
|
-
throw new Error(
|
|
27
|
+
throw new Error(dockerFailure(args, r));
|
|
13
28
|
return r.stdout.trim();
|
|
14
29
|
}
|
|
15
30
|
const mount = (type, source, target, readonly = false) => [
|
|
@@ -48,14 +63,14 @@ export function dockerEngine(runner, signal, env) {
|
|
|
48
63
|
"{{.ID}}",
|
|
49
64
|
]);
|
|
50
65
|
if (present.code !== 0)
|
|
51
|
-
throw new Error("
|
|
66
|
+
throw new Error(dockerFailure(["container", "ls"], present));
|
|
52
67
|
if (!present.stdout.trim() && !/^[a-f0-9]{12,64}$/.test(name))
|
|
53
68
|
return null;
|
|
54
69
|
const result = await run(["inspect", name]);
|
|
55
70
|
if (result.code !== 0) {
|
|
56
71
|
if (!present.stdout.trim())
|
|
57
72
|
return null;
|
|
58
|
-
throw new Error("
|
|
73
|
+
throw new Error(dockerFailure(["inspect", name], result));
|
|
59
74
|
}
|
|
60
75
|
const schema = z.array(z.object({
|
|
61
76
|
Id: z.string(),
|
|
@@ -105,11 +120,11 @@ export function dockerEngine(runner, signal, env) {
|
|
|
105
120
|
for (const id of ids) {
|
|
106
121
|
const c = await inspect(id);
|
|
107
122
|
if (!c)
|
|
108
|
-
throw new Error("Cannot verify volume claims
|
|
123
|
+
throw new Error("Cannot verify volume claims: a Docker container disappeared while Slopify was checking. Run the launcher again.");
|
|
109
124
|
if (!permitted.includes(c.id) &&
|
|
110
125
|
!spared(c) &&
|
|
111
126
|
c.mounts.some((m) => m.type === "volume" && m.name === volume))
|
|
112
|
-
throw new Error(`
|
|
127
|
+
throw new Error(`The Docker volume ${volume} that holds Slopify's data is also used by the container ${c.name}. Remove that container if you no longer need it (docker rm ${c.name}), or give this Slopify its own volume with SLOPIFY_DOCKER_VOLUME=<name>, then try again.`);
|
|
113
128
|
}
|
|
114
129
|
}
|
|
115
130
|
async function writers(volume, paths, permitted) {
|
|
@@ -122,7 +137,7 @@ export function dockerEngine(runner, signal, env) {
|
|
|
122
137
|
(m.name === volume ||
|
|
123
138
|
(m.type === "bind" &&
|
|
124
139
|
paths.some((p) => contains(p, m.source) || contains(m.source, p))))))
|
|
125
|
-
throw new Error(`Another running container
|
|
140
|
+
throw new Error(`Another running container (${c.name}) is writing to Slopify's data or project folder. Stop it (docker stop ${c.name}) and try again.`);
|
|
126
141
|
}
|
|
127
142
|
}
|
|
128
143
|
async function recoveryLabel(j) {
|
|
@@ -154,7 +169,13 @@ export function dockerEngine(runner, signal, env) {
|
|
|
154
169
|
.parse(raw)[0]?.Endpoints.docker.Host;
|
|
155
170
|
}
|
|
156
171
|
if (!endpoint?.startsWith("unix://"))
|
|
157
|
-
throw new Error(
|
|
172
|
+
throw new Error(`The --docker launcher only works with Docker running on this machine, not a remote Docker (${endpoint ?? "unknown endpoint"}). Switch to the local one (unset DOCKER_HOST, or docker context use default) and try again.`);
|
|
173
|
+
const rawInfo = JSON.parse(await command(["info", "--format", "{{json .}}"]));
|
|
174
|
+
// Some Docker versions print the daemon's absence inside the JSON instead of failing.
|
|
175
|
+
const serverErrors = z.object({ ServerErrors: z.array(z.string()) }).safeParse(rawInfo)
|
|
176
|
+
.data?.ServerErrors;
|
|
177
|
+
if (serverErrors !== undefined && serverErrors.length > 0)
|
|
178
|
+
throw new Error(dockerFailure(["info"], { code: 1, stderr: serverErrors.join("\n") }));
|
|
158
179
|
const info = z
|
|
159
180
|
.object({
|
|
160
181
|
ID: z.string(),
|
|
@@ -162,12 +183,12 @@ export function dockerEngine(runner, signal, env) {
|
|
|
162
183
|
OperatingSystem: z.string(),
|
|
163
184
|
SecurityOptions: z.array(z.string()),
|
|
164
185
|
})
|
|
165
|
-
.parse(
|
|
186
|
+
.parse(rawInfo);
|
|
166
187
|
if (/docker desktop/i.test(info.OperatingSystem))
|
|
167
|
-
throw new Error("Docker Desktop
|
|
188
|
+
throw new Error("Docker Desktop isn't supported by the --docker launcher because it can't give your project folder the right owner. Use Docker Engine on Linux (docker context use default), or run Slopify without Docker: npx @gentbajko/slopify");
|
|
168
189
|
const rootless = info.SecurityOptions.includes("name=rootless");
|
|
169
190
|
if (!rootless && info.SecurityOptions.some((s) => s.startsWith("name=userns")))
|
|
170
|
-
throw new Error("Docker userns-remap
|
|
191
|
+
throw new Error("Your Docker uses userns-remap, which the --docker launcher doesn't support. Use rootless Docker or a standard Docker Engine setup, or run Slopify without Docker: npx @gentbajko/slopify. Don't turn off Docker's isolation to get around this.");
|
|
171
192
|
return { daemon: info.ID, user: rootless ? "0:0" : `${uid}:${gid}` };
|
|
172
193
|
},
|
|
173
194
|
image: async (ref) => {
|
|
@@ -214,7 +235,7 @@ export function dockerEngine(runner, signal, env) {
|
|
|
214
235
|
})
|
|
215
236
|
.parse(JSON.parse(await command(["volume", "inspect", "--format", "{{json .}}", name])));
|
|
216
237
|
if (v.Name !== name || (v.Options && Object.keys(v.Options).length > 0))
|
|
217
|
-
throw new Error(
|
|
238
|
+
throw new Error(`The Docker volume ${name} uses a custom driver or options, which Slopify doesn't support. Give Slopify a plain local volume with SLOPIFY_DOCKER_VOLUME=<new name> and try again.`);
|
|
218
239
|
return JSON.stringify([v.Name, v.CreatedAt, v.Driver]);
|
|
219
240
|
},
|
|
220
241
|
probe: async (c, image, user, destination) => {
|
|
@@ -228,7 +249,7 @@ export function dockerEngine(runner, signal, env) {
|
|
|
228
249
|
s.gid !== c.gid ||
|
|
229
250
|
(s.mode & 0o077) !== 0 ||
|
|
230
251
|
(await readFile(join(probe, "container"), "utf8")) !== "slopify ownership probe")
|
|
231
|
-
throw new Error("Docker
|
|
252
|
+
throw new Error("Docker can't create files in your project folder as your user, so the files would end up with the wrong owner. Use a standard Docker Engine or rootless Docker setup, or run Slopify without Docker: npx @gentbajko/slopify");
|
|
232
253
|
await writeFile(join(probe, "container"), "host can write", { flag: "a" });
|
|
233
254
|
}
|
|
234
255
|
finally {
|
|
@@ -378,7 +399,7 @@ export function dockerEngine(runner, signal, env) {
|
|
|
378
399
|
}
|
|
379
400
|
await delay(Math.min(250, Math.max(0, deadline - Date.now())), undefined, { signal });
|
|
380
401
|
}
|
|
381
|
-
throw new Error(
|
|
402
|
+
throw new Error(`The new Slopify container did not start up within 2 minutes. See why with: docker logs ${id}`);
|
|
382
403
|
},
|
|
383
404
|
};
|
|
384
405
|
}
|
|
@@ -17,10 +17,10 @@ export async function installProjects(c, e, recovery) {
|
|
|
17
17
|
const currentVolume = await e.volume(c.volume);
|
|
18
18
|
if ((receipt && receipt.volumeIdentity !== currentVolume) ||
|
|
19
19
|
(previousJournal?.volumeIdentity && previousJournal.volumeIdentity !== currentVolume))
|
|
20
|
-
throw new Error(
|
|
20
|
+
throw new Error(`Remembered data volume ${c.volume} is missing or was replaced since Slopify was installed. Restore the original volume (see docker volume ls) and run the launcher again. Nothing was changed.`);
|
|
21
21
|
if ((receipt && receipt.daemon !== context.daemon) ||
|
|
22
22
|
(previousJournal && previousJournal.daemon !== context.daemon))
|
|
23
|
-
throw new Error("Docker
|
|
23
|
+
throw new Error("This is not the Docker that Slopify was installed with (a different Docker context or DOCKER_HOST is active). Switch back to the original one (docker context use <name>, or unset DOCKER_HOST) and try again.");
|
|
24
24
|
await assertVolumeClaims(c, e, context.daemon, currentVolume, await e.inspect(c.name));
|
|
25
25
|
if (previousJournal && previousJournal.phase !== "rolled-back")
|
|
26
26
|
previousJournal = await recoverInstallation(c, previousJournal, receipt, recovery());
|
|
@@ -42,7 +42,7 @@ export async function installProjects(c, e, recovery) {
|
|
|
42
42
|
continue;
|
|
43
43
|
const other = await readState(join(c.root, name, "receipt.json"), receiptSchema, c.uid);
|
|
44
44
|
if (other?.daemon === context.daemon && other.volume === c.volume)
|
|
45
|
-
throw new Error(`
|
|
45
|
+
throw new Error(`The Docker volume ${c.volume} already belongs to another Slopify installation (${other.name}). Give this one its own volume with SLOPIFY_DOCKER_VOLUME=<name> and try again.`);
|
|
46
46
|
}
|
|
47
47
|
let reuse = null;
|
|
48
48
|
if (previousJournal?.phase === "rolled-back" &&
|
|
@@ -66,7 +66,7 @@ export async function installProjects(c, e, recovery) {
|
|
|
66
66
|
const image = await e.image(c.image);
|
|
67
67
|
const expectedVersion = await e.version(image);
|
|
68
68
|
if (expectedVersion !== readVersion())
|
|
69
|
-
throw new Error(
|
|
69
|
+
throw new Error(`The Docker image is Slopify ${expectedVersion} but this launcher is ${readVersion()}. Run the latest of both (npx @gentbajko/slopify@latest --docker), or if you set SLOPIFY_DOCKER_IMAGE, point it at version ${readVersion()}. Your existing container was not changed.`);
|
|
70
70
|
const signature = createHash("sha256")
|
|
71
71
|
.update(JSON.stringify([image, c.volume, c.port, c.bridge, projects, context.user]))
|
|
72
72
|
.digest("hex");
|
|
@@ -228,19 +228,19 @@ export async function installProjects(c, e, recovery) {
|
|
|
228
228
|
await save({ phase: "committed" });
|
|
229
229
|
const ready = await e.inspect(candidate);
|
|
230
230
|
if (!ready?.port)
|
|
231
|
-
throw new Error("
|
|
231
|
+
throw new Error("Slopify started but Docker did not report which port it is on. Run the same command again to finish.");
|
|
232
232
|
return { url: `http://${ready.port}`, projects, recovery: j.backup };
|
|
233
233
|
}
|
|
234
234
|
catch (cause) {
|
|
235
235
|
const current = await readState(receiptPath, receiptSchema, c.uid);
|
|
236
236
|
if (current?.transaction === j.id)
|
|
237
|
-
throw new Error(`
|
|
237
|
+
throw new Error(`Slopify was installed (project files at ${projects}) but the last step did not finish. Run the same command again to complete it. Backup volume: ${j.backup}`, { cause });
|
|
238
238
|
try {
|
|
239
239
|
await recoverInstallation(c, j, current, recovery());
|
|
240
240
|
}
|
|
241
241
|
catch (failure) {
|
|
242
|
-
throw new Error(`Recovery incomplete. Leave container
|
|
242
|
+
throw new Error(`Recovery incomplete: the install failed and Slopify could not fully undo it. Leave the Slopify container stopped and don't delete anything: backup volume ${j.backup}, progress file ${journalPath}, project folder ${projects}. Fix the problem below, then run the same command again. ${failure instanceof Error ? failure.message : "The undo step failed."}`, { cause });
|
|
243
243
|
}
|
|
244
|
-
throw new Error(`Previous installation restored${old?.running ? " and restarted" : "
|
|
244
|
+
throw new Error(`Install failed. Previous installation restored${old?.running ? " and restarted" : " (not started)"}, so nothing was lost. Backup volume: ${j.backup}; project copy: ${projects}. Reason: ${cause instanceof Error ? cause.message : "Installation failed."}`, { cause });
|
|
245
245
|
}
|
|
246
246
|
}
|
|
@@ -112,7 +112,7 @@ export function dockerConfig(env, home, cwd, uid, gid) {
|
|
|
112
112
|
const projectsOverride = raw === null ? null : resolve(cwd, raw.startsWith("~/") ? join(home, raw.slice(2)) : raw);
|
|
113
113
|
const portText = env.SLOPIFY_DOCKER_HOST_PORT || "6969";
|
|
114
114
|
if (!/^\d{1,5}$/.test(portText) || Number(portText) > 65535)
|
|
115
|
-
throw new Error(
|
|
115
|
+
throw new Error(`Invalid port ${JSON.stringify(portText)}: use a whole number between 1 and 65535, for example --port 7070.`);
|
|
116
116
|
return {
|
|
117
117
|
home,
|
|
118
118
|
uid,
|
|
@@ -144,11 +144,11 @@ export async function privateDirectory(path, uid) {
|
|
|
144
144
|
if (!s.isDirectory() ||
|
|
145
145
|
s.isSymbolicLink() ||
|
|
146
146
|
((s.mode & 0o022) !== 0 && (s.mode & 0o1000) === 0))
|
|
147
|
-
throw new Error(
|
|
147
|
+
throw new Error(`${current} is not a normal folder, or other users can write to it, so Slopify won't keep its Docker settings under it. Fix its permissions (chmod go-w ${current}) or set XDG_DATA_HOME to another folder, then try again.`);
|
|
148
148
|
}
|
|
149
149
|
const s = await lstat(path);
|
|
150
150
|
if (s.uid !== uid || (s.mode & 0o077) !== 0)
|
|
151
|
-
throw new Error(
|
|
151
|
+
throw new Error(`${path} must belong to you and be private. Fix it with chmod 700 ${path} (and chown it to your user if needed), then try again.`);
|
|
152
152
|
}
|
|
153
153
|
export async function readState(path, schema, uid) {
|
|
154
154
|
const s = await lstat(path).catch((error) => {
|
|
@@ -186,7 +186,7 @@ export async function selectProjects(c, receipt, old, retry = null) {
|
|
|
186
186
|
throw new Error("Receipt selects a different installation or volume.");
|
|
187
187
|
await safePath(receipt.projects, c.home, c.root, false);
|
|
188
188
|
await assertIdentity(receipt.projects, receipt.directoryIdentity).catch((cause) => {
|
|
189
|
-
throw new Error(`Remembered project folder is missing or replaced
|
|
189
|
+
throw new Error(`Remembered project folder ${receipt.projects} is missing or was replaced. Put the original folder back where it was and run the launcher again.`, {
|
|
190
190
|
cause,
|
|
191
191
|
});
|
|
192
192
|
});
|
|
@@ -202,7 +202,7 @@ export async function selectProjects(c, receipt, old, retry = null) {
|
|
|
202
202
|
await safePath(chosen, c.home, c.root, true);
|
|
203
203
|
const source = receipt?.projects ?? bind?.source;
|
|
204
204
|
if (source && source !== chosen && (contains(source, chosen) || contains(chosen, source)))
|
|
205
|
-
throw new Error(
|
|
205
|
+
throw new Error(`The new project folder ${chosen} and the current one ${source} are inside each other. Choose a separate folder with --projects-dir <folder>.`);
|
|
206
206
|
if (chosen !== source) {
|
|
207
207
|
const entries = await readdir(chosen).catch((error) => {
|
|
208
208
|
if (isMissing(error))
|
|
@@ -211,7 +211,7 @@ export async function selectProjects(c, receipt, old, retry = null) {
|
|
|
211
211
|
});
|
|
212
212
|
if (entries.length > 0) {
|
|
213
213
|
if (retry?.destination !== chosen || !retry.publishedIdentity || !retry.sourceDigest)
|
|
214
|
-
throw new Error(`
|
|
214
|
+
throw new Error(`The project folder ${chosen} already has files in it that Slopify didn't put there. Choose an empty or new folder with --projects-dir <folder>, or move those files away, then try again.`);
|
|
215
215
|
await assertIdentity(chosen, retry.publishedIdentity);
|
|
216
216
|
if ((await treeDigest(chosen)).hash !== retry.sourceDigest.hash)
|
|
217
217
|
throw new Error(`Published retry destination changed: ${chosen}`);
|
|
@@ -19,7 +19,7 @@ export function contains(root, path) {
|
|
|
19
19
|
}
|
|
20
20
|
export async function safePath(path, home, stateRoot, createParents) {
|
|
21
21
|
if (!isAbsolute(path) || resolve(path) !== path || /[\p{Cc},]/u.test(path))
|
|
22
|
-
throw new Error(
|
|
22
|
+
throw new Error(`The project folder ${JSON.stringify(path)} can't be used: give a full path without commas, for example --projects-dir ~/Slopify/Projects.`);
|
|
23
23
|
const forbidden = [
|
|
24
24
|
"/",
|
|
25
25
|
"/home",
|
|
@@ -36,7 +36,7 @@ export async function safePath(path, home, stateRoot, createParents) {
|
|
|
36
36
|
contains(stateRoot, path) ||
|
|
37
37
|
contains(path, stateRoot) ||
|
|
38
38
|
["/proc", "/sys", "/dev", "/etc", "/usr", "/bin", "/sbin", "/lib", "/var/lib/docker"].some((p) => contains(p, path)))
|
|
39
|
-
throw new Error(
|
|
39
|
+
throw new Error(`${path} can't be the project folder because it is a system or shared folder (or holds Slopify's own settings). Choose a dedicated folder, for example --projects-dir ~/Slopify/Projects.`);
|
|
40
40
|
const parts = path.split(sep).filter(Boolean);
|
|
41
41
|
let current = sep;
|
|
42
42
|
for (const [i, part] of parts.entries()) {
|
|
@@ -53,11 +53,11 @@ export async function safePath(path, home, stateRoot, createParents) {
|
|
|
53
53
|
if (s === undefined)
|
|
54
54
|
continue;
|
|
55
55
|
if (!s.isDirectory() || s.isSymbolicLink())
|
|
56
|
-
throw new Error(
|
|
56
|
+
throw new Error(`${current} is a file or a link, not a folder, so it can't be part of the project folder path. Choose another folder with --projects-dir <folder>.`);
|
|
57
57
|
if (i === parts.length - 1 && (s.uid !== process.getuid?.() || (s.mode & 0o022) !== 0))
|
|
58
|
-
throw new Error(
|
|
58
|
+
throw new Error(`The project folder ${path} must belong to you and must not be writable by other users. Fix it (chmod go-w ${path}) or choose another folder with --projects-dir <folder>.`);
|
|
59
59
|
if (i < parts.length - 1 && (s.mode & 0o022) !== 0 && (s.mode & 0o1000) === 0)
|
|
60
|
-
throw new Error(`
|
|
60
|
+
throw new Error(`Other users can write to ${current}, which contains the project folder. Choose a project folder inside your home folder with --projects-dir <folder>, or fix it with chmod go-w ${current}.`);
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
export function isMissing(error) {
|
|
@@ -137,7 +137,7 @@ export async function assertWritableTree(root, uid) {
|
|
|
137
137
|
(s.isFile() && s.nlink !== 1) ||
|
|
138
138
|
s.uid !== uid ||
|
|
139
139
|
(s.mode & (s.isDirectory() ? 0o700 : 0o600)) !== (s.isDirectory() ? 0o700 : 0o600))
|
|
140
|
-
throw new Error(
|
|
140
|
+
throw new Error(`${root} in your project files doesn't have permissions your user can fully use. Make it yours and writable (chown -R $USER ${root}; chmod -R u+rwX ${root}), then try again.`);
|
|
141
141
|
if (s.isDirectory())
|
|
142
142
|
for (const name of await readdir(root))
|
|
143
143
|
await assertWritableTree(join(root, name), uid);
|
package/dist/edge/docker.js
CHANGED
|
@@ -5,13 +5,13 @@ import { z } from "zod";
|
|
|
5
5
|
import { installHostPackage } from "../host-cli/install.js";
|
|
6
6
|
import { ensureBridgeToken, hasCode, prepareHostPaths } from "../host-cli/paths.js";
|
|
7
7
|
import { ensureHostService, privateRead, privateWrite } from "../host-cli/service.js";
|
|
8
|
-
import { resolveHostCommand } from "../host-cli/status.js";
|
|
8
|
+
import { hostCommandName, resolveHostCommand } from "../host-cli/status.js";
|
|
9
9
|
import { hostCliProtocol, hostLlmIds } from "../kernel/ports/host-cli.js";
|
|
10
10
|
export function assertManagedDockerHost(platform, uid, gid) {
|
|
11
11
|
if (platform !== "linux")
|
|
12
|
-
throw new Error("The
|
|
12
|
+
throw new Error("The --docker launcher only works on Linux. On this system, run Slopify without Docker (npx @gentbajko/slopify), or start the Slopify Docker image yourself with docker run.");
|
|
13
13
|
if (uid === undefined || gid === undefined || uid === 0)
|
|
14
|
-
throw new Error("
|
|
14
|
+
throw new Error("Don't run the --docker launcher with sudo or as root. Run it as your normal user, and if Docker then says permission denied, give your user access: sudo usermod -aG docker $USER, then log out and back in.");
|
|
15
15
|
}
|
|
16
16
|
export async function prepareDockerHostCli(options) {
|
|
17
17
|
if (options.disabled)
|
|
@@ -24,16 +24,16 @@ export async function prepareDockerHostCli(options) {
|
|
|
24
24
|
}
|
|
25
25
|
catch (error) {
|
|
26
26
|
if (!hasCode(error, "ENOENT"))
|
|
27
|
-
throw new Error(
|
|
27
|
+
throw new Error(`Could not check whether ${hostCommandName(id)} is installed on this machine (${error instanceof Error ? error.message : String(error)}). Nothing was changed. Fix the problem, or start with --host-cli=off to use API keys only.`);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
if (!installed)
|
|
31
31
|
return {};
|
|
32
32
|
if (process.platform !== "linux")
|
|
33
|
-
throw new Error("
|
|
33
|
+
throw new Error("Using this machine's AI CLIs from Docker needs Linux with systemd. Start with --host-cli=off to use API keys only.");
|
|
34
34
|
const uid = process.getuid?.();
|
|
35
35
|
if (uid === undefined || uid === 0)
|
|
36
|
-
throw new Error("
|
|
36
|
+
throw new Error("Don't run the --docker launcher with sudo or as root; run it as your normal user. Or start with --host-cli=off to use API keys only.");
|
|
37
37
|
const receiptPath = join(options.root, "consent.json");
|
|
38
38
|
const existing = await privateRead(receiptPath, uid);
|
|
39
39
|
const receipt = z.object({ version: z.literal(1), automaticStartup: z.literal(true) }).strict();
|
|
@@ -43,15 +43,15 @@ export async function prepareDockerHostCli(options) {
|
|
|
43
43
|
approved ||= receipt.safeParse(JSON.parse(existing)).success;
|
|
44
44
|
}
|
|
45
45
|
catch {
|
|
46
|
-
throw new Error(
|
|
46
|
+
throw new Error(`The saved answer to the host CLI question (${receiptPath}) is damaged. Delete that file and run the launcher again to be asked again.`);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
if (!approved) {
|
|
50
50
|
if (!options.interactive)
|
|
51
|
-
throw new Error("
|
|
51
|
+
throw new Error("Slopify needs your OK to use the Claude Code, Codex or Gemini CLI installed on this machine, but can't ask because this isn't an interactive terminal. Rerun with --accept-host-cli to allow it, or with --host-cli=off to use API keys only.");
|
|
52
52
|
approved = await options.prompt("Allow Slopify to run your host Claude Code, Codex and Gemini CLIs using their existing logins? This installs a private helper and enables automatic startup. User lingering keeps your user services running after logout and at boot. Credentials stay on the host. [y/N] ");
|
|
53
53
|
if (!approved)
|
|
54
|
-
throw new Error("
|
|
54
|
+
throw new Error("You declined, so Slopify did not set up your host AI CLIs and nothing was changed. Run the launcher again and answer y to allow it, or add --host-cli=off to start with API keys only.");
|
|
55
55
|
}
|
|
56
56
|
const inspect = () => options.runner.exec("docker", [
|
|
57
57
|
"image",
|
|
@@ -64,14 +64,14 @@ export async function prepareDockerHostCli(options) {
|
|
|
64
64
|
if (image.code !== 0 || image.stdout.trim() !== String(hostCliProtocol)) {
|
|
65
65
|
const pulled = await options.runner.exec("docker", ["pull", options.image], options.signal);
|
|
66
66
|
if (pulled.code !== 0)
|
|
67
|
-
throw new Error(
|
|
67
|
+
throw new Error(`Could not download the Slopify Docker image ${options.image}. Check your internet connection and that Docker is running (docker info), then try again. Nothing was changed.`);
|
|
68
68
|
image = await inspect();
|
|
69
69
|
if (image.code !== 0 || image.stdout.trim() !== String(hostCliProtocol))
|
|
70
|
-
throw new Error(
|
|
70
|
+
throw new Error(`The Docker image ${options.image} is too old to use the AI CLIs on this machine. Use the latest image (unset SLOPIFY_DOCKER_IMAGE, or point it to a current release) and try again, or start with --host-cli=off.`);
|
|
71
71
|
}
|
|
72
72
|
const supported = await options.runner.exec("systemctl", ["--user", "list-units", "--no-legend", "--no-pager", "slopify-cli-bridge.service"], options.signal);
|
|
73
73
|
if (supported.code !== 0)
|
|
74
|
-
throw new Error("
|
|
74
|
+
throw new Error("Using this machine's AI CLIs from Docker needs systemd user services (systemctl --user), which aren't available here. Start with --host-cli=off to use API keys only, or run Slopify without Docker: npx @gentbajko/slopify.");
|
|
75
75
|
const paths = await prepareHostPaths(options.root);
|
|
76
76
|
const lock = join(paths.root, "setup.lock");
|
|
77
77
|
const deadline = Date.now() + 30_000;
|
|
@@ -84,7 +84,7 @@ export async function prepareDockerHostCli(options) {
|
|
|
84
84
|
if (!hasCode(error, "EEXIST"))
|
|
85
85
|
throw error;
|
|
86
86
|
if (Date.now() >= deadline)
|
|
87
|
-
throw new Error(
|
|
87
|
+
throw new Error(`Another Slopify launcher is setting up the host CLI helper right now. Wait for it to finish and try again. If no other launcher is running, delete the folder ${lock} and retry.`);
|
|
88
88
|
await delay(100, undefined, { signal: options.signal });
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -91,15 +91,15 @@ export function actionRoutes(deps) {
|
|
|
91
91
|
"rebuild-required": 409,
|
|
92
92
|
};
|
|
93
93
|
const messages = {
|
|
94
|
-
"no-project": "
|
|
95
|
-
running: "Pause
|
|
96
|
-
"not-editable": "Providers can be changed only while
|
|
97
|
-
"invalid-providers": "
|
|
98
|
-
"catalog-unavailable": "
|
|
99
|
-
"revision-required": "
|
|
100
|
-
conflict: "
|
|
101
|
-
"idempotency-conflict": "This
|
|
102
|
-
"rebuild-required": "
|
|
94
|
+
"no-project": "This project no longer exists. Go back to Projects to pick another.",
|
|
95
|
+
running: "This project is still running. Pause it and wait for the current step to stop before changing providers.",
|
|
96
|
+
"not-editable": "Providers can be changed only while the project is paused or has failed. Pause it first, then try again.",
|
|
97
|
+
"invalid-providers": "Some provider choices are not valid. Fix the highlighted fields and try again.",
|
|
98
|
+
"catalog-unavailable": "Slopify could not load the list of available models. Wait a moment and try again; if it keeps failing, check Settings → Models.",
|
|
99
|
+
"revision-required": "This page is out of date. Reload the page, then try again.",
|
|
100
|
+
conflict: "This project changed since the page loaded. Reload the page to see the latest, then try again.",
|
|
101
|
+
"idempotency-conflict": "This action was already sent with different details. Reload the page, then try again.",
|
|
102
|
+
"rebuild-required": "This change needs some outputs to be made again. In the Edit tab, use Rebuild affected outputs to see what will be regenerated and what it costs.",
|
|
103
103
|
};
|
|
104
104
|
return problem(c, {
|
|
105
105
|
status: codes[result.reason],
|
|
@@ -117,7 +117,7 @@ export function actionRoutes(deps) {
|
|
|
117
117
|
return problem(c, {
|
|
118
118
|
status: 503,
|
|
119
119
|
title: titleOf(503),
|
|
120
|
-
detail: "
|
|
120
|
+
detail: "Resume and retry are not ready yet because Slopify is still starting. Wait a moment, reload the page and try again.",
|
|
121
121
|
});
|
|
122
122
|
const result = recoveryResultSchema.parse(await recoverProject(deps.rebuild, id, { ...input, action }));
|
|
123
123
|
if (result.ok)
|
|
@@ -128,26 +128,28 @@ export function actionRoutes(deps) {
|
|
|
128
128
|
? 400
|
|
129
129
|
: 409;
|
|
130
130
|
const messages = {
|
|
131
|
-
"no-project": "This project no longer exists.",
|
|
132
|
-
"no-revision": "
|
|
133
|
-
conflict: "
|
|
134
|
-
"idempotency-conflict": "This
|
|
135
|
-
"invalid-edit": "This section
|
|
136
|
-
"stale-preview": "
|
|
137
|
-
"invalid-selection": "This section has
|
|
138
|
-
"review-required": "
|
|
139
|
-
"cost-ack-required": "
|
|
140
|
-
readiness: "Check the provider, model, voice
|
|
141
|
-
running: "Wait for
|
|
142
|
-
"accepted-job": "This section
|
|
143
|
-
"control-changed": "
|
|
131
|
+
"no-project": "This project no longer exists. Go back to Projects to pick another.",
|
|
132
|
+
"no-revision": "The project's saved settings changed since the page loaded. Reload the page, then try again.",
|
|
133
|
+
conflict: "This project changed since the page loaded. Reload the page to see the latest, then use Resume again.",
|
|
134
|
+
"idempotency-conflict": "This action was already sent with different details. Reload the page, then try again.",
|
|
135
|
+
"invalid-edit": "This section has settings that need fixing first. Open the Edit tab, choose Edit project and correct this section, then try again.",
|
|
136
|
+
"stale-preview": "What needs to be made changed since the page loaded. Reload the page, then use Resume again.",
|
|
137
|
+
"invalid-selection": "This section has nothing generated to re-run because its content was supplied by you. To change it, open the Edit tab and choose Edit project.",
|
|
138
|
+
"review-required": "Content you supplied or captions you edited by hand would be replaced. Change them in the Edit tab with Edit project, or use Rebuild affected outputs to confirm the replacement.",
|
|
139
|
+
"cost-ack-required": "This re-run needs you to review its cost first. In the Edit tab, use Rebuild affected outputs.",
|
|
140
|
+
readiness: "Slopify cannot run this yet. Check the provider, model, voice and any files you supplied, then use Resume.",
|
|
141
|
+
running: "This section is still running. Wait for it to finish, or Pause the project, before re-running it.",
|
|
142
|
+
"accepted-job": "This section has a finished provider job that Slopify has not collected yet.",
|
|
143
|
+
"control-changed": "Another action on this project (such as Pause or Cancel) happened at the same time. Reload the page to see its current state, then try again.",
|
|
144
144
|
};
|
|
145
145
|
const detail = [
|
|
146
146
|
messages[result.reason],
|
|
147
147
|
...(result.fields ?? []).map((row) => row.message),
|
|
148
148
|
...(result.intentRevisionId === undefined
|
|
149
149
|
? []
|
|
150
|
-
: [
|
|
150
|
+
: [
|
|
151
|
+
"Your re-run request was saved. Use Resume to continue it instead of re-running again.",
|
|
152
|
+
]),
|
|
151
153
|
].join(" ");
|
|
152
154
|
return problem(c, {
|
|
153
155
|
status,
|
|
@@ -218,7 +220,7 @@ function revisionRequired(c) {
|
|
|
218
220
|
return problem(c, {
|
|
219
221
|
status: 409,
|
|
220
222
|
title: titleOf(409),
|
|
221
|
-
detail: "
|
|
223
|
+
detail: "This change is now made from the Edit tab. Choose Edit project, save your change, then use Rebuild affected outputs.",
|
|
222
224
|
extensions: { reason: "revision-required" },
|
|
223
225
|
});
|
|
224
226
|
}
|
package/dist/edge/http/app.js
CHANGED
|
@@ -80,14 +80,14 @@ export function createApp(deps) {
|
|
|
80
80
|
return problem(c, {
|
|
81
81
|
status: 503,
|
|
82
82
|
title: titleOf(503),
|
|
83
|
-
detail: "
|
|
83
|
+
detail: "Slopify is still finishing its Docker setup. Wait for the launcher to finish, then try again.",
|
|
84
84
|
});
|
|
85
85
|
const releaseRequest = deps.mutations?.begin();
|
|
86
86
|
if (deps.mutations !== undefined && releaseRequest === undefined)
|
|
87
87
|
return problem(c, {
|
|
88
88
|
status: 503,
|
|
89
89
|
title: titleOf(503),
|
|
90
|
-
detail: "Slopify is shutting down.
|
|
90
|
+
detail: "Slopify is shutting down, so changes cannot be saved right now. Start it again, reload the page and try again.",
|
|
91
91
|
});
|
|
92
92
|
const updateRoute = [
|
|
93
93
|
"/api/update",
|
|
@@ -101,7 +101,7 @@ export function createApp(deps) {
|
|
|
101
101
|
return problem(c, {
|
|
102
102
|
status: 409,
|
|
103
103
|
title: titleOf(409),
|
|
104
|
-
detail: "Slopify is updating. Wait for it to restart
|
|
104
|
+
detail: "Slopify is updating. Wait for it to restart, reload the page and try again.",
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
try {
|
|
@@ -49,6 +49,6 @@ function missing(c) {
|
|
|
49
49
|
return problem(c, {
|
|
50
50
|
status: 404,
|
|
51
51
|
title: titleOf(404),
|
|
52
|
-
detail: "This live audio preview
|
|
52
|
+
detail: "This live audio preview has ended. Play the finished narration from the project's audio outputs instead.",
|
|
53
53
|
});
|
|
54
54
|
}
|
|
@@ -20,7 +20,11 @@ function refused(c, reason) {
|
|
|
20
20
|
return problem(c, {
|
|
21
21
|
status,
|
|
22
22
|
title: titleOf(status),
|
|
23
|
-
detail:
|
|
23
|
+
detail: reason === "not-found"
|
|
24
|
+
? "This checkpoint no longer exists; the project may have moved on or changed. Reload the page to see where it is now."
|
|
25
|
+
: reason === "invalid-input"
|
|
26
|
+
? "This checkpoint answer could not be read. Reload the page and try again."
|
|
27
|
+
: "This checkpoint changed since the page loaded, for example it was already approved. Reload the page to see its current state, then try again.",
|
|
24
28
|
extensions: { reason },
|
|
25
29
|
});
|
|
26
30
|
}
|
|
@@ -29,7 +29,9 @@ export function diagnosticsRoutes(deps) {
|
|
|
29
29
|
...(provider.cliPath === undefined ? {} : { cliPath: provider.cliPath }),
|
|
30
30
|
})),
|
|
31
31
|
projects: { total: typeof projectCount?.count === "number" ? projectCount.count : 0 },
|
|
32
|
-
catalogue: deps.catalogue?.status() ?? {
|
|
32
|
+
catalogue: deps.catalogue?.status() ?? {
|
|
33
|
+
warning: "The model list (models.yaml) is not loaded yet. Wait a moment and reload the page; if it stays like this, restart Slopify.",
|
|
34
|
+
},
|
|
33
35
|
});
|
|
34
36
|
});
|
|
35
37
|
}
|
|
@@ -32,7 +32,7 @@ export function draftFileRoutes(service) {
|
|
|
32
32
|
return problem(c, {
|
|
33
33
|
status: 400,
|
|
34
34
|
title: titleOf(400),
|
|
35
|
-
detail: "
|
|
35
|
+
detail: "No file arrived with the upload. Choose the file again and retry.",
|
|
36
36
|
});
|
|
37
37
|
return result.ok
|
|
38
38
|
? c.json(draftAttachmentSchema.parse(result.value))
|
|
@@ -57,7 +57,7 @@ export function draftFileRoutes(service) {
|
|
|
57
57
|
return problem(c, {
|
|
58
58
|
status: 404,
|
|
59
59
|
title: titleOf(404),
|
|
60
|
-
detail: "This
|
|
60
|
+
detail: "This attached file has not finished uploading, so it cannot be previewed yet. Wait a moment or attach it again.",
|
|
61
61
|
});
|
|
62
62
|
return c.body(Readable.toWeb(createReadStream(stagingPath(deps.paths, staged.path))), 200, {
|
|
63
63
|
"content-type": contentType,
|