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.
- package/README.md +58 -24
- package/dist/add-HGJCLWED.mjs +14 -0
- package/dist/add-MRGCS3US.mjs +14 -0
- package/dist/chunk-6PYTKGB5.mjs +316 -0
- package/dist/chunk-7TG3EAQ2.mjs +20 -0
- package/dist/chunk-EKMZZRWI.mjs +521 -0
- package/dist/chunk-JON4GCLR.mjs +59 -0
- package/dist/chunk-LOR7QBXX.mjs +3683 -0
- package/dist/chunk-MNMQC36F.mjs +556 -0
- package/dist/chunk-MRKOFVTM.mjs +537 -0
- package/dist/chunk-NTWO2LXB.mjs +52 -0
- package/dist/chunk-QBXHAXHI.mjs +562 -0
- package/dist/chunk-WGHJI3OI.mjs +214 -0
- package/dist/cli.mjs +4245 -0
- package/dist/create-MG7E7PLQ.mjs +10 -0
- package/dist/handle-UG5M2OON.mjs +22 -0
- package/dist/multiline-OHSNFCRG.mjs +40 -0
- package/dist/project-NT3L4FTB.mjs +28 -0
- package/dist/resolver-WSFWKACM.mjs +153 -0
- package/dist/sprint-4VHDLGFN.mjs +37 -0
- package/dist/wizard-LRELAN2J.mjs +196 -0
- package/package.json +19 -28
- package/CHANGELOG.md +0 -94
- package/bin/ralphctl +0 -13
- package/src/ai/executor.ts +0 -973
- package/src/ai/lifecycle.ts +0 -45
- package/src/ai/parser.ts +0 -40
- package/src/ai/permissions.ts +0 -207
- package/src/ai/process-manager.ts +0 -248
- package/src/ai/prompts/index.ts +0 -89
- package/src/ai/rate-limiter.ts +0 -89
- package/src/ai/runner.ts +0 -478
- package/src/ai/session.ts +0 -319
- package/src/ai/task-context.ts +0 -270
- package/src/cli-metadata.ts +0 -7
- package/src/cli.ts +0 -65
- package/src/commands/completion/index.ts +0 -33
- package/src/commands/config/config.ts +0 -58
- package/src/commands/config/index.ts +0 -33
- package/src/commands/dashboard/dashboard.ts +0 -5
- package/src/commands/dashboard/index.ts +0 -6
- package/src/commands/doctor/doctor.ts +0 -271
- package/src/commands/doctor/index.ts +0 -25
- package/src/commands/progress/index.ts +0 -25
- package/src/commands/progress/log.ts +0 -64
- package/src/commands/progress/show.ts +0 -14
- package/src/commands/project/add.ts +0 -336
- package/src/commands/project/index.ts +0 -104
- package/src/commands/project/list.ts +0 -31
- package/src/commands/project/remove.ts +0 -43
- package/src/commands/project/repo.ts +0 -118
- package/src/commands/project/show.ts +0 -49
- package/src/commands/sprint/close.ts +0 -180
- package/src/commands/sprint/context.ts +0 -109
- package/src/commands/sprint/create.ts +0 -60
- package/src/commands/sprint/current.ts +0 -75
- package/src/commands/sprint/delete.ts +0 -72
- package/src/commands/sprint/health.ts +0 -229
- package/src/commands/sprint/ideate.ts +0 -496
- package/src/commands/sprint/index.ts +0 -226
- package/src/commands/sprint/list.ts +0 -86
- package/src/commands/sprint/plan-utils.ts +0 -207
- package/src/commands/sprint/plan.ts +0 -549
- package/src/commands/sprint/refine.ts +0 -359
- package/src/commands/sprint/requirements.ts +0 -58
- package/src/commands/sprint/show.ts +0 -140
- package/src/commands/sprint/start.ts +0 -119
- package/src/commands/sprint/switch.ts +0 -20
- package/src/commands/task/add.ts +0 -316
- package/src/commands/task/import.ts +0 -150
- package/src/commands/task/index.ts +0 -123
- package/src/commands/task/list.ts +0 -145
- package/src/commands/task/next.ts +0 -45
- package/src/commands/task/remove.ts +0 -47
- package/src/commands/task/reorder.ts +0 -45
- package/src/commands/task/show.ts +0 -111
- package/src/commands/task/status.ts +0 -99
- package/src/commands/ticket/add.ts +0 -265
- package/src/commands/ticket/edit.ts +0 -166
- package/src/commands/ticket/index.ts +0 -114
- package/src/commands/ticket/list.ts +0 -128
- package/src/commands/ticket/refine-utils.ts +0 -89
- package/src/commands/ticket/refine.ts +0 -268
- package/src/commands/ticket/remove.ts +0 -48
- package/src/commands/ticket/show.ts +0 -74
- package/src/completion/handle.ts +0 -30
- package/src/completion/resolver.ts +0 -241
- package/src/interactive/dashboard.ts +0 -268
- package/src/interactive/escapable.ts +0 -81
- package/src/interactive/file-browser.ts +0 -153
- package/src/interactive/index.ts +0 -429
- package/src/interactive/menu.ts +0 -403
- package/src/interactive/selectors.ts +0 -273
- package/src/interactive/wizard.ts +0 -221
- package/src/providers/claude.ts +0 -53
- package/src/providers/copilot.ts +0 -86
- package/src/providers/index.ts +0 -43
- package/src/providers/types.ts +0 -85
- package/src/schemas/index.ts +0 -130
- package/src/store/config.ts +0 -74
- package/src/store/progress.ts +0 -230
- package/src/store/project.ts +0 -276
- package/src/store/sprint.ts +0 -229
- package/src/store/task.ts +0 -443
- package/src/store/ticket.ts +0 -178
- package/src/theme/index.ts +0 -215
- package/src/theme/ui.ts +0 -872
- package/src/utils/detect-scripts.ts +0 -247
- package/src/utils/editor-input.ts +0 -41
- package/src/utils/editor.ts +0 -37
- package/src/utils/exit-codes.ts +0 -27
- package/src/utils/file-lock.ts +0 -135
- package/src/utils/git.ts +0 -185
- package/src/utils/ids.ts +0 -37
- package/src/utils/issue-fetch.ts +0 -244
- package/src/utils/json-extract.ts +0 -62
- package/src/utils/multiline.ts +0 -61
- package/src/utils/path-selector.ts +0 -236
- package/src/utils/paths.ts +0 -108
- package/src/utils/provider.ts +0 -34
- package/src/utils/requirements-export.ts +0 -63
- package/src/utils/storage.ts +0 -107
- package/tsconfig.json +0 -25
- /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
- /package/{src/ai → dist}/prompts/ideate.md +0 -0
- /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
- /package/{src/ai → dist}/prompts/plan-common.md +0 -0
- /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
- /package/{src/ai → dist}/prompts/task-execution.md +0 -0
- /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
package/src/utils/ids.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Generate an 8-character UUID-like ID.
|
|
5
|
-
* Used for tickets and tasks.
|
|
6
|
-
*/
|
|
7
|
-
export function generateUuid8(): string {
|
|
8
|
-
return randomBytes(4).toString('hex');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Sanitize a string into a URL/filesystem-safe slug.
|
|
13
|
-
* Lowercase, alphanumeric + hyphens only, max 40 characters.
|
|
14
|
-
*/
|
|
15
|
-
export function slugify(input: string, maxLength = 40): string {
|
|
16
|
-
return input
|
|
17
|
-
.toLowerCase()
|
|
18
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
19
|
-
.replace(/^-+|-+$/g, '')
|
|
20
|
-
.replace(/-{2,}/g, '-')
|
|
21
|
-
.slice(0, maxLength)
|
|
22
|
-
.replace(/-$/, ''); // Remove trailing hyphen if truncation created one
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Generate a sprint ID in the format: YYYYMMDD-HHmmss-<slug>
|
|
27
|
-
* Lexicographically sortable by creation time.
|
|
28
|
-
*/
|
|
29
|
-
export function generateSprintId(name?: string): string {
|
|
30
|
-
const now = new Date();
|
|
31
|
-
// Format: YYYYMMDD-HHmmss (remove non-digits from ISO string parts)
|
|
32
|
-
const date = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
33
|
-
const time = now.toISOString().slice(11, 19).replace(/:/g, '');
|
|
34
|
-
const slug = name ? slugify(name) : generateUuid8();
|
|
35
|
-
|
|
36
|
-
return `${date}-${time}-${slug || generateUuid8()}`;
|
|
37
|
-
}
|
package/src/utils/issue-fetch.ts
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
const MAX_COMMENTS = 20;
|
|
4
|
-
|
|
5
|
-
export interface IssueComment {
|
|
6
|
-
author: string;
|
|
7
|
-
createdAt: string;
|
|
8
|
-
body: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface IssueData {
|
|
12
|
-
title: string;
|
|
13
|
-
body: string;
|
|
14
|
-
comments: IssueComment[];
|
|
15
|
-
url: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ParsedIssueUrl {
|
|
19
|
-
host: 'github' | 'gitlab';
|
|
20
|
-
hostname: string;
|
|
21
|
-
owner: string;
|
|
22
|
-
repo: string;
|
|
23
|
-
number: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Parse a GitHub or GitLab issue URL into its components.
|
|
28
|
-
* Returns null if the URL is not a recognized issue URL.
|
|
29
|
-
*
|
|
30
|
-
* GitHub: https://github.com/owner/repo/issues/123
|
|
31
|
-
* GitLab: https://gitlab.example.com/group/project/-/issues/456
|
|
32
|
-
* (any hostname with /-/issues/ path segment)
|
|
33
|
-
*/
|
|
34
|
-
export function parseIssueUrl(url: string): ParsedIssueUrl | null {
|
|
35
|
-
let parsed: URL;
|
|
36
|
-
try {
|
|
37
|
-
parsed = new URL(url);
|
|
38
|
-
} catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
47
|
-
|
|
48
|
-
// GitHub: /owner/repo/issues/123
|
|
49
|
-
if (parsed.hostname === 'github.com') {
|
|
50
|
-
const owner = segments[0];
|
|
51
|
-
const repo = segments[1];
|
|
52
|
-
if (segments.length >= 4 && segments[2] === 'issues' && owner && repo) {
|
|
53
|
-
const num = Number(segments[3]);
|
|
54
|
-
if (Number.isInteger(num) && num > 0) {
|
|
55
|
-
return { host: 'github', hostname: parsed.hostname, owner, repo, number: num };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// GitLab (any hostname): /.../group/project/-/issues/456
|
|
62
|
-
const dashIdx = segments.indexOf('-');
|
|
63
|
-
if (dashIdx >= 2 && segments[dashIdx + 1] === 'issues') {
|
|
64
|
-
const num = Number(segments[dashIdx + 2]);
|
|
65
|
-
if (Number.isInteger(num) && num > 0) {
|
|
66
|
-
const repo = segments[dashIdx - 1];
|
|
67
|
-
if (repo) {
|
|
68
|
-
const owner = segments.slice(0, dashIdx - 1).join('/');
|
|
69
|
-
return { host: 'gitlab', hostname: parsed.hostname, owner, repo, number: num };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
interface GhComment {
|
|
78
|
-
author?: { login?: string };
|
|
79
|
-
body?: string;
|
|
80
|
-
createdAt?: string;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface GhIssueResponse {
|
|
84
|
-
title?: string;
|
|
85
|
-
body?: string;
|
|
86
|
-
comments?: GhComment[];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
interface GlabNote {
|
|
90
|
-
author?: { username?: string };
|
|
91
|
-
body?: string;
|
|
92
|
-
created_at?: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
interface GlabIssueResponse {
|
|
96
|
-
title?: string;
|
|
97
|
-
description?: string;
|
|
98
|
-
notes?: GlabNote[];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function fetchGitHubIssue(parsed: ParsedIssueUrl): IssueData {
|
|
102
|
-
const result = spawnSync(
|
|
103
|
-
'gh',
|
|
104
|
-
[
|
|
105
|
-
'issue',
|
|
106
|
-
'view',
|
|
107
|
-
String(parsed.number),
|
|
108
|
-
'--repo',
|
|
109
|
-
`${parsed.owner}/${parsed.repo}`,
|
|
110
|
-
'--json',
|
|
111
|
-
'title,body,comments',
|
|
112
|
-
],
|
|
113
|
-
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 30_000 }
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
if (result.status !== 0) {
|
|
117
|
-
const stderr = result.stderr.trim();
|
|
118
|
-
throw new IssueFetchError(`gh issue view failed: ${stderr || 'unknown error'}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const data = JSON.parse(result.stdout) as GhIssueResponse;
|
|
122
|
-
|
|
123
|
-
const comments: IssueComment[] = (data.comments ?? []).slice(-MAX_COMMENTS).map((c) => ({
|
|
124
|
-
author: c.author?.login ?? 'unknown',
|
|
125
|
-
createdAt: c.createdAt ?? '',
|
|
126
|
-
body: c.body ?? '',
|
|
127
|
-
}));
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
title: data.title ?? '',
|
|
131
|
-
body: data.body ?? '',
|
|
132
|
-
comments,
|
|
133
|
-
url: `https://${parsed.hostname}/${parsed.owner}/${parsed.repo}/issues/${String(parsed.number)}`,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function fetchGitLabIssue(parsed: ParsedIssueUrl): IssueData {
|
|
138
|
-
// Fetch issue details
|
|
139
|
-
const result = spawnSync(
|
|
140
|
-
'glab',
|
|
141
|
-
['issue', 'view', String(parsed.number), '--repo', `${parsed.owner}/${parsed.repo}`, '--output', 'json'],
|
|
142
|
-
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 30_000 }
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
if (result.status !== 0) {
|
|
146
|
-
const stderr = result.stderr.trim();
|
|
147
|
-
throw new IssueFetchError(`glab issue view failed: ${stderr || 'unknown error'}`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const data = JSON.parse(result.stdout) as GlabIssueResponse;
|
|
151
|
-
|
|
152
|
-
// Fetch issue notes (comments) separately
|
|
153
|
-
const notesResult = spawnSync(
|
|
154
|
-
'glab',
|
|
155
|
-
['issue', 'note', 'list', String(parsed.number), '--repo', `${parsed.owner}/${parsed.repo}`, '--output', 'json'],
|
|
156
|
-
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 30_000 }
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
let comments: IssueComment[] = [];
|
|
160
|
-
if (notesResult.status === 0 && notesResult.stdout.trim()) {
|
|
161
|
-
try {
|
|
162
|
-
const notes = JSON.parse(notesResult.stdout) as GlabNote[];
|
|
163
|
-
comments = notes.slice(-MAX_COMMENTS).map((n) => ({
|
|
164
|
-
author: n.author?.username ?? 'unknown',
|
|
165
|
-
createdAt: n.created_at ?? '',
|
|
166
|
-
body: n.body ?? '',
|
|
167
|
-
}));
|
|
168
|
-
} catch {
|
|
169
|
-
// Non-fatal — continue without comments
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
title: data.title ?? '',
|
|
175
|
-
body: data.description ?? '',
|
|
176
|
-
comments,
|
|
177
|
-
url: `https://${parsed.hostname}/${parsed.owner}/${parsed.repo}/-/issues/${String(parsed.number)}`,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Fetch issue data from GitHub or GitLab using CLI tools.
|
|
183
|
-
* Throws IssueFetchError on failure.
|
|
184
|
-
*/
|
|
185
|
-
export function fetchIssue(parsed: ParsedIssueUrl): IssueData {
|
|
186
|
-
if (parsed.host === 'github') {
|
|
187
|
-
return fetchGitHubIssue(parsed);
|
|
188
|
-
}
|
|
189
|
-
return fetchGitLabIssue(parsed);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Fetch issue data from a URL string. Convenience wrapper around parseIssueUrl + fetchIssue.
|
|
194
|
-
* Returns null if the URL is not a recognized issue URL.
|
|
195
|
-
* Throws IssueFetchError on fetch failure.
|
|
196
|
-
*/
|
|
197
|
-
export function fetchIssueFromUrl(url: string): IssueData | null {
|
|
198
|
-
const parsed = parseIssueUrl(url);
|
|
199
|
-
if (!parsed) return null;
|
|
200
|
-
return fetchIssue(parsed);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Format issue data as markdown context for AI prompts.
|
|
205
|
-
*/
|
|
206
|
-
export function formatIssueContext(data: IssueData): string {
|
|
207
|
-
const lines: string[] = [];
|
|
208
|
-
|
|
209
|
-
lines.push('## Source Issue Data');
|
|
210
|
-
lines.push('');
|
|
211
|
-
lines.push(`> Fetched live from ${data.url}`);
|
|
212
|
-
lines.push('');
|
|
213
|
-
lines.push(`**Title:** ${data.title}`);
|
|
214
|
-
lines.push('');
|
|
215
|
-
|
|
216
|
-
if (data.body) {
|
|
217
|
-
lines.push('**Body:**');
|
|
218
|
-
lines.push('');
|
|
219
|
-
lines.push(data.body);
|
|
220
|
-
lines.push('');
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
if (data.comments.length > 0) {
|
|
224
|
-
lines.push(`**Comments (${String(data.comments.length)}):**`);
|
|
225
|
-
lines.push('');
|
|
226
|
-
for (const comment of data.comments) {
|
|
227
|
-
const timestamp = comment.createdAt ? ` (${comment.createdAt})` : '';
|
|
228
|
-
lines.push(`---`);
|
|
229
|
-
lines.push(`**@${comment.author}**${timestamp}:`);
|
|
230
|
-
lines.push('');
|
|
231
|
-
lines.push(comment.body);
|
|
232
|
-
lines.push('');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return lines.join('\n');
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export class IssueFetchError extends Error {
|
|
240
|
-
constructor(message: string) {
|
|
241
|
-
super(message);
|
|
242
|
-
this.name = 'IssueFetchError';
|
|
243
|
-
}
|
|
244
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extract a complete JSON structure (array or object) from text that may contain surrounding content.
|
|
3
|
-
* Uses depth tracking to handle nested structures and strings containing brackets/braces.
|
|
4
|
-
*/
|
|
5
|
-
function extractJsonStructure(output: string, open: string, close: string, typeName: string): string {
|
|
6
|
-
const start = output.indexOf(open);
|
|
7
|
-
if (start === -1) {
|
|
8
|
-
throw new Error(`No JSON ${typeName} found in output`);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
let depth = 0;
|
|
12
|
-
let inString = false;
|
|
13
|
-
let escape = false;
|
|
14
|
-
for (let i = start; i < output.length; i++) {
|
|
15
|
-
const ch = output[i];
|
|
16
|
-
if (escape) {
|
|
17
|
-
escape = false;
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
if (ch === '\\' && inString) {
|
|
21
|
-
escape = true;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
if (ch === '"') {
|
|
25
|
-
inString = !inString;
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (inString) continue;
|
|
29
|
-
if (ch === open) depth++;
|
|
30
|
-
if (ch === close) {
|
|
31
|
-
depth--;
|
|
32
|
-
if (depth === 0) {
|
|
33
|
-
return output.slice(start, i + 1);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
throw new Error(`No complete JSON ${typeName} found in output`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Extract a complete JSON array from text that may contain surrounding content.
|
|
42
|
-
* Uses bracket-depth tracking to handle nested arrays and strings containing brackets.
|
|
43
|
-
*
|
|
44
|
-
* @param output - The text containing a JSON array
|
|
45
|
-
* @returns The extracted JSON array string
|
|
46
|
-
* @throws Error if no complete JSON array is found
|
|
47
|
-
*/
|
|
48
|
-
export function extractJsonArray(output: string): string {
|
|
49
|
-
return extractJsonStructure(output, '[', ']', 'array');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Extract a complete JSON object from text that may contain surrounding content.
|
|
54
|
-
* Uses brace-depth tracking to handle nested objects and strings containing braces.
|
|
55
|
-
*
|
|
56
|
-
* @param output - The text containing a JSON object
|
|
57
|
-
* @returns The extracted JSON object string
|
|
58
|
-
* @throws Error if no complete JSON object is found
|
|
59
|
-
*/
|
|
60
|
-
export function extractJsonObject(output: string): string {
|
|
61
|
-
return extractJsonStructure(output, '{', '}', 'object');
|
|
62
|
-
}
|
package/src/utils/multiline.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import * as readline from 'node:readline';
|
|
2
|
-
import { muted } from '@src/theme/index.ts';
|
|
3
|
-
import { icons } from '@src/theme/ui.ts';
|
|
4
|
-
|
|
5
|
-
export interface MultilineInputOptions {
|
|
6
|
-
/** Message/prompt to display */
|
|
7
|
-
message: string;
|
|
8
|
-
/** Default value (will be shown as initial lines) */
|
|
9
|
-
default?: string;
|
|
10
|
-
/** Hint text shown after the message */
|
|
11
|
-
hint?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Prompt for multiline input with paste support.
|
|
16
|
-
* Ctrl+D to finish (standard Unix EOF).
|
|
17
|
-
* Supports pasting multiline text including blank lines.
|
|
18
|
-
*
|
|
19
|
-
* @returns Joined lines as a single string
|
|
20
|
-
*/
|
|
21
|
-
export async function multilineInput(options: MultilineInputOptions): Promise<string> {
|
|
22
|
-
const { message, default: defaultValue, hint = 'Ctrl+D to finish' } = options;
|
|
23
|
-
|
|
24
|
-
// Show the prompt with hint
|
|
25
|
-
console.log(`${icons.edit} ${message} ${muted(`(${hint})`)}`);
|
|
26
|
-
|
|
27
|
-
// If there's a default value, show it
|
|
28
|
-
if (defaultValue) {
|
|
29
|
-
console.log(muted(' Default:'));
|
|
30
|
-
for (const line of defaultValue.split('\n')) {
|
|
31
|
-
console.log(muted(` ${line}`));
|
|
32
|
-
}
|
|
33
|
-
console.log('');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const lines: string[] = [];
|
|
37
|
-
|
|
38
|
-
const rl = readline.createInterface({
|
|
39
|
-
input: process.stdin,
|
|
40
|
-
output: process.stdout,
|
|
41
|
-
terminal: process.stdin.isTTY,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
return new Promise<string>((resolve) => {
|
|
45
|
-
rl.on('line', (line) => {
|
|
46
|
-
lines.push(line);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
rl.on('close', () => {
|
|
50
|
-
// Print newline after Ctrl+D for clean formatting
|
|
51
|
-
console.log('');
|
|
52
|
-
|
|
53
|
-
// Trim trailing empty lines
|
|
54
|
-
while (lines.length > 0 && lines.at(-1)?.trim() === '') {
|
|
55
|
-
lines.pop();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
resolve(lines.join('\n'));
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
}
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
|
-
import { basename } from 'node:path';
|
|
3
|
-
import type { Project, Repository } from '@src/schemas/index.ts';
|
|
4
|
-
import { assertSafeCwd } from '@src/utils/paths.ts';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Result of path selection for a multi-repo project.
|
|
8
|
-
*/
|
|
9
|
-
export interface PathSelectionResult {
|
|
10
|
-
/** Primary working path (where most work will happen) */
|
|
11
|
-
primary: string;
|
|
12
|
-
/** Additional paths to include for context */
|
|
13
|
-
additional: string[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Common keywords that map to path patterns.
|
|
18
|
-
* Used for simple heuristic matching.
|
|
19
|
-
*/
|
|
20
|
-
const PATH_KEYWORDS: Record<string, string[]> = {
|
|
21
|
-
frontend: ['frontend', 'client', 'web', 'ui', 'app'],
|
|
22
|
-
backend: ['backend', 'server', 'api', 'service'],
|
|
23
|
-
mobile: ['mobile', 'ios', 'android', 'app'],
|
|
24
|
-
shared: ['shared', 'common', 'lib', 'core', 'utils'],
|
|
25
|
-
docs: ['docs', 'documentation'],
|
|
26
|
-
infra: ['infra', 'infrastructure', 'deploy', 'k8s', 'terraform'],
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Extract keywords from text (title, description).
|
|
31
|
-
*/
|
|
32
|
-
function extractKeywords(text: string): Set<string> {
|
|
33
|
-
// Normalize and split into words
|
|
34
|
-
const words = text
|
|
35
|
-
.toLowerCase()
|
|
36
|
-
.replace(/[^a-z0-9\s]/g, ' ')
|
|
37
|
-
.split(/\s+/)
|
|
38
|
-
.filter((w) => w.length > 2);
|
|
39
|
-
|
|
40
|
-
return new Set(words);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Score a repository based on keyword matches.
|
|
45
|
-
*/
|
|
46
|
-
function scoreRepo(repo: Repository, keywords: Set<string>): number {
|
|
47
|
-
const repoName = repo.name.toLowerCase();
|
|
48
|
-
const pathName = basename(repo.path).toLowerCase();
|
|
49
|
-
let score = 0;
|
|
50
|
-
|
|
51
|
-
// Check direct keyword matches in repo name or path basename
|
|
52
|
-
for (const keyword of keywords) {
|
|
53
|
-
if (repoName.includes(keyword) || pathName.includes(keyword)) {
|
|
54
|
-
score += 10;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Check category keywords
|
|
59
|
-
for (const [category, categoryKeywords] of Object.entries(PATH_KEYWORDS)) {
|
|
60
|
-
// If text mentions a category
|
|
61
|
-
if (keywords.has(category)) {
|
|
62
|
-
for (const catKeyword of categoryKeywords) {
|
|
63
|
-
if (repoName.includes(catKeyword) || pathName.includes(catKeyword)) {
|
|
64
|
-
score += 5;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// If text mentions specific keywords from a category
|
|
70
|
-
for (const catKeyword of categoryKeywords) {
|
|
71
|
-
if (keywords.has(catKeyword) && (repoName.includes(catKeyword) || pathName.includes(catKeyword))) {
|
|
72
|
-
score += 8;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return score;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Get recently modified paths from git history.
|
|
82
|
-
* Returns paths that were modified in the last N commits.
|
|
83
|
-
*/
|
|
84
|
-
function getRecentlyModifiedPaths(repos: Repository[], commits = 50): Map<string, number> {
|
|
85
|
-
const pathCounts = new Map<string, number>();
|
|
86
|
-
|
|
87
|
-
for (const repo of repos) {
|
|
88
|
-
try {
|
|
89
|
-
assertSafeCwd(repo.path);
|
|
90
|
-
// Get list of modified files from git log
|
|
91
|
-
const result = execSync(`git log -${String(commits)} --name-only --pretty=format:`, {
|
|
92
|
-
cwd: repo.path,
|
|
93
|
-
encoding: 'utf-8',
|
|
94
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Count modifications per path
|
|
98
|
-
const currentCount = pathCounts.get(repo.path) ?? 0;
|
|
99
|
-
const files = result.split('\n').filter((f) => f.trim());
|
|
100
|
-
pathCounts.set(repo.path, currentCount + files.length);
|
|
101
|
-
} catch {
|
|
102
|
-
// Ignore git errors (path might not be a git repo)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return pathCounts;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Find paths that are commonly co-modified.
|
|
111
|
-
* If pathA is often changed with pathB, they might be related.
|
|
112
|
-
*/
|
|
113
|
-
function getCoModifiedPaths(primaryPath: string, allRepos: Repository[]): string[] {
|
|
114
|
-
const coModified: string[] = [];
|
|
115
|
-
|
|
116
|
-
// This is a simplified version - just return paths that share recent commits
|
|
117
|
-
// A full implementation would analyze actual co-commit patterns
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
assertSafeCwd(primaryPath);
|
|
121
|
-
// Get commit hashes for primary path
|
|
122
|
-
const primaryCommits = execSync('git log -20 --pretty=format:%H', {
|
|
123
|
-
cwd: primaryPath,
|
|
124
|
-
encoding: 'utf-8',
|
|
125
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
126
|
-
})
|
|
127
|
-
.split('\n')
|
|
128
|
-
.filter((h) => h.trim());
|
|
129
|
-
|
|
130
|
-
const primarySet = new Set(primaryCommits);
|
|
131
|
-
|
|
132
|
-
// Check each other path for overlapping commits
|
|
133
|
-
for (const repo of allRepos) {
|
|
134
|
-
if (repo.path === primaryPath) continue;
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
assertSafeCwd(repo.path);
|
|
138
|
-
const otherCommits = execSync('git log -20 --pretty=format:%H', {
|
|
139
|
-
cwd: repo.path,
|
|
140
|
-
encoding: 'utf-8',
|
|
141
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
142
|
-
})
|
|
143
|
-
.split('\n')
|
|
144
|
-
.filter((h) => h.trim());
|
|
145
|
-
|
|
146
|
-
// Count overlapping commits
|
|
147
|
-
const overlap = otherCommits.filter((h) => primarySet.has(h)).length;
|
|
148
|
-
if (overlap > 3) {
|
|
149
|
-
coModified.push(repo.path);
|
|
150
|
-
}
|
|
151
|
-
} catch {
|
|
152
|
-
// Ignore errors
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
} catch {
|
|
156
|
-
// Ignore errors
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return coModified;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Select relevant paths for a ticket based on context.
|
|
164
|
-
*
|
|
165
|
-
* @param project - Project with multiple repositories
|
|
166
|
-
* @param context - Ticket context (title, description)
|
|
167
|
-
* @returns Selected paths (primary + additional for context)
|
|
168
|
-
*/
|
|
169
|
-
export function selectRelevantPaths(
|
|
170
|
-
project: Project,
|
|
171
|
-
context: { ticketTitle: string; ticketDescription?: string }
|
|
172
|
-
): PathSelectionResult {
|
|
173
|
-
const repos = project.repositories;
|
|
174
|
-
|
|
175
|
-
// Single repo project - simple case
|
|
176
|
-
if (repos.length <= 1) {
|
|
177
|
-
return {
|
|
178
|
-
primary: repos[0]?.path ?? '',
|
|
179
|
-
additional: [],
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Extract keywords from ticket
|
|
184
|
-
const text = `${context.ticketTitle} ${context.ticketDescription ?? ''}`;
|
|
185
|
-
const keywords = extractKeywords(text);
|
|
186
|
-
|
|
187
|
-
// Score each repo based on keywords
|
|
188
|
-
const scores = repos.map((repo) => ({
|
|
189
|
-
repo,
|
|
190
|
-
score: scoreRepo(repo, keywords),
|
|
191
|
-
}));
|
|
192
|
-
|
|
193
|
-
// Sort by score (highest first)
|
|
194
|
-
scores.sort((a, b) => b.score - a.score);
|
|
195
|
-
|
|
196
|
-
// If we have clear winners (score > 0), use those
|
|
197
|
-
const scoredRepos = scores.filter((s) => s.score > 0);
|
|
198
|
-
|
|
199
|
-
if (scoredRepos.length > 0) {
|
|
200
|
-
const primary = scoredRepos[0]?.repo.path ?? repos[0]?.path ?? '';
|
|
201
|
-
const additional = scoredRepos.slice(1).map((s) => s.repo.path);
|
|
202
|
-
return { primary, additional };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// No keyword matches - use activity-based heuristic
|
|
206
|
-
const activityScores = getRecentlyModifiedPaths(repos);
|
|
207
|
-
|
|
208
|
-
// Sort by activity
|
|
209
|
-
const byActivity = repos
|
|
210
|
-
.map((repo) => ({
|
|
211
|
-
repo,
|
|
212
|
-
activity: activityScores.get(repo.path) ?? 0,
|
|
213
|
-
}))
|
|
214
|
-
.sort((a, b) => b.activity - a.activity);
|
|
215
|
-
|
|
216
|
-
const primary = byActivity[0]?.repo.path ?? repos[0]?.path ?? '';
|
|
217
|
-
|
|
218
|
-
// Get co-modified paths for context
|
|
219
|
-
const coModified = getCoModifiedPaths(primary, repos);
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
primary,
|
|
223
|
-
additional: coModified,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Select all paths (when user explicitly requests it).
|
|
229
|
-
*/
|
|
230
|
-
export function selectAllPaths(project: Project): PathSelectionResult {
|
|
231
|
-
const [primary, ...additional] = project.repositories;
|
|
232
|
-
return {
|
|
233
|
-
primary: primary?.path ?? '',
|
|
234
|
-
additional: additional.map((r) => r.path),
|
|
235
|
-
};
|
|
236
|
-
}
|