code-squad-cli 1.2.3 → 1.2.5
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/dist/adapters/GitAdapter.d.ts +13 -0
- package/dist/adapters/GitAdapter.js +124 -99
- package/dist/flip/routes/file.js +122 -34
- package/dist/flip/routes/files.js +29 -2
- package/dist/flip-ui/dist/assets/index-DTf1sRAX.css +1 -0
- package/dist/flip-ui/dist/assets/index-DmJdj9bX.js +154 -0
- package/dist/flip-ui/dist/index.html +2 -2
- package/dist/index.js +311 -152
- package/package.json +1 -1
- package/dist/flip-ui/dist/assets/index-BqoCRrTA.css +0 -1
- package/dist/flip-ui/dist/assets/index-CgNq-jV5.js +0 -69
|
@@ -9,5 +9,18 @@ export declare class GitAdapter implements PartialGitPort {
|
|
|
9
9
|
deleteBranch(branchName: string, workspaceRoot: string, force?: boolean): Promise<void>;
|
|
10
10
|
isValidWorktree(path: string, workspaceRoot: string): Promise<boolean>;
|
|
11
11
|
getWorktreeBranch(worktreePath: string): Promise<string>;
|
|
12
|
+
/**
|
|
13
|
+
* 현재 디렉토리가 워크트리인지 확인하고 컨텍스트 반환
|
|
14
|
+
*/
|
|
15
|
+
getWorktreeContext(cwd: string): Promise<{
|
|
16
|
+
isWorktree: boolean;
|
|
17
|
+
mainRoot: string | null;
|
|
18
|
+
currentPath: string;
|
|
19
|
+
branch: string | null;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* staged 또는 unstaged 변경사항이 있는지 확인 (untracked 제외)
|
|
23
|
+
*/
|
|
24
|
+
hasDirtyState(workspacePath: string): Promise<boolean>;
|
|
12
25
|
}
|
|
13
26
|
export {};
|
|
@@ -1,102 +1,84 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
1
|
+
import { exec as execCallback } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
2
3
|
import * as fs from 'fs';
|
|
4
|
+
const exec = promisify(execCallback);
|
|
5
|
+
const execOptions = { maxBuffer: 1024 * 1024 };
|
|
3
6
|
export class GitAdapter {
|
|
4
7
|
async isGitRepository(workspaceRoot) {
|
|
5
|
-
|
|
6
|
-
exec(`cd "${workspaceRoot}" && git rev-parse --git-dir`,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
try {
|
|
9
|
+
await exec(`cd "${workspaceRoot}" && git rev-parse --git-dir`, execOptions);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
10
15
|
}
|
|
11
16
|
async getCurrentBranch(workspaceRoot) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (error) {
|
|
15
|
-
reject(error);
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
resolve(stdout.trim());
|
|
19
|
-
});
|
|
20
|
-
});
|
|
17
|
+
const { stdout } = await exec(`cd "${workspaceRoot}" && git rev-parse --abbrev-ref HEAD`, execOptions);
|
|
18
|
+
return stdout.trim();
|
|
21
19
|
}
|
|
22
20
|
async listWorktrees(workspaceRoot) {
|
|
23
|
-
|
|
24
|
-
exec(`cd "${workspaceRoot}" && git worktree list --porcelain`,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
try {
|
|
22
|
+
const { stdout } = await exec(`cd "${workspaceRoot}" && git worktree list --porcelain`, execOptions);
|
|
23
|
+
const worktrees = [];
|
|
24
|
+
const lines = stdout.split('\n').filter((line) => line.trim());
|
|
25
|
+
// Parse porcelain format: groups of 3 lines
|
|
26
|
+
// worktree /path
|
|
27
|
+
// HEAD sha
|
|
28
|
+
// branch refs/heads/name
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < lines.length) {
|
|
31
|
+
const worktreeLine = lines[i];
|
|
32
|
+
const headLine = lines[i + 1];
|
|
33
|
+
const branchLine = lines[i + 2];
|
|
34
|
+
if (!worktreeLine || !headLine) {
|
|
35
|
+
i++;
|
|
36
|
+
continue;
|
|
28
37
|
}
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const branchLine = lines[i + 2];
|
|
40
|
-
if (!worktreeLine || !headLine) {
|
|
41
|
-
i++;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
const pathMatch = worktreeLine.match(/^worktree (.+)$/);
|
|
45
|
-
const headMatch = headLine.match(/^HEAD (.+)$/);
|
|
46
|
-
const branchMatch = branchLine?.match(/^branch refs\/heads\/(.+)$/);
|
|
47
|
-
if (pathMatch && headMatch) {
|
|
48
|
-
const path = pathMatch[1];
|
|
49
|
-
const head = headMatch[1];
|
|
50
|
-
const branch = branchMatch ? branchMatch[1] : 'HEAD';
|
|
51
|
-
// Skip main repository root (first entry)
|
|
52
|
-
if (path !== workspaceRoot) {
|
|
53
|
-
worktrees.push({ path, branch, head });
|
|
54
|
-
}
|
|
38
|
+
const pathMatch = worktreeLine.match(/^worktree (.+)$/);
|
|
39
|
+
const headMatch = headLine.match(/^HEAD (.+)$/);
|
|
40
|
+
const branchMatch = branchLine?.match(/^branch refs\/heads\/(.+)$/);
|
|
41
|
+
if (pathMatch && headMatch) {
|
|
42
|
+
const path = pathMatch[1];
|
|
43
|
+
const head = headMatch[1];
|
|
44
|
+
const branch = branchMatch ? branchMatch[1] : 'HEAD';
|
|
45
|
+
// Skip main repository root (first entry)
|
|
46
|
+
if (path !== workspaceRoot) {
|
|
47
|
+
worktrees.push({ path, branch, head });
|
|
55
48
|
}
|
|
56
|
-
// Move to next worktree entry
|
|
57
|
-
i += 3;
|
|
58
49
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
// Move to next worktree entry
|
|
51
|
+
i += 3;
|
|
52
|
+
}
|
|
53
|
+
return worktrees;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
62
58
|
}
|
|
63
59
|
async createWorktree(worktreePath, branch, workspaceRoot) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
exec(`cd "${workspaceRoot}" && ${mkdirCmd}git worktree add "${worktreePath}" -b "${branch}"`, { maxBuffer: 1024 * 1024 }, (error) => {
|
|
69
|
-
if (error) {
|
|
70
|
-
reject(error);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
resolve();
|
|
74
|
-
});
|
|
75
|
-
});
|
|
60
|
+
// Extract parent directory and create it if needed
|
|
61
|
+
const parentDir = worktreePath.substring(0, worktreePath.lastIndexOf('/'));
|
|
62
|
+
const mkdirCmd = parentDir ? `mkdir -p "${parentDir}" && ` : '';
|
|
63
|
+
await exec(`cd "${workspaceRoot}" && ${mkdirCmd}git worktree add "${worktreePath}" -b "${branch}"`, execOptions);
|
|
76
64
|
}
|
|
77
65
|
async removeWorktree(worktreePath, workspaceRoot, force = false) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
exec(`cd "${workspaceRoot}" && git worktree remove "${worktreePath}"${forceFlag}`,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
resolve();
|
|
86
|
-
});
|
|
87
|
-
});
|
|
66
|
+
const forceFlag = force ? ' --force' : '';
|
|
67
|
+
try {
|
|
68
|
+
await exec(`cd "${workspaceRoot}" && git worktree remove "${worktreePath}"${forceFlag}`, execOptions);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
throw new Error(`Failed to remove worktree: ${error.message}`);
|
|
72
|
+
}
|
|
88
73
|
}
|
|
89
74
|
async deleteBranch(branchName, workspaceRoot, force = false) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
exec(`cd "${workspaceRoot}" && git branch ${deleteFlag} "${branchName}"`,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
resolve();
|
|
98
|
-
});
|
|
99
|
-
});
|
|
75
|
+
const deleteFlag = force ? '-D' : '-d';
|
|
76
|
+
try {
|
|
77
|
+
await exec(`cd "${workspaceRoot}" && git branch ${deleteFlag} "${branchName}"`, execOptions);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
throw new Error(`Failed to delete branch: ${error.message}`);
|
|
81
|
+
}
|
|
100
82
|
}
|
|
101
83
|
async isValidWorktree(path, workspaceRoot) {
|
|
102
84
|
// Step 1: Check if path exists and is accessible
|
|
@@ -107,12 +89,10 @@ export class GitAdapter {
|
|
|
107
89
|
return false;
|
|
108
90
|
}
|
|
109
91
|
// Step 2: Check if path is a valid git repository
|
|
110
|
-
|
|
111
|
-
exec(`cd "${path}" && git rev-parse --git-dir`,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
});
|
|
115
|
-
if (!isGitRepo) {
|
|
92
|
+
try {
|
|
93
|
+
await exec(`cd "${path}" && git rev-parse --git-dir`, execOptions);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
116
96
|
return false;
|
|
117
97
|
}
|
|
118
98
|
// Step 3: Verify path is listed in main repo's worktree list
|
|
@@ -120,15 +100,60 @@ export class GitAdapter {
|
|
|
120
100
|
return worktrees.some((wt) => wt.path === path);
|
|
121
101
|
}
|
|
122
102
|
async getWorktreeBranch(worktreePath) {
|
|
123
|
-
|
|
124
|
-
exec(`cd "${worktreePath}" && git rev-parse --abbrev-ref HEAD`,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
103
|
+
try {
|
|
104
|
+
const { stdout } = await exec(`cd "${worktreePath}" && git rev-parse --abbrev-ref HEAD`, execOptions);
|
|
105
|
+
return stdout.trim();
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw new Error(`Failed to get branch name: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 현재 디렉토리가 워크트리인지 확인하고 컨텍스트 반환
|
|
113
|
+
*/
|
|
114
|
+
async getWorktreeContext(cwd) {
|
|
115
|
+
// git-common-dir로 워크트리 여부 확인
|
|
116
|
+
let commonDir = null;
|
|
117
|
+
try {
|
|
118
|
+
const { stdout } = await exec(`cd "${cwd}" && git rev-parse --git-common-dir`, execOptions);
|
|
119
|
+
commonDir = stdout.trim();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return { isWorktree: false, mainRoot: null, currentPath: cwd, branch: null };
|
|
123
|
+
}
|
|
124
|
+
// .git 이면 메인 레포, 아니면 워크트리
|
|
125
|
+
const isWorktree = commonDir !== '.git';
|
|
126
|
+
if (!isWorktree) {
|
|
127
|
+
return { isWorktree: false, mainRoot: cwd, currentPath: cwd, branch: null };
|
|
128
|
+
}
|
|
129
|
+
// 메인 레포 루트 찾기 (worktree list의 첫 번째)
|
|
130
|
+
let mainRoot = null;
|
|
131
|
+
try {
|
|
132
|
+
const { stdout } = await exec(`cd "${cwd}" && git worktree list --porcelain`, execOptions);
|
|
133
|
+
const match = stdout.match(/^worktree (.+)$/m);
|
|
134
|
+
mainRoot = match ? match[1] : null;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// ignore
|
|
138
|
+
}
|
|
139
|
+
// 현재 브랜치
|
|
140
|
+
const branch = await this.getWorktreeBranch(cwd).catch(() => null);
|
|
141
|
+
return { isWorktree, mainRoot, currentPath: cwd, branch };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* staged 또는 unstaged 변경사항이 있는지 확인 (untracked 제외)
|
|
145
|
+
*/
|
|
146
|
+
async hasDirtyState(workspacePath) {
|
|
147
|
+
try {
|
|
148
|
+
const { stdout } = await exec(`cd "${workspacePath}" && git status --porcelain`, execOptions);
|
|
149
|
+
// ?? 로 시작하는 줄(untracked)을 제외한 변경사항이 있는지
|
|
150
|
+
const lines = stdout.split('\n').filter(line => line.trim());
|
|
151
|
+
const dirtyLines = lines.filter(line => !line.startsWith('??'));
|
|
152
|
+
return dirtyLines.length > 0;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// 에러 시 안전하게 dirty로 처리 (데이터 손실 방지)
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
133
158
|
}
|
|
134
159
|
}
|
package/dist/flip/routes/file.js
CHANGED
|
@@ -2,42 +2,130 @@ import { Router } from 'express';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
const router = Router();
|
|
5
|
+
// Extension to language mapping
|
|
6
|
+
const extensionMap = {
|
|
7
|
+
// JavaScript/TypeScript
|
|
8
|
+
js: 'javascript',
|
|
9
|
+
mjs: 'javascript',
|
|
10
|
+
cjs: 'javascript',
|
|
11
|
+
ts: 'typescript',
|
|
12
|
+
mts: 'typescript',
|
|
13
|
+
cts: 'typescript',
|
|
14
|
+
tsx: 'tsx',
|
|
15
|
+
jsx: 'jsx',
|
|
16
|
+
// Systems languages
|
|
17
|
+
rs: 'rust',
|
|
18
|
+
go: 'go',
|
|
19
|
+
c: 'c',
|
|
20
|
+
cpp: 'cpp',
|
|
21
|
+
cc: 'cpp',
|
|
22
|
+
cxx: 'cpp',
|
|
23
|
+
h: 'cpp',
|
|
24
|
+
hpp: 'cpp',
|
|
25
|
+
hxx: 'cpp',
|
|
26
|
+
// JVM languages
|
|
27
|
+
java: 'java',
|
|
28
|
+
kt: 'kotlin',
|
|
29
|
+
kts: 'kotlin',
|
|
30
|
+
scala: 'scala',
|
|
31
|
+
groovy: 'groovy',
|
|
32
|
+
// Scripting languages
|
|
33
|
+
py: 'python',
|
|
34
|
+
rb: 'ruby',
|
|
35
|
+
php: 'php',
|
|
36
|
+
pl: 'perl',
|
|
37
|
+
lua: 'lua',
|
|
38
|
+
// Mobile
|
|
39
|
+
swift: 'swift',
|
|
40
|
+
m: 'objective-c',
|
|
41
|
+
mm: 'objective-cpp',
|
|
42
|
+
dart: 'dart',
|
|
43
|
+
// Web
|
|
44
|
+
html: 'html',
|
|
45
|
+
htm: 'html',
|
|
46
|
+
css: 'css',
|
|
47
|
+
scss: 'scss',
|
|
48
|
+
sass: 'sass',
|
|
49
|
+
less: 'less',
|
|
50
|
+
vue: 'vue',
|
|
51
|
+
svelte: 'svelte',
|
|
52
|
+
astro: 'astro',
|
|
53
|
+
// Data formats
|
|
54
|
+
json: 'json',
|
|
55
|
+
jsonc: 'jsonc',
|
|
56
|
+
yaml: 'yaml',
|
|
57
|
+
yml: 'yaml',
|
|
58
|
+
toml: 'toml',
|
|
59
|
+
xml: 'xml',
|
|
60
|
+
csv: 'csv',
|
|
61
|
+
// Documentation
|
|
62
|
+
md: 'markdown',
|
|
63
|
+
mdx: 'mdx',
|
|
64
|
+
rst: 'rst',
|
|
65
|
+
tex: 'latex',
|
|
66
|
+
// Shell
|
|
67
|
+
sh: 'bash',
|
|
68
|
+
bash: 'bash',
|
|
69
|
+
zsh: 'bash',
|
|
70
|
+
fish: 'fish',
|
|
71
|
+
ps1: 'powershell',
|
|
72
|
+
bat: 'batch',
|
|
73
|
+
cmd: 'batch',
|
|
74
|
+
// Database
|
|
75
|
+
sql: 'sql',
|
|
76
|
+
prisma: 'prisma',
|
|
77
|
+
graphql: 'graphql',
|
|
78
|
+
gql: 'graphql',
|
|
79
|
+
// Config
|
|
80
|
+
ini: 'ini',
|
|
81
|
+
conf: 'ini',
|
|
82
|
+
cfg: 'ini',
|
|
83
|
+
env: 'dotenv',
|
|
84
|
+
// Other
|
|
85
|
+
dockerfile: 'dockerfile',
|
|
86
|
+
makefile: 'makefile',
|
|
87
|
+
cmake: 'cmake',
|
|
88
|
+
diff: 'diff',
|
|
89
|
+
patch: 'diff',
|
|
90
|
+
};
|
|
91
|
+
// Filename to language mapping (for files without extensions)
|
|
92
|
+
const filenameMap = {
|
|
93
|
+
'Makefile': 'makefile',
|
|
94
|
+
'makefile': 'makefile',
|
|
95
|
+
'GNUmakefile': 'makefile',
|
|
96
|
+
'Dockerfile': 'dockerfile',
|
|
97
|
+
'dockerfile': 'dockerfile',
|
|
98
|
+
'Containerfile': 'dockerfile',
|
|
99
|
+
'Jenkinsfile': 'groovy',
|
|
100
|
+
'Vagrantfile': 'ruby',
|
|
101
|
+
'Gemfile': 'ruby',
|
|
102
|
+
'Rakefile': 'ruby',
|
|
103
|
+
'Brewfile': 'ruby',
|
|
104
|
+
'Podfile': 'ruby',
|
|
105
|
+
'Fastfile': 'ruby',
|
|
106
|
+
'Guardfile': 'ruby',
|
|
107
|
+
'.gitignore': 'gitignore',
|
|
108
|
+
'.gitattributes': 'gitattributes',
|
|
109
|
+
'.editorconfig': 'editorconfig',
|
|
110
|
+
'.bashrc': 'bash',
|
|
111
|
+
'.zshrc': 'bash',
|
|
112
|
+
'.bash_profile': 'bash',
|
|
113
|
+
'.profile': 'bash',
|
|
114
|
+
'CMakeLists.txt': 'cmake',
|
|
115
|
+
'CODEOWNERS': 'gitignore',
|
|
116
|
+
};
|
|
5
117
|
function detectLanguage(filePath) {
|
|
118
|
+
const basename = path.basename(filePath);
|
|
119
|
+
// Check filename first (for files like Makefile, Dockerfile)
|
|
120
|
+
if (filenameMap[basename]) {
|
|
121
|
+
return filenameMap[basename];
|
|
122
|
+
}
|
|
123
|
+
// Then check extension
|
|
6
124
|
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
tsx: 'tsx',
|
|
12
|
-
jsx: 'jsx',
|
|
13
|
-
py: 'python',
|
|
14
|
-
go: 'go',
|
|
15
|
-
java: 'java',
|
|
16
|
-
c: 'c',
|
|
17
|
-
cpp: 'cpp',
|
|
18
|
-
cc: 'cpp',
|
|
19
|
-
cxx: 'cpp',
|
|
20
|
-
h: 'cpp',
|
|
21
|
-
hpp: 'cpp',
|
|
22
|
-
md: 'markdown',
|
|
23
|
-
json: 'json',
|
|
24
|
-
yaml: 'yaml',
|
|
25
|
-
yml: 'yaml',
|
|
26
|
-
toml: 'toml',
|
|
27
|
-
html: 'html',
|
|
28
|
-
css: 'css',
|
|
29
|
-
scss: 'scss',
|
|
30
|
-
sh: 'bash',
|
|
31
|
-
bash: 'bash',
|
|
32
|
-
sql: 'sql',
|
|
33
|
-
rb: 'ruby',
|
|
34
|
-
swift: 'swift',
|
|
35
|
-
kt: 'kotlin',
|
|
36
|
-
kts: 'kotlin',
|
|
37
|
-
xml: 'xml',
|
|
38
|
-
vue: 'vue',
|
|
39
|
-
};
|
|
40
|
-
return languageMap[ext] || 'plaintext';
|
|
125
|
+
if (extensionMap[ext]) {
|
|
126
|
+
return extensionMap[ext];
|
|
127
|
+
}
|
|
128
|
+
return 'text';
|
|
41
129
|
}
|
|
42
130
|
// GET /api/file?path=<path>
|
|
43
131
|
router.get('/', (req, res) => {
|
|
@@ -24,9 +24,36 @@ const DEFAULT_IGNORES = [
|
|
|
24
24
|
'yarn.lock',
|
|
25
25
|
'.DS_Store',
|
|
26
26
|
];
|
|
27
|
+
// Dotfiles that should be visible in the file tree
|
|
28
|
+
const VISIBLE_DOTFILES = new Set([
|
|
29
|
+
'.gitignore',
|
|
30
|
+
'.gitattributes',
|
|
31
|
+
'.env.example',
|
|
32
|
+
'.env.local.example',
|
|
33
|
+
'.eslintrc',
|
|
34
|
+
'.eslintrc.js',
|
|
35
|
+
'.eslintrc.cjs',
|
|
36
|
+
'.eslintrc.json',
|
|
37
|
+
'.eslintrc.yml',
|
|
38
|
+
'.prettierrc',
|
|
39
|
+
'.prettierrc.js',
|
|
40
|
+
'.prettierrc.cjs',
|
|
41
|
+
'.prettierrc.json',
|
|
42
|
+
'.prettierrc.yml',
|
|
43
|
+
'.editorconfig',
|
|
44
|
+
'.npmrc',
|
|
45
|
+
'.nvmrc',
|
|
46
|
+
'.node-version',
|
|
47
|
+
'.dockerignore',
|
|
48
|
+
'.browserslistrc',
|
|
49
|
+
'.babelrc',
|
|
50
|
+
'.babelrc.js',
|
|
51
|
+
'.babelrc.json',
|
|
52
|
+
]);
|
|
27
53
|
function shouldIgnore(name) {
|
|
28
|
-
if (name.startsWith('.')
|
|
29
|
-
|
|
54
|
+
if (name.startsWith('.')) {
|
|
55
|
+
// Allow specific dotfiles
|
|
56
|
+
return !VISIBLE_DOTFILES.has(name);
|
|
30
57
|
}
|
|
31
58
|
return DEFAULT_IGNORES.includes(name);
|
|
32
59
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*{box-sizing:border-box;margin:0;padding:0}:root{--bg-primary: #0d1117;--bg-secondary: #161b22;--bg-tertiary: #21262d;--bg-hover: #30363d;--bg-selected: #1f6feb26;--text-primary: #e6edf3;--text-secondary: #8b949e;--text-muted: #6e7681;--border-color: #30363d;--border-subtle: #21262d;--accent-primary: #238636;--accent-primary-hover: #2ea043;--accent-secondary: #1f6feb;--git-modified: #d29922;--git-untracked: #3fb950;--git-deleted: #f85149;--git-added: #3fb950;--line-number-color: #6e7681;--selection-bg: #264f78;--diff-add-bg: rgba(46, 160, 67, .15);--diff-add-text: #7ee787;--diff-add-border: rgba(46, 160, 67, .4);--diff-delete-bg: rgba(248, 81, 73, .15);--diff-delete-text: #ffa198;--diff-delete-border: rgba(248, 81, 73, .4);--diff-hunk-bg: rgba(56, 139, 253, .1);--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;--font-mono: "SF Mono", "Menlo", "Monaco", "Consolas", monospace;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 24px;--transition-fast: .15s ease;--transition-normal: .2s ease}body{font-family:var(--font-sans);font-size:14px;background-color:var(--bg-primary);color:var(--text-primary);height:100vh;overflow:hidden}#root{height:100%}.app{display:flex;flex-direction:column;height:100%}.main-content{display:flex;flex:1;overflow:hidden}.toolbar{display:flex;align-items:center;justify-content:space-between;height:40px;padding:0 var(--space-3);background-color:var(--bg-secondary);border-bottom:1px solid var(--border-subtle)}.toolbar-left,.toolbar-center,.toolbar-right{display:flex;align-items:center;gap:var(--space-2)}.toolbar-btn{display:flex;align-items:center;justify-content:center;gap:var(--space-1);height:28px;padding:0 var(--space-2);border:none;background:transparent;border-radius:6px;color:var(--text-secondary);cursor:pointer;transition:all var(--transition-fast)}.toolbar-btn:hover{background-color:var(--bg-hover);color:var(--text-primary)}.toolbar-btn.active{background-color:var(--bg-tertiary);color:var(--text-primary)}.toolbar-btn svg{flex-shrink:0}.diff-toggle{min-width:100px}.diff-mode-label{font-size:12px}.sidebar{width:280px;background-color:var(--bg-secondary);border-right:1px solid var(--border-subtle);overflow:hidden;display:flex;flex-direction:column;transition:width var(--transition-normal),opacity var(--transition-fast)}.sidebar.collapsed{width:0;opacity:0;border-right:none}.content{flex:1;overflow:hidden;display:flex;flex-direction:column;min-width:0}.staging-panel{width:320px;background-color:var(--bg-secondary);border-left:1px solid var(--border-subtle);overflow:hidden;display:flex;flex-direction:column;transition:width var(--transition-normal),opacity var(--transition-fast)}.staging-panel.collapsed{width:0;opacity:0;border-left:none}.footer{display:flex;align-items:center;justify-content:space-between;padding:var(--space-2) var(--space-4);background-color:var(--bg-secondary);border-top:1px solid var(--border-subtle);gap:var(--space-4)}.footer-actions{display:flex;gap:var(--space-2)}.file-tree{display:flex;flex-direction:column;height:100%}.file-tree-header{display:flex;align-items:center;justify-content:space-between;padding:var(--space-3) var(--space-4);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-secondary);border-bottom:1px solid var(--border-subtle)}.file-tree-header-actions{display:flex;gap:var(--space-1)}.filter-btn{display:flex;align-items:center;gap:var(--space-1);padding:2px 8px;border:none;background:transparent;border-radius:4px;color:var(--text-muted);font-size:10px;font-weight:600;cursor:pointer;transition:all var(--transition-fast)}.filter-btn:hover{background-color:var(--bg-hover);color:var(--text-secondary)}.filter-btn.active{background-color:var(--accent-secondary);color:#fff}.filter-btn .count{background-color:var(--bg-hover);padding:0 4px;border-radius:3px;font-size:9px}.filter-btn.active .count{background-color:#fff3}.file-tree-content{flex:1;overflow-y:auto;padding:var(--space-1) 0}.tree-item{display:flex;align-items:center;padding:var(--space-1) var(--space-2);cursor:pointer;-webkit-user-select:none;user-select:none;gap:6px}.tree-item:hover{background-color:var(--bg-hover)}.tree-icon{display:flex;align-items:center;justify-content:center;width:14px;color:var(--text-secondary);flex-shrink:0}.tree-folder-icon{display:flex;align-items:center;justify-content:center;width:14px;color:var(--git-modified);flex-shrink:0}.tree-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.git-badge{font-size:10px;font-weight:600;padding:1px 4px;border-radius:3px}.git-badge-modified{color:var(--git-modified);background-color:#d2992226}.git-badge-untracked{color:var(--git-untracked);background-color:#3fb95026}.git-badge-deleted{color:var(--git-deleted);background-color:#f8514926}.git-badge-added{color:var(--git-added);background-color:#3fb95026}.code-viewer{display:flex;flex-direction:column;height:100%;overflow:hidden}.code-viewer-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text-secondary);gap:var(--space-3)}.code-viewer-empty .hint{display:flex;align-items:center;gap:var(--space-1);font-size:12px;color:var(--text-muted)}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:var(--space-5);gap:var(--space-3)}.empty-state-icon{color:var(--text-muted);opacity:.5}.code-viewer-header{display:flex;align-items:center;justify-content:space-between;padding:var(--space-2) var(--space-4);background-color:var(--bg-secondary);border-bottom:1px solid var(--border-subtle)}.file-path{font-size:13px;font-family:var(--font-mono)}.file-language{font-size:11px;color:var(--text-secondary);text-transform:uppercase}.code-viewer-content{flex:1;overflow:auto;font-family:var(--font-mono);font-size:13px;line-height:1.5}.code-line{display:flex;min-height:20px;cursor:pointer}.code-line:hover{background-color:var(--bg-hover)}.code-line-selected{background-color:var(--selection-bg)!important}.code-line-added{background-color:var(--diff-add-bg)!important}.code-line-added:hover{background-color:#2ea04340!important}.code-line-deleted{background-color:var(--diff-delete-bg)!important}.code-line-deleted:hover{background-color:#f8514940!important}.line-number{display:inline-block;min-width:50px;padding:0 var(--space-3);text-align:right;color:var(--line-number-color);-webkit-user-select:none;user-select:none;background-color:var(--bg-tertiary);border-right:1px solid var(--border-subtle)}.code-line code{flex:1;padding:0 var(--space-3);white-space:pre;background:transparent}.code-line code.hljs{background:transparent;padding:0 var(--space-3)}.code-content{flex:1;padding:0 var(--space-3);white-space:pre}.staging-list{display:flex;flex-direction:column;height:100%}.staging-list-empty{padding:var(--space-4)}.staging-header{display:flex;align-items:center;justify-content:space-between;padding:var(--space-3) var(--space-4);font-weight:600;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-secondary);border-bottom:1px solid var(--border-subtle)}.staging-hint{font-size:12px;color:var(--text-muted);line-height:1.5}.staging-items{flex:1;overflow-y:auto;padding:var(--space-2)}.staging-item{background-color:var(--bg-tertiary);border-radius:6px;padding:var(--space-2) var(--space-3);margin-bottom:var(--space-2)}.staging-item-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-1)}.staging-location{font-family:var(--font-mono);font-size:12px;color:var(--text-secondary)}.staging-comment{font-size:13px;line-height:1.4}.comment-input{display:flex;align-items:center;gap:var(--space-3);flex:1}.comment-selection{font-family:var(--font-mono);font-size:12px;color:var(--accent-secondary);white-space:nowrap}.comment-field{flex:1;padding:var(--space-2) var(--space-3);background-color:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:6px;color:var(--text-primary);font-size:14px;transition:border-color var(--transition-fast)}.comment-field:focus{outline:none;border-color:var(--accent-secondary)}.comment-hint{color:var(--text-muted);font-size:13px}.fuzzy-finder-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#0009;display:flex;justify-content:center;padding-top:80px;z-index:100}.fuzzy-finder{background-color:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;box-shadow:0 16px 48px #00000080;width:600px;max-height:400px;display:flex;flex-direction:column;overflow:hidden}.fuzzy-finder-input{padding:var(--space-4);background-color:var(--bg-tertiary);border:none;border-bottom:1px solid var(--border-subtle);color:var(--text-primary);font-size:16px}.fuzzy-finder-input:focus{outline:none}.fuzzy-finder-results{flex:1;overflow-y:auto}.fuzzy-finder-item{padding:var(--space-3) var(--space-4);cursor:pointer;font-family:var(--font-mono);font-size:13px}.fuzzy-finder-item:hover,.fuzzy-finder-item-selected{background-color:var(--bg-selected)}.fuzzy-finder-empty{padding:var(--space-4);color:var(--text-muted);text-align:center}.btn{padding:var(--space-2) var(--space-4);border:none;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;transition:background-color var(--transition-fast)}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-primary{background-color:var(--accent-primary);color:#fff}.btn-primary:hover:not(:disabled){background-color:var(--accent-primary-hover)}.btn-secondary{background-color:var(--bg-tertiary);color:var(--text-primary)}.btn-secondary:hover:not(:disabled){background-color:var(--bg-hover)}.btn-small{padding:var(--space-1) var(--space-3)}.btn-link{background:none;border:none;color:var(--accent-secondary);cursor:pointer;font-size:11px}.btn-link:hover{text-decoration:underline}.btn-icon{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:14px;padding:2px 6px}.btn-icon:hover{color:var(--text-primary)}.diff-side-by-side{display:flex;height:100%;overflow:hidden}.diff-pane{flex:1;overflow:auto;font-family:var(--font-mono);font-size:13px;line-height:1.5}.diff-pane-left{border-right:1px solid var(--border-color)}.diff-pane-header{padding:var(--space-2) var(--space-3);background-color:var(--bg-tertiary);border-bottom:1px solid var(--border-subtle);font-size:11px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px}.code-viewer-loading{display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--bg-hover);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#484f58}::-webkit-scrollbar-corner{background:transparent}
|