@c4ccz/zero 1.0.0 → 1.0.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/package.json +10 -7
- package/packages/core/src/agent/config-loader.ts +174 -0
- package/packages/core/src/agent/engine.ts +266 -0
- package/packages/core/src/agent/registry-tools.ts +58 -0
- package/packages/core/src/agent/registry.ts +213 -0
- package/packages/core/src/commands/index.ts +116 -0
- package/packages/core/src/config/index.ts +139 -0
- package/packages/core/src/confirmation/message-bus.ts +206 -0
- package/packages/core/src/diff/index.ts +232 -0
- package/packages/core/src/diff-detect/index.ts +207 -0
- package/packages/core/src/edit-coder/index.ts +194 -0
- package/packages/core/src/events/index.ts +151 -0
- package/packages/core/src/file-tracker/index.ts +183 -0
- package/packages/core/src/git/index.ts +305 -0
- package/packages/core/src/history/index.ts +236 -0
- package/packages/core/src/image/index.ts +131 -0
- package/packages/core/src/index.ts +131 -0
- package/packages/core/src/lsp/index.ts +251 -0
- package/packages/core/src/mcp/index.ts +186 -0
- package/packages/core/src/memory/index.ts +141 -0
- package/packages/core/src/memory/prompts.ts +52 -0
- package/packages/core/src/orchestrator/protocol.ts +140 -0
- package/packages/core/src/orchestrator/server.ts +319 -0
- package/packages/core/src/output/index.ts +164 -0
- package/packages/core/src/permissions/index.ts +126 -0
- package/packages/core/src/prompts/engine.ts +188 -0
- package/packages/core/src/providers/extended.ts +8 -0
- package/packages/core/src/providers/registry.ts +687 -0
- package/packages/core/src/routing/model-router.ts +160 -0
- package/packages/core/src/server/index.ts +232 -0
- package/packages/core/src/session/index.ts +174 -0
- package/packages/core/src/session/service.ts +322 -0
- package/packages/core/src/skills/index.ts +205 -0
- package/packages/core/src/streaming/index.ts +166 -0
- package/packages/core/src/sub-agent/index.ts +179 -0
- package/packages/core/src/tools/builtin.ts +568 -0
- package/packages/core/src/tools/linter.ts +26 -0
- package/packages/core/src/tools/repo-map.ts +11 -0
- package/packages/core/src/types.ts +356 -0
- package/packages/sdk/src/index.ts +247 -0
- package/packages/tui/src/index.ts +141 -0
- package/bun.lock +0 -35
- package/bunfig.toml +0 -6
- package/packages/cli/package.json +0 -23
- package/packages/core/package.json +0 -41
- package/packages/sdk/package.json +0 -16
- package/packages/tui/package.json +0 -19
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO PubSub Events System
|
|
3
|
+
* Adapted from: crush (Charm) - MIT
|
|
4
|
+
*
|
|
5
|
+
* Event-driven pub/sub system for inter-component communication.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type EventHandler<T = unknown> = (data: T) => void | Promise<void>;
|
|
9
|
+
|
|
10
|
+
export interface EventDefinition<T = unknown> {
|
|
11
|
+
type: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class EventBus {
|
|
16
|
+
private handlers = new Map<string, Set<EventHandler>>();
|
|
17
|
+
private history: Array<{ type: string; data: unknown; timestamp: number }> = [];
|
|
18
|
+
private maxHistory = 100;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Subscribe to an event
|
|
22
|
+
*/
|
|
23
|
+
on<T = unknown>(eventType: string, handler: EventHandler<T>): () => void {
|
|
24
|
+
if (!this.handlers.has(eventType)) {
|
|
25
|
+
this.handlers.set(eventType, new Set());
|
|
26
|
+
}
|
|
27
|
+
this.handlers.get(eventType)!.add(handler as EventHandler);
|
|
28
|
+
|
|
29
|
+
// Return unsubscribe function
|
|
30
|
+
return () => {
|
|
31
|
+
this.handlers.get(eventType)?.delete(handler as EventHandler);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Subscribe to an event (one-time)
|
|
37
|
+
*/
|
|
38
|
+
once<T = unknown>(eventType: string, handler: EventHandler<T>): () => void {
|
|
39
|
+
const wrappedHandler: EventHandler<T> = async (data) => {
|
|
40
|
+
unsubscribe();
|
|
41
|
+
await handler(data);
|
|
42
|
+
};
|
|
43
|
+
const unsubscribe = this.on(eventType, wrappedHandler);
|
|
44
|
+
return unsubscribe;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Emit an event
|
|
49
|
+
*/
|
|
50
|
+
async emit<T = unknown>(eventType: string, data: T): Promise<void> {
|
|
51
|
+
// Add to history
|
|
52
|
+
this.history.push({ type: eventType, data, timestamp: Date.now() });
|
|
53
|
+
if (this.history.length > this.maxHistory) {
|
|
54
|
+
this.history.shift();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Call handlers
|
|
58
|
+
const handlers = this.handlers.get(eventType);
|
|
59
|
+
if (handlers) {
|
|
60
|
+
for (const handler of handlers) {
|
|
61
|
+
try {
|
|
62
|
+
await handler(data);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(`Error in event handler for ${eventType}:`, error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get event history
|
|
72
|
+
*/
|
|
73
|
+
getHistory(eventType?: string): Array<{ type: string; data: unknown; timestamp: number }> {
|
|
74
|
+
if (eventType) {
|
|
75
|
+
return this.history.filter((e) => e.type === eventType);
|
|
76
|
+
}
|
|
77
|
+
return [...this.history];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clear event history
|
|
82
|
+
*/
|
|
83
|
+
clearHistory(): void {
|
|
84
|
+
this.history = [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Remove all handlers for an event type
|
|
89
|
+
*/
|
|
90
|
+
off(eventType: string): void {
|
|
91
|
+
this.handlers.delete(eventType);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Remove all handlers
|
|
96
|
+
*/
|
|
97
|
+
removeAll(): void {
|
|
98
|
+
this.handlers.clear();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Pre-defined Event Types
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
export const ZERO_EVENTS = {
|
|
107
|
+
// Session events
|
|
108
|
+
SESSION_CREATED: "session:created",
|
|
109
|
+
SESSION_UPDATED: "session:updated",
|
|
110
|
+
SESSION_DELETED: "session:deleted",
|
|
111
|
+
|
|
112
|
+
// Agent events
|
|
113
|
+
AGENT_STARTED: "agent:started",
|
|
114
|
+
AGENT_COMPLETED: "agent:completed",
|
|
115
|
+
AGENT_ERROR: "agent:error",
|
|
116
|
+
|
|
117
|
+
// Tool events
|
|
118
|
+
TOOL_CALL_START: "tool:call:start",
|
|
119
|
+
TOOL_CALL_COMPLETE: "tool:call:complete",
|
|
120
|
+
TOOL_CALL_ERROR: "tool:call:error",
|
|
121
|
+
|
|
122
|
+
// Message events
|
|
123
|
+
MESSAGE_USER: "message:user",
|
|
124
|
+
MESSAGE_ASSISTANT: "message:assistant",
|
|
125
|
+
MESSAGE_SYSTEM: "message:system",
|
|
126
|
+
|
|
127
|
+
// Permission events
|
|
128
|
+
PERMISSION_REQUESTED: "permission:requested",
|
|
129
|
+
PERMISSION_GRANTED: "permission:granted",
|
|
130
|
+
PERMISSION_DENIED: "permission:denied",
|
|
131
|
+
|
|
132
|
+
// MCP events
|
|
133
|
+
MCP_CONNECTED: "mcp:connected",
|
|
134
|
+
MCP_DISCONNECTED: "mcp:disconnected",
|
|
135
|
+
MCP_ERROR: "mcp:error",
|
|
136
|
+
|
|
137
|
+
// Git events
|
|
138
|
+
GIT_COMMIT: "git:commit",
|
|
139
|
+
GIT_BRANCH: "git:branch",
|
|
140
|
+
GIT_MERGE: "git:merge",
|
|
141
|
+
|
|
142
|
+
// File events
|
|
143
|
+
FILE_CHANGED: "file:changed",
|
|
144
|
+
FILE_CREATED: "file:created",
|
|
145
|
+
FILE_DELETED: "file:deleted",
|
|
146
|
+
} as const;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Global event bus instance
|
|
150
|
+
*/
|
|
151
|
+
export const globalEventBus = new EventBus();
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO File Tracker Service
|
|
3
|
+
* Adapted from: crush (Charm) - MIT
|
|
4
|
+
*
|
|
5
|
+
* Tracks file changes and provides file system monitoring.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs/promises";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { watch, type FSWatcher } from "node:fs";
|
|
11
|
+
import { globalEventBus, ZERO_EVENTS } from "../events/index.js";
|
|
12
|
+
|
|
13
|
+
export interface FileChange {
|
|
14
|
+
path: string;
|
|
15
|
+
type: "created" | "modified" | "deleted";
|
|
16
|
+
timestamp: number;
|
|
17
|
+
size?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FileTrackerOptions {
|
|
21
|
+
workingDirectory: string;
|
|
22
|
+
ignorePatterns?: string[];
|
|
23
|
+
debounceMs?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class FileTracker {
|
|
27
|
+
private workingDirectory: string;
|
|
28
|
+
private ignorePatterns: RegExp[];
|
|
29
|
+
private debounceMs: number;
|
|
30
|
+
private watcher?: FSWatcher;
|
|
31
|
+
private changeHistory: FileChange[] = [];
|
|
32
|
+
private pendingChanges = new Map<string, NodeJS.Timeout>();
|
|
33
|
+
private fileHashes = new Map<string, string>();
|
|
34
|
+
|
|
35
|
+
constructor(options: FileTrackerOptions) {
|
|
36
|
+
this.workingDirectory = options.workingDirectory;
|
|
37
|
+
this.ignorePatterns = (options.ignorePatterns || [
|
|
38
|
+
"node_modules",
|
|
39
|
+
".git",
|
|
40
|
+
"__pycache__",
|
|
41
|
+
"dist",
|
|
42
|
+
"build",
|
|
43
|
+
"coverage",
|
|
44
|
+
".next",
|
|
45
|
+
".nuxt",
|
|
46
|
+
"target",
|
|
47
|
+
"vendor",
|
|
48
|
+
".venv",
|
|
49
|
+
"venv",
|
|
50
|
+
".cache",
|
|
51
|
+
".turbo",
|
|
52
|
+
"*.log",
|
|
53
|
+
]).map((p) => new RegExp(p.replace(/\*/g, ".*")));
|
|
54
|
+
this.debounceMs = options.debounceMs || 100;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start watching for file changes
|
|
59
|
+
*/
|
|
60
|
+
async start(): Promise<void> {
|
|
61
|
+
// Initial scan
|
|
62
|
+
await this.scanDirectory(this.workingDirectory);
|
|
63
|
+
|
|
64
|
+
// Start watcher
|
|
65
|
+
this.watcher = watch(this.workingDirectory, { recursive: true }, (eventType, filename) => {
|
|
66
|
+
if (!filename) return;
|
|
67
|
+
const fullPath = path.join(this.workingDirectory, filename);
|
|
68
|
+
|
|
69
|
+
// Check ignore patterns
|
|
70
|
+
if (this.shouldIgnore(fullPath)) return;
|
|
71
|
+
|
|
72
|
+
// Debounce
|
|
73
|
+
const existing = this.pendingChanges.get(fullPath);
|
|
74
|
+
if (existing) clearTimeout(existing);
|
|
75
|
+
|
|
76
|
+
this.pendingChanges.set(
|
|
77
|
+
fullPath,
|
|
78
|
+
setTimeout(() => this.handleFileChange(fullPath), this.debounceMs)
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Stop watching
|
|
85
|
+
*/
|
|
86
|
+
stop(): void {
|
|
87
|
+
if (this.watcher) {
|
|
88
|
+
this.watcher.close();
|
|
89
|
+
this.watcher = undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Clear pending changes
|
|
93
|
+
for (const timeout of this.pendingChanges.values()) {
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
}
|
|
96
|
+
this.pendingChanges.clear();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get change history
|
|
101
|
+
*/
|
|
102
|
+
getHistory(limit?: number): FileChange[] {
|
|
103
|
+
const history = [...this.changeHistory].sort((a, b) => b.timestamp - a.timestamp);
|
|
104
|
+
return limit ? history.slice(0, limit) : history;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clear history
|
|
109
|
+
*/
|
|
110
|
+
clearHistory(): void {
|
|
111
|
+
this.changeHistory = [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get recently changed files
|
|
116
|
+
*/
|
|
117
|
+
getRecentChanges(sinceMs: number): FileChange[] {
|
|
118
|
+
const cutoff = Date.now() - sinceMs;
|
|
119
|
+
return this.changeHistory.filter((c) => c.timestamp > cutoff);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private shouldIgnore(filePath: string): boolean {
|
|
123
|
+
const relativePath = path.relative(this.workingDirectory, filePath);
|
|
124
|
+
return this.ignorePatterns.some((pattern) => pattern.test(relativePath));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async handleFileChange(filePath: string): Promise<void> {
|
|
128
|
+
this.pendingChanges.delete(filePath);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const stats = await fs.stat(filePath);
|
|
132
|
+
const change: FileChange = {
|
|
133
|
+
path: path.relative(this.workingDirectory, filePath),
|
|
134
|
+
type: "modified",
|
|
135
|
+
timestamp: Date.now(),
|
|
136
|
+
size: stats.size,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (!this.fileHashes.has(filePath)) {
|
|
140
|
+
change.type = "created";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.fileHashes.set(filePath, `${stats.size}-${stats.mtimeMs}`);
|
|
144
|
+
this.changeHistory.push(change);
|
|
145
|
+
|
|
146
|
+
// Emit event
|
|
147
|
+
await globalEventBus.emit(ZERO_EVENTS.FILE_CHANGED, change);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// File was deleted
|
|
150
|
+
if (this.fileHashes.has(filePath)) {
|
|
151
|
+
const change: FileChange = {
|
|
152
|
+
path: path.relative(this.workingDirectory, filePath),
|
|
153
|
+
type: "deleted",
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
};
|
|
156
|
+
this.fileHashes.delete(filePath);
|
|
157
|
+
this.changeHistory.push(change);
|
|
158
|
+
await globalEventBus.emit(ZERO_EVENTS.FILE_DELETED, change);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async scanDirectory(dir: string): Promise<void> {
|
|
164
|
+
try {
|
|
165
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
166
|
+
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
const fullPath = path.join(dir, entry.name);
|
|
169
|
+
|
|
170
|
+
if (this.shouldIgnore(fullPath)) continue;
|
|
171
|
+
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
await this.scanDirectory(fullPath);
|
|
174
|
+
} else if (entry.isFile()) {
|
|
175
|
+
try {
|
|
176
|
+
const stats = await fs.stat(fullPath);
|
|
177
|
+
this.fileHashes.set(fullPath, `${stats.size}-${stats.mtimeMs}`);
|
|
178
|
+
} catch {}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO Git Service
|
|
3
|
+
* Adapted from: opencode (SST) - MIT
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive git integration with support for status, diff,
|
|
6
|
+
* patches, branching, and merge operations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
const GIT_CONFIG_ARGS = [
|
|
12
|
+
"--no-optional-locks",
|
|
13
|
+
"-c", "core.autocrlf=false",
|
|
14
|
+
"-c", "core.fsmonitor=false",
|
|
15
|
+
"-c", "core.longpaths=true",
|
|
16
|
+
"-c", "core.symlinks=true",
|
|
17
|
+
"-c", "core.quotepath=false",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export type ChangeKind = "added" | "deleted" | "modified";
|
|
21
|
+
|
|
22
|
+
export interface GitItem {
|
|
23
|
+
readonly file: string;
|
|
24
|
+
readonly code: string;
|
|
25
|
+
readonly status: ChangeKind;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface GitStat {
|
|
29
|
+
readonly file: string;
|
|
30
|
+
readonly additions: number;
|
|
31
|
+
readonly deletions: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GitPatch {
|
|
35
|
+
readonly text: string;
|
|
36
|
+
readonly truncated: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface GitBase {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly ref: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PatchOptions {
|
|
45
|
+
readonly context?: number;
|
|
46
|
+
readonly maxOutputBytes?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface GitResult {
|
|
50
|
+
readonly exitCode: number;
|
|
51
|
+
readonly stdout: string;
|
|
52
|
+
readonly stderr: string;
|
|
53
|
+
readonly truncated: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class GitService {
|
|
57
|
+
constructor(private cwd: string) {}
|
|
58
|
+
|
|
59
|
+
// ========================================================================
|
|
60
|
+
// Core Operations
|
|
61
|
+
// ========================================================================
|
|
62
|
+
|
|
63
|
+
private run(args: string[], options?: { stdin?: string; maxOutputBytes?: number }): GitResult {
|
|
64
|
+
try {
|
|
65
|
+
const fullArgs = [...GIT_CONFIG_ARGS, ...args];
|
|
66
|
+
const stdout = execSync(`git ${fullArgs.join(" ")}`, {
|
|
67
|
+
cwd: this.cwd,
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
maxBuffer: options?.maxOutputBytes || 10 * 1024 * 1024,
|
|
70
|
+
input: options?.stdin,
|
|
71
|
+
timeout: 30000,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
exitCode: 0,
|
|
76
|
+
stdout: stdout.trim(),
|
|
77
|
+
stderr: "",
|
|
78
|
+
truncated: false,
|
|
79
|
+
};
|
|
80
|
+
} catch (error: any) {
|
|
81
|
+
return {
|
|
82
|
+
exitCode: error.status || 1,
|
|
83
|
+
stdout: (error.stdout || "").trim(),
|
|
84
|
+
stderr: (error.stderr || error.message || "").trim(),
|
|
85
|
+
truncated: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ========================================================================
|
|
91
|
+
// Branch Operations
|
|
92
|
+
// ========================================================================
|
|
93
|
+
|
|
94
|
+
branch(): string | undefined {
|
|
95
|
+
const result = this.run(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
96
|
+
return result.exitCode === 0 ? result.stdout : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
defaultBranch(): GitBase | undefined {
|
|
100
|
+
// Try common default branch names
|
|
101
|
+
for (const name of ["main", "master", "develop"]) {
|
|
102
|
+
const result = this.run(["rev-parse", "--verify", `refs/heads/${name}`]);
|
|
103
|
+
if (result.exitCode === 0) {
|
|
104
|
+
return { name, ref: result.stdout };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fall back to git config
|
|
109
|
+
const result = this.run(["config", "init.defaultBranch"]);
|
|
110
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
111
|
+
const refResult = this.run(["rev-parse", "--verify", `refs/heads/${result.stdout}`]);
|
|
112
|
+
if (refResult.exitCode === 0) {
|
|
113
|
+
return { name: result.stdout, ref: refResult.stdout };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
hasHead(): boolean {
|
|
121
|
+
return this.run(["rev-parse", "HEAD"]).exitCode === 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
mergeBase(base: string, head?: string): string | undefined {
|
|
125
|
+
const args = ["merge-base", base];
|
|
126
|
+
if (head) args.push(head);
|
|
127
|
+
const result = this.run(args);
|
|
128
|
+
return result.exitCode === 0 ? result.stdout : undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ========================================================================
|
|
132
|
+
// Status & Diff
|
|
133
|
+
// ========================================================================
|
|
134
|
+
|
|
135
|
+
status(): GitItem[] {
|
|
136
|
+
const result = this.run(["status", "--porcelain=v1", "-z"]);
|
|
137
|
+
if (result.exitCode !== 0) return [];
|
|
138
|
+
|
|
139
|
+
const kind = (code: string): ChangeKind => {
|
|
140
|
+
if (code === "??") return "added";
|
|
141
|
+
if (code.includes("U")) return "modified";
|
|
142
|
+
if (code.includes("A") && !code.includes("D")) return "added";
|
|
143
|
+
if (code.includes("D") && !code.includes("A")) return "deleted";
|
|
144
|
+
return "modified";
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const entries = result.stdout.split("\0").filter(Boolean);
|
|
148
|
+
const items: GitItem[] = [];
|
|
149
|
+
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
if (entry.length < 4) continue;
|
|
152
|
+
const code = entry.slice(0, 2).trim();
|
|
153
|
+
const file = entry.slice(3);
|
|
154
|
+
items.push({ file, code, status: kind(code) });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return items;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
diff(ref: string): GitItem[] {
|
|
161
|
+
const result = this.run(["diff", "--name-status", "-z", ref]);
|
|
162
|
+
if (result.exitCode !== 0) return [];
|
|
163
|
+
|
|
164
|
+
const parts = result.stdout.split("\0").filter(Boolean);
|
|
165
|
+
const items: GitItem[] = [];
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
168
|
+
const code = parts[i]?.trim();
|
|
169
|
+
const file = parts[i + 1];
|
|
170
|
+
if (!code || !file) continue;
|
|
171
|
+
|
|
172
|
+
const status: ChangeKind = code.startsWith("A") ? "added"
|
|
173
|
+
: code.startsWith("D") ? "deleted"
|
|
174
|
+
: "modified";
|
|
175
|
+
|
|
176
|
+
items.push({ file, code, status });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return items;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
stats(ref: string): GitStat[] {
|
|
183
|
+
const result = this.run(["diff", "--numstat", ref]);
|
|
184
|
+
if (result.exitCode !== 0) return [];
|
|
185
|
+
|
|
186
|
+
return result.stdout.split("\n")
|
|
187
|
+
.filter(Boolean)
|
|
188
|
+
.map((line) => {
|
|
189
|
+
const [additions, deletions, file] = line.split("\t");
|
|
190
|
+
return {
|
|
191
|
+
file: file || "",
|
|
192
|
+
additions: parseInt(additions) || 0,
|
|
193
|
+
deletions: parseInt(deletions) || 0,
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ========================================================================
|
|
199
|
+
// Patch Operations
|
|
200
|
+
// ========================================================================
|
|
201
|
+
|
|
202
|
+
patch(ref: string, file: string, options?: PatchOptions): GitPatch {
|
|
203
|
+
const context = options?.context ?? 3;
|
|
204
|
+
const args = ["diff", `--unified=${context}`, ref, "--", file];
|
|
205
|
+
const result = this.run(args, { maxOutputBytes: options?.maxOutputBytes });
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
text: result.stdout,
|
|
209
|
+
truncated: result.truncated,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
patchAll(ref: string, options?: PatchOptions): GitPatch {
|
|
214
|
+
const context = options?.context ?? 3;
|
|
215
|
+
const args = ["diff", `--unified=${context}`, ref];
|
|
216
|
+
const result = this.run(args, { maxOutputBytes: options?.maxOutputBytes });
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
text: result.stdout,
|
|
220
|
+
truncated: result.truncated,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
patchUntracked(file: string, options?: PatchOptions): GitPatch {
|
|
225
|
+
// For untracked files, show the entire content as additions
|
|
226
|
+
const result = this.run(["diff", "--no-index", "/dev/null", file], {
|
|
227
|
+
maxOutputBytes: options?.maxOutputBytes,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
text: result.stdout || result.stderr,
|
|
232
|
+
truncated: result.truncated,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
applyPatch(patch: string): GitResult {
|
|
237
|
+
return this.run(["apply", "--allow-empty", "-"], { stdin: patch });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ========================================================================
|
|
241
|
+
// Show & Log
|
|
242
|
+
// ========================================================================
|
|
243
|
+
|
|
244
|
+
show(ref: string, file: string): string {
|
|
245
|
+
const result = this.run(["show", `${ref}:${file}`]);
|
|
246
|
+
return result.exitCode === 0 ? result.stdout : "";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
log(options?: { maxCount?: number; ref?: string }): Array<{ hash: string; message: string; date: string }> {
|
|
250
|
+
const maxCount = options?.maxCount || 10;
|
|
251
|
+
const args = ["log", `--max-count=${maxCount}`, "--format=%H|%s|%aI"];
|
|
252
|
+
if (options?.ref) args.push(options.ref);
|
|
253
|
+
|
|
254
|
+
const result = this.run(args);
|
|
255
|
+
if (result.exitCode !== 0) return [];
|
|
256
|
+
|
|
257
|
+
return result.stdout.split("\n")
|
|
258
|
+
.filter(Boolean)
|
|
259
|
+
.map((line) => {
|
|
260
|
+
const [hash, message, date] = line.split("|");
|
|
261
|
+
return { hash, message, date };
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ========================================================================
|
|
266
|
+
// Commit Operations
|
|
267
|
+
// ========================================================================
|
|
268
|
+
|
|
269
|
+
add(files: string[] | "."): GitResult {
|
|
270
|
+
const args = ["add"];
|
|
271
|
+
if (files === ".") {
|
|
272
|
+
args.push("-A");
|
|
273
|
+
} else {
|
|
274
|
+
args.push("--", ...files);
|
|
275
|
+
}
|
|
276
|
+
return this.run(args);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
commit(message: string, options?: { amend?: boolean; noVerify?: boolean }): GitResult {
|
|
280
|
+
const args = ["commit", "-m", message];
|
|
281
|
+
if (options?.amend) args.push("--amend");
|
|
282
|
+
if (options?.noVerify) args.push("--no-verify");
|
|
283
|
+
return this.run(args);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ========================================================================
|
|
287
|
+
// Stash Operations
|
|
288
|
+
// ========================================================================
|
|
289
|
+
|
|
290
|
+
stash(options?: { message?: string }): GitResult {
|
|
291
|
+
const args = ["stash", "push"];
|
|
292
|
+
if (options?.message) args.push("-m", options.message);
|
|
293
|
+
return this.run(args);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
stashPop(): GitResult {
|
|
297
|
+
return this.run(["stash", "pop"]);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
stashList(): string[] {
|
|
301
|
+
const result = this.run(["stash", "list"]);
|
|
302
|
+
if (result.exitCode !== 0) return [];
|
|
303
|
+
return result.stdout.split("\n").filter(Boolean);
|
|
304
|
+
}
|
|
305
|
+
}
|