ralphctl 0.1.0 → 0.1.2

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.
Files changed (130) hide show
  1. package/README.md +58 -24
  2. package/dist/add-HGJCLWED.mjs +14 -0
  3. package/dist/add-MRGCS3US.mjs +14 -0
  4. package/dist/chunk-6PYTKGB5.mjs +316 -0
  5. package/dist/chunk-7TG3EAQ2.mjs +20 -0
  6. package/dist/chunk-EKMZZRWI.mjs +521 -0
  7. package/dist/chunk-JON4GCLR.mjs +59 -0
  8. package/dist/chunk-LOR7QBXX.mjs +3683 -0
  9. package/dist/chunk-MNMQC36F.mjs +556 -0
  10. package/dist/chunk-MRKOFVTM.mjs +537 -0
  11. package/dist/chunk-NTWO2LXB.mjs +52 -0
  12. package/dist/chunk-QBXHAXHI.mjs +562 -0
  13. package/dist/chunk-WGHJI3OI.mjs +214 -0
  14. package/dist/cli.mjs +4245 -0
  15. package/dist/create-MG7E7PLQ.mjs +10 -0
  16. package/dist/handle-UG5M2OON.mjs +22 -0
  17. package/dist/multiline-OHSNFCRG.mjs +40 -0
  18. package/dist/project-NT3L4FTB.mjs +28 -0
  19. package/dist/resolver-WSFWKACM.mjs +153 -0
  20. package/dist/sprint-4VHDLGFN.mjs +37 -0
  21. package/dist/wizard-LRELAN2J.mjs +196 -0
  22. package/package.json +19 -28
  23. package/CHANGELOG.md +0 -94
  24. package/bin/ralphctl +0 -13
  25. package/src/ai/executor.ts +0 -973
  26. package/src/ai/lifecycle.ts +0 -45
  27. package/src/ai/parser.ts +0 -40
  28. package/src/ai/permissions.ts +0 -207
  29. package/src/ai/process-manager.ts +0 -248
  30. package/src/ai/prompts/index.ts +0 -89
  31. package/src/ai/rate-limiter.ts +0 -89
  32. package/src/ai/runner.ts +0 -478
  33. package/src/ai/session.ts +0 -319
  34. package/src/ai/task-context.ts +0 -270
  35. package/src/cli-metadata.ts +0 -7
  36. package/src/cli.ts +0 -65
  37. package/src/commands/completion/index.ts +0 -33
  38. package/src/commands/config/config.ts +0 -58
  39. package/src/commands/config/index.ts +0 -33
  40. package/src/commands/dashboard/dashboard.ts +0 -5
  41. package/src/commands/dashboard/index.ts +0 -6
  42. package/src/commands/doctor/doctor.ts +0 -271
  43. package/src/commands/doctor/index.ts +0 -25
  44. package/src/commands/progress/index.ts +0 -25
  45. package/src/commands/progress/log.ts +0 -64
  46. package/src/commands/progress/show.ts +0 -14
  47. package/src/commands/project/add.ts +0 -336
  48. package/src/commands/project/index.ts +0 -104
  49. package/src/commands/project/list.ts +0 -31
  50. package/src/commands/project/remove.ts +0 -43
  51. package/src/commands/project/repo.ts +0 -118
  52. package/src/commands/project/show.ts +0 -49
  53. package/src/commands/sprint/close.ts +0 -180
  54. package/src/commands/sprint/context.ts +0 -109
  55. package/src/commands/sprint/create.ts +0 -60
  56. package/src/commands/sprint/current.ts +0 -75
  57. package/src/commands/sprint/delete.ts +0 -72
  58. package/src/commands/sprint/health.ts +0 -229
  59. package/src/commands/sprint/ideate.ts +0 -496
  60. package/src/commands/sprint/index.ts +0 -226
  61. package/src/commands/sprint/list.ts +0 -86
  62. package/src/commands/sprint/plan-utils.ts +0 -207
  63. package/src/commands/sprint/plan.ts +0 -549
  64. package/src/commands/sprint/refine.ts +0 -359
  65. package/src/commands/sprint/requirements.ts +0 -58
  66. package/src/commands/sprint/show.ts +0 -140
  67. package/src/commands/sprint/start.ts +0 -119
  68. package/src/commands/sprint/switch.ts +0 -20
  69. package/src/commands/task/add.ts +0 -316
  70. package/src/commands/task/import.ts +0 -150
  71. package/src/commands/task/index.ts +0 -123
  72. package/src/commands/task/list.ts +0 -145
  73. package/src/commands/task/next.ts +0 -45
  74. package/src/commands/task/remove.ts +0 -47
  75. package/src/commands/task/reorder.ts +0 -45
  76. package/src/commands/task/show.ts +0 -111
  77. package/src/commands/task/status.ts +0 -99
  78. package/src/commands/ticket/add.ts +0 -265
  79. package/src/commands/ticket/edit.ts +0 -166
  80. package/src/commands/ticket/index.ts +0 -114
  81. package/src/commands/ticket/list.ts +0 -128
  82. package/src/commands/ticket/refine-utils.ts +0 -89
  83. package/src/commands/ticket/refine.ts +0 -268
  84. package/src/commands/ticket/remove.ts +0 -48
  85. package/src/commands/ticket/show.ts +0 -74
  86. package/src/completion/handle.ts +0 -30
  87. package/src/completion/resolver.ts +0 -241
  88. package/src/interactive/dashboard.ts +0 -268
  89. package/src/interactive/escapable.ts +0 -81
  90. package/src/interactive/file-browser.ts +0 -153
  91. package/src/interactive/index.ts +0 -429
  92. package/src/interactive/menu.ts +0 -403
  93. package/src/interactive/selectors.ts +0 -273
  94. package/src/interactive/wizard.ts +0 -221
  95. package/src/providers/claude.ts +0 -53
  96. package/src/providers/copilot.ts +0 -86
  97. package/src/providers/index.ts +0 -43
  98. package/src/providers/types.ts +0 -85
  99. package/src/schemas/index.ts +0 -130
  100. package/src/store/config.ts +0 -74
  101. package/src/store/progress.ts +0 -230
  102. package/src/store/project.ts +0 -276
  103. package/src/store/sprint.ts +0 -229
  104. package/src/store/task.ts +0 -443
  105. package/src/store/ticket.ts +0 -178
  106. package/src/theme/index.ts +0 -215
  107. package/src/theme/ui.ts +0 -872
  108. package/src/utils/detect-scripts.ts +0 -247
  109. package/src/utils/editor-input.ts +0 -41
  110. package/src/utils/editor.ts +0 -37
  111. package/src/utils/exit-codes.ts +0 -27
  112. package/src/utils/file-lock.ts +0 -135
  113. package/src/utils/git.ts +0 -185
  114. package/src/utils/ids.ts +0 -37
  115. package/src/utils/issue-fetch.ts +0 -244
  116. package/src/utils/json-extract.ts +0 -62
  117. package/src/utils/multiline.ts +0 -61
  118. package/src/utils/path-selector.ts +0 -236
  119. package/src/utils/paths.ts +0 -108
  120. package/src/utils/provider.ts +0 -34
  121. package/src/utils/requirements-export.ts +0 -63
  122. package/src/utils/storage.ts +0 -107
  123. package/tsconfig.json +0 -25
  124. /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
  125. /package/{src/ai → dist}/prompts/ideate.md +0 -0
  126. /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
  127. /package/{src/ai → dist}/prompts/plan-common.md +0 -0
  128. /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
  129. /package/{src/ai → dist}/prompts/task-execution.md +0 -0
  130. /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
