Skip to content

CLI Reference

magicelk235 edited this page Jul 22, 2026 · 7 revisions

CLI Reference

viaduct is the command-line interface that converts a Chrome extension into a Safari Web Extension. This page documents every flag, its default, the standalone modes, exit codes, and copy-paste recipes.

All flags on this page are parsed in src/cli.ts via Node's node:util parseArgs. Where the README's ## Options block and cli.ts disagree, this page follows cli.ts and calls out the discrepancy.

Synopsis

viaduct <input> [options]
viaduct <in1> <in2># batch: several inputs in one run
viaduct <input> --analyze          # report only, no conversion
viaduct --doctor                   # check local toolchain
viaduct --list                     # list registered Safari Web Extensions
viaduct --uninstall <AppName>      # remove a previously installed app

What <input> accepts

<input> is a positional argument. It may be any of:

Input Notes
.zip archive Extracted with macOS-native ditto (falls back to unzip).
.crx (Chrome) The CRX container is parsed (v2 and v3 headers) and the embedded ZIP is unpacked. The Chrome extension id is recovered from the CRX public key and injected as manifest.key (34d4724).
.xpi (zip-based) Treated as a ZIP.
Unpacked extension directory The folder holding manifest.json is passed through directly (no extraction; xattr -cr is not run on your source tree).
Chrome Web Store URL e.g. https://chromewebstore.google.com/detail/<name>/<id> (legacy chrome.google.com/webstore/detail/… also works). The 32-char extension id is pulled from the URL and the .crx is fetched from Google's clients2 CRX endpoint.
Direct .crx / .zip download URL http(s) link to the package itself; downloaded and detected by magic bytes.

Magic-byte detection. Archive type is sniffed from the file's leading bytes (Cr24 → CRX, PK\x03\x04 → ZIP), not from the file extension — so a CRX renamed to .zip, or a zip-based .xpi, still converts. The suffix is only a fallback when the bytes are inconclusive. (Verified in src/input/extract.ts sniffArchiveKind / extractExtension, and src/input/download.ts inferKind for downloads.)

For URLs, if the endpoint returns HTML (an error page or captcha) or an empty body, viaduct fails with an actionable message instead of writing a broken archive. Very large or policy-gated Web Store extensions that the on-demand CRX endpoint declines will report that explicitly — download the .crx manually and pass the local path.

Batch. Passing more than one input converts each independently; one bad input does not abort the rest, and the run exits non-zero if any input failed (33e9bce). See Batch mode and exit codes below.

Security: extraction rejects archives containing symlinks and any entry that escapes the extraction directory (zip-slip guard) in assertNoPathEscape.

Options

Grouped logically. Every flag below is parsed in cli.ts. Booleans default to false unless noted.

Input / Output

Flag Alias Argument Default Description
--output -o <dir> ./<AppName>_Safari Output directory. Default is <AppName>_Safari under the current working directory (src/convert.ts outputDir).
--bundle-id <id> com.viaduct.<app> Reverse-DNS bundle id for the host app. Default derived by defaultBundleId (com.viaduct.<slug>, leading digits stripped; a hash suffix when the name is all-symbol/non-Latin so distinct names stay distinct). Validated: letters/digits/hyphens, dot-separated, each segment starts with a letter, 2+ segments — invalid ids exit 2.
--app-name <name> extension's name Host app name. Sanitized (deriveAppName) to letters/digits/-/_.
--min-safari <ver> 15.4 Safari strict_min_version. Use 18.4 for world:MAIN content scripts. Validated as 1–3 dot-separated integers (e.g. 15.4); invalid exits 2. Default is DEFAULT_MIN_SAFARI_VERSION in src/manifest/manifest.ts.
--platforms all | macos | ios macos Target platform(s). Anything else exits 2.

README discrepancy: the README's ## Options block lists the --min-safari default as 15.4 literally, which matches cli.ts (the help text interpolates DEFAULT_MIN_SAFARI_VERSION = "15.4"). No conflict, but the source of truth is the constant, not the hard-coded README string.

Build modes

Flag Argument Default Description
--ci off Clean-copy resources into the generated project instead of symlinking them. Use for CI / TestFlight. Default (off) symlinks resources so you can live-edit extension files and reload in Safari (src/convert.ts: copyResources: values.ci).
--temp-load off Stage the extension only — no Xcode project, no build. Produces a folder + instructions for Safari 18's Develop → Add Temporary Extension… (src/build/tempload.ts). Cannot be combined with --install (exits 2).
--zip off Also emit a distributable .zip of the staged extension (<AppName>_SafariExtension.zip in the output dir).
--clean off Wipe the output directory before staging, dropping stale leftovers (a40114e).
--no-build off (builds) Generate the .xcodeproj but do not run xcodebuild. Cannot be combined with --install (exits 2).
--open-xcode off Open the generated .xcodeproj in Xcode when done (a40114e).

Signing & Install

