Compare commits

...

4 commits

Author SHA1 Message Date
dirtydishes
f22df976e4
fix ios bundle metadata
merge bundle metadata fix so Dreamio.app has a valid CFBundleIdentifier for install.
2026-05-24 22:07:15 -04:00
508f36aa01 fix ios bundle metadata 2026-05-24 21:25:55 -04:00
4855bbcf5b add html diff tooling as dev dependency 2026-05-24 21:13:33 -04:00
bcaaee818d update beads, add PLAN.md 2026-05-24 21:08:56 -04:00
7 changed files with 686 additions and 0 deletions

2
.beads/issues.jsonl Normal file
View file

@ -0,0 +1,2 @@
{"_type":"issue","id":"dreamio-4yn","title":"Build WKWebView MVP shell","description":"Create the first Dreamio MVP implementation: a minimal iOS WKWebView wrapper around hosted Stremio Web, with configuration, launch behavior, diagnostics, and documentation for real-device viability testing.","acceptance_criteria":"App project exists; WKWebView loads hosted Stremio Web; external/new-window navigation is handled; basic diagnostics and manual test documentation exist; quality gates are run or documented.","status":"closed","priority":1,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-24T14:55:12Z","created_by":"dirtydishes","updated_at":"2026-05-24T14:59:44Z","closed_at":"2026-05-24T14:59:44Z","close_reason":"Implemented the MVP WKWebView iOS shell, added run and validation documentation, and recorded current validation limits.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"dreamio-a5b","title":"Track HTML diff rendering tooling as dev dependency","description":"Move the HTML diff rendering package into devDependencies and ignore installed Node modules so the repo tracks reproducible tooling without vendoring dependencies.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:12:07Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:12:44Z","started_at":"2026-05-25T01:12:14Z","closed_at":"2026-05-25T01:12:44Z","close_reason":"Moved @pierre/diffs to devDependencies and ignored node_modules.","dependency_count":0,"dependent_count":0,"comment_count":0}

3
.gitignore vendored
View file

@ -3,3 +3,6 @@
.dolt/ .dolt/
*.db *.db
.beads-credential-key .beads-credential-key
# Node tooling
node_modules/

View file

@ -2,6 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>

245
PLAN.md Normal file
View file

