<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Aleksandr Sakov</title>
    <description>The latest articles on Forem by Aleksandr Sakov (@sundr_dev).</description>
    <link>https://forem.com/sundr_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3849697%2Fc4a179dc-8110-4ee2-84d4-bbaed6acd301.png</url>
      <title>Forem: Aleksandr Sakov</title>
      <link>https://forem.com/sundr_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sundr_dev"/>
    <language>en</language>
    <item>
      <title>From Vibe Coding to Shipping: My Spec-Driven Workflow with Claude Code</title>
      <dc:creator>Aleksandr Sakov</dc:creator>
      <pubDate>Thu, 30 Apr 2026 13:24:40 +0000</pubDate>
      <link>https://forem.com/sundr_dev/from-vibe-coding-to-shipping-my-spec-driven-workflow-with-claude-code-50ok</link>
      <guid>https://forem.com/sundr_dev/from-vibe-coding-to-shipping-my-spec-driven-workflow-with-claude-code-50ok</guid>
      <description>&lt;p&gt;On February 2, 2025, Andrej Karpathy &lt;a href="https://x.com/karpathy/status/1886192184808149383" rel="noopener noreferrer"&gt;coined the term "vibe coding"&lt;/a&gt;: &lt;em&gt;"fully give in to the vibes, embrace exponentials, and forget that the code even exists."&lt;/em&gt; A year later, on February 4, 2026, he &lt;a href="https://thenewstack.io/vibe-coding-is-passe/" rel="noopener noreferrer"&gt;retracted it&lt;/a&gt;. The new word was "agentic engineering" — because, as he put it, the new default is that you are not writing the code directly 99% of the time. You are orchestrating agents.&lt;/p&gt;

&lt;p&gt;Twelve months. From a meme to a discipline. Most teams I talk to have not noticed the difference.&lt;/p&gt;

&lt;p&gt;In between those two tweets, &lt;a href="https://arxiv.org/abs/2507.09089" rel="noopener noreferrer"&gt;a 2025 METR study&lt;/a&gt; ran a controlled experiment on senior open-source developers using Cursor and Claude on mature codebases — the kind of code base where you know all the corners. The developers &lt;em&gt;forecasted&lt;/em&gt; AI would make them 24% faster. After completing the tasks, they &lt;em&gt;estimated&lt;/em&gt; they had been 20% faster. Measurement showed they were actually &lt;strong&gt;19% slower&lt;/strong&gt;. Senior people, on code they had been writing for years.&lt;/p&gt;

&lt;p&gt;&lt;a href="/blog/ai-powered-development-real-talk"&gt;In an earlier post&lt;/a&gt; I said AI made me 30-40% faster on my own client work. Both numbers can be true at the same time. The difference is not the tool. It is the workflow around the tool.&lt;/p&gt;

&lt;p&gt;This post is that workflow.&lt;/p&gt;

&lt;p&gt;It is not theoretical. It is what I run every day on production code at sundr. Claude Code is the engine; a small set of open-source tools are the guardrails. Together they turn AI from an enthusiastic intern who ships 70% of a feature into a senior pair who ships the other 30%.&lt;/p&gt;

&lt;p&gt;If you have ever watched Claude generate a clean-looking pull request, hit merge with a smile, and then watched the same code break three days later under real traffic — this post is for you.&lt;/p&gt;

&lt;h2&gt;AI is an amplifier&lt;/h2&gt;

&lt;p&gt;The single most useful sentence I read about AI in 2025 came from Google's &lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/announcing-the-2025-dora-report" rel="noopener noreferrer"&gt;2025 DORA report&lt;/a&gt;, which surveyed nearly five thousand developers in September: &lt;em&gt;"AI doesn't fix a team; it amplifies what's already there."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Same report: 90% of developers are now using AI at work, and over 80% report productivity gains. About 30% report little or no trust in the code AI produces. That last number is what spec-driven workflow exists to address.&lt;/p&gt;

&lt;p&gt;Three months later, Stack Overflow's &lt;a href="https://survey.stackoverflow.co/2025/ai/" rel="noopener noreferrer"&gt;2025 developer survey&lt;/a&gt; dropped a sharper data point. Developer trust in AI accuracy fell from 40% in 2024 to 29% in 2025. Forty-six percent now actively distrust it. Sixty-six percent report being frustrated by output that is &lt;em&gt;"almost right but not quite"&lt;/em&gt;. That is exactly the failure mode that destroys you in code review six months later, when the bug is yours to debug and the original prompt is long gone.&lt;/p&gt;

&lt;p&gt;Adoption is up. Trust is down. The gap between those two lines is the discipline gap. AI is going to amplify whatever workflow you already have. If your workflow is "type until it compiles," that is what you are amplifying.&lt;/p&gt;

&lt;p&gt;The rest of this post is the workflow I amplify instead.&lt;/p&gt;

&lt;h2&gt;The 70% problem and the math of compounding&lt;/h2&gt;

&lt;p&gt;Addy Osmani named the trap clearly in &lt;a href="https://addyo.substack.com/p/the-70-problem-hard-truths-about" rel="noopener noreferrer"&gt;December 2024&lt;/a&gt;: &lt;em&gt;"AI can rapidly produce 70% of a solution, but that final 30% — edge cases, security, production integration — remains as challenging as ever."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I see this in every client review. The demo works. The pull request looks clean. The tests pass — because Claude wrote the tests too, which is its own problem. And then the bug surfaces three days into production traffic. A null check that was never wired up. A timezone offset that nobody noticed. An auth header that worked in development because the dev environment was permissive. Plausible-looking code that does not actually do the thing.&lt;/p&gt;

&lt;p&gt;Why does this happen? It is not Claude being dumb. It is compounding probability.&lt;/p&gt;

&lt;p&gt;Imagine each AI decision in a non-trivial task as a step that succeeds 99% of the time. Picking the right library. Understanding the data shape. Handling the empty case. Naming the variable. Calling the API correctly. Catching the right error. A real feature has dozens of these decisions in sequence.&lt;/p&gt;

&lt;p&gt;One hundred decisions at 99% success: 0.99 to the power of 100. That is about &lt;strong&gt;37%&lt;/strong&gt;. A 37% chance the whole chain holds together end to end.&lt;/p&gt;

&lt;p&gt;This is not pessimism. It is multiplication. You can see it in your own pull requests. Most of the line-by-line code is fine. The aggregate is broken because it has to be — even at 99% per decision, a hundred-step chain fails the majority of the time.&lt;/p&gt;

&lt;p&gt;You cannot out-prompt compounding probability. You can only insert review. A workflow that catches errors at three or four checkpoints during the work — instead of one big review at the end — bends the math back in your favor. That is not bureaucracy. That is arithmetic.&lt;/p&gt;

&lt;h2&gt;CLAUDE.md — where Claude's memory lives&lt;/h2&gt;

&lt;p&gt;Every project I run with Claude Code has a file at the root called &lt;code&gt;CLAUDE.md&lt;/code&gt;. It is the first thing Claude reads on every session. It is the closest thing the AI has to memory. It is also the smallest, cheapest investment that gives you the biggest return.&lt;/p&gt;

