@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
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5
|
+
/** The id a recovery volume of `volume` carries in its name, or null for any other volume. */
|
|
6
|
+
export function recoveryId(volume, name) {
|
|
7
|
+
const prefix = `${volume}-recovery-`;
|
|
8
|
+
if (!name.startsWith(prefix))
|
|
9
|
+
return null;
|
|
10
|
+
const id = name.slice(prefix.length);
|
|
11
|
+
return uuid.test(id) ? id : null;
|
|
12
|
+
}
|
|
13
|
+
// Docker's exit code alone says nothing, so its last error line decides the advice: the daemon
|
|
14
|
+
// being stopped and the socket being off-limits are by far the most common first-run failures.
|
|
15
|
+
export function dockerFailure(args, result) {
|
|
16
|
+
const detail = (result.stderr ?? "").trim();
|
|
17
|
+
if (/permission denied[^\n]*docker\.sock|docker\.sock[^\n]*permission denied/i.test(detail))
|
|
18
|
+
return "Your user isn't allowed to use Docker. Give it access with sudo usermod -aG docker $USER, then log out and back in (or run newgrp docker) and try again.";
|
|
19
|
+
if (/cannot connect to the docker daemon|is the docker daemon running|error during connect/i.test(detail))
|
|
20
|
+
return "Docker is installed but not running. Start it (sudo systemctl start docker, or systemctl --user start docker for rootless Docker) and try again.";
|
|
21
|
+
if (/port is already allocated|address already in use/i.test(detail))
|
|
22
|
+
return "The port Slopify wants is already in use by another program. Stop that program, or install on another port: npx @gentbajko/slopify --docker --port 7070";
|
|
23
|
+
if (/no space left on device/i.test(detail))
|
|
24
|
+
return "The disk Docker uses is full. Free up space (for example docker system prune) and try again.";
|
|
25
|
+
if (args[0] === "compose" && /is not a docker command|unknown command/i.test(detail))
|
|
26
|
+
return "Docker Compose isn't installed. Install the Docker Compose plugin (for example sudo apt install docker-compose-plugin, or sudo pacman -S docker-compose), check that docker compose version works, and try again.";
|
|
27
|
+
const last = detail.split("\n").at(-1)?.trim().slice(0, 300);
|
|
28
|
+
return `Docker command "docker ${args[0] ?? ""}" failed (exit code ${result.code}${last ? `: ${last}` : ""}). Check that Docker works (docker info) and run the command again.`;
|
|
29
|
+
}
|
|
30
|
+
const helperEntry = "/opt/slopify/packages/app/dist/edge/docker-install/volume.js";
|
|
31
|
+
const inspectSchema = z.array(z.object({
|
|
32
|
+
Id: z.string(),
|
|
33
|
+
Name: z.string(),
|
|
34
|
+
Config: z.object({
|
|
35
|
+
Image: z.string(),
|
|
36
|
+
User: z.string(),
|
|
37
|
+
Labels: z.record(z.string(), z.string()).nullable(),
|
|
38
|
+
}),
|
|
39
|
+
State: z.object({ Running: z.boolean() }),
|
|
40
|
+
HostConfig: z.object({
|
|
41
|
+
RestartPolicy: z.object({ Name: z.string(), MaximumRetryCount: z.number() }),
|
|
42
|
+
}),
|
|
43
|
+
Mounts: z.array(z.object({
|
|
44
|
+
Type: z.string(),
|
|
45
|
+
Name: z.string().optional(),
|
|
46
|
+
Source: z.string(),
|
|
47
|
+
Destination: z.string(),
|
|
48
|
+
})),
|
|
49
|
+
NetworkSettings: z.object({
|
|
50
|
+
Ports: z
|
|
51
|
+
.record(z.string(), z.array(z.object({ HostIp: z.string(), HostPort: z.string() })).nullable())
|
|
52
|
+
.nullable(),
|
|
53
|
+
}),
|
|
54
|
+
}));
|
|
55
|
+
export function dockerEngine(runner, signal, env) {
|
|
56
|
+
const run = (args, localSignal = signal) => runner.exec("docker", args, localSignal);
|
|
57
|
+
async function command(args, localSignal = signal) {
|
|
58
|
+
const r = await run(args, localSignal);
|
|
59
|
+
if (r.code !== 0)
|
|
60
|
+
throw new Error(dockerFailure(args, r));
|
|
61
|
+
return r.stdout.trim();
|
|
62
|
+
}
|
|
63
|
+
const mount = (type, source, target, readonly = false) => [
|
|
64
|
+
"--mount",
|
|
65
|
+
`type=${type},source=${source},target=${target}${readonly ? ",readonly" : ""}`,
|
|
66
|
+
];
|
|
67
|
+
// Fixed file operations inside the image being installed, as root, without network.
|
|
68
|
+
const helper = async (image, args, operation, user = "0:0") => JSON.parse(await command([
|
|
69
|
+
"run",
|
|
70
|
+
"--rm",
|
|
71
|
+
"--network",
|
|
72
|
+
"none",
|
|
73
|
+
"--read-only",
|
|
74
|
+
"--user",
|
|
75
|
+
"0:0",
|
|
76
|
+
...(args.some((a) => a.includes("target=/data")) ? [] : ["--tmpfs", "/data"]),
|
|
77
|
+
...args,
|
|
78
|
+
"--entrypoint",
|
|
79
|
+
"node",
|
|
80
|
+
image,
|
|
81
|
+
helperEntry,
|
|
82
|
+
operation,
|
|
83
|
+
user,
|
|
84
|
+
]));
|
|
85
|
+
async function inspect(name) {
|
|
86
|
+
const result = await run(["container", "inspect", name]);
|
|
87
|
+
if (result.code !== 0) {
|
|
88
|
+
if (/no such (container|object)/i.test(result.stderr ?? ""))
|
|
89
|
+
return null;
|
|
90
|
+
throw new Error(dockerFailure(["container", "inspect"], result));
|
|
91
|
+
}
|
|
92
|
+
const raw = inspectSchema.parse(JSON.parse(result.stdout))[0];
|
|
93
|
+
if (!raw)
|
|
94
|
+
return null;
|
|
95
|
+
const binding = raw.NetworkSettings.Ports?.["6969/tcp"]?.[0];
|
|
96
|
+
const restart = raw.HostConfig.RestartPolicy;
|
|
97
|
+
return {
|
|
98
|
+
id: raw.Id,
|
|
99
|
+
name: raw.Name.replace(/^\//, ""),
|
|
100
|
+
image: raw.Config.Image,
|
|
101
|
+
user: raw.Config.User,
|
|
102
|
+
running: raw.State.Running,
|
|
103
|
+
restart: restart.Name === "on-failure" && restart.MaximumRetryCount > 0
|
|
104
|
+
? `on-failure:${restart.MaximumRetryCount}`
|
|
105
|
+
: restart.Name || "no",
|
|
106
|
+
project: raw.Config.Labels?.["com.docker.compose.project"] ?? null,
|
|
107
|
+
launcher: raw.Config.Labels?.["io.slopify.launcher"] !== undefined,
|
|
108
|
+
mounts: raw.Mounts.map((m) => ({
|
|
109
|
+
type: m.Type,
|
|
110
|
+
name: m.Name ?? "",
|
|
111
|
+
source: m.Source,
|
|
112
|
+
destination: m.Destination,
|
|
113
|
+
})),
|
|
114
|
+
port: binding ? Number(binding.HostPort) : null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
context: async (uid, gid) => {
|
|
119
|
+
let endpoint = env.DOCKER_HOST;
|
|
120
|
+
if (env.DOCKER_CONTEXT || !endpoint) {
|
|
121
|
+
const raw = JSON.parse(await command([
|
|
122
|
+
"context",
|
|
123
|
+
"inspect",
|
|
124
|
+
...(env.DOCKER_CONTEXT ? [env.DOCKER_CONTEXT] : []),
|
|
125
|
+
]));
|
|
126
|
+
endpoint = z
|
|
127
|
+
.array(z.object({ Endpoints: z.object({ docker: z.object({ Host: z.string() }) }) }))
|
|
128
|
+
.parse(raw)[0]?.Endpoints.docker.Host;
|
|
129
|
+
}
|
|
130
|
+
if (!endpoint?.startsWith("unix://"))
|
|
131
|
+
throw new Error(`The --docker install only works with Docker running on this machine, not a remote Docker (${endpoint ?? "unknown endpoint"}). Switch to the local one (unset DOCKER_HOST, or docker context use default) and try again.`);
|
|
132
|
+
const rawInfo = JSON.parse(await command(["info", "--format", "{{json .}}"]));
|
|
133
|
+
// Some Docker versions print the daemon's absence inside the JSON instead of failing.
|
|
134
|
+
const serverErrors = z.object({ ServerErrors: z.array(z.string()) }).safeParse(rawInfo)
|
|
135
|
+
.data?.ServerErrors;
|
|
136
|
+
if (serverErrors !== undefined && serverErrors.length > 0)
|
|
137
|
+
throw new Error(dockerFailure(["info"], { code: 1, stderr: serverErrors.join("\n") }));
|
|
138
|
+
const info = z
|
|
139
|
+
.object({
|
|
140
|
+
ID: z.string(),
|
|
141
|
+
OSType: z.literal("linux"),
|
|
142
|
+
OperatingSystem: z.string(),
|
|
143
|
+
SecurityOptions: z.array(z.string()),
|
|
144
|
+
})
|
|
145
|
+
.parse(rawInfo);
|
|
146
|
+
if (/docker desktop/i.test(info.OperatingSystem))
|
|
147
|
+
throw new Error("Docker Desktop isn't supported by the --docker install because it can't give your project folder the right owner. Use Docker Engine on Linux (docker context use default), or run Slopify without Docker: npx @gentbajko/slopify");
|
|
148
|
+
const rootless = info.SecurityOptions.includes("name=rootless");
|
|
149
|
+
if (!rootless && info.SecurityOptions.some((s) => s.startsWith("name=userns")))
|
|
150
|
+
throw new Error("Your Docker uses userns-remap, which the --docker install doesn't support. Use rootless Docker or a standard Docker Engine setup, or run Slopify without Docker: npx @gentbajko/slopify. Don't turn off Docker's isolation to get around this.");
|
|
151
|
+
await command(["compose", "version", "--short"]);
|
|
152
|
+
return { daemon: info.ID, user: rootless ? "0:0" : `${uid}:${gid}` };
|
|
153
|
+
},
|
|
154
|
+
image: async (ref) => {
|
|
155
|
+
const found = await run(["image", "inspect", "--format", "{{.Id}}", ref]);
|
|
156
|
+
if (found.code !== 0 || ref.endsWith(":latest")) {
|
|
157
|
+
const pulled = await run(["pull", ref]);
|
|
158
|
+
if (pulled.code !== 0)
|
|
159
|
+
throw new Error(`Could not download the Slopify Docker image ${ref}. Check your internet connection and that Docker is running (docker info), then try again. Nothing was changed. ${dockerFailure(["pull"], pulled)}`);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
imageVersion: async (ref) => z
|
|
163
|
+
.string()
|
|
164
|
+
.regex(/^\d+\.\d+\.\d+$/)
|
|
165
|
+
.parse(await command([
|
|
166
|
+
"run",
|
|
167
|
+
"--rm",
|
|
168
|
+
"--network",
|
|
169
|
+
"none",
|
|
170
|
+
"--read-only",
|
|
171
|
+
"--tmpfs",
|
|
172
|
+
"/data",
|
|
173
|
+
"--entrypoint",
|
|
174
|
+
"node",
|
|
175
|
+
ref,
|
|
176
|
+
"-p",
|
|
177
|
+
"require('/opt/slopify/packages/app/package.json').version",
|
|
178
|
+
])),
|
|
179
|
+
inspect,
|
|
180
|
+
legacyPrevious: async (name) => {
|
|
181
|
+
const ids = (await command([
|
|
182
|
+
"container",
|
|
183
|
+
"ls",
|
|
184
|
+
"-a",
|
|
185
|
+
"--filter",
|
|
186
|
+
`name=^/${name}-previous-`,
|
|
187
|
+
"--format",
|
|
188
|
+
"{{.ID}}",
|
|
189
|
+
]))
|
|
190
|
+
.split(/\s+/)
|
|
191
|
+
.filter(Boolean);
|
|
192
|
+
const found = [];
|
|
193
|
+
for (const id of ids) {
|
|
194
|
+
const c = await inspect(id);
|
|
195
|
+
if (c && recoveryLikeName(name, c.name))
|
|
196
|
+
found.push(c);
|
|
197
|
+
}
|
|
198
|
+
return found;
|
|
199
|
+
},
|
|
200
|
+
volumeExists: async (name) => (await command(["volume", "ls", "--format", "{{.Name}}"])).split(/\s+/).includes(name),
|
|
201
|
+
createVolume: async (name) => {
|
|
202
|
+
await command(["volume", "create", name]);
|
|
203
|
+
},
|
|
204
|
+
volumeProjects: async (image, volume) => {
|
|
205
|
+
const value = z
|
|
206
|
+
.discriminatedUnion("exists", [
|
|
207
|
+
z.object({ exists: z.literal(false) }),
|
|
208
|
+
z.object({
|
|
209
|
+
exists: z.literal(true),
|
|
210
|
+
digest: z.object({ hash: z.string(), files: z.number(), bytes: z.number() }),
|
|
211
|
+
}),
|
|
212
|
+
])
|
|
213
|
+
.parse(await helper(image, mount("volume", volume, "/source", true), "projects"));
|
|
214
|
+
return value.exists ? value.digest : null;
|
|
215
|
+
},
|
|
216
|
+
copyVolumeProjects: async (image, volume, destination, id) => {
|
|
217
|
+
const reader = await command([
|
|
218
|
+
"create",
|
|
219
|
+
"--name",
|
|
220
|
+
`slopify-reader-${id}`,
|
|
221
|
+
"--network",
|
|
222
|
+
"none",
|
|
223
|
+
"--read-only",
|
|
224
|
+
"--tmpfs",
|
|
225
|
+
"/data",
|
|
226
|
+
...mount("volume", volume, "/source", true),
|
|
227
|
+
"--entrypoint",
|
|
228
|
+
"true",
|
|
229
|
+
image,
|
|
230
|
+
]);
|
|
231
|
+
try {
|
|
232
|
+
await command(["cp", `${reader}:/source/projects/.`, destination]);
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
await command(["rm", reader], AbortSignal.timeout(10_000));
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
snapshot: async (image, volume, backup, labels) => {
|
|
239
|
+
await command([
|
|
240
|
+
"volume",
|
|
241
|
+
"create",
|
|
242
|
+
...Object.entries(labels).flatMap(([key, value]) => ["--label", `${key}=${value}`]),
|
|
243
|
+
backup,
|
|
244
|
+
]);
|
|
245
|
+
return helper(image, [...mount("volume", volume, "/source", true), ...mount("volume", backup, "/backup")], "snapshot");
|
|
246
|
+
},
|
|
247
|
+
restore: async (image, backup, volume, expected) => {
|
|
248
|
+
const found = await helper(image, mount("volume", backup, "/source", true), "private");
|
|
249
|
+
if (found.hash !== expected.hash)
|
|
250
|
+
throw new Error(`The recovery volume ${backup} changed after it was made, so Slopify did not copy it back.`);
|
|
251
|
+
const restored = await helper(image, [...mount("volume", backup, "/source", true), ...mount("volume", volume, "/data")], "restore");
|
|
252
|
+
if (restored.hash !== expected.hash)
|
|
253
|
+
throw new Error(`The data copied back from ${backup} differs from the recovery volume.`);
|
|
254
|
+
},
|
|
255
|
+
own: async (image, volume, user) => {
|
|
256
|
+
await helper(image, mount("volume", volume, "/data"), "own", user);
|
|
257
|
+
},
|
|
258
|
+
recoveryVolumes: async (volume) => {
|
|
259
|
+
const names = (await command([
|
|
260
|
+
"volume",
|
|
261
|
+
"ls",
|
|
262
|
+
"--filter",
|
|
263
|
+
"label=io.slopify.transaction",
|
|
264
|
+
"--format",
|
|
265
|
+
"{{.Name}}",
|
|
266
|
+
]))
|
|
267
|
+
.split(/\s+/)
|
|
268
|
+
.filter((name) => recoveryId(volume, name) !== null);
|
|
269
|
+
const found = [];
|
|
270
|
+
for (const name of names) {
|
|
271
|
+
const labels = z
|
|
272
|
+
.record(z.string(), z.string())
|
|
273
|
+
.nullable()
|
|
274
|
+
.parse(JSON.parse(await command(["volume", "inspect", "--format", "{{json .Labels}}", name])));
|
|
275
|
+
found.push({
|
|
276
|
+
name,
|
|
277
|
+
transaction: labels?.["io.slopify.transaction"] ?? null,
|
|
278
|
+
container: labels?.["io.slopify.container"] ?? null,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return found;
|
|
282
|
+
},
|
|
283
|
+
// Never forced: Docker refuses a volume a container still mounts, and that refusal stands.
|
|
284
|
+
removeVolume: async (name) => {
|
|
285
|
+
await command(["volume", "rm", name]);
|
|
286
|
+
},
|
|
287
|
+
removeContainer: async (id) => {
|
|
288
|
+
await command(["rm", "-f", id]);
|
|
289
|
+
},
|
|
290
|
+
stop: async (id) => {
|
|
291
|
+
await command(["stop", id]);
|
|
292
|
+
},
|
|
293
|
+
start: async (id) => {
|
|
294
|
+
await command(["start", id]);
|
|
295
|
+
},
|
|
296
|
+
rename: async (id, name) => {
|
|
297
|
+
await command(["rename", id, name]);
|
|
298
|
+
},
|
|
299
|
+
restartPolicy: async (id, policy) => {
|
|
300
|
+
await command(["update", `--restart=${policy}`, id]);
|
|
301
|
+
},
|
|
302
|
+
compose: async (directory, name, args) => {
|
|
303
|
+
await command([
|
|
304
|
+
"compose",
|
|
305
|
+
"--project-directory",
|
|
306
|
+
directory,
|
|
307
|
+
"--file",
|
|
308
|
+
join(directory, "compose.yaml"),
|
|
309
|
+
"--env-file",
|
|
310
|
+
join(directory, ".env"),
|
|
311
|
+
"--project-name",
|
|
312
|
+
name,
|
|
313
|
+
...args,
|
|
314
|
+
]);
|
|
315
|
+
},
|
|
316
|
+
ready: async (id, check) => {
|
|
317
|
+
const deadline = Date.now() + 120_000;
|
|
318
|
+
while (Date.now() < deadline) {
|
|
319
|
+
signal.throwIfAborted();
|
|
320
|
+
const probe = AbortSignal.any([
|
|
321
|
+
signal,
|
|
322
|
+
AbortSignal.timeout(Math.max(1, Math.min(3000, deadline - Date.now()))),
|
|
323
|
+
]);
|
|
324
|
+
try {
|
|
325
|
+
const result = await run([
|
|
326
|
+
"exec",
|
|
327
|
+
id,
|
|
328
|
+
"node",
|
|
329
|
+
"-e",
|
|
330
|
+
"const [token,expected]=process.argv.slice(1); const get=(p,h)=>fetch('http://127.0.0.1:6969'+p,{headers:h}).then(async r=>{const b=await r.json(); if(!r.ok||b.status!=='ok'||(expected&&b.version!==expected)) process.exit(1)}); Promise.all([get('/api/health',{}), ...(token?[get('/api/update/ready',{'X-Slopify-Update-Token':token})]:[])]).catch(()=>process.exit(1))",
|
|
331
|
+
check?.token ?? "",
|
|
332
|
+
check?.version ?? "",
|
|
333
|
+
], probe);
|
|
334
|
+
if (result.code === 0)
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
if (signal.aborted || !probe.aborted)
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
await delay(500, undefined, { signal });
|
|
342
|
+
}
|
|
343
|
+
throw new Error(`The Slopify container did not answer within 2 minutes. See why with: docker logs ${id}`);
|
|
344
|
+
},
|
|
345
|
+
busy: async (id) => {
|
|
346
|
+
const result = await run([
|
|
347
|
+
"exec",
|
|
348
|
+
id,
|
|
349
|
+
"node",
|
|
350
|
+
"-e",
|
|
351
|
+
"fetch('http://127.0.0.1:6969/api/update').then(r=>r.json()).then(b=>process.stdout.write(String(b.busy===true))).catch(()=>process.exit(1))",
|
|
352
|
+
], AbortSignal.any([signal, AbortSignal.timeout(10_000)])).catch(() => undefined);
|
|
353
|
+
if (result === undefined || result.code !== 0)
|
|
354
|
+
return null;
|
|
355
|
+
return result.stdout.trim() === "true";
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function recoveryLikeName(name, candidate) {
|
|
360
|
+
const prefix = `${name}-previous-`;
|
|
361
|
+
return candidate.startsWith(prefix) && uuid.test(candidate.slice(prefix.length));
|
|
362
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { nodeHostSetupRunner } from "../../host-cli/install.js";
|
|
7
|
+
import { readVersion } from "../../kernel/version.js";
|
|
8
|
+
import { documentsDir, nodeDocumentsHost } from "../../slices/storage/documents.js";
|
|
9
|
+
import { recordLoginStart } from "../autostart/docker-record.js";
|
|
10
|
+
import { nodeAutostartExec } from "../autostart/native.js";
|
|
11
|
+
import { askTerminal } from "../autostart/prompt.js";
|
|
12
|
+
import { assertManagedDockerHost, planDockerHostCli } from "../docker.js";
|
|
13
|
+
import { applyDocker } from "./apply.js";
|
|
14
|
+
import { dockerEngine } from "./engine.js";
|
|
15
|
+
import { absolute, dockerRoot, identifier } from "./state.js";
|
|
16
|
+
/** `slopify --docker`, `slopify install --docker` and `slopify update --docker`. */
|
|
17
|
+
export async function runDockerCommand(command) {
|
|
18
|
+
const env = process.env;
|
|
19
|
+
const uid = process.getuid?.();
|
|
20
|
+
const gid = process.getgid?.();
|
|
21
|
+
assertManagedDockerHost(process.platform, uid, gid);
|
|
22
|
+
const home = homedir();
|
|
23
|
+
const version = readVersion();
|
|
24
|
+
const portText = command.port ?? env.SLOPIFY_DOCKER_HOST_PORT;
|
|
25
|
+
if (portText !== undefined &&
|
|
26
|
+
portText !== "" &&
|
|
27
|
+
(!/^\d{1,5}$/.test(portText) || Number(portText) > 65535))
|
|
28
|
+
throw new Error(`Invalid port ${JSON.stringify(portText)}: use a whole number between 1 and 65535, for example --port 7070.`);
|
|
29
|
+
const projectsText = command.projectsDir ?? env.SLOPIFY_DOCKER_PROJECTS_DIR;
|
|
30
|
+
const name = identifier.safeParse(env.SLOPIFY_DOCKER_NAME || "slopify");
|
|
31
|
+
const volume = identifier.safeParse(env.SLOPIFY_DOCKER_VOLUME || "slopify-data");
|
|
32
|
+
if (!name.success || !volume.success)
|
|
33
|
+
throw new Error("SLOPIFY_DOCKER_NAME and SLOPIFY_DOCKER_VOLUME may only use letters, digits, dots, dashes and underscores, and must start with a letter or digit. Fix them and try again.");
|
|
34
|
+
// New installs keep their files in this computer's Documents folder; `--projects-dir
|
|
35
|
+
// documents` moves an existing one there.
|
|
36
|
+
const documents = await documentsDir(nodeDocumentsHost());
|
|
37
|
+
const projects = !projectsText
|
|
38
|
+
? null
|
|
39
|
+
: projectsText === "documents"
|
|
40
|
+
? documentsProjects(documents, name.success ? name.data : "slopify")
|
|
41
|
+
: resolve(projectsText.startsWith("~/") ? join(home, projectsText.slice(2)) : projectsText);
|
|
42
|
+
if (projects !== null && !absolute.safeParse(projects).success)
|
|
43
|
+
throw new Error(`The project folder ${JSON.stringify(projectsText)} can't be used: give a full path without commas, for example --projects-dir ~/Slopify/Projects.`);
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const abort = () => controller.abort(new Error("Stopped before it finished. Run the same command again; Slopify puts the previous version back first if anything was half done."));
|
|
46
|
+
process.once("SIGINT", abort);
|
|
47
|
+
process.once("SIGTERM", abort);
|
|
48
|
+
try {
|
|
49
|
+
const hostCli = await planDockerHostCli({
|
|
50
|
+
root: join(env.XDG_DATA_HOME || join(home, ".local/share"), "slopify/host-cli"),
|
|
51
|
+
version,
|
|
52
|
+
disabled: command.hostCli === "off",
|
|
53
|
+
accepted: command.acceptHostCli === true,
|
|
54
|
+
interactive: process.stdin.isTTY === true,
|
|
55
|
+
env,
|
|
56
|
+
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(20 * 60_000)]),
|
|
57
|
+
runner: nodeHostSetupRunner,
|
|
58
|
+
prompt: async (message) => {
|
|
59
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout });
|
|
60
|
+
try {
|
|
61
|
+
return /^(?:y|yes)$/i.test((await terminal.question(message)).trim());
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
terminal.close();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const result = await applyDocker({
|
|
69
|
+
home,
|
|
70
|
+
uid,
|
|
71
|
+
gid: gid,
|
|
72
|
+
root: dockerRoot(env, home),
|
|
73
|
+
name: name.data,
|
|
74
|
+
volume: volume.data,
|
|
75
|
+
image: env.SLOPIFY_DOCKER_IMAGE || `ghcr.io/gentbajko/slopify:${version}`,
|
|
76
|
+
version,
|
|
77
|
+
port: portText === undefined ? null : portText === "" ? 0 : Number(portText),
|
|
78
|
+
projects,
|
|
79
|
+
documents,
|
|
80
|
+
mode: command.mode,
|
|
81
|
+
composeFile: await composeFile(),
|
|
82
|
+
hostCli,
|
|
83
|
+
report: (line) => console.log(line),
|
|
84
|
+
}, dockerEngine(nodeHostSetupRunner, controller.signal, env));
|
|
85
|
+
console.log(result.changed
|
|
86
|
+
? `Slopify ${version} is running at ${result.url}`
|
|
87
|
+
: `Slopify ${version} is already installed and running at ${result.url}`);
|
|
88
|
+
console.log(`Project files on this machine: ${result.projects}`);
|
|
89
|
+
if (result.backups !== null)
|
|
90
|
+
console.log(`Backups on this machine: ${result.backups}`);
|
|
91
|
+
console.log(hostCli.enabled
|
|
92
|
+
? "Host CLI bridge ready: Claude Code, Codex and Gemini run on this machine with their existing logins."
|
|
93
|
+
: command.hostCli === "off"
|
|
94
|
+
? "Using API keys only (--host-cli=off). Leave that option out to use the Claude Code, Codex or Gemini CLI installed on this machine."
|
|
95
|
+
: "Using API keys only. Install Claude Code, Codex or Gemini on this machine and run the command again to use them.");
|
|
96
|
+
if (result.recovery)
|
|
97
|
+
console.log(`Recovery copy of your data: Docker volume ${result.recovery}`);
|
|
98
|
+
for (const line of result.removed)
|
|
99
|
+
console.log(`Removed ${line}.`);
|
|
100
|
+
for (const problem of result.problems)
|
|
101
|
+
console.warn(problem);
|
|
102
|
+
await recordLoginStart({
|
|
103
|
+
directory: join(dockerRoot(env, home), name.data),
|
|
104
|
+
name: name.data,
|
|
105
|
+
uid,
|
|
106
|
+
flag: command.autostart,
|
|
107
|
+
interactive: process.stdin.isTTY === true,
|
|
108
|
+
ask: askTerminal,
|
|
109
|
+
exec: nodeAutostartExec,
|
|
110
|
+
now: () => new Date(),
|
|
111
|
+
report: (line) => console.log(line),
|
|
112
|
+
}).catch((error) => {
|
|
113
|
+
console.warn(`Slopify couldn't check whether Docker starts when you log in (${error instanceof Error ? error.message : String(error)}). Settings → General will say it doesn't know; run the command again to check again.`);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
process.off("SIGINT", abort);
|
|
118
|
+
process.off("SIGTERM", abort);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// dist/compose.yaml in the package; the repository's own compose.yaml when run from source.
|
|
122
|
+
async function composeFile() {
|
|
123
|
+
const candidates = ["../../compose.yaml", "../../../../../compose.yaml"].map((path) => fileURLToPath(new URL(path, import.meta.url)));
|
|
124
|
+
for (const path of candidates) {
|
|
125
|
+
try {
|
|
126
|
+
await access(path);
|
|
127
|
+
return path;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Try the next location.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw new Error("This Slopify package is missing its compose.yaml. Reinstall it (npx @gentbajko/slopify@latest --docker) and try again.");
|
|
134
|
+
}
|
|
135
|
+
/** Whether this user has a Docker installation, so `slopify update` knows which one to update. */
|
|
136
|
+
export async function hasDockerInstall(env) {
|
|
137
|
+
const directory = join(dockerRoot(env, homedir()), env.SLOPIFY_DOCKER_NAME || "slopify");
|
|
138
|
+
for (const file of ["install.json", "receipt.json"]) {
|
|
139
|
+
try {
|
|
140
|
+
await access(join(directory, file));
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Not this one.
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
/** `<Documents>/Slopify/Projects`, or `<Documents>/Slopify/<name>/Projects` for a second install. */
|
|
150
|
+
export function documentsProjects(documents, name) {
|
|
151
|
+
return name === "slopify"
|
|
152
|
+
? join(documents, "Slopify", "Projects")
|
|
153
|
+
: join(documents, "Slopify", name, "Projects");
|
|
154
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { lstat, mkdir } 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 { isMissing, syncDirectory } from "./tree.js";
|
|
6
|
+
export const identifier = z
|
|
7
|
+
.string()
|
|
8
|
+
.min(1)
|
|
9
|
+
.max(128)
|
|
10
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/);
|
|
11
|
+
export 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
|
+
const identity = z.object({ dev: z.string(), ino: z.string() }).strict();
|
|
17
|
+
const digest = 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
|
+
const token = z.string().regex(/^[a-f0-9]{64}$/);
|
|
25
|
+
/**
|
|
26
|
+
* install.json: what the Docker install command last committed. The compose file and its .env
|
|
27
|
+
* are rendered from it, the host helper reads `projects` and `projectsIdentity` to decide which
|
|
28
|
+
* folders it may open, and the next install or update starts from it.
|
|
29
|
+
*/
|
|
30
|
+
export const installSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
version: z.literal(2),
|
|
33
|
+
name: identifier,
|
|
34
|
+
volume: identifier,
|
|
35
|
+
daemon: z.string(),
|
|
36
|
+
image: z.string().min(1),
|
|
37
|
+
appVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
38
|
+
user: z.string().regex(/^\d+:\d+$/),
|
|
39
|
+
port: z.number().int().min(0).max(65535),
|
|
40
|
+
projects: absolute,
|
|
41
|
+
projectsIdentity: identity,
|
|
42
|
+
// The Backups folder shared beside Projects (installs from 3.0 on). Absent: backups stay in
|
|
43
|
+
// the Projects folder's own Backups folder, as older installs keep them.
|
|
44
|
+
backups: absolute.optional(),
|
|
45
|
+
hostCli: z.boolean(),
|
|
46
|
+
token,
|
|
47
|
+
recovery: z.string().nullable(),
|
|
48
|
+
})
|
|
49
|
+
.strict();
|
|
50
|
+
/**
|
|
51
|
+
* update.json exists only while an install or update is changing things. If a run is cut off,
|
|
52
|
+
* the next run finds it and puts the previous version back before doing anything else.
|
|
53
|
+
*/
|
|
54
|
+
export const updateSchema = z
|
|
55
|
+
.object({
|
|
56
|
+
version: z.literal(2),
|
|
57
|
+
id: z.string().uuid(),
|
|
58
|
+
phase: z.enum(["stopping", "snapshot", "starting"]),
|
|
59
|
+
// The image being installed; its fixed volume helper does the snapshot and the restore.
|
|
60
|
+
image: z.string().min(1),
|
|
61
|
+
previous: z.discriminatedUnion("kind", [
|
|
62
|
+
z.object({ kind: z.literal("none") }).strict(),
|
|
63
|
+
z.object({ kind: z.literal("compose"), env: z.string(), activation: z.string() }).strict(),
|
|
64
|
+
z
|
|
65
|
+
.object({
|
|
66
|
+
kind: z.literal("legacy"),
|
|
67
|
+
id: z.string(),
|
|
68
|
+
name: identifier,
|
|
69
|
+
renamed: identifier,
|
|
70
|
+
running: z.boolean(),
|
|
71
|
+
restart: z.string(),
|
|
72
|
+
})
|
|
73
|
+
.strict(),
|
|
74
|
+
]),
|
|
75
|
+
backup: z.string().nullable(),
|
|
76
|
+
backupDigest: digest.nullable(),
|
|
77
|
+
// A project folder this run filled with a verified copy; the undo removes it again.
|
|
78
|
+
published: z.object({ path: absolute, identity }).strict().nullable(),
|
|
79
|
+
})
|
|
80
|
+
.strict();
|
|
81
|
+
// 2.5.0's launcher records, read only to adopt an installation it made.
|
|
82
|
+
export const legacyReceiptSchema = z
|
|
83
|
+
.object({ name: identifier, volume: identifier, projects: absolute, directoryIdentity: identity })
|
|
84
|
+
.passthrough();
|
|
85
|
+
export const legacyJournalSchema = z
|
|
86
|
+
.object({
|
|
87
|
+
id: z.string(),
|
|
88
|
+
name: identifier,
|
|
89
|
+
volume: identifier,
|
|
90
|
+
phase: z.string(),
|
|
91
|
+
backup: z.string(),
|
|
92
|
+
})
|
|
93
|
+
.passthrough();
|
|
94
|
+
export const settledLegacyPhases = new Set(["healthy", "committed", "restored", "rolled-back"]);
|
|
95
|
+
/** Where the Docker install command keeps one folder per installation. */
|
|
96
|
+
export function dockerRoot(env, home) {
|
|
97
|
+
return join(absolute.parse(env.XDG_DATA_HOME || join(home, ".local/share")), "slopify/docker");
|
|
98
|
+
}
|
|
99
|
+
export async function privateDirectory(path, uid) {
|
|
100
|
+
absolute.parse(path);
|
|
101
|
+
let current = sep;
|
|
102
|
+
for (const name of path.split(sep).filter(Boolean)) {
|
|
103
|
+
current = join(current, name);
|
|
104
|
+
await mkdir(current, { mode: 0o700 }).catch((error) => {
|
|
105
|
+
if (!(typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST"))
|
|
106
|
+
throw error;
|
|
107
|
+
});
|
|
108
|
+
const s = await lstat(current);
|
|
109
|
+
if (!s.isDirectory() ||
|
|
110
|
+
s.isSymbolicLink() ||
|
|
111
|
+
((s.mode & 0o022) !== 0 && (s.mode & 0o1000) === 0))
|
|
112
|
+
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.`);
|
|
113
|
+
}
|
|
114
|
+
const s = await lstat(path);
|
|
115
|
+
if (s.uid !== uid || (s.mode & 0o077) !== 0)
|
|
116
|
+
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.`);
|
|
117
|
+
}
|
|
118
|
+
export async function readState(path, schema, uid) {
|
|
119
|
+
const s = await lstat(path).catch((error) => {
|
|
120
|
+
if (isMissing(error))
|
|
121
|
+
return undefined;
|
|
122
|
+
throw error;
|
|
123
|
+
});
|
|
124
|
+
if (s === undefined)
|
|
125
|
+
return null;
|
|
126
|
+
if (!s.isFile() || s.isSymbolicLink() || s.nlink !== 1 || s.uid !== uid || (s.mode & 0o077) !== 0)
|
|
127
|
+
throw new Error(`${path} is not a private file owned by you, so Slopify won't trust it. Fix it (chmod 600 ${path}) and try again.`);
|
|
128
|
+
const text = await privateRead(path, uid);
|
|
129
|
+
if (text === undefined)
|
|
130
|
+
throw new Error(`${path} changed while Slopify was reading it. Try again.`);
|
|
131
|
+
try {
|
|
132
|
+
return schema.parse(JSON.parse(text));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new Error(`${path} is damaged, so Slopify can't tell what is installed. Move it aside and run the install command again.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export async function writeState(path, value) {
|
|
139
|
+
await writeText(path, JSON.stringify(value));
|
|
140
|
+
}
|
|
141
|
+
export async function writeText(path, text) {
|
|
142
|
+
if (Buffer.byteLength(text) > 64 * 1024)
|
|
143
|
+
throw new Error("Installation control record exceeds 64 KiB.");
|
|
144
|
+
await privateWrite(path, text);
|
|
145
|
+
await syncDirectory(dirname(path));
|
|
146
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { chmod, lchown, lstat, mkdir, readdir, rm
|
|
2
|
+
import { chmod, lchown, lstat, mkdir, readdir, rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { isMissing, syncDirectory, treeDigest } from "./tree.js";
|
|
@@ -78,10 +78,6 @@ export async function volumeOperation(args) {
|
|
|
78
78
|
await lchown("/data/home", uid, gid);
|
|
79
79
|
return { ok: true };
|
|
80
80
|
}
|
|
81
|
-
if (operation === "probe") {
|
|
82
|
-
await writeFile("/probe/container", "slopify ownership probe", { flag: "wx", mode: 0o600 });
|
|
83
|
-
return { uid: process.getuid?.(), gid: process.getgid?.() };
|
|
84
|
-
}
|
|
85
81
|
throw new Error("Unknown fixed Docker volume operation.");
|
|
86
82
|
}
|
|
87
83
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|