@ -0,0 +1,245 @@
# Dreamio iOS Plan
## Goal
Build **Dreamio**, a thin iOS wrapper around `stremio-web` for TestFlight, personal sideload, and private-device use. The v1 target is direct web playback only: debrid-backed HTTP/HLS sources that iOS WebKit can play without an on-device torrent engine or local streaming server.
## Product Positioning
Dreamio is not a public App Store product by default. Treat TestFlight and personal sideload as the intended distribution path until a separate App Store review strategy exists.
Avoid bundling, hardcoding, or presenting anything that implies unauthorized content access. Dreamio should behave like a web shell for user-configured Stremio flows, not a content service.
## Non-Goals
- On-device torrent engine
- Local streaming server parity with desktop
- Public App Store release in v1
- Full desktop feature parity in v1
- Large native rewrite in Swift/UIKit
- Native player bridge unless real-device testing proves it is needed
## Recommended Base
1. Use `stremio-web` as the app UI and flow source.
2. Do not start from `stremio-shell` because the desktop Qt shell does not map cleanly to iOS.
3. Keep native iOS code minimal; let the web app do most work.
4. Start with hosted `stremio-web` in `WKWebView` before investing in local bundling.
5. Pin the upstream `stremio-web` version or commit once bundling begins.
## Architecture (v1)
1. Native iOS host app using `WKWebView`.
2. Hosted `stremio-web` viability check first.
3. Bundled `stremio-web` static assets only after hosted viability passes.
4. Local `index.html` loaded inside `WKWebView`, assuming routing, storage, CORS, CSP, and auth still work from the bundled context.
5. Tiny JS <-> native bridge only for proven gaps such as fullscreen, PiP, external links, share sheet behavior, or subtitles.
6. Optional `AVPlayer` bridge only if real-device playback or subtitle behavior makes web playback unacceptable.
## Why This Is Feasible
- Direct web playback removes the hardest iOS blockers.
- No torrent/local-streaming process is needed.
- iOS WebKit can handle a practical subset of media formats, especially HLS and many MP4 streams.
- `WKWebView` can host the Stremio web flow with minimal native code if auth, storage, and media constraints are handled carefully.
## Feasibility Gates
Do not move to the next phase until the current gate passes on a real iPhone.
1. **Hosted web gate:** hosted `stremio-web` works inside `WKWebView` for login, browsing, addon flow, and library screens.
2. **Playback gate:** representative debrid-backed HLS and MP4 streams play on real iPhone/iPad hardware.
3. **Bundled app gate:** bundled static assets work without routing, storage, CORS, CSP, service worker, or auth regressions.
4. **Web-player sufficiency gate:** fullscreen, subtitles, PiP expectations, pause/resume, and background/foreground behavior are acceptable without a native player bridge.
## Hard Blockers / Stop Conditions
Pause or redesign the approach if any of these remain unresolved after focused investigation:
1. Login cannot persist reliably in `WKWebView`.
2. Direct playback fails for both HLS and MP4 on real iOS devices.
3. `stremio-web` depends on browser APIs unavailable in `WKWebView` with no practical workaround.
4. Debrid or addon flows depend on blocked origins, popups, redirects, or auth behavior that cannot be safely handled.
5. Bundled local assets introduce CORS, CSP, or storage behavior that cannot be fixed without turning Dreamio into a large fork.
## Implementation Steps
### 1) Fast viability check
1. Create a throwaway iOS app with a single `WKWebView`.
2. Point it to hosted `stremio-web` first.
3. Enable Safari Web Inspector for real-device debugging.
4. Verify login, browsing, addon install/config, catalog navigation, and library sync.
5. Test one representative playback flow before doing any local bundling.
### 2) Playback validation (critical)
1. Test known direct debrid streams on real iPhone and iPad hardware.
2. Validate at minimum:
- HLS (`.m3u8`), expected best path
- MP4, expected generally workable
- MKV/custom codecs, expected mixed or unsupported
- HEVC/H.265, especially device-generation differences
- HDR, surround audio, and embedded subtitles as likely edge cases
3. Track failures by protocol, container, codec, subtitle type, MIME/content type, HTTP status, and WebKit media error.
4. Do not track failures only by provider name; provider labels are less useful than format behavior.
### 3) Local bundle integration
1. Pin the upstream `stremio-web` version or commit.
2. Build `stremio-web` static assets.
3. Copy build output into Xcode project resources through a reproducible script or documented build step.
4. Load bundled `index.html` from the app sandbox.
5. Verify routing, static asset paths, service worker behavior, API origins, CSP, CORS, auth redirects, cookies, local storage, and IndexedDB.
6. Document any local patches separately so upstream updates remain possible.
### 4) WebKit compatibility patches
1. Handle service worker limitations by disabling, bypassing, or adapting the affected behavior if needed.
2. Fix popup and new-tab flows in `WKWebView`.
3. Confirm auth/session persistence across app relaunch.
4. Confirm local storage and IndexedDB behavior under normal iOS cache pressure.
5. Confirm touch UX, viewport sizing, safe areas, and rotation.
6. Confirm ATS/HTTPS behavior for all required remote requests.
### 5) Optional native bridge additions
Add only when real-device tests show a specific gap:
- Fullscreen controls
- Picture in Picture
- External URL handling
- Share sheet/open-in-app behavior
- Subtitle import glue
- Minimal native player surface using `AVPlayer`
## Native Bridge Decision Rule
Stay web-only if HTML `<video>` playback, fullscreen, subtitles, and PiP behavior are good enough for v1.
Add a minimal JS <-> native bridge only for specific, test-proven gaps. Do not add broad native abstractions preemptively.
Consider an `AVPlayer` bridge only if one of these is true:
1. HLS/MP4 playback is materially more reliable in `AVPlayer` than in WebKit.
2. Subtitle behavior is unacceptable in WebKit but practical with native playback.
3. PiP, backgrounding, or fullscreen behavior cannot meet the v1 bar through the web path.
## Auth, Origin, and Storage Risks
Plan for these differences between hosted web, bundled local files, and `WKWebView`:
- OAuth/login redirects may need explicit navigation handling.
- Popup/new-tab flows need a `WKUIDelegate` path.
- Cookies may not behave exactly like Safari.
- Session persistence must be tested across app relaunch.
- Local storage and IndexedDB may be affected by iOS storage pressure.
- CORS and CSP behavior may differ between HTTPS origins and bundled local files.
- Raw `file://` loading may be insufficient; a local app-origin strategy may be needed if APIs assume a web origin.
## iOS Constraints To Plan For
- ATS/HTTPS requirements
- Autoplay and user-gesture media rules
- Background/foreground transitions during playback
- PiP entitlement and behavior
- Rotation and safe-area handling
- Storage limits and cache eviction patterns
- WebKit codec/container support differences by device generation
- AirPlay behavior if users expect it
## Debugging / Diagnostics
During PoC, keep a small manual compatibility log for every playback attempt:
1. Device and iOS version
2. Hosted or bundled build
3. Stream protocol and URL type, without storing sensitive tokens
4. MIME/content type and HTTP status
5. Container and codec guess
6. Subtitle type
7. WebKit media error, console error, or native navigation error
8. Outcome: plays, partially plays, fails clearly, or fails confusingly
Use Safari Web Inspector for console, network, storage, and media debugging wherever possible.
## v1 Scope
In scope:
1. Login/account sync
2. Catalog/discovery/library navigation
3. Addon install/config flow, assuming the web path supports it in `WKWebView`
4. Debrid-backed direct stream playback for iOS-compatible formats
5. Basic subtitle, fullscreen, rotation, and pause/resume behavior
6. TestFlight or personal sideload packaging
Out of scope:
1. Torrent-backed streaming
2. Universal codec parity with desktop
3. Desktop-equivalent advanced playback features
4. Public App Store optimization or review strategy
5. Native browsing/library rewrite
## Success Criteria
1. Dreamio starts quickly and consistently on real devices.
2. Login/session survives app relaunch.
3. Addon and library flows work without major rendering or redirect bugs.
4. Core browse flows work without major rendering bugs.
5. Representative HLS and MP4 direct streams play reliably.
6. Playback handles fullscreen, rotation, pause/resume, and background/foreground transitions acceptably.
7. Unsupported formats fail clearly rather than confusingly.
8. The build and bundle process is reproducible from a pinned `stremio-web` source version.
## Test Matrix (minimum)
Devices:
- One recent iPhone
- One older iPhone
- One iPad
Core flows:
1. Cold launch -> login -> browse -> select stream -> play
2. App relaunch preserves session
3. Addon install/config flow works
4. Library/catalog navigation works
Playback:
1. HLS baseline stream
2. MP4 baseline stream
3. MKV or codec-problem stream, documented as expected failure if unsupported
4. Subtitle on/off and language switch
5. Fullscreen enter/exit and rotation
6. Pause/resume -> background -> return -> continue
7. Network switch, Wi-Fi <-> cellular/hotspot
Bundle-specific checks:
1. Hosted `stremio-web` behavior compared against bundled build
2. Routing and asset loading
3. Storage persistence
4. CORS/CSP/auth redirect behavior
5. Service worker behavior
## Delivery Strategy
1. Build the `WKWebView` PoC against hosted `stremio-web`.
2. Continue only if hosted `stremio-web` inside `WKWebView` can complete login, browse, addon flow, and at least HLS/MP4 playback on a real iPhone.
3. If playback is good enough, bundle pinned `stremio-web` assets and harden routing, storage, auth, and media behavior.
4. Package for TestFlight or personal sideload.
5. Add native bridge features only when real-device tests prove they are necessary.
## Distribution and Review Risk
Dreamio should be treated as a private/TestFlight/sideload project unless and until a separate public App Store strategy exists.
A public App Store path may require changes to addon discovery, content positioning, account flows, and review messaging. Do not assume the private/TestFlight version can ship unchanged through public review.
## Bottom Line
For direct web-only debrid playback, Dreamio as a `stremio-web` + `WKWebView` wrapper is a realistic path if the real-device feasibility gates pass. Keep v1 narrow, prove hosted-web playback first, bundle only after the web flow works, and add native code only for specific problems that WebKit cannot solve well enough.