&lt;p&gt;Per &lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;Anthropic's documented memory hierarchy&lt;/a&gt;, CLAUDE.md files cascade — there is one at &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; for your global preferences, one at the project root that everyone working on the repo sees, and optional per-directory files for module-specific rules. I commit the project root one. I do not commit my personal one.&lt;/p&gt;

&lt;p&gt;I aim for twenty to fifty lines, structured into five short sections:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;# {Project Name}

## What this is
{2-3 sentences: what the product does, what stack it runs on}

## Key directories
- src/domain/  — pure business logic, no framework imports
- src/infrastructure/  — Express, DB, external services
- tests/  — unit + integration + characterization

## Code standards
- TypeScript strict mode; type hints required
- Test framework: vitest
- Imports: external, then internal, then relative

## Common commands
- pnpm test  — run all tests
- pnpm dev   — start dev server
- pnpm build — production build

## Anti-patterns
- Do NOT use console.log — use logger from app/utils
- Do NOT mutate objects — spread {...obj, key: value}
- Never use a double-hyphen in user-facing text; use em dash with spaces
- Do NOT import infrastructure/ from domain/
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The single most important section is the last one. &lt;strong&gt;Anti-patterns with concrete &lt;code&gt;// DO NOT&lt;/code&gt; examples are read as constraints. Abstract rules are read as suggestions.&lt;/strong&gt; Claude responds to the difference. So do humans, but they have the option to forget — Claude does not. That is the leverage.&lt;/p&gt;

&lt;p&gt;Keep the file short. Long files dilute attention. If a rule does not earn its line, cut it. I have started deleting items from CLAUDE.md when I notice myself violating them — not because the rule was wrong, but because if it could not survive my own habits, Claude was never going to internalize it either.&lt;/p&gt;

&lt;p&gt;Boris Cherny — who created Claude Code — publicly says his own setup is &lt;em&gt;"surprisingly vanilla"&lt;/em&gt; and that he does not customize Claude Code much (see his &lt;a href="https://www.threads.com/@boris_cherny/post/DTBVlMIkpcm/im-boris-and-i-created-claude-code-lots-of-people-have-asked-how-i-use-claude" rel="noopener noreferrer"&gt;Threads on how he uses Claude Code&lt;/a&gt;). That tracks with what I see in practice. Most projects need twenty to fifty lines, not two hundred. The file does its job by being noticed, not by being elaborate.&lt;/p&gt;

&lt;p&gt;The compound effect is bigger than it looks. Claude starts every session with this file in its head. That is what lets it follow conventions on the first try, write commands the way you actually run them, and refuse anti-patterns without being asked. Without CLAUDE.md, the AI is a brilliant amnesiac who reintroduces himself every morning. With it, you have continuity. And continuity is what turns "works in this prompt" into "works in this codebase."&lt;/p&gt;

&lt;h2&gt;The four phases: Specify, Plan, Tasks, Implement&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://code.claude.com/docs/en/best-practices" rel="noopener noreferrer"&gt;Anthropic's recommended workflow&lt;/a&gt; for Claude Code is four phases: Explore, Plan, Implement, Commit. That is the foundation. On top of it I run a four-phase artifact discipline that turns ambiguous intent into shippable code: Specify, Plan, Tasks, Implement — with the Commit step folded into the TDD-driven commits inside each task. Same shape as EPCC, but with explicit gates between phases that mean a human reads the artifact before AI starts on the next one.&lt;/p&gt;

&lt;p&gt;The workflow pairs two open-source tools, both of which I would point a new client at on day one.&lt;/p&gt;

&lt;p&gt;GitHub's &lt;a href="https://github.com/github/spec-kit" rel="noopener noreferrer"&gt;Spec Kit&lt;/a&gt; is the artifact half. It defines the file structure: a &lt;code&gt;constitution.md&lt;/code&gt; that captures operating principles, a &lt;code&gt;spec.md&lt;/code&gt; that captures intent and acceptance criteria, a &lt;code&gt;plan.md&lt;/code&gt; that captures the technical approach, a &lt;code&gt;tasks.md&lt;/code&gt; that breaks the work into bounded units. Each artifact has its own slash command — &lt;code&gt;/speckit.specify&lt;/code&gt;, &lt;code&gt;/speckit.plan&lt;/code&gt;, &lt;code&gt;/speckit.tasks&lt;/code&gt;, &lt;code&gt;/speckit.implement&lt;/code&gt; — that asks Claude the right questions in the right order. Open source, framework-agnostic, works with Claude Code, Copilot, Cursor, and Gemini.&lt;/p&gt;

&lt;p&gt;Anthropic's official &lt;a href="https://github.com/anthropics/claude-plugins-official" rel="noopener noreferrer"&gt;superpowers&lt;/a&gt; plugin is the discipline half. It bundles a set of skills that enforce the rituals around the artifacts: &lt;code&gt;brainstorming&lt;/code&gt; insists on a design conversation before any code, &lt;code&gt;writing-plans&lt;/code&gt; decomposes work into 2-to-5-minute tasks before implementation, &lt;code&gt;test-driven-development&lt;/code&gt; enforces RED → GREEN → REFACTOR, &lt;code&gt;verification-before-completion&lt;/code&gt; demands evidence — actual command output — before I am allowed to claim something is fixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spec Kit is the &lt;em&gt;what&lt;/em&gt;. Superpowers is the &lt;em&gt;how&lt;/em&gt;. Either one alone leaks.&lt;/strong&gt; Plenty of teams use Spec Kit and still vibe-code their way through implementation because nothing is enforcing the test-first habit. Plenty of others run superpowers and skip the spec because nobody made writing one a precondition. Together, they close the gaps where AI usually wanders off.&lt;/p&gt;

&lt;p&gt;Here is what each phase actually does in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specify&lt;/strong&gt; answers &lt;em&gt;what we are building and why&lt;/em&gt;. Five components, in order: functional requirements, acceptance criteria, input/output examples, constraints, and — the one most teams skip — &lt;em&gt;out of scope&lt;/em&gt;. AI expands scope by default. The line you do not draw is the line AI will cross. I write the explicit "do not implement X, Y, Z in this feature" list before I write anything else, even if Z feels obvious.&lt;/p&gt;

&lt;p&gt;Acceptance criteria use &lt;a href="https://alistairmavin.com/ears/" rel="noopener noreferrer"&gt;EARS notation&lt;/a&gt; — short, machine-parsable lines like &lt;em&gt;WHEN the user submits a checkout form without a payment method, the system SHALL reject the order AND display a recoverable error.&lt;/em&gt; EARS was published in a 2009 IEEE paper and has nothing to do with AI; it just happens to be unambiguous, and unambiguous is what AI needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan&lt;/strong&gt; answers &lt;em&gt;what technical shape it should take&lt;/em&gt;. The output is a &lt;code&gt;plan.md&lt;/code&gt; with the architectural sketch, the data model, the integration points, the risks, and an explicit "if this fails, here is the rollback." This is the highest-leverage gate in the workflow — the last cheap moment to change direction before tasks get cut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tasks&lt;/strong&gt; breaks the plan into 30-to-60-minute units of AI work, each independently testable. Markers like &lt;code&gt;[P]&lt;/code&gt; indicate parallelizable tasks. The list is ordered. Nothing is "TBD" — if I do not know how to bound a task, the spec is wrong, not the task list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implement&lt;/strong&gt; is the only phase where AI is in the driver's seat — and only inside one task at a time, with the test running before and after every change. Specify, Plan, and Tasks all stay in human hands. The artifact is mine; the implementation is Claude's. That flip is what makes the workflow work. Every gate between phases is a place where I can stop, edit, restart, or reverse course without losing more than the last task's worth of work.&lt;/p&gt;

