@reyanshgupta/ditto-cli 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +587 -0
- package/bin/ditto-cli.js +84 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reyansh Gupta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
# Ditto CLI
|
|
2
|
+
|
|
3
|
+
[](https://crates.io/crates/ditto-cli)
|
|
4
|
+
[](https://www.npmjs.com/package/@reyanshgupta/ditto-cli)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://www.rust-lang.org/)
|
|
7
|
+
|
|
8
|
+
Keep work, personal, and client Claude Code, Codex, opencode, and OMP logins apart.
|
|
9
|
+
|
|
10
|
+
Ditto CLI gives each profile its own authentication, settings, and session history. Pick a profile in the terminal, then launch any of the four tools. Your existing setup stays available as the `default` profile.
|
|
11
|
+
|
|
12
|
+
Ditto CLI takes its name from the shape-shifting Pokémon: one small tool, whichever coding identity you need.
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
16
|
+
│ Ditto CLI choose a profile, then a tool │
|
|
17
|
+
└──────────────────────────────────────────────────────────────────────────────┘
|
|
18
|
+
┌ Profiles ──────────────┐┌ Selected profile ──────────────────────────────────┐
|
|
19
|
+
│ default existing ││work Isolated profile │
|
|
20
|
+
│› work ★ ││ │
|
|
21
|
+
│ personal ││★ Used when no profile is named │
|
|
22
|
+
│ ││ │
|
|
23
|
+
│ ││Sign-in status │
|
|
24
|
+
│ ││Claude Code ● Signed in │
|
|
25
|
+
│ ││Codex ○ Sign in required │
|
|
26
|
+
│ ││opencode ⠹ Checking │
|
|
27
|
+
│ ││OMP · Use /login inside OMP │
|
|
28
|
+
│ ││ │
|
|
29
|
+
│ ││Profile directories │
|
|
30
|
+
│ ││Claude Code ~/.ditto/profiles/work/claude │
|
|
31
|
+
│ ││Codex ~/.ditto/profiles/work/codex │
|
|
32
|
+
│ ││opencode …/work/opencode/data/opencode │
|
|
33
|
+
│ ││OMP ~/.omp/profiles/work/agent │
|
|
34
|
+
└────────────────────────┘└────────────────────────────────────────────────────┘
|
|
35
|
+
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
36
|
+
│ c Claude Code · x Codex · o opencode · p OMP │
|
|
37
|
+
│ ↑↓ select · n new · e rename · d default │
|
|
38
|
+
│ l sign in · L sign out · r refresh · q quit │
|
|
39
|
+
└──────────────────────────────────────────────────────────────────────────────┘
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+
Claude Code, Codex, opencode, and OMP keep user-level configuration and login state on disk. That works until you need separate accounts for different jobs. Manually moving auth files around is easy to get wrong, and it is hard to tell which account a new session will use.
|
|
45
|
+
|
|
46
|
+
Instead, Ditto CLI launches each tool pointed at the selected profile:
|
|
47
|
+
|
|
48
|
+
| Tool | Setting used by Ditto CLI |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| Claude Code | `CLAUDE_CONFIG_DIR=~/.ditto/profiles/<name>/claude` |
|
|
51
|
+
| Codex | `CODEX_HOME=~/.ditto/profiles/<name>/codex` |
|
|
52
|
+
| opencode | `XDG_DATA_HOME`, `XDG_CONFIG_HOME`, and `XDG_STATE_HOME` under `~/.ditto/profiles/<name>/opencode` |
|
|
53
|
+
| OMP | `omp --profile <name>` and `OMP_PROFILE=<name>` |
|
|
54
|
+
|
|
55
|
+
opencode has no single home variable; it resolves its directories the XDG way, so Ditto CLI pins the three bases that hold credentials, configuration, and session state. `XDG_CACHE_HOME` is deliberately left alone so profiles keep sharing opencode's downloaded tooling. Those variables are set for the launched process and anything it starts, including commands opencode's own tools run.
|
|
56
|
+
|
|
57
|
+
No config files are swapped. Profiles remain independent, and switching only affects the process Ditto CLI launched.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
Ditto CLI needs at least one of the supported CLIs to be useful: [Claude Code](https://code.claude.com/docs/en/setup), [OpenAI Codex CLI](https://github.com/openai/codex), [opencode](https://opencode.ai/docs/), or [Oh My Pi](https://github.com/can1357/oh-my-pi).
|
|
62
|
+
|
|
63
|
+
On macOS and Linux, Homebrew installs the prebuilt binary for your platform, so it needs no Rust toolchain:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
brew install reyanshgupta/tap/ditto-cli
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
npm carries the same prebuilt binaries, on macOS, Linux, and Windows alike. If you installed Claude Code or Codex with npm, it is already here:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install -g @reyanshgupta/ditto-cli
|
|
73
|
+
npx @reyanshgupta/ditto-cli list # or try it without installing
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The npm package is scoped because plain `ditto-cli` on npm belongs to an unrelated project. The command it installs is still `ditto-cli`.
|
|
77
|
+
|
|
78
|
+
Cargo builds from source instead, and needs Rust 1.85 or newer:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
cargo install ditto-cli # from crates.io
|
|
82
|
+
cargo install --git https://github.com/reyanshgupta/ditto-cli # from the latest source
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`cargo binstall ditto-cli` takes the released binary instead of compiling one, which is a good deal quicker: a source build compiles a bundled SQLite before it links anything.
|
|
86
|
+
|
|
87
|
+
Or from a local checkout:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://github.com/reyanshgupta/ditto-cli.git
|
|
91
|
+
cd ditto-cli
|
|
92
|
+
cargo install --path .
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
After a Cargo install, make sure Cargo's binary directory is on your `PATH`:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
export PATH="$HOME/.cargo/bin:$PATH"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
On Windows, `cargo install` already writes into `%USERPROFILE%\.cargo\bin`, which rustup puts on your `PATH`, and an npm install needs nothing further either.
|
|
102
|
+
|
|
103
|
+
Ditto CLI installs as `ditto-cli`, not `ditto`: macOS already uses that name for its built-in file-copy utility at `/usr/bin/ditto`.
|
|
104
|
+
|
|
105
|
+
macOS, Linux, and Windows are all supported, and every command below behaves the same on each. Two smaller things differ on Windows — see [Windows notes](#windows-notes).
|
|
106
|
+
|
|
107
|
+
### Update
|
|
108
|
+
|
|
109
|
+
A Homebrew install updates through Homebrew:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
brew upgrade ditto-cli
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
An npm install updates through npm:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm install -g @reyanshgupta/ditto-cli@latest
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
A Cargo install updates itself:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
ditto-cli update # install the newest crates.io release
|
|
125
|
+
ditto-cli update --check # compare versions without installing
|
|
126
|
+
ditto-cli update --git # install from the Git repository instead
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`update` runs `cargo install` for you, so it needs Rust on your `PATH`. It stops early when you already have the newest release. Asking crates.io for the published version needs a network connection; without one it says so and installs anyway.
|
|
130
|
+
|
|
131
|
+
`update` only replaces a copy in Cargo's own bin directory. Run it against a Homebrew install, or a binary from the [releases page](https://github.com/reyanshgupta/ditto-cli/releases), and it says so before it starts, then leaves that copy alone: a `cargo install` would land in a different directory and shadow the existing binary rather than replace it. Use `brew upgrade ditto-cli` or the newer archive instead.
|
|
132
|
+
|
|
133
|
+
Against an npm install it does not start at all. npm keeps its copy inside its own tree, so `update` names the npm command and stops rather than leaving you with two copies and whichever `PATH` reaches first. Pass `--git` if a source build is what you actually want.
|
|
134
|
+
|
|
135
|
+
## Quick start
|
|
136
|
+
|
|
137
|
+
Open Ditto CLI:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
ditto-cli
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Then:
|
|
144
|
+
|
|
145
|
+
1. Press `n` and name the profile, such as `work`.
|
|
146
|
+
2. For Claude Code, Codex, or opencode, select the profile and press `l` to run the official login flow.
|
|
147
|
+
3. Press `c` for Claude Code, `x` for Codex, or `o` for opencode.
|
|
148
|
+
4. Press `p` to launch OMP, then use `/login anthropic` or `/login openai-codex` inside OMP.
|
|
149
|
+
|
|
150
|
+
Each tool keeps its own credentials. Signing in to one does not copy credentials into another.
|
|
151
|
+
|
|
152
|
+
## TUI controls
|
|
153
|
+
|
|
154
|
+
| Key | Action |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| `↑` / `↓` or `k` / `j` | Select a profile |
|
|
157
|
+
| `c` | Launch Claude Code |
|
|
158
|
+
| `x` | Launch Codex |
|
|
159
|
+
| `o` | Launch opencode |
|
|
160
|
+
| `p` | Launch OMP |
|
|
161
|
+
| `l` | Sign in with Claude Code, Codex, or opencode |
|
|
162
|
+
| `L` | Sign out, with confirmation |
|
|
163
|
+
| `n` | Create a profile |
|
|
164
|
+
| `e` | Rename the selected profile |
|
|
165
|
+
| `d` | Mark the selected profile `★` as the default, or unset it |
|
|
166
|
+
| `r` | Refresh sign-in status |
|
|
167
|
+
| `q`, `Esc`, or `Ctrl+C` | Quit or close a dialog |
|
|
168
|
+
|
|
169
|
+
Sign-in status is checked in the background, so the list stays responsive while each CLI is asked. A spinner marks the tools still being checked.
|
|
170
|
+
|
|
171
|
+
The selected profile is remembered for the next run. The `★` default is separate and stays put: it is the profile every command uses when you leave the name out, and running `ditto-cli claude personal` once does not move it. Any profile can be the default, including the built-in `default` one. See [which profile a command uses](#which-profile-a-command-uses).
|
|
172
|
+
|
|
173
|
+
Renaming keeps the profile's settings, session history, and its Codex and opencode logins. Claude Code is the exception, and the rename dialog says so before you commit to it: see [renaming a profile signs Claude Code out](#renaming-a-profile-signs-claude-code-out). The built-in `default` profile cannot be renamed.
|
|
174
|
+
|
|
175
|
+
## Command-line usage
|
|
176
|
+
|
|
177
|
+
The TUI is optional. Everything it does works directly from the shell:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Profiles
|
|
181
|
+
ditto-cli create work
|
|
182
|
+
ditto-cli rename work client-a
|
|
183
|
+
ditto-cli delete work --yes # removes credentials and sessions for good
|
|
184
|
+
ditto-cli list
|
|
185
|
+
ditto-cli status client-a # sign-in state for all four tools
|
|
186
|
+
ditto-cli paths client-a
|
|
187
|
+
ditto-cli sync client-a # re-copy your Claude Code settings into it
|
|
188
|
+
|
|
189
|
+
# Launch a tool, with short aliases for the first three
|
|
190
|
+
ditto-cli claude client-a # or: cc
|
|
191
|
+
ditto-cli codex client-a # or: cx
|
|
192
|
+
ditto-cli opencode client-a # or: oc
|
|
193
|
+
ditto-cli omp client-a
|
|
194
|
+
|
|
195
|
+
# Pass arguments to the underlying CLI after --
|
|
196
|
+
ditto-cli claude client-a -- --model opus
|
|
197
|
+
ditto-cli codex client-a -- --search
|
|
198
|
+
ditto-cli opencode client-a -- --model anthropic/claude-opus-5
|
|
199
|
+
ditto-cli omp client-a -- --model opus
|
|
200
|
+
|
|
201
|
+
# Bind a directory to a profile
|
|
202
|
+
ditto-cli workspace # what this directory launches with
|
|
203
|
+
ditto-cli workspace use client-a
|
|
204
|
+
ditto-cli workspace clear
|
|
205
|
+
ditto-cli workspace list
|
|
206
|
+
ditto-cli workspace auto off
|
|
207
|
+
|
|
208
|
+
# Put Ditto in front of the tools' own names
|
|
209
|
+
eval "$(ditto-cli shell-init zsh)" # bash, fish, zsh; reads SHELL when omitted
|
|
210
|
+
|
|
211
|
+
# The profile used when a command names none and no directory is bound
|
|
212
|
+
ditto-cli default # report it
|
|
213
|
+
ditto-cli default client-a # pin it
|
|
214
|
+
ditto-cli default --clear # release it
|
|
215
|
+
|
|
216
|
+
# Show the profile inside Claude Code
|
|
217
|
+
ditto-cli indicator client-a # report the status line setting
|
|
218
|
+
ditto-cli indicator client-a --on
|
|
219
|
+
ditto-cli indicator client-a --off
|
|
220
|
+
ditto-cli indicator client-a --keep-mine # in front of the status line you have
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The native authentication commands can be called through a profile too:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
ditto-cli claude client-a -- auth login
|
|
227
|
+
ditto-cli codex client-a -- login
|
|
228
|
+
ditto-cli opencode client-a -- auth login
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Deleting is the one command that destroys something, so it asks to be meant. Without `--yes` it lists the directories it would remove and stops. The built-in `default` profile cannot be deleted; it is your own configuration, which Ditto CLI never created.
|
|
232
|
+
|
|
233
|
+
Profile names use lowercase letters, numbers, `.`, `-` and `_`, start with a letter or number, and are at most 32 characters. Uppercase names are rejected because OMP accepts only lowercase profile names.
|
|
234
|
+
|
|
235
|
+
### Which profile a command uses
|
|
236
|
+
|
|
237
|
+
When the profile name is omitted, Ditto CLI takes the first answer it finds:
|
|
238
|
+
|
|
239
|
+
1. The directory's binding (see [Workspaces](#workspaces)).
|
|
240
|
+
2. The profile pinned as the default, with `d` in the TUI or `ditto-cli default <profile>`.
|
|
241
|
+
3. The last profile selected in the TUI.
|
|
242
|
+
4. Failing all of those, and before your first selection, `default`.
|
|
243
|
+
|
|
244
|
+
`ditto-cli list` marks the last selection with `*` and the default with a trailing `default`.
|
|
245
|
+
|
|
246
|
+
## Workspaces
|
|
247
|
+
|
|
248
|
+
A project usually belongs to one profile for its whole life. A workspace records that once, so every launch from the project uses the right profile without being told:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
cd ~/code/client-a
|
|
252
|
+
ditto-cli workspace use client-a
|
|
253
|
+
ditto-cli claude # runs as client-a, from here and every subdirectory
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`ditto-cli workspace use` writes a `.ditto.toml` at the project root:
|
|
257
|
+
|
|
258
|
+
```toml
|
|
259
|
+
# Written by ditto-cli. Names the profile this directory launches with.
|
|
260
|
+
profile = "client-a"
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
It is a normal file: readable, editable by hand, and safe to commit if everyone on the project uses the same profile. Add it to `.gitignore` if they do not.
|
|
264
|
+
|
|
265
|
+
### Directories are bound automatically
|
|
266
|
+
|
|
267
|
+
Launching from a directory that nothing yet answers for binds it to the profile that launch used:
|
|
268
|
+
|
|
269
|
+
```console
|
|
270
|
+
$ cd ~/code/new-project
|
|
271
|
+
$ ditto-cli cc client-a
|
|
272
|
+
Bound this directory to 'client-a' (~/code/new-project/.ditto.toml).
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The next `ditto-cli cc` from that project needs no name. Two directories are never bound this way: your home directory and the filesystem root, since a file at either would capture every project underneath it. Turn the behaviour off with `ditto-cli workspace auto off`.
|
|
276
|
+
|
|
277
|
+
### How a directory is resolved
|
|
278
|
+
|
|
279
|
+
Ditto walks from the current directory towards the filesystem root and stops at the first binding it finds, so the nearest one wins and a subdirectory can overrule the repository it sits in. A directory already answered for by an ancestor is left alone, which is what stops every subdirectory of a bound project from collecting a file of its own.
|
|
280
|
+
|
|
281
|
+
Naming a profile explicitly always wins, for that run only:
|
|
282
|
+
|
|
283
|
+
```console
|
|
284
|
+
$ cd ~/code/client-a # bound to client-a
|
|
285
|
+
$ ditto-cli cc work
|
|
286
|
+
ditto-cli: using 'work' for this run; ~/code/client-a/.ditto.toml still names 'client-a'
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The binding is untouched. Use `ditto-cli workspace use work` to change it for good.
|
|
290
|
+
|
|
291
|
+
Where no directory answers, the launch says which profile it fell back to and why, since nothing in the command or the directory pointed at it:
|
|
292
|
+
|
|
293
|
+
```console
|
|
294
|
+
$ cd ~/scratch
|
|
295
|
+
$ ditto-cli omp
|
|
296
|
+
ditto-cli: using 'work'; nothing binds this directory, and it is pinned as the default
|
|
297
|
+
Bound this directory to 'work' (~/scratch/.ditto.toml).
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Reporting commands stay quiet about it: for them a fallback is the ordinary case, and they are run in loops.
|
|
301
|
+
|
|
302
|
+
### Directories you cannot leave a file in
|
|
303
|
+
|
|
304
|
+
For a repository that is not yours to add a file to, record the binding in Ditto's own registry instead:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
ditto-cli workspace use client-a --global
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
That writes `~/.ditto/workspaces.json` rather than a file in the project. `ditto-cli workspace list` prints everything recorded there. Files cannot be listed, because they are found by walking up from wherever Ditto runs — `ditto-cli workspace` reports the one in effect where you are.
|
|
311
|
+
|
|
312
|
+
Where a directory has both, the file wins, so a project can carry a binding that overrides whatever this machine recorded for it. Between two directories, distance decides first: a nearer registry entry beats a further file.
|
|
313
|
+
|
|
314
|
+
### Clearing a binding
|
|
315
|
+
|
|
316
|
+
```console
|
|
317
|
+
$ ditto-cli workspace clear
|
|
318
|
+
Removed ~/code/client-a/crates/core/.ditto.toml.
|
|
319
|
+
~/code/client-a/crates/core now inherits 'client-a' from ~/code/client-a/.ditto.toml.
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Clearing touches only the directory you name, never its ancestors, and reports what the directory falls back to.
|
|
323
|
+
|
|
324
|
+
## Shell integration
|
|
325
|
+
|
|
326
|
+
A binding decides something only for a launch that goes through Ditto. Typing the tool's own name runs the tool, which has never heard of `.ditto.toml`:
|
|
327
|
+
|
|
328
|
+
```console
|
|
329
|
+
$ cd ~/code/client-a # bound to client-a
|
|
330
|
+
$ omp # OMP's own default profile, not client-a
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Nothing Ditto does at launch time can catch that, because Ditto is not in the process. The shell is the only place the bare name can be intercepted, so Ditto writes the functions that do it:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
# ~/.zshrc
|
|
337
|
+
eval "$(ditto-cli shell-init zsh)"
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
`bash`, `fish`, and `zsh` are all written for, and `ditto-cli shell-init` with no shell named reads `SHELL`. Fish loads it by piping into `source`:
|
|
341
|
+
|
|
342
|
+
```fish
|
|
343
|
+
# ~/.config/fish/config.fish
|
|
344
|
+
ditto-cli shell-init fish | source
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
From then on the tool's own name uses the directory's profile, and arguments pass straight through to the tool:
|
|
348
|
+
|
|
349
|
+
```console
|
|
350
|
+
$ cd ~/code/client-a
|
|
351
|
+
$ omp --model opus # the same as: ditto-cli omp -- --model opus
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Two ways back out, both printed in the script itself: `command omp` runs OMP with Ditto out of the way, and `ditto-cli omp <profile>` launches another profile for that one run.
|
|
355
|
+
|
|
356
|
+
A function named after a tool cannot loop back into itself. Ditto starts a tool through `execvp`, which searches `PATH` and never looks at shell functions. And if `ditto-cli` cannot be found at all, each function falls through to the tool, so a half-finished update cannot take `claude` with it.
|
|
357
|
+
|
|
358
|
+
## Knowing which profile you are in
|
|
359
|
+
|
|
360
|
+
Once a tool has taken over the terminal, nothing on screen says which profile it is running as. Ditto CLI answers that in the window title for every tool, and inside Claude Code's own interface as well.
|
|
361
|
+
|
|
362
|
+
Claude Code gets a status line along the bottom of its interface:
|
|
363
|
+
|
|
364
|
+
```text
|
|
365
|
+
⬖ client-a · you@example.com
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Launching Claude Code through Ditto CLI installs it, and `ditto-cli indicator` turns it on or off by hand. The rest of `settings.json` is written only when a profile is created or synced, described in [settings a new profile inherits](#settings-a-new-profile-inherits).
|
|
369
|
+
|
|
370
|
+
If the profile already has a `statusLine` of its own, Ditto CLI leaves it alone and says so. Claude Code renders one status line, and replacing the one you configured would quietly take it away. To have both, ask:
|
|
371
|
+
|
|
372
|
+
```console
|
|
373
|
+
$ ditto-cli indicator client-a --keep-mine
|
|
374
|
+
client-a: status line on, drawn in front of the one you already had
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Ditto CLI then runs your status line itself and prints the profile in front of what it says:
|
|
378
|
+
|
|
379
|
+
```text
|
|
380
|
+
⬖ client-a · you@example.com │ ⎇ main ✓ ~/code/client-a 12.4k
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Your command keeps the payload Claude Code sends it, every line it prints, and the settings it was written with. If it fails or prints nothing, you get the profile on its own rather than a broken line. `ditto-cli indicator --off` puts your original entry back exactly as you wrote it, and a later launch never changes the arrangement on its own — drawing over someone's status line is a thing to be asked for.
|
|
384
|
+
|
|
385
|
+
A status line set for a project (`.claude/settings.json`, `.claude/settings.local.json`) or by your administrator outranks the profile's, so Ditto CLI's would be installed and never seen. Rather than claim otherwise, `ditto-cli indicator` reports that:
|
|
386
|
+
|
|
387
|
+
```console
|
|
388
|
+
$ ditto-cli indicator client-a
|
|
389
|
+
client-a: installed, but a status line that outranks it is showing instead
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
The status line reads `DITTO_PROFILE`, so a `claude` you started yourself still reports the right profile as long as `CLAUDE_CONFIG_DIR` points at one.
|
|
393
|
+
|
|
394
|
+
Every tool also names the profile in the window and tab title:
|
|
395
|
+
|
|
396
|
+
```text
|
|
397
|
+
ditto:client-a — Codex — my-repo
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
All four tools write their own titles and keep updating them as you work, so a title set once before handing over is overwritten within moments. Instead, Ditto CLI runs the tool in a pseudoterminal and rewrites the title sequences on their way to the terminal, adding `ditto:<profile>` in front of whatever the tool called itself. You keep the tool's own title and gain the profile.
|
|
401
|
+
|
|
402
|
+
Everything else is forwarded byte for byte. Colours, hyperlinks, clipboard writes, mouse reporting, and anything the tool draws are untouched, and the tool still gets a real terminal, the right window size, and your keystrokes as it always did. Ditto CLI exits with the tool's own exit status.
|
|
403
|
+
|
|
404
|
+
To turn it off, set `DITTO_NO_PROXY=1` and Ditto CLI hands the terminal straight to the tool as it used to. The title stops naming the profile, and the Claude Code status line still works. Ditto CLI also steps aside on its own when output is redirected, or if the pseudoterminal cannot be opened. The pseudoterminal is a macOS and Linux feature; see [Windows notes](#windows-notes).
|
|
405
|
+
|
|
406
|
+
### Under herdr
|
|
407
|
+
|
|
408
|
+
[herdr](https://herdr.dev) works out which agent is in a pane, and what that agent is doing, by reading the pane's foreground process and the title the tool writes. Sitting between the tool and the terminal costs it both answers: the foreground process becomes `ditto-cli` while the tool moves to a pseudoterminal of its own, and a title with `ditto:<profile>` in front of it no longer matches the rules herdr reads state from. The pane stops listing an agent, and `herdr agent` stops seeing it.
|
|
409
|
+
|
|
410
|
+
So Ditto CLI steps aside on its own inside a herdr pane, exactly as `DITTO_NO_PROXY=1` does, and reports the profile to herdr instead:
|
|
411
|
+
|
|
412
|
+
```text
|
|
413
|
+
herdr pane report-metadata <pane> --source ditto --token profile=<profile>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
herdr shows it as a pane token, and detection, agent state, and the Claude Code status line all keep working. Nothing needs configuring — it keys off `HERDR_PANE_ID`, which herdr sets in every pane it opens. If herdr is not running or its CLI is not on `PATH` the report is skipped and the launch carries on.
|
|
417
|
+
|
|
418
|
+
## Scripting and agents
|
|
419
|
+
|
|
420
|
+
Add `--json` to any reporting command and it prints one JSON object on stdout. The flag is global, so it reads correctly on either side of the subcommand:
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
ditto-cli list --json
|
|
424
|
+
ditto-cli --json status client-a
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
```json
|
|
428
|
+
{
|
|
429
|
+
"profile": "client-a",
|
|
430
|
+
"managed": true,
|
|
431
|
+
"tools": [
|
|
432
|
+
{ "tool": "claude", "label": "Claude Code", "status": "signed_in", "signed_in": true },
|
|
433
|
+
{ "tool": "codex", "label": "Codex", "status": "signed_out", "signed_in": false }
|
|
434
|
+
]
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
`list`, `status`, `paths`, `create`, `rename`, `delete`, `default`, `workspace`, and `indicator` all answer in JSON. Errors become `{"error": "..."}` on stderr, and every failure exits 1. `shell-init` prints a script for a shell to read rather than a report, so `--json` has nothing to do there.
|
|
439
|
+
|
|
440
|
+
Running `ditto-cli` with no subcommand opens the picker, which needs an interactive terminal. Without one it exits 1 and names the commands to use instead, rather than failing on a terminal that was never there.
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
# Any tool needing a sign-in?
|
|
444
|
+
ditto-cli status client-a --json | jq -r '.tools[] | select(.signed_in | not) | .tool'
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
The `tool`, `status`, and `indicator` outcome strings are stable identifiers meant to be matched on; the `label` beside them is display text and may be reworded. [AGENTS.md](AGENTS.md) documents the full contract and has more recipes, along with how to edit Ditto CLI itself.
|
|
448
|
+
|
|
449
|
+
## Settings a new profile inherits
|
|
450
|
+
|
|
451
|
+
Claude Code reads its settings from the configuration directory it is pointed at, and Ditto CLI points it somewhere else. Left alone, a new profile would start with none of the permission mode, model, effort level, or hooks you set up once and expect everywhere.
|
|
452
|
+
|
|
453
|
+
So creating a profile copies `~/.claude/settings.json` into it. What Ditto CLI isolates is accounts, and none of those live in that file — Claude Code keeps credentials in the Keychain and the signed-in account in `.claude.json` — so your preferences travel and your logins stay apart. The status line travels too: the entry names the profile it was installed for, so what is copied is the status line underneath it, with the new profile's own indicator drawn in front of yours.
|
|
454
|
+
|
|
455
|
+
From then on the profile's settings are its own. Change the model in one profile and the others keep theirs; a later `ditto-cli sync` fills in settings the profile has never answered and leaves the ones it has:
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
ditto-cli sync client-a # bring it up to date, keeping its own answers
|
|
459
|
+
ditto-cli sync client-a --overwrite # your configuration wins outright
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
`sync` is also how profiles created before this behaviour existed catch up.
|
|
463
|
+
|
|
464
|
+
## Skills, subagents, and everything else you set up once
|
|
465
|
+
|
|
466
|
+
A profile exists to be signed in as somebody else, not to be a different working environment. Skills, subagents, slash commands, hooks, plugins, and the memory file each tool reads are not accounts, so a profile does not get its own copy of them — it reads yours:
|
|
467
|
+
|
|
468
|
+
| Tool | Read from your own configuration |
|
|
469
|
+
| --- | --- |
|
|
470
|
+
| Claude Code | `skills`, `agents`, `commands`, `hooks`, `plugins`, `output-styles`, `CLAUDE.md` |
|
|
471
|
+
| Codex | `skills`, `rules`, `prompts`, `plugins`, `config.toml`, `hooks.json`, `AGENTS.md`, `instructions.md` |
|
|
472
|
+
| opencode | the whole configuration directory |
|
|
473
|
+
| OMP | `config.yml`, `extensions` |
|
|
474
|
+
|
|
475
|
+
These are symbolic links, so a skill you write tomorrow is in every profile the moment you save it, with nothing to sync and no copies to drift apart. Everything else — `.claude.json`, `auth.json`, sessions, history, `agent.db` — stays inside the profile, which is the whole of what a profile keeps to itself.
|
|
476
|
+
|
|
477
|
+
What is shared is a named list rather than everything-but-the-credentials. Ditto CLI learning about a new extension directory late costs you a missing feature; sharing a new credential file by accident would cost you the isolation the tool exists for.
|
|
478
|
+
|
|
479
|
+
Profiles created before this have real directories where the links go. `sync` reports those and leaves them alone; `--adopt` points them at yours, moving what was there aside as `<name>.before-ditto` rather than deleting it:
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
ditto-cli sync client-a # link what can be linked, report what cannot
|
|
483
|
+
ditto-cli sync client-a --adopt # replace the profile's own copies too
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Where credentials and files are stored
|
|
487
|
+
|
|
488
|
+
Ditto CLI does not ask for passwords, parse OAuth tokens, or keep credentials in its state file. Claude Code, Codex, and opencode authentication still runs through their installed CLIs, and OMP authentication through `/login` inside OMP. Each tool stores the result wherever it normally would, under the directory Ditto CLI pointed it at:
|
|
489
|
+
|
|
490
|
+
- **Claude Code** uses the selected `CLAUDE_CONFIG_DIR`. On macOS the credentials themselves stay in the system Keychain, keyed to that directory's path, which is what keeps two profiles from sharing one login — and why [renaming a profile signs Claude Code out](#renaming-a-profile-signs-claude-code-out).
|
|
491
|
+
- **Codex** keeps its auth state under the selected `CODEX_HOME`.
|
|
492
|
+
- **opencode** writes `auth.json` into the selected data directory.
|
|
493
|
+
- **OMP** keeps auth, settings, sessions, and caches under `~/.omp/profiles/<name>/agent`.
|
|
494
|
+
|
|
495
|
+
Ditto CLI's own files are laid out like this:
|
|
496
|
+
|
|
497
|
+
```text
|
|
498
|
+
~/.ditto/
|
|
499
|
+
├── state.toml
|
|
500
|
+
├── workspaces.json # directories bound without a file of their own
|
|
501
|
+
└── profiles/
|
|
502
|
+
├── work/
|
|
503
|
+
│ ├── claude/
|
|
504
|
+
│ ├── codex/
|
|
505
|
+
│ └── opencode/
|
|
506
|
+
│ ├── config/opencode/
|
|
507
|
+
│ ├── data/opencode/ # auth.json lives here
|
|
508
|
+
│ └── state/opencode/
|
|
509
|
+
└── personal/
|
|
510
|
+
├── claude/
|
|
511
|
+
├── codex/
|
|
512
|
+
└── opencode/
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
The nested `opencode/` directory is opencode's own doing: it appends its name to every XDG base it is given. OMP profiles are the one thing kept outside this tree, since OMP manages its own profile directory.
|
|
516
|
+
|
|
517
|
+
Directories are created with user-only permissions on macOS and Linux; see [Windows notes](#windows-notes) for the difference there.
|
|
518
|
+
|
|
519
|
+
The `default` profile points to `~/.claude`, `~/.codex`, opencode's own `~/.local/share/opencode` and `~/.config/opencode` (or wherever your `XDG_*` variables already send them), and OMP's native `~/.omp/agent` profile. It exposes your existing setup without copying or migrating anything. For opencode the `default` profile resolves the same XDG bases opencode would pick on its own, so pointing at it changes nothing.
|
|
520
|
+
|
|
521
|
+
## Environment variables
|
|
522
|
+
|
|
523
|
+
| Variable | Purpose |
|
|
524
|
+
| --- | --- |
|
|
525
|
+
| `DITTO_HOME` | Move Ditto CLI's state and profile directory from `~/.ditto` |
|
|
526
|
+
| `DITTO_CLAUDE_BIN` | Override the `claude` executable |
|
|
527
|
+
| `DITTO_CODEX_BIN` | Override the `codex` executable |
|
|
528
|
+
| `DITTO_OPENCODE_BIN` | Override the `opencode` executable |
|
|
529
|
+
| `DITTO_OMP_BIN` | Override the `omp` executable |
|
|
530
|
+
| `DITTO_PROFILE` | Selected profile name exported to every launched tool, and what Claude Code's status line reports |
|
|
531
|
+
| `DITTO_NO_PROXY` | Hand the terminal straight to the tool, leaving the title to it (macOS and Linux) |
|
|
532
|
+
| `HERDR_PANE_ID` | Read, not set: herdr names the pane it opened, and Ditto CLI steps aside and reports the profile to herdr. See [Under herdr](#under-herdr) |
|
|
533
|
+
| `NO_COLOR` | Draw the Claude Code status line without colour |
|
|
534
|
+
|
|
535
|
+
Example:
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
DITTO_HOME="$HOME/.config/ditto" ditto-cli
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
`ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, `OPENAI_API_KEY`, and `OPENCODE_API_KEY` are inherited by launched tools. They may override a saved subscription login, so Ditto CLI shows a warning when one is set.
|
|
542
|
+
|
|
543
|
+
## Renaming a profile signs Claude Code out
|
|
544
|
+
|
|
545
|
+
Claude Code stores credentials against the configuration directory it was pointed at, so moving that directory loses the sign-in. Renaming a profile moves `~/.ditto/profiles/<name>/claude`, and the credentials do not follow it.
|
|
546
|
+
|
|
547
|
+
Ditto CLI warns before the rename and tells you how to get back in:
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
ditto-cli claude client-a -- auth login
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Codex and opencode keep their credentials in files inside the profile, so they survive a rename untouched.
|
|
554
|
+
|
|
555
|
+
## Windows notes
|
|
556
|
+
|
|
557
|
+
Profiles, launching, sign-in status, the Claude Code status line, and every command above work the same as they do elsewhere. Three things are worth knowing:
|
|
558
|
+
|
|
559
|
+
- **Window titles.** Naming the profile in the title means sitting between the tool and the terminal, which needs a pseudoterminal; Ditto CLI opens one on macOS and Linux only. On Windows the title is set once before the tool starts, and the tool then paints over it with its own. Claude Code's status line is unaffected, and is the better indicator anyway. Ditto CLI still waits on the tool and exits with its status, and Ctrl-C goes to the tool rather than ending Ditto CLI out from under it.
|
|
560
|
+
- **Directory permissions.** Profile directories are created owner-only on macOS and Linux. Windows has no equivalent mode to set, so profiles inherit the access control of the directory they are created in. That is enough under `%USERPROFILE%`; if you point `DITTO_HOME` somewhere else, put it somewhere private.
|
|
561
|
+
- **Finding the CLIs.** On Windows they are usually installed by npm, which writes `claude.cmd` rather than `claude.exe`. Windows itself only ever looks for `.exe`, so Ditto CLI searches `PATH` the way a command prompt does, honouring `PATHEXT`. A native `claude.exe` is preferred when both are present.
|
|
562
|
+
|
|
563
|
+
## Remove Ditto CLI
|
|
564
|
+
|
|
565
|
+
Uninstall the binary:
|
|
566
|
+
|
|
567
|
+
```bash
|
|
568
|
+
cargo uninstall ditto-cli
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Profiles are deliberately left alone. If you no longer need their settings, sessions, or credentials, remove `~/.ditto` and any matching `~/.omp/profiles/<name>` directories yourself.
|
|
572
|
+
|
|
573
|
+
## Development
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
cargo test
|
|
577
|
+
cargo fmt --check
|
|
578
|
+
cargo clippy --all-targets -- -D warnings
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
[AGENTS.md](AGENTS.md) has the module layout, the conventions this codebase holds to, and step-by-step recipes for adding a subcommand or a tool. `CLAUDE.md` points at the same file.
|
|
582
|
+
|
|
583
|
+
## License
|
|
584
|
+
|
|
585
|
+
Ditto CLI is available under the [MIT License](LICENSE).
|
|
586
|
+
|
|
587
|
+
Ditto CLI is an independent project. It is not affiliated with Anthropic, OpenAI, Nintendo, or The Pokémon Company.
|
package/bin/ditto-cli.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// npm has no way to publish one package carrying every platform's binary, so a
|
|
5
|
+
// release is a wrapper package — this one — depending on a binary package per
|
|
6
|
+
// platform. npm installs only the one whose `os` and `cpu` match and passes
|
|
7
|
+
// over the rest, because they are optional dependencies rather than required
|
|
8
|
+
// ones. All this file does is find the binary that was installed and hand the
|
|
9
|
+
// terminal to it.
|
|
10
|
+
|
|
11
|
+
const fs = require("node:fs");
|
|
12
|
+
const os = require("node:os");
|
|
13
|
+
const { spawnSync } = require("node:child_process");
|
|
14
|
+
|
|
15
|
+
const REPOSITORY = "https://github.com/reyanshgupta/ditto-cli";
|
|
16
|
+
const PACKAGES = {
|
|
17
|
+
"darwin-arm64": "@reyanshgupta/ditto-cli-darwin-arm64",
|
|
18
|
+
"darwin-x64": "@reyanshgupta/ditto-cli-darwin-x64",
|
|
19
|
+
"linux-x64": "@reyanshgupta/ditto-cli-linux-x64",
|
|
20
|
+
"win32-x64": "@reyanshgupta/ditto-cli-win32-x64",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const platform = `${process.platform}-${process.arch}`;
|
|
24
|
+
const packageName = PACKAGES[platform];
|
|
25
|
+
|
|
26
|
+
if (packageName === undefined) {
|
|
27
|
+
fail(
|
|
28
|
+
`ditto-cli: no prebuilt binary for ${platform}`,
|
|
29
|
+
"ditto-cli: build one with `cargo install ditto-cli`, which needs Rust 1.85 or newer",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const binaryName = process.platform === "win32" ? "ditto-cli.exe" : "ditto-cli";
|
|
34
|
+
|
|
35
|
+
let binary;
|
|
36
|
+
try {
|
|
37
|
+
// The binary packages deliberately carry no `exports` field, which is what
|
|
38
|
+
// lets a path inside one resolve at all. Adding one would have to list this
|
|
39
|
+
// path or this line stops finding it.
|
|
40
|
+
binary = require.resolve(`${packageName}/bin/${binaryName}`);
|
|
41
|
+
} catch {
|
|
42
|
+
fail(
|
|
43
|
+
`ditto-cli: ${packageName} is not installed`,
|
|
44
|
+
"ditto-cli: it is an optional dependency, so an install run with `--no-optional` or",
|
|
45
|
+
"ditto-cli: `--omit=optional` leaves the binary out; reinstall with neither, or take",
|
|
46
|
+
`ditto-cli: an archive from ${REPOSITORY}/releases`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Ditto draws over the whole terminal, and a launcher that exited first would
|
|
51
|
+
// hand the shell back a screen the process still running had not restored. So
|
|
52
|
+
// the signals a terminal sends to everything in the foreground are ignored
|
|
53
|
+
// here and left to the process actually drawing, which already handles them.
|
|
54
|
+
process.on("SIGINT", () => {});
|
|
55
|
+
process.on("SIGTERM", () => {});
|
|
56
|
+
|
|
57
|
+
// `ditto-cli update` installs with cargo, which would write a second copy into
|
|
58
|
+
// Cargo's bin directory rather than replace this one. Saying how this copy
|
|
59
|
+
// arrived is what lets it name the npm command instead.
|
|
60
|
+
const result = spawnSync(binary, process.argv.slice(2), {
|
|
61
|
+
stdio: "inherit",
|
|
62
|
+
env: { ...process.env, DITTO_INSTALL_SOURCE: "npm" },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (result.error) {
|
|
66
|
+
fail(`ditto-cli: could not run ${binary}: ${result.error.message}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Ditto's launch commands exit with the status of the tool they ran, so the
|
|
70
|
+
// wrapper has to carry that status out rather than report its own success.
|
|
71
|
+
// A run that ended on a signal has no status, and 128 plus the signal number
|
|
72
|
+
// is what a shell would have reported for it.
|
|
73
|
+
if (result.signal !== null) {
|
|
74
|
+
process.exit(128 + (os.constants.signals[result.signal] ?? 0));
|
|
75
|
+
}
|
|
76
|
+
process.exit(result.status ?? 1);
|
|
77
|
+
|
|
78
|
+
// Written straight to the file descriptor because `process.stderr.write` is
|
|
79
|
+
// not synchronous when stderr is a pipe, and the exit below would cut the
|
|
80
|
+
// message off before it left.
|
|
81
|
+
function fail(...lines) {
|
|
82
|
+
fs.writeSync(2, `${lines.join("\n")}\n`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reyanshgupta/ditto-cli",
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Switch between multiple Claude Code, Codex, opencode, and OMP accounts on one machine",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"codex",
|
|
8
|
+
"opencode",
|
|
9
|
+
"omp",
|
|
10
|
+
"profiles",
|
|
11
|
+
"cli"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Reyansh Gupta",
|
|
15
|
+
"homepage": "https://www.reyanshgupta.com/projects/ditto-cli",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/reyanshgupta/ditto-cli.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/reyanshgupta/ditto-cli/issues"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"ditto-cli": "bin/ditto-cli.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin/ditto-cli.js",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"optionalDependencies": {
|
|
35
|
+
"@reyanshgupta/ditto-cli-darwin-arm64": "0.3.3",
|
|
36
|
+
"@reyanshgupta/ditto-cli-darwin-x64": "0.3.3",
|
|
37
|
+
"@reyanshgupta/ditto-cli-linux-x64": "0.3.3",
|
|
38
|
+
"@reyanshgupta/ditto-cli-win32-x64": "0.3.3"
|
|
39
|
+
}
|
|
40
|
+
}
|