@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
package/dist/catalog/store.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { parse } from "yaml";
|
|
3
|
+
import { parse, stringify } from "yaml";
|
|
4
4
|
import { isLocalCliProvider } from "../slices/settings/model.js";
|
|
5
|
+
import { applyOpenRouterListing, catalogueFamilies, combineChanges, mergeCatalogue, modelKey, noChanges, } from "./merge.js";
|
|
5
6
|
import { catalogueSchema, isVideoModel } from "./schema.js";
|
|
6
7
|
export const catalogueSource = "https://raw.githubusercontent.com/GentBajko/slopify/main/packages/app/src/assets/models.yaml";
|
|
8
|
+
// OpenRouter's public model list: live ids and per-token prices, no key needed.
|
|
9
|
+
export const openRouterModelsSource = "https://openrouter.ai/api/v1/models";
|
|
10
|
+
// How often the automatic check runs; it also runs once at every start.
|
|
11
|
+
export const catalogueCheckIntervalMs = 24 * 60 * 60 * 1000;
|
|
7
12
|
export function parseCatalogue(text) {
|
|
8
13
|
if (Buffer.byteLength(text) > 1024 * 1024)
|
|
9
14
|
throw new Error("Model catalogue exceeds 1 MB");
|
|
@@ -26,6 +31,9 @@ export function createCatalogueStore(deps) {
|
|
|
26
31
|
let stamp = "";
|
|
27
32
|
let warning = null;
|
|
28
33
|
let refreshing;
|
|
34
|
+
let syncing;
|
|
35
|
+
const syncPath = join(deps.dataDir, "models-sync.json");
|
|
36
|
+
const now = deps.now ?? (() => new Date());
|
|
29
37
|
function read() {
|
|
30
38
|
try {
|
|
31
39
|
const file = statSync(path);
|
|
@@ -40,12 +48,12 @@ export function createCatalogueStore(deps) {
|
|
|
40
48
|
}
|
|
41
49
|
catch {
|
|
42
50
|
warning =
|
|
43
|
-
"Your models.yaml file is missing or has a mistake, so Slopify is still using the last working model list. Fix the file, or use
|
|
51
|
+
"Your models.yaml file is missing or has a mistake, so Slopify is still using the last working model list. Fix the file, or use Replace with published file in Settings → Models.";
|
|
44
52
|
}
|
|
45
53
|
return current;
|
|
46
54
|
}
|
|
47
|
-
async function
|
|
48
|
-
const response = await deps.fetch(
|
|
55
|
+
async function download(url, maxBytes) {
|
|
56
|
+
const response = await deps.fetch(url, {
|
|
49
57
|
signal: AbortSignal.timeout(15000),
|
|
50
58
|
redirect: "error",
|
|
51
59
|
});
|
|
@@ -62,8 +70,8 @@ export function createCatalogueStore(deps) {
|
|
|
62
70
|
if (next.done)
|
|
63
71
|
break;
|
|
64
72
|
size += next.value.byteLength;
|
|
65
|
-
if (size >
|
|
66
|
-
throw new Error("
|
|
73
|
+
if (size > maxBytes)
|
|
74
|
+
throw new Error("Download is too large");
|
|
67
75
|
chunks.push(next.value);
|
|
68
76
|
}
|
|
69
77
|
}
|
|
@@ -71,7 +79,10 @@ export function createCatalogueStore(deps) {
|
|
|
71
79
|
await reader.cancel().catch(() => { });
|
|
72
80
|
reader.releaseLock();
|
|
73
81
|
}
|
|
74
|
-
|
|
82
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
83
|
+
}
|
|
84
|
+
async function refresh() {
|
|
85
|
+
const text = await download(catalogueSource, 1024 * 1024);
|
|
75
86
|
const next = parseCatalogue(text);
|
|
76
87
|
mkdirSync(deps.dataDir, { recursive: true, mode: 0o700 });
|
|
77
88
|
// A manual refresh replaces the local catalogue; retain the previous text for recovery.
|
|
@@ -84,10 +95,91 @@ export function createCatalogueStore(deps) {
|
|
|
84
95
|
warning = null;
|
|
85
96
|
read();
|
|
86
97
|
}
|
|
98
|
+
function readSync() {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(readFileSync(syncPath, "utf8"));
|
|
101
|
+
if (isSyncFile(parsed))
|
|
102
|
+
return parsed;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Absent or damaged: the first check starts from the bundled list below.
|
|
106
|
+
}
|
|
107
|
+
return { checkedAt: null, known: null, changes: noChanges, warning: null };
|
|
108
|
+
}
|
|
109
|
+
function writeSync(value) {
|
|
110
|
+
writeFileSync(`${syncPath}.next`, JSON.stringify(value), { mode: 0o600 });
|
|
111
|
+
renameSync(`${syncPath}.next`, syncPath);
|
|
112
|
+
}
|
|
113
|
+
async function openRouterListing() {
|
|
114
|
+
try {
|
|
115
|
+
return parseOpenRouterListing(JSON.parse(await download(openRouterModelsSource, 16 * 1024 * 1024)));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// Optional: the published catalogue still carries OpenRouter's curated prices.
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function sync() {
|
|
123
|
+
const saved = readSync();
|
|
124
|
+
const local = read();
|
|
125
|
+
if (warning !== null) {
|
|
126
|
+
// Never overwrite a file the user is in the middle of fixing.
|
|
127
|
+
writeSync({ ...saved, warning });
|
|
128
|
+
return { checkedAt: saved.checkedAt, changes: saved.changes, warning };
|
|
129
|
+
}
|
|
130
|
+
let published;
|
|
131
|
+
try {
|
|
132
|
+
published = parseCatalogue(await download(catalogueSource, 1024 * 1024));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
const failed = "Slopify could not download the latest model list, so it is still using the one it has. Check your internet connection, then choose Check now in Settings → Models.";
|
|
136
|
+
writeSync({ ...saved, warning: failed });
|
|
137
|
+
return { checkedAt: saved.checkedAt, changes: saved.changes, warning: failed };
|
|
138
|
+
}
|
|
139
|
+
// Before the first check the models shipped with this version count as published, so a
|
|
140
|
+
// model the user added to the file is never mistaken for a retired one.
|
|
141
|
+
const known = new Set(saved.known ?? keysOf(parseCatalogue(bundled)));
|
|
142
|
+
const merged = mergeCatalogue(local, published, known);
|
|
143
|
+
const live = applyOpenRouterListing(merged.catalogue, await openRouterListing());
|
|
144
|
+
const changes = combineChanges(merged.changes, live.changes);
|
|
145
|
+
const text = `${syncedHeader}${stringify(live.catalogue, { aliasDuplicateObjects: false })}`;
|
|
146
|
+
const next = parseCatalogue(text);
|
|
147
|
+
if (JSON.stringify(next) !== JSON.stringify(local)) {
|
|
148
|
+
if (existsSync(path))
|
|
149
|
+
writeFileSync(`${path}.previous`, readFileSync(path), { mode: 0o600 });
|
|
150
|
+
writeFileSync(`${path}.next`, text, { mode: 0o600 });
|
|
151
|
+
renameSync(`${path}.next`, path);
|
|
152
|
+
current = next;
|
|
153
|
+
stamp = "";
|
|
154
|
+
read();
|
|
155
|
+
}
|
|
156
|
+
const status = { checkedAt: now().toISOString(), changes, warning: null };
|
|
157
|
+
writeSync({ ...status, known: [...merged.known].sort() });
|
|
158
|
+
return status;
|
|
159
|
+
}
|
|
87
160
|
return {
|
|
161
|
+
sync: () => {
|
|
162
|
+
syncing ??= sync().finally(() => {
|
|
163
|
+
syncing = undefined;
|
|
164
|
+
});
|
|
165
|
+
return syncing;
|
|
166
|
+
},
|
|
167
|
+
syncDue: (at) => {
|
|
168
|
+
const last = readSync().checkedAt;
|
|
169
|
+
return last === null || at - Date.parse(last) >= catalogueCheckIntervalMs;
|
|
170
|
+
},
|
|
88
171
|
read,
|
|
89
172
|
models: (provider, family) => read()[family].filter((m) => m.provider === provider && m.enabled && !m.deprecated && !isVideoModel(m)),
|
|
90
|
-
status: () =>
|
|
173
|
+
status: () => {
|
|
174
|
+
const saved = readSync();
|
|
175
|
+
return {
|
|
176
|
+
updatedAt: read().updatedAt,
|
|
177
|
+
path,
|
|
178
|
+
warning,
|
|
179
|
+
source: catalogueSource,
|
|
180
|
+
sync: { checkedAt: saved.checkedAt, changes: saved.changes, warning: saved.warning },
|
|
181
|
+
};
|
|
182
|
+
},
|
|
91
183
|
refresh: () => {
|
|
92
184
|
refreshing ??= refresh().finally(() => {
|
|
93
185
|
refreshing = undefined;
|
|
@@ -96,3 +188,50 @@ export function createCatalogueStore(deps) {
|
|
|
96
188
|
},
|
|
97
189
|
};
|
|
98
190
|
}
|
|
191
|
+
const syncedHeader = "# Slopify's model list, kept up to date automatically from the published catalogue.\n" +
|
|
192
|
+
"# Your edits are kept; retired models stay listed with deprecated: true.\n";
|
|
193
|
+
function isSyncFile(value) {
|
|
194
|
+
if (typeof value !== "object" || value === null)
|
|
195
|
+
return false;
|
|
196
|
+
const row = value;
|
|
197
|
+
const changes = row.changes;
|
|
198
|
+
return ((row.checkedAt === null || typeof row.checkedAt === "string") &&
|
|
199
|
+
(row.known === null || Array.isArray(row.known)) &&
|
|
200
|
+
(row.warning === null || typeof row.warning === "string") &&
|
|
201
|
+
typeof changes === "object" &&
|
|
202
|
+
changes !== null &&
|
|
203
|
+
Array.isArray(changes.added) &&
|
|
204
|
+
Array.isArray(changes.retired) &&
|
|
205
|
+
Array.isArray(changes.priced));
|
|
206
|
+
}
|
|
207
|
+
function keysOf(catalogue) {
|
|
208
|
+
return catalogueFamilies.flatMap((family) => catalogue[family].map((row) => modelKey(family, row.provider, row.id)));
|
|
209
|
+
}
|
|
210
|
+
// `{ data: [{ id, pricing: { prompt, completion } }] }`, prices in USD per token as strings.
|
|
211
|
+
// Rows that do not read cleanly are skipped; a negative price means "varies" and is ignored.
|
|
212
|
+
export function parseOpenRouterListing(value) {
|
|
213
|
+
if (typeof value !== "object" || value === null)
|
|
214
|
+
return [];
|
|
215
|
+
const data = value.data;
|
|
216
|
+
if (!Array.isArray(data))
|
|
217
|
+
return [];
|
|
218
|
+
const price = (raw) => {
|
|
219
|
+
const number = typeof raw === "string" || typeof raw === "number" ? Number(raw) : Number.NaN;
|
|
220
|
+
return Number.isFinite(number) && number >= 0 ? number : undefined;
|
|
221
|
+
};
|
|
222
|
+
const rows = [];
|
|
223
|
+
for (const row of data) {
|
|
224
|
+
if (typeof row !== "object" || row === null)
|
|
225
|
+
continue;
|
|
226
|
+
const { id, pricing } = row;
|
|
227
|
+
if (typeof id !== "string" || id === "")
|
|
228
|
+
continue;
|
|
229
|
+
const prices = typeof pricing === "object" && pricing !== null ? pricing : {};
|
|
230
|
+
rows.push({
|
|
231
|
+
id,
|
|
232
|
+
promptPerToken: price(prices.prompt),
|
|
233
|
+
completionPerToken: price(prices.completion),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return rows;
|
|
237
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Slopify on Docker: one service, one private data volume, one host folder for project files.
|
|
2
|
+
#
|
|
3
|
+
# `npx @gentbajko/slopify --docker` copies this file to ~/.local/share/slopify/docker/<name>/
|
|
4
|
+
# and writes the .env beside it, then runs `docker compose up`. You can also run it by hand; see
|
|
5
|
+
# docs/docker.md for the variables. The data volume is external on purpose: `docker compose down`
|
|
6
|
+
# (even with -v) never deletes it.
|
|
7
|
+
name: ${SLOPIFY_NAME:-slopify}
|
|
8
|
+
|
|
9
|
+
services:
|
|
10
|
+
slopify:
|
|
11
|
+
image: ${SLOPIFY_IMAGE:-ghcr.io/gentbajko/slopify:latest}
|
|
12
|
+
container_name: ${SLOPIFY_NAME:-slopify}
|
|
13
|
+
# Your own user id, so project files on the host belong to you (0:0 under rootless Docker).
|
|
14
|
+
user: ${SLOPIFY_USER:-1000:1000}
|
|
15
|
+
restart: unless-stopped
|
|
16
|
+
# Localhost only: anyone who reaches this port controls the app and its provider keys.
|
|
17
|
+
# An empty SLOPIFY_PORT picks a free port.
|
|
18
|
+
ports:
|
|
19
|
+
- 127.0.0.1:${SLOPIFY_PORT-6969}:6969
|
|
20
|
+
volumes:
|
|
21
|
+
- type: volume
|
|
22
|
+
source: data
|
|
23
|
+
target: /data
|
|
24
|
+
- type: bind
|
|
25
|
+
source: ${SLOPIFY_PROJECTS_DIR:?set SLOPIFY_PROJECTS_DIR to the folder for your project files}
|
|
26
|
+
target: /data/projects
|
|
27
|
+
# Scheduled backups' default folder, beside Projects (new installs from 3.0). Without
|
|
28
|
+
# SLOPIFY_BACKUPS_DIR it is an empty folder nobody reads and backups stay in
|
|
29
|
+
# Projects/Backups.
|
|
30
|
+
- type: bind
|
|
31
|
+
source: ${SLOPIFY_BACKUPS_DIR:-./backups-off}
|
|
32
|
+
target: /data/backups
|
|
33
|
+
bind:
|
|
34
|
+
create_host_path: true
|
|
35
|
+
# The host CLI bridge: a folder holding the helper's socket and token, nothing else.
|
|
36
|
+
# With the bridge off it is an empty folder and Settings shows the CLIs as unavailable.
|
|
37
|
+
- type: bind
|
|
38
|
+
source: ${SLOPIFY_HOST_CLI_SHARE:-./host-cli-off}
|
|
39
|
+
target: /opt/slopify-host
|
|
40
|
+
read_only: true
|
|
41
|
+
bind:
|
|
42
|
+
create_host_path: true
|
|
43
|
+
# The update handshake: activation.json says when the installer has committed this version.
|
|
44
|
+
- type: bind
|
|
45
|
+
source: ${SLOPIFY_ACTIVATION_DIR:-./activation}
|
|
46
|
+
target: /opt/slopify-install
|
|
47
|
+
read_only: true
|
|
48
|
+
bind:
|
|
49
|
+
create_host_path: true
|
|
50
|
+
environment:
|
|
51
|
+
SLOPIFY_DOCKER_PROJECTS_DIR: ${SLOPIFY_PROJECTS_DIR}
|
|
52
|
+
SLOPIFY_DOCKER_BACKUPS_DIR: ${SLOPIFY_BACKUPS_DIR:-}
|
|
53
|
+
SLOPIFY_HOST_CLI_DIR: /opt/slopify-host
|
|
54
|
+
SLOPIFY_DOCKER_INSTALL_STATE: /opt/slopify-install/activation.json
|
|
55
|
+
SLOPIFY_UPDATE_TOKEN: ${SLOPIFY_UPDATE_TOKEN:-}
|
|
56
|
+
SLOPIFY_UPDATE_PENDING: ${SLOPIFY_UPDATE_PENDING:-0}
|
|
57
|
+
healthcheck:
|
|
58
|
+
test:
|
|
59
|
+
- CMD
|
|
60
|
+
- node
|
|
61
|
+
- -e
|
|
62
|
+
- "fetch('http://127.0.0.1:6969/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
|
|
63
|
+
interval: 30s
|
|
64
|
+
timeout: 5s
|
|
65
|
+
start_period: 120s
|
|
66
|
+
start_interval: 2s
|
|
67
|
+
|
|
68
|
+
volumes:
|
|
69
|
+
data:
|
|
70
|
+
external: true
|
|
71
|
+
name: ${SLOPIFY_VOLUME:-slopify-data}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { installSchema, readState, writeState } from "../docker-install/state.js";
|
|
3
|
+
import { checkDockerStart, loginStartFile, loginStartReport, loginStartSchema, } from "./docker.js";
|
|
4
|
+
import { autostartQuestion } from "./prompt.js";
|
|
5
|
+
/**
|
|
6
|
+
* The --docker installer's part: ask "Start Slopify when you log in?" once (or take
|
|
7
|
+
* --autostart / --no-autostart), look whether Docker starts by itself, and leave the answer in
|
|
8
|
+
* the installation's activation folder, which the container reads at
|
|
9
|
+
* /opt/slopify-install/login-start.json. Docker's own settings are only read, never changed.
|
|
10
|
+
*/
|
|
11
|
+
export async function recordLoginStart(o) {
|
|
12
|
+
const install = await readState(join(o.directory, "install.json"), installSchema, o.uid);
|
|
13
|
+
// Rootless Docker runs the container as 0:0 (see compose.yaml's SLOPIFY_USER).
|
|
14
|
+
const manager = install?.user === "0:0" ? "rootless" : "system";
|
|
15
|
+
const path = join(o.directory, "activation", loginStartFile);
|
|
16
|
+
const previous = await readState(path, loginStartSchema, o.uid).catch(() => null);
|
|
17
|
+
let wanted = o.flag ?? previous?.wanted ?? null;
|
|
18
|
+
if (wanted === null && o.interactive)
|
|
19
|
+
wanted = (await o.ask(autostartQuestion)) ?? null;
|
|
20
|
+
const record = {
|
|
21
|
+
version: 1,
|
|
22
|
+
checkedAt: o.now().toISOString(),
|
|
23
|
+
docker: await checkDockerStart(o.exec, manager),
|
|
24
|
+
manager,
|
|
25
|
+
wanted,
|
|
26
|
+
};
|
|
27
|
+
await writeState(path, record);
|
|
28
|
+
o.report(wanted === false
|
|
29
|
+
? `Slopify in Docker still starts whenever Docker does, because its container restarts with Docker. When you don't want it running: docker stop ${o.name}`
|
|
30
|
+
: loginStartReport(record));
|
|
31
|
+
return record;
|
|
32
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Slopify in Docker restarts with Docker (`restart: unless-stopped` in compose.yaml), so it
|
|
3
|
+
// starts at login exactly when Docker does. Slopify never changes Docker's own setting; the
|
|
4
|
+
// installer looks once, on this machine, and leaves the answer where the container can read
|
|
5
|
+
// it (the read-only /opt/slopify-install folder), because a container can't see its host.
|
|
6
|
+
export const loginStartFile = "login-start.json";
|
|
7
|
+
export const containerLoginStart = `/opt/slopify-install/${loginStartFile}`;
|
|
8
|
+
export const loginStartSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
version: z.literal(1),
|
|
11
|
+
checkedAt: z.string().max(40),
|
|
12
|
+
// yes: Docker starts by itself; no: it doesn't; unknown: this machine couldn't say.
|
|
13
|
+
docker: z.enum(["yes", "no", "unknown"]),
|
|
14
|
+
// Which Docker the installation uses: the system one, or rootless Docker in your account.
|
|
15
|
+
manager: z.enum(["system", "rootless"]),
|
|
16
|
+
// The installer's "Start Slopify when you log in?" answer, or null when it wasn't asked.
|
|
17
|
+
wanted: z.boolean().nullable(),
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
export function dockerHowTo(manager) {
|
|
21
|
+
return manager === "rootless"
|
|
22
|
+
? "Turn on Docker's start at login in a terminal: systemctl --user enable docker (rootless Docker). Slopify then starts with it."
|
|
23
|
+
: "Turn on Docker's start with the computer in a terminal: sudo systemctl enable docker. Slopify then starts with it.";
|
|
24
|
+
}
|
|
25
|
+
export const desktopHowTo = "With Docker Desktop: Docker Desktop → Settings → General → Start Docker Desktop when you sign in. With Docker Engine on Linux: sudo systemctl enable docker. Slopify starts whenever Docker does.";
|
|
26
|
+
/**
|
|
27
|
+
* Reads, never changes, whether Docker starts by itself. `systemctl is-enabled` only reports;
|
|
28
|
+
* it answers "enabled" or "disabled" (exit code 1), and fails without systemd.
|
|
29
|
+
*/
|
|
30
|
+
export async function checkDockerStart(exec, manager) {
|
|
31
|
+
const result = await exec("systemctl", manager === "rootless"
|
|
32
|
+
? ["--user", "is-enabled", "docker.service"]
|
|
33
|
+
: ["is-enabled", "docker.service"]);
|
|
34
|
+
const answer = result.stdout.trim().split("\n")[0]?.trim() ?? "";
|
|
35
|
+
if (["enabled", "enabled-runtime", "alias"].includes(answer))
|
|
36
|
+
return "yes";
|
|
37
|
+
if (["disabled", "masked", "masked-runtime"].includes(answer))
|
|
38
|
+
return "no";
|
|
39
|
+
return "unknown";
|
|
40
|
+
}
|
|
41
|
+
// The terminal line the installer prints after its question.
|
|
42
|
+
export function loginStartReport(record) {
|
|
43
|
+
if (record.docker === "yes")
|
|
44
|
+
return record.manager === "rootless"
|
|
45
|
+
? "Slopify starts when you log in: rootless Docker starts with your login and restarts Slopify."
|
|
46
|
+
: "Slopify starts with this computer: Docker starts at boot and restarts Slopify.";
|
|
47
|
+
if (record.docker === "no")
|
|
48
|
+
return `Docker doesn't start by itself on this computer, so neither does Slopify. ${dockerHowTo(record.manager)}`;
|
|
49
|
+
return `Slopify couldn't tell whether Docker starts by itself here (systemctl didn't answer). ${dockerHowTo(record.manager)}`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { access, realpath } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { readState } from "../docker-install/state.js";
|
|
6
|
+
import { isWsl } from "../open-browser.js";
|
|
7
|
+
import { containerLoginStart, loginStartSchema } from "./docker.js";
|
|
8
|
+
import { createNativeAutostart, nodeAutostartExec, nodeAutostartFs } from "./native.js";
|
|
9
|
+
import { createAutostartService } from "./service.js";
|
|
10
|
+
/** The switch for this process: the container's report in Docker, the login entry otherwise. */
|
|
11
|
+
export async function createAutostart(context) {
|
|
12
|
+
if (context.env.SLOPIFY_CONTAINER === "1")
|
|
13
|
+
return createAutostartService(context.db, {
|
|
14
|
+
kind: "docker",
|
|
15
|
+
record: () => context.env.SLOPIFY_DOCKER_INSTALL_STATE === undefined
|
|
16
|
+
? Promise.resolve(null)
|
|
17
|
+
: readLoginStart(containerLoginStart),
|
|
18
|
+
});
|
|
19
|
+
return createAutostartService(context.db, {
|
|
20
|
+
kind: "native",
|
|
21
|
+
native: createNativeAutostart({
|
|
22
|
+
platform: process.platform,
|
|
23
|
+
home: homedir(),
|
|
24
|
+
env: context.env,
|
|
25
|
+
wsl: isWsl(process.platform, context.env),
|
|
26
|
+
fs: nodeAutostartFs,
|
|
27
|
+
exec: nodeAutostartExec,
|
|
28
|
+
node: process.execPath,
|
|
29
|
+
entry: await stableEntry(),
|
|
30
|
+
npxCli: await npxCli(),
|
|
31
|
+
version: context.version,
|
|
32
|
+
dataDir: context.dataDir,
|
|
33
|
+
port: context.port,
|
|
34
|
+
host: context.host,
|
|
35
|
+
log: join(context.logs, "autostart.log"),
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export async function readLoginStart(path) {
|
|
40
|
+
try {
|
|
41
|
+
return await readState(path, loginStartSchema, process.getuid?.() ?? -1);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Damaged, or not the installer's: the same as not knowing.
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// This package's cli.js, unless it lives in the npx cache, which npx may clean at any time.
|
|
49
|
+
async function stableEntry() {
|
|
50
|
+
const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
51
|
+
try {
|
|
52
|
+
await access(entry);
|
|
53
|
+
const real = await realpath(entry);
|
|
54
|
+
return real.includes(`${sep}_npx${sep}`) ? null : real;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Running from source (tsx): there is no built entry to start.
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// npm's npx script beside this Node (the Windows and the Unix installation layouts).
|
|
62
|
+
async function npxCli() {
|
|
63
|
+
const nodeDir = dirname(process.execPath);
|
|
64
|
+
for (const candidate of [
|
|
65
|
+
join(nodeDir, "node_modules", "npm", "bin", "npx-cli.js"),
|
|
66
|
+
join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npx-cli.js"),
|
|
67
|
+
]) {
|
|
68
|
+
try {
|
|
69
|
+
await access(candidate);
|
|
70
|
+
// Not resolved: Homebrew's links stay put while the folders they point to change.
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Try the next layout.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// The files "Start Slopify when I log in" writes, as pure text. Nothing here touches the disk,
|
|
2
|
+
// so every platform's output is tested on any machine (`launcher.test.ts`).
|
|
3
|
+
//
|
|
4
|
+
// The login entry never names the npx cache: npx may clean it or move it on its next run. It
|
|
5
|
+
// runs a small launcher kept in the data folder instead, which starts, in order:
|
|
6
|
+
// 1. the installed Slopify entry, when Slopify runs from a real install (npm i -g, a local
|
|
7
|
+
// checkout), not from the npx cache;
|
|
8
|
+
// 2. npx's own script next to this Node, asking for the version that was running when the
|
|
9
|
+
// switch was turned on (`@gentbajko/slopify@<version>`), so a login works offline once
|
|
10
|
+
// that version is in the cache and never jumps to a new release by itself;
|
|
11
|
+
// 3. plain `npx` from PATH, the same way.
|
|
12
|
+
// In-app updates still apply: the started version forwards to the newest one installed in the
|
|
13
|
+
// data folder (`updater/forward.ts`). Each start with the switch on writes the launcher again,
|
|
14
|
+
// so it follows the Node, the entry and the version actually in use.
|
|
15
|
+
export const packageName = "@gentbajko/slopify";
|
|
16
|
+
export const launcherMarker = "Written by Slopify: Settings";
|
|
17
|
+
export const desktopMarker = "X-Slopify-Autostart=true";
|
|
18
|
+
export const agentLabel = "stream.slopify.app";
|
|
19
|
+
export const runKey = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
20
|
+
export const runValue = "Slopify";
|
|
21
|
+
// The flags a login start runs with: quiet (no browser tab), and the data folder, port and
|
|
22
|
+
// host of the Slopify that turned the switch on, since no terminal is there to pass them.
|
|
23
|
+
export function launchArgs(spec) {
|
|
24
|
+
return [
|
|
25
|
+
"--no-open",
|
|
26
|
+
"--data-dir",
|
|
27
|
+
spec.dataDir,
|
|
28
|
+
"--port",
|
|
29
|
+
String(spec.port),
|
|
30
|
+
...(spec.host === "127.0.0.1" ? [] : ["--host", spec.host]),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
// POSIX sh single quotes: nothing inside is special except the quote itself.
|
|
34
|
+
export function shQuote(value) {
|
|
35
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
36
|
+
}
|
|
37
|
+
export function shellLauncher(spec) {
|
|
38
|
+
const args = launchArgs(spec).map(shQuote).join(" ");
|
|
39
|
+
const pkg = shQuote(`${packageName}@${spec.version}`);
|
|
40
|
+
const lines = [
|
|
41
|
+
"#!/bin/sh",
|
|
42
|
+
"# Starts Slopify quietly when you log in: no browser tab, open it from your bookmark.",
|
|
43
|
+
`# ${launcherMarker} → General → "Start Slopify when I log in".`,
|
|
44
|
+
"# Turning that switch off deletes this file and the login entry that runs it.",
|
|
45
|
+
`exec >>${shQuote(spec.log)} 2>&1`,
|
|
46
|
+
`echo "$(date) Starting Slopify at login"`,
|
|
47
|
+
`NODE=${shQuote(spec.node)}`,
|
|
48
|
+
'[ -x "$NODE" ] || NODE=$(command -v node) || {',
|
|
49
|
+
` echo "Slopify could not start at login: Node.js was not found. Install Node.js from https://nodejs.org, then start Slopify once with npx ${packageName}."`,
|
|
50
|
+
" exit 1",
|
|
51
|
+
"}",
|
|
52
|
+
'PATH=$(dirname "$NODE"):$PATH',
|
|
53
|
+
"export PATH",
|
|
54
|
+
];
|
|
55
|
+
if (spec.entry !== null)
|
|
56
|
+
lines.push(`if [ -f ${shQuote(spec.entry)} ]; then exec "$NODE" ${shQuote(spec.entry)} ${args}; fi`);
|
|
57
|
+
if (spec.npxCli !== null)
|
|
58
|
+
lines.push(`if [ -f ${shQuote(spec.npxCli)} ]; then exec "$NODE" ${shQuote(spec.npxCli)} --yes ${pkg} ${args}; fi`);
|
|
59
|
+
lines.push(`exec npx --yes ${pkg} ${args}`, "");
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
// cmd.exe: inside double quotes only `%` still expands, so it is doubled. A path can't hold a
|
|
63
|
+
// double quote on Windows, and `windowsPathProblem` refuses the rest before anything is written.
|
|
64
|
+
function cmdQuote(value) {
|
|
65
|
+
return `"${value.replaceAll("%", "%%")}"`;
|
|
66
|
+
}
|
|
67
|
+
export function cmdLauncher(spec) {
|
|
68
|
+
const args = launchArgs(spec)
|
|
69
|
+
.map((arg) => (/^[\w.:-]+$/u.test(arg) ? arg : cmdQuote(arg)))
|
|
70
|
+
.join(" ");
|
|
71
|
+
const pkg = `${packageName}@${spec.version}`;
|
|
72
|
+
const log = `>>${cmdQuote(spec.log)} 2>&1`;
|
|
73
|
+
const nodeDir = spec.node.slice(0, Math.max(spec.node.lastIndexOf("\\"), 0));
|
|
74
|
+
const lines = [
|
|
75
|
+
"@echo off",
|
|
76
|
+
"rem Starts Slopify quietly when you log in: no browser tab, open it from your bookmark.",
|
|
77
|
+
`rem ${launcherMarker} > General > "Start Slopify when I log in".`,
|
|
78
|
+
"rem Turning that switch off deletes this file and the login entry that runs it.",
|
|
79
|
+
"rem Closing this window stops Slopify.",
|
|
80
|
+
"chcp 65001 >nul",
|
|
81
|
+
"title Slopify",
|
|
82
|
+
`set "NODE=${spec.node.replaceAll("%", "%%")}"`,
|
|
83
|
+
`set "PATH=${nodeDir.replaceAll("%", "%%")};%PATH%"`,
|
|
84
|
+
'if not exist "%NODE%" set "NODE=node"',
|
|
85
|
+
];
|
|
86
|
+
if (spec.entry !== null)
|
|
87
|
+
lines.push(`if not exist ${cmdQuote(spec.entry)} goto npxcli`, `"%NODE%" ${cmdQuote(spec.entry)} ${args} ${log}`, "exit /b");
|
|
88
|
+
lines.push(":npxcli");
|
|
89
|
+
if (spec.npxCli !== null)
|
|
90
|
+
lines.push(`if not exist ${cmdQuote(spec.npxCli)} goto npx`, `"%NODE%" ${cmdQuote(spec.npxCli)} --yes ${pkg} ${args} ${log}`, "exit /b");
|
|
91
|
+
lines.push(":npx", `call npx --yes ${pkg} ${args} ${log}`, "");
|
|
92
|
+
return lines.join("\r\n");
|
|
93
|
+
}
|
|
94
|
+
// The registry's Run value: a minimised console running the launcher, so a login shows a
|
|
95
|
+
// taskbar button named Slopify rather than a window in the way. REG_SZ doesn't expand
|
|
96
|
+
// variables, and `start` takes the first quoted argument as the window title.
|
|
97
|
+
export function windowsRunCommand(launcher) {
|
|
98
|
+
return `cmd.exe /d /c start "Slopify" /min "${launcher}"`;
|
|
99
|
+
}
|
|
100
|
+
// Characters cmd.exe would read as its own inside the Run value (`%` expands even in quotes
|
|
101
|
+
// there, where a batch file's `%%` doesn't apply). Windows paths can't contain `"`.
|
|
102
|
+
export function windowsPathProblem(path) {
|
|
103
|
+
return /[%"\r\n]/u.test(path)
|
|
104
|
+
? `The Slopify data folder ${path} contains a % sign, which Windows' start-up list can't run. Start Slopify with another folder (npx ${packageName} --data-dir C:\\Slopify) and turn the switch on there.`
|
|
105
|
+
: undefined;
|
|
106
|
+
}
|
|
107
|
+
// Desktop Entry spec: an argument with spaces or reserved characters is double-quoted, with
|
|
108
|
+
// `"`, `` ` ``, `$` and `\` backslash-escaped inside; the value as a whole then escapes `\`
|
|
109
|
+
// again, and `%` is a field code unless doubled.
|
|
110
|
+
export function desktopExecArg(value) {
|
|
111
|
+
if (/^[A-Za-z0-9_./+,:@=-]+$/u.test(value))
|
|
112
|
+
return value;
|
|
113
|
+
const quoted = `"${value.replace(/["`$\\]/gu, (char) => `\\${char}`)}"`;
|
|
114
|
+
return quoted.replaceAll("\\", "\\\\").replaceAll("%", "%%");
|
|
115
|
+
}
|
|
116
|
+
// Freedesktop keys escape newlines, tabs and backslashes in plain string values.
|
|
117
|
+
function desktopString(value) {
|
|
118
|
+
return value
|
|
119
|
+
.replaceAll("\\", "\\\\")
|
|
120
|
+
.replaceAll("\n", "\\n")
|
|
121
|
+
.replaceAll("\t", "\\t")
|
|
122
|
+
.replaceAll("\r", "\\r");
|
|
123
|
+
}
|
|
124
|
+
export function desktopEntry(launcher) {
|
|
125
|
+
return [
|
|
126
|
+
"[Desktop Entry]",
|
|
127
|
+
"Type=Application",
|
|
128
|
+
"Name=Slopify",
|
|
129
|
+
desktopString(`Comment=Starts Slopify quietly when you log in. Turn it off in Slopify: Settings → General.`),
|
|
130
|
+
`Exec=/bin/sh ${desktopExecArg(launcher)}`,
|
|
131
|
+
"Terminal=false",
|
|
132
|
+
"NoDisplay=true",
|
|
133
|
+
"Hidden=false",
|
|
134
|
+
"X-GNOME-Autostart-enabled=true",
|
|
135
|
+
desktopMarker,
|
|
136
|
+
"",
|
|
137
|
+
].join("\n");
|
|
138
|
+
}
|
|
139
|
+
export function xmlText(value) {
|
|
140
|
+
return value
|
|
141
|
+
.replaceAll("&", "&")
|
|
142
|
+
.replaceAll("<", "<")
|
|
143
|
+
.replaceAll(">", ">")
|
|
144
|
+
.replaceAll('"', """);
|
|
145
|
+
}
|
|
146
|
+
// A per-user LaunchAgent: launchd loads every agent in ~/Library/LaunchAgents at login and
|
|
147
|
+
// RunAtLoad starts it once. No KeepAlive, so quitting Slopify keeps it quit until next login.
|
|
148
|
+
export function launchAgent(launcher, log) {
|
|
149
|
+
return [
|
|
150
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
151
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
152
|
+
`<!-- ${launcherMarker} → General → "Start Slopify when I log in". -->`,
|
|
153
|
+
'<plist version="1.0">',
|
|
154
|
+
"<dict>",
|
|
155
|
+
" <key>Label</key>",
|
|
156
|
+
` <string>${agentLabel}</string>`,
|
|
157
|
+
" <key>ProgramArguments</key>",
|
|
158
|
+
" <array>",
|
|
159
|
+
" <string>/bin/sh</string>",
|
|
160
|
+
` <string>${xmlText(launcher)}</string>`,
|
|
161
|
+
" </array>",
|
|
162
|
+
" <key>RunAtLoad</key>",
|
|
163
|
+
" <true/>",
|
|
164
|
+
" <key>KeepAlive</key>",
|
|
165
|
+
" <false/>",
|
|
166
|
+
" <key>ProcessType</key>",
|
|
167
|
+
" <string>Interactive</string>",
|
|
168
|
+
" <key>StandardOutPath</key>",
|
|
169
|
+
` <string>${xmlText(log)}</string>`,
|
|
170
|
+
" <key>StandardErrorPath</key>",
|
|
171
|
+
` <string>${xmlText(log)}</string>`,
|
|
172
|
+
"</dict>",
|
|
173
|
+
"</plist>",
|
|
174
|
+
"",
|
|
175
|
+
].join("\n");
|
|
176
|
+
}
|