&lt;p&gt;Two more pieces of this discipline are worth their own section: the Claude Code modes that compound the most, and the specific class of bug that only a fresh-context reviewer can see.&lt;/p&gt;

&lt;h2&gt;Two free tactics that compound&lt;/h2&gt;

&lt;p&gt;Most of what I have described costs effort. The two tactics in this section are nearly free, and they are the ones I use the most.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;Plan Mode&lt;/strong&gt;. Claude Code has a read-only mode where the AI can see every file and run every search, but cannot edit anything, run shell commands, or write to disk. Toggle it on, paste your task, and Claude reads, plans, and writes a markdown plan back at you — without touching the codebase. Then you toggle it off and let Claude execute the plan.&lt;/p&gt;

&lt;p&gt;I run Plan Mode before every non-trivial change. It costs me sixty seconds and saves me — conservatively — twenty minutes of cleanup per week. The plan that comes back is also a great input to Spec Kit's &lt;code&gt;/speckit.plan&lt;/code&gt;, since it surfaces files and risks I had not thought through.&lt;/p&gt;

&lt;p&gt;The second tactic is &lt;strong&gt;Writer-Reviewer with &lt;code&gt;/clear&lt;/code&gt;&lt;/strong&gt;. The setup: ask Claude to do a thing. When it claims to be done, type &lt;code&gt;/clear&lt;/code&gt; to wipe the context, then in the same session ask Claude to review the diff. The reviewer is technically the same model, but with no memory of the writer's reasoning or assumptions.&lt;/p&gt;

&lt;p&gt;This catches a specific class of bugs that pure in-session review misses — cases where the writer's mental model was wrong from step one and every subsequent decision compounded the error. The writer cannot see those bugs because the writer's mental model &lt;em&gt;is&lt;/em&gt; the bug. A fresh-context reviewer comes at the diff cold and asks "wait, why is this casting &lt;code&gt;userId&lt;/code&gt; to a number when the schema says it is a UUID?"&lt;/p&gt;

&lt;p&gt;In my experience, the post-&lt;code&gt;/clear&lt;/code&gt; review catches a meaningful share of issues that in-session review misses — roughly the lift you would expect from a second pair of eyes, except the second pair never gets tired and never has Friday-afternoon brain. It is not magic. It is just a different head reading the code. &lt;strong&gt;If you only adopt one tactic from this post, adopt this one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both Plan Mode and the &lt;code&gt;/clear&lt;/code&gt; trick are also free of any plugin or extra tool. They ship with Claude Code as it stands.&lt;/p&gt;

&lt;h2&gt;Tasks sized for thirty minutes of AI work, not three hours&lt;/h2&gt;

&lt;p&gt;The single biggest lever inside the Implement phase is task size. Too small, and the overhead of starting and committing each one drowns the value. Too large, and Claude drifts: the context fills up, the test runs become unreliable, and you find yourself reviewing a sprawling diff at the end.&lt;/p&gt;

&lt;p&gt;The size that works for me is thirty to sixty minutes of AI work per task. Each task ships its own commit and is independently verifiable. A real &lt;code&gt;tasks.md&lt;/code&gt; looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;- [ ] T1: Add /api/health endpoint  [P]
- [ ] T2: Wire health check to monitoring  [P]
- [ ] T3: Write characterization test for legacy /api/order  [blocks T4]
- [ ] T4: Refactor /api/order — extract validation helper
- [ ] T5: Add e2e test for /api/order happy path
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;code&gt;[P]&lt;/code&gt; markers say which tasks can run in parallel. &lt;code&gt;[blocks Tx]&lt;/code&gt; markers express dependencies. T3 has to ship before T4 because you do not refactor what you cannot characterize.&lt;/p&gt;

&lt;p&gt;Inside each task I run a tight loop: write the failing test, watch it fail, write the minimal code to pass, watch it pass, commit. This is just TDD; the superpowers &lt;code&gt;test-driven-development&lt;/code&gt; skill enforces the cycle when I am moving fast and tempted to skip steps. The loop matters because every committed step is a known-good rollback point. If a task goes off the rails, I lose minutes, not hours.&lt;/p&gt;

&lt;p&gt;One operational rule: I watch context utilization. Once Claude's context is over about 70%, output quality falls off — the reasoning gets sloppy, the code starts repeating earlier patterns wrong. When I see that bar climb, I either run &lt;code&gt;/compact&lt;/code&gt;, start a new session, or stop. Pushing past it is wasted tokens and wasted time.&lt;/p&gt;

&lt;h2&gt;What this looks like on a real project&lt;/h2&gt;

&lt;p&gt;Make this concrete: last month I shipped a tournament-staking system for MTT Tracker, the poker analytics product I run on the side. Players sell shares of their tournament action to backers; the tracker now models the full economy — markups, settlements, swaps, staking-adjusted P&amp;amp;L.&lt;/p&gt;

&lt;p&gt;One &lt;code&gt;proposal.md&lt;/code&gt; defined motivation and scope. The &lt;em&gt;Out of Scope&lt;/em&gt; section explicitly excluded a backer-facing portal, payment integration, a staking marketplace, and tax-reporting features — none of those were "obvious" exclusions, and every one of them is something AI would happily have expanded into. Drawing those lines up front saved hours of "let me add this while we are here" detours.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tasks.md&lt;/code&gt; grew to 195 ordered, bounded items, staged across three phases: feature flag and gating, schema and calculations, then UI and analytics. Each task carried a TDD micro-loop — write the failing test, watch it fail, write the minimal code, watch it pass, commit. Six locale files updated in lockstep with every UI change because the rule was in &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part is not how fast it shipped. It is that nothing leaked outside the spec. (For more on the product itself, I wrote a longer &lt;a href="/blog/poker-analytics-saas-case-study"&gt;MTT Tracker case study&lt;/a&gt; here.)&lt;/p&gt;

&lt;h2&gt;What I would skip — and what changed in 2026&lt;/h2&gt;

&lt;p&gt;Three things this post does not recommend, even though they show up in plenty of agentic-engineering write-ups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heavyweight multi-agent teams for sub-day features.&lt;/strong&gt; Spinning up a "team" of specialized agents — backend, frontend, devil's advocate, reviewer — is genuinely useful for cross-cutting work that takes a couple of days. For a one-hour change, single-agent EPCC plus the Writer-Reviewer trick from Section 6 is faster and produces less noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Magic-word thinking budgets.&lt;/strong&gt; Throughout 2025, prompts laced with &lt;code&gt;ultrathink&lt;/code&gt; or &lt;code&gt;think harder&lt;/code&gt; were a real lever — they bumped Claude's reasoning depth. As of January 16, 2026, Anthropic deprecated those keywords. Current models manage thinking budgets adaptively. If you see them in someone's CLAUDE.md, that file has not been updated this year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "AI coding makes you faster" headline as a single number.&lt;/strong&gt; Even METR — the team that ran the original &lt;a href="https://arxiv.org/abs/2507.09089" rel="noopener noreferrer"&gt;2025 −19% study&lt;/a&gt; — &lt;a href="https://metr.org/blog/2026-02-24-uplift-update/" rel="noopener noreferrer"&gt;updated their estimate in early 2026&lt;/a&gt; to roughly −18% with wide confidence intervals and an explicit caveat that the data is "very weak evidence." The honest framing is not that AI is universally slower. It is that AI without discipline is a coin flip on mature codebases. With discipline, it is a multiplier. The headline number depends entirely on the workflow around the tool.&lt;/p&gt;