Flag Argument Default Description
--install off Install the built app to the install dir and register it with Safari. Requires a build (rejects --no-build / --temp-load, exits 2). Targets macOS — rejects --platforms ios (exits 2). Plain --install with no --team triggers Xcode team auto-detection (see --team).
--uninstall <name> Standalone mode — remove the installed <name>.app and unregister it. Honors --install-dir. See Standalone modes.
--install-dir <dir> ~/Applications Install / uninstall target directory (src/build/installer.ts; ~ is expanded).
--team <id> ad-hoc / auto with --install Sign with a 10-char Apple Developer Team ID → real signing, so the extension persists across Safari quits (no unsigned toggle). --team auto (or plain --install) auto-detects the team from Xcode (detectXcodeTeam); if none found it warns and falls back to ad-hoc. Omit entirely for ad-hoc signing. Free personal teams expire in ~7 days — re-run to re-sign. Validated: exactly 10 uppercase alphanumerics or the literal auto (else exit 2). (a40114e)
--verify off After --install, check that Safari registered the extension and that it is enabled (src/build/verify.ts). Requires --install (exits 2 otherwise). A registered-but-disabled or unregistered result makes the run exit non-zero; "enabled unknown" is best-effort and not a failure (33e9bce).
--no-safari-restart off (restarts) With --install, do not quit/relaunch Safari and do not set the "Allow Unsigned Extensions" toggle.

README discrepancy: the README shows --team [<id>] (bracketed optional argument). In cli.ts, team is { type: "string" } — the value is not optional at the parser level: --team must be followed by a value (auto or a team id). "Plain --install auto-detects" is achieved by omitting --team, not by a bare --team. Also, the README's Options block omits --verify (it exists and is parsed).

Conversion toggles

Flag Argument Default Description
--no-shim off (shim on) Do not generate/inject the compatibility shim (src/convert.ts: generateShim: !values["no-shim"]). See Runtime Shim.
--no-oauth-bridge off (bridge on) Do not wire the Safari OAuth / externally_connectable bridge (src/runtime/oauth-bridge.ts).
--keep-module off (strips it) Keep background.type: "module" instead of stripping it. Also affects --analyze (the preview honors this, so the previewed manifest matches an actual convert).
--force off Convert despite blocking errors. Without it, a blocking issue count > 0 aborts with a non-zero exit (src/convert.ts).
--strict off Treat warnings as blocking too (CI gate). Changes countBlocking to also count warning-severity issues (src/analyze/report.ts countBlocking). With --analyze, exits 1 if any warning/error remains.

Analysis

Flag Argument Default Description
--analyze off Analyze and report only — no conversion. Prints issues and previews the exact manifest rewrites the converter would apply (side-effect-free transformManifest).
--json off With --analyze, print a machine-readable JSON report. Only valid with --analyze (else exit 2). In JSON mode, even a corrupt archive / missing manifest emits parseable {"error": …, "convertible": false} rather than a stack trace.
--report <file> With --analyze, also write the report to <file> — JSON if --json, else Markdown. Only valid with --analyze (else exit 2).

Diagnostics

Flag Alias Default Description
--doctor Standalone mode — verify local toolchain (xcrun, safari-web-extension-packager, xcodebuild, plutil, pluginkit, ditto, osascript, lsregister). See Standalone modes.
--quiet -q off Suppress progress messages. Warnings and errors still print. Ignored when -v is also present (setQuiet(quiet && !verbose)).
--verbose -v off Verbose output.

Meta

Flag Alias Default Description
--config ./viaduct.config.json if present Load defaults from a JSON file keyed by long-flag name. CLI flags override config; explicit path that doesn't exist exits 2. See Config file. (33e9bce)
--list Standalone mode — list Safari Web Extensions registered with this user (via pluginkit). (33e9bce)
--help -h Print help and exit 0.
--version Print the viaduct version and exit 0.

Standalone modes

These modes run before the conversion path and do not take an input archive the usual way. They are checked in main() in this order: --version, --help, --doctor, --list, --uninstall. The first one present wins.

Mode What it does Exit behavior
--version Prints the package version (or unknown if unreadable). Always 0.
--help / -h Prints the full usage/help text. Always 0.
--doctor Runs the toolchain checks listed under Diagnostics; prints ok/fail per tool with an install hint. 0 if all checks pass, 1 if any fail.
--list Lists registered Safari Web Extensions (bundle id + path); prints a friendly note if none. Always 0.
--uninstall <name> Removes <name>.app from the install dir and unregisters it from Safari (uninstallFromSafari). Honors --install-dir. An empty name exits 1. 0 on success, 1 on failure.

Config file (--config)

viaduct reads defaults from ./viaduct.config.json automatically if present, or from an explicit --config <file> (33e9bce). The file is JSON keyed by long-flag name and reads exactly like the CLI:

