kajji 0.1.0 → 0.1.1

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.
Files changed (50) hide show
  1. package/bin/kajji +60 -0
  2. package/package.json +35 -55
  3. package/script/postinstall.mjs +50 -0
  4. package/LICENSE +0 -21
  5. package/README.md +0 -128
  6. package/bin/kajji.js +0 -2
  7. package/src/App.tsx +0 -229
  8. package/src/commander/bookmarks.ts +0 -129
  9. package/src/commander/diff.ts +0 -186
  10. package/src/commander/executor.ts +0 -285
  11. package/src/commander/files.ts +0 -87
  12. package/src/commander/log.ts +0 -99
  13. package/src/commander/operations.ts +0 -313
  14. package/src/commander/types.ts +0 -21
  15. package/src/components/AnsiText.tsx +0 -77
  16. package/src/components/BorderBox.tsx +0 -124
  17. package/src/components/FileTreeList.tsx +0 -105
  18. package/src/components/Layout.tsx +0 -48
  19. package/src/components/Panel.tsx +0 -143
  20. package/src/components/RevisionPicker.tsx +0 -165
  21. package/src/components/StatusBar.tsx +0 -158
  22. package/src/components/modals/BookmarkNameModal.tsx +0 -170
  23. package/src/components/modals/DescribeModal.tsx +0 -124
  24. package/src/components/modals/HelpModal.tsx +0 -372
  25. package/src/components/modals/RevisionPickerModal.tsx +0 -70
  26. package/src/components/modals/UndoModal.tsx +0 -75
  27. package/src/components/panels/BookmarksPanel.tsx +0 -768
  28. package/src/components/panels/CommandLogPanel.tsx +0 -40
  29. package/src/components/panels/LogPanel.tsx +0 -774
  30. package/src/components/panels/MainArea.tsx +0 -354
  31. package/src/context/command.tsx +0 -106
  32. package/src/context/commandlog.tsx +0 -45
  33. package/src/context/dialog.tsx +0 -217
  34. package/src/context/focus.tsx +0 -63
  35. package/src/context/helper.tsx +0 -24
  36. package/src/context/keybind.tsx +0 -51
  37. package/src/context/loading.tsx +0 -68
  38. package/src/context/sync.tsx +0 -868
  39. package/src/context/theme.tsx +0 -90
  40. package/src/context/types.ts +0 -51
  41. package/src/index.tsx +0 -15
  42. package/src/keybind/index.ts +0 -2
  43. package/src/keybind/parser.ts +0 -88
  44. package/src/keybind/types.ts +0 -83
  45. package/src/theme/index.ts +0 -3
  46. package/src/theme/presets/lazygit.ts +0 -45
  47. package/src/theme/presets/opencode.ts +0 -45
  48. package/src/theme/types.ts +0 -47
  49. package/src/utils/double-click.ts +0 -59
  50. package/src/utils/file-tree.ts +0 -154