&lt;p&gt;That coin-flip-versus-multiplier framing is what ThoughtWorks calls &lt;a href="https://www.thoughtworks.com/about-us/news/2026/combat-ai-cognitive-debt-radar-v34" rel="noopener noreferrer"&gt;cognitive debt&lt;/a&gt; in their April 2026 Tech Radar. Spec-driven development is on their short list of habits that stop cognitive debt from piling up. They are right.&lt;/p&gt;

&lt;p&gt;That is also the answer to the puzzle I opened with. The same Claude can make a senior engineer 30-40% faster on one team and 19% slower on another. The model is identical. The workflow around it picks the outcome.&lt;/p&gt;

&lt;h2&gt;If you want this discipline applied to your codebase&lt;/h2&gt;

&lt;p&gt;Reading about a workflow is one thing. Running it on your actual repository — with your actual stack, your actual deadlines, and your actual technical debt — is another. If you are building a real product and would like the workflow described here applied directly to your codebase, not in theory, that is what I do at sundr.&lt;/p&gt;

&lt;p&gt;The two easiest first steps: try the &lt;a href="/calculator"&gt;project calculator&lt;/a&gt; for a quick sense of timeline and budget, or &lt;a href="/contact"&gt;book a free thirty-minute call&lt;/a&gt; and tell me what you are working on. I will give you a straight answer about whether this approach fits — and if it does not, I will tell you that too. (If you are still deciding between hiring solo and an agency, &lt;a href="/blog/solo-developer-vs-agency"&gt;my honest take on that question&lt;/a&gt; is in another post.)&lt;/p&gt;

&lt;p&gt;No hard sell. Just an experienced engineer giving you a real opinion.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>specdrivendevelopment</category>
      <category>programming</category>
    </item>
    <item>
      <title>React Native Cross-Platform Development: One Codebase for Mobile, TV, and Beyond</title>
      <dc:creator>Aleksandr Sakov</dc:creator>
      <pubDate>Fri, 17 Apr 2026 17:18:53 +0000</pubDate>
      <link>https://forem.com/sundr_dev/react-native-cross-platform-development-one-codebase-for-mobile-tv-and-beyond-30jf</link>
      <guid>https://forem.com/sundr_dev/react-native-cross-platform-development-one-codebase-for-mobile-tv-and-beyond-30jf</guid>
      <description>&lt;p&gt;Every article about React Native says the same thing: it lets you build iOS and Android apps from one codebase. That's true, but it's only half the story. I've spent 9+ years building cross-platform applications — not just for phones, but for Smart TVs, set-top boxes, and streaming platforms serving 80M+ viewers. React Native cross-platform development in 2026 means targeting 7+ platforms from a single monorepo. Here's what that actually looks like in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  React Native in 2026: More Than a Mobile Framework
&lt;/h2&gt;

&lt;p&gt;When people say "cross-platform," they usually mean iOS and Android. That definition is 5 years out of date. React Native now runs on iOS, Android, tvOS, Android TV, Fire TV, Web, Windows, macOS, and as of February 2026 — Meta Quest VR.&lt;/p&gt;

&lt;p&gt;The New Architecture is no longer optional. RN 0.82 made it mandatory — the old bridge code is physically removed from compiled binaries in 0.84. Hermes V1 is the default engine, delivering ~55% faster startup times and 26% lower memory usage compared to JavaScriptCore. Libraries that still depend on bridge APIs simply will not compile.&lt;/p&gt;

&lt;p&gt;Enterprise adoption tells the real story. Shopify reported 86% code unification with 59% faster screen loads after migrating to the New Architecture. Microsoft runs 40+ Office experiences on React Native for Windows, including parts of Copilot. Discord, Coinbase, and the Call of Duty companion app all ship on React Native.&lt;/p&gt;

&lt;p&gt;But the most significant development for cross-platform is Amazon Vega OS. Launched in October 2025, it integrates React Native at the &lt;em&gt;operating system&lt;/em&gt; level — the Hermes runtime is pre-loaded and pre-warmed by the OS, giving apps near-instant cold starts. Currently Vega ships with RN 0.72, which means maintaining a separate build configuration. But RN 0.82 support is coming in May 2026, and when it lands, we'll target Vega OS from the same monorepo as our mobile and other TV builds. That's a turning point for teams building cross-platform products.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Business Case: Why One Codebase Changes the Economics
&lt;/h2&gt;

&lt;p&gt;Let me put this in terms that matter to the person signing the check.&lt;/p&gt;

&lt;p&gt;Building a content app for 7 platforms the traditional way — native iOS, native Android, native tvOS, native Android TV/Fire TV, and a web-based Tizen (Samsung)/webOS (LG) app — means 5 codebases, potentially 5 different teams, and 5 separate bug-tracking cycles. A conservative estimate for a mid-complexity streaming app: $400K-600K and 6-9 months.&lt;/p&gt;

