kajji 0.1.0
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 +128 -0
- package/bin/kajji.js +2 -0
- package/package.json +56 -0
- package/src/App.tsx +229 -0
- package/src/commander/bookmarks.ts +129 -0
- package/src/commander/diff.ts +186 -0
- package/src/commander/executor.ts +285 -0
- package/src/commander/files.ts +87 -0
- package/src/commander/log.ts +99 -0
- package/src/commander/operations.ts +313 -0
- package/src/commander/types.ts +21 -0
- package/src/components/AnsiText.tsx +77 -0
- package/src/components/BorderBox.tsx +124 -0
- package/src/components/FileTreeList.tsx +105 -0
- package/src/components/Layout.tsx +48 -0
- package/src/components/Panel.tsx +143 -0
- package/src/components/RevisionPicker.tsx +165 -0
- package/src/components/StatusBar.tsx +158 -0
- package/src/components/modals/BookmarkNameModal.tsx +170 -0
- package/src/components/modals/DescribeModal.tsx +124 -0
- package/src/components/modals/HelpModal.tsx +372 -0
- package/src/components/modals/RevisionPickerModal.tsx +70 -0
- package/src/components/modals/UndoModal.tsx +75 -0
- package/src/components/panels/BookmarksPanel.tsx +768 -0
- package/src/components/panels/CommandLogPanel.tsx +40 -0
- package/src/components/panels/LogPanel.tsx +774 -0
- package/src/components/panels/MainArea.tsx +354 -0
- package/src/context/command.tsx +106 -0
- package/src/context/commandlog.tsx +45 -0
- package/src/context/dialog.tsx +217 -0
- package/src/context/focus.tsx +63 -0
- package/src/context/helper.tsx +24 -0
- package/src/context/keybind.tsx +51 -0
- package/src/context/loading.tsx +68 -0
- package/src/context/sync.tsx +868 -0
- package/src/context/theme.tsx +90 -0
- package/src/context/types.ts +51 -0
- package/src/index.tsx +15 -0
- package/src/keybind/index.ts +2 -0
- package/src/keybind/parser.ts +88 -0
- package/src/keybind/types.ts +83 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/presets/lazygit.ts +45 -0
- package/src/theme/presets/opencode.ts +45 -0
- package/src/theme/types.ts +47 -0
- package/src/utils/double-click.ts +59 -0
- package/src/utils/file-tree.ts +154 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 eliaskc
|
|
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,128 @@
|
|
|
1
|
+
# kajji
|
|
2
|
+
|
|
3
|
+
> The rudder for your jj
|
|
4
|
+
|
|
5
|
+
A simple terminal UI for [Jujutsu](https://github.com/martinvonz/jj), inspired by [lazygit](https://github.com/jesseduffield/lazygit). Built with [OpenTUI](https://github.com/sst/opentui) and [SolidJS](https://www.solidjs.com/).
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
While learning jj I found myself coming back to lazygit to view diffs and traverse the changes I'd made quickly and easily, which has become increasingly important to me with the rise of coding agents. While there are existing jj TUIs, none quite scratched that lazygit itch.
|
|
10
|
+
|
|
11
|
+
Kajji is my attempt to bring the simplicity and polish of lazygit to jj, while also leveraging coding agents effectively and building a TUI for the first time.
|
|
12
|
+
|
|
13
|
+
> Disclaimer: almost all code in this project has been written by coding agents (primarily Claude Opus 4.5 through [OpenCode](https://github.com/sst/opencode)).
|
|
14
|
+
|
|
15
|
+
## Principles
|
|
16
|
+
|
|
17
|
+
- **Polish & simplicity** - Do less, but do it well.
|
|
18
|
+
- **Intuitive UX** - Sensible defaults, consistent patterns.
|
|
19
|
+
- **Snappy** - If it feels slow, it's a bug.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **Full-color diffs** — works with difftastic, delta, or your configured diff tool
|
|
24
|
+
- **Commit log** — navigate jj's graph with vim-style keybindings
|
|
25
|
+
- **Bookmarks panel** — drill down into commits and files
|
|
26
|
+
- **Collapsible file tree** — with status colors (A/M/D)
|
|
27
|
+
- **Operation log** — view and restore from jj op history
|
|
28
|
+
- **Git operations** — fetch and push
|
|
29
|
+
- **Undo/redo** — with confirmation showing what will change
|
|
30
|
+
- **Help palette** — press `?` for all keybindings with fuzzy search
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
> **Requirements**: [Bun](https://bun.sh) and [jj](https://github.com/martinvonz/jj)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# npm
|
|
38
|
+
npm install -g kajji
|
|
39
|
+
|
|
40
|
+
# bun
|
|
41
|
+
bun install -g kajji
|
|
42
|
+
|
|
43
|
+
# pnpm
|
|
44
|
+
pnpm add -g kajji
|
|
45
|
+
|
|
46
|
+
# or run directly
|
|
47
|
+
bunx kajji
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### From source
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/eliaskc/kajji.git
|
|
54
|
+
cd kajji
|
|
55
|
+
bun install
|
|
56
|
+
bun dev
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
Run `kajji` in any jj repository:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
kajji
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Keybindings
|
|
68
|
+
|
|
69
|
+
| Key | Action |
|
|
70
|
+
| --- | ------ |
|
|
71
|
+
| `j` / `k` | Move down / up |
|
|
72
|
+
| `Tab` | Cycle focus between panels |
|
|
73
|
+
| `Enter` | Drill into commit / file |
|
|
74
|
+
| `Escape` | Back / close modal |
|
|
75
|
+
| `?` | Show help with fuzzy search |
|
|
76
|
+
| `q` | Quit |
|
|
77
|
+
|
|
78
|
+
### Operations
|
|
79
|
+
|
|
80
|
+
| Key | Action |
|
|
81
|
+
| --- | ------ |
|
|
82
|
+
| `n` | New change |
|
|
83
|
+
| `e` | Edit change |
|
|
84
|
+
| `d` | Describe change |
|
|
85
|
+
| `s` | Squash into parent |
|
|
86
|
+
| `a` | Abandon change |
|
|
87
|
+
| `u` / `U` | Undo / redo |
|
|
88
|
+
| `f` / `F` | Git fetch / fetch all |
|
|
89
|
+
| `p` / `P` | Git push / push all |
|
|
90
|
+
|
|
91
|
+
### Bookmarks
|
|
92
|
+
|
|
93
|
+
| Key | Action |
|
|
94
|
+
| --- | ------ |
|
|
95
|
+
| `c` | Create bookmark |
|
|
96
|
+
| `d` | Delete bookmark |
|
|
97
|
+
| `r` | Rename bookmark |
|
|
98
|
+
| `b` | Create bookmark on commit |
|
|
99
|
+
|
|
100
|
+
## Next up
|
|
101
|
+
|
|
102
|
+
- Multi-select for batch rebase and squash
|
|
103
|
+
- Search and filter (log, bookmarks, files)
|
|
104
|
+
- Workspaces tab (monitor agent commits across workspaces)
|
|
105
|
+
|
|
106
|
+
## Exploring
|
|
107
|
+
|
|
108
|
+
- Interactive `jj split` (file/hunk selection)
|
|
109
|
+
- Stacked PR creation and overview
|
|
110
|
+
- Configuration (user config file, theme switching)
|
|
111
|
+
|
|
112
|
+
See [PROJECT](./context/PROJECT.md) for the full plan.
|
|
113
|
+
|
|
114
|
+
## Built With
|
|
115
|
+
|
|
116
|
+
- [OpenTUI](https://github.com/sst/opentui) + [SolidJS](https://www.solidjs.com/) - Modern TypeScript TUI framework
|
|
117
|
+
- [Bun](https://bun.sh) - Fast JavaScript runtime
|
|
118
|
+
- [jj (Jujutsu)](https://github.com/martinvonz/jj) - Git-compatible VCS
|
|
119
|
+
|
|
120
|
+
## Related Projects
|
|
121
|
+
|
|
122
|
+
- [lazygit](https://github.com/jesseduffield/lazygit) - The inspiration for this project
|
|
123
|
+
- [jjui](https://github.com/idursun/jjui) - Go-based jj TUI
|
|
124
|
+
- [lazyjj](https://github.com/Cretezy/lazyjj) - Rust-based jj TUI
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
package/bin/kajji.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kajji",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A terminal UI for Jujutsu: the rudder for your jj",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"bun": ">=1.0.0"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"kajji": "./bin/kajji.js"
|
|
11
|
+
},
|
|
12
|
+
"files": ["bin", "src"],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "NODE_ENV=development bun --watch run src/index.tsx",
|
|
15
|
+
"test": "bun test",
|
|
16
|
+
"check": "tsc --noEmit",
|
|
17
|
+
"lint": "biome check .",
|
|
18
|
+
"lint:fix": "biome check --write ."
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@biomejs/biome": "^1.9.4",
|
|
22
|
+
"@types/bun": "latest"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"typescript": "^5"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@babel/core": "^7.28.5",
|
|
29
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
30
|
+
"@opentui/core": "^0.1.66",
|
|
31
|
+
"@opentui/solid": "^0.1.66",
|
|
32
|
+
"babel-preset-solid": "1.9.9",
|
|
33
|
+
"fuzzysort": "^3.1.0",
|
|
34
|
+
"ghostty-opentui": "^1.3.11",
|
|
35
|
+
"solid-js": "1.9.9"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"jj",
|
|
39
|
+
"jujutsu",
|
|
40
|
+
"tui",
|
|
41
|
+
"terminal",
|
|
42
|
+
"cli",
|
|
43
|
+
"vcs",
|
|
44
|
+
"version-control"
|
|
45
|
+
],
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/eliaskc/kajji.git"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/eliaskc/kajji#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/eliaskc/kajji/issues"
|
|
53
|
+
},
|
|
54
|
+
"author": "eliaskc",
|
|
55
|
+
"license": "MIT"
|
|
56
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { useRenderer } from "@opentui/solid"
|
|
2
|
+
import { onMount } from "solid-js"
|
|
3
|
+
import { jjGitFetch, jjGitPush } from "./commander/operations"
|
|
4
|
+
import { Layout } from "./components/Layout"
|
|
5
|
+
import { HelpModal } from "./components/modals/HelpModal"
|
|
6
|
+
import { BookmarksPanel } from "./components/panels/BookmarksPanel"
|
|
7
|
+
import { LogPanel } from "./components/panels/LogPanel"
|
|
8
|
+
import { MainArea } from "./components/panels/MainArea"
|
|
9
|
+
import { CommandProvider, useCommand } from "./context/command"
|
|
10
|
+
import { CommandLogProvider, useCommandLog } from "./context/commandlog"
|
|
11
|
+
import { DialogContainer, DialogProvider, useDialog } from "./context/dialog"
|
|
12
|
+
import { FocusProvider, useFocus } from "./context/focus"
|
|
13
|
+
import { KeybindProvider } from "./context/keybind"
|
|
14
|
+
import { LoadingProvider, useLoading } from "./context/loading"
|
|
15
|
+
import { SyncProvider, useSync } from "./context/sync"
|
|
16
|
+
import { ThemeProvider } from "./context/theme"
|
|
17
|
+
|
|
18
|
+
function AppContent() {
|
|
19
|
+
const renderer = useRenderer()
|
|
20
|
+
const { loadLog, loadBookmarks, refresh } = useSync()
|
|
21
|
+
const focus = useFocus()
|
|
22
|
+
const command = useCommand()
|
|
23
|
+
const dialog = useDialog()
|
|
24
|
+
const commandLog = useCommandLog()
|
|
25
|
+
const globalLoading = useLoading()
|
|
26
|
+
|
|
27
|
+
onMount(() => {
|
|
28
|
+
loadLog()
|
|
29
|
+
loadBookmarks()
|
|
30
|
+
|
|
31
|
+
renderer.console.keyBindings = [
|
|
32
|
+
{ name: "y", ctrl: true, action: "copy-selection" },
|
|
33
|
+
]
|
|
34
|
+
renderer.console.onCopySelection = (text) => {
|
|
35
|
+
const proc = Bun.spawn(["pbcopy"], { stdin: "pipe" })
|
|
36
|
+
proc.stdin.write(text)
|
|
37
|
+
proc.stdin.end()
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
command.register(() => [
|
|
42
|
+
{
|
|
43
|
+
id: "global.quit",
|
|
44
|
+
title: "quit",
|
|
45
|
+
keybind: "quit",
|
|
46
|
+
context: "global",
|
|
47
|
+
type: "action",
|
|
48
|
+
onSelect: () => {
|
|
49
|
+
renderer.destroy()
|
|
50
|
+
process.exit(0)
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
...(Bun.env.NODE_ENV === "development"
|
|
54
|
+
? [
|
|
55
|
+
{
|
|
56
|
+
id: "global.toggle_console",
|
|
57
|
+
title: "toggle console",
|
|
58
|
+
keybind: "toggle_console" as const,
|
|
59
|
+
context: "global" as const,
|
|
60
|
+
type: "action" as const,
|
|
61
|
+
onSelect: () => renderer.console.toggle(),
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
: []),
|
|
65
|
+
{
|
|
66
|
+
id: "global.focus_next",
|
|
67
|
+
title: "focus next panel",
|
|
68
|
+
keybind: "focus_next",
|
|
69
|
+
context: "global",
|
|
70
|
+
type: "navigation",
|
|
71
|
+
visibility: "help-only",
|
|
72
|
+
onSelect: () => focus.cycleNext(),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "global.focus_prev",
|
|
76
|
+
title: "focus previous panel",
|
|
77
|
+
keybind: "focus_prev",
|
|
78
|
+
context: "global",
|
|
79
|
+
type: "navigation",
|
|
80
|
+
visibility: "help-only",
|
|
81
|
+
onSelect: () => focus.cyclePrev(),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "global.focus_panel_1",
|
|
85
|
+
title: "focus log panel",
|
|
86
|
+
keybind: "focus_panel_1",
|
|
87
|
+
context: "global",
|
|
88
|
+
type: "navigation",
|
|
89
|
+
visibility: "help-only",
|
|
90
|
+
onSelect: () => focus.setPanel("log"),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "global.focus_panel_2",
|
|
94
|
+
title: "focus refs panel",
|
|
95
|
+
keybind: "focus_panel_2",
|
|
96
|
+
context: "global",
|
|
97
|
+
type: "navigation",
|
|
98
|
+
visibility: "help-only",
|
|
99
|
+
onSelect: () => focus.setPanel("refs"),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "global.focus_panel_3",
|
|
103
|
+
title: "focus detail panel",
|
|
104
|
+
keybind: "focus_panel_3",
|
|
105
|
+
context: "global",
|
|
106
|
+
type: "navigation",
|
|
107
|
+
visibility: "help-only",
|
|
108
|
+
onSelect: () => focus.setPanel("detail"),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "global.help",
|
|
112
|
+
title: "help",
|
|
113
|
+
keybind: "help",
|
|
114
|
+
context: "global",
|
|
115
|
+
type: "action",
|
|
116
|
+
onSelect: () =>
|
|
117
|
+
dialog.toggle("help", () => <HelpModal />, {
|
|
118
|
+
hints: [{ key: "enter", label: "execute" }],
|
|
119
|
+
}),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "global.refresh",
|
|
123
|
+
title: "refresh",
|
|
124
|
+
keybind: "refresh",
|
|
125
|
+
context: "global",
|
|
126
|
+
type: "action",
|
|
127
|
+
visibility: "help-only",
|
|
128
|
+
onSelect: () => refresh(),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "global.git_fetch",
|
|
132
|
+
title: "git fetch",
|
|
133
|
+
keybind: "jj_git_fetch",
|
|
134
|
+
context: "global",
|
|
135
|
+
type: "git",
|
|
136
|
+
visibility: "help-only",
|
|
137
|
+
onSelect: async () => {
|
|
138
|
+
const result = await globalLoading.run("Fetching...", () =>
|
|
139
|
+
jjGitFetch(),
|
|
140
|
+
)
|
|
141
|
+
commandLog.addEntry(result)
|
|
142
|
+
if (result.success) {
|
|
143
|
+
refresh()
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "global.git_fetch_all",
|
|
149
|
+
title: "git fetch all",
|
|
150
|
+
keybind: "jj_git_fetch_all",
|
|
151
|
+
context: "global",
|
|
152
|
+
type: "git",
|
|
153
|
+
visibility: "help-only",
|
|
154
|
+
onSelect: async () => {
|
|
155
|
+
const result = await globalLoading.run("Fetching all...", () =>
|
|
156
|
+
jjGitFetch({ allRemotes: true }),
|
|
157
|
+
)
|
|
158
|
+
commandLog.addEntry(result)
|
|
159
|
+
if (result.success) {
|
|
160
|
+
refresh()
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "global.git_push",
|
|
166
|
+
title: "git push",
|
|
167
|
+
keybind: "jj_git_push",
|
|
168
|
+
context: "global",
|
|
169
|
+
type: "git",
|
|
170
|
+
visibility: "help-only",
|
|
171
|
+
onSelect: async () => {
|
|
172
|
+
const result = await globalLoading.run("Pushing...", () => jjGitPush())
|
|
173
|
+
commandLog.addEntry(result)
|
|
174
|
+
if (result.success) {
|
|
175
|
+
refresh()
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "global.git_push_all",
|
|
181
|
+
title: "git push all",
|
|
182
|
+
keybind: "jj_git_push_all",
|
|
183
|
+
context: "global",
|
|
184
|
+
type: "git",
|
|
185
|
+
visibility: "help-only",
|
|
186
|
+
onSelect: async () => {
|
|
187
|
+
const result = await globalLoading.run("Pushing all...", () =>
|
|
188
|
+
jjGitPush({ all: true }),
|
|
189
|
+
)
|
|
190
|
+
commandLog.addEntry(result)
|
|
191
|
+
if (result.success) {
|
|
192
|
+
refresh()
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
])
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<DialogContainer>
|
|
200
|
+
<Layout
|
|
201
|
+
top={<LogPanel />}
|
|
202
|
+
bottom={<BookmarksPanel />}
|
|
203
|
+
right={<MainArea />}
|
|
204
|
+
/>
|
|
205
|
+
</DialogContainer>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function App() {
|
|
210
|
+
return (
|
|
211
|
+
<ThemeProvider>
|
|
212
|
+
<FocusProvider>
|
|
213
|
+
<LoadingProvider>
|
|
214
|
+
<SyncProvider>
|
|
215
|
+
<KeybindProvider>
|
|
216
|
+
<CommandLogProvider>
|
|
217
|
+
<DialogProvider>
|
|
218
|
+
<CommandProvider>
|
|
219
|
+
<AppContent />
|
|
220
|
+
</CommandProvider>
|
|
221
|
+
</DialogProvider>
|
|
222
|
+
</CommandLogProvider>
|
|
223
|
+
</KeybindProvider>
|
|
224
|
+
</SyncProvider>
|
|
225
|
+
</LoadingProvider>
|
|
226
|
+
</FocusProvider>
|
|
227
|
+
</ThemeProvider>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { execute } from "./executor"
|
|
2
|
+
import type { OperationResult } from "./operations"
|
|
3
|
+
|
|
4
|
+
export interface Bookmark {
|
|
5
|
+
name: string
|
|
6
|
+
changeId: string
|
|
7
|
+
commitId: string
|
|
8
|
+
description: string
|
|
9
|
+
isLocal: boolean
|
|
10
|
+
remote?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FetchBookmarksOptions {
|
|
14
|
+
cwd?: string
|
|
15
|
+
allRemotes?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function fetchBookmarks(
|
|
19
|
+
options: FetchBookmarksOptions = {},
|
|
20
|
+
): Promise<Bookmark[]> {
|
|
21
|
+
const args = ["bookmark", "list"]
|
|
22
|
+
|
|
23
|
+
if (options.allRemotes) {
|
|
24
|
+
args.push("--all-remotes")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = await execute(args, { cwd: options.cwd })
|
|
28
|
+
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
throw new Error(`jj bookmark list failed: ${result.stderr}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return parseBookmarkOutput(result.stdout)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseBookmarkOutput(output: string): Bookmark[] {
|
|
37
|
+
const bookmarks: Bookmark[] = []
|
|
38
|
+
const lines = output.split("\n")
|
|
39
|
+
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (!line.trim()) continue
|
|
42
|
+
|
|
43
|
+
const isRemote = line.startsWith(" @")
|
|
44
|
+
|
|
45
|
+
if (isRemote) {
|
|
46
|
+
const match = line.match(/^\s+@(\S+):\s+(\S+)\s+(\S+)\s*(.*)$/)
|
|
47
|
+
if (match) {
|
|
48
|
+
bookmarks.push({
|
|
49
|
+
name: match[1] ?? "",
|
|
50
|
+
changeId: match[2] ?? "",
|
|
51
|
+
commitId: match[3] ?? "",
|
|
52
|
+
description: match[4]?.trim() ?? "",
|
|
53
|
+
isLocal: false,
|
|
54
|
+
remote: match[1],
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
const match = line.match(/^(\S+):\s+(\S+)\s+(\S+)\s*(.*)$/)
|
|
59
|
+
if (match) {
|
|
60
|
+
bookmarks.push({
|
|
61
|
+
name: match[1] ?? "",
|
|
62
|
+
changeId: match[2] ?? "",
|
|
63
|
+
commitId: match[3] ?? "",
|
|
64
|
+
description: match[4]?.trim() ?? "",
|
|
65
|
+
isLocal: true,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return bookmarks
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function jjBookmarkCreate(
|
|
75
|
+
name: string,
|
|
76
|
+
options?: { revision?: string },
|
|
77
|
+
): Promise<OperationResult> {
|
|
78
|
+
const args = ["bookmark", "create", name]
|
|
79
|
+
if (options?.revision) {
|
|
80
|
+
args.push("-r", options.revision)
|
|
81
|
+
}
|
|
82
|
+
const result = await execute(args)
|
|
83
|
+
return {
|
|
84
|
+
...result,
|
|
85
|
+
command: `jj ${args.join(" ")}`,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function jjBookmarkDelete(name: string): Promise<OperationResult> {
|
|
90
|
+
const args = ["bookmark", "delete", name]
|
|
91
|
+
const result = await execute(args)
|
|
92
|
+
return {
|
|
93
|
+
...result,
|
|
94
|
+
command: `jj ${args.join(" ")}`,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function jjBookmarkRename(
|
|
99
|
+
oldName: string,
|
|
100
|
+
newName: string,
|
|
101
|
+
): Promise<OperationResult> {
|
|
102
|
+
const args = ["bookmark", "rename", oldName, newName]
|
|
103
|
+
const result = await execute(args)
|
|
104
|
+
return {
|
|
105
|
+
...result,
|
|
106
|
+
command: `jj ${args.join(" ")}`,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function jjBookmarkForget(name: string): Promise<OperationResult> {
|
|
111
|
+
const args = ["bookmark", "forget", name]
|
|
112
|
+
const result = await execute(args)
|
|
113
|
+
return {
|
|
114
|
+
...result,
|
|
115
|
+
command: `jj ${args.join(" ")}`,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function jjBookmarkSet(
|
|
120
|
+
name: string,
|
|
121
|
+
revision: string,
|
|
122
|
+
): Promise<OperationResult> {
|
|
123
|
+
const args = ["bookmark", "set", name, "-r", revision]
|
|
124
|
+
const result = await execute(args)
|
|
125
|
+
return {
|
|
126
|
+
...result,
|
|
127
|
+
command: `jj ${args.join(" ")}`,
|
|
128
|
+
}
|
|
129
|
+
}
|