package/bin/kajji ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+
3
+ const childProcess = require("child_process")
4
+ const fs = require("fs")
5
+ const path = require("path")
6
+ const os = require("os")
7
+
8
+ function run(target) {
9
+ const result = childProcess.spawnSync(target, process.argv.slice(2), {
10
+ stdio: "inherit",
11
+ })
12
+ if (result.error) {
13
+ console.error(result.error.message)
14
+ process.exit(1)
15
+ }
16
+ process.exit(typeof result.status === "number" ? result.status : 0)
17
+ }
18
+
19
+ const envPath = process.env.KAJJI_BIN_PATH
20
+ if (envPath) {
21
+ run(envPath)
22
+ }
23
+
24
+ const scriptPath = fs.realpathSync(__filename)
25
+ const scriptDir = path.dirname(scriptPath)
26
+
27
+ const platformMap = { darwin: "darwin", linux: "linux" }
28
+ const archMap = { x64: "x64", arm64: "arm64" }
29
+
30
+ const platform = platformMap[os.platform()] || os.platform()
31
+ const arch = archMap[os.arch()] || os.arch()
32
+ const base = "kajji-" + platform + "-" + arch
33
+
34
+ function findBinary(startDir) {
35
+ let current = startDir
36
+ for (;;) {
37
+ const modules = path.join(current, "node_modules")
38
+ if (fs.existsSync(modules)) {
39
+ for (const entry of fs.readdirSync(modules)) {
40
+ if (!entry.startsWith(base)) continue
41
+ const candidate = path.join(modules, entry, "bin", "kajji")
42
+ if (fs.existsSync(candidate)) return candidate
43
+ }
44
+ }
45
+ const parent = path.dirname(current)
46
+ if (parent === current) return
47
+ current = parent
48
+ }
49
+ }
50
+
51
+ const resolved = findBinary(scriptDir)
52
+ if (!resolved) {
53
+ console.error(
54
+ `Could not find kajji binary for your platform (${platform}-${arch}).\n` +
55
+ `Try installing the platform package: npm install ${base}`
56
+ )
57
+ process.exit(1)
58
+ }
59
+
60
+ run(resolved)
package/package.json CHANGED
@@ -1,56 +1,36 @@
1
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
- }
2
+ "name": "kajji",
3
+ "version": "0.1.1",
4
+ "description": "A terminal UI for Jujutsu: the rudder for your jj",
5
+ "bin": {
6
+ "kajji": "./bin/kajji"
7
+ },
8
+ "scripts": {
9
+ "postinstall": "node ./script/postinstall.mjs"
10
+ },
11
+ "optionalDependencies": {
12
+ "kajji-darwin-arm64": "0.1.1",
13
+ "kajji-darwin-x64": "0.1.1",
14
+ "kajji-linux-x64": "0.1.1",
15
+ "kajji-linux-arm64": "0.1.1"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/eliaskc/kajji.git"
20
+ },
21
+ "homepage": "https://github.com/eliaskc/kajji#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/eliaskc/kajji/issues"
24
+ },
25
+ "keywords": [
26
+ "jj",
27
+ "jujutsu",
28
+ "tui",
29
+ "terminal",
30
+ "cli",
31
+ "vcs",
32
+ "version-control"
33
+ ],
34
+ "author": "eliaskc",
35
+ "license": "MIT"
36
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs"
4
+ import os from "node:os"
5
+ import path from "node:path"
6
+ import { fileURLToPath } from "node:url"
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
+
10
+ const platformMap = { darwin: "darwin", linux: "linux" }
11
+ const archMap = { x64: "x64", arm64: "arm64" }
12
+
13
+ const platform = platformMap[os.platform()]
14
+ const arch = archMap[os.arch()]
15
+
16
+ if (!platform || !arch) {
17
+ console.log(`Unsupported platform: ${os.platform()}-${os.arch()}`)
18
+ process.exit(0)
19
+ }
20
+
21
+ const packageName = `kajji-${platform}-${arch}`
22
+
23
+ let packageDir
24
+ try {
25
+ const packageJsonPath = require.resolve(`${packageName}/package.json`, {
26
+ paths: [path.join(__dirname, "..")],
27
+ })
28
+ packageDir = path.dirname(packageJsonPath)
29
+ } catch {
30
+ console.log(`Platform package ${packageName} not found, skipping symlink`)
31
+ process.exit(0)
32
+ }
33
+
34
+ const binaryPath = path.join(packageDir, "bin", "kajji")
35
+ const targetPath = path.join(__dirname, "..", "bin", "kajji-binary")
36
+
37
+ if (!fs.existsSync(binaryPath)) {
38
+ console.log(`Binary not found at ${binaryPath}`)
39
+ process.exit(0)
40
+ }
41
+
42
+ try {
43
+ if (fs.existsSync(targetPath)) {
44
+ fs.unlinkSync(targetPath)
45
+ }
46
+ fs.symlinkSync(binaryPath, targetPath)
47
+ console.log(`kajji binary linked: ${targetPath} -> ${binaryPath}`)
48
+ } catch (err) {
49
+ console.log(`Could not create symlink: ${err.message}`)
50
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
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 DELETED
@@ -1,128 +0,0 @@
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
- ![kajji screenshot](./kajji.png)
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 DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env bun
2
- import "../src/index.tsx"
package/src/App.tsx DELETED
@@ -1,229 +0,0 @@
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
- }