&lt;p&gt;With React Native and a monorepo architecture, I build one codebase that targets all five. Business logic, state management, API layer, and data models are written once. Platform-specific work — focus management for TV, touch interactions for mobile — accounts for 20-40% of the total effort. In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One senior developer instead of 5 platform specialists&lt;/li&gt;
&lt;li&gt;3-4 months instead of 6-9&lt;/li&gt;
&lt;li&gt;One test suite, one CI pipeline, one deployment process&lt;/li&gt;
&lt;li&gt;Bug fixes ship to all platforms simultaneously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ship mobile and TV apps as a &lt;a href="https://www.sundr.dev/blog/solo-developer-vs-agency" rel="noopener noreferrer"&gt;solo developer&lt;/a&gt;. That's not theoretical — it's how I work daily. &lt;a href="https://www.sundr.dev/blog/ai-powered-development-real-talk" rel="noopener noreferrer"&gt;AI-powered workflows&lt;/a&gt; handle the boilerplate, while I focus on architecture and platform-specific edge cases that require real expertise. The result: startup-speed delivery at a fraction of agency cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Code Gets Shared Between Mobile and TV (And What Doesn't)
&lt;/h2&gt;

&lt;p&gt;The "60-80% code sharing" claim gets thrown around a lot. Let me be specific.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The shared 60-80%&lt;/strong&gt; is almost entirely business logic and data. Zustand stores, API clients, data models, authentication flows, feature flags, validation — all platform-agnostic. A simplified example of a store that runs identically on phones and TVs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ContentStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;ContentItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;fetchItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useContentStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ContentStore&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;
  &lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fetchItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createJSONStorage&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This store works everywhere without a single line of platform-specific code. Same for hooks, utilities, and the entire networking layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The platform-specific 20-40%&lt;/strong&gt; is disproportionately expensive because it covers the parts that determine whether users feel the app is native or a bad port:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Focus management and spatial navigation.&lt;/strong&gt; On mobile, users tap what they want. On TV, they navigate with a D-pad. Every focusable element needs explicit spatial relationships. More on this below — it's an architectural concern, not a widget toggle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input handling.&lt;/strong&gt; Samsung Tizen's back button sends keycode &lt;code&gt;10009&lt;/code&gt;. LG webOS sends &lt;code&gt;461&lt;/code&gt;. tvOS uses a gesture-based Siri Remote. Each needs explicit handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout and sizing.&lt;/strong&gt; A phone screen is 375px wide at arm's length. A TV is 1920px wide at 3 meters. Font sizes, spacing, focus targets — everything changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animation strategies.&lt;/strong&gt; A phone with 8 GB RAM handles 10 parallel animations. A TV with 512 MB gets one, and you hope the GC doesn't stutter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ratio depends on your product. A content browsing app (movie catalogs, channel guides) pushes toward 80% shared. An interactive app with live features drops to 55-60%. Even at 55%, that's one codebase instead of five.&lt;/p&gt;

&lt;h2&gt;
  
  
  React Native vs Flutter: The TV Factor Nobody Mentions
&lt;/h2&gt;

&lt;p&gt;Every "React Native vs Flutter" comparison frames them as mobile frameworks. Flutter leads in market share — 46% to React Native's 35%. Flutter's Impeller rendering engine delivers excellent animation performance. For a mobile-only product, it's a legitimate choice.&lt;/p&gt;

&lt;p&gt;But if your product needs to run on a TV, the comparison ends before it starts.&lt;/p&gt;

&lt;p&gt;React Native has a production-grade TV ecosystem. Amazon built an entire operating system on it. DIRECTV ships their app on it. The react-native-tvos fork tracks core releases. Expo has official TV build support. Callstack publishes enterprise guides for TV development.&lt;/p&gt;

&lt;p&gt;Flutter's TV support is experimental. No official TV framework exists, no major streaming platform ships Flutter on TV, and community effort is fragmented.&lt;/p&gt;

&lt;p&gt;Kotlin Multiplatform is the more interesting comparison — it's tripled its adoption to 23% and Airbnb chose it for shared business logic. But KMP shares &lt;em&gt;logic&lt;/em&gt;, not UI. For a team that wants to share the entire component layer across mobile and TV, React Native is the only framework that delivers this in production.&lt;/p&gt;

&lt;p&gt;If you're evaluating frameworks for a multi-surface product, read my post on &lt;a href="https://www.sundr.dev/blog/smart-tv-development-challenges" rel="noopener noreferrer"&gt;what Smart TV development actually involves&lt;/a&gt; before deciding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: How One Project Targets 7+ Platforms
&lt;/h2&gt;

&lt;p&gt;Here's the monorepo structure I use for cross-platform projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── apps/
│   ├── expo/          # iOS, Android, tvOS, Android TV, Fire TV
│   └── web/           # Samsung Tizen, LG webOS
├── packages/
│   ├── platform/      # Device detection, keycodes, capabilities
│   ├── uikit/         # Focus-aware UI components
│   ├── shared/        # Hooks, state, API client, navigation
│   └── theme/         # Design tokens via React Context
└── turbo.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dependency direction is deliberate. &lt;code&gt;uikit&lt;/code&gt; depends on &lt;code&gt;platform&lt;/code&gt; and &lt;code&gt;theme&lt;/code&gt;. &lt;code&gt;shared&lt;/code&gt; depends on &lt;code&gt;platform&lt;/code&gt;. Apps depend on all four. Nothing flows backwards. When I change a keycode map, only &lt;code&gt;platform&lt;/code&gt; and its dependents rebuild — Turborepo's caching handles the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The platform package&lt;/strong&gt; is the anti-corruption layer. It abstracts the hardware zoo behind a capability-based API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Samsung Tizen keycodes&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tizenKeyCodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyCodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ENTER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;BACK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10009&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;// Samsung's unique back button&lt;/span&gt;
  &lt;span class="na"&gt;RED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;      &lt;span class="c1"&gt;// Color buttons on Samsung remote&lt;/span&gt;
  &lt;span class="na"&gt;GREEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// LG webOS keycodes — completely different&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webosKeyCodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyCodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;ENTER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;BACK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;461&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;     &lt;span class="c1"&gt;// LG's back code&lt;/span&gt;
  &lt;span class="na"&gt;RED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;GREEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We program against capabilities, not platform names. The package exposes &lt;code&gt;PlatformCapabilities&lt;/code&gt; — &lt;code&gt;hasColorButtons&lt;/code&gt;, &lt;code&gt;maxResolution&lt;/code&gt;, &lt;code&gt;hasVoiceControl&lt;/code&gt; — so the UI adapts to what the remote can actually do. Detection uses platform-native APIs first (&lt;code&gt;tizen.systeminfo&lt;/code&gt;, &lt;code&gt;webOS.platform&lt;/code&gt;, &lt;code&gt;Platform.isTV&lt;/code&gt;) with user agent parsing as a fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dual-build strategy.&lt;/strong&gt; Expo with the react-native-tvos fork handles native platforms (tvOS, Android TV). Vite with react-native-web handles web-based TVs (Tizen, webOS). Platform-specific file extensions — &lt;code&gt;.tizen.tsx&lt;/code&gt;, &lt;code&gt;.webos.tsx&lt;/code&gt;, &lt;code&gt;.web.tsx&lt;/code&gt; — are resolved at build time. The same component can have a shared version and platform-specific overrides, and the bundler picks the right one automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Gotchas: What I Learned Shipping to Phones and TVs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Focus management is architecture, not a widget.&lt;/strong&gt; This is the single biggest misconception about TV development. On mobile, navigation is solved — stack, tabs, drawer. On TV, spatial navigation is a cross-cutting concern that affects your entire application.&lt;/p&gt;

&lt;p&gt;When a user presses DOWN on a hero carousel, the focus system must calculate which item in the content rail below should receive focus. This depends on the rail's horizontal scroll position, the width of the focused hero item, and whether the rail has finished layout. Get this wrong and focus jumps to an unexpected item across the screen.&lt;/p&gt;

&lt;p&gt;Focus requires a history stack — not just current state, but the ability to restore focus on back navigation. It shapes your testing strategy: you can't test a TV app with click events, you need D-pad sequence simulation. And it's the primary performance bottleneck — traversing a grid of 200 items to find the nearest focusable neighbor must complete in under 16ms or the UI feels broken.&lt;/p&gt;

&lt;p&gt;Every component in our UIKit declares whether it's focusable, defines its spatial boundaries, and knows how to animate focus transitions. This is what separates a "React Native app on TV" from a "TV app built with React Native."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory is your invisible ceiling.&lt;/strong&gt; A phone has 6-8 GB of RAM. A Smart TV has 512 MB to 1.5 GB, and your app gets a fraction. The same React Native code that runs smoothly on an iPhone will crash a Samsung TV in 10 minutes if you're not virtualizing lists and actively managing image memory. I covered this in detail in &lt;a href="https://www.sundr.dev/blog/smart-tv-development-challenges" rel="noopener noreferrer"&gt;my post on Smart TV challenges&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The react-native-tvos fork is a managed risk.&lt;/strong&gt; Using &lt;code&gt;"react-native": "npm:react-native-tvos@..."&lt;/code&gt; as an npm alias creates upgrade friction — every Expo SDK bump needs a compatible fork release. We mitigate by pinning versions and running a dedicated upgrade validation pipeline. The ecosystem is more stable than 2 years ago (Expo SDK 54+ with RNTV 0.81 works well), but it's a cost to budget for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TV animations need different thinking.&lt;/strong&gt; Focus-driven animations must coordinate with focus state that lives in JavaScript. This means consciously choosing JS-driven animations for focus transitions — a trade-off between architectural simplicity and raw frame rate. React Native Reanimated 3.x largely solves this with native-level performance while reading JS-side state, but you need to design for it from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AI Makes This Possible for One Developer
&lt;/h2&gt;

&lt;p&gt;Shipping to 7+ platforms as a solo developer would have been absurd a few years ago. &lt;a href="https://www.sundr.dev/blog/ai-powered-development-real-talk" rel="noopener noreferrer"&gt;AI-powered development&lt;/a&gt; changed that equation.&lt;/p&gt;

&lt;p&gt;Claude, Cursor, and Copilot handle boilerplate that used to eat half my day. Scaffolding a new Zustand store, writing test suites, generating platform-specific configuration — tasks that took 30-40 minutes now take 5 minutes of review.&lt;/p&gt;

&lt;p&gt;But AI doesn't replace the expertise. It can't decide that focus management needs a history stack. It doesn't know Samsung Tizen's back button is keycode &lt;code&gt;10009&lt;/code&gt;. It won't tell you to virtualize everything because the TV's WebView gets 200 MB of RAM. That's 9+ years of domain knowledge that makes AI output useful rather than plausible-looking but subtly wrong.&lt;/p&gt;

&lt;p&gt;The combination — deep platform expertise plus AI acceleration — is what lets &lt;a href="https://www.sundr.dev/blog/solo-developer-vs-agency" rel="noopener noreferrer"&gt;one senior developer outperform agency teams&lt;/a&gt;. The expertise sets the architecture. The AI fills in the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Need a React Native App for Phones and TVs?
&lt;/h2&gt;

&lt;p&gt;If you're building a product that needs to run on both mobile and TV — a streaming service, a content platform, a live events app — this is exactly what I do. I've built &lt;a href="https://www.sundr.dev/services/ott-streaming" rel="noopener noreferrer"&gt;OTT platforms&lt;/a&gt; serving 80M+ viewers across 15+ device types and ship &lt;a href="https://www.sundr.dev/services/mobile-apps" rel="noopener noreferrer"&gt;React Native mobile apps&lt;/a&gt; with the same monorepo architecture. Whether you need a cross-platform app from scratch or want to extend an existing mobile app to Smart TV, I can give you an honest assessment.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.sundr.dev/blog/react-native-cross-platform-mobile-tv-development" rel="noopener noreferrer"&gt;sundr.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>mobile</category>
      <category>typescript</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Deploy Apps to Samsung Smart TV: The Bash Script That Saves Hours</title>
      <dc:creator>Aleksandr Sakov</dc:creator>
      <pubDate>Tue, 31 Mar 2026 16:28:17 +0000</pubDate>
      <link>https://forem.com/sundr_dev/deploy-apps-to-samsung-smart-tv-the-bash-script-that-saves-hours-175b</link>
      <guid>https://forem.com/sundr_dev/deploy-apps-to-samsung-smart-tv-the-bash-script-that-saves-hours-175b</guid>
      <description>&lt;p&gt;If you've ever deployed a web app to a Samsung Smart TV, you know the pain. Connect via sdb, resolve the device name, package the .wgt file with the right certificate, uninstall the old version, install the new one, run it, pray it doesn't crash. Every. Single. Time.&lt;/p&gt;

&lt;p&gt;After 9+ years of doing this across dozens of OTT projects  —  including platforms serving 80M+ viewers  —  I finally snapped and wrote a script that automates the entire thing.&lt;/p&gt;

&lt;p&gt;I'm sharing it here because I wish someone had shared something like this with me years ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Need Before Starting
&lt;/h2&gt;

&lt;p&gt;Before the script can do its magic, you need three things set up on your machine and your TV.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install Tizen Studio.&lt;/strong&gt; Download it from the &lt;a href="https://developer.tizen.org/development/tizen-studio/download/" rel="noopener noreferrer"&gt;official Tizen Studio page&lt;/a&gt;. You need the CLI tools  —  specifically &lt;code&gt;sdb&lt;/code&gt; and the &lt;code&gt;tizen&lt;/code&gt; command-line utility. The script expects them at &lt;code&gt;~/tizen-studio/tools/&lt;/code&gt;. After the base installation, open Package Manager, go to the Extension SDK tab, and install &lt;strong&gt;TV Extensions&lt;/strong&gt; and &lt;strong&gt;Samsung Certificate Extension&lt;/strong&gt;. The full process is described in the &lt;a href="https://developer.samsung.com/smarttv/develop/getting-started/setting-up-sdk/installing-tv-sdk.html" rel="noopener noreferrer"&gt;Samsung TV SDK installation guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Create a Samsung certificate.&lt;/strong&gt; This is the part that trips most people up. You need a signing certificate to package and install apps on a physical TV. Follow the &lt;a href="https://developer.samsung.com/smarttv/develop/getting-started/setting-up-sdk/creating-certificates.html" rel="noopener noreferrer"&gt;Samsung certificate guide&lt;/a&gt;: open Certificate Manager in Tizen Studio, create a &lt;strong&gt;Samsung&lt;/strong&gt; certificate profile (not a Tizen one), authenticate with your Samsung Developer account, and register your TV's DUID as the target device. Keep the certificate backed up  —  future updates must use the same author certificate or the TV treats them as a completely new app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Enable Developer Mode on the TV.&lt;/strong&gt; On the TV itself, go to the Apps panel, open App Settings, and enter the code &lt;code&gt;12345&lt;/code&gt;. This opens the Developer Mode popup. Toggle it on, enter your computer's IP address, and reboot the TV. After restart you'll see "Develop Mode" at the top of the Apps panel. The full walkthrough is in the &lt;a href="https://developer.samsung.com/smarttv/develop/getting-started/using-sdk/tv-device.html" rel="noopener noreferrer"&gt;Samsung device setup guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Also required: Java 8.&lt;/strong&gt; Tizen CLI tools still depend on Java 8, not newer versions. On macOS, install it with &lt;code&gt;brew install --cask temurin8&lt;/code&gt;. The script automatically detects and switches to Java 8 for the session, so it won't mess with your system Java.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Script Does
&lt;/h2&gt;

&lt;p&gt;The script gives you an interactive menu powered by &lt;a href="https://github.com/charmbracelet/gum" rel="noopener noreferrer"&gt;gum&lt;/a&gt; (a terminal UI toolkit that it auto-installs if missing). You get 8 options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run All Steps&lt;/strong&gt;  —  connect, resolve device, package, uninstall old version, install, and run. One selection, done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect to TV&lt;/strong&gt;  —  establishes sdb connection to your TV's IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve device name&lt;/strong&gt;  —  reads the device identifier from sdb, needed for all Tizen CLI commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package app&lt;/strong&gt;  —  signs your build directory as a .wgt file with your certificate. Handles filename sanitization automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uninstall app&lt;/strong&gt;  —  removes the existing version from the TV (gracefully ignores if not installed).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install app&lt;/strong&gt;  —  pushes the .wgt to the TV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debug app&lt;/strong&gt;  —  launches a debug session and forwards the debug port so you can attach Chrome DevTools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run app&lt;/strong&gt;  —  launches the app in non-debug mode.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can also pass a pre-built &lt;code&gt;.wgt&lt;/code&gt; file directly, and the script skips the packaging step entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config Persistence: Set Once, Deploy Forever
&lt;/h2&gt;

&lt;p&gt;On first run, the script asks you four things: TV IP address, certificate name, package ID, and the path to your build directory (or .wgt file). The package ID comes from your project's &lt;code&gt;config.xml&lt;/code&gt;  —  it's the combination of the &lt;code&gt;tizen:application&lt;/code&gt; package attribute, in the format &lt;code&gt;AbCdEf1234.MyApp&lt;/code&gt;. The script saves all of this to &lt;code&gt;~/.tizen_deploy_config&lt;/code&gt;, so every subsequent run just works  —  no re-entering values.&lt;/p&gt;

&lt;p&gt;Working on a different project or switched TVs? Run the script with &lt;code&gt;--clear-config&lt;/code&gt; to reset everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./tizen_deploy.sh &lt;span class="nt"&gt;--clear-config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This wipes the saved config and prompts you fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Script
&lt;/h2&gt;

&lt;p&gt;Here's the complete source. Save it as &lt;code&gt;tizen_deploy.sh&lt;/code&gt;, make it executable with &lt;code&gt;chmod +x tizen_deploy.sh&lt;/code&gt;, and run it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# PLATFORM DETECTION&lt;/span&gt;
&lt;span class="nv"&gt;OS_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;IS_MAC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
&lt;/span&gt;&lt;span class="nv"&gt;IS_LINUX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
&lt;/span&gt;&lt;span class="nv"&gt;IS_WIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false

&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OS_TYPE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;Darwin&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;IS_MAC&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  Linux&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;IS_LINUX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  MINGW&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;MSYS&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;CYGWIN&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;IS_WIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;

&lt;span class="c"&gt;# CONFIG PATH&lt;/span&gt;
&lt;span class="nv"&gt;CONFIG_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.tizen_deploy_config"&lt;/span&gt;
&lt;span class="nv"&gt;$IS_WIN&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;CONFIG_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/_tizen_deploy_config"&lt;/span&gt;

&lt;span class="c"&gt;# --clear-config ARG HANDLING&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"--clear-config"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Clearing config at &lt;/span&gt;&lt;span class="nv"&gt;$CONFIG_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFIG_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# INSTALL GUM IF NOT FOUND&lt;/span&gt;
install_gum&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; gum &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"'gum' is not installed. Installing..."&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;$IS_MAC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; brew &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Homebrew is required on macOS."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
      &lt;span class="nb"&gt;exit &lt;/span&gt;1
    &lt;span class="k"&gt;fi
    &lt;/span&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;charmbracelet/tap/gum
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nv"&gt;$IS_LINUX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; apt &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; gum
    &lt;span class="k"&gt;elif &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; dnf &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; gum
    &lt;span class="k"&gt;elif &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; pacman &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-Sy&lt;/span&gt; gum
    &lt;span class="k"&gt;else
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Unsupported package manager."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
      &lt;span class="nb"&gt;exit &lt;/span&gt;1
    &lt;span class="k"&gt;fi
  elif&lt;/span&gt; &lt;span class="nv"&gt;$IS_WIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"On Windows, install gum manually:"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  scoop install gum"&lt;/span&gt;
    &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Press Enter when installed..."&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; tizen &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Tizen CLI not found. Install Tizen Studio first."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;install_gum

&lt;span class="c"&gt;# FORCE JAVA 8 FOR TIZEN&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; /usr/libexec/java_home &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;JAVA_8_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;/usr/libexec/java_home &lt;span class="nt"&gt;-v&lt;/span&gt; 1.8 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JAVA_8_HOME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Java 8 is required. Install: brew install --cask temurin8"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JAVA_8_HOME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JAVA_HOME&lt;/span&gt;&lt;span class="s2"&gt;/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# LOAD / PROMPT CONFIGURATION&lt;/span&gt;
prompt_or_load&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;var_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;prompt_msg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;current_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="nv"&gt;$var_name&lt;/span&gt;&lt;span class="s2"&gt;="&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFIG_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt; &lt;span class="nt"&gt;-f2-&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$current_value&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$var_name&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$current_value&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$prompt_msg&lt;/span&gt;&lt;span class="s2"&gt;: "&lt;/span&gt; user_input
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$var_name&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$user_input&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFIG_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$var_name&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$user_input&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

prompt_or_load TV_IP &lt;span class="s2"&gt;"Enter TV IP address"&lt;/span&gt;
prompt_or_load CERTIFICATE_NAME &lt;span class="s2"&gt;"Enter Certificate/Profile name"&lt;/span&gt;
prompt_or_load PACKAGE_ID &lt;span class="s2"&gt;"Enter PACKAGE_ID (e.g., abc123.MyApp)"&lt;/span&gt;
prompt_or_load INPUT_PATH &lt;span class="s2"&gt;"Enter build dir or .wgt file path"&lt;/span&gt;

&lt;span class="c"&gt;# Derive behavior based on INPUT_PATH&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.wgt &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;BUILD_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;SKIP_PACKAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nv"&gt;BUILD_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PACKAGE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-f2&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.wgt"&lt;/span&gt;
  &lt;span class="nv"&gt;SKIP_PACKAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false
&lt;/span&gt;&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;SDB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/tizen-studio/tools/sdb"&lt;/span&gt;
&lt;span class="nv"&gt;TIZEN_CLI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/tizen-studio/tools/ide/bin/tizen"&lt;/span&gt;

&lt;span class="nv"&gt;TV_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"26101"&lt;/span&gt;
&lt;span class="nv"&gt;DEVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nv"&gt;DEBUG_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;

&lt;span class="c"&gt;# STEP FUNCTIONS&lt;/span&gt;
connect&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Connecting to &lt;/span&gt;&lt;span class="nv"&gt;$TV_IP&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$TV_PORT&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;
  &lt;span class="nv"&gt;CONNECT_OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$SDB&lt;/span&gt; connect &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TV_IP&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$TV_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONNECT_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eq&lt;/span&gt; &lt;span class="s2"&gt;"connected to|is already connected"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Connected."&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Failed."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONNECT_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

resolve_device&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;DEVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$SDB&lt;/span&gt; devices | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TV_IP&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$TV_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $3}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEVICE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Could not resolve device."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Device: &lt;/span&gt;&lt;span class="nv"&gt;$DEVICE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

