-
Notifications
You must be signed in to change notification settings - Fork 3
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.
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
viaduct --logs <name> # dump a --debug build's persisted debug log<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.
Grouped logically. Every flag below is parsed in cli.ts. Booleans default to false unless noted.
| 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
## Optionsblock lists the--min-safaridefault as15.4literally, which matchescli.ts(the help text interpolatesDEFAULT_MIN_SAFARI_VERSION = "15.4"). No conflict, but the source of truth is the constant, not the hard-coded README string.
| 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). |
| 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 via detectXcodeTeam: Xcode's cached team list (both keys, both preference domains) or the codesigning identity in the keychain, with the provisioning profiles on disk choosing between them (newest first). A team id that appears only in a profile is ignored, because a profile can name a team this Mac has no account for and every build then dies on error: No Account for Team (issue #15). When a team does reach xcodebuild the result is checked against the artifact — codesign -dvv on the built .appex (verifySigning, src/build/verify.ts) — and a bundle that came out ad-hoc anyway exits non-zero, not behind --verify, on the same footing as an install that didn't land. When no team is found at all, the run warns up front and the announced ad-hoc fallback is a warning, not a failure (issue #14); an auto-detected team that then fails the build for a signing reason is retried ad-hoc the same way, while a team you named with --team <id> fails the run instead of being silently downgraded. Omit --team entirely for ad-hoc signing, which is left unchecked. 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). Signing is not part of this flag — it is checked on every run that asks for a team (see --team). |
|
--no-safari-restart |
off (restarts) | With --install, do not quit/relaunch Safari and do not set the "Allow Unsigned Extensions" toggle. |
|
--background-launch |
off (opens visibly) | With --install, launch the host app with open -g -j (hidden, no activation). The launch still makes PlugInKit register the appex, but no window opens over whatever is on screen. Built for the Viaduct app, which finishes its converting animation and then opens the host app itself. |
README discrepancy: the README shows
--team [<id>](bracketed optional argument). Incli.ts,teamis{ type: "string" }, the value is not optional at the parser level:--teammust be followed by a value (autoor a team id). "Plain--installauto-detects" is achieved by omitting--team, not by a bare--team.
| 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). |
|
--debug |
off | Emit the shim with debug tracing enabled: shimSource({ debug: true }) flips the compiled-in __C2S_DEBUG__ gate to true and splices the persistent ring-buffer logger (src/runtime/debug-ring.js) into the staged shim. Every gated trace then also lands in storage.local under __viaduct_debug_log__ (last 2000 entries, writes batched ~1 s), tagged with a timestamp and a background/content/page context label. Read it with --logs or the console one-liner in Testing and Debugging. A default conversion carries no ring-buffer write path at all. Rejects --no-shim (exits 2). Dev builds only. |
|
--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. |
| 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). |
| Flag | Alias | Default | Description |
|---|---|---|---|
--doctor |
, | Standalone mode, verify local toolchain (xcrun, safari-web-extension-packager, xcodebuild, plutil, pluginkit, ditto, osascript, lsregister). See Standalone modes. |
|
--logs |
<name> |
, | Standalone mode, dump the persisted __viaduct_debug_log__ ring buffer of an installed --debug build. <name> matches the app name or bundle id (case/punctuation-insensitive) against Safari's on-disk extension storage at ~/Library/Containers/com.apple.Safari/Data/Library/WebKit/WebExtensions/Default/<bundle-id>.Extension (<team>)/LocalStorage.db; the WAL trio is snapshotted to a scratch dir and queried read-only, so Safari can stay open. 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. |
| 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. |
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, --logs. 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. |
--logs <name> |
Prints one line per persisted debug-log entry (ISO time [context] message) for the installed extension matching <name> (src/runtime/debug-logs.ts). An empty name, no match, an ambiguous match, or an extension without a recorded log exits 1 with the reason (the last one names --debug as the fix). |
0 on success, 1 on failure. |
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-oshort 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,background-launch,team,no-shim,no-oauth-bridge,keep-module,force,strict,verify,clean,debug. One-shot/meta flags (analyze,doctor,list,version,json,report,config,help,quiet,verbose,uninstall,logs,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-safariis 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.
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.
| 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. |
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> 0and not--force, it prints"<n> blocking … Re-run with --force to convert anyway."and the run exits1.--forceproceeds regardless.--strictwidens what counts as blocking. -
Analyze path (
--analyze): returns1when the blocking count is> 0, else0. Because auto-fixed issues are excluded,--analyzeand a real convert agree on the verdict. With--strict,--analyzeexits1if any warning or error is present. The analyze exit code was fixed in146b6ca(andprocess.exitCodeis set rather than callingprocess.exit()so a piped--jsonpayload isn't truncated).
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 --analyzeMachine-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 --strictStage 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 folderGenerate the Xcode project but skip xcodebuild (open it yourself):
viaduct ./my-extension.zip --no-build --open-xcodeCI / TestFlight-safe build (clean-copy resources instead of symlinking):
viaduct ./my-extension.zip --ciBuild, 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 --verifyBatch-convert several extensions in one run:
viaduct ext-a.zip ext-b.crx ./ext-c-unpackedUninstall a previously installed app:
viaduct --uninstall "My Extension"
# add --install-dir if you installed somewhere other than ~/ApplicationsCheck your local toolchain and list what's registered:
viaduct --doctor
viaduct --list- Conversion Pipeline, how an input becomes a staged/built Safari extension.
-
Analyzer, the issue checks behind
--analyzeand 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
commandscapd3fcb9a, stripped permission tokens774d0a8).
Viaduct CLI · @magicelk235/viaduct · PolyForm Shield 1.0.0 · Verified against src/ and grounded in git history.