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/commands/task/add.ts
DELETED
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'node:path';
|
|
2
|
-
import { confirm, input } from '@inquirer/prompts';
|
|
3
|
-
import { error, muted } from '@src/theme/index.ts';
|
|
4
|
-
import { emoji, field, icons, log, showError, showNextSteps, showSuccess } from '@src/theme/ui.ts';
|
|
5
|
-
import { editorInput } from '@src/utils/editor-input.ts';
|
|
6
|
-
import { validateProjectPath } from '@src/utils/paths.ts';
|
|
7
|
-
import { addTask } from '@src/store/task.ts';
|
|
8
|
-
import { formatTicketDisplay, getTicket, listTickets } from '@src/store/ticket.ts';
|
|
9
|
-
import { getProject, listProjects } from '@src/store/project.ts';
|
|
10
|
-
import {
|
|
11
|
-
assertSprintStatus,
|
|
12
|
-
getSprint,
|
|
13
|
-
NoCurrentSprintError,
|
|
14
|
-
resolveSprintId,
|
|
15
|
-
SprintStatusError,
|
|
16
|
-
} from '@src/store/sprint.ts';
|
|
17
|
-
import { EXIT_ERROR, exitWithCode } from '@src/utils/exit-codes.ts';
|
|
18
|
-
import { selectProjectRepository } from '@src/interactive/selectors.ts';
|
|
19
|
-
|
|
20
|
-
export interface TaskAddOptions {
|
|
21
|
-
name?: string;
|
|
22
|
-
description?: string;
|
|
23
|
-
steps?: string[];
|
|
24
|
-
ticket?: string;
|
|
25
|
-
project?: string;
|
|
26
|
-
interactive?: boolean; // Set by REPL or CLI (default true unless --no-interactive)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function taskAddCommand(options: TaskAddOptions = {}): Promise<void> {
|
|
30
|
-
const isInteractive = options.interactive !== false;
|
|
31
|
-
|
|
32
|
-
// FAIL FAST: Check sprint status before collecting any input
|
|
33
|
-
try {
|
|
34
|
-
const sprintId = await resolveSprintId();
|
|
35
|
-
const sprint = await getSprint(sprintId);
|
|
36
|
-
assertSprintStatus(sprint, ['draft'], 'add tasks');
|
|
37
|
-
} catch (err) {
|
|
38
|
-
if (err instanceof SprintStatusError) {
|
|
39
|
-
// Show only the main error, not the built-in hint
|
|
40
|
-
const mainError = err.message.split('\n')[0] ?? err.message;
|
|
41
|
-
showError(mainError);
|
|
42
|
-
showNextSteps([
|
|
43
|
-
['ralphctl sprint close', 'close current sprint'],
|
|
44
|
-
['ralphctl sprint create', 'start a new draft sprint'],
|
|
45
|
-
]);
|
|
46
|
-
log.newline();
|
|
47
|
-
if (!isInteractive) exitWithCode(EXIT_ERROR);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (err instanceof NoCurrentSprintError) {
|
|
51
|
-
showError('No current sprint set.');
|
|
52
|
-
showNextSteps([['ralphctl sprint create', 'create a new sprint']]);
|
|
53
|
-
log.newline();
|
|
54
|
-
if (!isInteractive) exitWithCode(EXIT_ERROR);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
throw err;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let name: string;
|
|
61
|
-
let description: string | undefined;
|
|
62
|
-
let steps: string[];
|
|
63
|
-
let ticketId: string | undefined;
|
|
64
|
-
let projectPath: string | undefined;
|
|
65
|
-
|
|
66
|
-
if (options.interactive === false) {
|
|
67
|
-
// Non-interactive mode: validate required params
|
|
68
|
-
const errors: string[] = [];
|
|
69
|
-
const trimmedName = options.name?.trim();
|
|
70
|
-
const trimmedProject = options.project?.trim();
|
|
71
|
-
|
|
72
|
-
if (!trimmedName) {
|
|
73
|
-
errors.push('--name is required');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Project is required unless we can get it from a ticket
|
|
77
|
-
if (!trimmedProject && !options.ticket) {
|
|
78
|
-
errors.push('--project is required (or --ticket to inherit from ticket)');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (errors.length > 0 || !trimmedName) {
|
|
82
|
-
showError('Validation failed');
|
|
83
|
-
for (const e of errors) {
|
|
84
|
-
log.item(error(e));
|
|
85
|
-
}
|
|
86
|
-
log.newline();
|
|
87
|
-
exitWithCode(EXIT_ERROR);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
name = trimmedName;
|
|
91
|
-
const trimmedDesc = options.description?.trim();
|
|
92
|
-
description = trimmedDesc === '' ? undefined : trimmedDesc;
|
|
93
|
-
steps = options.steps ?? [];
|
|
94
|
-
const trimmedTicket = options.ticket?.trim();
|
|
95
|
-
ticketId = trimmedTicket === '' ? undefined : trimmedTicket;
|
|
96
|
-
|
|
97
|
-
// Get project path from ticket or option
|
|
98
|
-
if (ticketId) {
|
|
99
|
-
try {
|
|
100
|
-
const ticket = await getTicket(ticketId);
|
|
101
|
-
// Get first repository path from project
|
|
102
|
-
const project = await getProject(ticket.projectName);
|
|
103
|
-
projectPath = project.repositories[0]?.path;
|
|
104
|
-
} catch {
|
|
105
|
-
if (!trimmedProject) {
|
|
106
|
-
showError(`Ticket not found: ${ticketId}`);
|
|
107
|
-
console.log(muted(' Provide --project or a valid --ticket\n'));
|
|
108
|
-
exitWithCode(EXIT_ERROR);
|
|
109
|
-
}
|
|
110
|
-
const validation = await validateProjectPath(trimmedProject);
|
|
111
|
-
if (validation !== true) {
|
|
112
|
-
showError(`Invalid project path: ${validation}`);
|
|
113
|
-
exitWithCode(EXIT_ERROR);
|
|
114
|
-
}
|
|
115
|
-
projectPath = resolve(trimmedProject);
|
|
116
|
-
}
|
|
117
|
-
} else if (trimmedProject) {
|
|
118
|
-
const validation = await validateProjectPath(trimmedProject);
|
|
119
|
-
if (validation !== true) {
|
|
120
|
-
showError(`Invalid project path: ${validation}`);
|
|
121
|
-
exitWithCode(EXIT_ERROR);
|
|
122
|
-
}
|
|
123
|
-
projectPath = resolve(trimmedProject);
|
|
124
|
-
} else {
|
|
125
|
-
// This shouldn't happen due to earlier validation
|
|
126
|
-
showError('--project is required');
|
|
127
|
-
exitWithCode(EXIT_ERROR);
|
|
128
|
-
}
|
|
129
|
-
} else {
|
|
130
|
-
// Interactive mode (default): prompt for missing params, use provided values as defaults
|
|
131
|
-
name = await input({
|
|
132
|
-
message: `${icons.task} Task name:`,
|
|
133
|
-
default: options.name?.trim(),
|
|
134
|
-
validate: (v) => (v.trim().length > 0 ? true : 'Name is required'),
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
description = await editorInput({
|
|
138
|
-
message: 'Description (optional):',
|
|
139
|
-
default: options.description?.trim(),
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// Add steps one by one
|
|
143
|
-
steps = options.steps ? [...options.steps] : [];
|
|
144
|
-
const addSteps = await confirm({
|
|
145
|
-
message: `${emoji.donut} ${steps.length > 0 ? `Add more steps? (${String(steps.length)} pre-filled)` : 'Add implementation steps?'}`,
|
|
146
|
-
default: steps.length === 0,
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
if (addSteps) {
|
|
150
|
-
let stepNum = steps.length + 1;
|
|
151
|
-
let adding = true;
|
|
152
|
-
while (adding) {
|
|
153
|
-
const step = await input({
|
|
154
|
-
message: ` Step ${String(stepNum)} (empty to finish):`,
|
|
155
|
-
});
|
|
156
|
-
if (step.trim()) {
|
|
157
|
-
steps.push(step.trim());
|
|
158
|
-
stepNum++;
|
|
159
|
-
} else {
|
|
160
|
-
adding = false;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Optionally link to a ticket
|
|
166
|
-
const tickets = await listTickets();
|
|
167
|
-
|
|
168
|
-
if (tickets.length > 0) {
|
|
169
|
-
const { select } = await import('@inquirer/prompts');
|
|
170
|
-
const defaultTicketValue = options.ticket ? (tickets.find((t) => t.id === options.ticket)?.id ?? '') : '';
|
|
171
|
-
const ticketChoice = await select({
|
|
172
|
-
message: `${icons.ticket} Link to ticket:`,
|
|
173
|
-
default: defaultTicketValue,
|
|
174
|
-
choices: [
|
|
175
|
-
{ name: `${emoji.donut} None (select project/repo manually)`, value: '' },
|
|
176
|
-
...tickets.map((t) => ({
|
|
177
|
-
name: `${icons.ticket} ${formatTicketDisplay(t)} ${muted(`(${t.projectName})`)}`,
|
|
178
|
-
value: t.id,
|
|
179
|
-
})),
|
|
180
|
-
],
|
|
181
|
-
});
|
|
182
|
-
if (ticketChoice) {
|
|
183
|
-
ticketId = ticketChoice;
|
|
184
|
-
const ticket = tickets.find((t) => t.id === ticketChoice);
|
|
185
|
-
if (ticket) {
|
|
186
|
-
try {
|
|
187
|
-
const project = await getProject(ticket.projectName);
|
|
188
|
-
// Auto-select first repo for ticket, or prompt if multiple
|
|
189
|
-
if (project.repositories.length === 1) {
|
|
190
|
-
projectPath = project.repositories[0]?.path;
|
|
191
|
-
} else {
|
|
192
|
-
// Multiple repos - let user pick
|
|
193
|
-
const { select: selectRepo } = await import('@inquirer/prompts');
|
|
194
|
-
projectPath = await selectRepo({
|
|
195
|
-
message: `${emoji.donut} Select repository for this task:`,
|
|
196
|
-
choices: project.repositories.map((r) => ({
|
|
197
|
-
name: `${r.name} (${r.path})`,
|
|
198
|
-
value: r.path,
|
|
199
|
-
})),
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
} catch {
|
|
203
|
-
log.warn(`Project '${ticket.projectName}' not found, will prompt for path.`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
} else if (options.ticket) {
|
|
208
|
-
ticketId = options.ticket;
|
|
209
|
-
try {
|
|
210
|
-
const ticket = await getTicket(ticketId);
|
|
211
|
-
const project = await getProject(ticket.projectName);
|
|
212
|
-
projectPath = project.repositories[0]?.path;
|
|
213
|
-
} catch {
|
|
214
|
-
// Will prompt for project below
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// If no project from ticket, use two-step selector
|
|
219
|
-
if (projectPath === undefined) {
|
|
220
|
-
const projects = await listProjects();
|
|
221
|
-
|
|
222
|
-
if (projects.length > 0) {
|
|
223
|
-
const { select } = await import('@inquirer/prompts');
|
|
224
|
-
const choice = await select({
|
|
225
|
-
message: `${icons.project} Select project:`,
|
|
226
|
-
choices: [
|
|
227
|
-
{ name: `${icons.edit} Enter path manually`, value: '__manual__' },
|
|
228
|
-
{ name: `${emoji.donut} Select project/repository`, value: '__select__' },
|
|
229
|
-
],
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
if (choice === '__manual__') {
|
|
233
|
-
projectPath = await input({
|
|
234
|
-
message: `${icons.project} Project path:`,
|
|
235
|
-
default: options.project?.trim() ?? process.cwd(),
|
|
236
|
-
validate: async (v) => {
|
|
237
|
-
const result = await validateProjectPath(v.trim());
|
|
238
|
-
return result;
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
projectPath = resolve(projectPath.trim());
|
|
242
|
-
} else {
|
|
243
|
-
// Two-step selector: project → repository
|
|
244
|
-
const selectedPath = await selectProjectRepository('Select repository:');
|
|
245
|
-
if (!selectedPath) {
|
|
246
|
-
showError('No repository selected');
|
|
247
|
-
exitWithCode(EXIT_ERROR);
|
|
248
|
-
}
|
|
249
|
-
projectPath = selectedPath;
|
|
250
|
-
}
|
|
251
|
-
} else {
|
|
252
|
-
projectPath = await input({
|
|
253
|
-
message: `${icons.project} Project path:`,
|
|
254
|
-
default: options.project?.trim() ?? process.cwd(),
|
|
255
|
-
validate: async (v) => {
|
|
256
|
-
const result = await validateProjectPath(v.trim());
|
|
257
|
-
return result;
|
|
258
|
-
},
|
|
259
|
-
});
|
|
260
|
-
projectPath = resolve(projectPath.trim());
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
name = name.trim();
|
|
265
|
-
const trimmedDescription = description.trim();
|
|
266
|
-
description = trimmedDescription === '' ? undefined : trimmedDescription;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// projectPath must be set by this point
|
|
270
|
-
if (!projectPath) {
|
|
271
|
-
showError('Project path is required');
|
|
272
|
-
exitWithCode(EXIT_ERROR);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
const task = await addTask({
|
|
277
|
-
name,
|
|
278
|
-
description,
|
|
279
|
-
steps,
|
|
280
|
-
ticketId,
|
|
281
|
-
projectPath,
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
showSuccess('Task added!', [
|
|
285
|
-
['ID', task.id],
|
|
286
|
-
['Name', task.name],
|
|
287
|
-
['Project', task.projectPath],
|
|
288
|
-
['Order', String(task.order)],
|
|
289
|
-
]);
|
|
290
|
-
|
|
291
|
-
if (task.ticketId) {
|
|
292
|
-
console.log(field('Ticket', task.ticketId));
|
|
293
|
-
}
|
|
294
|
-
if (task.steps.length > 0) {
|
|
295
|
-
console.log(field('Steps', ''));
|
|
296
|
-
task.steps.forEach((step, i) => {
|
|
297
|
-
console.log(muted(` ${String(i + 1)}. ${step}`));
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
console.log('');
|
|
301
|
-
} catch (err) {
|
|
302
|
-
if (err instanceof SprintStatusError) {
|
|
303
|
-
// Fallback handler (shouldn't reach here due to early check)
|
|
304
|
-
const mainError = err.message.split('\n')[0] ?? err.message;
|
|
305
|
-
showError(mainError);
|
|
306
|
-
showNextSteps([
|
|
307
|
-
['ralphctl sprint close', 'close current sprint'],
|
|
308
|
-
['ralphctl sprint create', 'start a new draft sprint'],
|
|
309
|
-
]);
|
|
310
|
-
log.newline();
|
|
311
|
-
if (!isInteractive) exitWithCode(EXIT_ERROR);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
throw err;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { error, muted } from '@src/theme/index.ts';
|
|
3
|
-
import { createSpinner, log, showError, showNextStep } from '@src/theme/ui.ts';
|
|
4
|
-
import { addTask, getTasks, saveTasks, validateImportTasks } from '@src/store/task.ts';
|
|
5
|
-
import { getSprint, resolveSprintId, SprintStatusError } from '@src/store/sprint.ts';
|
|
6
|
-
import { ImportTasksSchema } from '@src/schemas/index.ts';
|
|
7
|
-
import { withFileLock } from '@src/utils/file-lock.ts';
|
|
8
|
-
import { getTasksFilePath } from '@src/utils/paths.ts';
|
|
9
|
-
|
|
10
|
-
export async function taskImportCommand(args: string[]): Promise<void> {
|
|
11
|
-
const filePath = args[0];
|
|
12
|
-
|
|
13
|
-
if (!filePath) {
|
|
14
|
-
showError('File path required.');
|
|
15
|
-
showNextStep('ralphctl task import <file.json>', 'provide a task file');
|
|
16
|
-
log.dim('Expected JSON format:');
|
|
17
|
-
console.log(
|
|
18
|
-
muted(`[
|
|
19
|
-
{
|
|
20
|
-
"id": "1",
|
|
21
|
-
"name": "Task name",
|
|
22
|
-
"projectPath": "/path/to/repo",
|
|
23
|
-
"description": "Optional description",
|
|
24
|
-
"steps": ["Step 1", "Step 2"],
|
|
25
|
-
"ticketId": "abc12345",
|
|
26
|
-
"blockedBy": ["task-001"]
|
|
27
|
-
}
|
|
28
|
-
]`)
|
|
29
|
-
);
|
|
30
|
-
log.dim('Note: projectPath is required for each task.');
|
|
31
|
-
log.newline();
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let content: string;
|
|
36
|
-
try {
|
|
37
|
-
content = await readFile(filePath, 'utf-8');
|
|
38
|
-
} catch {
|
|
39
|
-
showError(`Failed to read file: ${filePath}`);
|
|
40
|
-
log.newline();
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
let data: unknown;
|
|
45
|
-
try {
|
|
46
|
-
data = JSON.parse(content);
|
|
47
|
-
} catch {
|
|
48
|
-
showError('Invalid JSON format.');
|
|
49
|
-
log.newline();
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const result = ImportTasksSchema.safeParse(data);
|
|
54
|
-
if (!result.success) {
|
|
55
|
-
showError('Invalid task format');
|
|
56
|
-
for (const issue of result.error.issues) {
|
|
57
|
-
log.item(error(`${issue.path.join('.')}: ${issue.message}`));
|
|
58
|
-
}
|
|
59
|
-
log.newline();
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const tasks = result.data;
|
|
64
|
-
if (tasks.length === 0) {
|
|
65
|
-
showError('No tasks to import.');
|
|
66
|
-
log.newline();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Validate dependencies and ticketId references before importing
|
|
71
|
-
const existingTasks = await getTasks();
|
|
72
|
-
const sprintId = await resolveSprintId();
|
|
73
|
-
const sprint = await getSprint(sprintId);
|
|
74
|
-
const ticketIds = new Set(sprint.tickets.map((t) => t.id));
|
|
75
|
-
const validationErrors = validateImportTasks(tasks, existingTasks, ticketIds);
|
|
76
|
-
if (validationErrors.length > 0) {
|
|
77
|
-
showError('Dependency validation failed');
|
|
78
|
-
for (const err of validationErrors) {
|
|
79
|
-
log.item(error(err));
|
|
80
|
-
}
|
|
81
|
-
log.newline();
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Build local ID to real ID mapping
|
|
86
|
-
const localToRealId = new Map<string, string>();
|
|
87
|
-
const createdTasks: { task: (typeof tasks)[0]; realId: string }[] = [];
|
|
88
|
-
|
|
89
|
-
// First pass: create tasks without blockedBy
|
|
90
|
-
const spinner = createSpinner(`Importing ${String(tasks.length)} task(s)...`).start();
|
|
91
|
-
let imported = 0;
|
|
92
|
-
for (const taskInput of tasks) {
|
|
93
|
-
try {
|
|
94
|
-
// projectPath is required from the schema
|
|
95
|
-
const task = await addTask({
|
|
96
|
-
name: taskInput.name,
|
|
97
|
-
description: taskInput.description,
|
|
98
|
-
steps: taskInput.steps ?? [],
|
|
99
|
-
ticketId: taskInput.ticketId,
|
|
100
|
-
blockedBy: [], // Set later
|
|
101
|
-
projectPath: taskInput.projectPath,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// Map local ID to real ID
|
|
105
|
-
if (taskInput.id) {
|
|
106
|
-
localToRealId.set(taskInput.id, task.id);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
createdTasks.push({ task: taskInput, realId: task.id });
|
|
110
|
-
imported++;
|
|
111
|
-
spinner.text = `Importing tasks... (${String(imported)}/${String(tasks.length)})`;
|
|
112
|
-
} catch (err) {
|
|
113
|
-
if (err instanceof SprintStatusError) {
|
|
114
|
-
spinner.fail('Import failed');
|
|
115
|
-
showError(err.message);
|
|
116
|
-
log.newline();
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
log.itemError(`Failed to add: ${taskInput.name}`);
|
|
120
|
-
if (err instanceof Error) {
|
|
121
|
-
console.log(muted(` ${err.message}`));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Second pass: update blockedBy with resolved real IDs (under file lock)
|
|
127
|
-
spinner.text = 'Resolving task dependencies...';
|
|
128
|
-
const tasksFilePath = getTasksFilePath(sprintId);
|
|
129
|
-
await withFileLock(tasksFilePath, async () => {
|
|
130
|
-
const allTasks = await getTasks();
|
|
131
|
-
for (const { task: taskInput, realId } of createdTasks) {
|
|
132
|
-
const blockedBy = (taskInput.blockedBy ?? [])
|
|
133
|
-
.map((localId) => localToRealId.get(localId) ?? '')
|
|
134
|
-
.filter((id) => id !== '');
|
|
135
|
-
|
|
136
|
-
if (blockedBy.length > 0) {
|
|
137
|
-
const taskToUpdate = allTasks.find((t) => t.id === realId);
|
|
138
|
-
if (taskToUpdate) {
|
|
139
|
-
taskToUpdate.blockedBy = blockedBy;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
await saveTasks(allTasks);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
spinner.succeed(`Imported ${String(imported)}/${String(tasks.length)} tasks`);
|
|
147
|
-
for (const { task: taskInput, realId } of createdTasks) {
|
|
148
|
-
log.itemSuccess(`${realId}: ${taskInput.name}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import type { Command } from 'commander';
|
|
2
|
-
import { taskAddCommand } from '@src/commands/task/add.ts';
|
|
3
|
-
import { taskListCommand } from '@src/commands/task/list.ts';
|
|
4
|
-
import { taskShowCommand } from '@src/commands/task/show.ts';
|
|
5
|
-
import { taskRemoveCommand } from '@src/commands/task/remove.ts';
|
|
6
|
-
import { taskStatusCommand } from '@src/commands/task/status.ts';
|
|
7
|
-
import { taskNextCommand } from '@src/commands/task/next.ts';
|
|
8
|
-
import { taskReorderCommand } from '@src/commands/task/reorder.ts';
|
|
9
|
-
import { taskImportCommand } from '@src/commands/task/import.ts';
|
|
10
|
-
|
|
11
|
-
export function registerTaskCommands(program: Command): void {
|
|
12
|
-
const task = program.command('task').description('Manage tasks');
|
|
13
|
-
|
|
14
|
-
task.addHelpText(
|
|
15
|
-
'after',
|
|
16
|
-
`
|
|
17
|
-
Examples:
|
|
18
|
-
$ ralphctl task add --name "Implement login" --ticket abc123
|
|
19
|
-
$ ralphctl task list
|
|
20
|
-
$ ralphctl task status abc123 done
|
|
21
|
-
$ ralphctl task next
|
|
22
|
-
`
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
task
|
|
26
|
-
.command('add')
|
|
27
|
-
.description('Add task to current sprint')
|
|
28
|
-
.option('--name <name>', 'Task name')
|
|
29
|
-
.option('-d, --description <desc>', 'Description')
|
|
30
|
-
.option('--step <step...>', 'Implementation step (repeatable)')
|
|
31
|
-
.option('--ticket <id>', 'Link to ticket ID')
|
|
32
|
-
.option('-p, --project <path>', 'Project path')
|
|
33
|
-
.option('-n, --no-interactive', 'Non-interactive mode (error on missing params)')
|
|
34
|
-
.action(
|
|
35
|
-
async (opts: {
|
|
36
|
-
name?: string;
|
|
37
|
-
description?: string;
|
|
38
|
-
step?: string[];
|
|
39
|
-
ticket?: string;
|
|
40
|
-
project?: string;
|
|
41
|
-
interactive?: boolean;
|
|
42
|
-
}) => {
|
|
43
|
-
await taskAddCommand({
|
|
44
|
-
name: opts.name,
|
|
45
|
-
description: opts.description,
|
|
46
|
-
steps: opts.step,
|
|
47
|
-
ticket: opts.ticket,
|
|
48
|
-
project: opts.project,
|
|
49
|
-
// --no-interactive sets interactive=false, otherwise true (prompt for missing)
|
|
50
|
-
interactive: opts.interactive !== false,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
task
|
|
56
|
-
.command('import <file>')
|
|
57
|
-
.description('Import tasks from JSON file')
|
|
58
|
-
.action(async (file: string) => {
|
|
59
|
-
await taskImportCommand([file]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
task
|
|
63
|
-
.command('list')
|
|
64
|
-
.description('List tasks')
|
|
65
|
-
.option('-b, --brief', 'Brief format')
|
|
66
|
-
.option('--status <status>', 'Filter by status (todo, in_progress, done)')
|
|
67
|
-
.option('--project <name>', 'Filter by project path')
|
|
68
|
-
.option('--ticket <id>', 'Filter by ticket ID')
|
|
69
|
-
.option('--blocked', 'Show only blocked tasks')
|
|
70
|
-
.action(
|
|
71
|
-
async (opts: { brief?: boolean; status?: string; project?: string; ticket?: string; blocked?: boolean }) => {
|
|
72
|
-
const args: string[] = [];
|
|
73
|
-
if (opts.brief) args.push('-b');
|
|
74
|
-
if (opts.status) args.push('--status', opts.status);
|
|
75
|
-
if (opts.project) args.push('--project', opts.project);
|
|
76
|
-
if (opts.ticket) args.push('--ticket', opts.ticket);
|
|
77
|
-
if (opts.blocked) args.push('--blocked');
|
|
78
|
-
await taskListCommand(args);
|
|
79
|
-
}
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
task
|
|
83
|
-
.command('show [id]')
|
|
84
|
-
.description('Show task details')
|
|
85
|
-
.action(async (id?: string) => {
|
|
86
|
-
await taskShowCommand(id ? [id] : []);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
task
|
|
90
|
-
.command('remove [id]')
|
|
91
|
-
.description('Remove a task')
|
|
92
|
-
.option('-y, --yes', 'Skip confirmation')
|
|
93
|
-
.action(async (id?: string, opts?: { yes?: boolean }) => {
|
|
94
|
-
const args: string[] = [];
|
|
95
|
-
if (id) args.push(id);
|
|
96
|
-
if (opts?.yes) args.push('-y');
|
|
97
|
-
await taskRemoveCommand(args);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
task
|
|
101
|
-
.command('status [id] [status]')
|
|
102
|
-
.description('Update task status (todo/in_progress/done)')
|
|
103
|
-
.option('-n, --no-interactive', 'Non-interactive mode (exit with error codes)')
|
|
104
|
-
.action(async (id?: string, status?: string, opts?: { interactive?: boolean }) => {
|
|
105
|
-
await taskStatusCommand([], {
|
|
106
|
-
taskId: id,
|
|
107
|
-
status,
|
|
108
|
-
noInteractive: opts?.interactive === false,
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
task.command('next').description('Get next task').action(taskNextCommand);
|
|
113
|
-
|
|
114
|
-
task
|
|
115
|
-
.command('reorder [id] [position]')
|
|
116
|
-
.description('Change task priority')
|
|
117
|
-
.action(async (id?: string, position?: string) => {
|
|
118
|
-
const args: string[] = [];
|
|
119
|
-
if (id) args.push(id);
|
|
120
|
-
if (position) args.push(position);
|
|
121
|
-
await taskReorderCommand(args);
|
|
122
|
-
});
|
|
123
|
-
}
|