package_app&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;$SKIP_PACKAGE&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Skipping packaging (.wgt provided)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;PACKAGE_OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$TIZEN_CLI&lt;/span&gt; package &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CERTIFICATE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; wgt &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUILD_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PACKAGE_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"Package File Location"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;PKG_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PACKAGE_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Package File Location"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'s/.*:\s*//'&lt;/span&gt; | xargs&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;NEW_BASENAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PKG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;mv&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PKG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PKG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$NEW_BASENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null
    &lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW_BASENAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Packaged: &lt;/span&gt;&lt;span class="nv"&gt;$APP_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Packaging failed."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PACKAGE_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

uninstall_app&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;$TIZEN_CLI&lt;/span&gt; uninstall &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEVICE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PACKAGE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"uninstall completed"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Uninstalled."&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Not installed, skipping."&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

install_app&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;INSTALL_OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$TIZEN_CLI&lt;/span&gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$APP_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEVICE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUILD_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTALL_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"successfully installed"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Installed."&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Install failed."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTALL_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

debug_app&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;DEBUG_OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$SDB&lt;/span&gt; shell 0 debug &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PACKAGE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;DEBUG_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEBUG_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s2"&gt;"port: [0-9]+"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEBUG_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Debug failed."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEBUG_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;$SDB&lt;/span&gt; forward &lt;span class="s2"&gt;"tcp:&lt;/span&gt;&lt;span class="nv"&gt;$DEBUG_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"tcp:&lt;/span&gt;&lt;span class="nv"&gt;$DEBUG_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Debug on port &lt;/span&gt;&lt;span class="nv"&gt;$DEBUG_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

