backlog.md 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/backlog-darwin-arm64/backlog +0 -0
- package/bin/backlog-darwin-x64/backlog +0 -0
- package/bin/backlog-linux-arm64/backlog +0 -0
- package/{cli → bin/backlog-linux-x64}/backlog +0 -0
- package/bin/backlog-win32-x64/backlog.exe +0 -0
- package/cli.js +62 -0
- package/package.json +57 -46
- package/.backlog/archive/drafts/readme.md +0 -3
- package/.backlog/archive/drafts/task-41 - temporary-test-task.md +0 -13
- package/.backlog/archive/readme.md +0 -6
- package/.backlog/archive/tasks/readme.md +0 -3
- package/.backlog/archive/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +0 -14
- package/.backlog/config.yml +0 -7
- package/.backlog/decisions/readme.md +0 -7
- package/.backlog/docs/readme.md +0 -20
- package/.backlog/drafts/readme.md +0 -3
- package/.backlog/drafts/task-26 - docs-add-board-export-step-to-agent-dod.md +0 -21
- package/.backlog/drafts/task-28 - add-code-of-conduct.md +0 -20
- package/.backlog/drafts/task-30 - create-changelog.md +0 -19
- package/.backlog/milestones/m-0 - project-setup.md +0 -8
- package/.backlog/milestones/m-1 - cli.md +0 -8
- package/.backlog/milestones/m-2 - cli-kanban.md +0 -8
- package/.backlog/milestones/m-3 - gui.md +0 -8
- package/.backlog/milestones/m-4 - gui-kanban.md +0 -8
- package/.backlog/milestones/m-5 - gui-advanced.md +0 -12
- package/.backlog/milestones/readme.md +0 -3
- package/.backlog/readme.md +0 -5
- package/.backlog/tasks/readme.md +0 -37
- package/.backlog/tasks/task-1 - cli-setup-core-project.md +0 -23
- package/.backlog/tasks/task-10 - gui-init-packaging.md +0 -23
- package/.backlog/tasks/task-11 - gui-kanban-board.md +0 -26
- package/.backlog/tasks/task-12 - gui-advanced.md +0 -25
- package/.backlog/tasks/task-13 - cli-add-agent-instruction-prompt.md +0 -53
- package/.backlog/tasks/task-13.1 - cli-agent-instruction-file-selection.md +0 -40
- package/.backlog/tasks/task-14 - gui-introduction-screens.md +0 -21
- package/.backlog/tasks/task-15 - improve-tasks-readme-with-generic-example-and-cli-reference.md +0 -20
- package/.backlog/tasks/task-16 - improve-docs-readme-with-generic-example-and-cli-reference.md +0 -20
- package/.backlog/tasks/task-17 - improve-drafts-readme-with-generic-example-and-cli-reference.md +0 -20
- package/.backlog/tasks/task-18 - improve-decisions-readme-with-generic-example-and-cli-reference.md +0 -20
- package/.backlog/tasks/task-19 - cli-fix-default-task-status-and-remove-draft-from-statuses.md +0 -55
- package/.backlog/tasks/task-2 - cli-core-logic-library.md +0 -28
- package/.backlog/tasks/task-20 - add-agent-guideline-to-mark-tasks-in-progress-on-start.md +0 -32
- package/.backlog/tasks/task-21 - kanban-board-vertical-layout.md +0 -31
- package/.backlog/tasks/task-22 - cli-prevent-double-dash-in-task-filenames.md +0 -24
- package/.backlog/tasks/task-23 - cli-kanban-board-order-tasks-by-id-asc.md +0 -30
- package/.backlog/tasks/task-24 - handle-subtasks-in-the-kanban-view.md +0 -38
- package/.backlog/tasks/task-24.1 - cli-kanban-board-milestone-view.md +0 -19
- package/.backlog/tasks/task-25 - cli-export-kanban-board-to-readme.md +0 -28
- package/.backlog/tasks/task-27 - add-contributing-guidelines.md +0 -27
- package/.backlog/tasks/task-29 - add-github-templates.md +0 -28
- package/.backlog/tasks/task-3 - cli-implement-backlog-init.md +0 -63
- package/.backlog/tasks/task-31 - update-readme-for-open-source.md +0 -26
- package/.backlog/tasks/task-32 - cli-hide-empty-'no-status'-column.md +0 -31
- package/.backlog/tasks/task-33 - cli-export-milestones-board-as-roadmap.md +0 -20
- package/.backlog/tasks/task-34 - split-readme.md-for-users-and-contributors.md +0 -26
- package/.backlog/tasks/task-35 - finalize-package.json-metadata-for-publishing.md +0 -24
- package/.backlog/tasks/task-36 - cli-prompt-for-project-name-in-init.md +0 -24
- package/.backlog/tasks/task-37 - cli-board-view-open-tasks-in-ide.md +0 -19
- package/.backlog/tasks/task-38 - cli-improved-agent-selection-for-init.md +0 -25
- package/.backlog/tasks/task-39 - cli-fix-empty-agent-instruction-files-on-init.md +0 -31
- package/.backlog/tasks/task-4 - cli-task-management-commands.md +0 -28
- package/.backlog/tasks/task-4.1 - cli-task-create.md +0 -27
- package/.backlog/tasks/task-4.10 - use-cli-to-mark-tasks-done.md +0 -51
- package/.backlog/tasks/task-4.11 - docs-add-definition-of-done-to-agent-guidelines.md +0 -23
- package/.backlog/tasks/task-4.12 - cli-handle-task-id-conflicts-across-branches.md +0 -53
- package/.backlog/tasks/task-4.13 - cli-fix-config-command-local-global-logic.md +0 -58
- package/.backlog/tasks/task-4.2 - cli-task-list-view.md +0 -25
- package/.backlog/tasks/task-4.3 - cli-task-edit.md +0 -24
- package/.backlog/tasks/task-4.4 - cli-task-archive-transition.md +0 -27
- package/.backlog/tasks/task-4.5 - cli-init-prompts-for-reporter-name-and-global-local-config.md +0 -28
- package/.backlog/tasks/task-4.6 - cli-add-empty-assignee-array-field-for-new-tasks.md +0 -35
- package/.backlog/tasks/task-4.7 - cli-parse-unquoted-created_date.md +0 -40
- package/.backlog/tasks/task-4.8 - cli-enforce-description-header.md +0 -48
- package/.backlog/tasks/task-4.9 - cli-normalize-task-id-inputs.md +0 -66
- package/.backlog/tasks/task-40 - cli-board-command-defaults-to-view.md +0 -38
- package/.backlog/tasks/task-41 - cli-migrate-terminal-ui-to-bblessed.md +0 -93
- package/.backlog/tasks/task-41.1 - cli-bblessed-init-wizard.md +0 -42
- package/.backlog/tasks/task-41.2 - cli-bblessed-task-view.md +0 -44
- package/.backlog/tasks/task-41.3 - cli-bblessed-doc-view.md +0 -45
- package/.backlog/tasks/task-41.4 - cli-bblessed-board-view.md +0 -49
- package/.backlog/tasks/task-41.5 - cli-audit-remaining-ui-for-bblessed.md +0 -55
- package/.backlog/tasks/task-42 - visual-hierarchy.md +0 -54
- package/.backlog/tasks/task-43 - remove-duplicate-acceptance-criteria-and-style-metadata.md +0 -56
- package/.backlog/tasks/task-44 - checklist-alignment.md +0 -24
- package/.backlog/tasks/task-45 - safe-line-wrapping.md +0 -23
- package/.backlog/tasks/task-46 - split-pane-layout.md +0 -24
- package/.backlog/tasks/task-47 - sticky-header-in-detail-view.md +0 -43
- package/.backlog/tasks/task-48 - footer-hint-line.md +0 -21
- package/.backlog/tasks/task-49 - status-styling.md +0 -53
- package/.backlog/tasks/task-5 - cli-docs-decisions.md +0 -57
- package/.backlog/tasks/task-50 - borders-&-padding.md +0 -22
- package/.backlog/tasks/task-51 - code-path-styling.md +0 -23
- package/.backlog/tasks/task-52 - cli-filter-tasks-list-by-status-or-assignee.md +0 -29
- package/.backlog/tasks/task-6 - cli-packaging.md +0 -65
- package/.backlog/tasks/task-6.1 - cli-local-installation-support-for-bunx-npx.md +0 -49
- package/.backlog/tasks/task-6.2 - cli-github-actions-for-build-&-publish.md +0 -64
- package/.backlog/tasks/task-7 - cli-kanban-view.md +0 -60
- package/.backlog/tasks/task-7.1 - cli-kanban-board-detect-remote-task-status.md +0 -62
- package/.backlog/tasks/task-8 - gui-project-setup.md +0 -21
- package/.backlog/tasks/task-9 - gui-task-crud.md +0 -24
- package/.cursorrules +0 -223
- package/.gitattributes +0 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -25
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -15
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -8
- package/.github/workflows/ci.yml +0 -36
- package/.husky/pre-commit +0 -1
- package/AGENTS.md +0 -65
- package/CLAUDE.md +0 -87
- package/CONTRIBUTING.md +0 -19
- package/DEVELOPMENT.md +0 -37
- package/biome.json +0 -31
- package/bun.lock +0 -152
- package/cli/.cursorrules-xh86jabm.md +0 -82
- package/cli/AGENTS-xh86jabm.md +0 -82
- package/cli/CLAUDE-xh86jabm.md +0 -82
- package/cli/cli.js +0 -19622
- package/cli/index.js +0 -2
- package/docs/npm-publishing.md +0 -69
- package/scripts/build.js +0 -73
- package/src/agent-instructions.ts +0 -54
- package/src/board.ts +0 -263
- package/src/cli.ts +0 -806
- package/src/constants/index.ts +0 -48
- package/src/core/backlog.ts +0 -183
- package/src/core/remote-tasks.ts +0 -168
- package/src/file-system/operations.ts +0 -515
- package/src/git/operations.ts +0 -189
- package/src/guidelines/.cursorrules.md +0 -82
- package/src/guidelines/AGENTS.md +0 -82
- package/src/guidelines/CLAUDE.md +0 -82
- package/src/guidelines/index.ts +0 -7
- package/src/index.ts +0 -30
- package/src/markdown/parser.ts +0 -145
- package/src/markdown/serializer.ts +0 -71
- package/src/test/agent-instructions.test.ts +0 -62
- package/src/test/board.test.ts +0 -291
- package/src/test/build.test.ts +0 -28
- package/src/test/checklist.test.ts +0 -273
- package/src/test/cli.test.ts +0 -1300
- package/src/test/code-path.test.ts +0 -204
- package/src/test/core.test.ts +0 -330
- package/src/test/filesystem.test.ts +0 -435
- package/src/test/git.test.ts +0 -26
- package/src/test/heading.test.ts +0 -102
- package/src/test/line-wrapping.test.ts +0 -252
- package/src/test/local-install.test.ts +0 -34
- package/src/test/markdown.test.ts +0 -526
- package/src/test/parallel-loading.test.ts +0 -160
- package/src/test/parent-id-normalization.test.ts +0 -48
- package/src/test/remote-id-conflict.test.ts +0 -60
- package/src/test/status-icon.test.ts +0 -93
- package/src/types/blessed.d.ts +0 -14
- package/src/types/index.ts +0 -55
- package/src/types/raw.d.ts +0 -4
- package/src/ui/board.ts +0 -322
- package/src/ui/checklist.ts +0 -103
- package/src/ui/code-path.ts +0 -113
- package/src/ui/heading.ts +0 -121
- package/src/ui/loading.ts +0 -216
- package/src/ui/status-icon.ts +0 -53
- package/src/ui/task-list.ts +0 -168
- package/src/ui/task-viewer.ts +0 -640
- package/src/ui/tui.ts +0 -301
- package/tsconfig.json +0 -26
|
@@ -1,515 +0,0 @@
|
|
|
1
|
-
import { mkdir, unlink } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { DEFAULT_DIRECTORIES, DEFAULT_FILES, DEFAULT_STATUSES } from "../constants/index.ts";
|
|
5
|
-
import { parseDecisionLog, parseDocument, parseTask } from "../markdown/parser.ts";
|
|
6
|
-
import { serializeDecisionLog, serializeDocument, serializeTask } from "../markdown/serializer.ts";
|
|
7
|
-
import type { BacklogConfig, DecisionLog, Document, Task } from "../types/index.ts";
|
|
8
|
-
|
|
9
|
-
export class FileSystem {
|
|
10
|
-
private backlogDir: string;
|
|
11
|
-
private projectRoot: string;
|
|
12
|
-
|
|
13
|
-
constructor(projectRoot: string) {
|
|
14
|
-
this.projectRoot = projectRoot;
|
|
15
|
-
this.backlogDir = join(projectRoot, DEFAULT_DIRECTORIES.BACKLOG);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Public accessors for directory paths
|
|
19
|
-
get tasksDir(): string {
|
|
20
|
-
return join(this.backlogDir, DEFAULT_DIRECTORIES.TASKS);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
get draftsDir(): string {
|
|
24
|
-
return join(this.backlogDir, DEFAULT_DIRECTORIES.DRAFTS);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
get archiveTasksDir(): string {
|
|
28
|
-
return join(this.backlogDir, DEFAULT_DIRECTORIES.ARCHIVE_TASKS);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
get archiveDraftsDir(): string {
|
|
32
|
-
return join(this.backlogDir, DEFAULT_DIRECTORIES.ARCHIVE_DRAFTS);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
get decisionsDir(): string {
|
|
36
|
-
return join(this.backlogDir, DEFAULT_DIRECTORIES.DECISIONS);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
get docsDir(): string {
|
|
40
|
-
return join(this.backlogDir, DEFAULT_DIRECTORIES.DOCS);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async ensureBacklogStructure(): Promise<void> {
|
|
44
|
-
const directories = [
|
|
45
|
-
this.backlogDir,
|
|
46
|
-
join(this.backlogDir, DEFAULT_DIRECTORIES.TASKS),
|
|
47
|
-
join(this.backlogDir, DEFAULT_DIRECTORIES.DRAFTS),
|
|
48
|
-
join(this.backlogDir, DEFAULT_DIRECTORIES.ARCHIVE_TASKS),
|
|
49
|
-
join(this.backlogDir, DEFAULT_DIRECTORIES.ARCHIVE_DRAFTS),
|
|
50
|
-
join(this.backlogDir, DEFAULT_DIRECTORIES.DOCS),
|
|
51
|
-
join(this.backlogDir, DEFAULT_DIRECTORIES.DECISIONS),
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
for (const dir of directories) {
|
|
55
|
-
await mkdir(dir, { recursive: true });
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Task operations
|
|
60
|
-
async saveTask(task: Task): Promise<void> {
|
|
61
|
-
const taskId = task.id.startsWith("task-") ? task.id : `task-${task.id}`;
|
|
62
|
-
const filename = `${taskId} - ${this.sanitizeFilename(task.title)}.md`;
|
|
63
|
-
const filepath = join(this.tasksDir, filename);
|
|
64
|
-
const content = serializeTask(task);
|
|
65
|
-
|
|
66
|
-
// Delete any existing task files with the same ID but different filenames
|
|
67
|
-
try {
|
|
68
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: this.tasksDir }));
|
|
69
|
-
const normalizedTaskId = taskId;
|
|
70
|
-
const existingFiles = files.filter((file) => file.startsWith(`${normalizedTaskId} -`) && file !== filename);
|
|
71
|
-
|
|
72
|
-
for (const existingFile of existingFiles) {
|
|
73
|
-
const existingPath = join(this.tasksDir, existingFile);
|
|
74
|
-
await unlink(existingPath);
|
|
75
|
-
}
|
|
76
|
-
} catch {
|
|
77
|
-
// Ignore errors if no existing files found
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
await this.ensureDirectoryExists(dirname(filepath));
|
|
81
|
-
await Bun.write(filepath, content);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async loadTask(taskId: string): Promise<Task | null> {
|
|
85
|
-
try {
|
|
86
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: this.tasksDir }));
|
|
87
|
-
const normalizedTaskId = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
|
|
88
|
-
const taskFile = files.find((file) => file.startsWith(`${normalizedTaskId} -`));
|
|
89
|
-
|
|
90
|
-
if (!taskFile) return null;
|
|
91
|
-
|
|
92
|
-
const filepath = join(this.tasksDir, taskFile);
|
|
93
|
-
const content = await Bun.file(filepath).text();
|
|
94
|
-
return parseTask(content);
|
|
95
|
-
} catch (error) {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async listTasks(): Promise<Task[]> {
|
|
101
|
-
try {
|
|
102
|
-
const taskFiles = await Array.fromAsync(new Bun.Glob("task-*.md").scan({ cwd: this.tasksDir }));
|
|
103
|
-
|
|
104
|
-
const tasks: Task[] = [];
|
|
105
|
-
for (const file of taskFiles) {
|
|
106
|
-
const filepath = join(this.tasksDir, file);
|
|
107
|
-
const content = await Bun.file(filepath).text();
|
|
108
|
-
tasks.push(parseTask(content));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return tasks.sort((a, b) => a.id.localeCompare(b.id));
|
|
112
|
-
} catch (error) {
|
|
113
|
-
return [];
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async listTasksWithMetadata(): Promise<Array<Task & { lastModified?: Date }>> {
|
|
118
|
-
try {
|
|
119
|
-
const taskFiles = await Array.fromAsync(new Bun.Glob("task-*.md").scan({ cwd: this.tasksDir }));
|
|
120
|
-
|
|
121
|
-
const tasks: Array<Task & { lastModified?: Date }> = [];
|
|
122
|
-
for (const file of taskFiles) {
|
|
123
|
-
const filepath = join(this.tasksDir, file);
|
|
124
|
-
const bunFile = Bun.file(filepath);
|
|
125
|
-
const content = await bunFile.text();
|
|
126
|
-
const task = parseTask(content);
|
|
127
|
-
|
|
128
|
-
// Get file stats for modification time
|
|
129
|
-
const stats = await bunFile.stat();
|
|
130
|
-
tasks.push({
|
|
131
|
-
...task,
|
|
132
|
-
lastModified: new Date(stats.mtime),
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return tasks.sort((a, b) => a.id.localeCompare(b.id));
|
|
137
|
-
} catch (error) {
|
|
138
|
-
return [];
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async archiveTask(taskId: string): Promise<boolean> {
|
|
143
|
-
try {
|
|
144
|
-
const sourceFiles = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: this.tasksDir }));
|
|
145
|
-
const normalizedTaskId = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
|
|
146
|
-
const taskFile = sourceFiles.find((file) => file.startsWith(`${normalizedTaskId} -`));
|
|
147
|
-
|
|
148
|
-
if (!taskFile) return false;
|
|
149
|
-
|
|
150
|
-
const sourcePath = join(this.tasksDir, taskFile);
|
|
151
|
-
const targetPath = join(this.archiveTasksDir, taskFile);
|
|
152
|
-
|
|
153
|
-
// Read source file
|
|
154
|
-
const content = await Bun.file(sourcePath).text();
|
|
155
|
-
|
|
156
|
-
// Write to target and ensure directory exists
|
|
157
|
-
await this.ensureDirectoryExists(dirname(targetPath));
|
|
158
|
-
await Bun.write(targetPath, content);
|
|
159
|
-
|
|
160
|
-
// Remove source file
|
|
161
|
-
await unlink(sourcePath);
|
|
162
|
-
|
|
163
|
-
return true;
|
|
164
|
-
} catch (error) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async archiveDraft(taskId: string): Promise<boolean> {
|
|
170
|
-
try {
|
|
171
|
-
const sourceFiles = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: this.draftsDir }));
|
|
172
|
-
const normalizedTaskId = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
|
|
173
|
-
const taskFile = sourceFiles.find((file) => file.startsWith(`${normalizedTaskId} -`));
|
|
174
|
-
|
|
175
|
-
if (!taskFile) return false;
|
|
176
|
-
|
|
177
|
-
const sourcePath = join(this.draftsDir, taskFile);
|
|
178
|
-
const targetPath = join(this.archiveDraftsDir, taskFile);
|
|
179
|
-
|
|
180
|
-
const content = await Bun.file(sourcePath).text();
|
|
181
|
-
await this.ensureDirectoryExists(dirname(targetPath));
|
|
182
|
-
await Bun.write(targetPath, content);
|
|
183
|
-
|
|
184
|
-
await unlink(sourcePath);
|
|
185
|
-
|
|
186
|
-
return true;
|
|
187
|
-
} catch {
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async promoteDraft(taskId: string): Promise<boolean> {
|
|
193
|
-
try {
|
|
194
|
-
const sourceFiles = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: this.draftsDir }));
|
|
195
|
-
const normalizedTaskId = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
|
|
196
|
-
const taskFile = sourceFiles.find((file) => file.startsWith(`${normalizedTaskId} -`));
|
|
197
|
-
|
|
198
|
-
if (!taskFile) return false;
|
|
199
|
-
|
|
200
|
-
const sourcePath = join(this.draftsDir, taskFile);
|
|
201
|
-
const targetPath = join(this.tasksDir, taskFile);
|
|
202
|
-
|
|
203
|
-
const content = await Bun.file(sourcePath).text();
|
|
204
|
-
await this.ensureDirectoryExists(dirname(targetPath));
|
|
205
|
-
await Bun.write(targetPath, content);
|
|
206
|
-
|
|
207
|
-
await unlink(sourcePath);
|
|
208
|
-
|
|
209
|
-
return true;
|
|
210
|
-
} catch {
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async demoteTask(taskId: string): Promise<boolean> {
|
|
216
|
-
try {
|
|
217
|
-
const sourceFiles = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: this.tasksDir }));
|
|
218
|
-
const normalizedTaskId = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
|
|
219
|
-
const taskFile = sourceFiles.find((file) => file.startsWith(`${normalizedTaskId} -`));
|
|
220
|
-
|
|
221
|
-
if (!taskFile) return false;
|
|
222
|
-
|
|
223
|
-
const sourcePath = join(this.tasksDir, taskFile);
|
|
224
|
-
const targetPath = join(this.draftsDir, taskFile);
|
|
225
|
-
|
|
226
|
-
const content = await Bun.file(sourcePath).text();
|
|
227
|
-
await this.ensureDirectoryExists(dirname(targetPath));
|
|
228
|
-
await Bun.write(targetPath, content);
|
|
229
|
-
|
|
230
|
-
await unlink(sourcePath);
|
|
231
|
-
|
|
232
|
-
return true;
|
|
233
|
-
} catch {
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Draft operations
|
|
239
|
-
async saveDraft(task: Task): Promise<void> {
|
|
240
|
-
const taskId = task.id.startsWith("task-") ? task.id : `task-${task.id}`;
|
|
241
|
-
const filename = `${taskId} - ${this.sanitizeFilename(task.title)}.md`;
|
|
242
|
-
const filepath = join(this.draftsDir, filename);
|
|
243
|
-
const content = serializeTask(task);
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: this.draftsDir }));
|
|
247
|
-
const existingFiles = files.filter((file) => file.startsWith(`${taskId} -`) && file !== filename);
|
|
248
|
-
|
|
249
|
-
for (const existingFile of existingFiles) {
|
|
250
|
-
const existingPath = join(this.draftsDir, existingFile);
|
|
251
|
-
await unlink(existingPath);
|
|
252
|
-
}
|
|
253
|
-
} catch {
|
|
254
|
-
// Ignore errors if no existing files found
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
await this.ensureDirectoryExists(dirname(filepath));
|
|
258
|
-
await Bun.write(filepath, content);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async loadDraft(taskId: string): Promise<Task | null> {
|
|
262
|
-
try {
|
|
263
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: this.draftsDir }));
|
|
264
|
-
const normalizedTaskId = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
|
|
265
|
-
const taskFile = files.find((file) => file.startsWith(`${normalizedTaskId} -`));
|
|
266
|
-
|
|
267
|
-
if (!taskFile) return null;
|
|
268
|
-
|
|
269
|
-
const filepath = join(this.draftsDir, taskFile);
|
|
270
|
-
const content = await Bun.file(filepath).text();
|
|
271
|
-
return parseTask(content);
|
|
272
|
-
} catch {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
async listDrafts(): Promise<Task[]> {
|
|
278
|
-
try {
|
|
279
|
-
const taskFiles = await Array.fromAsync(new Bun.Glob("task-*.md").scan({ cwd: this.draftsDir }));
|
|
280
|
-
|
|
281
|
-
const tasks: Task[] = [];
|
|
282
|
-
for (const file of taskFiles) {
|
|
283
|
-
const filepath = join(this.draftsDir, file);
|
|
284
|
-
const content = await Bun.file(filepath).text();
|
|
285
|
-
tasks.push(parseTask(content));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return tasks.sort((a, b) => a.id.localeCompare(b.id));
|
|
289
|
-
} catch {
|
|
290
|
-
return [];
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Decision log operations
|
|
295
|
-
async saveDecisionLog(decision: DecisionLog): Promise<void> {
|
|
296
|
-
const filename = `decision-${decision.id} - ${this.sanitizeFilename(decision.title)}.md`;
|
|
297
|
-
const filepath = join(this.decisionsDir, filename);
|
|
298
|
-
const content = serializeDecisionLog(decision);
|
|
299
|
-
|
|
300
|
-
await this.ensureDirectoryExists(dirname(filepath));
|
|
301
|
-
await Bun.write(filepath, content);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async loadDecisionLog(decisionId: string): Promise<DecisionLog | null> {
|
|
305
|
-
try {
|
|
306
|
-
const files = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: this.decisionsDir }));
|
|
307
|
-
const decisionFile = files.find((file) => file.startsWith(`decision-${decisionId} -`));
|
|
308
|
-
|
|
309
|
-
if (!decisionFile) return null;
|
|
310
|
-
|
|
311
|
-
const filepath = join(this.decisionsDir, decisionFile);
|
|
312
|
-
const content = await Bun.file(filepath).text();
|
|
313
|
-
return parseDecisionLog(content);
|
|
314
|
-
} catch (error) {
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Document operations
|
|
320
|
-
async saveDocument(document: Document, subPath = ""): Promise<void> {
|
|
321
|
-
const dir = join(this.docsDir, subPath);
|
|
322
|
-
const filename = `${this.sanitizeFilename(document.title)}.md`;
|
|
323
|
-
const filepath = join(dir, filename);
|
|
324
|
-
const content = serializeDocument(document);
|
|
325
|
-
|
|
326
|
-
await this.ensureDirectoryExists(dirname(filepath));
|
|
327
|
-
await Bun.write(filepath, content);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async listDecisionLogs(): Promise<DecisionLog[]> {
|
|
331
|
-
try {
|
|
332
|
-
const decisionFiles = await Array.fromAsync(new Bun.Glob("decision-*.md").scan({ cwd: this.decisionsDir }));
|
|
333
|
-
const decisions: DecisionLog[] = [];
|
|
334
|
-
for (const file of decisionFiles) {
|
|
335
|
-
const filepath = join(this.decisionsDir, file);
|
|
336
|
-
const content = await Bun.file(filepath).text();
|
|
337
|
-
decisions.push(parseDecisionLog(content));
|
|
338
|
-
}
|
|
339
|
-
return decisions.sort((a, b) => a.id.localeCompare(b.id));
|
|
340
|
-
} catch {
|
|
341
|
-
return [];
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async listDocuments(): Promise<Document[]> {
|
|
346
|
-
try {
|
|
347
|
-
const docFiles = await Array.fromAsync(new Bun.Glob("*.md").scan({ cwd: this.docsDir }));
|
|
348
|
-
const docs: Document[] = [];
|
|
349
|
-
for (const file of docFiles) {
|
|
350
|
-
const filepath = join(this.docsDir, file);
|
|
351
|
-
const content = await Bun.file(filepath).text();
|
|
352
|
-
docs.push(parseDocument(content));
|
|
353
|
-
}
|
|
354
|
-
return docs.sort((a, b) => a.title.localeCompare(b.title));
|
|
355
|
-
} catch {
|
|
356
|
-
return [];
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Config operations
|
|
361
|
-
async loadConfig(): Promise<BacklogConfig | null> {
|
|
362
|
-
try {
|
|
363
|
-
const configPath = join(this.backlogDir, DEFAULT_FILES.CONFIG);
|
|
364
|
-
const content = await Bun.file(configPath).text();
|
|
365
|
-
return this.parseConfig(content);
|
|
366
|
-
} catch (error) {
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async saveConfig(config: BacklogConfig): Promise<void> {
|
|
372
|
-
const configPath = join(this.backlogDir, DEFAULT_FILES.CONFIG);
|
|
373
|
-
const content = this.serializeConfig(config);
|
|
374
|
-
await Bun.write(configPath, content);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
async getUserSetting(key: string, global = false): Promise<string | undefined> {
|
|
378
|
-
const settings = await this.loadUserSettings(global);
|
|
379
|
-
return settings ? settings[key] : undefined;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async setUserSetting(key: string, value: string, global = false): Promise<void> {
|
|
383
|
-
const settings = (await this.loadUserSettings(global)) || {};
|
|
384
|
-
settings[key] = value;
|
|
385
|
-
await this.saveUserSettings(settings, global);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
private async loadUserSettings(global = false): Promise<Record<string, string> | null> {
|
|
389
|
-
const filePath = global
|
|
390
|
-
? join(homedir(), ".backlog", DEFAULT_FILES.USER)
|
|
391
|
-
: join(this.projectRoot, DEFAULT_FILES.USER);
|
|
392
|
-
try {
|
|
393
|
-
const content = await Bun.file(filePath).text();
|
|
394
|
-
const result: Record<string, string> = {};
|
|
395
|
-
for (const line of content.split(/\r?\n/)) {
|
|
396
|
-
const trimmed = line.trim();
|
|
397
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
398
|
-
const idx = trimmed.indexOf(":");
|
|
399
|
-
if (idx === -1) continue;
|
|
400
|
-
const k = trimmed.substring(0, idx).trim();
|
|
401
|
-
const v = trimmed
|
|
402
|
-
.substring(idx + 1)
|
|
403
|
-
.trim()
|
|
404
|
-
.replace(/^['"]|['"]$/g, "");
|
|
405
|
-
result[k] = v;
|
|
406
|
-
}
|
|
407
|
-
return result;
|
|
408
|
-
} catch {
|
|
409
|
-
return null;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
private async saveUserSettings(settings: Record<string, string>, global = false): Promise<void> {
|
|
414
|
-
const filePath = global
|
|
415
|
-
? join(homedir(), ".backlog", DEFAULT_FILES.USER)
|
|
416
|
-
: join(this.projectRoot, DEFAULT_FILES.USER);
|
|
417
|
-
await this.ensureDirectoryExists(dirname(filePath));
|
|
418
|
-
const lines = Object.entries(settings).map(([k, v]) => `${k}: ${v}`);
|
|
419
|
-
await Bun.write(filePath, `${lines.join("\n")}\n`);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Utility methods
|
|
423
|
-
private sanitizeFilename(filename: string): string {
|
|
424
|
-
return filename
|
|
425
|
-
.replace(/[<>:"/\\|?*]/g, "-")
|
|
426
|
-
.replace(/\s+/g, "-")
|
|
427
|
-
.replace(/-+/g, "-")
|
|
428
|
-
.replace(/^-|-$/g, "")
|
|
429
|
-
.toLowerCase();
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
private async ensureDirectoryExists(dirPath: string): Promise<void> {
|
|
433
|
-
try {
|
|
434
|
-
await mkdir(dirPath, { recursive: true });
|
|
435
|
-
} catch (error) {
|
|
436
|
-
// Directory creation failed, ignore
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
private parseConfig(content: string): BacklogConfig {
|
|
441
|
-
const config: Partial<BacklogConfig> = {};
|
|
442
|
-
const lines = content.split("\n");
|
|
443
|
-
|
|
444
|
-
for (const line of lines) {
|
|
445
|
-
const trimmed = line.trim();
|
|
446
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
447
|
-
|
|
448
|
-
const colonIndex = trimmed.indexOf(":");
|
|
449
|
-
if (colonIndex === -1) continue;
|
|
450
|
-
|
|
451
|
-
const key = trimmed.substring(0, colonIndex).trim();
|
|
452
|
-
const value = trimmed.substring(colonIndex + 1).trim();
|
|
453
|
-
|
|
454
|
-
switch (key) {
|
|
455
|
-
case "project_name":
|
|
456
|
-
config.projectName = value.replace(/['"]/g, "");
|
|
457
|
-
break;
|
|
458
|
-
case "default_assignee":
|
|
459
|
-
config.defaultAssignee = value.replace(/['"]/g, "");
|
|
460
|
-
break;
|
|
461
|
-
case "default_reporter":
|
|
462
|
-
config.defaultReporter = value.replace(/['"]/g, "");
|
|
463
|
-
break;
|
|
464
|
-
case "default_status":
|
|
465
|
-
config.defaultStatus = value.replace(/['"]/g, "");
|
|
466
|
-
break;
|
|
467
|
-
case "statuses":
|
|
468
|
-
case "labels":
|
|
469
|
-
case "milestones":
|
|
470
|
-
if (value.startsWith("[") && value.endsWith("]")) {
|
|
471
|
-
const arrayContent = value.slice(1, -1);
|
|
472
|
-
config[key] = arrayContent
|
|
473
|
-
.split(",")
|
|
474
|
-
.map((item) => item.trim().replace(/['"]/g, ""))
|
|
475
|
-
.filter(Boolean);
|
|
476
|
-
}
|
|
477
|
-
break;
|
|
478
|
-
case "date_format":
|
|
479
|
-
config.dateFormat = value.replace(/['"]/g, "");
|
|
480
|
-
break;
|
|
481
|
-
case "max_column_width":
|
|
482
|
-
config.maxColumnWidth = Number.parseInt(value, 10);
|
|
483
|
-
break;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
return {
|
|
488
|
-
projectName: config.projectName || "",
|
|
489
|
-
defaultAssignee: config.defaultAssignee,
|
|
490
|
-
defaultReporter: config.defaultReporter,
|
|
491
|
-
statuses: config.statuses || [...DEFAULT_STATUSES],
|
|
492
|
-
labels: config.labels || [],
|
|
493
|
-
milestones: config.milestones || [],
|
|
494
|
-
defaultStatus: config.defaultStatus,
|
|
495
|
-
dateFormat: config.dateFormat || "yyyy-mm-dd",
|
|
496
|
-
maxColumnWidth: config.maxColumnWidth,
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
private serializeConfig(config: BacklogConfig): string {
|
|
501
|
-
const lines = [
|
|
502
|
-
`project_name: "${config.projectName}"`,
|
|
503
|
-
...(config.defaultAssignee ? [`default_assignee: "${config.defaultAssignee}"`] : []),
|
|
504
|
-
...(config.defaultReporter ? [`default_reporter: "${config.defaultReporter}"`] : []),
|
|
505
|
-
...(config.defaultStatus ? [`default_status: "${config.defaultStatus}"`] : []),
|
|
506
|
-
`statuses: [${config.statuses.map((s) => `"${s}"`).join(", ")}]`,
|
|
507
|
-
`labels: [${config.labels.map((l) => `"${l}"`).join(", ")}]`,
|
|
508
|
-
`milestones: [${config.milestones.map((m) => `"${m}"`).join(", ")}]`,
|
|
509
|
-
`date_format: ${config.dateFormat}`,
|
|
510
|
-
...(config.maxColumnWidth ? [`max_column_width: ${config.maxColumnWidth}`] : []),
|
|
511
|
-
];
|
|
512
|
-
|
|
513
|
-
return `${lines.join("\n")}\n`;
|
|
514
|
-
}
|
|
515
|
-
}
|
package/src/git/operations.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
export class GitOperations {
|
|
2
|
-
private projectRoot: string;
|
|
3
|
-
|
|
4
|
-
constructor(projectRoot: string) {
|
|
5
|
-
this.projectRoot = projectRoot;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
async addFile(filePath: string): Promise<void> {
|
|
9
|
-
await this.execGit(["add", filePath]);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async addFiles(filePaths: string[]): Promise<void> {
|
|
13
|
-
await this.execGit(["add", ...filePaths]);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async commitTaskChange(taskId: string, message: string): Promise<void> {
|
|
17
|
-
const commitMessage = `${taskId} - ${message}`;
|
|
18
|
-
await this.execGit(["commit", "-m", commitMessage]);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async commitChanges(message: string): Promise<void> {
|
|
22
|
-
await this.execGit(["commit", "-m", message]);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async getStatus(): Promise<string> {
|
|
26
|
-
const { stdout } = await this.execGit(["status", "--porcelain"]);
|
|
27
|
-
return stdout;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async isClean(): Promise<boolean> {
|
|
31
|
-
const status = await this.getStatus();
|
|
32
|
-
return status.trim() === "";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async getCurrentBranch(): Promise<string> {
|
|
36
|
-
const { stdout } = await this.execGit(["branch", "--show-current"]);
|
|
37
|
-
return stdout.trim();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async createBranch(branchName: string): Promise<void> {
|
|
41
|
-
await this.execGit(["checkout", "-b", branchName]);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async switchBranch(branchName: string): Promise<void> {
|
|
45
|
-
await this.execGit(["checkout", branchName]);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async hasUncommittedChanges(): Promise<boolean> {
|
|
49
|
-
const status = await this.getStatus();
|
|
50
|
-
return status.trim() !== "";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async getLastCommitMessage(): Promise<string> {
|
|
54
|
-
const { stdout } = await this.execGit(["log", "-1", "--pretty=format:%s"]);
|
|
55
|
-
return stdout.trim();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async fetch(remote = "origin"): Promise<void> {
|
|
59
|
-
await this.execGit(["fetch", remote]);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async listFilesInRemoteBranch(branch: string, path: string): Promise<string[]> {
|
|
63
|
-
const { stdout } = await this.execGit(["ls-tree", "-r", `origin/${branch}`, "--name-only", "--", path]);
|
|
64
|
-
return stdout
|
|
65
|
-
.split(/\r?\n/)
|
|
66
|
-
.map((l) => l.trim())
|
|
67
|
-
.filter(Boolean);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async addAndCommitTaskFile(taskId: string, filePath: string, action: "create" | "update" | "archive"): Promise<void> {
|
|
71
|
-
await this.addFile(filePath);
|
|
72
|
-
|
|
73
|
-
const actionMessages = {
|
|
74
|
-
create: `Create task ${taskId}`,
|
|
75
|
-
update: `Update task ${taskId}`,
|
|
76
|
-
archive: `Archive task ${taskId}`,
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
await this.commitTaskChange(taskId, actionMessages[action]);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async stageBacklogDirectory(): Promise<void> {
|
|
83
|
-
await this.execGit(["add", ".backlog/"]);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async commitBacklogChanges(message: string): Promise<void> {
|
|
87
|
-
await this.stageBacklogDirectory();
|
|
88
|
-
|
|
89
|
-
const hasChanges = !(await this.isClean());
|
|
90
|
-
if (hasChanges) {
|
|
91
|
-
await this.commitChanges(`backlog: ${message}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async listRemoteBranches(remote = "origin"): Promise<string[]> {
|
|
96
|
-
const { stdout } = await this.execGit(["branch", "-r", "--format=%(refname:strip=2)"]);
|
|
97
|
-
return stdout
|
|
98
|
-
.split("\n")
|
|
99
|
-
.map((l) => l.trim())
|
|
100
|
-
.filter((b) => b.startsWith(`${remote}/`))
|
|
101
|
-
.map((b) => b.replace(`${remote}/`, ""))
|
|
102
|
-
.filter(Boolean);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async listFilesInTree(ref: string, path: string): Promise<string[]> {
|
|
106
|
-
const { stdout } = await this.execGit(["ls-tree", "-r", "--name-only", ref, "--", path]);
|
|
107
|
-
return stdout
|
|
108
|
-
.split("\n")
|
|
109
|
-
.map((l) => l.trim())
|
|
110
|
-
.filter(Boolean);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async showFile(ref: string, filePath: string): Promise<string> {
|
|
114
|
-
const { stdout } = await this.execGit(["show", `${ref}:${filePath}`]);
|
|
115
|
-
return stdout;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async getFileLastModifiedTime(ref: string, filePath: string): Promise<Date | null> {
|
|
119
|
-
try {
|
|
120
|
-
// Get the last commit that modified this file in the given ref
|
|
121
|
-
const { stdout } = await this.execGit([
|
|
122
|
-
"log",
|
|
123
|
-
"-1",
|
|
124
|
-
"--format=%aI", // Author date in ISO 8601 format
|
|
125
|
-
ref,
|
|
126
|
-
"--",
|
|
127
|
-
filePath,
|
|
128
|
-
]);
|
|
129
|
-
const timestamp = stdout.trim();
|
|
130
|
-
if (timestamp) {
|
|
131
|
-
return new Date(timestamp);
|
|
132
|
-
}
|
|
133
|
-
return null;
|
|
134
|
-
} catch {
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private async execGit(args: string[]): Promise<{ stdout: string; stderr: string }> {
|
|
140
|
-
try {
|
|
141
|
-
const proc = Bun.spawn(["git", ...args], {
|
|
142
|
-
cwd: this.projectRoot,
|
|
143
|
-
stdout: "pipe",
|
|
144
|
-
stderr: "pipe",
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
148
|
-
|
|
149
|
-
const exitCode = await proc.exited;
|
|
150
|
-
|
|
151
|
-
if (exitCode !== 0) {
|
|
152
|
-
throw new Error(`Git command failed (exit code ${exitCode}): git ${args.join(" ")}\n${stderr}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return { stdout, stderr };
|
|
156
|
-
} catch (error: unknown) {
|
|
157
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
158
|
-
throw new Error(`Git command failed: git ${args.join(" ")}\n${message}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export async function isGitRepository(projectRoot: string): Promise<boolean> {
|
|
164
|
-
try {
|
|
165
|
-
const proc = Bun.spawn(["git", "rev-parse", "--git-dir"], {
|
|
166
|
-
cwd: projectRoot,
|
|
167
|
-
stdout: "pipe",
|
|
168
|
-
stderr: "pipe",
|
|
169
|
-
});
|
|
170
|
-
const exitCode = await proc.exited;
|
|
171
|
-
return exitCode === 0;
|
|
172
|
-
} catch {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export async function initializeGitRepository(projectRoot: string): Promise<void> {
|
|
178
|
-
const proc = Bun.spawn(["git", "init"], {
|
|
179
|
-
cwd: projectRoot,
|
|
180
|
-
stdout: "pipe",
|
|
181
|
-
stderr: "pipe",
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const exitCode = await proc.exited;
|
|
185
|
-
if (exitCode !== 0) {
|
|
186
|
-
const stderr = await new Response(proc.stderr).text();
|
|
187
|
-
throw new Error(`Failed to initialize git repository: ${stderr}`);
|
|
188
|
-
}
|
|
189
|
-
}
|