{
  "bundle-id": "com.example.myext",
  "min-safari": "18.4",
  "team": "auto",
  "ci": true
}
  • Precedence: a flag the user typed on the CLI always wins; config fills the rest. (Provenance is detected by scanning argv, including the -o short alias for --output.)
  • Allowed keys (CONFIG_KEYS): output, bundle-id, app-name, min-safari, platforms, ci, zip, no-build, open-xcode, install, install-dir, no-safari-restart, team, no-shim, no-oauth-bridge, keep-module, force, strict, verify, clean. One-shot/meta flags (analyze, doctor, list, version, json, report, config, help, quiet, verbose, uninstall, temp-load) are not persistable.
  • Type-checked: boolean keys must be a real true/false — a string like "false" is rejected (it would be truthy in JS and silently flip the flag on). Unknown keys warn and are skipped.
  • Config-supplied values are overlaid before validation, so a config bundle-id/team/min-safari is validated exactly like a CLI one.
  • Batch: per-extension keys (output, app-name, bundle-id) from config are dropped with a note for batch runs.

Batch mode

Passing multiple positionals converts each independently (33e9bce). Single-file / single-extension flags are rejected for batch and exit 2:

  • --output (one directory can't hold several extensions — omit it; each gets its default ./<App>_Safari)
  • --report (names a single file)
  • --json (emits one object per extension → not parseable as one stream; run one at a time)
  • --app-name / --bundle-id (apply to one extension)

One failing input does not abort the batch; the run exits 1 if any input failed, 0 if all succeeded. Ctrl-C mid-download is routed through cleanup so scratch dirs don't leak.

Exit codes

Code Meaning
0 Success. Conversion completed / analysis found nothing blocking / standalone mode succeeded.
1 Runtime failure or blocking result: conversion didn't complete, a blocking issue count > 0 without --force, --analyze found blocking issues, an install/verify the user requested didn't land, --doctor failed a check, --uninstall failed, or (in batch) any input failed.
2 Usage error: unknown/invalid flag, invalid --platforms/--bundle-id/--min-safari/--team, illegal flag combination (--install with --no-build/--temp-load/ios, --verify without --install, --json/--report without --analyze), missing <input>, a bad --config/batch flag. Help is printed alongside.
130 Interrupted (SIGINT/SIGTERM); routed through cleanup.

How --strict / --force affect exit

The blocking count comes from countBlocking(issues, strict) — issues with severity === "error", plus warning-severity issues when --strict, excluding anything already autoFixed.

  • Convert path (src/convert.ts): if blocking > 0 and not --force, it prints "<n> blocking … Re-run with --force to convert anyway." and the run exits 1. --force proceeds regardless. --strict widens what counts as blocking.
  • Analyze path (--analyze): returns 1 when the blocking count is > 0, else 0. Because auto-fixed issues are excluded, --analyze and a real convert agree on the verdict. With --strict, --analyze exits 1 if any warning or error is present. The analyze exit code was fixed in 146b6ca (and process.exitCode is set rather than calling process.exit() so a piped --json payload isn't truncated).

Common recipes

Convert directly from a Chrome Web Store URL (the .crx is fetched automatically):

viaduct "https://chromewebstore.google.com/detail/ublock-origin-lite/ddkjiahejlhfcafbddmgiahcphecmpfh"

Analyze only — report issues and preview the manifest rewrites, no conversion:

viaduct ./my-extension.zip --analyze

Machine-readable analysis for CI (pipe to jq):

viaduct ./my-extension.zip --analyze --json | jq '.convertible'

Strict CI gate — fail the analysis if there is any warning or error:

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

Stage for Safari 18 "Add Temporary Extension…" (no Xcode, no build):

viaduct ./my-extension.crx --temp-load
# then in Safari: Develop → Add Temporary Extension… → pick the staged folder

Generate the Xcode project but skip xcodebuild (open it yourself):

viaduct ./my-extension.zip --no-build --open-xcode

CI / TestFlight-safe build (clean-copy resources instead of symlinking):

viaduct ./my-extension.zip --ci

Build, sign with an auto-detected Apple team, and install to Safari (persists across restarts):

viaduct ./my-extension.zip --install --team auto
# (plain --install also auto-detects the team; use an explicit ID to pin it: --team A1B2C3D4E5)

Verify the install actually registered and is enabled:

viaduct ./my-extension.zip --install --team auto --verify

Batch-convert several extensions in one run:

viaduct ext-a.zip ext-b.crx ./ext-c-unpacked

Uninstall a previously installed app:

viaduct --uninstall "My Extension"
# add --install-dir if you installed somewhere other than ~/Applications

Check your local toolchain and list what's registered:

viaduct --doctor
viaduct --list

See also

  • Conversion Pipeline — how an input becomes a staged/built Safari extension.
  • Analyzer — the issue checks behind --analyze and the blocking-vs-warning model.
  • Build and Install — Xcode project generation, signing, and Safari registration (--install, --team, --verify).
  • Runtime Shim — the compatibility shim injected unless --no-shim.
  • Limitations and FAQ — Safari-specific constraints (e.g. the 4-shortcut commands cap d3fcb9a, stripped permission tokens 774d0a8).

Clone this wiki locally