run_app&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;RUN_OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$TIZEN_CLI&lt;/span&gt; run &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEVICE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PACKAGE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"successfully launched"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Launched."&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Launch failed."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RUN_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

run_all&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; connect&lt;span class="p"&gt;;&lt;/span&gt; resolve_device&lt;span class="p"&gt;;&lt;/span&gt; package_app&lt;span class="p"&gt;;&lt;/span&gt; uninstall_app&lt;span class="p"&gt;;&lt;/span&gt; install_app&lt;span class="p"&gt;;&lt;/span&gt; run_app&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# INTERACTIVE MENU&lt;/span&gt;
&lt;span class="nv"&gt;choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gum choose &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"Run All Steps (1-5, 7)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"1. Connect to TV"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"2. Resolve device name"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"3. Package app"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"4. Uninstall app"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"5. Install app"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"6. Debug app + forward port"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"7. Run app (non-debug)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"8. Disconnect from TV"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$choice&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="s2"&gt;"Run All Steps (1-5, 7)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; run_all &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="s2"&gt;"1. Connect to TV"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; connect &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="s2"&gt;"2. Resolve device name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; resolve_device &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="s2"&gt;"3. Package app"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; package_app &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="s2"&gt;"4. Uninstall app"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; resolve_device &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uninstall_app &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="s2"&gt;"5. Install app"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; resolve_device &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; install_app &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="s2"&gt;"6. Debug app + forward port"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; resolve_device &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; debug_app &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="s2"&gt;"7. Run app (non-debug)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; resolve_device &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; run_app &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="s2"&gt;"8. Disconnect from TV"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; disconnect &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Invalid selection."&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Done."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to Debug Your App with Chrome DevTools
&lt;/h2&gt;

