@gentbajko/slopify 2.0.1 → 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/cache.js +17 -1
- package/dist/adapters/alignment/prefetch.js +35 -0
- 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 +37 -8
- 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/errors.js +12 -0
- package/dist/kernel/lock.js +4 -3
- package/dist/kernel/paths.js +5 -0
- package/dist/kernel/ports/subtitles.js +15 -1
- package/dist/kernel/runner/attempt.js +26 -2
- package/dist/kernel/runner/index.js +16 -6
- package/dist/kernel/runner/progress.js +56 -0
- package/dist/main.js +20 -4
- 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-store.js +45 -15
- package/dist/slices/rebuild/runtime-subtitles.js +80 -17
- 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 +7 -6
- 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
|
@@ -47,7 +47,7 @@ export function revisionFolderRoutes(deps) {
|
|
|
47
47
|
return problem(c, {
|
|
48
48
|
status: 403,
|
|
49
49
|
title: titleOf(403),
|
|
50
|
-
detail: "
|
|
50
|
+
detail: "For your safety, folders can only be opened from the Slopify page itself. Open Slopify and try again there.",
|
|
51
51
|
});
|
|
52
52
|
const { id: projectId, revisionId, recordId } = c.req.valid("param");
|
|
53
53
|
const result = findRevisionDownload(deps, projectId, revisionId, recordId);
|
|
@@ -61,8 +61,8 @@ function unavailable(c, reason) {
|
|
|
61
61
|
status: 404,
|
|
62
62
|
title: titleOf(404),
|
|
63
63
|
detail: reason === "missing-file"
|
|
64
|
-
? "This
|
|
65
|
-
: "
|
|
64
|
+
? "This file was deleted or moved from the project folder. In the Edit tab, use Rebuild affected outputs to make it again."
|
|
65
|
+
: "This file is no longer part of the project. Reload the page to see its current files.",
|
|
66
66
|
extensions: { reason },
|
|
67
67
|
});
|
|
68
68
|
}
|
|
@@ -3,6 +3,7 @@ import { Hono } from "hono";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { projectExists } from "../../slices/admission/repo.js";
|
|
5
5
|
import { previewRebuildSchema, startRebuildSchema } from "../../slices/rebuild/model.js";
|
|
6
|
+
import { narrationChunkOrder } from "../../slices/rebuild/runtime-narration-text.js";
|
|
6
7
|
import { previewRebuild, startRebuild } from "../../slices/rebuild/service.js";
|
|
7
8
|
import { ensureBaseline, restoreRevision, saveRevision } from "../../slices/revisions/index.js";
|
|
8
9
|
import { currentRevisionId, listRevisionHistory } from "../../slices/revisions/repo.js";
|
|
@@ -30,12 +31,24 @@ const statuses = {
|
|
|
30
31
|
"cost-ack-required": 409,
|
|
31
32
|
readiness: 409,
|
|
32
33
|
};
|
|
34
|
+
const details = {
|
|
35
|
+
"no-project": "This project no longer exists. Go back to Projects to pick another.",
|
|
36
|
+
"no-revision": "This saved version of the project no longer exists. Reload the page, then try again.",
|
|
37
|
+
conflict: "This project changed while you were editing. Reload the page to see the latest, then make your change again.",
|
|
38
|
+
"idempotency-conflict": "This change was already sent with different details. Reload the page, then try again.",
|
|
39
|
+
"invalid-edit": "Some of your changes are not valid. Fix the highlighted fields and save again.",
|
|
40
|
+
"stale-preview": "What needs rebuilding changed since you opened this review. Cancel it and open Rebuild affected outputs again to see the latest.",
|
|
41
|
+
"invalid-selection": "Some of the outputs you picked can no longer be rebuilt. Reload the page and choose again.",
|
|
42
|
+
"review-required": 'This rebuild touches content you supplied yourself. Tick "Keep the provided content" for each item in the review, then choose Start rebuild.',
|
|
43
|
+
"cost-ack-required": "Some costs for this rebuild are unknown. Tick the box confirming you understand, then choose Start rebuild.",
|
|
44
|
+
readiness: "Slopify cannot run this rebuild yet. Fix the problems listed, then try again.",
|
|
45
|
+
};
|
|
33
46
|
function refused(c, deps, projectId, value) {
|
|
34
47
|
const status = statuses[value.reason];
|
|
35
48
|
return problem(c, {
|
|
36
49
|
status,
|
|
37
50
|
title: titleOf(status),
|
|
38
|
-
detail: value.reason
|
|
51
|
+
detail: details[value.reason],
|
|
39
52
|
extensions: {
|
|
40
53
|
reason: value.reason,
|
|
41
54
|
currentRevisionId: currentRevisionId(deps.db, projectId) ?? null,
|
|
@@ -47,11 +60,11 @@ function unavailable(c) {
|
|
|
47
60
|
return problem(c, {
|
|
48
61
|
status: 503,
|
|
49
62
|
title: titleOf(503),
|
|
50
|
-
detail: "
|
|
63
|
+
detail: "Rebuilding is not ready yet because Slopify is still starting. Wait a moment, reload the page and try again.",
|
|
51
64
|
});
|
|
52
65
|
}
|
|
53
66
|
export function revisionRoutes(deps) {
|
|
54
|
-
return new Hono()
|
|
67
|
+
return (new Hono()
|
|
55
68
|
.post("/:id/revisions/prepare", zValidator("param", projectParam, onInvalid), async (c) => {
|
|
56
69
|
const projectId = c.req.valid("param").id;
|
|
57
70
|
const result = await ensureBaseline(deps, projectId);
|
|
@@ -69,6 +82,19 @@ export function revisionRoutes(deps) {
|
|
|
69
82
|
return view === undefined
|
|
70
83
|
? refused(c, deps, projectId, { reason: "no-revision" })
|
|
71
84
|
: c.json({ view });
|
|
85
|
+
})
|
|
86
|
+
// Chunk keys in spoken order, so the Narration editor numbers chunks the way a subtitle
|
|
87
|
+
// error names them. Building the plan is the costly part, so it has its own request.
|
|
88
|
+
.get("/:id/revisions/:revisionId/narration-chunks", zValidator("param", revisionParam, onInvalid), (c) => {
|
|
89
|
+
const { id: projectId, revisionId } = c.req.valid("param");
|
|
90
|
+
let chunks;
|
|
91
|
+
try {
|
|
92
|
+
chunks = narrationChunkOrder(deps, projectId, revisionId);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
chunks = undefined;
|
|
96
|
+
}
|
|
97
|
+
return c.json({ chunks: chunks ?? null });
|
|
72
98
|
})
|
|
73
99
|
.post("/:id/revisions", zValidator("param", projectParam, onInvalid), zValidator("json", saveBody, onInvalid), async (c) => {
|
|
74
100
|
const projectId = c.req.valid("param").id;
|
|
@@ -99,5 +125,5 @@ export function revisionRoutes(deps) {
|
|
|
99
125
|
const projectId = c.req.valid("param").id;
|
|
100
126
|
const result = await startRebuild(deps.rebuild, { projectId, ...c.req.valid("json") });
|
|
101
127
|
return result.ok ? c.json(result, 202) : refused(c, deps, projectId, result);
|
|
102
|
-
});
|
|
128
|
+
}));
|
|
103
129
|
}
|
|
@@ -13,15 +13,15 @@ function refused(c, result) {
|
|
|
13
13
|
? 409
|
|
14
14
|
: 400;
|
|
15
15
|
const detail = {
|
|
16
|
-
"invalid-input": "Check
|
|
17
|
-
"not-found": "
|
|
18
|
-
conflict: "
|
|
19
|
-
"cancel-required": "Cancel
|
|
20
|
-
"missing-template": "The
|
|
21
|
-
"unsupported-media": "This template
|
|
22
|
-
"not-due": "
|
|
23
|
-
"spend-limit": "The
|
|
24
|
-
readiness: "
|
|
16
|
+
"invalid-input": "Some schedule settings are not valid. Check each field and save again.",
|
|
17
|
+
"not-found": "This schedule no longer exists; it may have been deleted. Reload the page to see your schedules.",
|
|
18
|
+
conflict: "This schedule changed while you were editing, possibly because a run just started. Reload the page to see the latest, then make your change again.",
|
|
19
|
+
"cancel-required": "This schedule is still active. Cancel it first, then delete it. Completed schedules can be deleted straight away.",
|
|
20
|
+
"missing-template": "The template you picked was deleted or changed. Choose a template again, then save.",
|
|
21
|
+
"unsupported-media": "This template uses audio, images or a thumbnail you supplied, so it cannot run on a schedule. Pick a template that generates these instead.",
|
|
22
|
+
"not-due": "This one-off time has already passed. Choose a time in the future.",
|
|
23
|
+
"spend-limit": "The estimated cost is above this schedule's spend limit, or some prices are unknown. Raise the spend limit or choose models with known prices.",
|
|
24
|
+
readiness: "A provider this template needs is not ready, for example a missing API key, model or voice. Check Settings → Providers, then try again.",
|
|
25
25
|
};
|
|
26
26
|
return problem(c, {
|
|
27
27
|
status,
|
|
@@ -30,7 +30,7 @@ export function settingsRoutes(deps) {
|
|
|
30
30
|
return problem(c, {
|
|
31
31
|
status: 400,
|
|
32
32
|
title: titleOf(400),
|
|
33
|
-
detail: "These settings cannot be saved
|
|
33
|
+
detail: "These settings cannot be saved yet. Fix the highlighted fields and try again.",
|
|
34
34
|
extensions: { fields: result.fields },
|
|
35
35
|
});
|
|
36
36
|
}
|
|
@@ -46,12 +46,12 @@ export function settingsRoutes(deps) {
|
|
|
46
46
|
? problem(c, {
|
|
47
47
|
status: 409,
|
|
48
48
|
title: titleOf(409),
|
|
49
|
-
detail: "This voice ID is already
|
|
49
|
+
detail: "This voice ID is already saved for this provider. Use the existing voice, or enter a different voice ID.",
|
|
50
50
|
})
|
|
51
51
|
: problem(c, {
|
|
52
52
|
status: 400,
|
|
53
53
|
title: titleOf(400),
|
|
54
|
-
detail: "This voice cannot be added
|
|
54
|
+
detail: "This voice cannot be added yet. Fix the highlighted field and try again.",
|
|
55
55
|
extensions: { fields: [fieldOf(result.reason)] },
|
|
56
56
|
});
|
|
57
57
|
}
|
|
@@ -62,7 +62,7 @@ export function settingsRoutes(deps) {
|
|
|
62
62
|
return problem(c, {
|
|
63
63
|
status: 404,
|
|
64
64
|
title: titleOf(404),
|
|
65
|
-
detail: "
|
|
65
|
+
detail: "This voice no longer exists; it may have been deleted already. Reload the page to see your voices.",
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
return c.body(null, 204);
|
|
@@ -71,20 +71,26 @@ export function settingsRoutes(deps) {
|
|
|
71
71
|
function fieldOf(reason) {
|
|
72
72
|
switch (reason) {
|
|
73
73
|
case "blank-name":
|
|
74
|
-
return { field: "name", message: "
|
|
74
|
+
return { field: "name", message: "Enter a name for this voice." };
|
|
75
75
|
case "name-too-long":
|
|
76
76
|
return {
|
|
77
77
|
field: "name",
|
|
78
|
-
message: `
|
|
78
|
+
message: `Keep the voice name to ${String(voiceNameMax)} characters or fewer.`,
|
|
79
79
|
};
|
|
80
80
|
case "blank-voice-id":
|
|
81
|
-
return {
|
|
81
|
+
return {
|
|
82
|
+
field: "voiceId",
|
|
83
|
+
message: "Enter the voice ID from your provider's voice library.",
|
|
84
|
+
};
|
|
82
85
|
case "voice-id-too-long":
|
|
83
86
|
return {
|
|
84
87
|
field: "voiceId",
|
|
85
|
-
message: `A voice ID is at most ${String(voiceIdMax)} characters.`,
|
|
88
|
+
message: `A voice ID is at most ${String(voiceIdMax)} characters. Check you copied only the ID.`,
|
|
86
89
|
};
|
|
87
90
|
case "not-a-tts-provider":
|
|
88
|
-
return {
|
|
91
|
+
return {
|
|
92
|
+
field: "provider",
|
|
93
|
+
message: "Choose a text-to-speech provider, such as Inworld or Cartesia.",
|
|
94
|
+
};
|
|
89
95
|
}
|
|
90
96
|
}
|
|
@@ -34,7 +34,7 @@ export function stagingRoutes(deps) {
|
|
|
34
34
|
return problem(c, {
|
|
35
35
|
status: 400,
|
|
36
36
|
title: titleOf(400),
|
|
37
|
-
detail: "
|
|
37
|
+
detail: "No file arrived with the upload. Choose the file again and retry.",
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
if (!result.ok) {
|
|
@@ -49,8 +49,8 @@ export function stagingRoutes(deps) {
|
|
|
49
49
|
status: result.reason === "in-use" ? 409 : 404,
|
|
50
50
|
title: titleOf(result.reason === "in-use" ? 409 : 404),
|
|
51
51
|
detail: result.reason === "in-use"
|
|
52
|
-
? "This file is
|
|
53
|
-
: "
|
|
52
|
+
? "This file is still attached to a draft in Play. Remove it from the draft first."
|
|
53
|
+
: "This uploaded file no longer exists; it may have been removed already. Reload the page.",
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
return c.body(null, 204);
|
|
@@ -58,6 +58,6 @@ export function stagingRoutes(deps) {
|
|
|
58
58
|
}
|
|
59
59
|
function detailOf(reason) {
|
|
60
60
|
return reason === "empty-file"
|
|
61
|
-
? "
|
|
62
|
-
: "
|
|
61
|
+
? "This file is empty (0 bytes). Choose a file that has content."
|
|
62
|
+
: "This file's name is empty or contains a slash. Rename the file on your computer, then upload it again.";
|
|
63
63
|
}
|
|
@@ -23,7 +23,7 @@ export function storageRoutes(deps) {
|
|
|
23
23
|
return problem(c, {
|
|
24
24
|
status: 500,
|
|
25
25
|
title: titleOf(500),
|
|
26
|
-
detail: "
|
|
26
|
+
detail: "Slopify could not create the backup. Check there is free disk space and try again; if it keeps failing, use Download diagnostics in Settings and report it.",
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
})
|
|
@@ -33,14 +33,14 @@ export function storageRoutes(deps) {
|
|
|
33
33
|
return problem(c, {
|
|
34
34
|
status: 415,
|
|
35
35
|
title: titleOf(415),
|
|
36
|
-
detail: "
|
|
36
|
+
detail: "Choose a Slopify backup file (.zip) made with Backup & storage in Settings.",
|
|
37
37
|
});
|
|
38
38
|
const upload = await readPortableUpload(c.req.raw);
|
|
39
39
|
if (!upload.ok)
|
|
40
40
|
return problem(c, {
|
|
41
41
|
status: 413,
|
|
42
42
|
title: titleOf(413),
|
|
43
|
-
detail: "
|
|
43
|
+
detail: "This backup file is empty or larger than 100 MB. Choose a Slopify backup file under 100 MB.",
|
|
44
44
|
});
|
|
45
45
|
try {
|
|
46
46
|
const imported = importPortable({
|
|
@@ -55,7 +55,7 @@ export function storageRoutes(deps) {
|
|
|
55
55
|
return problem(c, {
|
|
56
56
|
status: 400,
|
|
57
57
|
title: titleOf(400),
|
|
58
|
-
detail: "
|
|
58
|
+
detail: "This file is not a complete Slopify backup; it may be damaged or a different kind of ZIP. Choose a backup made with Backup & storage in Settings.",
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
})
|
|
@@ -16,7 +16,7 @@ function revisionRequired(c) {
|
|
|
16
16
|
return problem(c, {
|
|
17
17
|
status: 409,
|
|
18
18
|
title: titleOf(409),
|
|
19
|
-
detail: "
|
|
19
|
+
detail: "This change is now made from the Edit tab. Choose Edit project, save your change, then use Rebuild affected outputs.",
|
|
20
20
|
extensions: { reason: "revision-required" },
|
|
21
21
|
});
|
|
22
22
|
}
|
|
@@ -13,8 +13,8 @@ export function tutorialRoutes(deps) {
|
|
|
13
13
|
status: 409,
|
|
14
14
|
title: titleOf(409),
|
|
15
15
|
detail: result.reason === "unreadable"
|
|
16
|
-
? "
|
|
17
|
-
: "
|
|
16
|
+
? "Your saved tutorial progress could not be read. Choose Restart tutorial to start it again."
|
|
17
|
+
: "Your tutorial progress changed in another tab or window. Reload the page, then try again.",
|
|
18
18
|
extensions: { reason: result.reason },
|
|
19
19
|
});
|
|
20
20
|
return c.json(result.value);
|
package/dist/edge/http/update.js
CHANGED
|
@@ -8,7 +8,7 @@ export function updateRoutes(deps) {
|
|
|
8
8
|
busy: false,
|
|
9
9
|
canUpdate: false,
|
|
10
10
|
status: "idle",
|
|
11
|
-
blockedReason: "
|
|
11
|
+
blockedReason: "This copy of Slopify cannot update itself from the app. Update it the way you installed it, for example by running the launcher again.",
|
|
12
12
|
};
|
|
13
13
|
return new Hono()
|
|
14
14
|
.get("/ready", (c) => {
|
|
@@ -38,7 +38,7 @@ export function updateRoutes(deps) {
|
|
|
38
38
|
return problem(c, {
|
|
39
39
|
status: 403,
|
|
40
40
|
title: titleOf(403),
|
|
41
|
-
detail: "
|
|
41
|
+
detail: "For your safety, updates can only be started from the Slopify page itself. Open Slopify and try again there.",
|
|
42
42
|
});
|
|
43
43
|
if (deps.updater === undefined)
|
|
44
44
|
return problem(c, { status: 409, title: titleOf(409), detail: unavailable.blockedReason });
|
|
@@ -51,7 +51,7 @@ export function updateRoutes(deps) {
|
|
|
51
51
|
title: titleOf(status),
|
|
52
52
|
detail: result.info.blockedReason ??
|
|
53
53
|
result.info.error ??
|
|
54
|
-
"
|
|
54
|
+
"You already have the latest version of Slopify.",
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
57
|
}
|
package/dist/host-cli/install.js
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { access, lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { isStableVersion, updatePackage } from "../updater/model.js";
|
|
6
6
|
import { installArgs, npmCommand } from "../updater/plan.js";
|
|
7
7
|
import { hasCode } from "./paths.js";
|
|
8
8
|
export const nodeHostSetupRunner = {
|
|
9
9
|
exec: (file, args, signal) => new Promise((resolve, reject) => {
|
|
10
|
-
execFile(file, [...args], { signal, timeout: 15 * 60_000, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout) => {
|
|
10
|
+
execFile(file, [...args], { signal, timeout: 15 * 60_000, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
11
11
|
if (error && typeof error.code !== "number")
|
|
12
|
-
return reject(new Error(
|
|
13
|
-
resolve({ code: typeof error?.code === "number" ? error.code : 0, stdout });
|
|
12
|
+
return reject(new Error(setupCommandFailure(file, args, error)));
|
|
13
|
+
resolve({ code: typeof error?.code === "number" ? error.code : 0, stdout, stderr });
|
|
14
14
|
});
|
|
15
15
|
}),
|
|
16
16
|
};
|
|
17
|
+
// Says which program could not run and what to do about it; Node's own error only names the
|
|
18
|
+
// errno, and the person running the launcher needs the fix.
|
|
19
|
+
function setupCommandFailure(file, args, error) {
|
|
20
|
+
const name = basename(file);
|
|
21
|
+
const step = `"${[name, ...args.slice(0, 2)].join(" ")}"`;
|
|
22
|
+
if (error.code === "ENOENT") {
|
|
23
|
+
if (name === "docker")
|
|
24
|
+
return "Docker is not installed, or the docker command is not on your PATH. Install Docker Engine (https://docs.docker.com/engine/install/), check that docker info works, and run the launcher again.";
|
|
25
|
+
if (name === "systemctl" || name === "loginctl")
|
|
26
|
+
return `${name} was not found. Using this machine's AI CLIs from Docker needs systemd; start with --host-cli=off to use API keys only.`;
|
|
27
|
+
if (name === "npm" || name === "npm.cmd")
|
|
28
|
+
return "npm was not found. Install Node.js with npm (https://nodejs.org) and run the launcher again.";
|
|
29
|
+
return `${name} was not found on your PATH. Install it and run the launcher again.`;
|
|
30
|
+
}
|
|
31
|
+
if (error.code === "EACCES")
|
|
32
|
+
return `${file} could not be run (permission denied). Check that your user may run it, then try again.`;
|
|
33
|
+
if (error.code === "ABORT_ERR" || error.name === "AbortError")
|
|
34
|
+
return `${step} was stopped before it finished. Run the launcher again; Slopify will finish or undo the half-done step.`;
|
|
35
|
+
if (error.killed)
|
|
36
|
+
return `${step} took too long and was stopped. Check your internet connection and that Docker is running (docker info), then run the launcher again.`;
|
|
37
|
+
return `${step} could not finish (${error.message}). Check that Docker, Node.js and systemd user services work on this machine, then run the launcher again.`;
|
|
38
|
+
}
|
|
17
39
|
async function entryAt(directory, version) {
|
|
18
40
|
const path = join(directory, "node_modules", "@gentbajko", "slopify");
|
|
19
41
|
z.object({ name: z.literal(updatePackage), version: z.literal(version) }).parse(JSON.parse(await readFile(join(path, "package.json"), "utf8")));
|
|
@@ -34,7 +56,7 @@ export async function installHostPackage(options) {
|
|
|
34
56
|
}
|
|
35
57
|
catch (error) {
|
|
36
58
|
if (!hasCode(error, "ENOENT"))
|
|
37
|
-
throw new Error(
|
|
59
|
+
throw new Error(`The installed host helper in ${directory} is damaged. Delete that folder and run the launcher again. Nothing else was changed.`);
|
|
38
60
|
}
|
|
39
61
|
await mkdir(versions, { recursive: true, mode: 0o700 });
|
|
40
62
|
const staging = await mkdtemp(join(versions, `.install-${version}-`));
|
|
@@ -50,7 +72,7 @@ export async function installHostPackage(options) {
|
|
|
50
72
|
options.packageSource ?? `${updatePackage}@${version}`,
|
|
51
73
|
], signal);
|
|
52
74
|
if (result.code !== 0)
|
|
53
|
-
throw new Error("npm could not install the host helper.
|
|
75
|
+
throw new Error("npm could not install the Slopify host helper. Check your internet connection (npm must reach registry.npmjs.org) and run the launcher again. Your existing container was not changed.");
|
|
54
76
|
await entryAt(staging, version);
|
|
55
77
|
await rename(staging, directory);
|
|
56
78
|
return { entry: await entryAt(directory, version) };
|
package/dist/host-cli/paths.js
CHANGED
|
@@ -4,12 +4,12 @@ import { chmod, lstat, mkdir, open } from "node:fs/promises";
|
|
|
4
4
|
import { isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
export async function prepareHostPaths(root) {
|
|
6
6
|
if (!isAbsolute(root) || /[\p{Cc}]/u.test(root))
|
|
7
|
-
throw new Error(
|
|
7
|
+
throw new Error(`Unsafe host helper directory ${JSON.stringify(root)}: it must be a full path. Check XDG_DATA_HOME and HOME, then run the launcher again.`);
|
|
8
8
|
const normalized = resolve(root);
|
|
9
9
|
const share = join(normalized, "share");
|
|
10
10
|
const socket = join(share, "cli.sock");
|
|
11
11
|
if (Buffer.byteLength(socket) > 100)
|
|
12
|
-
throw new Error(
|
|
12
|
+
throw new Error(`The host helper path ${socket} is too long for this system (over 100 bytes). Set a shorter XDG_DATA_HOME, for example XDG_DATA_HOME=~/.local/share, and run the launcher again.`);
|
|
13
13
|
await mkdir(normalized, { recursive: true, mode: 0o700 });
|
|
14
14
|
await ownedDirectory(normalized, true);
|
|
15
15
|
await chmod(normalized, 0o700);
|
|
@@ -24,19 +24,19 @@ async function ownedDirectory(path, privateRoot = false) {
|
|
|
24
24
|
entry.isSymbolicLink() ||
|
|
25
25
|
(privateRoot && (entry.mode & 0o077) !== 0) ||
|
|
26
26
|
(process.getuid && entry.uid !== process.getuid()))
|
|
27
|
-
throw new Error(
|
|
27
|
+
throw new Error(`Unsafe host helper directory ${path}: it must be a real folder owned by you${privateRoot ? " that only you can read" : ""}. Fix its owner and permissions (chmod ${privateRoot ? "700" : "755"} ${path}) or delete it, then run the launcher again.`);
|
|
28
28
|
}
|
|
29
29
|
export async function readBridgeToken(path) {
|
|
30
30
|
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
31
31
|
try {
|
|
32
32
|
const entry = await file.stat();
|
|
33
33
|
if (!entry.isFile() || entry.size !== 64 || (entry.mode & 0o022) !== 0)
|
|
34
|
-
throw new Error(
|
|
34
|
+
throw new Error(`The host helper token file ${path} is damaged. Delete it and run the launcher again.`);
|
|
35
35
|
const buffer = Buffer.alloc(65);
|
|
36
36
|
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
37
37
|
const value = buffer.subarray(0, bytesRead).toString("utf8");
|
|
38
38
|
if (!/^[a-f0-9]{64}$/.test(value))
|
|
39
|
-
throw new Error(
|
|
39
|
+
throw new Error(`The host helper token file ${path} is damaged. Delete it and run the launcher again.`);
|
|
40
40
|
return value;
|
|
41
41
|
}
|
|
42
42
|
finally {
|
|
@@ -60,7 +60,7 @@ export async function ensureBridgeToken(path) {
|
|
|
60
60
|
}
|
|
61
61
|
const entry = await lstat(path);
|
|
62
62
|
if (entry.isSymbolicLink() || (process.getuid && entry.uid !== process.getuid()))
|
|
63
|
-
throw new Error(
|
|
63
|
+
throw new Error(`The host helper token file ${path} is a link or belongs to another user. Delete it and run the launcher again.`);
|
|
64
64
|
return readBridgeToken(path);
|
|
65
65
|
}
|
|
66
66
|
export function hasCode(error, code) {
|
package/dist/host-cli/server.js
CHANGED
|
@@ -19,7 +19,7 @@ export async function startHostServer(options) {
|
|
|
19
19
|
if (!existing.isSocket() || (process.getuid && existing.uid !== process.getuid()))
|
|
20
20
|
throw new Error("Unsafe host helper socket.");
|
|
21
21
|
if (await listening(socket))
|
|
22
|
-
throw new Error(
|
|
22
|
+
throw new Error(`A Slopify host helper is already listening on ${socket}. Stop it first (systemctl --user stop slopify-cli-bridge.service).`);
|
|
23
23
|
await unlink(socket);
|
|
24
24
|
}
|
|
25
25
|
catch (error) {
|
package/dist/host-cli/service.js
CHANGED
|
@@ -39,11 +39,11 @@ export async function privateRead(path, uid) {
|
|
|
39
39
|
try {
|
|
40
40
|
const stat = await file.stat();
|
|
41
41
|
if (!stat.isFile() || stat.uid !== uid || stat.size > 64 * 1024 || (stat.mode & 0o022) !== 0)
|
|
42
|
-
throw new Error(
|
|
42
|
+
throw new Error(`${path} must be a regular file owned by you that others can't write to. Fix it (chmod 600 ${path}) or delete it, then run the launcher again.`);
|
|
43
43
|
const buffer = Buffer.alloc(64 * 1024 + 1);
|
|
44
44
|
const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
|
|
45
45
|
if (bytesRead > 64 * 1024)
|
|
46
|
-
throw new Error(
|
|
46
|
+
throw new Error(`${path} is too large to be a Slopify file. Delete it and run the launcher again.`);
|
|
47
47
|
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
48
48
|
}
|
|
49
49
|
finally {
|
|
@@ -106,35 +106,35 @@ async function waitHealth(options, accepting) {
|
|
|
106
106
|
return health;
|
|
107
107
|
await delay(100, undefined, { signal: options.signal });
|
|
108
108
|
}
|
|
109
|
-
throw new Error(
|
|
109
|
+
throw new Error(`The Slopify host helper did not start within 30 seconds. See why with: systemctl --user status ${hostUnit}`);
|
|
110
110
|
}
|
|
111
111
|
export async function ensureHostService(options) {
|
|
112
112
|
const { root, env, runner, signal, uid } = options;
|
|
113
113
|
const environment = JSON.stringify(hostEnvironment(env));
|
|
114
114
|
const directory = join(env.XDG_CONFIG_HOME ?? join(env.HOME ?? "", ".config"), "systemd/user");
|
|
115
115
|
if (!isAbsolute(directory))
|
|
116
|
-
throw new Error("
|
|
116
|
+
throw new Error("Could not find your systemd user folder because HOME (or XDG_CONFIG_HOME) is not a full path. Set it and run the launcher again.");
|
|
117
117
|
const unitPath = join(directory, hostUnit);
|
|
118
118
|
const configPath = join(root, "host-environment.json");
|
|
119
119
|
const previous = await privateRead(unitPath, uid);
|
|
120
120
|
const previousConfig = await privateRead(configPath, uid);
|
|
121
121
|
if (previous !== undefined && !previous.startsWith(`${marker}\n`))
|
|
122
|
-
throw new Error(
|
|
122
|
+
throw new Error(`A service file ${unitPath} already exists and is not owned by Slopify, so it was left alone. Rename or remove it, or start with --host-cli=off to use API keys only.`);
|
|
123
123
|
const unit = serviceUnit(options.node, options.entry, root);
|
|
124
124
|
const run = async (file, args, required = true, commandSignal = signal) => {
|
|
125
125
|
const result = await runner.exec(file, args, commandSignal);
|
|
126
126
|
if (required && result.code !== 0)
|
|
127
|
-
throw new Error(`Host helper setup failed
|
|
127
|
+
throw new Error(`Host helper setup failed at "${[file, ...args.slice(0, 2)].join(" ")}"${result.stderr?.trim() ? ` (${result.stderr.trim().split("\n").at(-1)?.slice(0, 300)})` : ""}. Check that systemd user services work (systemctl --user status), or start with --host-cli=off to use API keys only. Your existing container was not changed.`);
|
|
128
128
|
return result;
|
|
129
129
|
};
|
|
130
130
|
const fragment = (await run("systemctl", ["--user", "show", hostUnit, "--property=FragmentPath", "--value"])).stdout.trim();
|
|
131
131
|
if (fragment && fragment !== unitPath)
|
|
132
|
-
throw new Error(
|
|
132
|
+
throw new Error(`A ${hostUnit} service from somewhere else (${fragment}) is already installed and is not owned by this Slopify, so it was left alone. Remove it, or start with --host-cli=off to use API keys only.`);
|
|
133
133
|
const active = (await run("systemctl", ["--user", "is-active", "--quiet", hostUnit], false)).code === 0;
|
|
134
134
|
const wasEnabled = (await run("systemctl", ["--user", "is-enabled", "--quiet", hostUnit], false)).code === 0;
|
|
135
135
|
const current = await helperHealth(root, signal);
|
|
136
136
|
if (active && (!previous || !current))
|
|
137
|
-
throw new Error(
|
|
137
|
+
throw new Error(`The Slopify host helper is running but not answering, so it was not replaced. Restart it (systemctl --user restart ${hostUnit}) and run the launcher again; no work was interrupted.`);
|
|
138
138
|
const linger = (await run("loginctl", ["show-user", String(uid), "-p", "Linger", "--value"])).stdout.trim();
|
|
139
139
|
if (linger !== "yes")
|
|
140
140
|
await run("loginctl", ["enable-linger", String(uid)]);
|
|
@@ -152,12 +152,12 @@ export async function ensureHostService(options) {
|
|
|
152
152
|
paused = true;
|
|
153
153
|
await run("systemctl", ["--user", "kill", "--kill-whom=main", "--signal=SIGHUP", hostUnit]);
|
|
154
154
|
if ((await waitHealth(options, false)).active !== 0)
|
|
155
|
-
throw new Error("The host helper is doing work. Wait for
|
|
155
|
+
throw new Error("The host helper is doing work for Slopify right now. Wait for running projects to finish, then run the launcher again.");
|
|
156
156
|
}
|
|
157
157
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
158
158
|
const directoryStat = await lstat(directory);
|
|
159
159
|
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink() || directoryStat.uid !== uid)
|
|
160
|
-
throw new Error(
|
|
160
|
+
throw new Error(`${directory} is not a normal folder owned by your user, so Slopify can't add its service there. Fix its owner and run the launcher again.`);
|
|
161
161
|
replaced = true;
|
|
162
162
|
await privateWrite(configPath, environment);
|
|
163
163
|
await privateWrite(unitPath, unit);
|
|
@@ -193,7 +193,7 @@ export async function ensureHostService(options) {
|
|
|
193
193
|
await restore(["--user", "kill", "--kill-whom=main", "--signal=SIGUSR2", hostUnit]);
|
|
194
194
|
}
|
|
195
195
|
catch {
|
|
196
|
-
throw new Error(
|
|
196
|
+
throw new Error(`Host helper setup failed and Slopify could not fully undo it. Check the service with systemctl --user status ${hostUnit}; your Docker container was not changed.`);
|
|
197
197
|
}
|
|
198
198
|
throw error;
|
|
199
199
|
}
|
package/dist/host-cli/status.js
CHANGED
|
@@ -24,7 +24,9 @@ export async function resolveHostCommand(id, env) {
|
|
|
24
24
|
throw error;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
throw Object.assign(new Error(
|
|
27
|
+
throw Object.assign(new Error(`${hostCommandName(id)} was not found on this machine's PATH.`), {
|
|
28
|
+
code: "ENOENT",
|
|
29
|
+
});
|
|
28
30
|
}
|
|
29
31
|
const authSchema = z.object({ loggedIn: z.boolean() });
|
|
30
32
|
export function parseHostLogin(id, stdout, stderr) {
|
|
@@ -72,7 +74,9 @@ export function createHostStatus(deps) {
|
|
|
72
74
|
installed: false,
|
|
73
75
|
login: "unknown",
|
|
74
76
|
issueKind: isCode(error, "ENOENT") ? "missing" : "bridge",
|
|
75
|
-
issue:
|
|
77
|
+
issue: isCode(error, "ENOENT")
|
|
78
|
+
? `${hostCommandName(id)} was not found on this machine. Install it and sign in there, then run the launcher again: npx @gentbajko/slopify --docker`
|
|
79
|
+
: `Slopify could not check for ${hostCommandName(id)} on this machine (${error instanceof Error ? error.message : String(error)}). Fix the problem, then run the launcher again: npx @gentbajko/slopify --docker`,
|
|
76
80
|
};
|
|
77
81
|
}
|
|
78
82
|
const readiness = readinessFromProbe(await deps.probe(command, ["--version"], cliProbeTimeoutMs), {
|
|
@@ -96,7 +100,7 @@ export function createHostStatus(deps) {
|
|
|
96
100
|
...base,
|
|
97
101
|
login: "unknown",
|
|
98
102
|
issueKind: "missing",
|
|
99
|
-
issue:
|
|
103
|
+
issue: `${command} is installed but did not answer "${hostCommandName(id)} --version". Reinstall or update it on this machine, then run the launcher again: npx @gentbajko/slopify --docker`,
|
|
100
104
|
};
|
|
101
105
|
if (readiness.issue)
|
|
102
106
|
return { ...base, login: "unknown", issueKind: "version", issue: readiness.issue };
|
|
@@ -7,19 +7,21 @@ export function configFrom(flags, env) {
|
|
|
7
7
|
const host = flags.host ?? env.SLOPIFY_HOST ?? defaultHost;
|
|
8
8
|
const dataDir = flags["data-dir"] ?? env.SLOPIFY_DATA_DIR ?? join(homedir(), ".slopify");
|
|
9
9
|
if (host.trim() === "") {
|
|
10
|
-
throw new Error(
|
|
10
|
+
throw new Error(`The host setting is empty (from ${flags.host !== undefined ? "--host" : "SLOPIFY_HOST"}). Leave it out to use 127.0.0.1, or give an address such as --host 127.0.0.1.`);
|
|
11
11
|
}
|
|
12
12
|
return {
|
|
13
|
-
port: port === undefined
|
|
13
|
+
port: port === undefined
|
|
14
|
+
? defaultPort
|
|
15
|
+
: parsePort(port, flags.port !== undefined ? "--port" : "SLOPIFY_PORT"),
|
|
14
16
|
host,
|
|
15
17
|
dataDir: resolve(dataDir),
|
|
16
18
|
open: flags["no-open"] === true ? false : !isTruthy(env.SLOPIFY_NO_OPEN),
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
|
-
function parsePort(value) {
|
|
21
|
+
function parsePort(value, source) {
|
|
20
22
|
const port = Number(value.trim());
|
|
21
23
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
22
|
-
throw new Error(`
|
|
24
|
+
throw new Error(`Invalid port ${JSON.stringify(value)} (from ${source}): use a whole number between 1 and 65535, for example --port 7070.`);
|
|
23
25
|
}
|
|
24
26
|
return port;
|
|
25
27
|
}
|
|
@@ -12,7 +12,8 @@ export function migrate(db, clock) {
|
|
|
12
12
|
const newest = Math.max(0, ...applied);
|
|
13
13
|
const newestKnown = Math.max(0, ...known);
|
|
14
14
|
if (newest > newestKnown) {
|
|
15
|
-
throw new Error(`database schema ${newest} is newer than this app knows (${newestKnown})`
|
|
15
|
+
throw new Error(`Your Slopify database was last used by a newer version of Slopify (database schema ${newest} is newer than this app knows (${newestKnown})). ` +
|
|
16
|
+
"Update Slopify and start it again: npx @gentbajko/slopify@latest (or pull the latest Docker image). Your data was not changed.");
|
|
16
17
|
}
|
|
17
18
|
for (const [index, file] of files.entries()) {
|
|
18
19
|
const version = known[index];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- A schedule's items became a queue of topics: each run fills one template keyword with the
|
|
2
|
+
-- first topic and the rest of the keywords with fixed values.
|
|
3
|
+
ALTER TABLE schedules ADD COLUMN topic_keyword TEXT;
|
|
4
|
+
ALTER TABLE schedules ADD COLUMN values_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(values_json));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// A failure's own sentence and every cause it wraps, for the log. The page shows only the
|
|
2
|
+
// sentence; the chain is the only place a "download interrupted" says the server closed the
|
|
3
|
+
// stream.
|
|
4
|
+
export function causedBy(error) {
|
|
5
|
+
const chain = [error instanceof Error ? error.message : String(error)];
|
|
6
|
+
let cause = error instanceof Error ? error.cause : undefined;
|
|
7
|
+
while (cause !== undefined && cause !== null && chain.length < 6) {
|
|
8
|
+
chain.push(cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause));
|
|
9
|
+
cause = cause instanceof Error ? cause.cause : undefined;
|
|
10
|
+
}
|
|
11
|
+
return chain.join(" <- ");
|
|
12
|
+
}
|
package/dist/kernel/lock.js
CHANGED
|
@@ -6,14 +6,15 @@ export function acquireInstanceLock(lockPath) {
|
|
|
6
6
|
if (!claim(lockPath, pid)) {
|
|
7
7
|
const holder = holderOf(lockPath);
|
|
8
8
|
if (holder !== undefined && holds(holder, pid)) {
|
|
9
|
-
throw new Error(`
|
|
10
|
-
|
|
9
|
+
throw new Error(`Slopify is already running on this data directory (process ${holder.pid}). ` +
|
|
10
|
+
"Use the copy that is already open, or stop it (Ctrl+C in its terminal) and start again. " +
|
|
11
|
+
`If no Slopify is running, delete ${lockPath} and try again.`);
|
|
11
12
|
}
|
|
12
13
|
// The recorded process is gone, or the file says nothing usable. Reclaiming it here
|
|
13
14
|
// is what keeps a crash from bricking the data directory until a file is deleted by hand.
|
|
14
15
|
discard(lockPath);
|
|
15
16
|
if (!claim(lockPath, pid)) {
|
|
16
|
-
throw new Error(`
|
|
17
|
+
throw new Error(`Another Slopify started on this data directory at the same moment (lock file ${lockPath}). Close the extra copy and start Slopify again.`);
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
return {
|