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
@@ -1,129 +0,0 @@
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
- }
@@ -1,186 +0,0 @@
1
- import {
2
- type PTYStreamingOptions,
3
- execute,
4
- executePTYStreaming,
5
- executeStreaming,
6
- } from "./executor"
7
-
8
- const PROFILE = process.env.KAJJI_PROFILE === "1"
9
-
10
- function profile(label: string) {
11
- if (!PROFILE) return () => {}
12
- const start = performance.now()
13
- return (extra?: string) => {
14
- const ms = (performance.now() - start).toFixed(2)
15
- console.error(`[PROFILE] ${label}: ${ms}ms${extra ? ` (${extra})` : ""}`)
16
- }
17
- }
18
-
19
- export interface FetchDiffOptions {
20
- cwd?: string
21
- columns?: number
22
- paths?: string[]
23
- }
24
-
25
- export async function fetchDiff(
26
- changeId: string,
27
- options: FetchDiffOptions = {},
28
- ): Promise<string> {
29
- const endTotal = profile(`fetchDiff(${changeId.slice(0, 8)})`)
30
-
31
- const env: Record<string, string> = {}
32
-
33
- if (options.columns) {
34
- env.COLUMNS = String(options.columns)
35
- }
36
-
37
- const args = [
38
- "diff",
39
- "-r",
40
- changeId,
41
- "--color",
42
- "always",
43
- "--ignore-working-copy",
44
- ]
45
-
46
- if (options.paths && options.paths.length > 0) {
47
- args.push(...options.paths)
48
- }
49
-
50
- const result = await execute(args, { cwd: options.cwd, env })
51
-
52
- if (!result.success) {
53
- throw new Error(`jj diff failed: ${result.stderr}`)
54
- }
55
-
56
- endTotal()
57
- return result.stdout
58
- }
59
-
60
- export interface StreamDiffCallbacks {
61
- onUpdate: (content: string, lineCount: number, complete: boolean) => void
62
- onError: (error: Error) => void
63
- }
64
-
65
- export function streamDiff(
66
- changeId: string,
67
- options: FetchDiffOptions,
68
- callbacks: StreamDiffCallbacks,
69
- ): { cancel: () => void } {
70
- const endTotal = profile(`streamDiff(${changeId.slice(0, 8)})`)
71
-
72
- const env: Record<string, string> = {}
73
-
74
- if (options.columns) {
75
- env.COLUMNS = String(options.columns)
76
- }
77
-
78
- const args = [
79
- "diff",
80
- "-r",
81
- changeId,
82
- "--color",
83
- "always",
84
- "--ignore-working-copy",
85
- ]
86
-
87
- if (options.paths && options.paths.length > 0) {
88
- args.push(...options.paths)
89
- }
90
-
91
- let firstUpdate = true
92
- const endFirstChunk = profile(" first chunk")
93
-
94
- return executeStreaming(
95
- args,
96
- { cwd: options.cwd, env },
97
- {
98
- onChunk: (content, lineCount) => {
99
- if (firstUpdate) {
100
- endFirstChunk(`${lineCount} lines`)
101
- firstUpdate = false
102
- }
103
- callbacks.onUpdate(content, lineCount, false)
104
- },
105
- onComplete: (result) => {
106
- endTotal()
107
- if (result.success) {
108
- callbacks.onUpdate(
109
- result.stdout,
110
- result.stdout.split("\n").length,
111
- true,
112
- )
113
- } else {
114
- callbacks.onError(new Error(`jj diff failed: ${result.stderr}`))
115
- }
116
- },
117
- onError: callbacks.onError,
118
- },
119
- )
120
- }
121
-
122
- export interface StreamDiffPTYOptions extends FetchDiffOptions {
123
- cols?: number
124
- rows?: number
125
- }
126
-
127
- export function streamDiffPTY(
128
- changeId: string,
129
- options: StreamDiffPTYOptions,
130
- callbacks: StreamDiffCallbacks,
131
- ): { cancel: () => void } {
132
- const endTotal = profile(`streamDiffPTY(${changeId.slice(0, 8)})`)
133
-
134
- const env: Record<string, string> = {}
135
-
136
- if (options.columns) {
137
- env.COLUMNS = String(options.columns)
138
- }
139
-
140
- const args = [
141
- "diff",
142
- "-r",
143
- changeId,
144
- "--color",
145
- "always",
146
- "--no-pager",
147
- "--ignore-working-copy",
148
- ]
149
-
150
- if (options.paths && options.paths.length > 0) {
151
- args.push(...options.paths)
152
- }
153
-
154
- let firstUpdate = true
155
- const endFirstChunk = profile(" PTY first chunk")
156
-
157
- const ptyOptions: PTYStreamingOptions = {
158
- cwd: options.cwd,
159
- env,
160
- cols: options.cols,
161
- rows: options.rows,
162
- }
163
-
164
- return executePTYStreaming(args, ptyOptions, {
165
- onChunk: (content, lineCount) => {
166
- if (firstUpdate) {
167
- endFirstChunk(`${lineCount} lines`)
168
- firstUpdate = false
169
- }
170
- callbacks.onUpdate(content, lineCount, false)
171
- },
172
- onComplete: (result) => {
173
- endTotal()
174
- if (result.success) {
175
- callbacks.onUpdate(
176
- result.stdout,
177
- result.stdout.split("\n").length,
178
- true,
179
- )
180
- } else {
181
- callbacks.onError(new Error(`jj diff failed: ${result.stderr}`))
182
- }
183
- },
184
- onError: callbacks.onError,
185
- })
186
- }
@@ -1,285 +0,0 @@
1
- export interface ExecuteResult {
2
- stdout: string
3
- stderr: string
4
- exitCode: number
5
- success: boolean
6
- }
7
-
8
- export interface ExecuteOptions {
9
- cwd?: string
10
- env?: Record<string, string>
11
- timeout?: number
12
- }
13
-
14
- const PROFILE = process.env.KAJJI_PROFILE === "1"
15
-
16
- function profile(label: string) {
17
- if (!PROFILE) return () => {}
18
- const start = performance.now()
19
- return (extra?: string) => {
20
- const ms = (performance.now() - start).toFixed(2)
21
- console.error(`[PROFILE] ${label}: ${ms}ms${extra ? ` (${extra})` : ""}`)
22
- }
23
- }
24
-
25
- export async function execute(
26
- args: string[],
27
- options: ExecuteOptions = {},
28
- ): Promise<ExecuteResult> {
29
- const endTotal = profile(`execute [jj ${args[0]}]`)
30
- const endSpawn = profile(" spawn")
31
-
32
- const proc = Bun.spawn(["jj", ...args], {
33
- cwd: options.cwd,
34
- env: {
35
- ...process.env,
36
- // Prevent jj from opening editors
37
- JJ_EDITOR: "true",
38
- EDITOR: "true",
39
- VISUAL: "true",
40
- ...options.env,
41
- },
42
- stdin: "ignore",
43
- stdout: "pipe",
44
- stderr: "pipe",
45
- })
46
- endSpawn()
47
-
48
- const endRead = profile(" read stdout/stderr")
49
- const [stdout, stderr] = await Promise.all([
50
- new Response(proc.stdout).text(),
51
- new Response(proc.stderr).text(),
52
- ])
53
- endRead(`${stdout.length} chars, ${stdout.split("\n").length} lines`)
54
-
55
- const endWait = profile(" wait for exit")
56
- const exitCode = await proc.exited
57
- endWait()
58
-
59
- endTotal()
60
-
61
- return {
62
- stdout,
63
- stderr,
64
- exitCode,
65
- success: exitCode === 0,
66
- }
67
- }
68
-
69
- export async function executeWithColor(
70
- args: string[],
71
- options: ExecuteOptions = {},
72
- ): Promise<ExecuteResult> {
73
- return execute(["--color", "always", ...args], options)
74
- }
75
-
76
- export interface StreamingExecuteCallbacks {
77
- onChunk: (content: string, lineCount: number) => void
78
- onComplete: (result: ExecuteResult) => void
79
- onError: (error: Error) => void
80
- }
81
-
82
- export function executeStreaming(
83
- args: string[],
84
- options: ExecuteOptions,
85
- callbacks: StreamingExecuteCallbacks,
86
- ): { cancel: () => void } {
87
- const endTotal = profile(`executeStreaming [jj ${args[0]}]`)
88
-
89
- const proc = Bun.spawn(["jj", ...args], {
90
- cwd: options.cwd,
91
- env: {
92
- ...process.env,
93
- JJ_EDITOR: "true",
94
- EDITOR: "true",
95
- VISUAL: "true",
96
- ...options.env,
97
- },
98
- stdin: "ignore",
99
- stdout: "pipe",
100
- stderr: "pipe",
101
- })
102
-
103
- let cancelled = false
104
- let stdout = ""
105
- let lineCount = 0
106
-
107
- const readStream = async () => {
108
- try {
109
- const reader = proc.stdout.getReader()
110
- const decoder = new TextDecoder()
111
- let chunkCount = 0
112
-
113
- while (!cancelled) {
114
- const { done, value } = await reader.read()
115
- if (done) break
116
-
117
- chunkCount++
118
- const chunk = decoder.decode(value, { stream: true })
119
- stdout += chunk
120
-
121
- const newLines = chunk.split("\n").length - 1
122
- lineCount += newLines
123
-
124
- if (PROFILE) {
125
- console.error(
126
- `[PROFILE] chunk #${chunkCount}: ${chunk.length} bytes, ${newLines} lines, total ${lineCount} lines`,
127
- )
128
- }
129
-
130
- callbacks.onChunk(stdout, lineCount)
131
-
132
- await new Promise((r) => setImmediate(r))
133
- }
134
-
135
- const stderr = await new Response(proc.stderr).text()
136
- const exitCode = await proc.exited
137
-
138
- endTotal()
139
-
140
- if (!cancelled) {
141
- callbacks.onComplete({
142
- stdout,
143
- stderr,
144
- exitCode,
145
- success: exitCode === 0,
146
- })
147
- }
148
- } catch (error) {
149
- if (!cancelled) {
150
- callbacks.onError(
151
- error instanceof Error ? error : new Error(String(error)),
152
- )
153
- }
154
- }
155
- }
156
-
157
- readStream()
158
-
159
- return {
160
- cancel: () => {
161
- cancelled = true
162
- proc.kill()
163
- },
164
- }
165
- }
166
-
167
- export interface PTYStreamingOptions extends ExecuteOptions {
168
- cols?: number
169
- rows?: number
170
- }
171
-
172
- /**
173
- * Execute jj command via PTY for true streaming output.
174
- * Uses Bun.spawn with terminal option (requires Bun 1.3.5+).
175
- * Falls back to regular executeStreaming on Windows or if PTY fails.
176
- */
177
- export function executePTYStreaming(
178
- args: string[],
179
- options: PTYStreamingOptions,
180
- callbacks: StreamingExecuteCallbacks,
181
- ): { cancel: () => void } {
182
- const endTotal = profile(`executePTYStreaming [jj ${args[0]}]`)
183
-
184
- const cols = options.cols || 120
185
- const rows = options.rows || 50
186
- const cwd = options.cwd || process.cwd()
187
-
188
- let cancelled = false
189
- let stdout = ""
190
- let lineCount = 0
191
- let chunkCount = 0
192
- let pendingUpdate = false
193
- let lastUpdateTime = 0
194
- const UPDATE_INTERVAL = 60
195
- const startTime = performance.now()
196
-
197
- try {
198
- const proc = Bun.spawn(["jj", ...args], {
199
- cwd,
200
- env: {
201
- ...process.env,
202
- JJ_EDITOR: "true",
203
- EDITOR: "true",
204
- VISUAL: "true",
205
- ...options.env,
206
- },
207
- terminal: {
208
- cols,
209
- rows,
210
- data(_terminal, data) {
211
- if (cancelled) return
212
-
213
- chunkCount++
214
- const chunk = data.toString()
215
- stdout += chunk
216
-
217
- if (chunkCount === 1 && PROFILE) {
218
- const firstChunkTime = performance.now() - startTime
219
- console.error(
220
- `[PROFILE] PTY first chunk: ${firstChunkTime.toFixed(0)}ms, ${chunk.length} bytes`,
221
- )
222
- }
223
-
224
- let newLines = 0
225
- for (let i = 0; i < chunk.length; i++) {
226
- if (chunk[i] === "\n") newLines++
227
- }
228
- lineCount += newLines
229
-
230
- if (PROFILE && chunkCount <= 5) {
231
- console.error(
232
- `[PROFILE] PTY chunk #${chunkCount}: ${chunk.length} bytes, ${newLines} lines, total ${lineCount} lines`,
233
- )
234
- }
235
-
236
- const now = performance.now()
237
- const shouldUpdate =
238
- chunkCount === 1 || now - lastUpdateTime >= UPDATE_INTERVAL
239
- if (shouldUpdate) {
240
- lastUpdateTime = now
241
- callbacks.onChunk(stdout, lineCount)
242
- } else if (!pendingUpdate) {
243
- pendingUpdate = true
244
- setTimeout(() => {
245
- pendingUpdate = false
246
- if (!cancelled) {
247
- lastUpdateTime = performance.now()
248
- callbacks.onChunk(stdout, lineCount)
249
- }
250
- }, UPDATE_INTERVAL)
251
- }
252
- },
253
- },
254
- })
255
-
256
- proc.exited.then((exitCode) => {
257
- if (cancelled) return
258
-
259
- proc.terminal?.close()
260
- endTotal()
261
-
262
- callbacks.onComplete({
263
- stdout,
264
- stderr: "",
265
- exitCode,
266
- success: exitCode === 0,
267
- })
268
- })
269
-
270
- return {
271
- cancel: () => {
272
- cancelled = true
273
- proc.terminal?.close()
274
- proc.kill()
275
- },
276
- }
277
- } catch (error) {
278
- if (PROFILE) {
279
- console.error(
280
- `[PROFILE] PTY error: ${error}, falling back to pipe streaming`,
281
- )
282
- }
283
- return executeStreaming(args, options, callbacks)
284
- }
285
- }
@@ -1,87 +0,0 @@
1
- import { execute } from "./executor"
2
- import type { FileChange, FileStatus } from "./types"
3
-
4
- const STATUS_MAP: Record<string, FileStatus> = {
5
- A: "added",
6
- M: "modified",
7
- D: "deleted",
8
- R: "renamed",
9
- C: "copied",
10
- }
11
-
12
- const BRACED_RENAME_REGEX = /^(.*)\{(.+) => (.+)\}(.*)$/
13
-
14
- function parseRenamedPath(rawPath: string): {
15
- oldPath: string
16
- newPath: string
17
- } {
18
- const match = rawPath.match(BRACED_RENAME_REGEX)
19
- if (match?.[2] && match[3]) {
20
- const prefix = match[1] ?? ""
21
- const oldPart = match[2]
22
- const newPart = match[3]
23
- const suffix = match[4] ?? ""
24
- return {
25
- oldPath: prefix + oldPart + suffix,
26
- newPath: prefix + newPart + suffix,
27
- }
28
- }
29
-
30
- const arrowIndex = rawPath.indexOf(" => ")
31
- if (arrowIndex !== -1) {
32
- return {
33
- oldPath: rawPath.slice(0, arrowIndex),
34
- newPath: rawPath.slice(arrowIndex + 4),
35
- }
36
- }
37
-
38
- return { oldPath: rawPath, newPath: rawPath }
39
- }
40
-
41
- export function parseFileSummary(output: string): FileChange[] {
42
- const files: FileChange[] = []
43
-
44
- for (const line of output.split("\n")) {
45
- const trimmed = line.trim()
46
- if (!trimmed) continue
47
-
48
- const statusChar = trimmed[0]
49
- if (!statusChar) continue
50
- const status = STATUS_MAP[statusChar]
51
- if (!status) continue
52
-
53
- const rawPath = trimmed.slice(2)
54
-
55
- if (status === "renamed" || status === "copied") {
56
- const { oldPath, newPath } = parseRenamedPath(rawPath)
57
- files.push({ path: newPath, status, oldPath })
58
- } else {
59
- files.push({ path: rawPath, status })
60
- }
61
- }
62
-
63
- return files
64
- }
65
-
66
- export interface FetchFilesOptions {
67
- ignoreWorkingCopy?: boolean
68
- }
69
-
70
- export async function fetchFiles(
71
- changeId: string,
72
- options: FetchFilesOptions = {},
73
- ): Promise<FileChange[]> {
74
- const args = ["diff", "--summary", "-r", changeId]
75
-
76
- if (options.ignoreWorkingCopy) {
77
- args.push("--ignore-working-copy")
78
- }
79
-
80
- const result = await execute(args)
81
-
82
- if (!result.success) {
83
- throw new Error(`Failed to fetch files: ${result.stderr}`)
84
- }
85
-
86
- return parseFileSummary(result.stdout)
87
- }