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
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import type { Command } from 'commander';
|
|
2
|
-
import { sprintCreateCommand } from '@src/commands/sprint/create.ts';
|
|
3
|
-
import { sprintListCommand } from '@src/commands/sprint/list.ts';
|
|
4
|
-
import { sprintShowCommand } from '@src/commands/sprint/show.ts';
|
|
5
|
-
import { sprintContextCommand } from '@src/commands/sprint/context.ts';
|
|
6
|
-
import { sprintCloseCommand } from '@src/commands/sprint/close.ts';
|
|
7
|
-
import { sprintStartCommand } from '@src/commands/sprint/start.ts';
|
|
8
|
-
import { sprintPlanCommand } from '@src/commands/sprint/plan.ts';
|
|
9
|
-
import { sprintCurrentCommand } from '@src/commands/sprint/current.ts';
|
|
10
|
-
import { sprintSwitchCommand } from '@src/commands/sprint/switch.ts';
|
|
11
|
-
import { sprintRefineCommand } from '@src/commands/sprint/refine.ts';
|
|
12
|
-
import { sprintIdeateCommand } from '@src/commands/sprint/ideate.ts';
|
|
13
|
-
import { sprintRequirementsCommand } from '@src/commands/sprint/requirements.ts';
|
|
14
|
-
import { sprintHealthCommand } from '@src/commands/sprint/health.ts';
|
|
15
|
-
import { sprintDeleteCommand } from '@src/commands/sprint/delete.ts';
|
|
16
|
-
|
|
17
|
-
export function registerSprintCommands(program: Command): void {
|
|
18
|
-
const sprint = program.command('sprint').description('Manage sprints');
|
|
19
|
-
|
|
20
|
-
sprint.addHelpText(
|
|
21
|
-
'after',
|
|
22
|
-
`
|
|
23
|
-
Examples:
|
|
24
|
-
$ ralphctl sprint create --name "Sprint 1"
|
|
25
|
-
$ ralphctl sprint refine # Refine ticket requirements with AI
|
|
26
|
-
$ ralphctl sprint plan --auto # Generate tasks automatically
|
|
27
|
-
$ ralphctl sprint start -s # Start with interactive session
|
|
28
|
-
`
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
sprint
|
|
32
|
-
.command('create')
|
|
33
|
-
.description('Create a new sprint')
|
|
34
|
-
.option('--name <name>', 'Sprint name')
|
|
35
|
-
.option('-n, --no-interactive', 'Non-interactive mode (error on missing params)')
|
|
36
|
-
.action(async (opts: { name?: string; interactive?: boolean }) => {
|
|
37
|
-
await sprintCreateCommand({
|
|
38
|
-
name: opts.name,
|
|
39
|
-
// --no-interactive sets interactive=false, otherwise true (prompt for missing)
|
|
40
|
-
interactive: opts.interactive !== false,
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
sprint
|
|
45
|
-
.command('list')
|
|
46
|
-
.description('List all sprints')
|
|
47
|
-
.option('--status <status>', 'Filter by status (draft, active, closed)')
|
|
48
|
-
.action(async (opts: { status?: string }) => {
|
|
49
|
-
const args: string[] = [];
|
|
50
|
-
if (opts.status) args.push('--status', opts.status);
|
|
51
|
-
await sprintListCommand(args);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
sprint
|
|
55
|
-
.command('show [id]')
|
|
56
|
-
.description('Show sprint details')
|
|
57
|
-
.action(async (id?: string) => {
|
|
58
|
-
await sprintShowCommand(id ? [id] : []);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
sprint
|
|
62
|
-
.command('context [id]')
|
|
63
|
-
.description('Output full context for planning')
|
|
64
|
-
.action(async (id?: string) => {
|
|
65
|
-
await sprintContextCommand(id ? [id] : []);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
sprint
|
|
69
|
-
.command('current [id]')
|
|
70
|
-
.description('Show/set current sprint (use "-" to open selector)')
|
|
71
|
-
.action(async (id?: string) => {
|
|
72
|
-
await sprintCurrentCommand(id ? [id] : []);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
sprint
|
|
76
|
-
.command('switch')
|
|
77
|
-
.description('Quick sprint switcher (opens selector)')
|
|
78
|
-
.action(async () => {
|
|
79
|
-
await sprintSwitchCommand();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
sprint
|
|
83
|
-
.command('refine [id]')
|
|
84
|
-
.description('Refine ticket specifications')
|
|
85
|
-
.option('--project <name>', 'Only refine tickets for specific project')
|
|
86
|
-
.action(async (id?: string, opts?: { project?: string }) => {
|
|
87
|
-
const args: string[] = [];
|
|
88
|
-
if (id) args.push(id);
|
|
89
|
-
if (opts?.project) args.push('--project', opts.project);
|
|
90
|
-
await sprintRefineCommand(args);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
sprint
|
|
94
|
-
.command('ideate [id]')
|
|
95
|
-
.description('Quick idea to tasks (refine + plan in one session)')
|
|
96
|
-
.option('--auto', 'Run without user interaction (AI decides autonomously)')
|
|
97
|
-
.option('--all-paths', 'Explore all project repositories instead of prompting for selection')
|
|
98
|
-
.option('--project <name>', 'Pre-select project (skip interactive selection)')
|
|
99
|
-
.action(async (id?: string, opts?: { auto?: boolean; allPaths?: boolean; project?: string }) => {
|
|
100
|
-
const args: string[] = [];
|
|
101
|
-
if (id) args.push(id);
|
|
102
|
-
if (opts?.auto) args.push('--auto');
|
|
103
|
-
if (opts?.allPaths) args.push('--all-paths');
|
|
104
|
-
if (opts?.project) args.push('--project', opts.project);
|
|
105
|
-
await sprintIdeateCommand(args);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
sprint
|
|
109
|
-
.command('plan [id]')
|
|
110
|
-
.description('Generate tasks using AI CLI')
|
|
111
|
-
.option('--auto', 'Run without user interaction (AI decides autonomously)')
|
|
112
|
-
.option('--all-paths', 'Explore all project repositories instead of prompting for selection')
|
|
113
|
-
.action(async (id?: string, opts?: { auto?: boolean; allPaths?: boolean }) => {
|
|
114
|
-
const args: string[] = [];
|
|
115
|
-
if (id) args.push(id);
|
|
116
|
-
if (opts?.auto) args.push('--auto');
|
|
117
|
-
if (opts?.allPaths) args.push('--all-paths');
|
|
118
|
-
await sprintPlanCommand(args);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
sprint
|
|
122
|
-
.command('close [id]')
|
|
123
|
-
.description('Close an active sprint')
|
|
124
|
-
.option('--create-pr', 'Create pull requests for sprint branches')
|
|
125
|
-
.action(async (id?: string, opts?: { createPr?: boolean }) => {
|
|
126
|
-
const args: string[] = [];
|
|
127
|
-
if (id) args.push(id);
|
|
128
|
-
if (opts?.createPr) args.push('--create-pr');
|
|
129
|
-
await sprintCloseCommand(args);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
sprint
|
|
133
|
-
.command('delete [id]')
|
|
134
|
-
.description('Delete a sprint permanently')
|
|
135
|
-
.option('-y, --yes', 'Skip confirmation')
|
|
136
|
-
.action(async (id?: string, opts?: { yes?: boolean }) => {
|
|
137
|
-
const args: string[] = [];
|
|
138
|
-
if (id) args.push(id);
|
|
139
|
-
if (opts?.yes) args.push('-y');
|
|
140
|
-
await sprintDeleteCommand(args);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
sprint
|
|
144
|
-
.command('requirements [id]')
|
|
145
|
-
.description('Export refined requirements to file')
|
|
146
|
-
.action(async (id?: string) => {
|
|
147
|
-
await sprintRequirementsCommand(id ? [id] : []);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
sprint
|
|
151
|
-
.command('health')
|
|
152
|
-
.description('Check sprint health')
|
|
153
|
-
.action(async () => {
|
|
154
|
-
await sprintHealthCommand();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
sprint
|
|
158
|
-
.command('start [id]')
|
|
159
|
-
.description('Run automated implementation loop')
|
|
160
|
-
.option('-s, --session', 'Interactive AI session (collaborate with your AI provider)')
|
|
161
|
-
.option('-t, --step', 'Step through tasks with approval between each')
|
|
162
|
-
.option('-c, --count <n>', 'Limit to N tasks')
|
|
163
|
-
.option('--no-commit', 'Skip automatic git commit after each task completes')
|
|
164
|
-
.option('--concurrency <n>', 'Max parallel tasks (default: auto based on unique repos)')
|
|
165
|
-
.option('--max-retries <n>', 'Max rate-limit retries per task (default: 5)')
|
|
166
|
-
.option('--fail-fast', 'Stop launching new tasks on first failure')
|
|
167
|
-
.option('-f, --force', 'Skip precondition checks (e.g., unplanned tickets)')
|
|
168
|
-
.option('--refresh-check', 'Force re-run check scripts even if they already ran this sprint')
|
|
169
|
-
.option('-b, --branch', 'Create sprint branch (ralphctl/<sprint-id>) in all repos')
|
|
170
|
-
.option('--branch-name <name>', 'Use a custom branch name for sprint execution')
|
|
171
|
-
.addHelpText(
|
|
172
|
-
'after',
|
|
173
|
-
`
|
|
174
|
-
Exit Codes:
|
|
175
|
-
0 - Success (all requested operations completed)
|
|
176
|
-
1 - Error (validation, missing params, execution failed)
|
|
177
|
-
2 - No tasks available
|
|
178
|
-
3 - All remaining tasks blocked by dependencies
|
|
179
|
-
|
|
180
|
-
Parallel Execution:
|
|
181
|
-
Tasks targeting different repos run concurrently by default.
|
|
182
|
-
At most one task per repository runs at a time to avoid git conflicts.
|
|
183
|
-
Use --concurrency 1 to force sequential execution.
|
|
184
|
-
Session (--session) and step (--step) modes always run sequentially.
|
|
185
|
-
|
|
186
|
-
Branch Management:
|
|
187
|
-
Use -b/--branch to auto-create a sprint branch in all repos.
|
|
188
|
-
Use --branch-name <name> to specify a custom branch name.
|
|
189
|
-
On first run, an interactive prompt offers branch strategy selection.
|
|
190
|
-
The chosen branch is persisted and reused on subsequent runs.
|
|
191
|
-
`
|
|
192
|
-
)
|
|
193
|
-
.action(
|
|
194
|
-
async (
|
|
195
|
-
id?: string,
|
|
196
|
-
opts?: {
|
|
197
|
-
session?: boolean;
|
|
198
|
-
step?: boolean;
|
|
199
|
-
count?: string;
|
|
200
|
-
commit?: boolean;
|
|
201
|
-
concurrency?: string;
|
|
202
|
-
maxRetries?: string;
|
|
203
|
-
failFast?: boolean;
|
|
204
|
-
force?: boolean;
|
|
205
|
-
refreshCheck?: boolean;
|
|
206
|
-
branch?: boolean;
|
|
207
|
-
branchName?: string;
|
|
208
|
-
}
|
|
209
|
-
) => {
|
|
210
|
-
const args: string[] = [];
|
|
211
|
-
if (id) args.push(id);
|
|
212
|
-
if (opts?.session) args.push('--session');
|
|
213
|
-
if (opts?.step) args.push('--step');
|
|
214
|
-
if (opts?.count) args.push('--count', opts.count);
|
|
215
|
-
if (opts?.commit === false) args.push('--no-commit');
|
|
216
|
-
if (opts?.concurrency) args.push('--concurrency', opts.concurrency);
|
|
217
|
-
if (opts?.maxRetries) args.push('--max-retries', opts.maxRetries);
|
|
218
|
-
if (opts?.failFast) args.push('--fail-fast');
|
|
219
|
-
if (opts?.force) args.push('--force');
|
|
220
|
-
if (opts?.refreshCheck) args.push('--refresh-check');
|
|
221
|
-
if (opts?.branch) args.push('--branch');
|
|
222
|
-
if (opts?.branchName) args.push('--branch-name', opts.branchName);
|
|
223
|
-
await sprintStartCommand(args);
|
|
224
|
-
}
|
|
225
|
-
);
|
|
226
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { listSprints } from '@src/store/sprint.ts';
|
|
2
|
-
import { getCurrentSprint } from '@src/store/config.ts';
|
|
3
|
-
import { SprintStatusSchema } from '@src/schemas/index.ts';
|
|
4
|
-
import {
|
|
5
|
-
badge,
|
|
6
|
-
formatSprintStatus,
|
|
7
|
-
icons,
|
|
8
|
-
log,
|
|
9
|
-
printHeader,
|
|
10
|
-
renderTable,
|
|
11
|
-
showEmpty,
|
|
12
|
-
showError,
|
|
13
|
-
showNextStep,
|
|
14
|
-
} from '@src/theme/ui.ts';
|
|
15
|
-
|
|
16
|
-
export async function sprintListCommand(args: string[] = []): Promise<void> {
|
|
17
|
-
// Parse status filter
|
|
18
|
-
let statusFilter: string | undefined;
|
|
19
|
-
for (let i = 0; i < args.length; i++) {
|
|
20
|
-
if (args[i] === '--status' && args[i + 1]) {
|
|
21
|
-
statusFilter = args[i + 1];
|
|
22
|
-
i++;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Validate status filter
|
|
27
|
-
if (statusFilter) {
|
|
28
|
-
const result = SprintStatusSchema.safeParse(statusFilter);
|
|
29
|
-
if (!result.success) {
|
|
30
|
-
showError(`Invalid status: "${statusFilter}". Valid values: draft, active, closed`);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const sprints = await listSprints();
|
|
36
|
-
|
|
37
|
-
if (sprints.length === 0) {
|
|
38
|
-
showEmpty('sprints', 'Create one with: ralphctl sprint create');
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const filtered = statusFilter ? sprints.filter((s) => s.status === statusFilter) : sprints;
|
|
43
|
-
const isFiltered = filtered.length !== sprints.length;
|
|
44
|
-
const filterStr = statusFilter ? ` (filtered: status=${statusFilter})` : '';
|
|
45
|
-
|
|
46
|
-
if (filtered.length === 0) {
|
|
47
|
-
showEmpty('matching sprints', 'Try adjusting your filters');
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
printHeader('Sprints', icons.sprint);
|
|
52
|
-
|
|
53
|
-
const currentSprintId = await getCurrentSprint();
|
|
54
|
-
|
|
55
|
-
const rows: string[][] = filtered.map((sprint) => {
|
|
56
|
-
const isCurrent = sprint.id === currentSprintId;
|
|
57
|
-
const marker = isCurrent ? badge('current', 'success') : '';
|
|
58
|
-
return [marker, sprint.id, formatSprintStatus(sprint.status), sprint.name, String(sprint.tickets.length)];
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
console.log(
|
|
62
|
-
renderTable(
|
|
63
|
-
[
|
|
64
|
-
{ header: '', minWidth: 0 },
|
|
65
|
-
{ header: 'ID' },
|
|
66
|
-
{ header: 'Status' },
|
|
67
|
-
{ header: 'Name' },
|
|
68
|
-
{ header: 'Tickets', align: 'right' },
|
|
69
|
-
],
|
|
70
|
-
rows
|
|
71
|
-
)
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
log.newline();
|
|
75
|
-
const showingLabel = isFiltered
|
|
76
|
-
? `Showing ${String(filtered.length)} of ${String(sprints.length)} sprint(s)${filterStr}`
|
|
77
|
-
: `Showing ${String(sprints.length)} sprint(s)`;
|
|
78
|
-
log.dim(showingLabel);
|
|
79
|
-
|
|
80
|
-
const hasActive = sprints.some((s) => s.status === 'active');
|
|
81
|
-
if (!hasActive) {
|
|
82
|
-
log.newline();
|
|
83
|
-
showNextStep('ralphctl sprint start', 'start a sprint');
|
|
84
|
-
}
|
|
85
|
-
log.newline();
|
|
86
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { muted } from '@src/theme/index.ts';
|
|
3
|
-
import { log, renderTable } from '@src/theme/ui.ts';
|
|
4
|
-
import { addTask, getTasks, saveTasks } from '@src/store/task.ts';
|
|
5
|
-
import { getSchemaPath, getTasksFilePath } from '@src/utils/paths.ts';
|
|
6
|
-
import { withFileLock } from '@src/utils/file-lock.ts';
|
|
7
|
-
import { type ImportTask, ImportTasksSchema, type Task } from '@src/schemas/index.ts';
|
|
8
|
-
import { extractJsonArray } from '@src/utils/json-extract.ts';
|
|
9
|
-
import { generateUuid8 } from '@src/utils/ids.ts';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Load the task import JSON schema from file.
|
|
13
|
-
*/
|
|
14
|
-
export async function getTaskImportSchema(): Promise<string> {
|
|
15
|
-
const schemaPath = getSchemaPath('task-import.schema.json');
|
|
16
|
-
return readFile(schemaPath, 'utf-8');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Check if AI output contains a planning-blocked signal.
|
|
21
|
-
* Returns the reason if blocked, null otherwise.
|
|
22
|
-
*/
|
|
23
|
-
export function parsePlanningBlocked(output: string): string | null {
|
|
24
|
-
const match = /<planning-blocked>([\s\S]*?)<\/planning-blocked>/.exec(output);
|
|
25
|
-
return match?.[1]?.trim() ?? null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Parse AI output to extract and validate task JSON array.
|
|
30
|
-
*/
|
|
31
|
-
export function parseTasksJson(output: string): ImportTask[] {
|
|
32
|
-
// Try to extract a balanced JSON array from the output (handles nested arrays like steps)
|
|
33
|
-
const jsonStr = extractJsonArray(output);
|
|
34
|
-
|
|
35
|
-
let parsed: unknown;
|
|
36
|
-
try {
|
|
37
|
-
parsed = JSON.parse(jsonStr);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
throw new Error(`Invalid JSON: ${err instanceof Error ? err.message : 'parse error'}`, { cause: err });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (!Array.isArray(parsed)) {
|
|
43
|
-
throw new Error('Expected JSON array');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Validate against schema
|
|
47
|
-
const result = ImportTasksSchema.safeParse(parsed);
|
|
48
|
-
if (!result.success) {
|
|
49
|
-
const issues = result.error.issues
|
|
50
|
-
.map((issue) => {
|
|
51
|
-
const path = issue.path.length > 0 ? `[${issue.path.join('.')}]` : '';
|
|
52
|
-
return ` ${path}: ${issue.message}`;
|
|
53
|
-
})
|
|
54
|
-
.join('\n');
|
|
55
|
-
throw new Error(`Invalid task format:\n${issues}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return result.data;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Render parsed tasks as a formatted table.
|
|
63
|
-
*/
|
|
64
|
-
export function renderParsedTasksTable(parsedTasks: ImportTask[]): string {
|
|
65
|
-
const rows = parsedTasks.map((task, i) => {
|
|
66
|
-
const deps = task.blockedBy?.length ? task.blockedBy.join(', ') : '';
|
|
67
|
-
return [String(i + 1), task.name, task.projectPath, deps];
|
|
68
|
-
});
|
|
69
|
-
return renderTable(
|
|
70
|
-
[{ header: '#', align: 'right' as const }, { header: 'Name' }, { header: 'Path' }, { header: 'Blocked By' }],
|
|
71
|
-
rows
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Import tasks with two-pass ID resolution.
|
|
77
|
-
* When `replace: true`, builds the complete task list in memory and writes atomically
|
|
78
|
-
* (interruption-safe: original tasks.json untouched until final write).
|
|
79
|
-
* When `replace: false` (default), appends via addTask() one-by-one.
|
|
80
|
-
* Returns the number of successfully imported tasks.
|
|
81
|
-
*/
|
|
82
|
-
export async function importTasks(
|
|
83
|
-
tasks: ImportTask[],
|
|
84
|
-
sprintId: string,
|
|
85
|
-
options?: { replace?: boolean }
|
|
86
|
-
): Promise<number> {
|
|
87
|
-
if (options?.replace) {
|
|
88
|
-
return importTasksReplace(tasks, sprintId);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return importTasksAppend(tasks, sprintId);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Append tasks one-by-one via addTask() (first plan — no existing tasks).
|
|
96
|
-
*/
|
|
97
|
-
async function importTasksAppend(tasks: ImportTask[], sprintId: string): Promise<number> {
|
|
98
|
-
// Build mapping from local IDs to real IDs
|
|
99
|
-
const localToRealId = new Map<string, string>();
|
|
100
|
-
|
|
101
|
-
// First pass: create all tasks and build ID mapping
|
|
102
|
-
const createdTasks: { task: ImportTask; realId: string }[] = [];
|
|
103
|
-
|
|
104
|
-
for (const taskInput of tasks) {
|
|
105
|
-
try {
|
|
106
|
-
const projectPath = taskInput.projectPath;
|
|
107
|
-
|
|
108
|
-
// Create task without blockedBy first
|
|
109
|
-
const task = await addTask(
|
|
110
|
-
{
|
|
111
|
-
name: taskInput.name,
|
|
112
|
-
description: taskInput.description,
|
|
113
|
-
steps: taskInput.steps ?? [],
|
|
114
|
-
ticketId: taskInput.ticketId,
|
|
115
|
-
blockedBy: [], // Set later
|
|
116
|
-
projectPath,
|
|
117
|
-
},
|
|
118
|
-
sprintId
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
// Map local ID to real ID
|
|
122
|
-
if (taskInput.id) {
|
|
123
|
-
localToRealId.set(taskInput.id, task.id);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
createdTasks.push({ task: taskInput, realId: task.id });
|
|
127
|
-
log.itemSuccess(`${task.id}: ${task.name}`);
|
|
128
|
-
} catch (err) {
|
|
129
|
-
log.itemError(`Failed to add: ${taskInput.name}`);
|
|
130
|
-
if (err instanceof Error) {
|
|
131
|
-
console.log(muted(` ${err.message}`));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Second pass: update blockedBy with resolved real IDs (under file lock)
|
|
137
|
-
const tasksFilePath = getTasksFilePath(sprintId);
|
|
138
|
-
await withFileLock(tasksFilePath, async () => {
|
|
139
|
-
const allTasks = await getTasks(sprintId);
|
|
140
|
-
for (const { task: taskInput, realId } of createdTasks) {
|
|
141
|
-
const blockedBy = (taskInput.blockedBy ?? [])
|
|
142
|
-
.map((localId) => localToRealId.get(localId) ?? '')
|
|
143
|
-
.filter((id) => id !== '');
|
|
144
|
-
|
|
145
|
-
if (blockedBy.length > 0) {
|
|
146
|
-
const taskToUpdate = allTasks.find((t) => t.id === realId);
|
|
147
|
-
if (taskToUpdate) {
|
|
148
|
-
taskToUpdate.blockedBy = blockedBy;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
await saveTasks(allTasks, sprintId);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
return createdTasks.length;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Build the complete task list in memory and write atomically via saveTasks().
|
|
160
|
-
* Original tasks.json is untouched until the final write — interruption-safe.
|
|
161
|
-
*/
|
|
162
|
-
async function importTasksReplace(tasks: ImportTask[], sprintId: string): Promise<number> {
|
|
163
|
-
// Build mapping from local IDs to real IDs
|
|
164
|
-
const localToRealId = new Map<string, string>();
|
|
165
|
-
const newTasks: Task[] = [];
|
|
166
|
-
|
|
167
|
-
// First pass: generate real IDs and build mapping
|
|
168
|
-
for (const taskInput of tasks) {
|
|
169
|
-
const realId = generateUuid8();
|
|
170
|
-
if (taskInput.id) {
|
|
171
|
-
localToRealId.set(taskInput.id, realId);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
newTasks.push({
|
|
175
|
-
id: realId,
|
|
176
|
-
name: taskInput.name,
|
|
177
|
-
description: taskInput.description,
|
|
178
|
-
steps: taskInput.steps ?? [],
|
|
179
|
-
status: 'todo',
|
|
180
|
-
order: newTasks.length + 1,
|
|
181
|
-
ticketId: taskInput.ticketId,
|
|
182
|
-
blockedBy: [], // Set in second pass
|
|
183
|
-
projectPath: taskInput.projectPath,
|
|
184
|
-
verified: false,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
log.itemSuccess(`${realId}: ${taskInput.name}`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Second pass: resolve blockedBy references
|
|
191
|
-
for (let i = 0; i < tasks.length; i++) {
|
|
192
|
-
const taskInput = tasks[i];
|
|
193
|
-
const newTask = newTasks[i];
|
|
194
|
-
if (!taskInput || !newTask) continue;
|
|
195
|
-
|
|
196
|
-
const blockedBy = (taskInput.blockedBy ?? [])
|
|
197
|
-
.map((localId) => localToRealId.get(localId) ?? '')
|
|
198
|
-
.filter((id) => id !== '');
|
|
199
|
-
|
|
200
|
-
newTask.blockedBy = blockedBy;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Atomic write — replaces all existing tasks in one operation
|
|
204
|
-
await saveTasks(newTasks, sprintId);
|
|
205
|
-
|
|
206
|
-
return newTasks.length;
|
|
207
|
-
}
|