116
bun.lock Normal file
View file

@ -0,0 +1,116 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"devDependencies": {
"@pierre/diffs": "^1.2.3",
},
},
},
"packages": {
"@pierre/diffs": ["@pierre/diffs@1.2.3", "", { "dependencies": { "@pierre/theme": "1.0.3", "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-ul83DHH1yqgGxJAw2tqQm2gDO+oQsaF82ZVocwJYfXAm2FhZyyKPTdtv6jswR4A5eF/ILPjiQxyfScMhQcofbA=="],
"@pierre/theme": ["@pierre/theme@1.0.3", "", {}, "sha512-sWHv11TMoqKxKDgTIk5VbhQjdPhs8DCcBxbjh3mRlS3YOM/OcrWoGX6MM8eBGn9cUu3M46Py0JnxsG2nJaFTuA=="],
"@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
"@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
"@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
"@shikijs/transformers": ["@shikijs/transformers@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/types": "3.23.0" } }, "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ=="],
"@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
}
}

View file

@ -0,0 +1,299 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fix iOS Bundle Identifier Install Failure</title>
<style>
:root {
color-scheme: light;
--paper: oklch(98% 0.008 270);
--ink: oklch(22% 0.026 268);
--muted: oklch(46% 0.03 268);
--line: oklch(86% 0.025 268);
--wash: oklch(94% 0.018 280);
--accent: oklch(57% 0.19 302);
--good: oklch(58% 0.14 155);
--warn: oklch(64% 0.14 65);
--code: oklch(18% 0.024 268);
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font: 16px/1.6 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif;
}
main {
width: min(980px, calc(100vw - 32px));
margin: 0 auto;
padding: 48px 0 64px;
}
header {
border-bottom: 1px solid var(--line);
padding-bottom: 28px;
margin-bottom: 34px;
}
h1, h2, h3 {
line-height: 1.15;
margin: 0;
color: var(--ink);
letter-spacing: 0;
}
h1 {
max-width: 780px;
font-size: clamp(2.1rem, 6vw, 4rem);
font-weight: 780;
}
h2 {
margin-top: 36px;
padding-top: 18px;
border-top: 1px solid var(--line);
font-size: 1.35rem;
}
h3 {
margin-top: 22px;
font-size: 1rem;
}
p, li { max-width: 74ch; }
.meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 22px;
color: var(--muted);
}
.pill {
border: 1px solid var(--line);
border-radius: 999px;
padding: 5px 10px;
background: oklch(99% 0.006 270);
font-size: 0.86rem;
}
code, pre {
font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
}
pre {
overflow: auto;
border-radius: 8px;
padding: 16px;
background: var(--code);
color: oklch(94% 0.012 270);
line-height: 1.5;
max-width: 100%;
}
.callout {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--wash);
padding: 16px 18px;
margin: 18px 0;
}
.status {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 12px;
margin: 18px 0;
}
.status div {
border: 1px solid var(--line);
border-radius: 8px;
padding: 14px;
background: oklch(99% 0.006 270);
}
.status strong {
display: block;
margin-bottom: 4px;
color: var(--accent);
}
a { color: var(--accent); }
.diff-shell {
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
background: oklch(99% 0.006 270);
margin-top: 16px;
}
.diff-fallback {
margin: 0;
border-radius: 0;
background: oklch(17% 0.018 270);
}
</style>
</head>
<body>
<main>
<header>
<h1>Fix iOS Bundle Identifier Install Failure</h1>
<div class="meta">
<span class="pill">2026-05-24</span>
<span class="pill">Issue dreamio-tnv</span>
<span class="pill">Dreamio iOS bundle metadata</span>
</div>
</header>
<section>
<h2>Summary</h2>
<p>Fixed the Xcode device install failure by adding the missing app bundle metadata to <code>Dreamio/Info.plist</code>. The key fix is <code>CFBundleIdentifier</code>, which now resolves from the target's existing <code>PRODUCT_BUNDLE_IDENTIFIER</code> build setting.</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Added <code>CFBundleIdentifier</code> to the explicit Info.plist so Xcode can identify the built app bundle.</li>
<li>Added related standard bundle keys for executable name, display name, package type, and version fields.</li>
<li>Used Xcode build setting placeholders instead of hard-coded duplicate values.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>The install error said Xcode could not get the identifier for <code>Dreamio.app</code> and suggested ensuring <code>CFBundleIdentifier</code> exists. The project already defined <code>PRODUCT_BUNDLE_IDENTIFIER = com.kell.dreamio</code>, but <code>GENERATE_INFOPLIST_FILE</code> is set to <code>NO</code>, so Xcode uses the checked-in plist as the source of truth.</p>
<p>Because the plist did not include <code>CFBundleIdentifier</code>, the processed app bundle was invalid for device installation.</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<div class="status">
<div><strong>Bundle ID</strong><code>$(PRODUCT_BUNDLE_IDENTIFIER)</code> resolves to <code>com.kell.dreamio</code>.</div>
<div><strong>Version</strong><code>$(MARKETING_VERSION)</code> resolves to <code>0.1.0</code>.</div>
<div><strong>Build</strong><code>$(CURRENT_PROJECT_VERSION)</code> resolves to <code>1</code>.</div>
</div>
<p>This keeps Debug and Release behavior aligned with the target build settings. If the app identifier changes later, it only needs to change in the Xcode project build settings.</p>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<p>The diff below follows the diffs.com guidance for patch-style diff rendering with <code>@pierre/diffs</code>. A plain preformatted fallback is included so the document remains readable offline.</p>
<div id="diff-view" class="diff-shell"></div>
<pre class="diff-fallback"><code>diff --git a/Dreamio/Info.plist b/Dreamio/Info.plist
index 9451a6a..a037c11 100644
--- a/Dreamio/Info.plist
+++ b/Dreamio/Info.plist
@@ -2,6 +2,22 @@
&lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&gt;
&lt;plist version="1.0"&gt;
&lt;dict&gt;
+ &lt;key&gt;CFBundleDisplayName&lt;/key&gt;
+ &lt;string&gt;$(PRODUCT_NAME)&lt;/string&gt;
+ &lt;key&gt;CFBundleExecutable&lt;/key&gt;
+ &lt;string&gt;$(EXECUTABLE_NAME)&lt;/string&gt;
+ &lt;key&gt;CFBundleIdentifier&lt;/key&gt;
+ &lt;string&gt;$(PRODUCT_BUNDLE_IDENTIFIER)&lt;/string&gt;
+ &lt;key&gt;CFBundleInfoDictionaryVersion&lt;/key&gt;
+ &lt;string&gt;6.0&lt;/string&gt;
+ &lt;key&gt;CFBundleName&lt;/key&gt;
+ &lt;string&gt;$(PRODUCT_NAME)&lt;/string&gt;
+ &lt;key&gt;CFBundlePackageType&lt;/key&gt;
+ &lt;string&gt;APPL&lt;/string&gt;
+ &lt;key&gt;CFBundleShortVersionString&lt;/key&gt;
+ &lt;string&gt;$(MARKETING_VERSION)&lt;/string&gt;
+ &lt;key&gt;CFBundleVersion&lt;/key&gt;
+ &lt;string&gt;$(CURRENT_PROJECT_VERSION)&lt;/string&gt;
&lt;key&gt;UIApplicationSceneManifest&lt;/key&gt;
&lt;dict&gt;
&lt;key&gt;UIApplicationSupportsMultipleScenes&lt;/key&gt;</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>Developers should be able to install and run Dreamio from Xcode without the app bundle being rejected as invalid for a missing identifier. The app's visible behavior is unchanged.</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>Ran <code>plutil -lint Dreamio/Info.plist</code>; the plist is valid.</li>
<li>Built the app with <code>DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild -project Dreamio.xcodeproj -scheme Dreamio -configuration Debug -destination 'generic/platform=iOS Simulator' -derivedDataPath /tmp/dreamio-derived build</code>; the build succeeded.</li>
<li>Read the processed bundle plist at <code>/tmp/dreamio-derived/Build/Products/Debug-iphonesimulator/Dreamio.app/Info.plist</code> and confirmed <code>CFBundleIdentifier = com.kell.dreamio</code>.</li>
<li>Closed Beads issue <code>dreamio-tnv</code>. <code>bd dolt pull</code> failed afterward with <code>Error 1105: no remote</code>, so Beads sync was blocked by local Dolt remote configuration.</li>
</ul>
<div class="callout">
<p>The first direct <code>xcodebuild</code> attempt failed because the active developer directory is <code>/Library/Developer/CommandLineTools</code>. Validation used <code>DEVELOPER_DIR</code> to point only this command at <code>/Applications/Xcode.app</code>, leaving the global machine setting unchanged.</p>
</div>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>I validated a simulator build, not a physical-device install. The original failure path should be addressed because the processed bundle now has the missing identifier.</li>
<li>The target still has an empty <code>DEVELOPMENT_TEAM</code>. If physical-device signing fails next, set the team in Xcode's Signing &amp; Capabilities tab.</li>
<li>The repo is currently on a detached <code>HEAD</code>, so push behavior may need special handling depending on the intended branch workflow.</li>
<li>Beads Dolt sync is not currently available from this worktree because <code>bd dolt pull</code> reports no remote.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>Run once on the actual iPhone from Xcode to confirm install and launch on device.</li>
<li>Consider setting a project development team if this app should install from fresh checkouts without manual signing setup.</li>
</ul>
</section>
</main>
<script type="module">
const patch = `diff --git a/Dreamio/Info.plist b/Dreamio/Info.plist
index 9451a6a..a037c11 100644
--- a/Dreamio/Info.plist
+++ b/Dreamio/Info.plist
@@ -2,6 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
+ <key>CFBundleDisplayName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(MARKETING_VERSION)</string>
+ <key>CFBundleVersion</key>
+ <string>$(CURRENT_PROJECT_VERSION)</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>`;
try {
const diffs = await import("https://esm.sh/@pierre/diffs@1.2.3");
const target = document.querySelector("#diff-view");
if (diffs.PatchDiff && target) {
const element = new diffs.PatchDiff();
element.patch = patch;
element.layout = "stacked";
target.appendChild(element);
document.querySelector(".diff-fallback").style.display = "none";
}
} catch (error) {
console.info("Diff component unavailable; showing fallback.", error);
}
</script>
</body>
</html>

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"devDependencies": {
"@pierre/diffs": "^1.2.3"
}
}