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.
- package/bin/kajji +60 -0
- package/package.json +35 -55
- package/script/postinstall.mjs +50 -0
- package/LICENSE +0 -21
- package/README.md +0 -128
- package/bin/kajji.js +0 -2
- package/src/App.tsx +0 -229
- package/src/commander/bookmarks.ts +0 -129
- package/src/commander/diff.ts +0 -186
- package/src/commander/executor.ts +0 -285
- package/src/commander/files.ts +0 -87
- package/src/commander/log.ts +0 -99
- package/src/commander/operations.ts +0 -313
- package/src/commander/types.ts +0 -21
- package/src/components/AnsiText.tsx +0 -77
- package/src/components/BorderBox.tsx +0 -124
- package/src/components/FileTreeList.tsx +0 -105
- package/src/components/Layout.tsx +0 -48
- package/src/components/Panel.tsx +0 -143
- package/src/components/RevisionPicker.tsx +0 -165
- package/src/components/StatusBar.tsx +0 -158
- package/src/components/modals/BookmarkNameModal.tsx +0 -170
- package/src/components/modals/DescribeModal.tsx +0 -124
- package/src/components/modals/HelpModal.tsx +0 -372
- package/src/components/modals/RevisionPickerModal.tsx +0 -70
- package/src/components/modals/UndoModal.tsx +0 -75
- package/src/components/panels/BookmarksPanel.tsx +0 -768
- package/src/components/panels/CommandLogPanel.tsx +0 -40
- package/src/components/panels/LogPanel.tsx +0 -774
- package/src/components/panels/MainArea.tsx +0 -354
- package/src/context/command.tsx +0 -106
- package/src/context/commandlog.tsx +0 -45
- package/src/context/dialog.tsx +0 -217
- package/src/context/focus.tsx +0 -63
- package/src/context/helper.tsx +0 -24
- package/src/context/keybind.tsx +0 -51
- package/src/context/loading.tsx +0 -68
- package/src/context/sync.tsx +0 -868
- package/src/context/theme.tsx +0 -90
- package/src/context/types.ts +0 -51
- package/src/index.tsx +0 -15
- package/src/keybind/index.ts +0 -2
- package/src/keybind/parser.ts +0 -88
- package/src/keybind/types.ts +0 -83
- package/src/theme/index.ts +0 -3
- package/src/theme/presets/lazygit.ts +0 -45
- package/src/theme/presets/opencode.ts +0 -45
- package/src/theme/types.ts +0 -47
- package/src/utils/double-click.ts +0 -59
- 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
|
-
}
|
package/src/commander/diff.ts
DELETED
|
@@ -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
|
-
}
|
package/src/commander/files.ts
DELETED
|
@@ -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
|
-
}
|