<div align="center">

```
  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 
 ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 
 ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 
   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   
   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓  
   ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  
  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  
  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  
  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓        ▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  
  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓              ▒▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  
  ▒▒▒▒▒▒▒▒▒▒▓▒▒▒▓▓▓▓▓                  ▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒  
  ▒▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓▓                      ▓▓▓▓▓▒▒▓▓▓▓▒▒▓▒▓▒  
  ▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓                        ▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓  
  ▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓▓                        ▓▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓  
  ▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓                          ▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓  
  ▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓                          ▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓ 
 ▓▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓                          ▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓ 
 ▓▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓                          ▓▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓ 
 ▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓▓                          ▓▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓ 
 ▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓▓                          ▓▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓ 
 ▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓▓                          ▓▓▓▓▓▒▒▒▓▓▓▓▓▓▓▓ 
 ▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓▓                          ▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓ 
 ▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓                          ▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓ 
 ▓▓▓▓▓▓▓▓▒▓▓▓▓▓▓▓                          ▓▓▓▓▓▓▒▓▓▓▓▓▓▓▓▓ 
 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                          ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 
 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                          ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 
 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                          ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                          ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                          ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                          ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                          ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                          ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓                            ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 
```

# Viaduct

</div>

A command-line tool that turns a Google Chrome extension into a Safari Web
Extension. It unpacks the extension, checks what Safari will refuse, rewrites the
manifest, injects a runtime shim for the `chrome.*` APIs Safari does not
implement, then runs Apple's `safari-web-extension-packager` and `xcodebuild` to
produce a signed app. The result can be loaded straight into Safari for
development or built through Xcode for TestFlight.

## What it does

### Input

Give it a `.zip`, a `.crx`, an `.xpi`, an unpacked extension directory, or a URL.
A Chrome Web Store link or a direct `.crx`/`.zip` download link is fetched for
you. The archive type comes from the file's magic bytes rather than its
extension, so a CRX someone renamed to `.zip` still works. Pass several inputs to
convert them in one run.

### Analysis

viaduct detects MV2 versus MV3 and reports what will break before it converts
anything. You get a readable `CONVERSION_REPORT.md`, and `--analyze --json`
prints the same findings as a machine-readable payload: issue counts,
`autoFixed`/`blocking` totals, a `convertible` verdict that uses the same gate as
a real conversion, the per-issue list, the permissions that were removed, and the
bundle id and name.

### Manifest rewrite

- Removes Chrome-only keys (`update_url`, `key`, `minimum_chrome_version`).
- Strips permissions Safari does not implement, such as `tabGroups`,
  `offscreen`, `sidePanel`, and `debugger`.
