@gentbajko/slopify 2.5.0 → 3.0.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/LICENSE +201 -21
- package/NOTICE +12 -0
- package/README.md +21 -40
- package/SUBTITLES.md +1 -1
- package/dist/adapter-registry.js +3 -0
- package/dist/adapters/alignment/cache.js +4 -1
- package/dist/adapters/alignment/ctc.js +25 -23
- package/dist/adapters/alignment/index.js +37 -6
- package/dist/adapters/alignment/multilingual.js +267 -0
- package/dist/adapters/alignment/protocol.js +11 -0
- package/dist/adapters/alignment/quality.js +4 -3
- package/dist/adapters/alignment/sentences.js +95 -0
- package/dist/adapters/alignment/spec.js +24 -0
- package/dist/adapters/alignment/spell.js +769 -0
- package/dist/adapters/alignment/text.js +56 -9
- package/dist/adapters/alignment/window.js +34 -30
- package/dist/adapters/alignment/worker.js +23 -21
- package/dist/adapters/explain.js +11 -11
- package/dist/adapters/host-cli/index.js +44 -5
- package/dist/adapters/host-cli/transport.js +7 -5
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +1 -1
- package/dist/adapters/image/codex.js +87 -22
- package/dist/adapters/image/fal.js +13 -5
- package/dist/adapters/image/google.js +11 -8
- package/dist/adapters/image/openai.js +14 -7
- package/dist/adapters/image/reference.js +34 -0
- package/dist/adapters/image/replicate.js +7 -2
- package/dist/adapters/image/video.js +2 -2
- package/dist/adapters/key-probes.js +36 -0
- package/dist/adapters/llm/claude-code.js +168 -32
- package/dist/adapters/llm/cli-login-error.js +1 -1
- package/dist/adapters/llm/codex-limits.js +157 -0
- package/dist/adapters/llm/codex.js +99 -12
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +16 -3
- package/dist/adapters/llm/image-workspace.js +58 -0
- package/dist/adapters/llm/openrouter.js +11 -3
- package/dist/adapters/llm/run-cli.js +12 -7
- package/dist/adapters/tts/cartesia.js +10 -0
- package/dist/adapters/tts/elevenlabs.js +31 -9
- package/dist/adapters/tts/inworld-async.js +9 -5
- package/dist/adapters/tts/inworld.js +17 -3
- package/dist/adapters/tts/openai.js +3 -0
- package/dist/adapters/tts/voice-languages.js +80 -0
- package/dist/assets/fonts/noto/NotoSans-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansArabic-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansDevanagari-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansHebrew-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/NotoSansThai-Regular.ttf +0 -0
- package/dist/assets/fonts/noto/OFL.txt +93 -0
- package/dist/assets/fonts/noto/SOURCE.txt +18 -0
- package/dist/assets/sample/sample-audiobook.tar +0 -0
- package/dist/assets/sample/sample-podcast.tar +0 -0
- package/dist/assets/sample/sample-project.tar +0 -0
- package/dist/catalog/merge.js +132 -0
- package/dist/catalog/registry.js +13 -13
- package/dist/catalog/schema.js +2 -0
- package/dist/catalog/store.js +147 -8
- package/dist/compose.yaml +71 -0
- package/dist/edge/autostart/docker-record.js +32 -0
- package/dist/edge/autostart/docker.js +50 -0
- package/dist/edge/autostart/index.js +78 -0
- package/dist/edge/autostart/launcher.js +176 -0
- package/dist/edge/autostart/model.js +3 -0
- package/dist/edge/autostart/native.js +210 -0
- package/dist/edge/autostart/prompt.js +66 -0
- package/dist/edge/autostart/service.js +81 -0
- package/dist/edge/cli.js +57 -51
- package/dist/edge/docker-install/activation.js +56 -0
- package/dist/edge/docker-install/apply.js +458 -0
- package/dist/edge/docker-install/engine.js +362 -0
- package/dist/edge/docker-install/run.js +154 -0
- package/dist/edge/docker-install/state.js +146 -0
- package/dist/edge/{docker-projects → docker-install}/volume.js +1 -5
- package/dist/edge/docker.js +14 -22
- package/dist/edge/host-cli.js +1 -1
- package/dist/edge/http/actions.js +32 -10
- package/dist/edge/http/app.js +58 -2
- package/dist/edge/http/auditions.js +70 -0
- package/dist/edge/http/autostart.js +37 -0
- package/dist/edge/http/channel-memory.js +92 -0
- package/dist/edge/http/channels.js +128 -0
- package/dist/edge/http/entries.js +22 -3
- package/dist/edge/http/files.js +1 -1
- package/dist/edge/http/folder-location.js +1 -1
- package/dist/edge/http/home.js +39 -0
- package/dist/edge/http/host-cli.js +29 -5
- package/dist/edge/http/narration-peaks.js +76 -0
- package/dist/edge/http/onboarding.js +152 -0
- package/dist/edge/http/open-folder.js +1 -1
- package/dist/edge/http/patch-notes.js +45 -0
- package/dist/edge/http/planning.js +4 -2
- package/dist/edge/http/project-create.js +87 -0
- package/dist/edge/http/projects.js +31 -99
- package/dist/edge/http/prompts.js +40 -3
- package/dist/edge/http/pronunciations.js +23 -4
- package/dist/edge/http/providers.js +86 -0
- package/dist/edge/http/reviews.js +121 -0
- package/dist/edge/http/revision-files.js +1 -1
- package/dist/edge/http/revisions.js +1 -1
- package/dist/edge/http/run-cost.js +25 -0
- package/dist/edge/http/schedules.js +111 -6
- package/dist/edge/http/settings.js +84 -6
- package/dist/edge/http/storage-files.js +50 -0
- package/dist/edge/http/storage.js +29 -2
- package/dist/edge/http/studio.js +207 -0
- package/dist/edge/http/style-preview.js +100 -0
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/trash.js +69 -0
- package/dist/edge/http/update.js +24 -3
- package/dist/edge/http/whats-new.js +15 -0
- package/dist/edge/http/youtube-edits.js +83 -0
- package/dist/edge/native-update.js +62 -0
- package/dist/host-cli/open-folder.js +10 -10
- package/dist/host-cli/runtime.js +9 -1
- package/dist/kernel/db/migrations/0025-automatic-reviews.sql +40 -0
- package/dist/kernel/db/migrations/0026-schedule-topic-generation.sql +23 -0
- package/dist/kernel/db/migrations/0027-run-cost.sql +58 -0
- package/dist/kernel/db/migrations/0030-automatic-retries.sql +18 -0
- package/dist/kernel/db/migrations/0031-prompt-history-and-description-edits.sql +33 -0
- package/dist/kernel/db/migrations/0032-channels.sql +65 -0
- package/dist/kernel/db/migrations/0034-script-prompts.sql +23 -0
- package/dist/kernel/db/migrations/0035-narration-aliases.sql +13 -0
- package/dist/kernel/db/migrations/0037-project-uploads.sql +8 -0
- package/dist/kernel/db/migrations/0038-voice-languages.sql +7 -0
- package/dist/kernel/db/migrations/0039-channel-essentials.sql +64 -0
- package/dist/kernel/db/migrations/0040-standalone-usage.sql +35 -0
- package/dist/kernel/paths.js +27 -6
- package/dist/kernel/pipeline.js +4 -1
- package/dist/kernel/ports/host-cli.js +122 -14
- package/dist/kernel/ports/key-probe.js +1 -0
- package/dist/kernel/ports/languages.js +157 -0
- package/dist/kernel/ports/model.js +5 -2
- package/dist/kernel/ports/narration-aliases.js +60 -0
- package/dist/kernel/ports/plan-limits.js +36 -0
- package/dist/kernel/runner/attempt.js +17 -8
- package/dist/kernel/runner/audition.js +36 -0
- package/dist/kernel/runner/graph.js +24 -5
- package/dist/kernel/runner/index.js +35 -5
- package/dist/kernel/runner/meter.js +1 -0
- package/dist/kernel/runner/providers.js +142 -7
- package/dist/kernel/runner/retry-policy.js +43 -0
- package/dist/kernel/runner/standalone.js +134 -0
- package/dist/kernel/runner/work-authority.js +28 -2
- package/dist/main.js +272 -16
- package/dist/patch-notes/0.4.2-to-1.0.md +93 -0
- package/dist/patch-notes/1.0-to-2.0.md +74 -0
- package/dist/patch-notes/2.0-to-3.0.md +257 -0
- package/dist/patch-notes/3.0.0.md +70 -0
- package/dist/patch-notes/index.json +26 -0
- package/dist/slices/admission/model.js +27 -0
- package/dist/slices/admission/repo.js +24 -4
- package/dist/slices/admission/rules.js +79 -1
- package/dist/slices/admission/schema.js +58 -1
- package/dist/slices/admission/short-mode.js +47 -0
- package/dist/slices/admission/start.js +41 -5
- package/dist/slices/article/continuation.js +2 -2
- package/dist/slices/article/provided-entries.js +2 -2
- package/dist/slices/article/run.js +6 -2
- package/dist/slices/article/segments.js +3 -3
- package/dist/slices/backups/folder.js +14 -6
- package/dist/slices/batch/index.js +7 -2
- package/dist/slices/cancel/index.js +1 -1
- package/dist/slices/channels/cast-images.js +110 -0
- package/dist/slices/channels/cast-match.js +32 -0
- package/dist/slices/channels/images.js +19 -0
- package/dist/slices/channels/model.js +5 -0
- package/dist/slices/channels/rebrand.js +100 -0
- package/dist/slices/channels/repo.js +218 -0
- package/dist/slices/channels/runs.js +129 -0
- package/dist/slices/channels/schema.js +129 -0
- package/dist/slices/channels/service.js +180 -0
- package/dist/slices/channels/studio-csv.js +110 -0
- package/dist/slices/channels/videos.js +120 -0
- package/dist/slices/checkpoints/recovery.js +10 -0
- package/dist/slices/control/index.js +2 -1
- package/dist/slices/document/fonts.js +1 -1
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/episodes/related.js +90 -0
- package/dist/slices/episodes/repo.js +60 -0
- package/dist/slices/episodes/service.js +58 -0
- package/dist/slices/episodes/summarize.js +170 -0
- package/dist/slices/estimate/index.js +109 -28
- package/dist/slices/estimate/requests.js +60 -2
- package/dist/slices/fixes/rules.js +73 -0
- package/dist/slices/fonts/coverage.js +82 -0
- package/dist/slices/fonts/sfnt.js +74 -0
- package/dist/slices/images/run.js +8 -5
- package/dist/slices/images/scale.js +84 -0
- package/dist/slices/library/diff.js +122 -0
- package/dist/slices/library/history.js +100 -0
- package/dist/slices/library/model.js +3 -0
- package/dist/slices/library/photorealistic.js +49 -0
- package/dist/slices/library/repo.js +19 -14
- package/dist/slices/library/save.js +62 -12
- package/dist/slices/library/slots.js +7 -1
- package/dist/slices/library/used-by.js +153 -0
- package/dist/slices/loudness/level-pieces.js +41 -0
- package/dist/slices/loudness/loudnorm.js +164 -0
- package/dist/slices/loudness/model.js +153 -0
- package/dist/slices/model-upkeep/model.js +10 -0
- package/dist/slices/model-upkeep/switch.js +217 -0
- package/dist/slices/model-upkeep/usage.js +183 -0
- package/dist/slices/narration/aliases-library.js +31 -0
- package/dist/slices/narration/aliases-schema.js +53 -0
- package/dist/slices/narration/aliases.js +49 -0
- package/dist/slices/narration/blocks.js +352 -0
- package/dist/slices/narration/chunk.js +19 -7
- package/dist/slices/narration/concat.js +2 -2
- package/dist/slices/narration/describe.js +45 -0
- package/dist/slices/narration/pauses-model.js +63 -0
- package/dist/slices/narration/pauses.js +269 -0
- package/dist/slices/narration/peaks-model.js +1 -0
- package/dist/slices/narration/peaks.js +94 -0
- package/dist/slices/narration/preparation.js +21 -3
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +72 -25
- package/dist/slices/narration/run.js +6 -6
- package/dist/slices/narration/shared-glossary.js +1 -1
- package/dist/slices/narration/steering.js +15 -4
- package/dist/slices/notifications/notifier.js +21 -3
- package/dist/slices/notifications/rules.js +22 -1
- package/dist/slices/onboarding/copy.js +143 -0
- package/dist/slices/onboarding/first-run.js +46 -0
- package/dist/slices/onboarding/install.js +119 -0
- package/dist/slices/onboarding/model.js +64 -0
- package/dist/slices/onboarding/packs.js +491 -0
- package/dist/slices/onboarding/quick-short.js +129 -0
- package/dist/slices/onboarding/sample.js +132 -0
- package/dist/slices/onboarding/state.js +68 -0
- package/dist/slices/onboarding/template.js +73 -0
- package/dist/slices/patch-notes/library.js +73 -0
- package/dist/slices/patch-notes/seen.js +77 -0
- package/dist/slices/play-drafts/convert.js +125 -6
- package/dist/slices/play-drafts/readiness.js +3 -0
- package/dist/slices/play-drafts/repo.js +2 -0
- package/dist/slices/play-drafts/review-inputs.js +20 -3
- package/dist/slices/play-drafts/schema.js +89 -0
- package/dist/slices/play-drafts/service.js +4 -1
- package/dist/slices/play-drafts/start.js +43 -18
- package/dist/slices/project-templates/from-project.js +50 -4
- package/dist/slices/project-templates/one-off.js +26 -0
- package/dist/slices/project-templates/repo.js +2 -1
- package/dist/slices/project-templates/service.js +15 -4
- package/dist/slices/project-templates/setup.js +18 -2
- package/dist/slices/rebuild/admission-repo.js +1 -1
- package/dist/slices/rebuild/model.js +6 -0
- package/dist/slices/rebuild/preview-details.js +46 -3
- package/dist/slices/rebuild/preview-plan.js +13 -6
- package/dist/slices/rebuild/recipe-audio-parts.js +40 -7
- package/dist/slices/rebuild/recipe-audio.js +66 -12
- package/dist/slices/rebuild/recipe-build.js +8 -4
- package/dist/slices/rebuild/recipe-cast.js +18 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +11 -9
- package/dist/slices/rebuild/recipe-describe.js +93 -0
- package/dist/slices/rebuild/recipe-edit.js +13 -1
- package/dist/slices/rebuild/recipe-exports.js +62 -8
- package/dist/slices/rebuild/recipe-input-schema.js +35 -0
- package/dist/slices/rebuild/recipe-legacy.js +9 -0
- package/dist/slices/rebuild/recipe-loudness.js +45 -0
- package/dist/slices/rebuild/recipe-model.js +25 -6
- package/dist/slices/rebuild/recipe-pauses.js +7 -0
- package/dist/slices/rebuild/recipe-preparation.js +3 -3
- package/dist/slices/rebuild/recipe-provider-choice.js +11 -1
- package/dist/slices/rebuild/recipe-reference.js +15 -1
- package/dist/slices/rebuild/recipe-reviews.js +117 -0
- package/dist/slices/rebuild/recipe-save.js +4 -1
- package/dist/slices/rebuild/recipe-shorts.js +23 -3
- package/dist/slices/rebuild/recipe-text.js +114 -19
- package/dist/slices/rebuild/recipe-validation.js +14 -2
- package/dist/slices/rebuild/recipe-visual.js +96 -33
- package/dist/slices/rebuild/recipe-voices.js +286 -0
- package/dist/slices/rebuild/recipe-work.js +77 -6
- package/dist/slices/rebuild/recipe-youtube.js +2 -0
- package/dist/slices/rebuild/recovery-conflict.js +7 -3
- package/dist/slices/rebuild/recovery-model.js +3 -0
- package/dist/slices/rebuild/recovery-selection.js +28 -2
- package/dist/slices/rebuild/recovery.js +35 -17
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/retry.js +18 -0
- package/dist/slices/rebuild/review-redo.js +110 -0
- package/dist/slices/rebuild/runtime-actions.js +5 -0
- package/dist/slices/rebuild/runtime-admission.js +4 -1
- package/dist/slices/rebuild/runtime-animate.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-document.js +4 -4
- package/dist/slices/rebuild/runtime-export-bed.js +43 -0
- package/dist/slices/rebuild/runtime-export-edit.js +70 -11
- package/dist/slices/rebuild/runtime-export-inputs.js +52 -10
- package/dist/slices/rebuild/runtime-export-short.js +85 -0
- package/dist/slices/rebuild/runtime-export.js +84 -19
- package/dist/slices/rebuild/runtime-figure-card.js +87 -0
- package/dist/slices/rebuild/runtime-image.js +21 -2
- package/dist/slices/rebuild/runtime-local.js +148 -24
- package/dist/slices/rebuild/runtime-materialize.js +8 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +2 -1
- package/dist/slices/rebuild/runtime-narration-text.js +7 -7
- package/dist/slices/rebuild/runtime-pictures.js +28 -0
- package/dist/slices/rebuild/runtime-piece-label.js +27 -0
- package/dist/slices/rebuild/runtime-provider.js +126 -18
- package/dist/slices/rebuild/runtime-publication.js +21 -5
- package/dist/slices/rebuild/runtime-review.js +237 -0
- package/dist/slices/rebuild/runtime-run.js +17 -11
- package/dist/slices/rebuild/runtime-shorts.js +44 -17
- package/dist/slices/rebuild/runtime-store.js +39 -10
- package/dist/slices/rebuild/runtime-subtitles.js +72 -19
- package/dist/slices/rebuild/runtime-voices.js +103 -0
- package/dist/slices/rebuild/runtime-youtube.js +7 -6
- package/dist/slices/rebuild/soften.js +49 -0
- package/dist/slices/rebuild/transition-repo.js +9 -2
- package/dist/slices/research/run.js +4 -4
- package/dist/slices/research/synthesis.js +2 -2
- package/dist/slices/reviews/model.js +105 -0
- package/dist/slices/reviews/outcome.js +22 -0
- package/dist/slices/reviews/repo.js +106 -0
- package/dist/slices/reviews/rules.js +63 -0
- package/dist/slices/reviews/verdict.js +71 -0
- package/dist/slices/reviews/view.js +30 -0
- package/dist/slices/revisions/adopt-content.js +5 -2
- package/dist/slices/revisions/adopt.js +3 -1
- package/dist/slices/revisions/downloads.js +7 -1
- package/dist/slices/revisions/image-plan.js +183 -0
- package/dist/slices/revisions/mutation-assets.js +10 -0
- package/dist/slices/revisions/mutation-cues.js +71 -0
- package/dist/slices/revisions/mutation-request.js +2 -1
- package/dist/slices/revisions/mutations.js +97 -3
- package/dist/slices/revisions/schema.js +11 -1
- package/dist/slices/run-cost/limits.js +128 -0
- package/dist/slices/run-cost/meter.js +47 -0
- package/dist/slices/run-cost/panel.js +234 -0
- package/dist/slices/run-cost/pricing.js +92 -0
- package/dist/slices/run-cost/week.js +108 -0
- package/dist/slices/schedules/agenda.js +158 -0
- package/dist/slices/schedules/repo.js +58 -18
- package/dist/slices/schedules/scheduler.js +67 -16
- package/dist/slices/schedules/schema.js +138 -1
- package/dist/slices/schedules/service.js +43 -4
- package/dist/slices/schedules/similar.js +90 -0
- package/dist/slices/schedules/topic-list.js +163 -0
- package/dist/slices/schedules/topics.js +422 -0
- package/dist/slices/settings/first-run.js +102 -0
- package/dist/slices/settings/health.js +190 -0
- package/dist/slices/settings/key-guides.js +128 -0
- package/dist/slices/settings/key-test.js +85 -0
- package/dist/slices/settings/model.js +6 -1
- package/dist/slices/settings/playback.js +15 -0
- package/dist/slices/settings/repo.js +17 -3
- package/dist/slices/settings/voices.js +64 -4
- package/dist/slices/settings/whats-new.js +64 -0
- package/dist/slices/shorts/captions.js +9 -0
- package/dist/slices/shorts/pick.js +3 -3
- package/dist/slices/shorts/prompts.js +1 -1
- package/dist/slices/shorts/render.js +54 -5
- package/dist/slices/storage/asset-name.js +3 -0
- package/dist/slices/storage/backup-export.js +48 -9
- package/dist/slices/storage/backup-format.js +38 -1
- package/dist/slices/storage/backup-import.js +191 -19
- package/dist/slices/storage/delete-project.js +7 -4
- package/dist/slices/storage/documents.js +79 -0
- package/dist/slices/storage/files-location.js +531 -0
- package/dist/slices/storage/layout.js +14 -4
- package/dist/slices/storage/model.js +11 -0
- package/dist/slices/storage/portable.js +92 -12
- package/dist/slices/storage/reconcile.js +14 -1
- package/dist/slices/storage/repo.js +4 -1
- package/dist/slices/storage/schema.js +11 -0
- package/dist/slices/storage/trim.js +146 -0
- package/dist/slices/studio/disclosure.js +74 -0
- package/dist/slices/studio/model.js +28 -0
- package/dist/slices/studio/pack.js +209 -0
- package/dist/slices/studio/settings.js +133 -0
- package/dist/slices/style-preview/images.js +61 -0
- package/dist/slices/style-preview/render.js +161 -0
- package/dist/slices/style-preview/schema.js +45 -0
- package/dist/slices/style-preview/service.js +93 -0
- package/dist/slices/style-preview/settings.js +89 -0
- package/dist/slices/subtitles/captions.js +42 -9
- package/dist/slices/subtitles/model.js +22 -0
- package/dist/slices/subtitles/prepare.js +27 -16
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +7 -7
- package/dist/slices/trash/model.js +25 -0
- package/dist/slices/trash/service.js +273 -0
- package/dist/slices/uploads/repo.js +20 -0
- package/dist/slices/video/ambient-bed-schema.js +15 -0
- package/dist/slices/video/ambient-bed.js +95 -0
- package/dist/slices/video/ambient-mix.js +72 -0
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/cards.js +5 -2
- package/dist/slices/video/edit-list.js +23 -2
- package/dist/slices/video/edit-settings.js +13 -3
- package/dist/slices/video/ffmpeg.js +63 -9
- package/dist/slices/video/figure-card.js +492 -0
- package/dist/slices/video/figure-spans.js +51 -0
- package/dist/slices/video/plan.js +125 -13
- package/dist/slices/video/run.js +14 -12
- package/dist/slices/video/slideshow.js +39 -4
- package/dist/slices/voices/attribution.js +69 -0
- package/dist/slices/voices/audio-files.js +82 -0
- package/dist/slices/voices/audition.js +25 -0
- package/dist/slices/voices/cast.js +56 -0
- package/dist/slices/voices/grouping.js +49 -0
- package/dist/slices/voices/join.js +35 -0
- package/dist/slices/voices/languages.js +25 -0
- package/dist/slices/voices/model.js +179 -0
- package/dist/slices/voices/palette.js +26 -0
- package/dist/slices/voices/panel.js +91 -0
- package/dist/slices/voices/portraits.js +55 -0
- package/dist/slices/voices/script.js +136 -0
- package/dist/slices/voices/timing.js +58 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/chapters.js +126 -0
- package/dist/slices/youtube/edits-repo.js +69 -0
- package/dist/slices/youtube/edits.js +89 -0
- package/dist/slices/youtube/placeholders.js +98 -0
- package/dist/slices/youtube/transcript.js +11 -4
- package/dist/updater/service.js +66 -21
- package/dist/web/assets/index-CUA2sUxU.css +1 -0
- package/dist/web/assets/index-CX590kE-.js +303 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/{pdf-BLF2i8c2.js → pdf-BG8n_uc5.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +4 -4
- package/dist/edge/docker-launch.js +0 -81
- package/dist/edge/docker-projects/activation.js +0 -35
- package/dist/edge/docker-projects/claims.js +0 -85
- package/dist/edge/docker-projects/committed.js +0 -40
- package/dist/edge/docker-projects/engine.js +0 -461
- package/dist/edge/docker-projects/install.js +0 -254
- package/dist/edge/docker-projects/prune.js +0 -66
- package/dist/edge/docker-projects/recover.js +0 -100
- package/dist/edge/docker-projects/state.js +0 -225
- package/dist/web/assets/index-CktpxlJd.js +0 -171
- package/dist/web/assets/index-DB-vOnGU.css +0 -1
- package/scripts/docker-run.sh +0 -3
- /package/dist/edge/{docker-projects → docker-install}/tree.js +0 -0
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import { lstat, mkdir, readdir, rename } from "node:fs/promises";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { readVersion } from "../../kernel/version.js";
|
|
5
|
-
import { assertVolumeClaims } from "./claims.js";
|
|
6
|
-
import { readCommittedJournal } from "./committed.js";
|
|
7
|
-
import { pruneRecoveryVolumes } from "./prune.js";
|
|
8
|
-
import { assertSourceIdentity, recoverInstallation } from "./recover.js";
|
|
9
|
-
import { journalSchema, privateDirectory, readState, receiptSchema, selectProjects, writeState, } from "./state.js";
|
|
10
|
-
import { assertIdentity, assertWritableTree, identity, isMissing, privateTree, safePath, syncDirectory, treeDigest, } from "./tree.js";
|
|
11
|
-
export async function installProjects(c, e, recovery) {
|
|
12
|
-
await privateDirectory(c.directory, c.uid);
|
|
13
|
-
const receiptPath = join(c.directory, "receipt.json");
|
|
14
|
-
const journalPath = join(c.directory, "journal.json");
|
|
15
|
-
let receipt = await readState(receiptPath, receiptSchema, c.uid);
|
|
16
|
-
let previousJournal = await readState(journalPath, journalSchema, c.uid);
|
|
17
|
-
const context = await e.context(c.uid, c.gid);
|
|
18
|
-
const currentVolume = await e.volume(c.volume);
|
|
19
|
-
if ((receipt && receipt.volumeIdentity !== currentVolume) ||
|
|
20
|
-
(previousJournal?.volumeIdentity && previousJournal.volumeIdentity !== currentVolume))
|
|
21
|
-
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.`);
|
|
22
|
-
if ((receipt && receipt.daemon !== context.daemon) ||
|
|
23
|
-
(previousJournal && previousJournal.daemon !== context.daemon))
|
|
24
|
-
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.");
|
|
25
|
-
await assertVolumeClaims(c, e, context.daemon, currentVolume, await e.inspect(c.name));
|
|
26
|
-
if (previousJournal && previousJournal.phase !== "rolled-back")
|
|
27
|
-
previousJournal = await recoverInstallation(c, previousJournal, receipt, recovery());
|
|
28
|
-
if (previousJournal?.phase === "rolled-back")
|
|
29
|
-
await assertSourceIdentity(previousJournal);
|
|
30
|
-
receipt = await readState(receiptPath, receiptSchema, c.uid);
|
|
31
|
-
const old = await e.inspect(c.name);
|
|
32
|
-
if (!receipt &&
|
|
33
|
-
previousJournal?.sourceBind &&
|
|
34
|
-
(old?.id !== previousJournal.previous?.id ||
|
|
35
|
-
old?.mounts.find((m) => m.destination === "/data/projects")?.source !==
|
|
36
|
-
previousJournal.sourceBind))
|
|
37
|
-
throw new Error(`Original bound container is missing or changed. Restore the recorded original container using ${previousJournal.sourceBind}; the hidden volume projects are not authoritative.`);
|
|
38
|
-
for (const name of await readdir(c.root)) {
|
|
39
|
-
if (name === c.name || name === "setup.lock")
|
|
40
|
-
continue;
|
|
41
|
-
const s = await lstat(join(c.root, name));
|
|
42
|
-
if (!s.isDirectory() || s.isSymbolicLink())
|
|
43
|
-
continue;
|
|
44
|
-
const other = await readState(join(c.root, name, "receipt.json"), receiptSchema, c.uid);
|
|
45
|
-
if (other?.daemon === context.daemon && other.volume === c.volume)
|
|
46
|
-
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.`);
|
|
47
|
-
}
|
|
48
|
-
let reuse = null;
|
|
49
|
-
if (previousJournal?.phase === "rolled-back" &&
|
|
50
|
-
previousJournal.sourceBind !== previousJournal.destination &&
|
|
51
|
-
previousJournal.publishedIdentity &&
|
|
52
|
-
previousJournal.sourceDigest &&
|
|
53
|
-
(c.projectsOverride === null || c.projectsOverride === previousJournal.destination)) {
|
|
54
|
-
await assertIdentity(previousJournal.destination, previousJournal.publishedIdentity);
|
|
55
|
-
if ((await treeDigest(previousJournal.destination)).hash !== previousJournal.sourceDigest.hash)
|
|
56
|
-
throw new Error(`Published retry folder changed; preserve it for inspection: ${previousJournal.destination}`);
|
|
57
|
-
reuse = previousJournal;
|
|
58
|
-
}
|
|
59
|
-
const base = await selectProjects(c, receipt, old, reuse);
|
|
60
|
-
const projects = reuse?.destination ?? base;
|
|
61
|
-
await safePath(projects, c.home, c.root, true);
|
|
62
|
-
const bind = receipt?.projects ??
|
|
63
|
-
old?.mounts.find((m) => m.destination === "/data/projects")?.source ??
|
|
64
|
-
null;
|
|
65
|
-
if (bind !== null)
|
|
66
|
-
await assertWritableTree(bind, c.uid);
|
|
67
|
-
const image = await e.image(c.image);
|
|
68
|
-
const expectedVersion = await e.version(image);
|
|
69
|
-
if (expectedVersion !== readVersion())
|
|
70
|
-
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.`);
|
|
71
|
-
const signature = createHash("sha256")
|
|
72
|
-
.update(JSON.stringify([image, c.volume, c.port, c.bridge, projects, context.user]))
|
|
73
|
-
.digest("hex");
|
|
74
|
-
await e.writers(c.volume, [projects, ...(bind ? [bind] : [])], old ? [old.id] : []);
|
|
75
|
-
await e.probe(c, image, context.user, projects);
|
|
76
|
-
if (receipt &&
|
|
77
|
-
old &&
|
|
78
|
-
old.signature === signature &&
|
|
79
|
-
old.image === image &&
|
|
80
|
-
old.user === context.user &&
|
|
81
|
-
old.mounts.find((m) => m.destination === "/data/projects")?.source === projects &&
|
|
82
|
-
old.mounts.find((m) => m.destination === "/data")?.name === c.volume &&
|
|
83
|
-
old.mounts.find((m) => m.destination === "/data")?.rw === true &&
|
|
84
|
-
old.mounts.find((m) => m.destination === "/opt/slopify-install")?.source ===
|
|
85
|
-
join(c.directory, receipt.transaction) &&
|
|
86
|
-
old.mounts.find((m) => m.destination === "/opt/slopify-install")?.rw === false &&
|
|
87
|
-
old.restart.Name === "always" &&
|
|
88
|
-
old.port?.startsWith("127.0.0.1:") &&
|
|
89
|
-
(c.port === 0 || old.port === `127.0.0.1:${c.port}`) &&
|
|
90
|
-
(old.mounts.find((m) => m.destination === "/opt/slopify-host")?.source ?? null) === c.bridge) {
|
|
91
|
-
const j = await readCommittedJournal(c, receipt, old);
|
|
92
|
-
if (!old.running)
|
|
93
|
-
await e.command(["start", old.id]);
|
|
94
|
-
await e.health(old.id, j.token, expectedVersion);
|
|
95
|
-
return {
|
|
96
|
-
url: `http://${old.port}`,
|
|
97
|
-
projects,
|
|
98
|
-
recovery: j.backup,
|
|
99
|
-
pruned: { removed: [], problems: [] },
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
const id = randomUUID();
|
|
103
|
-
const directory = join(c.directory, id);
|
|
104
|
-
await privateDirectory(directory, c.uid);
|
|
105
|
-
const destinationBefore = await identity(projects).catch((error) => {
|
|
106
|
-
if (isMissing(error))
|
|
107
|
-
return null;
|
|
108
|
-
throw error;
|
|
109
|
-
});
|
|
110
|
-
let j = {
|
|
111
|
-
version: 1,
|
|
112
|
-
id,
|
|
113
|
-
installation: receipt?.installation ?? randomUUID(),
|
|
114
|
-
daemon: context.daemon,
|
|
115
|
-
name: c.name,
|
|
116
|
-
volume: c.volume,
|
|
117
|
-
volumeIdentity: currentVolume,
|
|
118
|
-
image,
|
|
119
|
-
user: context.user,
|
|
120
|
-
signature,
|
|
121
|
-
phase: "prepared",
|
|
122
|
-
previous: old,
|
|
123
|
-
previousReceipt: receipt,
|
|
124
|
-
sourceBind: bind,
|
|
125
|
-
sourceIdentity: bind === null ? null : (receipt?.directoryIdentity ?? (await identity(bind))),
|
|
126
|
-
destination: projects,
|
|
127
|
-
destinationBefore,
|
|
128
|
-
staging: join(dirname(projects), `.slopify-projects-${id}`),
|
|
129
|
-
stagingIdentity: null,
|
|
130
|
-
publishedIdentity: reuse?.publishedIdentity ?? null,
|
|
131
|
-
sourceDigest: reuse?.sourceDigest ?? null,
|
|
132
|
-
sourceAbsent: reuse?.sourceAbsent ?? false,
|
|
133
|
-
backup: `${c.volume}-recovery-${id}`,
|
|
134
|
-
backupDigest: null,
|
|
135
|
-
candidate: null,
|
|
136
|
-
token: randomBytes(32).toString("hex"),
|
|
137
|
-
};
|
|
138
|
-
async function save(change) {
|
|
139
|
-
j = journalSchema.parse({ ...j, ...change });
|
|
140
|
-
await writeState(join(directory, "journal.json"), j);
|
|
141
|
-
await writeState(journalPath, j);
|
|
142
|
-
}
|
|
143
|
-
await save({});
|
|
144
|
-
await writeState(join(directory, "activation.json"), {
|
|
145
|
-
version: 1,
|
|
146
|
-
token: j.token,
|
|
147
|
-
committed: false,
|
|
148
|
-
});
|
|
149
|
-
try {
|
|
150
|
-
await save({ phase: "stopping" });
|
|
151
|
-
if (old)
|
|
152
|
-
await e.stop(old);
|
|
153
|
-
await e.writers(c.volume, [projects, ...(bind ? [bind] : [])], []);
|
|
154
|
-
await assertSourceIdentity(j);
|
|
155
|
-
await save({ phase: "stopped" });
|
|
156
|
-
await e.ensureVolume(c.volume);
|
|
157
|
-
await save({ volumeIdentity: await e.volume(c.volume) });
|
|
158
|
-
const source = await e.projects(image, c.volume, bind);
|
|
159
|
-
if (reuse && (reuse.sourceAbsent ? source !== null : source?.hash !== reuse.sourceDigest?.hash))
|
|
160
|
-
throw new Error(`Stale migration baseline. Retain ${projects} and ${reuse.staging}; select a new empty --projects-dir for a fresh verified copy.`);
|
|
161
|
-
await save({
|
|
162
|
-
sourceDigest: source ?? reuse?.sourceDigest ?? null,
|
|
163
|
-
sourceAbsent: source === null,
|
|
164
|
-
});
|
|
165
|
-
const backupDigest = await e.snapshot(j);
|
|
166
|
-
await save({ phase: "snapshot", backupDigest });
|
|
167
|
-
if (projects !== bind && reuse === null) {
|
|
168
|
-
await mkdir(j.staging, { mode: 0o700 });
|
|
169
|
-
await save({ phase: "copying", stagingIdentity: await identity(j.staging) });
|
|
170
|
-
if (source !== null)
|
|
171
|
-
await e.copy(image, c.volume, bind, j.staging, j.id);
|
|
172
|
-
await privateTree(j.staging, c.uid, c.gid);
|
|
173
|
-
const copied = await treeDigest(j.staging);
|
|
174
|
-
const after = await e.projects(image, c.volume, bind);
|
|
175
|
-
if ((source === null && copied.files !== 0) ||
|
|
176
|
-
(source !== null && (copied.hash !== source.hash || after?.hash !== source.hash)))
|
|
177
|
-
throw new Error("Project copy verification failed; source and staging are retained.");
|
|
178
|
-
await save({ phase: "verified", sourceDigest: source ?? copied });
|
|
179
|
-
await safePath(projects, c.home, c.root, false);
|
|
180
|
-
if (j.destinationBefore) {
|
|
181
|
-
await assertIdentity(projects, j.destinationBefore);
|
|
182
|
-
if ((await readdir(projects)).length !== 0)
|
|
183
|
-
throw new Error("Destination became populated before publication.");
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
const exists = await lstat(projects).catch((error) => {
|
|
187
|
-
if (isMissing(error))
|
|
188
|
-
return null;
|
|
189
|
-
throw error;
|
|
190
|
-
});
|
|
191
|
-
if (exists !== null)
|
|
192
|
-
throw new Error("Destination appeared before publication.");
|
|
193
|
-
}
|
|
194
|
-
if (!j.stagingIdentity)
|
|
195
|
-
throw new Error("Missing staging identity.");
|
|
196
|
-
await assertIdentity(j.staging, j.stagingIdentity);
|
|
197
|
-
await rename(j.staging, projects);
|
|
198
|
-
await syncDirectory(dirname(projects));
|
|
199
|
-
}
|
|
200
|
-
await save({ phase: "published", publishedIdentity: await identity(projects) });
|
|
201
|
-
await assertSourceIdentity(j);
|
|
202
|
-
await e.writers(c.volume, [projects, ...(bind ? [bind] : [])], []);
|
|
203
|
-
await e.own(j);
|
|
204
|
-
if (old)
|
|
205
|
-
await e.command(["rename", old.id, `${c.name}-previous-${j.id}`]);
|
|
206
|
-
await save({ phase: "starting" });
|
|
207
|
-
const candidate = await e.start(c, j, directory);
|
|
208
|
-
await save({ candidate });
|
|
209
|
-
await e.health(candidate, j.token, expectedVersion);
|
|
210
|
-
await save({ phase: "healthy" });
|
|
211
|
-
if (!j.publishedIdentity || !j.volumeIdentity)
|
|
212
|
-
throw new Error("Missing published storage identity.");
|
|
213
|
-
const next = {
|
|
214
|
-
version: 1,
|
|
215
|
-
installation: j.installation,
|
|
216
|
-
daemon: j.daemon,
|
|
217
|
-
name: c.name,
|
|
218
|
-
volume: c.volume,
|
|
219
|
-
volumeIdentity: j.volumeIdentity,
|
|
220
|
-
projects,
|
|
221
|
-
directoryIdentity: j.publishedIdentity,
|
|
222
|
-
user: j.user,
|
|
223
|
-
image,
|
|
224
|
-
signature,
|
|
225
|
-
transaction: j.id,
|
|
226
|
-
};
|
|
227
|
-
await writeState(receiptPath, next);
|
|
228
|
-
await e.command(["update", "--restart=always", candidate]);
|
|
229
|
-
await writeState(join(directory, "activation.json"), {
|
|
230
|
-
version: 1,
|
|
231
|
-
token: j.token,
|
|
232
|
-
committed: true,
|
|
233
|
-
});
|
|
234
|
-
await save({ phase: "committed" });
|
|
235
|
-
const ready = await e.inspect(candidate);
|
|
236
|
-
if (!ready?.port)
|
|
237
|
-
throw new Error("Slopify started but Docker did not report which port it is on. Run the same command again to finish.");
|
|
238
|
-
// Only now, with the new container committed and healthy, are older snapshots redundant.
|
|
239
|
-
const pruned = await pruneRecoveryVolumes(c, e, j);
|
|
240
|
-
return { url: `http://${ready.port}`, projects, recovery: j.backup, pruned };
|
|
241
|
-
}
|
|
242
|
-
catch (cause) {
|
|
243
|
-
const current = await readState(receiptPath, receiptSchema, c.uid);
|
|
244
|
-
if (current?.transaction === j.id)
|
|
245
|
-
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 });
|
|
246
|
-
try {
|
|
247
|
-
await recoverInstallation(c, j, current, recovery());
|
|
248
|
-
}
|
|
249
|
-
catch (failure) {
|
|
250
|
-
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 });
|
|
251
|
-
}
|
|
252
|
-
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 });
|
|
253
|
-
}
|
|
254
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { recoveryId } from "./engine.js";
|
|
3
|
-
import { journalSchema, readState } from "./state.js";
|
|
4
|
-
// Phases an update's record is left in once it is over, one way or the other. Anything else
|
|
5
|
-
// is an update that never finished, and its volume may be the only copy of the data.
|
|
6
|
-
const settled = new Set(["healthy", "committed", "restored", "rolled-back"]);
|
|
7
|
-
// After an update is committed and healthy, keep only its own recovery volume. An older one is
|
|
8
|
-
// removed only when all of these hold, so a volume some other installation, container or person
|
|
9
|
-
// made is never touched:
|
|
10
|
-
// - its name is exactly `<this data volume>-recovery-<uuid>`, never the data volume itself;
|
|
11
|
-
// - its io.slopify.transaction label is that same uuid (every launcher has set it);
|
|
12
|
-
// - the container label, which this launcher adds, names this container (earlier launchers
|
|
13
|
-
// set none, and for those the next check decides alone);
|
|
14
|
-
// - the update's own record, <install dir>/<uuid>/journal.json in this container's private
|
|
15
|
-
// folder, exists and names this container, data volume and Docker, the installation the
|
|
16
|
-
// volume's label names, and exactly this volume as its backup, and says that update is
|
|
17
|
-
// over. A first install that was rolled back has an installation id of its own, so the
|
|
18
|
-
// id is matched against the volume's label rather than the current update's.
|
|
19
|
-
// Docker refuses to remove a volume a container still uses; that refusal is reported, not
|
|
20
|
-
// forced. Nothing here can fail the update, which has already finished.
|
|
21
|
-
export async function pruneRecoveryVolumes(c, e, j) {
|
|
22
|
-
const removed = [];
|
|
23
|
-
const problems = [];
|
|
24
|
-
let volumes;
|
|
25
|
-
try {
|
|
26
|
-
volumes = await e.recoveryVolumes(c.volume);
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
return {
|
|
30
|
-
removed,
|
|
31
|
-
problems: [
|
|
32
|
-
`Couldn't list older recovery volumes to tidy up, so all of them are kept. See them with docker volume ls --filter label=io.slopify.transaction and remove the ones you no longer need with docker volume rm <name>. ${reason(error)}`,
|
|
33
|
-
],
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
for (const volume of volumes) {
|
|
37
|
-
const id = recoveryId(c.volume, volume.name);
|
|
38
|
-
if (id === null || id === j.id || volume.name === j.backup || volume.name === c.volume)
|
|
39
|
-
continue;
|
|
40
|
-
if (volume.transaction !== id)
|
|
41
|
-
continue;
|
|
42
|
-
if (volume.container !== null && volume.container !== c.name)
|
|
43
|
-
continue;
|
|
44
|
-
const record = await readState(join(c.directory, id, "journal.json"), journalSchema, c.uid).catch(() => null);
|
|
45
|
-
if (record === null ||
|
|
46
|
-
record.id !== id ||
|
|
47
|
-
record.name !== c.name ||
|
|
48
|
-
record.volume !== c.volume ||
|
|
49
|
-
record.daemon !== j.daemon ||
|
|
50
|
-
(volume.installation !== null && volume.installation !== record.installation) ||
|
|
51
|
-
record.backup !== volume.name ||
|
|
52
|
-
!settled.has(record.phase))
|
|
53
|
-
continue;
|
|
54
|
-
try {
|
|
55
|
-
await e.removeVolume(volume.name);
|
|
56
|
-
removed.push(volume.name);
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
problems.push(`Couldn't remove the older recovery volume ${volume.name}, so it is kept. Once nothing uses it, remove it with docker volume rm ${volume.name}. ${reason(error)}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return { removed, problems };
|
|
63
|
-
}
|
|
64
|
-
function reason(error) {
|
|
65
|
-
return error instanceof Error ? `Why: ${error.message}` : "";
|
|
66
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { readCommittedJournal } from "./committed.js";
|
|
3
|
-
import { selectProjects, writeState, } from "./state.js";
|
|
4
|
-
import { assertIdentity, assertWritableTree, identity, isMissing, treeDigest } from "./tree.js";
|
|
5
|
-
export async function assertSourceIdentity(j) {
|
|
6
|
-
if (j.sourceBind === null)
|
|
7
|
-
return;
|
|
8
|
-
if (!j.sourceIdentity)
|
|
9
|
-
throw new Error("Original project bind identity is missing.");
|
|
10
|
-
await assertIdentity(j.sourceBind, j.sourceIdentity);
|
|
11
|
-
}
|
|
12
|
-
export async function recoverInstallation(c, j, receipt, e) {
|
|
13
|
-
const directory = join(c.directory, j.id);
|
|
14
|
-
if (j.name !== c.name || j.volume !== c.volume)
|
|
15
|
-
throw new Error("Journal installation conflict.");
|
|
16
|
-
if (receipt?.transaction === j.id) {
|
|
17
|
-
const current = await e.inspect(c.name);
|
|
18
|
-
await selectProjects({ ...c, projectsOverride: null }, receipt, current);
|
|
19
|
-
await assertWritableTree(receipt.projects, c.uid);
|
|
20
|
-
j = await readCommittedJournal(c, receipt, current);
|
|
21
|
-
await e.writers(c.volume, [receipt.projects], current ? [current.id] : []);
|
|
22
|
-
await writeState(join(directory, "activation.json"), {
|
|
23
|
-
version: 1,
|
|
24
|
-
token: j.token,
|
|
25
|
-
committed: true,
|
|
26
|
-
});
|
|
27
|
-
if (current && current.installation === j.installation && current.signature === j.signature) {
|
|
28
|
-
await e.command(["update", "--restart=always", current.id]);
|
|
29
|
-
if (!current.running)
|
|
30
|
-
await e.command(["start", current.id]);
|
|
31
|
-
}
|
|
32
|
-
const finished = { ...j, phase: "committed" };
|
|
33
|
-
await writeState(join(c.directory, "journal.json"), finished);
|
|
34
|
-
return finished;
|
|
35
|
-
}
|
|
36
|
-
if (j.phase === "rolled-back")
|
|
37
|
-
return j;
|
|
38
|
-
await assertSourceIdentity(j);
|
|
39
|
-
const occupant = await e.inspect(c.name);
|
|
40
|
-
if (j.phase === "verified" && j.stagingIdentity && j.sourceDigest) {
|
|
41
|
-
const found = await identity(j.destination).catch((error) => {
|
|
42
|
-
if (isMissing(error))
|
|
43
|
-
return null;
|
|
44
|
-
throw error;
|
|
45
|
-
});
|
|
46
|
-
if (found?.dev === j.stagingIdentity.dev &&
|
|
47
|
-
found.ino === j.stagingIdentity.ino &&
|
|
48
|
-
(await treeDigest(j.destination)).hash === j.sourceDigest.hash) {
|
|
49
|
-
j = { ...j, publishedIdentity: found };
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const possible = j.candidate ? await e.inspect(j.candidate) : occupant;
|
|
53
|
-
if (occupant && occupant.id !== j.previous?.id && occupant.id !== possible?.id)
|
|
54
|
-
throw new Error("Recovery name is occupied by an unrelated container.");
|
|
55
|
-
if (possible && possible.id !== j.previous?.id) {
|
|
56
|
-
const transaction = await e.command([
|
|
57
|
-
"inspect",
|
|
58
|
-
"--format",
|
|
59
|
-
'{{index .Config.Labels "io.slopify.transaction"}}',
|
|
60
|
-
possible.id,
|
|
61
|
-
]);
|
|
62
|
-
if (transaction !== j.id ||
|
|
63
|
-
possible.installation !== j.installation ||
|
|
64
|
-
possible.signature !== j.signature)
|
|
65
|
-
throw new Error("Recovery name is occupied by an unrelated container.");
|
|
66
|
-
if (j.candidate !== possible.id) {
|
|
67
|
-
j = { ...j, candidate: possible.id };
|
|
68
|
-
await writeState(join(c.directory, "journal.json"), j);
|
|
69
|
-
await writeState(join(directory, "journal.json"), j);
|
|
70
|
-
}
|
|
71
|
-
await e.stop(possible);
|
|
72
|
-
const failedName = `${c.name}-failed-${j.id}`;
|
|
73
|
-
if (possible.name !== failedName)
|
|
74
|
-
await e.command(["rename", possible.id, failedName]);
|
|
75
|
-
}
|
|
76
|
-
if (j.phase !== "restored") {
|
|
77
|
-
if (j.backupDigest) {
|
|
78
|
-
await e.writers(c.volume, [j.destination, ...(j.sourceBind ? [j.sourceBind] : [])], []);
|
|
79
|
-
await e.restore(j);
|
|
80
|
-
}
|
|
81
|
-
j = { ...j, phase: "restored" };
|
|
82
|
-
await writeState(join(c.directory, "journal.json"), j);
|
|
83
|
-
await writeState(join(directory, "journal.json"), j);
|
|
84
|
-
}
|
|
85
|
-
if (j.previous) {
|
|
86
|
-
const old = await e.inspect(j.previous.id);
|
|
87
|
-
if (!old || old.mounts.find((m) => m.destination === "/data")?.name !== c.volume)
|
|
88
|
-
throw new Error("Original container/storage is missing; automatic rollback is unsafe.");
|
|
89
|
-
if (j.previousReceipt)
|
|
90
|
-
await selectProjects({ ...c, projectsOverride: null }, j.previousReceipt, old);
|
|
91
|
-
if (j.sourceBind !== null)
|
|
92
|
-
await assertWritableTree(j.sourceBind, c.uid);
|
|
93
|
-
await e.writers(c.volume, [j.destination, ...(j.sourceBind ? [j.sourceBind] : [])], [old.id]);
|
|
94
|
-
await e.restart(j.previous);
|
|
95
|
-
}
|
|
96
|
-
const rolledBack = { ...j, phase: "rolled-back" };
|
|
97
|
-
await writeState(join(c.directory, "journal.json"), rolledBack);
|
|
98
|
-
await writeState(join(directory, "journal.json"), rolledBack);
|
|
99
|
-
return rolledBack;
|
|
100
|
-
}
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { lstat, mkdir, readdir } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { privateRead, privateWrite } from "../../host-cli/service.js";
|
|
5
|
-
import { assertIdentity, contains, isMissing, safePath, syncDirectory, treeDigest, } from "./tree.js";
|
|
6
|
-
const identifier = z
|
|
7
|
-
.string()
|
|
8
|
-
.min(1)
|
|
9
|
-
.max(128)
|
|
10
|
-
.regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/);
|
|
11
|
-
const absolute = z
|
|
12
|
-
.string()
|
|
13
|
-
.min(1)
|
|
14
|
-
.max(4096)
|
|
15
|
-
.refine((p) => isAbsolute(p) && resolve(p) === p && !/[\p{Cc},]/u.test(p));
|
|
16
|
-
export const identitySchema = z.object({ dev: z.string(), ino: z.string() }).strict();
|
|
17
|
-
export const digestSchema = z
|
|
18
|
-
.object({
|
|
19
|
-
hash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
20
|
-
files: z.number().int().nonnegative(),
|
|
21
|
-
bytes: z.number().int().nonnegative(),
|
|
22
|
-
})
|
|
23
|
-
.strict();
|
|
24
|
-
export const containerSchema = z
|
|
25
|
-
.object({
|
|
26
|
-
id: z.string(),
|
|
27
|
-
name: z.string(),
|
|
28
|
-
image: z.string(),
|
|
29
|
-
user: z.string(),
|
|
30
|
-
running: z.boolean(),
|
|
31
|
-
restart: z.object({ Name: z.string(), MaximumRetryCount: z.number().int() }),
|
|
32
|
-
signature: z.string().nullable(),
|
|
33
|
-
installation: z.string().nullable(),
|
|
34
|
-
mounts: z.array(z.object({
|
|
35
|
-
type: z.string(),
|
|
36
|
-
name: z.string(),
|
|
37
|
-
source: z.string(),
|
|
38
|
-
destination: z.string(),
|
|
39
|
-
rw: z.boolean(),
|
|
40
|
-
})),
|
|
41
|
-
port: z.string().nullable(),
|
|
42
|
-
})
|
|
43
|
-
.strict();
|
|
44
|
-
export const receiptSchema = z
|
|
45
|
-
.object({
|
|
46
|
-
version: z.literal(1),
|
|
47
|
-
installation: z.string().uuid(),
|
|
48
|
-
daemon: z.string(),
|
|
49
|
-
name: identifier,
|
|
50
|
-
volume: identifier,
|
|
51
|
-
volumeIdentity: z.string(),
|
|
52
|
-
projects: absolute,
|
|
53
|
-
directoryIdentity: identitySchema,
|
|
54
|
-
user: z.string(),
|
|
55
|
-
image: z.string(),
|
|
56
|
-
signature: z.string(),
|
|
57
|
-
transaction: z.string().uuid(),
|
|
58
|
-
})
|
|
59
|
-
.strict();
|
|
60
|
-
export const phases = [
|
|
61
|
-
"prepared",
|
|
62
|
-
"stopping",
|
|
63
|
-
"stopped",
|
|
64
|
-
"snapshot",
|
|
65
|
-
"copying",
|
|
66
|
-
"verified",
|
|
67
|
-
"published",
|
|
68
|
-
"starting",
|
|
69
|
-
"healthy",
|
|
70
|
-
"committed",
|
|
71
|
-
"restored",
|
|
72
|
-
"rolled-back",
|
|
73
|
-
];
|
|
74
|
-
export const journalSchema = z
|
|
75
|
-
.object({
|
|
76
|
-
version: z.literal(1),
|
|
77
|
-
id: z.string().uuid(),
|
|
78
|
-
installation: z.string().uuid(),
|
|
79
|
-
daemon: z.string(),
|
|
80
|
-
name: identifier,
|
|
81
|
-
volume: identifier,
|
|
82
|
-
volumeIdentity: z.string().nullable(),
|
|
83
|
-
image: z.string(),
|
|
84
|
-
user: z.string(),
|
|
85
|
-
signature: z.string(),
|
|
86
|
-
phase: z.enum(phases),
|
|
87
|
-
previous: containerSchema.nullable(),
|
|
88
|
-
previousReceipt: receiptSchema.nullable(),
|
|
89
|
-
sourceBind: absolute.nullable(),
|
|
90
|
-
sourceIdentity: identitySchema.nullable().default(null),
|
|
91
|
-
destination: absolute,
|
|
92
|
-
destinationBefore: identitySchema.nullable(),
|
|
93
|
-
staging: absolute,
|
|
94
|
-
stagingIdentity: identitySchema.nullable(),
|
|
95
|
-
publishedIdentity: identitySchema.nullable(),
|
|
96
|
-
sourceDigest: digestSchema.nullable(),
|
|
97
|
-
sourceAbsent: z.boolean().default(false),
|
|
98
|
-
backup: z
|
|
99
|
-
.string()
|
|
100
|
-
.max(174)
|
|
101
|
-
.regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/),
|
|
102
|
-
backupDigest: digestSchema.nullable(),
|
|
103
|
-
candidate: z.string().nullable(),
|
|
104
|
-
token: z.string().regex(/^[a-f0-9]{64}$/),
|
|
105
|
-
})
|
|
106
|
-
.strict();
|
|
107
|
-
/** Where the Docker launcher keeps one folder (and receipt) per installation. */
|
|
108
|
-
export function dockerRoot(env, home) {
|
|
109
|
-
return join(absolute.parse(env.XDG_DATA_HOME || join(home, ".local/share")), "slopify/docker");
|
|
110
|
-
}
|
|
111
|
-
export function dockerConfig(env, home, cwd, uid, gid) {
|
|
112
|
-
const name = identifier.parse(env.SLOPIFY_DOCKER_NAME || "slopify");
|
|
113
|
-
const volume = identifier.parse(env.SLOPIFY_DOCKER_VOLUME || "slopify-data");
|
|
114
|
-
const root = dockerRoot(env, home);
|
|
115
|
-
const raw = env.SLOPIFY_DOCKER_PROJECTS_DIR || null;
|
|
116
|
-
const projectsOverride = raw === null ? null : resolve(cwd, raw.startsWith("~/") ? join(home, raw.slice(2)) : raw);
|
|
117
|
-
const portText = env.SLOPIFY_DOCKER_HOST_PORT || "6969";
|
|
118
|
-
if (!/^\d{1,5}$/.test(portText) || Number(portText) > 65535)
|
|
119
|
-
throw new Error(`Invalid port ${JSON.stringify(portText)}: use a whole number between 1 and 65535, for example --port 7070.`);
|
|
120
|
-
return {
|
|
121
|
-
home,
|
|
122
|
-
uid,
|
|
123
|
-
gid,
|
|
124
|
-
root,
|
|
125
|
-
directory: join(root, name),
|
|
126
|
-
name,
|
|
127
|
-
volume,
|
|
128
|
-
image: z
|
|
129
|
-
.string()
|
|
130
|
-
.min(1)
|
|
131
|
-
.refine((s) => !/[\p{Cc}]/u.test(s))
|
|
132
|
-
.parse(env.SLOPIFY_DOCKER_IMAGE || "ghcr.io/gentbajko/slopify:latest"),
|
|
133
|
-
port: Number(portText),
|
|
134
|
-
projectsOverride,
|
|
135
|
-
bridge: env.SLOPIFY_HOST_CLI_DIR || null,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
export async function privateDirectory(path, uid) {
|
|
139
|
-
absolute.parse(path);
|
|
140
|
-
let current = sep;
|
|
141
|
-
for (const name of path.split(sep).filter(Boolean)) {
|
|
142
|
-
current = join(current, name);
|
|
143
|
-
await mkdir(current, { mode: 0o700 }).catch((error) => {
|
|
144
|
-
if (!(typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST"))
|
|
145
|
-
throw error;
|
|
146
|
-
});
|
|
147
|
-
const s = await lstat(current);
|
|
148
|
-
if (!s.isDirectory() ||
|
|
149
|
-
s.isSymbolicLink() ||
|
|
150
|
-
((s.mode & 0o022) !== 0 && (s.mode & 0o1000) === 0))
|
|
151
|
-
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.`);
|
|
152
|
-
}
|
|
153
|
-
const s = await lstat(path);
|
|
154
|
-
if (s.uid !== uid || (s.mode & 0o077) !== 0)
|
|
155
|
-
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.`);
|
|
156
|
-
}
|
|
157
|
-
export async function readState(path, schema, uid) {
|
|
158
|
-
const s = await lstat(path).catch((error) => {
|
|
159
|
-
if (isMissing(error))
|
|
160
|
-
return undefined;
|
|
161
|
-
throw error;
|
|
162
|
-
});
|
|
163
|
-
if (s === undefined)
|
|
164
|
-
return null;
|
|
165
|
-
if (!s.isFile() || s.isSymbolicLink() || s.nlink !== 1 || s.uid !== uid || (s.mode & 0o077) !== 0)
|
|
166
|
-
throw new Error("Unsafe private installation record.");
|
|
167
|
-
const text = await privateRead(path, uid);
|
|
168
|
-
if (text === undefined)
|
|
169
|
-
throw new Error("Installation record changed while reading.");
|
|
170
|
-
return schema.parse(JSON.parse(text));
|
|
171
|
-
}
|
|
172
|
-
export async function writeState(path, value) {
|
|
173
|
-
const text = JSON.stringify(value);
|
|
174
|
-
if (Buffer.byteLength(text) > 64 * 1024)
|
|
175
|
-
throw new Error("Installation control record exceeds 64 KiB.");
|
|
176
|
-
await privateWrite(path, text);
|
|
177
|
-
await syncDirectory(dirname(path));
|
|
178
|
-
}
|
|
179
|
-
export async function selectProjects(c, receipt, old, retry = null) {
|
|
180
|
-
const data = old?.mounts.find((m) => m.destination === "/data");
|
|
181
|
-
const bind = old?.mounts.find((m) => m.destination === "/data/projects");
|
|
182
|
-
if (old && (data?.type !== "volume" || data.name !== c.volume))
|
|
183
|
-
throw new Error("Existing container uses a different data volume.");
|
|
184
|
-
if (bind && (bind.type !== "bind" || !bind.rw))
|
|
185
|
-
throw new Error("Unsupported existing projects mount.");
|
|
186
|
-
if (old?.mounts.some((m) => m.destination.startsWith("/data/") && m.destination !== "/data/projects"))
|
|
187
|
-
throw new Error("Unexpected nested data mount; use the existing installation for recovery.");
|
|
188
|
-
if (receipt) {
|
|
189
|
-
if (receipt.name !== c.name || receipt.volume !== c.volume)
|
|
190
|
-
throw new Error("Receipt selects a different installation or volume.");
|
|
191
|
-
await safePath(receipt.projects, c.home, c.root, false);
|
|
192
|
-
await assertIdentity(receipt.projects, receipt.directoryIdentity).catch((cause) => {
|
|
193
|
-
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.`, {
|
|
194
|
-
cause,
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
if (old && (bind?.source !== receipt.projects || old.installation !== receipt.installation))
|
|
198
|
-
throw new Error("Actual container mounts/installation disagree with its receipt.");
|
|
199
|
-
}
|
|
200
|
-
const chosen = c.projectsOverride ??
|
|
201
|
-
receipt?.projects ??
|
|
202
|
-
bind?.source ??
|
|
203
|
-
(c.name === "slopify"
|
|
204
|
-
? join(c.home, "Slopify/Projects")
|
|
205
|
-
: join(c.home, "Slopify", c.name, "Projects"));
|
|
206
|
-
await safePath(chosen, c.home, c.root, true);
|
|
207
|
-
const source = receipt?.projects ?? bind?.source;
|
|
208
|
-
if (source && source !== chosen && (contains(source, chosen) || contains(chosen, source)))
|
|
209
|
-
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>.`);
|
|
210
|
-
if (chosen !== source) {
|
|
211
|
-
const entries = await readdir(chosen).catch((error) => {
|
|
212
|
-
if (isMissing(error))
|
|
213
|
-
return [];
|
|
214
|
-
throw error;
|
|
215
|
-
});
|
|
216
|
-
if (entries.length > 0) {
|
|
217
|
-
if (retry?.destination !== chosen || !retry.publishedIdentity || !retry.sourceDigest)
|
|
218
|
-
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.`);
|
|
219
|
-
await assertIdentity(chosen, retry.publishedIdentity);
|
|
220
|
-
if ((await treeDigest(chosen)).hash !== retry.sourceDigest.hash)
|
|
221
|
-
throw new Error(`Published retry destination changed: ${chosen}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
return chosen;
|
|
225
|
-
}
|