@@ -1,230 +0,0 @@
1
- import { execSync } from 'node:child_process';
2
- import { assertSafeCwd, getProgressFilePath } from '@src/utils/paths.ts';
3
- import { appendToFile, FileNotFoundError, readTextFile } from '@src/utils/storage.ts';
4
- import { assertSprintStatus, getSprint, resolveSprintId } from '@src/store/sprint.ts';
5
- import { withFileLock } from '@src/utils/file-lock.ts';
6
- import { log } from '@src/theme/ui.ts';
7
-
8
- export interface LogProgressOptions {
9
- sprintId?: string;
10
- projectPath?: string;
11
- }
12
-
13
- export async function logProgress(message: string, options: LogProgressOptions = {}): Promise<void> {
14
- const id = await resolveSprintId(options.sprintId);
15
- const sprint = await getSprint(id);
16
-
17
- // Check sprint status - must be active to log progress
18
- assertSprintStatus(sprint, ['active'], 'log progress');
19
-
20
- const timestamp = new Date().toISOString();
21
- const projectMarker = options.projectPath ? `**Project:** ${options.projectPath}\n\n` : '';
22
- const entry = `## ${timestamp}\n\n${projectMarker}${message}\n\n---\n\n`;
23
- const progressPath = getProgressFilePath(id);
24
- await withFileLock(progressPath, async () => {
25
- await appendToFile(progressPath, entry);
26
- });
27
- }
28
-
29
- function isExecError(err: unknown): err is Error & { status: number } {
30
- return err instanceof Error && typeof (err as unknown as Record<string, unknown>)['status'] === 'number';
31
- }
32
-
33
- function isNodeError(err: unknown): err is Error & { code: string } {
34
- return err instanceof Error && typeof (err as unknown as Record<string, unknown>)['code'] === 'string';
35
- }
36
-
37
- /**
38
- * Get the current git commit hash and message for a path.
39
- */
40
- function getGitCommitInfo(projectPath: string): { hash: string; message: string } | null {
41
- try {
42
- assertSafeCwd(projectPath);
43
- // Single git command: "hash message"
44
- const output = execSync('git log -1 --pretty=format:%H\\ %s', {
45
- cwd: projectPath,
46
- encoding: 'utf-8',
47
- stdio: ['pipe', 'pipe', 'pipe'],
48
- }).trim();
49
- const spaceIndex = output.indexOf(' ');
50
- return {
51
- hash: output.slice(0, spaceIndex),
52
- message: output.slice(spaceIndex + 1),
53
- };
54
- } catch (err: unknown) {
55
- // Expected: not a git repo (exit code 128) — return null silently
56
- if (isExecError(err) && err.status === 128) {
57
- return null;
58
- }
59
- // Expected: git not installed (ENOENT) — return null silently
60
- if (isNodeError(err) && err.code === 'ENOENT') {
61
- return null;
62
- }
63
- // Unexpected: permission denied, corrupt repo, etc. — warn the user
64
- const detail = err instanceof Error ? err.message : String(err);
65
- log.warn(`Failed to get git info for ${projectPath}: ${detail}`);
66
- return null;
67
- }
68
- }
69
-
70
- export interface LogBaselinesOptions {
71
- sprintId: string;
72
- sprintName: string;
73
- projectPaths: string[];
74
- }
75
-
76
- /**
77
- * Log baseline git state for each project when a sprint is activated.
78
- * This enables "git log baseline..HEAD" style reviews of sprint changes.
79
- */
80
- export async function logBaselines(options: LogBaselinesOptions): Promise<void> {
81
- const { sprintId, sprintName, projectPaths } = options;
82
- const timestamp = new Date().toISOString();
83
-
84
- const lines: string[] = [
85
- `## ${timestamp}`,
86
- '',
87
- '### Sprint Baseline State',
88
- '',
89
- `Sprint: ${sprintName} (${sprintId})`,
90
- `Activated: ${timestamp}`,
91
- '',
92
- '#### Project Git State at Activation',
93
- '',
94
- ];
95
-
96
- // Get unique paths
97
- const uniquePaths = [...new Set(projectPaths)];
98
-
99
- for (const path of uniquePaths) {
100
- const commitInfo = getGitCommitInfo(path);
101
- if (commitInfo) {
102
- lines.push(`- **${path}**`);
103
- lines.push(` \`${commitInfo.hash} ${commitInfo.message}\``);
104
- } else {
105
- lines.push(`- **${path}**`);
106
- lines.push(` *(not a git repository or unable to retrieve state)*`);
107
- }
108
- }
109
-
110
- lines.push('');
111
- lines.push('---');
112
- lines.push('');
113
-
114
- await appendToFile(getProgressFilePath(sprintId), lines.join('\n'));
115
- }
116
-
117
- export async function getProgress(sprintId?: string): Promise<string> {
118
- const id = await resolveSprintId(sprintId);
119
- try {
120
- return await readTextFile(getProgressFilePath(id));
121
- } catch (err) {
122
- if (err instanceof FileNotFoundError) {
123
- return '';
124
- }
125
- throw err;
126
- }
127
- }
128
-
129
- /**
130
- * Parse progress entries and filter by project path.
131
- * Entries are delimited by `---` and may contain project markers in either format:
132
- * - Legacy HTML comments: `<!-- project: /path -->`
133
- * - Visible format: `**Project:** /path`
134
- */
135
- /**
136
- * Extract only "Learnings and Context" and "Notes for Next Tasks" sections
137
- * from progress entries, capped at maxEntries most recent.
138
- * Returns compressed summary suitable for task context files.
139
- */
140
- export function summarizeProgressForContext(progress: string, projectPath: string, maxEntries = 3): string {
141
- const filtered = filterProgressByProject(progress, projectPath);
142
- if (!filtered.trim()) {
143
- return '';
144
- }
145
-
146
- // Split into entries by --- delimiter
147
- const entries = filtered.split(/\n---\n/).filter((e) => e.trim());
148
-
149
- // Take last maxEntries entries
150
- const recent = entries.slice(-maxEntries);
151
-
152
- const summaries: string[] = [];
153
-
154
- for (const entry of recent) {
155
- // Extract entry header (first ## line with timestamp and task name)
156
- const headerMatch = /^##\s+(.+)$/m.exec(entry);
157
- const header = headerMatch?.[1] ?? 'Unknown entry';
158
-
159
- // Extract "Learnings and Context" section
160
- const learnings = extractSection(entry, 'Learnings and Context');
161
-
162
- // Extract "Notes for Next Tasks" section
163
- const notes = extractSection(entry, 'Notes for Next Tasks');
164
-
165
- // Only include entries that have at least one useful section
166
- if (learnings || notes) {
167
- const parts: string[] = [`**${header}**`];
168
- if (learnings) {
169
- parts.push(`**Learnings:** ${learnings}`);
170
- }
171
- if (notes) {
172
- parts.push(`**Notes for next tasks:** ${notes}`);
173
- }
174
- summaries.push(parts.join('\n'));
175
- }
176
- }
177
-
178
- if (summaries.length === 0) {
179
- return '';
180
- }
181
-
182
- return summaries.join('\n\n');
183
- }
184
-
185
- /**
186
- * Extract content of a markdown section (### heading) from a progress entry.
187
- * Returns the section content trimmed, or null if section not found.
188
- */
189
- function extractSection(entry: string, sectionName: string): string | null {
190
- // Match ### Section Name followed by content until next ### or end of string
191
- // No 'm' flag — $ must match end of string, not end of line
192
- const regex = new RegExp(`###\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\n([\\s\\S]*?)(?=###|$)`);
193
- const match = regex.exec(entry);
194
- if (!match?.[1]) return null;
195
-
196
- const content = match[1].trim();
197
- return content || null;
198
- }
199
-
200
- export function filterProgressByProject(progress: string, projectPath: string): string {
201
- if (!progress.trim()) {
202
- return '';
203
- }
204
-
205
- // Split by entry delimiter
206
- const entries = progress.split(/\n---\n/).filter((e) => e.trim());
207
-
208
- const filtered = entries.filter((entry) => {
209
- // Try visible format first: **Project:** /some/path
210
- const visibleMatch = /\*\*Project:\*\*\s*(.+?)(?:\n|$)/.exec(entry);
211
- if (visibleMatch?.[1]) {
212
- return visibleMatch[1].trim() === projectPath;
213
- }
214
-
215
- // Fall back to legacy HTML comment format: <!-- project: /some/path -->
216
- const htmlMatch = /<!--\s*project:\s*(.+?)\s*-->/.exec(entry);
217
- if (htmlMatch?.[1]) {
218
- return htmlMatch[1] === projectPath;
219
- }
220
-
221
- // No marker = include (baseline entries, general notes)
222
- return true;
223
- });
224
-
225
- if (filtered.length === 0) {
226
- return '';
227
- }
228
-
229
- return filtered.join('\n---\n') + '\n\n---\n\n';
230
- }
@@ -1,276 +0,0 @@
1
- import { basename, resolve } from 'node:path';
2
- import { getProjectsFilePath, validateProjectPath } from '../utils/paths.js';
3
- import { fileExists, readValidatedJson, writeValidatedJson } from '../utils/storage.js';
4
- import { type Project, type Projects, ProjectsSchema, type Repository } from '../schemas/index.js';
5
-
6
- export class ProjectNotFoundError extends Error {
7
- public readonly projectName: string;
8
-
9
- constructor(projectName: string) {
10
- super(`Project not found: ${projectName}`);
11
- this.name = 'ProjectNotFoundError';
12
- this.projectName = projectName;
13
- }
14
- }
15
-
16
- export class ProjectExistsError extends Error {
17
- public readonly projectName: string;
18
-
19
- constructor(projectName: string) {
20
- super(`Project already exists: ${projectName}`);
21
- this.name = 'ProjectExistsError';
22
- this.projectName = projectName;
23
- }
24
- }
25
-
26
- /**
27
- * Migration: Convert old paths[] format to repositories[] format.
28
- * Non-production tool - minimal migration support.
29
- */
30
- interface LegacyProject {
31
- name: string;
32
- displayName: string;
33
- paths?: string[];
34
- repositories?: Repository[];
35
- description?: string;
36
- }
37
-
38
- function migrateProjectIfNeeded(project: LegacyProject): Project {
39
- // Already in new format
40
- if (project.repositories) {
41
- return project as Project;
42
- }
43
-
44
- // Old paths[] format - convert to repositories[]
45
- if (project.paths) {
46
- return {
47
- name: project.name,
48
- displayName: project.displayName,
49
- repositories: project.paths.map((p) => ({
50
- name: basename(p),
51
- path: p,
52
- })),
53
- description: project.description,
54
- };
55
- }
56
-
57
- throw new Error(`Invalid project data: no paths or repositories for ${project.name}`);
58
- }
59
-
60
- /**
61
- * Get all projects.
62
- * Handles migration from old paths[] format to repositories[] format.
63
- */
64
- export async function listProjects(): Promise<Projects> {
65
- const filePath = getProjectsFilePath();
66
- if (!(await fileExists(filePath))) {
67
- return [];
68
- }
69
-
70
- // Read raw data to check for migration needs
71
- const { readFile } = await import('node:fs/promises');
72
- const content = await readFile(filePath, 'utf-8');
73
- const rawData = JSON.parse(content) as LegacyProject[];
74
-
75
- // Check if any projects need migration (old paths[] format)
76
- const needsMigration = rawData.some((p) => p.paths && !p.repositories);
77
-
78
- if (needsMigration) {
79
- const migrated = rawData.map(migrateProjectIfNeeded);
80
- const validated = ProjectsSchema.parse(migrated);
81
- await writeValidatedJson(filePath, validated, ProjectsSchema);
82
- return validated;
83
- }
84
-
85
- return readValidatedJson(filePath, ProjectsSchema);
86
- }
87
-
88
- /**
89
- * Get a project by name.
90
- * @throws ProjectNotFoundError if project doesn't exist
91
- */
92
- export async function getProject(name: string): Promise<Project> {
93
- const projects = await listProjects();
94
- const project = projects.find((p) => p.name === name);
95
- if (!project) {
96
- throw new ProjectNotFoundError(name);
97
- }
98
- return project;
99
- }
100
-
101
- /**
102
- * Check if a project exists.
103
- */
104
- export async function projectExists(name: string): Promise<boolean> {
105
- const projects = await listProjects();
106
- return projects.some((p) => p.name === name);
107
- }
108
-
109
- /**
110
- * Create a new project.
111
- * @throws ProjectExistsError if project already exists
112
- */
113
- export async function createProject(project: Project): Promise<Project> {
114
- const projects = await listProjects();
115
-
116
- if (projects.some((p) => p.name === project.name)) {
117
- throw new ProjectExistsError(project.name);
118
- }
119
-
120
- // Validate that all repository paths exist
121
- const pathErrors: string[] = [];
122
- for (const repo of project.repositories) {
123
- const resolved = resolve(repo.path);
124
- const validation = await validateProjectPath(resolved);
125
- if (validation !== true) {
126
- pathErrors.push(` ${repo.path}: ${validation}`);
127
- }
128
- }
129
- if (pathErrors.length > 0) {
130
- throw new Error(`Invalid project paths:\n${pathErrors.join('\n')}`);
131
- }
132
-
133
- // Resolve all paths to absolute and derive names, preserving scripts
134
- const normalizedProject: Project = {
135
- ...project,
136
- repositories: project.repositories.map((repo) => ({
137
- ...repo,
138
- name: repo.name || basename(repo.path),
139
- path: resolve(repo.path),
140
- })),
141
- };
142
-
143
- projects.push(normalizedProject);
144
- await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
145
-
146
- return normalizedProject;
147
- }
148
-
149
- /**
150
- * Update an existing project.
151
- * @throws ProjectNotFoundError if project doesn't exist
152
- */
153
- export async function updateProject(name: string, updates: Partial<Omit<Project, 'name'>>): Promise<Project> {
154
- const projects = await listProjects();
155
- const index = projects.findIndex((p) => p.name === name);
156
-
157
- if (index === -1) {
158
- throw new ProjectNotFoundError(name);
159
- }
160
-
161
- // Validate new repositories if provided
162
- if (updates.repositories) {
163
- const pathErrors: string[] = [];
164
- for (const repo of updates.repositories) {
165
- const resolved = resolve(repo.path);
166
- const validation = await validateProjectPath(resolved);
167
- if (validation !== true) {
168
- pathErrors.push(` ${repo.path}: ${validation}`);
169
- }
170
- }
171
- if (pathErrors.length > 0) {
172
- throw new Error(`Invalid project paths:\n${pathErrors.join('\n')}`);
173
- }
174
- // Resolve paths to absolute and ensure names, preserving scripts
175
- updates.repositories = updates.repositories.map((repo) => ({
176
- ...repo,
177
- name: repo.name || basename(repo.path),
178
- path: resolve(repo.path),
179
- }));
180
- }
181
-
182
- const existingProject = projects[index];
183
- if (!existingProject) {
184
- throw new ProjectNotFoundError(name);
185
- }
186
-
187
- const updatedProject: Project = {
188
- name: existingProject.name,
189
- displayName: updates.displayName ?? existingProject.displayName,
190
- repositories: updates.repositories ?? existingProject.repositories,
191
- description: updates.description ?? existingProject.description,
192
- };
193
-
194
- projects[index] = updatedProject;
195
- await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
196
-
197
- return updatedProject;
198
- }
199
-
200
- /**
201
- * Remove a project.
202
- * @throws ProjectNotFoundError if project doesn't exist
203
- */
204
- export async function removeProject(name: string): Promise<void> {
205
- const projects = await listProjects();
206
- const index = projects.findIndex((p) => p.name === name);
207
-
208
- if (index === -1) {
209
- throw new ProjectNotFoundError(name);
210
- }
211
-
212
- projects.splice(index, 1);
213
- await writeValidatedJson(getProjectsFilePath(), projects, ProjectsSchema);
214
- }
215
-
216
- /**
217
- * Get all repositories for a project.
218
- * @throws ProjectNotFoundError if project doesn't exist
219
- */
220
- export async function getProjectRepos(name: string): Promise<Repository[]> {
221
- const project = await getProject(name);
222
- return project.repositories;
223
- }
224
-
225
- /**
226
- * Add a repository to an existing project.
227
- * Accepts a full Repository object to preserve scripts set during interactive prompting.
228
- * @throws ProjectNotFoundError if project doesn't exist
229
- */
230
- export async function addProjectRepo(name: string, repo: Repository): Promise<Project> {
231
- const project = await getProject(name);
232
- const resolvedPath = resolve(repo.path);
233
-
234
- // Validate the path
235
- const validation = await validateProjectPath(resolvedPath);
236
- if (validation !== true) {
237
- throw new Error(`Invalid path ${repo.path}: ${validation}`);
238
- }
239
-
240
- // Check if path already exists
241
- if (project.repositories.some((r) => r.path === resolvedPath)) {
242
- return project; // Already exists, no-op
243
- }
244
-
245
- const normalizedRepo: Repository = {
246
- ...repo,
247
- name: repo.name || basename(resolvedPath),
248
- path: resolvedPath,
249
- };
250
-
251
- return updateProject(name, {
252
- repositories: [...project.repositories, normalizedRepo],
253
- });
254
- }
255
-
256
- /**
257
- * Remove a repository from an existing project.
258
- * @throws ProjectNotFoundError if project doesn't exist
259
- * @throws Error if trying to remove the last repository
260
- */
261
- export async function removeProjectRepo(name: string, path: string): Promise<Project> {
262
- const project = await getProject(name);
263
- const resolvedPath = resolve(path);
264
-
265
- const newRepos = project.repositories.filter((r) => r.path !== resolvedPath);
266
-
267
- if (newRepos.length === 0) {
268
- throw new Error('Cannot remove the last repository from a project');
269
- }
270
-
271
- if (newRepos.length === project.repositories.length) {
272
- return project; // Path wasn't in the list, no-op
273
- }
274
-
275
- return updateProject(name, { repositories: newRepos });
276
- }