&lt;p&gt;One of the most useful features of the script is remote debugging. Here's how to connect Chrome DevTools to your app running on the TV.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Close the app on the TV.&lt;/strong&gt; If the app is already running, close it first. The debug session needs to launch the app itself  —  it won't attach to an already running instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Run the debug command.&lt;/strong&gt; Launch the script and select "Debug app + forward port" from the menu. The script will output a port number  —  copy it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Open Chrome DevTools for devices.&lt;/strong&gt; In Chrome, go to &lt;code&gt;chrome://inspect/#devices&lt;/code&gt;. Make sure "Discover network targets" is checked, then click &lt;strong&gt;Configure...&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj61rnpb94g1hxdpvxrtx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj61rnpb94g1hxdpvxrtx.png" alt="Chrome DevTools Devices page with Configure button highlighted" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Add the debug port.&lt;/strong&gt; In the Target discovery settings dialog, add &lt;code&gt;localhost:{port}&lt;/code&gt; where &lt;code&gt;{port}&lt;/code&gt; is the number the script returned. Click &lt;strong&gt;Done&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx1eibhvy7hu65gab08f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx1eibhvy7hu65gab08f.png" alt="Target discovery settings with debug port added" width="600" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Inspect.&lt;/strong&gt; Your TV app should appear under Remote Target. Click &lt;strong&gt;inspect&lt;/strong&gt; on the first item in the list  —  a full Chrome DevTools window opens, connected to your app on the TV. You can inspect DOM, debug JavaScript, profile performance, and see network requests just like you would on a regular web page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibn7sjxwb60rrculqblo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fibn7sjxwb60rrculqblo.png" alt="Remote Target showing the TV app with inspect link highlighted" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Important Notes and Limitations
&lt;/h2&gt;

&lt;p&gt;A few things to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tested on macOS.&lt;/strong&gt; The script has platform detection for Linux and Windows (Git Bash), but I've only battle-tested it on macOS. Linux should work with minor adjustments. Windows via Git Bash/MSYS is experimental  —  your mileage may vary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java 8 is mandatory.&lt;/strong&gt; Tizen CLI refuses to work with Java 11+. The script handles this by temporarily switching &lt;code&gt;JAVA_HOME&lt;/code&gt; for the session only  —  your system Java stays untouched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate issues are the #1 problem.&lt;/strong&gt; If install fails with a signing error, check three things: the certificate profile name must match exactly, the TV's DUID must be registered in the distributor certificate, and the certificate type matters  —  you need a &lt;strong&gt;Samsung&lt;/strong&gt; certificate (not Tizen), and the privilege level must be correct (&lt;strong&gt;Partner&lt;/strong&gt; for apps using privileged APIs, &lt;strong&gt;Public&lt;/strong&gt; for basic apps).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gum is auto-installed.&lt;/strong&gt; The interactive menu uses &lt;a href="https://github.com/charmbracelet/gum" rel="noopener noreferrer"&gt;gum&lt;/a&gt; from Charm. If it's not installed, the script installs it via Homebrew (macOS), apt/dnf/pacman (Linux), or asks you to install it manually (Windows).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About Me
&lt;/h2&gt;

&lt;p&gt;I'm Aleksandr Sakov  —  a full-stack developer with 9+ years of experience building OTT streaming platforms, mobile apps, and web products. The platforms I've built serve 80M+ viewers across 15+ device types including Samsung Tizen, LG WebOS, Android TV, Roku, and more.&lt;/p&gt;

&lt;p&gt;If you're building a Smart TV app or any streaming product and want someone who's already solved the hard problems  —  &lt;a href="https://sundr.dev/contact" rel="noopener noreferrer"&gt;book a free 30-minute call&lt;/a&gt; or &lt;a href="https://sundr.dev/calculator" rel="noopener noreferrer"&gt;try the project calculator&lt;/a&gt; for a quick estimate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sundr.dev" rel="noopener noreferrer"&gt;sundr.dev&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/aleksandr-sakov-sundr-dev/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tizen</category>
      <category>smarttv</category>
      <category>bash</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
