kaizenai 0.3.0 → 0.4.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/bin/kaizen +16 -5
- package/dist/client/app-icon-180.png +0 -0
- package/dist/client/app-icon-192.png +0 -0
- package/dist/client/app-icon-512.png +0 -0
- package/dist/client/app-icon-96.png +0 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/index-BR0jGwm_.css +32 -0
- package/dist/client/assets/index-CYqy6l6r.js +628 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/index.html +17 -10
- package/dist/client/manifest-dark.webmanifest +3 -3
- package/dist/client/manifest.webmanifest +3 -3
- package/dist/client/pwa-192.png +0 -0
- package/dist/client/pwa-512.png +0 -0
- package/dist/server/cli-supervisor.js +307 -0
- package/dist/server/cli.js +12371 -0
- package/package.json +7 -9
- package/dist/client/assets/index-BBs80KD-.js +0 -623
- package/dist/client/assets/index-CkCgyLNq.css +0 -32
- package/src/server/acp-shared.ts +0 -315
- package/src/server/agent.ts +0 -1159
- package/src/server/attachments.ts +0 -133
- package/src/server/backgrounds.ts +0 -74
- package/src/server/cli-runtime.ts +0 -375
- package/src/server/cli-supervisor.ts +0 -97
- package/src/server/cli.ts +0 -68
- package/src/server/codex-app-server-protocol.ts +0 -453
- package/src/server/codex-app-server.ts +0 -1350
- package/src/server/cursor-acp.ts +0 -819
- package/src/server/discovery.ts +0 -322
- package/src/server/event-store.ts +0 -1470
- package/src/server/events.ts +0 -252
- package/src/server/external-open.ts +0 -272
- package/src/server/gemini-acp.ts +0 -844
- package/src/server/gemini-cli.ts +0 -525
- package/src/server/generate-title.ts +0 -36
- package/src/server/git-manager.ts +0 -79
- package/src/server/git-repository.ts +0 -101
- package/src/server/harness-types.ts +0 -20
- package/src/server/keybindings.ts +0 -177
- package/src/server/machine-name.ts +0 -22
- package/src/server/paths.ts +0 -112
- package/src/server/process-utils.ts +0 -22
- package/src/server/project-icon.ts +0 -352
- package/src/server/project-metadata.ts +0 -10
- package/src/server/provider-catalog.ts +0 -85
- package/src/server/provider-settings.ts +0 -155
- package/src/server/quick-response.ts +0 -153
- package/src/server/read-models.ts +0 -275
- package/src/server/recovery.ts +0 -507
- package/src/server/restart.ts +0 -56
- package/src/server/server.ts +0 -244
- package/src/server/terminal-manager.ts +0 -350
- package/src/server/theme-settings.ts +0 -179
- package/src/server/update-manager.ts +0 -230
- package/src/server/usage/base-provider-usage.ts +0 -57
- package/src/server/usage/claude-usage.ts +0 -558
- package/src/server/usage/codex-usage.ts +0 -144
- package/src/server/usage/cursor-browser.ts +0 -120
- package/src/server/usage/cursor-cookies.ts +0 -390
- package/src/server/usage/cursor-usage.ts +0 -490
- package/src/server/usage/gemini-usage.ts +0 -24
- package/src/server/usage/provider-usage.ts +0 -61
- package/src/server/usage/test-helpers.ts +0 -9
- package/src/server/usage/types.ts +0 -54
- package/src/server/usage/utils.ts +0 -325
- package/src/server/ws-router.ts +0 -742
- package/src/shared/branding.ts +0 -83
- package/src/shared/dev-ports.ts +0 -43
- package/src/shared/ports.ts +0 -2
- package/src/shared/protocol.ts +0 -156
- package/src/shared/tools.ts +0 -251
- package/src/shared/types.ts +0 -1040
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
import { resolveLocalPath } from "./paths"
|
|
3
|
-
|
|
4
|
-
export interface ProjectRepositoryIdentity {
|
|
5
|
-
isGitRepo: boolean
|
|
6
|
-
localPath: string
|
|
7
|
-
repoKey: string
|
|
8
|
-
repoRootPath: string | null
|
|
9
|
-
title: string
|
|
10
|
-
worktreePath: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function normalizeAbsolutePath(value: string, cwd: string) {
|
|
14
|
-
const resolved = path.isAbsolute(value) ? value : path.resolve(cwd, value)
|
|
15
|
-
return path.normalize(resolved)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function resolveProjectWorktreePaths(localPath: string): string[] {
|
|
19
|
-
const normalizedLocalPath = resolveLocalPath(localPath)
|
|
20
|
-
const result = Bun.spawnSync(
|
|
21
|
-
["git", "-C", normalizedLocalPath, "worktree", "list", "--porcelain"],
|
|
22
|
-
{
|
|
23
|
-
stdout: "pipe",
|
|
24
|
-
stderr: "pipe",
|
|
25
|
-
}
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
if (result.exitCode !== 0) {
|
|
29
|
-
return [normalizedLocalPath]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const worktreePaths: string[] = []
|
|
33
|
-
for (const line of new TextDecoder().decode(result.stdout).split("\n")) {
|
|
34
|
-
const match = line.match(/^worktree\s+(.+)$/)
|
|
35
|
-
if (!match?.[1]) continue
|
|
36
|
-
worktreePaths.push(normalizeAbsolutePath(match[1], normalizedLocalPath))
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return worktreePaths.length > 0 ? [...new Set(worktreePaths)] : [normalizedLocalPath]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function resolveProjectRepositoryIdentity(localPath: string): ProjectRepositoryIdentity {
|
|
43
|
-
const normalizedLocalPath = resolveLocalPath(localPath)
|
|
44
|
-
const fallbackTitle = path.basename(normalizedLocalPath) || normalizedLocalPath
|
|
45
|
-
const fallbackRepoKey = `path:${normalizedLocalPath}`
|
|
46
|
-
|
|
47
|
-
const result = Bun.spawnSync(
|
|
48
|
-
[
|
|
49
|
-
"git",
|
|
50
|
-
"-C",
|
|
51
|
-
normalizedLocalPath,
|
|
52
|
-
"rev-parse",
|
|
53
|
-
"--path-format=absolute",
|
|
54
|
-
"--show-toplevel",
|
|
55
|
-
"--git-common-dir",
|
|
56
|
-
],
|
|
57
|
-
{
|
|
58
|
-
stdout: "pipe",
|
|
59
|
-
stderr: "pipe",
|
|
60
|
-
}
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
if (result.exitCode !== 0) {
|
|
64
|
-
return {
|
|
65
|
-
isGitRepo: false,
|
|
66
|
-
localPath: normalizedLocalPath,
|
|
67
|
-
repoKey: fallbackRepoKey,
|
|
68
|
-
repoRootPath: null,
|
|
69
|
-
title: fallbackTitle,
|
|
70
|
-
worktreePath: normalizedLocalPath,
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const stdout = new TextDecoder().decode(result.stdout).trim()
|
|
75
|
-
const [repoRootPathRaw, gitCommonDirRaw] = stdout.split("\n").map((line) => line.trim())
|
|
76
|
-
if (!repoRootPathRaw || !gitCommonDirRaw) {
|
|
77
|
-
return {
|
|
78
|
-
isGitRepo: false,
|
|
79
|
-
localPath: normalizedLocalPath,
|
|
80
|
-
repoKey: fallbackRepoKey,
|
|
81
|
-
repoRootPath: null,
|
|
82
|
-
title: fallbackTitle,
|
|
83
|
-
worktreePath: normalizedLocalPath,
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const gitCommonDir = normalizeAbsolutePath(gitCommonDirRaw, normalizedLocalPath)
|
|
88
|
-
const repoRootPath = normalizeAbsolutePath(repoRootPathRaw, normalizedLocalPath)
|
|
89
|
-
const repositoryDisplayPath = path.basename(gitCommonDir) === ".git"
|
|
90
|
-
? path.dirname(gitCommonDir)
|
|
91
|
-
: repoRootPath
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
isGitRepo: true,
|
|
95
|
-
localPath: normalizedLocalPath,
|
|
96
|
-
repoKey: `git:${gitCommonDir}`,
|
|
97
|
-
repoRootPath: repositoryDisplayPath,
|
|
98
|
-
title: path.basename(repositoryDisplayPath) || fallbackTitle,
|
|
99
|
-
worktreePath: repoRootPath,
|
|
100
|
-
}
|
|
101
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { AccountInfo, AgentProvider, ChatUsageSnapshot, NormalizedToolCall, TranscriptEntry } from "../shared/types"
|
|
2
|
-
|
|
3
|
-
export interface HarnessEvent {
|
|
4
|
-
type: "transcript" | "session_token" | "usage"
|
|
5
|
-
entry?: TranscriptEntry
|
|
6
|
-
sessionToken?: string
|
|
7
|
-
usage?: ChatUsageSnapshot
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface HarnessToolRequest {
|
|
11
|
-
tool: NormalizedToolCall & { toolKind: "ask_user_question" | "exit_plan_mode" }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface HarnessTurn {
|
|
15
|
-
provider: AgentProvider
|
|
16
|
-
stream: AsyncIterable<HarnessEvent>
|
|
17
|
-
getAccountInfo?: () => Promise<AccountInfo | null>
|
|
18
|
-
interrupt: () => Promise<void>
|
|
19
|
-
close: () => void
|
|
20
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { watch, type FSWatcher } from "node:fs"
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
3
|
-
import { homedir } from "node:os"
|
|
4
|
-
import path from "node:path"
|
|
5
|
-
import { getKeybindingsFilePath, LOG_PREFIX } from "../shared/branding"
|
|
6
|
-
import { DEFAULT_KEYBINDINGS, type KeybindingAction, type KeybindingsSnapshot } from "../shared/types"
|
|
7
|
-
|
|
8
|
-
const KEYBINDING_ACTIONS = Object.keys(DEFAULT_KEYBINDINGS) as KeybindingAction[]
|
|
9
|
-
|
|
10
|
-
type KeybindingsFile = Partial<Record<KeybindingAction, unknown>>
|
|
11
|
-
|
|
12
|
-
export class KeybindingsManager {
|
|
13
|
-
readonly filePath: string
|
|
14
|
-
private watcher: FSWatcher | null = null
|
|
15
|
-
private snapshot: KeybindingsSnapshot
|
|
16
|
-
private readonly listeners = new Set<(snapshot: KeybindingsSnapshot) => void>()
|
|
17
|
-
|
|
18
|
-
constructor(filePath = getKeybindingsFilePath(homedir())) {
|
|
19
|
-
this.filePath = filePath
|
|
20
|
-
this.snapshot = createDefaultSnapshot(this.filePath)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async initialize() {
|
|
24
|
-
await mkdir(path.dirname(this.filePath), { recursive: true })
|
|
25
|
-
const file = Bun.file(this.filePath)
|
|
26
|
-
if (!(await file.exists())) {
|
|
27
|
-
await writeFile(this.filePath, `${JSON.stringify(DEFAULT_KEYBINDINGS, null, 2)}\n`, "utf8")
|
|
28
|
-
}
|
|
29
|
-
await this.reload()
|
|
30
|
-
this.startWatching()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
dispose() {
|
|
34
|
-
this.watcher?.close()
|
|
35
|
-
this.watcher = null
|
|
36
|
-
this.listeners.clear()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
getSnapshot() {
|
|
40
|
-
return this.snapshot
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
onChange(listener: (snapshot: KeybindingsSnapshot) => void) {
|
|
44
|
-
this.listeners.add(listener)
|
|
45
|
-
return () => {
|
|
46
|
-
this.listeners.delete(listener)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async reload() {
|
|
51
|
-
const nextSnapshot = await readKeybindingsSnapshot(this.filePath)
|
|
52
|
-
this.setSnapshot(nextSnapshot)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async write(bindings: Partial<Record<KeybindingAction, string[]>>) {
|
|
56
|
-
const nextSnapshot = normalizeKeybindings(bindings, this.filePath)
|
|
57
|
-
await mkdir(path.dirname(this.filePath), { recursive: true })
|
|
58
|
-
await writeFile(this.filePath, `${JSON.stringify(nextSnapshot.bindings, null, 2)}\n`, "utf8")
|
|
59
|
-
this.setSnapshot(nextSnapshot)
|
|
60
|
-
return nextSnapshot
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private setSnapshot(snapshot: KeybindingsSnapshot) {
|
|
64
|
-
this.snapshot = snapshot
|
|
65
|
-
for (const listener of this.listeners) {
|
|
66
|
-
listener(snapshot)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private startWatching() {
|
|
71
|
-
this.watcher?.close()
|
|
72
|
-
try {
|
|
73
|
-
this.watcher = watch(path.dirname(this.filePath), { persistent: false }, (_eventType, filename) => {
|
|
74
|
-
if (filename && filename !== path.basename(this.filePath)) {
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
void this.reload().catch((error: unknown) => {
|
|
78
|
-
console.warn(`${LOG_PREFIX} Failed to reload keybindings:`, error)
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
} catch (error) {
|
|
82
|
-
console.warn(`${LOG_PREFIX} Failed to watch keybindings file:`, error)
|
|
83
|
-
this.watcher = null
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export async function readKeybindingsSnapshot(filePath: string) {
|
|
89
|
-
try {
|
|
90
|
-
const text = await readFile(filePath, "utf8")
|
|
91
|
-
if (!text.trim()) {
|
|
92
|
-
return createDefaultSnapshot(filePath, "Keybindings file was empty. Using defaults.")
|
|
93
|
-
}
|
|
94
|
-
const parsed = JSON.parse(text) as KeybindingsFile
|
|
95
|
-
return normalizeKeybindings(parsed, filePath)
|
|
96
|
-
} catch (error) {
|
|
97
|
-
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
98
|
-
return createDefaultSnapshot(filePath)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (error instanceof SyntaxError) {
|
|
102
|
-
return createDefaultSnapshot(filePath, "Keybindings file is invalid JSON. Using defaults.")
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
throw error
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function normalizeKeybindings(value: KeybindingsFile | null | undefined, filePath = getKeybindingsFilePath(homedir())): KeybindingsSnapshot {
|
|
110
|
-
const warnings: string[] = []
|
|
111
|
-
const source = value && typeof value === "object" && !Array.isArray(value)
|
|
112
|
-
? value
|
|
113
|
-
: null
|
|
114
|
-
|
|
115
|
-
if (!source) {
|
|
116
|
-
return createDefaultSnapshot(filePath, "Keybindings file must contain a JSON object. Using defaults.")
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const bindings = {} as Record<KeybindingAction, string[]>
|
|
120
|
-
for (const action of KEYBINDING_ACTIONS) {
|
|
121
|
-
const rawValue = source[action]
|
|
122
|
-
if (!Array.isArray(rawValue)) {
|
|
123
|
-
bindings[action] = [...DEFAULT_KEYBINDINGS[action]]
|
|
124
|
-
if (rawValue !== undefined) {
|
|
125
|
-
warnings.push(`${action} must be an array of shortcut strings`)
|
|
126
|
-
}
|
|
127
|
-
continue
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const normalized = rawValue
|
|
131
|
-
.filter((entry): entry is string => typeof entry === "string")
|
|
132
|
-
.map((entry) => entry.trim())
|
|
133
|
-
.map((entry) => entry.toLowerCase())
|
|
134
|
-
.filter(Boolean)
|
|
135
|
-
|
|
136
|
-
if (normalized.length === 0) {
|
|
137
|
-
bindings[action] = [...DEFAULT_KEYBINDINGS[action]]
|
|
138
|
-
if (rawValue.length > 0 || source[action] !== undefined) {
|
|
139
|
-
warnings.push(`${action} did not contain any valid shortcut strings`)
|
|
140
|
-
}
|
|
141
|
-
continue
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
bindings[action] = normalized
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return {
|
|
148
|
-
bindings,
|
|
149
|
-
warning: warnings.length > 0 ? `Some keybindings were reset to defaults: ${warnings.join("; ")}` : null,
|
|
150
|
-
filePathDisplay: formatDisplayPath(filePath),
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function createDefaultSnapshot(filePath: string, warning: string | null = null): KeybindingsSnapshot {
|
|
155
|
-
return {
|
|
156
|
-
bindings: {
|
|
157
|
-
submitChatMessage: [...DEFAULT_KEYBINDINGS.submitChatMessage],
|
|
158
|
-
toggleProjectsSidebar: [...DEFAULT_KEYBINDINGS.toggleProjectsSidebar],
|
|
159
|
-
toggleEmbeddedTerminal: [...DEFAULT_KEYBINDINGS.toggleEmbeddedTerminal],
|
|
160
|
-
toggleRightSidebar: [...DEFAULT_KEYBINDINGS.toggleRightSidebar],
|
|
161
|
-
openInFinder: [...DEFAULT_KEYBINDINGS.openInFinder],
|
|
162
|
-
openInEditor: [...DEFAULT_KEYBINDINGS.openInEditor],
|
|
163
|
-
addSplitTerminal: [...DEFAULT_KEYBINDINGS.addSplitTerminal],
|
|
164
|
-
},
|
|
165
|
-
warning,
|
|
166
|
-
filePathDisplay: formatDisplayPath(filePath),
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function formatDisplayPath(filePath: string) {
|
|
171
|
-
const homePath = homedir()
|
|
172
|
-
if (filePath === homePath) return "~"
|
|
173
|
-
if (filePath.startsWith(`${homePath}${path.sep}`)) {
|
|
174
|
-
return `~${filePath.slice(homePath.length)}`
|
|
175
|
-
}
|
|
176
|
-
return filePath
|
|
177
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { hostname } from "node:os"
|
|
2
|
-
import process from "node:process"
|
|
3
|
-
import { spawnSync } from "node:child_process"
|
|
4
|
-
|
|
5
|
-
function runAndRead(command: string, args: string[]) {
|
|
6
|
-
const result = spawnSync(command, args, { encoding: "utf8" })
|
|
7
|
-
if (result.status !== 0) return null
|
|
8
|
-
const value = result.stdout.trim()
|
|
9
|
-
return value || null
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function getMachineDisplayName() {
|
|
13
|
-
if (process.platform === "darwin") {
|
|
14
|
-
const computerName = runAndRead("scutil", ["--get", "ComputerName"])
|
|
15
|
-
if (computerName) {
|
|
16
|
-
return computerName
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const rawHostname = hostname().trim()
|
|
21
|
-
return rawHostname.replace(/\.local$|\.lan$/i, "") || "This Machine"
|
|
22
|
-
}
|
package/src/server/paths.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { statSync } from "node:fs"
|
|
2
|
-
import { mkdir, readdir, stat } from "node:fs/promises"
|
|
3
|
-
import { homedir } from "node:os"
|
|
4
|
-
import path from "node:path"
|
|
5
|
-
import type { DirectoryBrowserSnapshot } from "../shared/types"
|
|
6
|
-
|
|
7
|
-
export function getDefaultDirectoryRoot() {
|
|
8
|
-
const home = homedir()
|
|
9
|
-
const candidates = [
|
|
10
|
-
path.join(home, "Documents"),
|
|
11
|
-
path.join(home, "projects"),
|
|
12
|
-
path.join(home, "Projects"),
|
|
13
|
-
home,
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
for (const candidate of candidates) {
|
|
17
|
-
try {
|
|
18
|
-
const info = statSync(candidate)
|
|
19
|
-
if (info.isDirectory()) {
|
|
20
|
-
return candidate
|
|
21
|
-
}
|
|
22
|
-
} catch {
|
|
23
|
-
continue
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (process.platform === "win32") {
|
|
28
|
-
return path.parse(home).root || "C:\\"
|
|
29
|
-
}
|
|
30
|
-
return "/"
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function resolveLocalPath(localPath: string) {
|
|
34
|
-
const trimmed = localPath.trim()
|
|
35
|
-
if (!trimmed) {
|
|
36
|
-
throw new Error("Project path is required")
|
|
37
|
-
}
|
|
38
|
-
if (trimmed === "~") {
|
|
39
|
-
return homedir()
|
|
40
|
-
}
|
|
41
|
-
if (trimmed.startsWith("~/")) {
|
|
42
|
-
return path.join(homedir(), trimmed.slice(2))
|
|
43
|
-
}
|
|
44
|
-
return path.resolve(trimmed)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function ensureProjectDirectory(localPath: string) {
|
|
48
|
-
const resolvedPath = resolveLocalPath(localPath)
|
|
49
|
-
|
|
50
|
-
await mkdir(resolvedPath, { recursive: true })
|
|
51
|
-
const info = await stat(resolvedPath)
|
|
52
|
-
if (!info.isDirectory()) {
|
|
53
|
-
throw new Error("Project path must be a directory")
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function requireProjectDirectory(localPath: string) {
|
|
58
|
-
const resolvedPath = resolveLocalPath(localPath)
|
|
59
|
-
const info = await stat(resolvedPath).catch(() => null)
|
|
60
|
-
if (!info) {
|
|
61
|
-
throw new Error(`Project folder not found: ${resolvedPath}`)
|
|
62
|
-
}
|
|
63
|
-
if (!info.isDirectory()) {
|
|
64
|
-
throw new Error("Project path must be a directory")
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function getDirectoryRoots() {
|
|
69
|
-
const home = homedir()
|
|
70
|
-
if (process.platform === "win32") {
|
|
71
|
-
const drive = path.parse(home).root || "C:\\"
|
|
72
|
-
return [
|
|
73
|
-
{ name: "Home", localPath: home },
|
|
74
|
-
{ name: drive, localPath: drive },
|
|
75
|
-
]
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return [
|
|
79
|
-
{ name: "Home", localPath: home },
|
|
80
|
-
{ name: "/", localPath: "/" },
|
|
81
|
-
]
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function listProjectDirectories(localPath?: string): Promise<DirectoryBrowserSnapshot> {
|
|
85
|
-
const currentPath = localPath ? resolveLocalPath(localPath) : getDefaultDirectoryRoot()
|
|
86
|
-
const info = await stat(currentPath).catch(() => null)
|
|
87
|
-
|
|
88
|
-
if (!info) {
|
|
89
|
-
throw new Error(`Folder not found: ${currentPath}`)
|
|
90
|
-
}
|
|
91
|
-
if (!info.isDirectory()) {
|
|
92
|
-
throw new Error("Project path must be a directory")
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const directoryEntries = await readdir(currentPath, { withFileTypes: true })
|
|
96
|
-
const entries = directoryEntries
|
|
97
|
-
.filter((entry) => entry.isDirectory())
|
|
98
|
-
.map((entry) => ({
|
|
99
|
-
name: entry.name,
|
|
100
|
-
localPath: path.join(currentPath, entry.name),
|
|
101
|
-
}))
|
|
102
|
-
.sort((a, b) => a.name.localeCompare(b.name))
|
|
103
|
-
|
|
104
|
-
const parentPath = path.dirname(currentPath) === currentPath ? null : path.dirname(currentPath)
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
currentPath,
|
|
108
|
-
parentPath,
|
|
109
|
-
roots: getDirectoryRoots(),
|
|
110
|
-
entries,
|
|
111
|
-
}
|
|
112
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from "node:child_process"
|
|
2
|
-
|
|
3
|
-
export function spawnDetached(command: string, args: string[]) {
|
|
4
|
-
spawn(command, args, { stdio: "ignore", detached: true }).unref()
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function hasCommand(command: string) {
|
|
8
|
-
const result = spawnSync("sh", ["-lc", `command -v ${command}`], { stdio: "ignore" })
|
|
9
|
-
return result.status === 0
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function resolveCommandPath(command: string) {
|
|
13
|
-
const result = spawnSync("sh", ["-lc", `command -v ${command}`], { stdio: ["ignore", "pipe", "ignore"] })
|
|
14
|
-
if (result.status !== 0) return null
|
|
15
|
-
const output = result.stdout?.toString().trim()
|
|
16
|
-
return output || null
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function canOpenMacApp(appName: string) {
|
|
20
|
-
const result = spawnSync("open", ["-Ra", appName], { stdio: "ignore" })
|
|
21
|
-
return result.status === 0
|
|
22
|
-
}
|