- Converts an MV3 service worker into a non-persistent background page, and
  forces `persistent: false` on MV2 backgrounds too, because Safari rejects a
  persistent MV3 background outright ("a manifest_version >= 3 must be
  non-persistent"). It also strips `background.type: "module"`, a known cause of
  popups that fail silently.
- Injects `browser_specific_settings.safari` with a minimum version (`15.4` by
  default, changed with `--min-safari`) and no maximum cap. An `18.*` cap hides
  the extension on Safari 18 and later, including Safari 26.
- Flags icons Safari cannot render (anything that is not PNG), `content_scripts`
  that use `world: "MAIN"` (Safari 18.4 and up only), and a missing App Store
  description.
- Flags hardcoded `chrome-extension://<id>/` URLs in JS, CSS, and HTML. Safari
  gives every install its own origin, so these need `chrome.runtime.getURL()`.
- Checks `commands` shortcuts. A chord with no primary modifier is dropped by
  Safari without a word, and ChromeOS-only modifiers like `Search` have no
  Safari equivalent.
- Checks `_locales` and `__MSG_*__` placeholders, so an unresolvable
  `name`/`description` reference does not ship as a literal placeholder string.
- Flags URL match patterns left behind in `permissions` under MV3, a common
  migration slip. Safari ignores them there; they belong in `host_permissions`.
- Validates the `version` string. A missing, non-numeric, or out-of-range version
  is rejected by Apple's `CFBundleShortVersionString` and fails the build.
- Clears `use_dynamic_url` on `web_accessible_resources`. Chrome rotates those
  URLs per session and Safari cannot serve such an entry at all, so
  `chrome.runtime.getURL()` hands back a URL that 404s. Anything loaded that way
  (a content script's injected CSS, an `<img>`, a `<link>`, an iframe `src`)
  fails without an error, which is a frequent reason an in-page panel toggles but
  never appears.

### Toolbar buttons Safari never clicks

Safari 26 does not dispatch `action.onClicked` to a converted background, and it
does not fire `commands.onCommand` either. A popup-less toolbar button that
toggles in-page UI, a sidebar or an overlay, is therefore dead on arrival.
viaduct has two ways to revive it and tries them in this order.

The first is an in-page hotkey, and it is preferred because it never involves a
popover. The toggle really happens inside the content script, in its
`runtime.onMessage` listener. The shim loads ahead of the bundle's content script
and captures those listeners; viaduct then reads, statically, the message the
`onClicked` handler sends to the tab (something like `{type:"TOGGLE_SHELL"}`) and
generates a content script that replays that message to the captured listeners
when you press a keyboard shortcut. The toggle happens with no toolbar popup and
no popover at all. The shortcut reuses a declared `commands` key when there is
one, since Safari cannot fire `onCommand` and that key is otherwise dead weight
(the inert command is removed), and falls back to `Ctrl+Shift+Y`. When this path
is wired the toolbar button is deliberately left inert, because giving it a popup
would summon Safari's un-closable popover. It needs two things: a statically
determinable message, and an extension that has content scripts.

The second path is a synthetic popup, used only when the hotkey cannot be wired.
viaduct adds a tiny transparent `default_popup` that wakes the background on
click through `runtime.getBackgroundPage()`, which is the only call that wakes a
suspended Safari background (`runtime.sendMessage` does not), then replays the
captured `onClicked` listeners inside the background realm so their
`tabs.sendMessage` actually reaches the content script, deduped down to a single
toggle. This path carries a Safari limitation you will see: a toolbar popup
always draws a popover that script cannot close, since `window.close`, `blur`,
and refocus are all ignored, so a popover flashes up and goes away on your next
interaction.

### Runtime shim

The shim is injected into content scripts and into every extension HTML page:
popup, options, side panel. It:

- Routes `storage.sync` to `storage.local`, since Safari has no iCloud sync.
- Stubs `sidePanel`, `identity`, `notifications`, `tabGroups`, `debugger`, and
  `offscreen` so evaluating a module does not throw and leave you a blank page.
  The `sidePanel` fallback opens whichever panel page the extension actually
  configured, from the manifest's `side_panel.default_path` or a
  `setOptions({path})` call, instead of guessing at a filename.
- Completes `chrome.i18n` by backfilling `detectLanguage`, `getUILanguage`, and
  `getAcceptLanguages` without clobbering Safari's native `getMessage`, so code
  calling the missing `detectLanguage` degrades to `und` rather than throwing.
- Makes keyboard-shortcut management work without `chrome://extensions/shortcuts`,
  which Safari does not have. `chrome.commands.getAll()` is rebuilt from the
  manifest so an extension's own shortcut UI has something to show, and a
  navigation to `chrome://extensions/shortcuts` or `chrome://settings` is
  swallowed instead of opening a dead tab. Shortcuts themselves are edited in
  Safari, under Settings, Extensions. The analyzer warns when the source
  hardcodes one of those links.

### Staging, packaging, install

- Side-panel pages wired up as the action popup get sized automatically, so the
  popup is not a tiny collapsed window.
- Staging copies a clean tree and drops dev cruft (`*.map`, `*.ts`, `README`,
  lockfiles, store metadata) while keeping any file the manifest declares as a
  runtime asset. A web-accessible `LICENSE.txt` or a deliberately served `.map`
  will not go missing and 404 in Safari.
- The extension is packaged into an Xcode project, bundle identifiers are
  patched, and the app is optionally built ad-hoc or team-signed.
- The bundle identifier is verified on the compiled `.appex` rather than on the
  project files, so the wrong extension never gets registered with Safari.
- With `--install` the built host app is moved into `~/Applications`, with no
  copy left behind, and registered with Safari. When it is team-signed it
  survives Safari restarts.

## Requirements

- macOS with a full Xcode install, not just the Command Line Tools, for the
  packaging and build steps. Both `xcrun safari-web-extension-packager` and
  `xcodebuild` come with Xcode.
- Node.js 18 or newer.
- No runtime dependencies. TypeScript is the only dev dependency.

You can check the toolchain at any time:

```
viaduct --doctor
```

## Install

```
npm install -g @magicelk235/viaduct
viaduct <input> [options]
```

The command is `viaduct`. It is macOS only, because it needs Xcode. See
Requirements above.

## Build from source

```
npm install
npm run build
```

That compiles `src/` into `dist/`. The CLI entry point is `dist/cli.js`.

Run it directly with Node:

```
node dist/cli.js <input> [options]
```

Or link it as a global command:

```
npm link
viaduct <input> [options]
```

## Usage

Convert straight from a Chrome Web Store link and let viaduct download the CRX:

```
viaduct "https://chromewebstore.google.com/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm"
```

A direct `.crx` or `.zip` URL works the same way:

```
viaduct "https://example.com/my-extension.crx"
```

Analyze an extension and report the issues without converting it:

```
viaduct ./my-extension.zip --analyze
```

Issues are tagged so you can tell what needs your attention from what the
converter already dealt with. `[auto-fixed]` means the manifest rewrite resolves
it and there is nothing for you to do. `[shimmed]` means Safari rejects the API
or permission but the injected shim emulates it, so the feature still works, and
you only need a real migration if the shim's documented limitation matters to
you. The summary line and the `--analyze --json` payload both carry `autoFixed`
and `shimmed` counts, which are disjoint, so CI can see how much the converter
absorbed.

Stage for Safari 18's "Add Temporary Extension", which skips Xcode entirely and
is the fastest way to iterate:

```
viaduct ./my-extension.zip --temp-load
```

Then, in Safari: under Settings, Advanced, turn on "Show features for web
developers"; under Settings, Developer, turn on "Allow Unsigned Extensions";
then use the Develop menu, "Add Temporary Extension", and pick the staged
folder. Temporary extensions have to be re-added every time Safari restarts.

Generate an Xcode project without building it:

```
viaduct ./my-extension.zip --no-build
```

Full conversion with an ad-hoc build, using a clean copy that is safe for CI and
TestFlight:

```
viaduct ./my-extension.zip --ci
```

Without `--ci`, resources are symlinked instead, so your edits show up live
during development. Use `--ci` to clean-copy them into the project.

Convert several extensions in one go:

```
viaduct ./one.zip ./two.crx ./unpacked-dir
```

Batch runs give each extension its own default `./<App>_Safari` output, so
`--output`, `--report`, `--app-name`, `--bundle-id`, and `--json` are
single-extension flags and are rejected here.

## Options

```
-o, --output <dir>       Output directory (default: ./<AppName>_Safari)
    --bundle-id <id>     Reverse-DNS bundle id (default: com.viaduct.<app>)
    --app-name <name>    Host app name (default: extension name)
    --min-safari <ver>   Safari strict_min_version (default: 15.4; use 18.4 for world:MAIN)
    --platforms <p>      all | macos | ios            (default: macos)
    --ci                 Clean-copy resources (CI/TestFlight-safe)
    --temp-load          Stage only, for Safari 18 "Add Temporary Extension"
    --zip                Also emit a distributable .zip of the staged extension
    --clean              Wipe the output directory before staging
    --no-build           Generate the Xcode project but do not run xcodebuild
    --open-xcode         Open the generated .xcodeproj in Xcode when done
    --install            Install the built app to ~/Applications + register w/ Safari
    --verify             After --install, check Safari registered/enabled it
    --install-dir <dir>  Install target directory (default: ~/Applications)
    --uninstall <name>   Remove the installed <name>.app + unregister it
    --no-safari-restart  With --install, don't quit/relaunch Safari or set the toggle
    --background-launch  With --install, launch the host app hidden: the extension
                         still registers, but no window opens over your work
    --team [<id>]        Sign with an Apple Team ID. --team auto (or plain
                         --install) auto-detects it from Xcode, a provisioning
                         profile, or your signing certificate. Omit for ad-hoc.
    --no-shim            Do not generate/inject the compatibility shim
    --no-oauth-bridge    Do not wire the Safari OAuth/externally_connectable bridge
    --keep-module        Keep background.type:"module" (default strips it)
    --debug              Emit the shim with debug tracing enabled. Traces persist
                         to a bounded ring buffer (last 2000 entries) in
                         storage.local under __viaduct_debug_log__; read it with
                         --logs. Dev builds only — never ship a --debug build.
    --force              Convert despite blocking errors
    --strict             Treat warnings as blocking too (CI gate). With --analyze,
                         exit 1 if any warning or error is present.
    --analyze            Analyze and report only (also previews the manifest rewrites)
    --json               With --analyze, print a machine-readable JSON report
    --report <file>      With --analyze, also write the report to <file>
                         (.json if --json, else Markdown)
    --config <file>      Load defaults from <file> (default: ./viaduct.config.json
                         if present). JSON keyed by long-flag name; CLI flags win.
    --doctor             Verify xcrun/packager/xcodebuild availability
    --list               List Safari Web Extensions registered with pluginkit
    --logs <name>        Dump the persisted debug log of an installed --debug
                         build. <name> matches the app name or bundle id; reads
                         Safari's on-disk storage, so Safari can stay open.
-q, --quiet              Suppress progress messages (warnings/errors still print)
-v, --verbose            Verbose output
-h, --help               Show this help
    --version            Print the viaduct version and exit
```

## Installing a built app

Easiest is to let the tool do it. It moves the built app into `~/Applications`,
leaving no duplicate behind, registers it with LaunchServices, and launches it
once so Safari picks up the extension:

```
viaduct ./my-extension.zip --install
```

Then enable the extension in Safari, under Settings, Extensions.

To remove an app you installed earlier, which unregisters it from LaunchServices
and deletes it from the install directory:

```
viaduct --uninstall <AppName>                       # ~/Applications
viaduct --uninstall <AppName> --install-dir <dir>   # custom directory
```

### Persisting across Safari restarts (team signing)

How long the extension sticks around depends on how it was signed.

Ad-hoc, with no `--team`, means Safari only loads it while "Allow Unsigned
Extensions" is on in the Develop menu, and that setting resets every time Safari
restarts. With `--install` the tool flips the toggle and bounces Safari for you;
pass `--no-safari-restart` if you would rather it did not.

Team-signed, with `--team`, uses a real Apple Developer certificate. Safari loads
the extension without the unsigned toggle and it survives quitting Safari.

`--team auto`, which is also what plain `--install` does, finds your Team ID in
Xcode so you never have to know or type it:

```
viaduct ./my-extension.zip --install            # auto-detects the team
viaduct ./my-extension.zip --install --team auto # same, explicit
viaduct ./my-extension.zip --install --team V8K8L3ZSD5  # exact id
```

Auto-detection reads the team Xcode cached (`IDEProvisioningTeamByIdentifier` or
`IDEProvisioningTeams`, in both the `com.apple.dt.Xcode` and
`com.apple.dt.xcodebuild` domains) plus the codesigning identities in your
keychain, then uses the provisioning profiles on disk to choose between them,
newest first, so when there are several the team you provisioned for most
recently wins. Each source covers a few layouts, because which preference key
gets written, where profiles live, and what your certificate is called all vary
with the Xcode version. A team id that appears *only* in a profile is ignored:
profiles outlive the account that installed them, and handing xcodebuild a team
you have no account for fails every build. The team id is all the build needs,
since xcodebuild runs with `-allowProvisioningUpdates` and Xcode mints the
development certificate itself. If nothing usable turns up, the run tells you and
falls back to ad-hoc signing.

Signing is not taken on trust. When a team does reach xcodebuild, the signature
is read back off the built app and checked, so a build that quietly came out
ad-hoc fails instead of handing you an extension that vanishes the next time
Safari quits. An ad-hoc fallback that was announced up front is a warning rather
than a failure, since you asked for "a team if there is one" and there wasn't
one. If an auto-detected team turns out not to be able to sign, an expired
certificate for instance, the build is retried ad-hoc so the conversion still
finishes. A team you named yourself with `--team <id>` fails instead, because
that was a deliberate request.

A free personal Apple team works, but its provisioning profile expires roughly
every 7 days, so re-run the command to re-sign. A paid Developer Program account
lasts about a year.

If you would rather install by hand, copy the app path the run prints:

```
cp -R "<AppName>_Safari/<AppName>.app" ~/Applications/
open "~/Applications/<AppName>.app"
```

## Tested extensions

Dark Reader, Claude in Chrome, and TWP (Translate Web Pages) are verified working
on the current build. Cloaked gets as far as a real sign-in window, which is
where testing stopped for want of an account. Grammarly, LastPass, and several
others were verified in an earlier round and are due a re-check. The wiki keeps
the full list, including what was actually exercised in Safari and what is still
untested: [Tested Extensions][tested].

[tested]: https://github.com/magicelk235/Viaduct-CLI/wiki/Tested-Extensions

## Limitations

- **Ad and content blockers built on blocking `webRequest`, full uBlock Origin
  among them, cannot block network requests in Safari.** WebKit decides each
  request before extension JavaScript runs and ignores the blocking return
  value. No shim can change that, because the decision happens below JS. The
  extension still installs and its cosmetic and element-hiding features still
  run, and viaduct reports this class of extension as an `error`. For real ad and
  tracker blocking on Safari, convert the extension's `declarativeNetRequest`
  build instead, for example **uBlock Origin Lite** (uBOL). Safari honors DNR
  rulesets, so uBOL blocks for real once converted.
- APIs with no Safari equivalent are stubbed so the extension loads, but the
  feature behind them does not work. The analyzer reports each one with a
  suggested remediation.
- Extensions that authenticate with `chrome.identity` or a hardcoded
  `chrome-extension://` OAuth redirect cannot complete login. The OAuth client is
  registered on the provider's server against the original Chrome extension
  identity and scheme, which Safari cannot reproduce. Fixing it needs the
  provider to register a Safari redirect or a hosted HTTPS callback flow, and it
  is not something conversion alone can do.
- `storage.sync` is mapped to `storage.local`. Data persists, but it does not
  sync across devices.
- Native messaging (`connectNative` and `sendNativeMessage`) has no Chrome-style
  host manifest or host binary in Safari; messages route to the containing macOS
  app instead. The analyzer flags it, and you implement the response in the app's
  `SafariWebExtensionHandler` (`beginRequest`).
- `declarativeNetRequest` `modifyHeaders` rules are dropped, from static rulesets
  and from dynamic `updateSessionRules`/`updateDynamicRules` calls alike. Safari
  accepts such a rule and then never applies it (verified against a live server,
  with a `block` rule in the same call blocking correctly), and one header name off
  WebKit's allowlist makes the whole update throw and takes the working rules with
  it. Spoofing `user-agent`/`referer`, stripping `Cookie`, a CORS bypass or a
  response-header rewrite all need a native-messaging proxy instead.
  The tool also warns when static rulesets use `regexFilter`, since Safari
  supports only a limited regex subset and silently drops rules it cannot
  compile, and when the number of enabled rules exceeds what Safari honors, since
  the overflow is ignored.

## License

Licensed under the [PolyForm Shield License 1.0.0](LICENSE). Copyright (c) 2026
Yehonatan Cohen (magicelk235). You may freely use, modify, and share it, but you
may not use the Viaduct CLI to build a product that competes with it.
